Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40698efb7d | ||
|
|
3d2a2b3d98 | ||
|
|
78a314b761 | ||
|
|
2d1dd3788b | ||
|
|
56d3160641 | ||
|
|
5db99041bc | ||
|
|
9ec56338c3 | ||
|
|
894ebf8eb7 | ||
|
|
6bec8a6c5c | ||
|
|
b9e7bc761b | ||
|
|
b54c896498 | ||
|
|
1cd7b7db8f | ||
|
|
f87aaca2fb | ||
|
|
901ba615ab | ||
|
|
cdbf9ac1eb | ||
|
|
f098bc1b5d | ||
|
|
e1f7c8c06c | ||
|
|
a9987546d5 | ||
|
|
6a39887e8f | ||
|
|
349e314850 | ||
|
|
f383fbbff4 | ||
|
|
b1112d8cdc | ||
|
|
27c383a5d9 | ||
|
|
7cf24feef6 | ||
|
|
4443766d39 | ||
|
|
ea2166b09a | ||
|
|
9e4e69be1c | ||
|
|
dd0655bfde | ||
|
|
d2d275706b | ||
|
|
d76d75b7f8 | ||
|
|
b7b63dd30f | ||
|
|
aacab4036c | ||
|
|
e104b1d936 | ||
|
|
537828acc2 | ||
|
|
5be2f68a55 | ||
|
|
f7e519bae1 | ||
|
|
ad8fbfcf65 | ||
|
|
96e1966d16 | ||
|
|
becaee71c0 | ||
|
|
bf6a6b417d | ||
|
|
dfd35b8cc8 | ||
|
|
1632681e6c | ||
|
|
ab4944a907 | ||
|
|
88d10d53f2 | ||
|
|
eae9fa7030 | ||
|
|
ad76f68224 | ||
|
|
d783793b4e | ||
|
|
017f79398d | ||
|
|
fc120e8285 | ||
|
|
117b9b2016 | ||
|
|
1376ab2474 | ||
|
|
fdce53421f | ||
|
|
770ed086d5 | ||
|
|
c48db09f13 | ||
|
|
0548ebd253 |
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`.
|
||||
|
||||
@@ -64,3 +64,4 @@ jobs:
|
||||
bash tests/managed_payload_build_contract_test.sh .
|
||||
bash tests/flatpak_packaging_test.sh .
|
||||
bash tests/native_packaging_test.sh .
|
||||
bash tests/anylinux_dependencies_test.sh .
|
||||
@@ -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:
|
||||
@@ -45,10 +53,13 @@ jobs:
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
@@ -135,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:
|
||||
@@ -294,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*/
|
||||
@@ -57,19 +58,7 @@ Makefile.cmake
|
||||
/cmake-build-release/
|
||||
|
||||
# Roblox payload
|
||||
/rbx_bin/libroblox.so
|
||||
/rbx_bin/libroblox.so.*
|
||||
/rbx_bin/client_settings_android.json
|
||||
/rbx_bin/assets/
|
||||
/rbx_bin/android/
|
||||
/rbx_bin/sober_apk/
|
||||
/rbx_bin/versions/
|
||||
/rbx_bin/.payload-update.*/
|
||||
/rbx_bin/*.apk
|
||||
/rbx_bin/*.so
|
||||
/rbx_bin/*.so.*
|
||||
|
||||
!/rbx_bin/.gitkeep
|
||||
/rbx_bin/
|
||||
|
||||
# Runtime state
|
||||
/runtime/
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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.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.2.0...HEAD
|
||||
[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");
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 1.0.3
|
||||
VERSION 1.0.4
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
@@ -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}"
|
||||
@@ -268,6 +269,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
src/runtime/frame_rate_policy.cc
|
||||
src/runtime/fleasion.cc
|
||||
src/runtime/game_mode.cc
|
||||
src/runtime/game_session_coordinator.cc
|
||||
src/runtime/graphics_launch_policy.cc
|
||||
@@ -286,6 +288,8 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/roblox_web_view_bridge.cc
|
||||
src/runtime/roblox_permissions_bridge.cc
|
||||
src/runtime/payload_update_preflight.cc
|
||||
src/runtime/process_launch_policy.cc
|
||||
src/runtime/process_diagnostics.cc
|
||||
src/runtime/runtime_config.cc
|
||||
src/runtime/runtime_config_bootstrap.cc
|
||||
src/runtime/runtime_config_file.cc
|
||||
@@ -297,6 +301,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)
|
||||
@@ -327,6 +332,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
endif()
|
||||
target_link_libraries(mocktail_runtime PUBLIC Threads::Threads)
|
||||
target_link_libraries(mocktail_runtime PRIVATE
|
||||
OpenSSL::Crypto
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
PkgConfig::GIO
|
||||
@@ -465,7 +471,7 @@ target_link_libraries(mocktail_window PUBLIC
|
||||
Mocktail::SdlVulkanWsi
|
||||
SDL3::SDL3
|
||||
${CMAKE_DL_LIBS}
|
||||
PRIVATE Mocktail::Runtime
|
||||
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay
|
||||
)
|
||||
|
||||
add_library(mocktail_roblox_output_device_runtime STATIC
|
||||
@@ -720,13 +726,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
|
||||
@@ -735,7 +752,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,20 +1,65 @@
|
||||
# Frequently asked questions
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
# FAQ
|
||||
|
||||
[How do I change my FPS?](/FAQ.md#how-do-i-change-my-fps)
|
||||
## How do I turn Discord RPC off?
|
||||
|
||||
### How do I change my FPS?
|
||||
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/io.github.CoderDayton.nightcap/config/mocktail/config.yaml`
|
||||
|
||||
Go to $HOME/.config/mocktail, there is a file called config.yaml (appears after opening for the first time mocktail)
|
||||
For native installs, a custom `$XDG_CONFIG_HOME` replaces `~/.config`.
|
||||
|
||||
Set `enabled` to `false` under `integrations.discord_rpc`. If the block is
|
||||
missing, add it under the existing `integrations` section:
|
||||
|
||||
<img width="460" height="241" alt="image" src="https://github.com/user-attachments/assets/b30b1fea-4937-4858-937a-3096a9e8b2ad" />
|
||||
```yaml
|
||||
integrations:
|
||||
discord_rpc:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Save the file and restart Nightcap. Set `enabled` back to `true` to turn
|
||||
RPC on again.
|
||||
|
||||
with your text editor of preference search for "graphics"
|
||||
## 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.
|
||||
|
||||
<img width="1022" height="619" alt="image" src="https://github.com/user-attachments/assets/f86791e9-7ac3-4d13-b81b-3cf9e3e64c3d" />
|
||||
## Where can I find logs for a bug report?
|
||||
|
||||
Nightcap saves logs automatically. Reproduce the issue, close Nightcap, and
|
||||
attach `latest.log`:
|
||||
|
||||
change the display value to your desired fps, also turn off vsync if you want more fps than your display supports
|
||||
- Native / AppImage: `~/.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 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 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
|
||||
```
|
||||
|
||||
Start WiVRn or SteamVR/ALVR, connect your headset, then run:
|
||||
|
||||
```bash
|
||||
./build/mocktail -vr
|
||||
```
|
||||
@@ -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,69 @@
|
||||
# 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`.
|
||||
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, 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 +71,28 @@ 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
|
||||
`FFlagLuaAppChartsAppPage` override takes precedence over this default.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -113,15 +100,18 @@ Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
|
||||
"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,
|
||||
@@ -168,17 +158,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
|
||||
|
||||
@@ -186,7 +193,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.
|
||||
|
||||
@@ -9,9 +10,10 @@ get_filename_component(MOCKTAIL_PLATFORM_GRAPHICS_ROOT
|
||||
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
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}"
|
||||
@@ -103,6 +105,18 @@ target_link_libraries(mocktail_graphics_foundation PRIVATE
|
||||
target_compile_features(mocktail_graphics_foundation PUBLIC cxx_std_17)
|
||||
add_library(Mocktail::GraphicsFoundation ALIAS mocktail_graphics_foundation)
|
||||
|
||||
add_library(mocktail_gles_text_overlay STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/gles_text_overlay_compositor.cc
|
||||
)
|
||||
target_include_directories(mocktail_gles_text_overlay
|
||||
PUBLIC ${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/include
|
||||
PRIVATE ${MOCKTAIL_GLES3_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(mocktail_gles_text_overlay PUBLIC SDL3::SDL3)
|
||||
target_compile_features(mocktail_gles_text_overlay PUBLIC cxx_std_17)
|
||||
mocktail_apply_compile_options(mocktail_gles_text_overlay)
|
||||
add_library(Mocktail::GlesTextOverlay ALIAS mocktail_gles_text_overlay)
|
||||
|
||||
add_library(mocktail_sdl_vulkan_wsi STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/sdl_vulkan_wsi.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/android_vulkan_wsi_adapter.cc
|
||||
@@ -120,6 +134,16 @@ target_compile_features(mocktail_sdl_vulkan_wsi PUBLIC cxx_std_17)
|
||||
add_library(Mocktail::SdlVulkanWsi ALIAS mocktail_sdl_vulkan_wsi)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(gles_text_overlay_compositor_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/gles_text_overlay_compositor_test.cc
|
||||
)
|
||||
target_include_directories(gles_text_overlay_compositor_test PRIVATE
|
||||
${MOCKTAIL_GLES3_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(gles_text_overlay_compositor_test PRIVATE
|
||||
Mocktail::GlesTextOverlay
|
||||
GTest::gtest_main
|
||||
)
|
||||
add_executable(bionic_egl_bridge_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/bionic_egl_bridge_test.cc
|
||||
)
|
||||
@@ -153,6 +177,7 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
GTest::gtest_main
|
||||
)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(gles_text_overlay_compositor_test)
|
||||
gtest_discover_tests(bionic_egl_bridge_test)
|
||||
gtest_discover_tests(platform_graphics_foundation_test)
|
||||
gtest_discover_tests(display_refresh_capabilities_test)
|
||||
|
||||
@@ -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,11 +77,29 @@ 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:
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
enabled: false
|
||||
# String: match Fleasion's routing mode: env or hosts.
|
||||
proxy_mode: env
|
||||
# Integer: Fleasion's local HTTP proxy port in env mode.
|
||||
proxy_port: 58443
|
||||
# Optional absolute public CA path. Default: $XDG_CONFIG_HOME/Fleasion/proxy_ca/ca.crt
|
||||
# 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: true): publish Nightcap activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
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.
|
||||
|
||||
@@ -79,7 +79,11 @@
|
||||
"count_method_rva": "0x32d0ee0",
|
||||
"info_method_rva": "0x32d0f80",
|
||||
"current_method_rva": "0x32d0f30",
|
||||
"select_method_rva": "0x32d0cb4"
|
||||
"select_method_rva": "0x32d0cb4",
|
||||
"input_count_method_rva": "0x32d12dc",
|
||||
"input_info_method_rva": "0x32d12ea",
|
||||
"input_current_method_rva": "0x32d13a6",
|
||||
"input_select_method_rva": "0x32d0a56"
|
||||
},
|
||||
"reason": "Verified 2026-09-06: exact Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries and real game sessions, including fullscreen state synchronization and host FMOD output-device interposition; legacy executable patches and signal recovery remain disabled."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# 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: the `discord_rpc` block (see item 3).
|
||||
- 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. Discord Rich Presence
|
||||
|
||||
Upstream ships Discord RPC. Nightcap turns it on by default; see the
|
||||
[FAQ](../FAQ.md) to switch it off. It shows the experience name, elapsed
|
||||
time, the experience icon, and a join button when the server is public.
|
||||
|
||||
- Custom presence: set the title, details line, state line, and large and
|
||||
small icons from `config.yaml`, with placeholders like the experience name
|
||||
and elapsed time.
|
||||
- A Discord tab in the settings window to toggle each field.
|
||||
|
||||
Done when a friend can see what you are playing and join in one click, and
|
||||
you can switch it all off in one click.
|
||||
|
||||
## 4. Performance profiling
|
||||
|
||||
Every performance change should come with numbers. Today they come with a
|
||||
feeling.
|
||||
|
||||
- A frame time overlay, toggled by a key and a config flag, showing CPU time,
|
||||
GPU time, and present latency.
|
||||
- Shader compile and pipeline cache tracing, so first-load stutter can be
|
||||
measured and cut.
|
||||
- A `--profile` flag that writes a Chrome-format trace file, readable in
|
||||
`ui.perfetto.dev` and `chrome://tracing`.
|
||||
- A short benchmark guide in `docs/` with a fixed set of experiences, so
|
||||
before and after numbers can be compared across machines.
|
||||
|
||||
Done when a pull request that claims a speedup can show the trace that
|
||||
proves it.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
@@ -2,6 +2,8 @@
|
||||
#define MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
@@ -29,4 +31,10 @@ extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument);
|
||||
|
||||
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
|
||||
extern "C" int mocktail_bionic_pthread_setschedparam(
|
||||
pthread_t thread, int policy, const struct sched_param* parameters);
|
||||
extern "C" int mocktail_bionic_sched_setscheduler(
|
||||
pid_t tid, int policy, const struct sched_param* parameters);
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
@@ -1,6 +1,9 @@
|
||||
#ifndef MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
#define MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -22,6 +25,27 @@ struct FmodOutputDeviceBridgeProfile {
|
||||
std::uintptr_t info_method_rva = 0;
|
||||
std::uintptr_t current_method_rva = 0;
|
||||
std::uintptr_t select_method_rva = 0;
|
||||
// Roblox 2.738 inserted device-list methods before the current/select slots.
|
||||
int vtable_layout_version = 1;
|
||||
|
||||
// Optional exact-build input count/info/current/select methods.
|
||||
std::array<std::uintptr_t, 4> input_method_rvas{};
|
||||
bool has_input_devices() const { return input_method_rvas[0] != 0; }
|
||||
std::array<std::size_t, 4> input_vtable_indexes() const {
|
||||
return vtable_layout_version == 2
|
||||
? std::array<std::size_t, 4>{9, 10, 12, 18}
|
||||
: std::array<std::size_t, 4>{8, 9, 10, 16};
|
||||
}
|
||||
|
||||
bool valid_vtable_layout() const {
|
||||
return vtable_layout_version == 1 || vtable_layout_version == 2;
|
||||
}
|
||||
std::size_t current_vtable_index() const {
|
||||
return vtable_layout_version == 2 ? 8 : 7;
|
||||
}
|
||||
std::size_t select_vtable_index() const {
|
||||
return vtable_layout_version == 2 ? 19 : 17;
|
||||
}
|
||||
};
|
||||
|
||||
struct BuildProfile {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef MOCKTAIL_COMPAT_FMOD_OUTPUT_DEVICE_CONTRACT_H_
|
||||
#define MOCKTAIL_COMPAT_FMOD_OUTPUT_DEVICE_CONTRACT_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// The 2.738 integer-index selector adds a feature-flag dispatch before the
|
||||
// count/clamp/setDriver path. Keep the calling convention, count slot (5),
|
||||
// object offset (0x28), and branch into that path exact. Only relocated
|
||||
// addresses and non-local branch/call targets vary between builds.
|
||||
inline constexpr char kFmodDeviceListsSelectContract[] =
|
||||
"\x55\x48\x89\xe5\x41\x57\x41\x56\x41\x54\x53\x48\x83\xec\x50\x41"
|
||||
"\x89\xf7\x49\x89\xfe\x4c\x8b\x25\x00\x00\x00\x00\x49\x8b\x04\x24"
|
||||
"\x48\x89\x45\xd8\x80\x3d\x00\x00\x00\x00\x00\x74\x25\x49\x8b\x04"
|
||||
"\x24\x48\x3b\x45\xd8\x0f\x85\x00\x00\x00\x00\x4c\x89\xf7\x44\x89"
|
||||
"\xfe\x48\x83\xc4\x50\x5b\x41\x5c\x41\x5e\x41\x5f\x5d\xe9\x00\x00"
|
||||
"\x00\x00\x49\x8b\x06\x4c\x89\xf7\xff\x50\x28\x44\x39\xf8\x0f\x8e"
|
||||
"\x00\x00\x00\x00\x31\xdb\x45\x85\xff\x41\x0f\x4f\xdf\x49\x8b\x7e"
|
||||
"\x28\x89\xde\xe8\x00\x00\x00\x00";
|
||||
inline constexpr std::size_t kFmodDeviceListsSelectContractSize =
|
||||
sizeof(kFmodDeviceListsSelectContract) - 1;
|
||||
|
||||
inline bool HasFmodDeviceListsSelectContract(std::string_view code) {
|
||||
if (code.size() < kFmodDeviceListsSelectContractSize) return false;
|
||||
for (std::size_t index = 0; index < kFmodDeviceListsSelectContractSize;
|
||||
++index) {
|
||||
bool relocated = false;
|
||||
for (const std::size_t start : {24, 38, 55, 78, 96, 116}) {
|
||||
if (index >= start && index < start + 4) relocated = true;
|
||||
}
|
||||
if (!relocated && code[index] != kFmodDeviceListsSelectContract[index])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_FMOD_OUTPUT_DEVICE_CONTRACT_H_
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
|
||||
#define MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#include <xmmintrin.h>
|
||||
#endif
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// FMOD's Android threads identify themselves through JavaVMAttachArgs before
|
||||
// entering their mix loop. Voice DSP filters can accumulate subnormal values
|
||||
// and spend an entire x86 core on floating-point assists (issue #128).
|
||||
// Limit FTZ/DAZ to these audio threads; retain the caller's other FP controls.
|
||||
class FmodThreadFloatingPointMode final {
|
||||
public:
|
||||
FmodThreadFloatingPointMode() = default;
|
||||
~FmodThreadFloatingPointMode() { Restore(); }
|
||||
FmodThreadFloatingPointMode(const FmodThreadFloatingPointMode&) = delete;
|
||||
FmodThreadFloatingPointMode& operator=(const FmodThreadFloatingPointMode&) =
|
||||
delete;
|
||||
|
||||
bool Enable(const char* thread_name) noexcept {
|
||||
#if defined(__x86_64__)
|
||||
if (active_ || thread_name == nullptr) return false;
|
||||
const std::string_view name(thread_name);
|
||||
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
|
||||
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
|
||||
return false;
|
||||
}
|
||||
const unsigned int control = _mm_getcsr();
|
||||
original_mode_ = control & kModeMask;
|
||||
_mm_setcsr(control | kModeMask);
|
||||
active_ = true;
|
||||
return true;
|
||||
#else
|
||||
(void)thread_name;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Restore() noexcept {
|
||||
#if defined(__x86_64__)
|
||||
if (active_) {
|
||||
// Restore only our two bits, preserving rounding, exception masks and
|
||||
// status flags set by the guest while the thread was attached.
|
||||
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
|
||||
active_ = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
#if defined(__x86_64__)
|
||||
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
|
||||
unsigned int original_mode_ = 0;
|
||||
bool active_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -13,13 +14,15 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Replaces only the output-device query/select slots of the exact
|
||||
class NativeInputCapture;
|
||||
|
||||
// Replaces the profiled input/output-device query/select slots of the exact
|
||||
// Build-ID-scoped FmodAudioDevice vtable. Roblox keeps owning its FMOD engine,
|
||||
// while the existing settings UI sees and selects the SDL host routes that
|
||||
// consume Android AudioTrack PCM.
|
||||
class RobloxOutputDeviceBridge final {
|
||||
public:
|
||||
RobloxOutputDeviceBridge() = default;
|
||||
RobloxOutputDeviceBridge();
|
||||
~RobloxOutputDeviceBridge();
|
||||
|
||||
RobloxOutputDeviceBridge(const RobloxOutputDeviceBridge&) = delete;
|
||||
@@ -48,16 +51,25 @@ class RobloxOutputDeviceBridge final {
|
||||
Status Activate(std::uintptr_t image_base);
|
||||
Status PatchVtableLocked();
|
||||
bool RestoreVtableLocked();
|
||||
int DeviceCount();
|
||||
void* DeviceInfo(void* result, int index);
|
||||
int CurrentDevice();
|
||||
void SelectDevice(int index);
|
||||
static int GetInputDeviceCount(void *self);
|
||||
static void *GetInputDeviceInfo(void *result, void *self, int index);
|
||||
static int GetCurrentInputDevice(void *self);
|
||||
static void SetCurrentInputDevice(void *self, int index);
|
||||
int DeviceCount(bool input = false);
|
||||
void *DeviceInfo(void *result, int index, bool input = false);
|
||||
int CurrentDevice(bool input = false);
|
||||
void SelectDevice(int index, bool input = false);
|
||||
void ConstructGuestString(void* destination, std::string_view value) const;
|
||||
|
||||
std::unique_ptr<NativeInputCapture> capture_;
|
||||
bool capture_profile_supported_ = false;
|
||||
std::array<std::uintptr_t, 6> original_capture_methods_{};
|
||||
mutable std::mutex mutex_;
|
||||
compat::FmodOutputDeviceBridgeProfile profile_;
|
||||
std::vector<MenuDevice> devices_;
|
||||
std::array<std::uintptr_t, 4> original_methods_{};
|
||||
std::vector<MenuDevice> input_devices_;
|
||||
int selected_input_index_ = 0;
|
||||
std::array<std::uintptr_t, 8> original_methods_{};
|
||||
std::uintptr_t library_base_ = 0;
|
||||
std::uintptr_t* vtable_ = nullptr;
|
||||
void* string_constructor_ = nullptr;
|
||||
|
||||
@@ -36,6 +36,10 @@ Status ConfigureSdlRecordingDevice(
|
||||
Status GetConfiguredSdlRecordingDevice(std::uint32_t* recording_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Migrates live captures without changing their format or callback buffers.
|
||||
Status SwitchSdlRecordingDevice(std::uint32_t recording_device_id,
|
||||
std::string *resolved_name);
|
||||
|
||||
using AudioCaptureDataCallback = void (*)(void* context,
|
||||
std::size_t size_bytes);
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#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);
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_GLES_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
#define MOCKTAIL_GRAPHICS_GLES_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/graphics/text_overlay_frame.h"
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
struct GlesTextOverlaySource {
|
||||
bool (*may_present)() = nullptr;
|
||||
bool (*query)(MocktailTextOverlayFrameInfo *) = nullptr;
|
||||
bool (*copy)(std::uint64_t, void *, std::size_t) = nullptr;
|
||||
};
|
||||
|
||||
// Composites the host text editor before SDL swaps the game's GLES backbuffer.
|
||||
// Draw and destruction use the owning GL context when it is current. If that
|
||||
// context is no longer current at destruction, its destruction frees the GL
|
||||
// resources. Create a new compositor when creating a new context.
|
||||
class GlesTextOverlayCompositor {
|
||||
public:
|
||||
explicit GlesTextOverlayCompositor(GlesTextOverlaySource source);
|
||||
~GlesTextOverlayCompositor();
|
||||
|
||||
bool Draw(SDL_Window *window);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_GLES_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Process-local ABI with libvulkan.so. Pixels are RGBA8; consumers must copy a
|
||||
// Process-local ABI for host compositors. Pixels are RGBA8; consumers must copy a
|
||||
// matching revision before the producer clears its storage.
|
||||
struct MocktailTextOverlayFrameInfo {
|
||||
static constexpr std::uint32_t kAbiVersion = 2;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
@@ -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,28 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_FLEASION_H_
|
||||
#define MOCKTAIL_RUNTIME_FLEASION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "runtime/runtime_config.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
struct FleasionPreparation {
|
||||
std::filesystem::path certificate;
|
||||
std::filesystem::path base_bundle;
|
||||
std::filesystem::path bundle;
|
||||
std::string error;
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Generates a private, replaceable trust bundle outside the managed payload.
|
||||
// Reads certificates only; never reads Fleasion's private key or changes it.
|
||||
FleasionPreparation PrepareFleasion(const RuntimeConfig& config,
|
||||
const Environment& environment,
|
||||
const RuntimePaths& paths);
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif
|
||||
@@ -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,22 +51,26 @@ 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
|
||||
// query opens a recording device.
|
||||
bool MergeAudioDeviceMenuClientSettingsOverrides(std::string_view base_json,
|
||||
std::string *merged_json,
|
||||
std::string *error);
|
||||
|
||||
bool MergeAudioCaptureClientSettingsOverrides(bool microphone_enabled,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_PROCESS_DIAGNOSTICS_H_
|
||||
#define MOCKTAIL_RUNTIME_PROCESS_DIAGNOSTICS_H_
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
enum class ProcessDiagnosticStage { kStartup, kNativeRuntime, kShutdown };
|
||||
|
||||
// Installs a diagnostic handler only if SIGXCPU still has its default action.
|
||||
// The handler logs to stderr and re-delivers SIGXCPU with the default action.
|
||||
void InstallCpuLimitDiagnostics();
|
||||
void LogProcessDiagnostics(ProcessDiagnosticStage stage);
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PROCESS_DIAGNOSTICS_H_
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_PROCESS_LAUNCH_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_PROCESS_LAUNCH_POLICY_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
// Call only for interactive launches, before creating any threads. Removes an
|
||||
// inherited soft CPU limit when the hard limit is unlimited and demotes the
|
||||
// calling thread from FIFO/RR scheduling. Returns diagnostics for the session
|
||||
// log, including failures; never changes hard limits or signal dispositions.
|
||||
std::string ApplyInteractiveProcessLaunchPolicy();
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PROCESS_LAUNCH_POLICY_H_
|
||||
@@ -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);
|
||||
|
||||
@@ -138,6 +138,8 @@ struct RobloxInputSnapshot {
|
||||
uint64_t keyboard_events = 0;
|
||||
uint64_t ignored_text_events = 0;
|
||||
uint64_t text_events = 0;
|
||||
// Zero after the host editor finishes, even if Roblox sends no hide callback.
|
||||
uint64_t text_focus_generation = 0;
|
||||
uint64_t native_errors = 0;
|
||||
uint32_t active_mouse_buttons = 0;
|
||||
uint32_t active_touches = 0;
|
||||
@@ -169,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 {
|
||||
@@ -235,6 +240,7 @@ class RobloxInputRouter final {
|
||||
std::vector<ActiveGamepad> gamepads_;
|
||||
float mouse_x_ = 0.0f;
|
||||
float mouse_y_ = 0.0f;
|
||||
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 {
|
||||
@@ -55,7 +60,7 @@ struct DiscordRpcTextConfig {
|
||||
};
|
||||
|
||||
struct DiscordRpcConfig {
|
||||
bool enabled = false;
|
||||
bool enabled = true;
|
||||
bool show_place_name = true;
|
||||
bool show_elapsed_time = true;
|
||||
bool join_enabled = true;
|
||||
@@ -119,6 +124,13 @@ class RuntimeConfig {
|
||||
}
|
||||
bool ca_bundle_valid() const { return ca_bundle_valid_; }
|
||||
bool use_system_proxy() const { return use_system_proxy_; }
|
||||
bool fleasion_enabled() const { return fleasion_enabled_; }
|
||||
bool fleasion_valid() const { return fleasion_valid_; }
|
||||
const std::string& fleasion_proxy_mode() const { return fleasion_proxy_mode_; }
|
||||
int fleasion_proxy_port() const { return fleasion_proxy_port_; }
|
||||
const std::optional<std::filesystem::path>& fleasion_ca_certificate() const {
|
||||
return fleasion_ca_certificate_;
|
||||
}
|
||||
const DiscordRpcConfig& discord_rpc() const { return discord_rpc_; }
|
||||
bool discord_rpc_valid() const { return discord_rpc_valid_; }
|
||||
|
||||
@@ -152,6 +164,11 @@ class RuntimeConfig {
|
||||
std::string audio_input_device_ = "default";
|
||||
bool audio_input_device_valid_ = true;
|
||||
bool use_system_proxy_ = false;
|
||||
bool fleasion_enabled_ = false;
|
||||
bool fleasion_valid_ = true;
|
||||
std::string fleasion_proxy_mode_ = "env";
|
||||
int fleasion_proxy_port_ = 58443;
|
||||
std::optional<std::filesystem::path> fleasion_ca_certificate_;
|
||||
std::optional<NetworkProxyConfig> network_proxy_;
|
||||
std::optional<std::filesystem::path> ca_bundle_;
|
||||
bool ca_bundle_valid_ = true;
|
||||
|
||||
@@ -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,11 +0,0 @@
|
||||
# pkgforge-dev/archlinux-pkgs-debloated, continuous release, 2026-09-11.
|
||||
# These assets are rolling. Refresh names/hashes together; do not bypass a
|
||||
# checksum mismatch. Only the disposable AppImage build container installs them.
|
||||
19e57cf78e309052b1ce55293374599f19fbc56cd2ac572191f475e718b42503 ffmpeg-mini-x86_64.pkg.tar.zst
|
||||
5a796f6846a898d5f706d92ae34a70d6f6cf28595369ccafbfda21ad6ccc6b7f gdk-pixbuf2-mini-x86_64.pkg.tar.zst
|
||||
9a42b41a03fc493a9bc4d00c9882e34ee28b5239a79175cc47e1dfe869f5d4a6 gtk4-mini-x86_64.pkg.tar.zst
|
||||
8bec7844a49eb7e3efc020ba0858e69cfb4fed64b83c116df04b4179cc2fa1a6 icu-mini-x86_64.pkg.tar.zst
|
||||
91fd7550208a306ff4e43fe10f405db01f2bc1ce8bdad5469229c05185201983 librsvg-mini-x86_64.pkg.tar.zst
|
||||
d5220136f8d9b4eaaf173a44d3f0092e97b61cfe9f5d627c570c6e81add9e789 libxml2-mini-x86_64.pkg.tar.zst
|
||||
90ec938ec71e62478745185f76d64253cc0a7ac2e0a18aaff0fb59cd70eb1f92 mesa-mini-x86_64.pkg.tar.zst
|
||||
857f7d895a471786b7920b430fbf0b91078bfb8efc6bf332c27928f6104929ae opus-mini-x86_64.pkg.tar.zst
|
||||
@@ -0,0 +1,12 @@
|
||||
# Allowed reduced packages from pkgforge-dev/archlinux-pkgs-debloated.
|
||||
# continuous is rolling: the installer snapshots GitHub asset IDs and SHA256
|
||||
# digests together at build time, and verifies every archive before installation.
|
||||
# Only the disposable AppImage build container installs these packages.
|
||||
ffmpeg-mini-x86_64.pkg.tar.zst
|
||||
gdk-pixbuf2-mini-x86_64.pkg.tar.zst
|
||||
gtk4-mini-x86_64.pkg.tar.zst
|
||||
icu-mini-x86_64.pkg.tar.zst
|
||||
librsvg-mini-x86_64.pkg.tar.zst
|
||||
libxml2-mini-x86_64.pkg.tar.zst
|
||||
mesa-mini-x86_64.pkg.tar.zst
|
||||
opus-mini-x86_64.pkg.tar.zst
|
||||
@@ -1,7 +1,7 @@
|
||||
pkgbase = mocktail-bin
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux (prebuilt)
|
||||
pkgver = 1.0.3
|
||||
pkgrel = 2
|
||||
pkgver = 1.0.4
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
@@ -31,10 +31,10 @@ pkgbase = mocktail-bin
|
||||
provides = mocktail
|
||||
conflicts = mocktail
|
||||
conflicts = mocktail-git
|
||||
noextract = mocktail-1.0.3-2-x86_64.pkg.tar.zst
|
||||
noextract = mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
options = !debug
|
||||
options = !strip
|
||||
source = mocktail-1.0.3-2-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/1.0.3/mocktail-1.0.3-2-x86_64.pkg.tar.zst
|
||||
sha256sums = be945181d73d28d9bf09a93927a215d03a3a8b479ffddab3378a8113b56e4b99
|
||||
source = mocktail-1.0.4-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/1.0.4/mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
sha256sums = d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7
|
||||
|
||||
pkgname = mocktail-bin
|
||||
@@ -1,8 +1,8 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-bin
|
||||
pkgver=1.0.3
|
||||
pkgrel=2
|
||||
pkgver=1.0.4
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux (prebuilt)'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
@@ -35,10 +35,11 @@ optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'mocktail-git')
|
||||
options=('!debug' '!strip')
|
||||
_archive="mocktail-${pkgver}-${pkgrel}-${CARCH}.pkg.tar.zst"
|
||||
# Release asset names do not include the AUR package release.
|
||||
_archive="mocktail-${pkgver}-${CARCH}.pkg.tar.zst"
|
||||
source=("${_archive}::https://github.com/komaruworld/mocktail/releases/download/${pkgver}/${_archive}")
|
||||
noextract=("${_archive}")
|
||||
sha256sums=('be945181d73d28d9bf09a93927a215d03a3a8b479ffddab3378a8113b56e4b99')
|
||||
sha256sums=('d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7')
|
||||
|
||||
package() {
|
||||
bsdtar -xf "${srcdir}/${_archive}" -C "${pkgdir}" usr
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pkgbase = mocktail
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.3
|
||||
pkgrel = 2
|
||||
pkgver = 1.0.4
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
@@ -39,7 +39,7 @@ pkgbase = mocktail
|
||||
conflicts = mocktail-git
|
||||
options = !debug
|
||||
options = !lto
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.3
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.4
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail
|
||||
@@ -1,8 +1,8 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.3
|
||||
pkgrel=2
|
||||
pkgver=1.0.4
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
@@ -42,7 +42,6 @@ makedepends=(
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
conflicts=('mocktail-bin' 'mocktail-git')
|
||||
# The 1.0.3 tag predates the source-level FORTIFY/LTO isolation fix.
|
||||
options=('!debug' '!lto')
|
||||
source=(
|
||||
"mocktail::git+https://github.com/komaruworld/mocktail.git#tag=${pkgver}"
|
||||
|
||||
@@ -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>
|
||||
@@ -55,6 +56,19 @@
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="1.0.4" date="2026-09-11">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed login issues</li>
|
||||
<li>Fixed low-resolution textures, skybox rendering and HiDPI scaling.</li>
|
||||
<li>Added vc support.</li>
|
||||
<li>Added Xbox and PlayStation gamepad support.</li>
|
||||
<li>Added Nix support</li>
|
||||
<li>Improved text input, fullscreen mode, window sizing, and mouse controls.</li>
|
||||
<li>Updated support for Roblox 2.736.1408.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.3" date="2026-08-16">
|
||||
<description>
|
||||
<ul>
|
||||
@@ -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() {
|
||||
|
||||
@@ -1271,6 +1271,33 @@ def has_fmod_string_constructor_contract(image: ElfImage, rva: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def has_fmod_device_lists_select_contract(image: ElfImage, rva: int) -> bool:
|
||||
# Keep in sync with compat/fmod_output_device_contract.h. The fixed bytes
|
||||
# bind the int-index ABI, count slot, clamp, and FMOD system-object offset.
|
||||
expected = bytes.fromhex(
|
||||
"55 48 89 e5 41 57 41 56 41 54 53 48 83 ec 50 41 "
|
||||
"89 f7 49 89 fe 4c 8b 25 00 00 00 00 49 8b 04 24 "
|
||||
"48 89 45 d8 80 3d 00 00 00 00 00 74 25 49 8b 04 "
|
||||
"24 48 3b 45 d8 0f 85 00 00 00 00 4c 89 f7 44 89 "
|
||||
"fe 48 83 c4 50 5b 41 5c 41 5e 41 5f 5d e9 00 00 "
|
||||
"00 00 49 8b 06 4c 89 f7 ff 50 28 44 39 f8 0f 8e "
|
||||
"00 00 00 00 31 db 45 85 ff 41 0f 4f df 49 8b 7e "
|
||||
"28 89 de e8 00 00 00 00"
|
||||
)
|
||||
try:
|
||||
image.require_code_rva(rva, len(expected))
|
||||
offset = image.rva_to_offset(rva, len(expected))
|
||||
code = image.bytes_at(offset, len(expected), "FMOD device-list selector")
|
||||
except AnalyzerError:
|
||||
return False
|
||||
relocated = {index for start in (24, 38, 55, 78, 96, 116)
|
||||
for index in range(start, start + 4)}
|
||||
return len(code) == len(expected) and all(
|
||||
index in relocated or actual == wanted
|
||||
for index, (actual, wanted) in enumerate(zip(code, expected))
|
||||
)
|
||||
|
||||
|
||||
def load_reference_runtime_profile(
|
||||
paths: Sequence[Path], reference_build_id: str
|
||||
) -> dict[str, Any]:
|
||||
@@ -1306,7 +1333,11 @@ def load_reference_runtime_profile(
|
||||
}
|
||||
if (
|
||||
not isinstance(bridge, dict)
|
||||
or set(bridge) != fields
|
||||
or not fields <= set(bridge)
|
||||
or set(bridge) - fields - {"vtable_layout_version"} - {
|
||||
"input_count_method_rva", "input_info_method_rva",
|
||||
"input_current_method_rva", "input_select_method_rva",
|
||||
}
|
||||
or profile.get("allow_host_abi_bridges") is not True
|
||||
):
|
||||
raise AnalyzerError(
|
||||
@@ -1315,12 +1346,32 @@ def load_reference_runtime_profile(
|
||||
discovered["fmod_output_device_bridge"] = {
|
||||
field: parse_rva(value, f"reference FMOD {field}")
|
||||
for field, value in bridge.items()
|
||||
if field != "vtable_layout_version"
|
||||
}
|
||||
layout = bridge.get("vtable_layout_version", 1)
|
||||
if type(layout) is not int or layout not in (1, 2):
|
||||
raise AnalyzerError("reference FMOD vtable layout is unsupported")
|
||||
if layout != 1:
|
||||
discovered["fmod_output_device_bridge"]["vtable_layout_version"] = layout
|
||||
for field, value in discovered.items():
|
||||
if field in result and result[field] != value:
|
||||
raise AnalyzerError(
|
||||
f"reference compatibility manifests disagree on {field}"
|
||||
)
|
||||
if field in result:
|
||||
previous = result[field]
|
||||
if field == "fmod_output_device_bridge":
|
||||
input_fields = {
|
||||
"input_count_method_rva", "input_info_method_rva",
|
||||
"input_current_method_rva", "input_select_method_rva",
|
||||
}
|
||||
compatible = ((set(previous) ^ set(value)) <= input_fields and
|
||||
all(previous[key] == value[key]
|
||||
for key in previous.keys() & value.keys()))
|
||||
if compatible:
|
||||
value = {**previous, **value}
|
||||
else:
|
||||
compatible = previous == value
|
||||
if not compatible:
|
||||
raise AnalyzerError(
|
||||
f"reference compatibility manifests disagree on {field}"
|
||||
)
|
||||
result[field] = value
|
||||
return result
|
||||
|
||||
@@ -1329,12 +1380,13 @@ def derive_fmod_output_device_bridge(
|
||||
reference: ElfImage,
|
||||
candidate: ElfImage,
|
||||
source: dict[str, int],
|
||||
) -> dict[str, str]:
|
||||
) -> dict[str, Any]:
|
||||
source_layout = source.get("vtable_layout_version", 1)
|
||||
indexes = {
|
||||
"count_method_rva": 5,
|
||||
"info_method_rva": 6,
|
||||
"current_method_rva": 7,
|
||||
"select_method_rva": 17,
|
||||
"current_method_rva": 8 if source_layout == 2 else 7,
|
||||
"select_method_rva": 19 if source_layout == 2 else 17,
|
||||
}
|
||||
source_vtable = source["vtable_rva"]
|
||||
reference.require_relro_rva(source_vtable, (max(indexes.values()) + 1) * 8)
|
||||
@@ -1368,16 +1420,20 @@ def derive_fmod_output_device_bridge(
|
||||
minimum_anchor_bytes=3,
|
||||
),
|
||||
).rva
|
||||
select_method = unique_semantic_signature_match(
|
||||
reference,
|
||||
candidate,
|
||||
SignatureSpec(
|
||||
"fmod-output-select",
|
||||
source["select_method_rva"],
|
||||
18,
|
||||
minimum_anchor_bytes=3,
|
||||
),
|
||||
).rva
|
||||
try:
|
||||
select_method = unique_semantic_signature_match(
|
||||
reference,
|
||||
candidate,
|
||||
SignatureSpec(
|
||||
"fmod-output-select",
|
||||
source["select_method_rva"],
|
||||
18,
|
||||
minimum_anchor_bytes=3,
|
||||
),
|
||||
).rva
|
||||
except AnalyzerError:
|
||||
# The device-list layout has a separately checked selector contract.
|
||||
select_method = None
|
||||
count_candidates = {
|
||||
match.rva
|
||||
for match in find_signature_matches(
|
||||
@@ -1416,32 +1472,35 @@ def derive_fmod_output_device_bridge(
|
||||
continue
|
||||
vtable = offset - indexes["info_method_rva"] * 8
|
||||
count_method = relocations.get(vtable + indexes["count_method_rva"] * 8)
|
||||
current_method = relocations.get(
|
||||
vtable + indexes["current_method_rva"] * 8
|
||||
)
|
||||
if (
|
||||
vtable % 8 == 0
|
||||
and count_method in count_candidates
|
||||
and current_method in current_candidates
|
||||
and count_method != current_method
|
||||
and relocations.get(vtable + indexes["select_method_rva"] * 8)
|
||||
== select_method
|
||||
):
|
||||
for layout, current_index, select_index in ((1, 7, 17), (2, 8, 19)):
|
||||
current_method = relocations.get(vtable + current_index * 8)
|
||||
selected = relocations.get(vtable + select_index * 8)
|
||||
if (
|
||||
vtable % 8 != 0
|
||||
or count_method not in count_candidates
|
||||
or current_method not in current_candidates
|
||||
or count_method == current_method
|
||||
or selected is None
|
||||
):
|
||||
continue
|
||||
try:
|
||||
candidate.require_relro_rva(
|
||||
vtable, (max(indexes.values()) + 1) * 8
|
||||
)
|
||||
candidate.require_relro_rva(vtable, (select_index + 1) * 8)
|
||||
except AnalyzerError:
|
||||
continue
|
||||
vtables.append((vtable, count_method, current_method))
|
||||
if layout == 1:
|
||||
if source_layout != 1 or select_method is None or selected != select_method:
|
||||
continue
|
||||
elif not has_fmod_device_lists_select_contract(candidate, selected):
|
||||
continue
|
||||
vtables.append((vtable, count_method, current_method, selected, layout))
|
||||
if len(vtables) != 1:
|
||||
raise AnalyzerError(
|
||||
f"FMOD output vtable matched {len(vtables)} candidate locations"
|
||||
)
|
||||
vtable, count_method, current_method = vtables[0]
|
||||
vtable, count_method, current_method, select_method, layout = vtables[0]
|
||||
if not has_fmod_string_constructor_contract(candidate, string_constructor):
|
||||
raise AnalyzerError("candidate FMOD string constructor contract changed")
|
||||
return {
|
||||
result = {
|
||||
"vtable_rva": format_rva(vtable),
|
||||
"string_constructor_rva": format_rva(string_constructor),
|
||||
"count_method_rva": format_rva(count_method),
|
||||
@@ -1449,6 +1508,33 @@ def derive_fmod_output_device_bridge(
|
||||
"current_method_rva": format_rva(current_method),
|
||||
"select_method_rva": format_rva(select_method),
|
||||
}
|
||||
if layout != 1:
|
||||
result["vtable_layout_version"] = layout
|
||||
input_fields = (
|
||||
"input_count_method_rva", "input_info_method_rva",
|
||||
"input_current_method_rva", "input_select_method_rva",
|
||||
)
|
||||
if any(field in source for field in input_fields):
|
||||
if not all(field in source for field in input_fields):
|
||||
raise AnalyzerError("reference FMOD input profile is incomplete")
|
||||
source_slots = (9, 10, 12, 18) if source_layout == 2 else (8, 9, 10, 16)
|
||||
target_slots = (9, 10, 12, 18) if layout == 2 else (8, 9, 10, 16)
|
||||
for field, old_slot, new_slot, length in zip(
|
||||
input_fields, source_slots, target_slots, (4, 16, 12, 16)
|
||||
):
|
||||
if reference_relocations.get(source_vtable + old_slot * 8) != source[field]:
|
||||
raise AnalyzerError("reference FMOD input slot changed")
|
||||
target = relocations.get(vtable + new_slot * 8)
|
||||
matches = find_signature_matches(
|
||||
reference, candidate,
|
||||
SignatureSpec(field, source[field], length, minimum_anchor_bytes=3),
|
||||
allow_multiple_candidates=True,
|
||||
)
|
||||
if target is None or target not in {match.rva for match in matches}:
|
||||
raise AnalyzerError(f"candidate FMOD input contract changed: {field}")
|
||||
candidate.require_code_rva(target)
|
||||
result[field] = format_rva(target)
|
||||
return result
|
||||
|
||||
|
||||
def derive_runtime_compatibility(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,36 +4,122 @@
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# The reduced packages are intentionally limited to the disposable builder.
|
||||
# Never replace a developer's desktop / graphics packages on the host.
|
||||
[[ -f /.dockerenv || -f /run/.containerenv ]] || {
|
||||
printf 'AnyLinux dependency installation requires a disposable container\n' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$(id -u)" == 0 ]] || {
|
||||
printf 'AnyLinux dependency installation requires container root\n' >&2
|
||||
AnyLinuxDependencyDie() {
|
||||
printf '[anylinux-dependencies] error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.sha256"
|
||||
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
|
||||
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
|
||||
readonly BASE_URL=https://github.com/pkgforge-dev/archlinux-pkgs-debloated/releases/download/continuous
|
||||
declare -a packages=()
|
||||
while read -r digest filename remainder; do
|
||||
[[ -n "${digest}" && "${digest}" != \#* ]] || continue
|
||||
[[ "${digest}" =~ ^[0-9a-f]{64}$ &&
|
||||
"${filename}" =~ ^[a-z0-9-]+-x86_64\.pkg\.tar\.zst$ &&
|
||||
-z "${remainder}" ]] || {
|
||||
printf 'Invalid AnyLinux dependency manifest entry\n' >&2
|
||||
exit 1
|
||||
}
|
||||
curl --fail --location --retry 3 --output "${DOWNLOADS}/${filename}" \
|
||||
"${BASE_URL}/${filename}"
|
||||
printf '%s %s\n' "${digest}" "${DOWNLOADS}/${filename}" |
|
||||
sha256sum --check --strict
|
||||
packages+=("${DOWNLOADS}/${filename}")
|
||||
done <"${MANIFEST}"
|
||||
(( ${#packages[@]} > 0 ))
|
||||
pacman -U --noconfirm --needed "${packages[@]}"
|
||||
AnyLinuxDependencyRequireContainer() {
|
||||
# Never replace a developer's desktop / graphics packages on the host.
|
||||
[[ -f /.dockerenv || -f /run/.containerenv ]] ||
|
||||
AnyLinuxDependencyDie 'installation requires a disposable container'
|
||||
[[ "$(id -u)" == 0 ]] ||
|
||||
AnyLinuxDependencyDie 'installation requires container root'
|
||||
}
|
||||
|
||||
AnyLinuxDependencySnapshot() {
|
||||
local manifest="$1" release="$2" filename remainder names_json
|
||||
local -a names=()
|
||||
local -A seen=()
|
||||
while read -r filename remainder || [[ -n "${filename}" ]]; do
|
||||
[[ -n "${filename}" && "${filename}" != \#* ]] || continue
|
||||
[[ "${filename}" =~ ^[a-z0-9-]+-mini-x86_64\.pkg\.tar\.zst$ &&
|
||||
-z "${remainder}" && -z "${seen[${filename}]:-}" ]] ||
|
||||
AnyLinuxDependencyDie 'invalid or duplicate dependency manifest entry'
|
||||
seen["${filename}"]=1
|
||||
names+=("${filename}")
|
||||
done <"${manifest}"
|
||||
(( ${#names[@]} > 0 )) || AnyLinuxDependencyDie 'empty dependency manifest'
|
||||
names_json="$(printf '%s\n' "${names[@]}" | jq --raw-input --slurp \
|
||||
'split("\n")[:-1]')"
|
||||
|
||||
# continuous assets are replaced on upstream rebuilds. Resolve all required
|
||||
# names and GitHub-computed digests in one response, then download by asset ID
|
||||
# rather than by rolling filename. These are per-build integrity checks, not
|
||||
# immutable version pins. Never accept absent digests or compute our own trust.
|
||||
jq --exit-status --raw-output --argjson names "${names_json}" \
|
||||
--arg base 'https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/assets/' '
|
||||
(if .tag_name != "continuous" or (.assets | type) != "array" then
|
||||
error("invalid continuous release metadata")
|
||||
else .assets end) as $assets |
|
||||
$names[] as $name |
|
||||
[$assets[] | select(.name == $name)] as $matches |
|
||||
if ($matches | length) != 1 then
|
||||
error("missing or duplicate dependency asset: " + $name)
|
||||
else $matches[0] end |
|
||||
if .state != "uploaded" or (.id | type) != "number" or
|
||||
.id <= 0 or .id != (.id | floor) or
|
||||
(.digest | type) != "string" then
|
||||
error("invalid dependency asset metadata: " + $name)
|
||||
elif (.digest | test("^sha256:[0-9a-f]{64}$") | not) or
|
||||
.url != ($base + (.id | tostring)) then
|
||||
error("invalid dependency digest or URL: " + $name)
|
||||
else
|
||||
[(.digest | ltrimstr("sha256:")), $name, .url] | @tsv
|
||||
end
|
||||
' "${release}" || AnyLinuxDependencyDie 'cannot resolve verified dependency assets'
|
||||
}
|
||||
|
||||
AnyLinuxDownloadDependencies() {
|
||||
local manifest="$1" downloads="$2" attempt digest filename url failed
|
||||
local -a curl_options=(
|
||||
--fail --silent --show-error --location --retry 3
|
||||
--proto '=https' --proto-redir '=https'
|
||||
--connect-timeout 15 --max-time 180
|
||||
--header 'X-GitHub-Api-Version: 2022-11-28'
|
||||
)
|
||||
if [[ -n "${GH_TOKEN:-}" ]]; then
|
||||
curl_options+=(--header "Authorization: Bearer ${GH_TOKEN}")
|
||||
fi
|
||||
for attempt in 1 2 3; do
|
||||
curl "${curl_options[@]}" --header 'Accept: application/vnd.github+json' \
|
||||
--output "${downloads}/release.json" \
|
||||
https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/tags/continuous ||
|
||||
AnyLinuxDependencyDie 'cannot fetch upstream release metadata'
|
||||
AnyLinuxDependencySnapshot "${manifest}" "${downloads}/release.json" \
|
||||
>"${downloads}/dependencies.tsv"
|
||||
: >"${downloads}/SHA256SUMS"
|
||||
failed=false
|
||||
while IFS=$'\t' read -r digest filename url; do
|
||||
printf '[anylinux-dependencies] downloading %s (SHA256 %s)\n' \
|
||||
"${filename}" "${digest}" >&2
|
||||
if ! curl "${curl_options[@]}" --header 'Accept: application/octet-stream' \
|
||||
--output "${downloads}/${filename}" "${url}"; then
|
||||
failed=true
|
||||
break
|
||||
fi
|
||||
printf '%s %s\n' "${digest}" "${filename}" >>"${downloads}/SHA256SUMS"
|
||||
done <"${downloads}/dependencies.tsv"
|
||||
if [[ "${failed}" == true ]]; then
|
||||
# An upstream upload can delete an ID between resolution and download.
|
||||
# Retry a complete snapshot, never mix old checksums with new filenames.
|
||||
printf '[anylinux-dependencies] download failed; refreshing snapshot (%s/3)\n' \
|
||||
"${attempt}" >&2
|
||||
continue
|
||||
fi
|
||||
(cd -- "${downloads}" && sha256sum --check --strict SHA256SUMS) ||
|
||||
AnyLinuxDependencyDie 'dependency checksum verification failed'
|
||||
return 0
|
||||
done
|
||||
AnyLinuxDependencyDie 'dependency downloads failed after 3 snapshots'
|
||||
}
|
||||
|
||||
AnyLinuxDependenciesMain() (
|
||||
AnyLinuxDependencyRequireContainer
|
||||
(( $# == 0 )) || AnyLinuxDependencyDie 'no arguments are supported'
|
||||
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.txt"
|
||||
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
|
||||
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
|
||||
AnyLinuxDownloadDependencies "${MANIFEST}" "${DOWNLOADS}"
|
||||
local digest filename url
|
||||
local -a packages=()
|
||||
while IFS=$'\t' read -r digest filename url; do
|
||||
packages+=("${DOWNLOADS}/${filename}")
|
||||
done <"${DOWNLOADS}/dependencies.tsv"
|
||||
pacman -U --noconfirm --needed "${packages[@]}"
|
||||
)
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
AnyLinuxDependenciesMain "$@"
|
||||
fi
|
||||
@@ -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}"
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
BUILD_DIR="${MOCKTAIL_BUILD_DIR:-${PROJECT_ROOT}/build}"
|
||||
BINARY="${BUILD_DIR}/mocktail"
|
||||
LIBROBLOX_PATH="${ROBLOX_LIB_PATH:-${PROJECT_ROOT}/rbx_bin/libroblox.so}"
|
||||
|
||||
if [[ "${LIBROBLOX_PATH}" != /* ]]; then
|
||||
LIBROBLOX_PATH="${PROJECT_ROOT}/${LIBROBLOX_PATH}"
|
||||
fi
|
||||
|
||||
target_user_home() {
|
||||
local user="$1"
|
||||
local entry
|
||||
entry="$(getent passwd "${user}" 2>/dev/null || true)"
|
||||
if [[ -z "${entry}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local name passwd uid gid gecos home shell
|
||||
IFS=: read -r name passwd uid gid gecos home shell <<< "${entry}"
|
||||
printf '%s\n' "${home}"
|
||||
}
|
||||
|
||||
configure_wayland_env() {
|
||||
local uid="$1"
|
||||
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/${uid}}"
|
||||
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
|
||||
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=${XDG_RUNTIME_DIR}/bus}"
|
||||
export MOCKTAIL_PREFER_WAYLAND="${MOCKTAIL_PREFER_WAYLAND:-1}"
|
||||
|
||||
# Leave SDL_VIDEODRIVER unset so the runtime policy can avoid native
|
||||
# Wayland Vulkan WSI on NVIDIA. Explicit caller overrides remain untouched.
|
||||
}
|
||||
|
||||
if [[ ! -f "${LIBROBLOX_PATH}" ]]; then
|
||||
echo "[error] libroblox.so not found: ${LIBROBLOX_PATH}" >&2
|
||||
echo " Put an x86_64 Roblox lib at rbx_bin/libroblox.so or set ROBLOX_LIB_PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "${BINARY}" ]]; then
|
||||
cmake -S "${PROJECT_ROOT}" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build "${BUILD_DIR}" -j"$(nproc)"
|
||||
fi
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
export ROBLOX_LIB_PATH="${LIBROBLOX_PATH}"
|
||||
export MOCKTAIL_SOBER_MODE="${MOCKTAIL_SOBER_MODE:-1}"
|
||||
export MOCKTAIL_ASSET_ROOT="${MOCKTAIL_ASSET_ROOT:-${PROJECT_ROOT}/rbx_bin/assets}"
|
||||
export MOCKTAIL_ASSET_PATH="${MOCKTAIL_ASSET_PATH:-rbx_bin/assets/content}"
|
||||
export MOCKTAIL_SOBER_BOOTSTRAP_ONLY="${MOCKTAIL_SOBER_BOOTSTRAP_ONLY:-0}"
|
||||
export MOCKTAIL_GRAPHICS_BACKEND="${MOCKTAIL_GRAPHICS_BACKEND:-system}"
|
||||
|
||||
RUN_AS_USER="${MOCKTAIL_RUN_AS_USER:-}"
|
||||
if [[ -n "${RUN_AS_USER}" && "${MOCKTAIL_RUNNER_REEXECED:-0}" == "0" &&
|
||||
"$(id -un)" != "${RUN_AS_USER}" ]]; then
|
||||
if [[ "$(id -u)" != "0" ]]; then
|
||||
echo "[warn] not running as root; cannot switch GUI process to ${RUN_AS_USER}" >&2
|
||||
elif ! id "${RUN_AS_USER}" >/dev/null 2>&1; then
|
||||
echo "[warn] user not found: ${RUN_AS_USER}; running as $(id -un)" >&2
|
||||
else
|
||||
RUN_AS_UID="$(id -u "${RUN_AS_USER}")"
|
||||
RUN_AS_HOME="$(target_user_home "${RUN_AS_USER}")"
|
||||
configure_wayland_env "${RUN_AS_UID}"
|
||||
echo "[info] switching GUI process to ${RUN_AS_USER} on ${WAYLAND_DISPLAY}"
|
||||
exec runuser -u "${RUN_AS_USER}" -- env \
|
||||
HOME="${RUN_AS_HOME}" \
|
||||
USER="${RUN_AS_USER}" \
|
||||
LOGNAME="${RUN_AS_USER}" \
|
||||
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" \
|
||||
WAYLAND_DISPLAY="${WAYLAND_DISPLAY}" \
|
||||
DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" \
|
||||
SDL_VIDEODRIVER="${SDL_VIDEODRIVER:-}" \
|
||||
MOCKTAIL_RUNNER_REEXECED=1 \
|
||||
ROBLOX_LIB_PATH="${ROBLOX_LIB_PATH}" \
|
||||
MOCKTAIL_BUILD_DIR="${BUILD_DIR}" \
|
||||
MOCKTAIL_SOBER_MODE="${MOCKTAIL_SOBER_MODE}" \
|
||||
MOCKTAIL_ASSET_ROOT="${MOCKTAIL_ASSET_ROOT}" \
|
||||
MOCKTAIL_ASSET_PATH="${MOCKTAIL_ASSET_PATH}" \
|
||||
MOCKTAIL_SOBER_BOOTSTRAP_ONLY="${MOCKTAIL_SOBER_BOOTSTRAP_ONLY}" \
|
||||
MOCKTAIL_GRAPHICS_BACKEND="${MOCKTAIL_GRAPHICS_BACKEND}" \
|
||||
MOCKTAIL_PREFER_WAYLAND="${MOCKTAIL_PREFER_WAYLAND}" \
|
||||
MOCKTAIL_EGL_LIBRARY="${MOCKTAIL_EGL_LIBRARY:-}" \
|
||||
MOCKTAIL_GLES_LIBRARY="${MOCKTAIL_GLES_LIBRARY:-}" \
|
||||
MOCKTAIL_FORCE_X11="${MOCKTAIL_FORCE_X11:-0}" \
|
||||
MOCKTAIL_ANGLE_FORCE_X11="${MOCKTAIL_ANGLE_FORCE_X11:-0}" \
|
||||
"${BASH}" "${BASH_SOURCE[0]}" "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${RUN_AS_USER}" && "$(id -un)" == "${RUN_AS_USER}" ]]; then
|
||||
configure_wayland_env "$(id -u)"
|
||||
fi
|
||||
|
||||
if [[ "${MOCKTAIL_SOBER_BOOTSTRAP_ONLY}" != "0" ]]; then
|
||||
export MOCKTAIL_STEP_APP_BRIDGE_APP_START="${MOCKTAIL_STEP_APP_BRIDGE_APP_START:-0}"
|
||||
export MOCKTAIL_STEP_START_LUA_APP_DM="${MOCKTAIL_STEP_START_LUA_APP_DM:-0}"
|
||||
fi
|
||||
|
||||
echo "[info] starting Mocktail"
|
||||
echo "[info] SDL video driver: ${SDL_VIDEODRIVER:-auto}"
|
||||
echo "[info] graphics backend: ${MOCKTAIL_GRAPHICS_BACKEND}"
|
||||
echo "[info] display: DISPLAY=${DISPLAY:-unset} WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-unset}"
|
||||
echo "[info] bootstrap-only: ${MOCKTAIL_SOBER_BOOTSTRAP_ONLY}"
|
||||
echo "[info] press Ctrl+C to stop"
|
||||
|
||||
if [[ -n "${MOCKTAIL_LOG_PATH:-}" ]]; then
|
||||
set +e
|
||||
"${BINARY}" "$@" 2>&1 | tee "${MOCKTAIL_LOG_PATH}"
|
||||
binary_status="${PIPESTATUS[0]}"
|
||||
set -e
|
||||
exit "${binary_status}"
|
||||
fi
|
||||
|
||||
exec "${BINARY}" "$@"
|
||||
@@ -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>
|
||||
@@ -0,0 +1,225 @@
|
||||
#ifndef MOCKTAIL_AUDIO_NATIVE_INPUT_CAPTURE_H_
|
||||
#define MOCKTAIL_AUDIO_NATIVE_INPUT_CAPTURE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/audio/sdl_audio_capture.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
class NativeInputCapture;
|
||||
|
||||
struct NativePcmFormat {
|
||||
std::uint32_t rate = 48000;
|
||||
std::uint8_t channels = 1;
|
||||
std::uint8_t bytes_per_sample = 2;
|
||||
std::uint8_t flags = 0;
|
||||
std::uint8_t padding = 0;
|
||||
};
|
||||
struct NativeOptionalFormat {
|
||||
NativePcmFormat format{};
|
||||
std::uint64_t present = 0;
|
||||
};
|
||||
struct NativeSharedSink { void* object; void* owner; };
|
||||
static_assert(sizeof(NativeOptionalFormat) == 16);
|
||||
static_assert(sizeof(NativePcmFormat) == 8);
|
||||
inline std::atomic<NativeInputCapture*> g_native_capture{nullptr};
|
||||
|
||||
class NativeInputCapture {
|
||||
public:
|
||||
using DeliverPcm = std::size_t (*)(void*, const void*, std::size_t,
|
||||
std::uint64_t, const NativePcmFormat*);
|
||||
using Destroy = void (*)(NativeSharedSink*);
|
||||
using OriginalLatency = void (*)(void*, std::uint32_t*, std::uint32_t*);
|
||||
struct SinkAbi {
|
||||
std::uintptr_t vtable;
|
||||
DeliverPcm deliver;
|
||||
Destroy release;
|
||||
OriginalLatency latency;
|
||||
};
|
||||
NativeInputCapture(SinkAbi abi, bool enabled)
|
||||
: abi_(abi), enabled_(enabled) {}
|
||||
~NativeInputCapture() { Shutdown(); }
|
||||
|
||||
static NativeOptionalFormat Format(void*) {
|
||||
auto* capture = g_native_capture.load(std::memory_order_acquire);
|
||||
return {{}, capture && capture->enabled_ ? 1ULL : 0ULL};
|
||||
}
|
||||
static void Latency(void* self, std::uint32_t* playback,
|
||||
std::uint32_t* recording) {
|
||||
// Preserve the native playback latency and report the host capture queue.
|
||||
auto* capture = g_native_capture.load(std::memory_order_acquire);
|
||||
if (!capture) return;
|
||||
capture->abi_.latency(self, playback, recording);
|
||||
if (recording) {
|
||||
std::lock_guard lock(capture->mutex_);
|
||||
*recording = 10;
|
||||
auto it = capture->sessions_.find(self);
|
||||
if (it != capture->sessions_.end()) {
|
||||
std::lock_guard queue_lock(it->second->queue_mutex);
|
||||
*recording += static_cast<std::uint32_t>(it->second->count * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
static bool Start(void* self, NativeSharedSink* sink) {
|
||||
auto* capture = g_native_capture.load(std::memory_order_acquire);
|
||||
return capture && capture->StartSink(self, sink);
|
||||
}
|
||||
static void Stop(void* self, NativeSharedSink* sink) {
|
||||
auto* capture = g_native_capture.load(std::memory_order_acquire);
|
||||
if (capture) capture->StopSink(self, sink);
|
||||
}
|
||||
static bool Recording(void* self) {
|
||||
auto* capture = g_native_capture.load(std::memory_order_acquire);
|
||||
if (!capture) return false;
|
||||
std::lock_guard lock(capture->mutex_);
|
||||
return capture->sessions_.find(self) != capture->sessions_.end();
|
||||
}
|
||||
static void Poll(void* self) {
|
||||
auto* capture = g_native_capture.load(std::memory_order_acquire);
|
||||
if (capture) capture->Deliver(self);
|
||||
}
|
||||
void Shutdown() {
|
||||
std::lock_guard lock(mutex_);
|
||||
for (auto& [self, session] : sessions_) Close(*session);
|
||||
sessions_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Session {
|
||||
std::unique_ptr<AudioCapture> input;
|
||||
std::vector<NativeSharedSink> sinks;
|
||||
std::mutex queue_mutex;
|
||||
// At most 200 ms; a stalled consumer must not accumulate stale speech.
|
||||
std::array<std::array<std::int16_t, 480>, 20> frames{};
|
||||
std::size_t head = 0, count = 0;
|
||||
std::uint64_t delivered = 0, dropped = 0;
|
||||
bool audible = false;
|
||||
};
|
||||
static void OnData(void* context, std::size_t bytes) {
|
||||
auto& session = *static_cast<Session*>(context);
|
||||
if (bytes != sizeof(session.frames[0])) return;
|
||||
std::lock_guard lock(session.queue_mutex);
|
||||
if (session.count == session.frames.size()) {
|
||||
session.head = (session.head + 1) % session.frames.size();
|
||||
--session.count;
|
||||
++session.dropped;
|
||||
}
|
||||
std::memcpy(session.frames[(session.head + session.count) %
|
||||
session.frames.size()].data(),
|
||||
session.input->buffer_data(), bytes);
|
||||
++session.count;
|
||||
}
|
||||
bool StartSink(void* self, NativeSharedSink* sink) {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!enabled_ || !self || !sink || !sink->object || !sink->owner)
|
||||
return false;
|
||||
auto* table = *reinterpret_cast<std::uintptr_t**>(sink->object);
|
||||
if (reinterpret_cast<std::uintptr_t>(table) != abi_.vtable ||
|
||||
table[2] != reinterpret_cast<std::uintptr_t>(abi_.deliver)) {
|
||||
std::fprintf(stderr, " [audio-input] unsupported native PCM sink ABI\n");
|
||||
return false;
|
||||
}
|
||||
auto it = sessions_.find(self);
|
||||
if (it != sessions_.end()) {
|
||||
for (const auto& existing : it->second->sinks)
|
||||
if (existing.object == sink->object) return true;
|
||||
} else {
|
||||
auto session = std::make_unique<Session>();
|
||||
SdlAudioCaptureOptions options;
|
||||
options.output_spec.channels = 1;
|
||||
options.data_callback = &OnData;
|
||||
options.data_context = session.get();
|
||||
auto status = CreateSdlAudioCapture(options, &session->input);
|
||||
if (status.ok()) status = session->input->Start();
|
||||
if (!status.ok()) {
|
||||
std::fprintf(stderr, " [audio-input] capture start failed: %s\n",
|
||||
status.message().c_str());
|
||||
return false;
|
||||
}
|
||||
it = sessions_.emplace(self, std::move(session)).first;
|
||||
std::fprintf(stderr,
|
||||
" [audio-input] native capture started: 48000 Hz mono signed16\n");
|
||||
}
|
||||
// The native parameter is a by-value shared_ptr. Retain its existing
|
||||
// ownership by moving it, leaving the caller's temporary empty.
|
||||
it->second->sinks.push_back(*sink);
|
||||
*sink = {};
|
||||
return true;
|
||||
}
|
||||
void Release(NativeSharedSink& sink) {
|
||||
abi_.release(&sink);
|
||||
sink = {};
|
||||
}
|
||||
void Close(Session& session) {
|
||||
session.input->Shutdown();
|
||||
for (auto& sink : session.sinks) Release(sink);
|
||||
session.sinks.clear();
|
||||
std::fprintf(stderr,
|
||||
" [audio-input] native capture stopped: delivered=%llu dropped=%llu\n",
|
||||
static_cast<unsigned long long>(session.delivered),
|
||||
static_cast<unsigned long long>(session.dropped));
|
||||
}
|
||||
void StopSink(void* self, NativeSharedSink* sink) {
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sessions_.find(self);
|
||||
if (it == sessions_.end() || !sink) return;
|
||||
auto& session = *it->second;
|
||||
auto existing = std::find_if(session.sinks.begin(), session.sinks.end(),
|
||||
[&](const auto& entry) { return entry.object == sink->object; });
|
||||
if (existing == session.sinks.end()) return;
|
||||
Release(*existing);
|
||||
session.sinks.erase(existing);
|
||||
if (session.sinks.empty()) {
|
||||
Close(session);
|
||||
sessions_.erase(it);
|
||||
}
|
||||
}
|
||||
void Deliver(void* self) {
|
||||
// Invoked by Roblox's recording thread. Guest code and allocator TLS must
|
||||
// never run on SDL's capture callback thread.
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sessions_.find(self);
|
||||
if (it == sessions_.end()) return;
|
||||
auto& session = *it->second;
|
||||
for (std::size_t n = 0; n < session.frames.size(); ++n) {
|
||||
std::array<std::int16_t, 480> pcm;
|
||||
std::uint64_t delay;
|
||||
{
|
||||
std::lock_guard queue_lock(session.queue_mutex);
|
||||
if (!session.count) break;
|
||||
pcm = session.frames[session.head];
|
||||
delay = session.count * 10000000ULL;
|
||||
session.head = (session.head + 1) % session.frames.size();
|
||||
--session.count;
|
||||
}
|
||||
const NativePcmFormat format;
|
||||
for (const auto& sink : session.sinks)
|
||||
abi_.deliver(
|
||||
sink.object, pcm.data(), pcm.size(), delay, &format);
|
||||
if (++session.delivered == 1)
|
||||
std::fprintf(stderr, " [audio-input] PCM delivered to native voice sink\n");
|
||||
if (!session.audible && std::any_of(pcm.begin(), pcm.end(),
|
||||
[](auto value) { return value != 0; })) {
|
||||
session.audible = true;
|
||||
std::fprintf(stderr, " [audio-input] nonzero microphone PCM delivered\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
SinkAbi abi_;
|
||||
bool enabled_;
|
||||
std::mutex mutex_;
|
||||
std::map<void*, std::unique_ptr<Session>> sessions_;
|
||||
};
|
||||
|
||||
|
||||
} // namespace mocktail::audio
|
||||
#endif
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
@@ -20,7 +21,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include "audio/roblox_output_device_bridge_internal.h"
|
||||
#include "audio/native_input_capture.h"
|
||||
#include "linker/linker.h"
|
||||
#include "mocktail/audio/sdl_audio_capture.h"
|
||||
#include "mocktail/audio/sdl_audio_sink.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
@@ -28,8 +31,6 @@ namespace {
|
||||
|
||||
constexpr std::size_t kOutputCountVtableIndex = 5;
|
||||
constexpr std::size_t kOutputInfoVtableIndex = 6;
|
||||
constexpr std::size_t kCurrentOutputVtableIndex = 7;
|
||||
constexpr std::size_t kSelectOutputVtableIndex = 17;
|
||||
constexpr std::size_t kGuestStringSize = 24;
|
||||
constexpr std::size_t kGuestDeviceInfoSize = kGuestStringSize * 2 + 1;
|
||||
constexpr std::size_t kStringConstructorContractSize = 24;
|
||||
@@ -94,7 +95,8 @@ bool IsRelroImageRange(std::uintptr_t image_base, std::uintptr_t rva,
|
||||
return IsProgramHeaderRange(image_base, rva, size, PT_GNU_RELRO, 0);
|
||||
}
|
||||
|
||||
bool SetVtableWritable(std::uintptr_t* vtable, bool writable) {
|
||||
bool SetVtableWritable(std::uintptr_t* vtable, std::size_t select_index,
|
||||
bool writable) {
|
||||
if (vtable == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -104,9 +106,9 @@ bool SetVtableWritable(std::uintptr_t* vtable, bool writable) {
|
||||
}
|
||||
const std::uintptr_t page_size = static_cast<std::uintptr_t>(page_size_value);
|
||||
const std::uintptr_t first =
|
||||
reinterpret_cast<std::uintptr_t>(vtable + kOutputCountVtableIndex);
|
||||
reinterpret_cast<std::uintptr_t>(vtable + 4);
|
||||
const std::uintptr_t last =
|
||||
reinterpret_cast<std::uintptr_t>(vtable + kSelectOutputVtableIndex + 1);
|
||||
reinterpret_cast<std::uintptr_t>(vtable + select_index + 1);
|
||||
if (last <= first) {
|
||||
return false;
|
||||
}
|
||||
@@ -128,6 +130,16 @@ std::uintptr_t FunctionAddress(Function function) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// ABI verified for Roblox 2.738.1397. Keep capture separate from the menu
|
||||
// profile: relocating query methods alone does not establish a recording ABI.
|
||||
namespace {
|
||||
constexpr std::array<std::size_t, 6> kCaptureSlots{4, 13, 14, 15, 16, 17};
|
||||
constexpr std::array<std::uintptr_t, 6> kCaptureRvas{
|
||||
0x320c630, 0x320bcf6, 0x320d4b4, 0x320dd94, 0x320b844, 0x320e49a};
|
||||
} // namespace
|
||||
|
||||
RobloxOutputDeviceBridge::RobloxOutputDeviceBridge() = default;
|
||||
|
||||
namespace internal {
|
||||
|
||||
bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
|
||||
@@ -145,8 +157,19 @@ bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
|
||||
bool HasExpectedFmodOutputDeviceVtable(
|
||||
const std::uintptr_t* vtable, std::uintptr_t image_base,
|
||||
const compat::FmodOutputDeviceBridgeProfile& profile) {
|
||||
return vtable != nullptr && image_base != 0 &&
|
||||
vtable[kOutputCountVtableIndex] ==
|
||||
const std::size_t kCurrentOutputVtableIndex = profile.current_vtable_index();
|
||||
const std::size_t kSelectOutputVtableIndex = profile.select_vtable_index();
|
||||
if (vtable == nullptr || image_base == 0 || !profile.valid_vtable_layout())
|
||||
return false;
|
||||
if (profile.has_input_devices()) {
|
||||
const auto indexes = profile.input_vtable_indexes();
|
||||
for (std::size_t i = 0; i < indexes.size(); ++i) {
|
||||
if (profile.input_method_rvas[i] == 0 ||
|
||||
vtable[indexes[i]] != image_base + profile.input_method_rvas[i])
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return vtable[kOutputCountVtableIndex] ==
|
||||
image_base + profile.count_method_rva &&
|
||||
vtable[kOutputInfoVtableIndex] ==
|
||||
image_base + profile.info_method_rva &&
|
||||
@@ -191,6 +214,9 @@ Status RobloxOutputDeviceBridge::Install(const compat::BuildProfile& profile) {
|
||||
return FailedPrecondition(
|
||||
"FMOD output-device profile requires host ABI bridges");
|
||||
}
|
||||
if (!profile.fmod_output_device_bridge->valid_vtable_layout()) {
|
||||
return FailedPrecondition("unsupported FMOD output-device vtable layout");
|
||||
}
|
||||
|
||||
RobloxOutputDeviceBridge* expected = nullptr;
|
||||
if (!g_active_bridge.compare_exchange_strong(expected, this,
|
||||
@@ -200,6 +226,10 @@ Status RobloxOutputDeviceBridge::Install(const compat::BuildProfile& profile) {
|
||||
"another Roblox output-device bridge owns the process");
|
||||
}
|
||||
profile_ = *profile.fmod_output_device_bridge;
|
||||
capture_profile_supported_ =
|
||||
profile.elf_build_id == "5f0704edd9064f566ee3d6df2bd2fabbcc709f03" &&
|
||||
profile_.vtable_rva == 0x6cd3ce0 &&
|
||||
profile_.vtable_layout_version == 2 && profile_.has_input_devices();
|
||||
if (!linker::SetAndroidLibraryLoadObserver(&ObserveAndroidLibrary, this)) {
|
||||
g_active_bridge.store(nullptr, std::memory_order_release);
|
||||
profile_ = {};
|
||||
@@ -225,8 +255,12 @@ void RobloxOutputDeviceBridge::Shutdown() {
|
||||
" [audio-menu] failed to restore FmodAudioDevice "
|
||||
"vtable protection\n");
|
||||
}
|
||||
g_native_capture.store(nullptr, std::memory_order_release);
|
||||
capture_.reset();
|
||||
active_ = false;
|
||||
devices_.clear();
|
||||
input_devices_.clear();
|
||||
selected_input_index_ = 0;
|
||||
string_constructor_ = nullptr;
|
||||
library_base_ = 0;
|
||||
vtable_ = nullptr;
|
||||
@@ -318,6 +352,37 @@ Status RobloxOutputDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
return FailedPrecondition("configured SDL audio output disappeared");
|
||||
}
|
||||
|
||||
input_devices_.clear();
|
||||
selected_input_index_ = 0;
|
||||
const char *input_target = std::getenv("MOCKTAIL_AUDIO_INPUT_DEVICE");
|
||||
if (profile_.has_input_devices() &&
|
||||
(input_target == nullptr ||
|
||||
std::string_view(input_target) != "disabled")) {
|
||||
std::vector<SdlRecordingDevice> recording_devices;
|
||||
status = ListSdlRecordingDevices(&recording_devices);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
if (recording_devices.size() + 1 > kMaximumMenuDevices)
|
||||
return InvalidArgument("host exposes too many audio input devices");
|
||||
std::uint32_t selected = 0;
|
||||
std::string selected_name;
|
||||
status = GetConfiguredSdlRecordingDevice(&selected, &selected_name);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
input_devices_.push_back({0, "System Default", "mocktail:input:default"});
|
||||
for (const auto &device : recording_devices) {
|
||||
if (device.name.empty() || device.name.size() > kMaximumDeviceNameBytes)
|
||||
return InvalidArgument("SDL audio input name is empty or too long");
|
||||
input_devices_.push_back(
|
||||
{device.id, device.name,
|
||||
"input:" + internal::MakeOutputDeviceGuid(device.id, device.name)});
|
||||
if (device.id == selected)
|
||||
selected_input_index_ = static_cast<int>(input_devices_.size() - 1);
|
||||
}
|
||||
if (selected != 0 && selected_input_index_ == 0)
|
||||
return FailedPrecondition("configured SDL audio input disappeared");
|
||||
}
|
||||
|
||||
library_base_ = image_base;
|
||||
vtable_ = reinterpret_cast<std::uintptr_t*>(image_base + profile_.vtable_rva);
|
||||
string_constructor_ =
|
||||
@@ -325,6 +390,8 @@ Status RobloxOutputDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
status = PatchVtableLocked();
|
||||
if (!status.ok()) {
|
||||
devices_.clear();
|
||||
input_devices_.clear();
|
||||
selected_input_index_ = 0;
|
||||
string_constructor_ = nullptr;
|
||||
library_base_ = 0;
|
||||
vtable_ = nullptr;
|
||||
@@ -338,19 +405,29 @@ Status RobloxOutputDeviceBridge::Activate(std::uintptr_t image_base) {
|
||||
for (const MenuDevice& device : devices_) {
|
||||
std::fprintf(stderr, " [audio-menu] %s\n", device.name.c_str());
|
||||
}
|
||||
if (profile_.has_input_devices()) {
|
||||
std::fprintf(stderr,
|
||||
" [audio-menu] exposed %zu host input routes; selected=%s\n",
|
||||
input_devices_.size(),
|
||||
input_devices_.empty()
|
||||
? "disabled"
|
||||
: input_devices_[selected_input_index_].name.c_str());
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
const std::size_t kCurrentOutputVtableIndex = profile_.current_vtable_index();
|
||||
const std::size_t kSelectOutputVtableIndex = profile_.select_vtable_index();
|
||||
if (profile_.vtable_rva >
|
||||
std::numeric_limits<std::uintptr_t>::max() -
|
||||
(kSelectOutputVtableIndex + 1) * sizeof(std::uintptr_t)) {
|
||||
return FailedPrecondition("FMOD output-device vtable range overflows");
|
||||
}
|
||||
const std::uintptr_t first_slot_rva =
|
||||
profile_.vtable_rva + kOutputCountVtableIndex * sizeof(std::uintptr_t);
|
||||
profile_.vtable_rva + 4 * sizeof(std::uintptr_t);
|
||||
const std::size_t slot_range_size =
|
||||
(kSelectOutputVtableIndex - kOutputCountVtableIndex + 1) *
|
||||
(kSelectOutputVtableIndex - 4 + 1) *
|
||||
sizeof(std::uintptr_t);
|
||||
if (!IsRelroImageRange(library_base_, first_slot_rva, slot_range_size) ||
|
||||
!IsExecutableImageRange(library_base_, profile_.string_constructor_rva,
|
||||
@@ -362,6 +439,15 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
return FailedPrecondition(
|
||||
"FMOD output-device profile points outside verified image ranges");
|
||||
}
|
||||
if (profile_.has_input_devices()) {
|
||||
for (const auto rva : profile_.input_method_rvas) {
|
||||
if (rva == 0 ||
|
||||
rva > std::numeric_limits<std::uintptr_t>::max() - library_base_ ||
|
||||
!IsExecutableImageRange(library_base_, rva, 1))
|
||||
return FailedPrecondition(
|
||||
"FMOD input profile points outside executable image");
|
||||
}
|
||||
}
|
||||
if (!internal::HasExpectedFmodOutputDeviceVtable(vtable_, library_base_,
|
||||
profile_)) {
|
||||
return FailedPrecondition(
|
||||
@@ -375,13 +461,41 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
"FmodAudioDevice string ABI contract validation failed");
|
||||
}
|
||||
|
||||
if (capture_profile_supported_) {
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i) {
|
||||
if (!IsExecutableImageRange(library_base_, kCaptureRvas[i], 1) ||
|
||||
vtable_[kCaptureSlots[i]] != library_base_ + kCaptureRvas[i])
|
||||
return FailedPrecondition("native input capture vtable ABI mismatch");
|
||||
original_capture_methods_[i] = vtable_[kCaptureSlots[i]];
|
||||
}
|
||||
if (!IsExecutableImageRange(library_base_, 0x3213154, 1) ||
|
||||
!IsExecutableImageRange(library_base_, 0x1dc4cd0, 1) ||
|
||||
!IsRelroImageRange(library_base_, 0x6cd4550, 24))
|
||||
return FailedPrecondition("native input capture sink ABI outside image");
|
||||
capture_ = std::make_unique<NativeInputCapture>(
|
||||
NativeInputCapture::SinkAbi{
|
||||
library_base_ + 0x6cd4550,
|
||||
reinterpret_cast<NativeInputCapture::DeliverPcm>(
|
||||
library_base_ + 0x3213154),
|
||||
reinterpret_cast<NativeInputCapture::Destroy>(
|
||||
library_base_ + 0x1dc4cd0),
|
||||
reinterpret_cast<NativeInputCapture::OriginalLatency>(
|
||||
library_base_ + kCaptureRvas[1])},
|
||||
!input_devices_.empty());
|
||||
}
|
||||
|
||||
original_methods_ = {
|
||||
vtable_[kOutputCountVtableIndex],
|
||||
vtable_[kOutputInfoVtableIndex],
|
||||
vtable_[kCurrentOutputVtableIndex],
|
||||
vtable_[kSelectOutputVtableIndex],
|
||||
};
|
||||
if (!SetVtableWritable(vtable_, true)) {
|
||||
if (profile_.has_input_devices()) {
|
||||
const auto indexes = profile_.input_vtable_indexes();
|
||||
for (std::size_t i = 0; i < indexes.size(); ++i)
|
||||
original_methods_[4 + i] = vtable_[indexes[i]];
|
||||
}
|
||||
if (!SetVtableWritable(vtable_, kSelectOutputVtableIndex, true)) {
|
||||
return Status::Error(StatusCode::kPlatformError,
|
||||
"cannot make FmodAudioDevice vtable writable");
|
||||
}
|
||||
@@ -394,7 +508,29 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
FunctionAddress(&GetCurrentOutputDevice), __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kSelectOutputVtableIndex],
|
||||
FunctionAddress(&SetCurrentOutputDevice), __ATOMIC_RELEASE);
|
||||
if (!SetVtableWritable(vtable_, false)) {
|
||||
if (profile_.has_input_devices()) {
|
||||
const auto indexes = profile_.input_vtable_indexes();
|
||||
const std::array<std::uintptr_t, 4> methods = {
|
||||
FunctionAddress(&GetInputDeviceCount),
|
||||
FunctionAddress(&GetInputDeviceInfo),
|
||||
FunctionAddress(&GetCurrentInputDevice),
|
||||
FunctionAddress(&SetCurrentInputDevice)};
|
||||
for (std::size_t i = 0; i < indexes.size(); ++i)
|
||||
__atomic_store_n(&vtable_[indexes[i]], methods[i], __ATOMIC_RELEASE);
|
||||
}
|
||||
if (capture_) {
|
||||
g_native_capture.store(capture_.get(), std::memory_order_release);
|
||||
const std::array<std::uintptr_t, 6> methods{
|
||||
FunctionAddress(&NativeInputCapture::Format),
|
||||
FunctionAddress(&NativeInputCapture::Latency),
|
||||
FunctionAddress(&NativeInputCapture::Start),
|
||||
FunctionAddress(&NativeInputCapture::Stop),
|
||||
FunctionAddress(&NativeInputCapture::Poll),
|
||||
FunctionAddress(&NativeInputCapture::Recording)};
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[kCaptureSlots[i]], methods[i], __ATOMIC_RELEASE);
|
||||
}
|
||||
if (!SetVtableWritable(vtable_, kSelectOutputVtableIndex, false)) {
|
||||
__atomic_store_n(&vtable_[kOutputCountVtableIndex], original_methods_[0],
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kOutputInfoVtableIndex], original_methods_[1],
|
||||
@@ -403,7 +539,20 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kSelectOutputVtableIndex], original_methods_[3],
|
||||
__ATOMIC_RELEASE);
|
||||
(void)SetVtableWritable(vtable_, false);
|
||||
if (profile_.has_input_devices()) {
|
||||
const auto indexes = profile_.input_vtable_indexes();
|
||||
for (std::size_t i = 0; i < indexes.size(); ++i)
|
||||
__atomic_store_n(&vtable_[indexes[i]], original_methods_[4 + i],
|
||||
__ATOMIC_RELEASE);
|
||||
}
|
||||
if (capture_) {
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[kCaptureSlots[i]], original_capture_methods_[i],
|
||||
__ATOMIC_RELEASE);
|
||||
g_native_capture.store(nullptr, std::memory_order_release);
|
||||
capture_.reset();
|
||||
}
|
||||
(void)SetVtableWritable(vtable_, kSelectOutputVtableIndex, false);
|
||||
return Status::Error(StatusCode::kPlatformError,
|
||||
"cannot restore FmodAudioDevice RELRO protection");
|
||||
}
|
||||
@@ -411,7 +560,10 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
}
|
||||
|
||||
bool RobloxOutputDeviceBridge::RestoreVtableLocked() {
|
||||
if (vtable_ == nullptr || !SetVtableWritable(vtable_, true)) {
|
||||
const std::size_t kCurrentOutputVtableIndex = profile_.current_vtable_index();
|
||||
const std::size_t kSelectOutputVtableIndex = profile_.select_vtable_index();
|
||||
if (vtable_ == nullptr ||
|
||||
!SetVtableWritable(vtable_, kSelectOutputVtableIndex, true)) {
|
||||
return false;
|
||||
}
|
||||
__atomic_store_n(&vtable_[kOutputCountVtableIndex], original_methods_[0],
|
||||
@@ -422,7 +574,18 @@ bool RobloxOutputDeviceBridge::RestoreVtableLocked() {
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&vtable_[kSelectOutputVtableIndex], original_methods_[3],
|
||||
__ATOMIC_RELEASE);
|
||||
return SetVtableWritable(vtable_, false);
|
||||
if (profile_.has_input_devices()) {
|
||||
const auto indexes = profile_.input_vtable_indexes();
|
||||
for (std::size_t i = 0; i < indexes.size(); ++i)
|
||||
__atomic_store_n(&vtable_[indexes[i]], original_methods_[4 + i],
|
||||
__ATOMIC_RELEASE);
|
||||
}
|
||||
if (capture_) {
|
||||
for (std::size_t i = 0; i < kCaptureSlots.size(); ++i)
|
||||
__atomic_store_n(&vtable_[kCaptureSlots[i]], original_capture_methods_[i],
|
||||
__ATOMIC_RELEASE);
|
||||
}
|
||||
return SetVtableWritable(vtable_, kSelectOutputVtableIndex, false);
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::GetOutputDeviceCount(void* self) {
|
||||
@@ -462,22 +625,51 @@ void RobloxOutputDeviceBridge::SetCurrentOutputDevice(void* self, int index) {
|
||||
}
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::DeviceCount() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return active_ ? static_cast<int>(devices_.size()) : 0;
|
||||
int RobloxOutputDeviceBridge::GetInputDeviceCount(void *) {
|
||||
auto *bridge = g_active_bridge.load(std::memory_order_acquire);
|
||||
return bridge ? bridge->DeviceCount(true) : 0;
|
||||
}
|
||||
|
||||
void* RobloxOutputDeviceBridge::DeviceInfo(void* result, int index) {
|
||||
void *RobloxOutputDeviceBridge::GetInputDeviceInfo(void *result, void *,
|
||||
int index) {
|
||||
auto *bridge = g_active_bridge.load(std::memory_order_acquire);
|
||||
if (bridge)
|
||||
return bridge->DeviceInfo(result, index, true);
|
||||
if (result)
|
||||
std::memset(result, 0, kGuestDeviceInfoSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::GetCurrentInputDevice(void *) {
|
||||
auto *bridge = g_active_bridge.load(std::memory_order_acquire);
|
||||
return bridge ? bridge->CurrentDevice(true) : 0;
|
||||
}
|
||||
|
||||
void RobloxOutputDeviceBridge::SetCurrentInputDevice(void *, int index) {
|
||||
auto *bridge = g_active_bridge.load(std::memory_order_acquire);
|
||||
if (bridge)
|
||||
bridge->SelectDevice(index, true);
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::DeviceCount(bool input) {
|
||||
const auto &devices = input ? input_devices_ : devices_;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return active_ ? static_cast<int>(devices.size()) : 0;
|
||||
}
|
||||
|
||||
void *RobloxOutputDeviceBridge::DeviceInfo(void *result, int index,
|
||||
bool input) {
|
||||
const auto &devices = input ? input_devices_ : devices_;
|
||||
if (result == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
std::memset(result, 0, kGuestDeviceInfoSize);
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!active_ || index < 0 ||
|
||||
static_cast<std::size_t>(index) >= devices_.size()) {
|
||||
static_cast<std::size_t>(index) >= devices.size()) {
|
||||
return result;
|
||||
}
|
||||
const MenuDevice& device = devices_[static_cast<std::size_t>(index)];
|
||||
const MenuDevice &device = devices[static_cast<std::size_t>(index)];
|
||||
ConstructGuestString(result, device.name);
|
||||
ConstructGuestString(static_cast<std::uint8_t*>(result) + kGuestStringSize,
|
||||
device.guid);
|
||||
@@ -485,29 +677,33 @@ void* RobloxOutputDeviceBridge::DeviceInfo(void* result, int index) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::CurrentDevice() {
|
||||
int RobloxOutputDeviceBridge::CurrentDevice(bool input) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return active_ ? selected_index_ : 0;
|
||||
return active_ ? (input ? selected_input_index_ : selected_index_) : 0;
|
||||
}
|
||||
|
||||
void RobloxOutputDeviceBridge::SelectDevice(int index) {
|
||||
void RobloxOutputDeviceBridge::SelectDevice(int index, bool input) {
|
||||
const auto &devices = input ? input_devices_ : devices_;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!active_ || index < 0 ||
|
||||
static_cast<std::size_t>(index) >= devices_.size()) {
|
||||
static_cast<std::size_t>(index) >= devices.size()) {
|
||||
return;
|
||||
}
|
||||
const MenuDevice& device = devices_[static_cast<std::size_t>(index)];
|
||||
const MenuDevice &device = devices[static_cast<std::size_t>(index)];
|
||||
std::string resolved_name;
|
||||
const Status status =
|
||||
SwitchSdlPlaybackDevice(device.playback_device_id, &resolved_name);
|
||||
input
|
||||
? SwitchSdlRecordingDevice(device.playback_device_id, &resolved_name)
|
||||
: SwitchSdlPlaybackDevice(device.playback_device_id, &resolved_name);
|
||||
if (!status.ok()) {
|
||||
std::fprintf(stderr, " [audio-menu] cannot select host output '%s': %s\n",
|
||||
device.name.c_str(), status.message().c_str());
|
||||
std::fprintf(stderr, " [audio-menu] cannot select host %s '%s': %s\n",
|
||||
input ? "input" : "output", device.name.c_str(),
|
||||
status.message().c_str());
|
||||
return;
|
||||
}
|
||||
selected_index_ = index;
|
||||
std::fprintf(stderr, " [audio-menu] selected host output=%s\n",
|
||||
resolved_name.c_str());
|
||||
(input ? selected_input_index_ : selected_index_) = index;
|
||||
std::fprintf(stderr, " [audio-menu] selected host %s=%s\n",
|
||||
input ? "input" : "output", resolved_name.c_str());
|
||||
}
|
||||
|
||||
void RobloxOutputDeviceBridge::ConstructGuestString(
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
namespace mocktail::audio {
|
||||
namespace {
|
||||
|
||||
class SdlAudioCapture;
|
||||
|
||||
struct SdlRecordingSubsystemState {
|
||||
std::mutex mutex;
|
||||
std::uint32_t configured_device_id = 0;
|
||||
std::string configured_device_name = "default";
|
||||
std::size_t live_captures = 0;
|
||||
std::vector<SdlAudioCapture *> live_captures;
|
||||
};
|
||||
|
||||
SdlRecordingSubsystemState& RecordingSubsystemState() {
|
||||
@@ -63,42 +65,6 @@ SDL_AudioFormat ToSdlFormat(PcmSampleFormat format) {
|
||||
return SDL_AUDIO_UNKNOWN;
|
||||
}
|
||||
|
||||
Status ResolveConfiguredDevice(std::uint32_t requested_device_id,
|
||||
std::uint32_t* resolved_device_id) {
|
||||
if (resolved_device_id == nullptr) {
|
||||
return InvalidArgument("resolved SDL recording device pointer is null");
|
||||
}
|
||||
if (!AudioSubsystemInitialized()) {
|
||||
return FailedPrecondition(
|
||||
"SDL audio subsystem must be initialized before recording");
|
||||
}
|
||||
SdlRecordingSubsystemState& state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
*resolved_device_id = requested_device_id == 0
|
||||
? state.configured_device_id
|
||||
: requested_device_id;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RegisterCapture() {
|
||||
if (!AudioSubsystemInitialized()) {
|
||||
return FailedPrecondition(
|
||||
"SDL audio subsystem stopped while opening a capture");
|
||||
}
|
||||
SdlRecordingSubsystemState& state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
++state.live_captures;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
void ReleaseCapture() {
|
||||
SdlRecordingSubsystemState& state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if (state.live_captures != 0) {
|
||||
--state.live_captures;
|
||||
}
|
||||
}
|
||||
|
||||
class SdlAudioCapture final : public AudioCapture {
|
||||
public:
|
||||
SdlAudioCapture(PcmSpec spec, std::size_t frames_per_buffer,
|
||||
@@ -155,6 +121,8 @@ class SdlAudioCapture final : public AudioCapture {
|
||||
}
|
||||
|
||||
void Shutdown() override {
|
||||
auto &state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> registry_lock(state.mutex);
|
||||
std::lock_guard<std::mutex> lock(operation_mutex_);
|
||||
if (stream_ == nullptr) {
|
||||
return;
|
||||
@@ -166,10 +134,63 @@ class SdlAudioCapture final : public AudioCapture {
|
||||
stream_ = nullptr;
|
||||
if (registered_) {
|
||||
registered_ = false;
|
||||
ReleaseCapture();
|
||||
auto &captures = state.live_captures;
|
||||
captures.erase(std::remove(captures.begin(), captures.end(), this),
|
||||
captures.end());
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex &operation_mutex() { return operation_mutex_; }
|
||||
|
||||
// The registry and operation mutexes are held throughout a migration.
|
||||
Status PrepareSwitch(std::uint32_t id) {
|
||||
SDL_AudioSpec spec{};
|
||||
spec.format = ToSdlFormat(output_spec_.format);
|
||||
spec.channels = output_spec_.channels;
|
||||
spec.freq = output_spec_.sample_rate_hz;
|
||||
replacement_ = SDL_OpenAudioDeviceStream(
|
||||
id == 0 ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : id, &spec,
|
||||
&OnAudioAvailable, this);
|
||||
return replacement_ ? Status::Ok()
|
||||
: SdlError("open replacement microphone");
|
||||
}
|
||||
|
||||
Status PauseForSwitch() {
|
||||
if (!SDL_PauseAudioStreamDevice(stream_))
|
||||
return SdlError("pause old microphone");
|
||||
// Wait for an in-flight callback before the replacement can use buffer_.
|
||||
if (!SDL_LockAudioStream(stream_))
|
||||
return SdlError("lock old microphone");
|
||||
SDL_UnlockAudioStream(stream_);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status StartReplacement() {
|
||||
if (running_.load(std::memory_order_acquire) &&
|
||||
!SDL_ResumeAudioStreamDevice(replacement_))
|
||||
return SdlError("resume replacement microphone");
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
void CancelReplacement() {
|
||||
if (replacement_) {
|
||||
SDL_DestroyAudioStream(replacement_);
|
||||
replacement_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Status RestoreAfterSwitch() {
|
||||
if (running_.load(std::memory_order_acquire) &&
|
||||
!SDL_ResumeAudioStreamDevice(stream_))
|
||||
return SdlError("restore original microphone");
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
void CommitSwitch() {
|
||||
SDL_DestroyAudioStream(stream_);
|
||||
stream_ = std::exchange(replacement_, nullptr);
|
||||
}
|
||||
|
||||
static void SDLCALL OnAudioAvailable(void* userdata,
|
||||
SDL_AudioStream* stream,
|
||||
int /*additional_amount*/,
|
||||
@@ -202,6 +223,7 @@ class SdlAudioCapture final : public AudioCapture {
|
||||
std::size_t frames_per_buffer_ = 0;
|
||||
std::vector<std::uint8_t> buffer_;
|
||||
SDL_AudioStream* stream_ = nullptr;
|
||||
SDL_AudioStream *replacement_ = nullptr;
|
||||
AudioCaptureDataCallback callback_ = nullptr;
|
||||
void* callback_context_ = nullptr;
|
||||
std::mutex operation_mutex_;
|
||||
@@ -335,7 +357,7 @@ Status ConfigureSdlRecordingDevice(
|
||||
|
||||
SdlRecordingSubsystemState& state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if (state.live_captures != 0) {
|
||||
if (!state.live_captures.empty()) {
|
||||
return FailedPrecondition(
|
||||
"SDL recording device must be configured before opening a capture");
|
||||
}
|
||||
@@ -391,12 +413,14 @@ Status CreateSdlAudioCapture(const SdlAudioCaptureOptions& options,
|
||||
return InvalidArgument("SDL capture frame size is invalid");
|
||||
}
|
||||
|
||||
std::uint32_t recording_device_id = 0;
|
||||
status = ResolveConfiguredDevice(options.recording_device_id,
|
||||
&recording_device_id);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
auto &state = RecordingSubsystemState();
|
||||
std::unique_lock<std::mutex> registry_lock(state.mutex);
|
||||
if (!AudioSubsystemInitialized())
|
||||
return FailedPrecondition(
|
||||
"SDL audio subsystem stopped while opening capture");
|
||||
const std::uint32_t recording_device_id = options.recording_device_id == 0
|
||||
? state.configured_device_id
|
||||
: options.recording_device_id;
|
||||
const SDL_AudioDeviceID device =
|
||||
recording_device_id == 0
|
||||
? SDL_AUDIO_DEVICE_DEFAULT_RECORDING
|
||||
@@ -417,25 +441,87 @@ Status CreateSdlAudioCapture(const SdlAudioCaptureOptions& options,
|
||||
SDL_AudioStream* stream = SDL_OpenAudioDeviceStream(
|
||||
device, &spec, &SdlAudioCapture::OnAudioAvailable, implementation);
|
||||
if (stream == nullptr) {
|
||||
registry_lock.unlock();
|
||||
delete implementation;
|
||||
return SdlError("SDL_OpenAudioDeviceStream(recording)");
|
||||
}
|
||||
|
||||
implementation->AdoptStream(stream);
|
||||
status = RegisterCapture();
|
||||
if (!status.ok()) {
|
||||
delete implementation;
|
||||
return status;
|
||||
}
|
||||
state.live_captures.push_back(implementation);
|
||||
implementation->MarkRegistered();
|
||||
capture->reset(implementation);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status SwitchSdlRecordingDevice(std::uint32_t recording_device_id,
|
||||
std::string *resolved_name) {
|
||||
if (resolved_name == nullptr)
|
||||
return InvalidArgument("microphone switch output is null");
|
||||
resolved_name->clear();
|
||||
std::vector<SdlRecordingDevice> devices;
|
||||
Status status = ListSdlRecordingDevices(&devices);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
std::uint32_t resolved_id = 0;
|
||||
std::string name;
|
||||
status = ResolveSdlRecordingDevice(
|
||||
recording_device_id == 0 ? "default"
|
||||
: "id:" + std::to_string(recording_device_id),
|
||||
devices, &resolved_id, &name);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
auto &state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> registry_lock(state.mutex);
|
||||
if (!AudioSubsystemInitialized())
|
||||
return FailedPrecondition(
|
||||
"SDL audio subsystem stopped during microphone switch");
|
||||
std::vector<std::unique_lock<std::mutex>> locks;
|
||||
locks.reserve(state.live_captures.size());
|
||||
for (auto *capture : state.live_captures)
|
||||
locks.emplace_back(capture->operation_mutex());
|
||||
// Keep the old streams alive until every replacement is ready and running.
|
||||
for (auto *capture : state.live_captures) {
|
||||
status = capture->PrepareSwitch(resolved_id);
|
||||
if (!status.ok())
|
||||
break;
|
||||
}
|
||||
if (status.ok()) {
|
||||
for (auto *capture : state.live_captures) {
|
||||
status = capture->PauseForSwitch();
|
||||
if (!status.ok())
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (status.ok()) {
|
||||
for (auto *capture : state.live_captures) {
|
||||
status = capture->StartReplacement();
|
||||
if (!status.ok())
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!status.ok()) {
|
||||
for (auto *capture : state.live_captures)
|
||||
capture->CancelReplacement();
|
||||
for (auto *capture : state.live_captures) {
|
||||
const Status restored = capture->RestoreAfterSwitch();
|
||||
if (!restored.ok())
|
||||
return restored;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
for (auto *capture : state.live_captures)
|
||||
capture->CommitSwitch();
|
||||
state.configured_device_id = resolved_id;
|
||||
state.configured_device_name = name;
|
||||
*resolved_name = name;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status PrepareSdlAudioCaptureSubsystemShutdown() {
|
||||
SdlRecordingSubsystemState& state = RecordingSubsystemState();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if (state.live_captures != 0) {
|
||||
if (!state.live_captures.empty()) {
|
||||
return FailedPrecondition(
|
||||
"SDL audio shutdown requires every capture to close");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ struct CaptureSession {
|
||||
void* data_context = nullptr;
|
||||
std::unique_ptr<AudioCapture> capture;
|
||||
std::shared_ptr<std::atomic<bool>> microphone_muted;
|
||||
std::atomic<std::uint64_t> delivered_buffers{0};
|
||||
std::atomic<std::uint64_t> audible_buffers{0};
|
||||
std::atomic<std::uint64_t> muted_buffers{0};
|
||||
};
|
||||
|
||||
struct PlayoutSession {
|
||||
@@ -112,22 +115,40 @@ void DisposeManager(void* opaque_context, const void* identity) {
|
||||
void SetMicrophoneMute(void* opaque_context, bool muted) {
|
||||
auto* context = static_cast<WebRtcJniAudioBridgeContext*>(opaque_context);
|
||||
if (context == nullptr) return;
|
||||
context->microphone_muted->store(muted, std::memory_order_release);
|
||||
if (context->microphone_muted->exchange(muted, std::memory_order_acq_rel) != muted) {
|
||||
std::cerr << " [mocktail][audio] WebRTC microphone muted=" << muted << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void OnCapturedAudio(void* opaque_session, std::size_t size_bytes) {
|
||||
auto* session = static_cast<CaptureSession*>(opaque_session);
|
||||
if (session != nullptr && session->data_callback != nullptr) {
|
||||
if (session->capture == nullptr ||
|
||||
size_bytes > session->capture->buffer_size_bytes()) {
|
||||
return;
|
||||
}
|
||||
const auto* samples = static_cast<const unsigned char*>(
|
||||
session->capture->buffer_data());
|
||||
int peak = 0;
|
||||
for (std::size_t i = 0; i + 1 < size_bytes; i += 2) {
|
||||
const int word = samples[i] | (static_cast<int>(samples[i + 1]) << 8);
|
||||
peak = std::max(peak, std::abs(word >= 32768 ? word - 65536 : word));
|
||||
}
|
||||
if (peak != 0 && session->audible_buffers.fetch_add(1) == 0) {
|
||||
std::cerr << " [mocktail][audio] WebRTC microphone first nonzero PCM"
|
||||
<< " peak=" << peak << "/32768\n";
|
||||
}
|
||||
if (session->microphone_muted != nullptr &&
|
||||
session->microphone_muted->load(std::memory_order_acquire)) {
|
||||
if (session->capture == nullptr ||
|
||||
size_bytes > session->capture->buffer_size_bytes()) {
|
||||
return;
|
||||
}
|
||||
session->muted_buffers.fetch_add(1, std::memory_order_relaxed);
|
||||
std::memset(session->capture->buffer_data(), 0, size_bytes);
|
||||
}
|
||||
session->data_callback(session->data_context, session->identity,
|
||||
size_bytes);
|
||||
if (session->delivered_buffers.fetch_add(1) == 0) {
|
||||
std::cerr << " [mocktail][audio] WebRTC microphone first JNI delivery"
|
||||
<< " bytes=" << size_bytes << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +158,10 @@ void CloseCaptureSession(const std::shared_ptr<CaptureSession>& session) {
|
||||
}
|
||||
(void)session->capture->Stop();
|
||||
session->capture->Shutdown();
|
||||
std::cerr << " [mocktail][audio] WebRTC microphone capture summary"
|
||||
<< " buffers=" << session->delivered_buffers.load()
|
||||
<< " nonzero=" << session->audible_buffers.load()
|
||||
<< " muted=" << session->muted_buffers.load() << '\n';
|
||||
}
|
||||
|
||||
int Init(void* opaque_context, const void* identity, int sample_rate_hz,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "compat/bionic_pthread_create_runtime.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -22,6 +23,24 @@ struct ThreadStartContext {
|
||||
|
||||
std::atomic<NativeThreadInitializer> g_thread_initializer{nullptr};
|
||||
|
||||
// Roblox requests maximum-priority FIFO for its Main thread. On hosts with
|
||||
// RLIMIT_RTPRIO=99 this succeeds and later trips the finite RLIMIT_RTTIME.
|
||||
bool RejectRealtimeScheduling(int policy) noexcept {
|
||||
const int base_policy = policy & ~SCHED_RESET_ON_FORK;
|
||||
if (base_policy != SCHED_FIFO && base_policy != SCHED_RR) {
|
||||
return false;
|
||||
}
|
||||
static std::atomic_flag logged = ATOMIC_FLAG_INIT;
|
||||
if (!logged.test_and_set(std::memory_order_relaxed)) {
|
||||
const int saved_errno = errno;
|
||||
fprintf(stderr, " [scheduler] denied guest real-time scheduling (policy=%d): "
|
||||
"keeping host scheduling to prevent RLIMIT_RTTIME SIGXCPU\n",
|
||||
policy);
|
||||
errno = saved_errno;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept {
|
||||
#if defined(__GLIBC__)
|
||||
using GetStaticTlsInfo = void (*)(size_t*, size_t*);
|
||||
@@ -99,6 +118,9 @@ int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getschedparam(&source, &scheduler_parameters);
|
||||
}
|
||||
if (result == 0 && RejectRealtimeScheduling(scheduler_policy)) {
|
||||
return EPERM;
|
||||
}
|
||||
if (result == 0) {
|
||||
result = pthread_attr_setschedpolicy(destination, scheduler_policy);
|
||||
}
|
||||
@@ -274,3 +296,20 @@ extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
return mocktail::compat::CreateBionicPthread(thread, attr, start_routine,
|
||||
argument);
|
||||
}
|
||||
|
||||
extern "C" int mocktail_bionic_pthread_setschedparam(
|
||||
pthread_t thread, int policy, const struct sched_param* parameters) {
|
||||
if (mocktail::compat::RejectRealtimeScheduling(policy)) {
|
||||
return EPERM;
|
||||
}
|
||||
return pthread_setschedparam(thread, policy, parameters);
|
||||
}
|
||||
|
||||
extern "C" int mocktail_bionic_sched_setscheduler(
|
||||
pid_t tid, int policy, const struct sched_param* parameters) {
|
||||
if (mocktail::compat::RejectRealtimeScheduling(policy)) {
|
||||
errno = EPERM;
|
||||
return -1;
|
||||
}
|
||||
return sched_setscheduler(tid, policy, parameters);
|
||||
}
|
||||
@@ -119,11 +119,33 @@ bool ParseOptionalFmodOutputDeviceBridge(
|
||||
return false;
|
||||
}
|
||||
|
||||
int layout = 1;
|
||||
const auto layout_field = field->find("vtable_layout_version");
|
||||
if (layout_field != field->end()) {
|
||||
if (!layout_field->is_number_integer() ||
|
||||
(*layout_field != 1 && *layout_field != 2)) {
|
||||
return false;
|
||||
}
|
||||
layout = layout_field->get<int>();
|
||||
}
|
||||
*bridge_profile = FmodOutputDeviceBridgeProfile{
|
||||
*vtable_rva, *string_constructor_rva, *count_method_rva,
|
||||
*info_method_rva, *current_method_rva, *select_method_rva,
|
||||
*info_method_rva, *current_method_rva, *select_method_rva, layout,
|
||||
};
|
||||
return true;
|
||||
constexpr const char *input_fields[] = {
|
||||
"input_count_method_rva", "input_info_method_rva",
|
||||
"input_current_method_rva", "input_select_method_rva"};
|
||||
std::size_t input_fields_present = 0;
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
std::optional<std::uintptr_t> rva;
|
||||
if (!ParseOptionalRva(*field, input_fields[i], &rva))
|
||||
return false;
|
||||
if (rva) {
|
||||
++input_fields_present;
|
||||
(*bridge_profile)->input_method_rvas[i] = *rva;
|
||||
}
|
||||
}
|
||||
return input_fields_present == 0 || input_fields_present == 4;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -229,6 +251,22 @@ ProfileLookupResult FindBuildProfile(const std::string& manifest_path,
|
||||
normalized_requested);
|
||||
}
|
||||
|
||||
// Upgrade previously cached manifests for these exact, validated images.
|
||||
// This adds input routes without changing the payload's approval status.
|
||||
if (fmod_output_device_bridge &&
|
||||
!fmod_output_device_bridge->has_input_devices()) {
|
||||
auto &bridge = *fmod_output_device_bridge;
|
||||
if (normalized_profile == "ade08266c67aee88ec9c1d00902150e1684dad3a" &&
|
||||
bridge.vtable_rva == 0x6c58040 && bridge.vtable_layout_version == 1) {
|
||||
bridge.input_method_rvas = {0x32d12dc, 0x32d12ea, 0x32d13a6, 0x32d0a56};
|
||||
} else if (normalized_profile ==
|
||||
"5f0704edd9064f566ee3d6df2bd2fabbcc709f03" &&
|
||||
bridge.vtable_rva == 0x6cd3ce0 &&
|
||||
bridge.vtable_layout_version == 2) {
|
||||
bridge.input_method_rvas = {0x320ccf6, 0x320cd04, 0x320d152, 0x320bd18};
|
||||
}
|
||||
}
|
||||
|
||||
matched_profile = BuildProfile{
|
||||
version_name->get_ref<const std::string&>(),
|
||||
version_code->get<int>(),
|
||||
|
||||
@@ -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>
|
||||
@@ -17,6 +18,7 @@
|
||||
|
||||
#include "mocktail/graphics/android_vulkan_wsi_adapter.h"
|
||||
#include "mocktail/graphics/present_mode_policy.h"
|
||||
#include "mocktail/graphics/vulkan_etc2_emulation.h"
|
||||
#include "mocktail/graphics/vulkan_text_overlay_compositor.h"
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
|
||||
@@ -188,6 +190,7 @@ struct AdapterState {
|
||||
VkInstance latest_instance = VK_NULL_HANDLE;
|
||||
size_t active_instance_count = 0;
|
||||
mocktail::graphics::VulkanTextOverlayCompositor text_overlay;
|
||||
mocktail::graphics::VulkanEtc2Emulation etc2;
|
||||
std::atomic<NotePresentFn> note_present{nullptr};
|
||||
std::atomic<NoteHostPresentBeginFn> note_host_present_begin{nullptr};
|
||||
std::atomic<NoteHostPresentEndFn> note_host_present_end{nullptr};
|
||||
@@ -727,12 +730,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 ||
|
||||
@@ -959,11 +1042,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 +1080,18 @@ struct FpsWaitTrace {
|
||||
const std::uint64_t total = total_ns.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t peak = max_ns.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t count = samples.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t gap_peak =
|
||||
max_gap_ns.exchange(0, std::memory_order_relaxed);
|
||||
window_start_ns.store(start_ns, std::memory_order_relaxed);
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
std::fprintf(stderr, " [fps] %s n=%llu avg=%llu us max=%llu us\n", name,
|
||||
static_cast<unsigned long long>(count),
|
||||
std::fprintf(stderr,
|
||||
" [fps] %s n=%llu avg=%llu us max=%llu us gap_max=%llu us\n",
|
||||
name, static_cast<unsigned long long>(count),
|
||||
static_cast<unsigned long long>(total / count / 1000ULL),
|
||||
static_cast<unsigned long long>(peak / 1000ULL));
|
||||
static_cast<unsigned long long>(peak / 1000ULL),
|
||||
static_cast<unsigned long long>(gap_peak / 1000ULL));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1042,6 +1142,58 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Function>
|
||||
Function HostPhysicalDeviceProc(const char* name, const char* alias) {
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
{
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
instance = state.latest_instance;
|
||||
}
|
||||
PFN_vkVoidFunction proc = HostInstanceProc(instance, name);
|
||||
if (proc == nullptr && alias != nullptr) {
|
||||
proc = HostInstanceProc(instance, alias);
|
||||
}
|
||||
return reinterpret_cast<Function>(proc);
|
||||
}
|
||||
|
||||
bool Etc2EmulatedPhysicalDevice(VkPhysicalDevice physical_device) {
|
||||
return State().etc2.PhysicalDeviceNeedsEmulation(
|
||||
physical_device, HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
"vkGetPhysicalDeviceFeatures", nullptr));
|
||||
}
|
||||
|
||||
void ReleaseEtc2PoolCommandBuffers(VkDevice device,
|
||||
VkCommandPool command_pool) {
|
||||
std::vector<VkCommandBuffer> command_buffers;
|
||||
{
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
for (const auto& binding : state.command_buffer_bindings) {
|
||||
if (binding.device == device && binding.command_pool == command_pool) {
|
||||
command_buffers.push_back(binding.command_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (VkCommandBuffer command_buffer : command_buffers) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareEtc2Submit2(std::uint32_t submit_count,
|
||||
const VkSubmitInfo2* submits) {
|
||||
if (submits == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
for (std::uint32_t info = 0; info < submits[index].commandBufferInfoCount;
|
||||
++info) {
|
||||
State().etc2.PrepareSubmit(
|
||||
&submits[index].pCommandBufferInfos[info].commandBuffer, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -1247,7 +1399,7 @@ vkGetInstanceProcAddr(VkInstance instance, const char* name) {
|
||||
}
|
||||
if (const PFN_vkVoidFunction adapter = AdapterProc(name);
|
||||
adapter != nullptr) {
|
||||
if (IsDeviceAdapterProc(name) &&
|
||||
if ((IsDeviceAdapterProc(name) || IsEtc2PhysicalDeviceProc(name)) &&
|
||||
HostInstanceProc(instance, name) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1306,6 +1458,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 +1518,25 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
|
||||
const VkResult result =
|
||||
host_create(physical_device, &host_info, allocator, device);
|
||||
if (requested_features2 != nullptr) {
|
||||
requested_features2->features.textureCompressionETC2 =
|
||||
requested_features2_etc2;
|
||||
}
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
RegisterHostDeviceDispatch(*device, physical_device,
|
||||
host_get_device_proc_addr);
|
||||
VkPhysicalDeviceMemoryProperties memory_properties{};
|
||||
if (const auto host_memory_properties =
|
||||
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
HostInstanceProc(instance,
|
||||
"vkGetPhysicalDeviceMemoryProperties"));
|
||||
host_memory_properties != nullptr) {
|
||||
host_memory_properties(physical_device, &memory_properties);
|
||||
}
|
||||
State().etc2.RegisterDevice(*device, physical_device, etc2_emulated,
|
||||
memory_properties, host_get_device_proc_addr);
|
||||
|
||||
std::uint32_t queue_family_count = 0;
|
||||
const auto host_get_queue_families =
|
||||
@@ -1403,6 +1594,7 @@ vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* allocator) {
|
||||
const auto host_destroy = reinterpret_cast<PFN_vkDestroyDevice>(
|
||||
HostDeviceProc(device, "vkDestroyDevice"));
|
||||
State().text_overlay.DestroyDevice(device);
|
||||
State().etc2.DestroyDevice(device);
|
||||
if (host_destroy != nullptr) {
|
||||
host_destroy(device, allocator);
|
||||
}
|
||||
@@ -1650,6 +1842,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 +1899,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 +1920,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 +1936,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 +1965,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 +2002,12 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
if (submits != nullptr) {
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
State().etc2.PrepareSubmit(submits[index].pCommandBuffers,
|
||||
submits[index].commandBufferCount);
|
||||
}
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueSubmit(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
@@ -1817,6 +2024,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
@@ -1833,6 +2041,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence,
|
||||
reinterpret_cast<PFN_vkQueueSubmit2>(host_submit));
|
||||
@@ -2025,4 +2234,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,402 @@
|
||||
#include "mocktail/graphics/etc2_decoder.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<int, 8> kDistance = {3, 6, 11, 16, 23, 32, 41, 64};
|
||||
|
||||
// Indexed by (msb << 1) | lsb.
|
||||
constexpr int kEtc1Modifiers[8][4] = {
|
||||
{2, 8, -2, -8}, {5, 17, -5, -17}, {9, 29, -9, -29},
|
||||
{13, 42, -13, -42}, {18, 60, -18, -60}, {24, 80, -24, -80},
|
||||
{33, 106, -33, -106}, {47, 183, -47, -183},
|
||||
};
|
||||
|
||||
constexpr int kEacModifiers[16][8] = {
|
||||
{-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12},
|
||||
{-2, -5, -8, -13, 1, 4, 7, 12}, {-2, -4, -6, -13, 1, 3, 5, 12},
|
||||
{-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10},
|
||||
{-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10},
|
||||
{-2, -6, -8, -10, 1, 5, 7, 9}, {-2, -5, -8, -10, 1, 4, 7, 9},
|
||||
{-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9},
|
||||
{-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9},
|
||||
{-4, -6, -8, -9, 3, 5, 7, 8}, {-3, -5, -7, -9, 2, 4, 6, 8},
|
||||
};
|
||||
|
||||
std::uint8_t Clamp255(int value) {
|
||||
return static_cast<std::uint8_t>(std::clamp(value, 0, 255));
|
||||
}
|
||||
|
||||
int Expand4(int value) { return (value << 4) | value; }
|
||||
int Expand5(int value) { return (value << 3) | (value >> 2); }
|
||||
int Expand6(int value) { return (value << 2) | (value >> 4); }
|
||||
int Expand7(int value) { return (value << 1) | (value >> 6); }
|
||||
|
||||
// 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 256x256 texel image.
|
||||
constexpr std::uint64_t kBlocksPerBand = 16384;
|
||||
// Batches smaller than a 1024x1024 texel image decode on the caller.
|
||||
constexpr std::uint64_t kParallelMinimumBlocks = 65536;
|
||||
|
||||
bool ImageFits(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, const std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
if (source == nullptr || destination == nullptr || width == 0 ||
|
||||
height == 0) {
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
|
||||
const std::uint64_t blocks_high =
|
||||
(static_cast<std::uint64_t>(height) + 3) / 4;
|
||||
return blocks_wide * blocks_high * EtcBlockBytes(format) <= source_bytes &&
|
||||
static_cast<std::uint64_t>(width) * height *
|
||||
EtcDecodedTexelBytes(format) <=
|
||||
destination_bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
return DecodeEtcImageBlockRows(format, source, source_bytes, width, height, 0,
|
||||
(height + 3) / 4, destination,
|
||||
destination_bytes);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct DecodeBand {
|
||||
EtcDecodeJob* job;
|
||||
std::uint32_t first_block_row;
|
||||
std::uint32_t block_row_count;
|
||||
};
|
||||
|
||||
struct DecodeBandQueue {
|
||||
std::vector<DecodeBand> bands;
|
||||
std::atomic<std::size_t> next{0};
|
||||
|
||||
void Run() {
|
||||
for (;;) {
|
||||
const std::size_t index = next.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index >= bands.size()) {
|
||||
return;
|
||||
}
|
||||
const DecodeBand& band = bands[index];
|
||||
const EtcDecodeJob& job = *band.job;
|
||||
DecodeEtcImageBlockRows(job.format, job.source, job.source_bytes,
|
||||
job.width, job.height, band.first_block_row,
|
||||
band.block_row_count, job.destination,
|
||||
job.destination_bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void* RunDecodeBandQueue(void* queue) {
|
||||
static_cast<DecodeBandQueue*>(queue)->Run();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
unsigned worker_count) {
|
||||
if (jobs == nullptr) {
|
||||
return;
|
||||
}
|
||||
DecodeBandQueue queue;
|
||||
std::vector<DecodeBand>& bands = queue.bands;
|
||||
std::uint64_t total_blocks = 0;
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
EtcDecodeJob& job = jobs[index];
|
||||
job.ok = ImageFits(job.format, job.source, job.source_bytes, job.width,
|
||||
job.height, job.destination, job.destination_bytes);
|
||||
if (!job.ok) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t blocks_wide = (job.width + 3) / 4;
|
||||
const std::uint32_t blocks_high = (job.height + 3) / 4;
|
||||
const std::uint32_t rows_per_band = static_cast<std::uint32_t>(
|
||||
std::max<std::uint64_t>(1, kBlocksPerBand / blocks_wide));
|
||||
for (std::uint32_t row = 0; row < blocks_high; row += rows_per_band) {
|
||||
bands.push_back(
|
||||
{&job, row, std::min(rows_per_band, blocks_high - row)});
|
||||
}
|
||||
total_blocks += static_cast<std::uint64_t>(blocks_wide) * blocks_high;
|
||||
}
|
||||
|
||||
const std::size_t threads =
|
||||
total_blocks < kParallelMinimumBlocks
|
||||
? 1
|
||||
: std::min<std::size_t>(std::max(worker_count, 1U), bands.size());
|
||||
std::vector<pthread_t> workers;
|
||||
workers.reserve(threads > 0 ? threads - 1 : 0);
|
||||
for (std::size_t index = 1; index < threads; ++index) {
|
||||
pthread_t worker;
|
||||
// The caller decodes whatever workers that failed to start leave behind.
|
||||
if (pthread_create(&worker, nullptr, RunDecodeBandQueue, &queue) != 0) {
|
||||
break;
|
||||
}
|
||||
workers.push_back(worker);
|
||||
}
|
||||
queue.Run();
|
||||
for (pthread_t worker : workers) {
|
||||
pthread_join(worker, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint32_t first_block_row,
|
||||
std::uint32_t block_row_count,
|
||||
std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
if (!ImageFits(format, source, source_bytes, width, height, destination,
|
||||
destination_bytes)) {
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
|
||||
const std::uint64_t blocks_high =
|
||||
(static_cast<std::uint64_t>(height) + 3) / 4;
|
||||
if (static_cast<std::uint64_t>(first_block_row) + block_row_count >
|
||||
blocks_high) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t texel_bytes = EtcDecodedTexelBytes(format);
|
||||
const std::size_t block_bytes = EtcBlockBytes(format);
|
||||
|
||||
std::array<std::uint8_t, 64> rgba{};
|
||||
const std::uint64_t end_block_row =
|
||||
static_cast<std::uint64_t>(first_block_row) + block_row_count;
|
||||
for (std::uint64_t by = first_block_row; by < end_block_row; ++by) {
|
||||
for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) {
|
||||
const std::uint8_t* block =
|
||||
source + (by * blocks_wide + bx) * block_bytes;
|
||||
if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) {
|
||||
DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba);
|
||||
} else if (format == EtcFormat::kEtc2Rgba8) {
|
||||
DecodeRgbBlock(block + 8, false, &rgba);
|
||||
}
|
||||
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
|
||||
const int texel = static_cast<int>(x * 4 + y);
|
||||
std::uint8_t* out =
|
||||
destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes;
|
||||
switch (format) {
|
||||
case EtcFormat::kEtc2Rgb8:
|
||||
case EtcFormat::kEtc2Rgb8A1:
|
||||
case EtcFormat::kEtc2Rgba8:
|
||||
std::copy_n(rgba.data() + texel * 4, 4, out);
|
||||
if (format == EtcFormat::kEtc2Rgba8) {
|
||||
const int alpha =
|
||||
block[0] + EacModifier(block, texel) * (block[1] >> 4);
|
||||
out[3] = Clamp255(alpha);
|
||||
}
|
||||
break;
|
||||
case EtcFormat::kEacR11:
|
||||
case EtcFormat::kEacR11Signed:
|
||||
case EtcFormat::kEacRg11:
|
||||
case EtcFormat::kEacRg11Signed: {
|
||||
const bool is_signed = format == EtcFormat::kEacR11Signed ||
|
||||
format == EtcFormat::kEacRg11Signed;
|
||||
const int channels = texel_bytes / 2;
|
||||
for (int channel = 0; channel < channels; ++channel) {
|
||||
const std::uint16_t value =
|
||||
DecodeR11(block + channel * 8, texel, is_signed);
|
||||
out[channel * 2] = static_cast<std::uint8_t>(value);
|
||||
out[channel * 2 + 1] = static_cast<std::uint8_t>(value >> 8);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
@@ -0,0 +1,375 @@
|
||||
#include "mocktail/graphics/gles_text_overlay_compositor.h"
|
||||
|
||||
#include <GLES3/gl3.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t kMaximumOverlayBytes = 64ULL * 1024ULL * 1024ULL;
|
||||
|
||||
#define OVERLAY_GL_FUNCTIONS(F) \
|
||||
F(PFNGLACTIVETEXTUREPROC, ActiveTexture) \
|
||||
F(PFNGLATTACHSHADERPROC, AttachShader) \
|
||||
F(PFNGLBINDBUFFERPROC, BindBuffer) \
|
||||
F(PFNGLBINDFRAMEBUFFERPROC, BindFramebuffer) \
|
||||
F(PFNGLBINDSAMPLERPROC, BindSampler) \
|
||||
F(PFNGLBINDTEXTUREPROC, BindTexture) \
|
||||
F(PFNGLBINDVERTEXARRAYPROC, BindVertexArray) \
|
||||
F(PFNGLBLENDEQUATIONSEPARATEPROC, BlendEquationSeparate) \
|
||||
F(PFNGLBLENDFUNCSEPARATEPROC, BlendFuncSeparate) \
|
||||
F(PFNGLCOLORMASKPROC, ColorMask) \
|
||||
F(PFNGLCOMPILESHADERPROC, CompileShader) \
|
||||
F(PFNGLCREATEPROGRAMPROC, CreateProgram) \
|
||||
F(PFNGLCREATESHADERPROC, CreateShader) \
|
||||
F(PFNGLDELETEPROGRAMPROC, DeleteProgram) \
|
||||
F(PFNGLDELETESHADERPROC, DeleteShader) \
|
||||
F(PFNGLDELETETEXTURESPROC, DeleteTextures) \
|
||||
F(PFNGLDELETEVERTEXARRAYSPROC, DeleteVertexArrays) \
|
||||
F(PFNGLDISABLEPROC, Disable) \
|
||||
F(PFNGLDRAWARRAYSPROC, DrawArrays) \
|
||||
F(PFNGLDRAWBUFFERSPROC, DrawBuffers) \
|
||||
F(PFNGLENABLEPROC, Enable) \
|
||||
F(PFNGLGENTEXTURESPROC, GenTextures) \
|
||||
F(PFNGLGENVERTEXARRAYSPROC, GenVertexArrays) \
|
||||
F(PFNGLGETBOOLEANVPROC, GetBooleanv) \
|
||||
F(PFNGLGETINTEGERVPROC, GetIntegerv) \
|
||||
F(PFNGLGETPROGRAMINFOLOGPROC, GetProgramInfoLog) \
|
||||
F(PFNGLGETPROGRAMIVPROC, GetProgramiv) \
|
||||
F(PFNGLGETSHADERINFOLOGPROC, GetShaderInfoLog) \
|
||||
F(PFNGLGETSHADERIVPROC, GetShaderiv) \
|
||||
F(PFNGLGETUNIFORMLOCATIONPROC, GetUniformLocation) \
|
||||
F(PFNGLISENABLEDPROC, IsEnabled) \
|
||||
F(PFNGLLINKPROGRAMPROC, LinkProgram) \
|
||||
F(PFNGLPIXELSTOREIPROC, PixelStorei) \
|
||||
F(PFNGLSHADERSOURCEPROC, ShaderSource) \
|
||||
F(PFNGLTEXIMAGE2DPROC, TexImage2D) \
|
||||
F(PFNGLTEXPARAMETERIPROC, TexParameteri) \
|
||||
F(PFNGLUNIFORM1IPROC, Uniform1i) \
|
||||
F(PFNGLUNIFORM4FPROC, Uniform4f) \
|
||||
F(PFNGLUSEPROGRAMPROC, UseProgram) \
|
||||
F(PFNGLVIEWPORTPROC, Viewport)
|
||||
|
||||
struct Gl {
|
||||
#define DECLARE_GL(type, name) type name = nullptr;
|
||||
OVERLAY_GL_FUNCTIONS(DECLARE_GL)
|
||||
#undef DECLARE_GL
|
||||
|
||||
bool Load() {
|
||||
#define LOAD_GL(type, name) \
|
||||
name = reinterpret_cast<type>(SDL_GL_GetProcAddress("gl" #name)); \
|
||||
if (name == nullptr) \
|
||||
return false;
|
||||
OVERLAY_GL_FUNCTIONS(LOAD_GL)
|
||||
#undef LOAD_GL
|
||||
return true;
|
||||
}
|
||||
};
|
||||
#undef OVERLAY_GL_FUNCTIONS
|
||||
|
||||
constexpr GLenum kCapabilities[] = {GL_BLEND,
|
||||
GL_CULL_FACE,
|
||||
GL_DEPTH_TEST,
|
||||
GL_STENCIL_TEST,
|
||||
GL_SCISSOR_TEST,
|
||||
GL_RASTERIZER_DISCARD,
|
||||
GL_SAMPLE_ALPHA_TO_COVERAGE,
|
||||
GL_SAMPLE_COVERAGE};
|
||||
constexpr GLenum kUnpackParameters[] = {
|
||||
GL_UNPACK_ALIGNMENT, GL_UNPACK_ROW_LENGTH, GL_UNPACK_SKIP_ROWS,
|
||||
GL_UNPACK_SKIP_PIXELS};
|
||||
|
||||
// Roblox caches GL state. Every binding and state changed here must be restored
|
||||
// even if a new overlay revision disappears while it is being copied.
|
||||
struct SavedGlState {
|
||||
explicit SavedGlState(Gl &api) : gl(api) {
|
||||
gl.GetIntegerv(GL_CURRENT_PROGRAM, &program);
|
||||
gl.GetIntegerv(GL_VERTEX_ARRAY_BINDING, &vertex_array);
|
||||
gl.GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &framebuffer);
|
||||
gl.GetIntegerv(GL_VIEWPORT, viewport.data());
|
||||
gl.GetIntegerv(GL_ACTIVE_TEXTURE, &active_texture);
|
||||
gl.ActiveTexture(GL_TEXTURE0);
|
||||
gl.GetIntegerv(GL_TEXTURE_BINDING_2D, &texture);
|
||||
gl.GetIntegerv(GL_SAMPLER_BINDING, &sampler);
|
||||
gl.GetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &unpack_buffer);
|
||||
for (std::size_t i = 0; i < unpack.size(); ++i) {
|
||||
gl.GetIntegerv(kUnpackParameters[i], &unpack[i]);
|
||||
}
|
||||
gl.GetIntegerv(GL_BLEND_SRC_RGB, &blend_src_rgb);
|
||||
gl.GetIntegerv(GL_BLEND_DST_RGB, &blend_dst_rgb);
|
||||
gl.GetIntegerv(GL_BLEND_SRC_ALPHA, &blend_src_alpha);
|
||||
gl.GetIntegerv(GL_BLEND_DST_ALPHA, &blend_dst_alpha);
|
||||
gl.GetIntegerv(GL_BLEND_EQUATION_RGB, &blend_equation_rgb);
|
||||
gl.GetIntegerv(GL_BLEND_EQUATION_ALPHA, &blend_equation_alpha);
|
||||
gl.GetBooleanv(GL_COLOR_WRITEMASK, color_mask.data());
|
||||
for (std::size_t i = 0; i < enabled.size(); ++i) {
|
||||
enabled[i] = gl.IsEnabled(kCapabilities[i]);
|
||||
}
|
||||
gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
gl.GetIntegerv(GL_DRAW_BUFFER0, &backbuffer_draw_buffer);
|
||||
}
|
||||
|
||||
~SavedGlState() {
|
||||
const GLenum draw_buffer = static_cast<GLenum>(backbuffer_draw_buffer);
|
||||
gl.DrawBuffers(1, &draw_buffer);
|
||||
gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
gl.Viewport(viewport[0], viewport[1], viewport[2], viewport[3]);
|
||||
gl.UseProgram(program);
|
||||
gl.BindVertexArray(vertex_array);
|
||||
gl.BindTexture(GL_TEXTURE_2D, texture);
|
||||
gl.BindSampler(0, sampler);
|
||||
gl.ActiveTexture(active_texture);
|
||||
gl.BindBuffer(GL_PIXEL_UNPACK_BUFFER, unpack_buffer);
|
||||
for (std::size_t i = 0; i < unpack.size(); ++i) {
|
||||
gl.PixelStorei(kUnpackParameters[i], unpack[i]);
|
||||
}
|
||||
gl.BlendFuncSeparate(blend_src_rgb, blend_dst_rgb, blend_src_alpha,
|
||||
blend_dst_alpha);
|
||||
gl.BlendEquationSeparate(blend_equation_rgb, blend_equation_alpha);
|
||||
gl.ColorMask(color_mask[0], color_mask[1], color_mask[2], color_mask[3]);
|
||||
for (std::size_t i = 0; i < enabled.size(); ++i) {
|
||||
(enabled[i] ? gl.Enable : gl.Disable)(kCapabilities[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Gl ≷
|
||||
GLint program = 0, vertex_array = 0, framebuffer = 0;
|
||||
GLint active_texture = GL_TEXTURE0, texture = 0, sampler = 0;
|
||||
GLint unpack_buffer = 0, backbuffer_draw_buffer = GL_BACK;
|
||||
GLint blend_src_rgb = 0, blend_dst_rgb = 0;
|
||||
GLint blend_src_alpha = 0, blend_dst_alpha = 0;
|
||||
GLint blend_equation_rgb = 0, blend_equation_alpha = 0;
|
||||
std::array<GLint, 4> viewport{}, unpack{};
|
||||
std::array<GLboolean, 4> color_mask{};
|
||||
std::array<GLboolean, sizeof(kCapabilities) / sizeof(kCapabilities[0])>
|
||||
enabled{};
|
||||
};
|
||||
|
||||
struct PixelCopy {
|
||||
explicit PixelCopy(std::size_t bytes) : pixels(bytes) {}
|
||||
~PixelCopy() {
|
||||
volatile std::uint8_t *bytes = pixels.data();
|
||||
for (std::size_t i = 0; i < pixels.size(); ++i)
|
||||
bytes[i] = 0;
|
||||
}
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
bool ValidFrame(const MocktailTextOverlayFrameInfo &frame) {
|
||||
const std::uint64_t row_bytes = std::uint64_t{frame.width} * 4;
|
||||
const std::uint64_t bytes = row_bytes * frame.height;
|
||||
return frame.abi_version == MocktailTextOverlayFrameInfo::kAbiVersion &&
|
||||
frame.coordinate_width != 0 && frame.coordinate_height != 0 &&
|
||||
frame.width != 0 && frame.height != 0 &&
|
||||
row_bytes <= kMaximumOverlayBytes && bytes <= kMaximumOverlayBytes &&
|
||||
frame.row_bytes == row_bytes && frame.rgba_bytes == bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GlesTextOverlayCompositor::Impl {
|
||||
explicit Impl(GlesTextOverlaySource input) : source(input) {}
|
||||
~Impl() {
|
||||
if (context != nullptr && SDL_GL_GetCurrentContext() == context) {
|
||||
if (texture != 0)
|
||||
gl.DeleteTextures(1, &texture);
|
||||
if (vertex_array != 0)
|
||||
gl.DeleteVertexArrays(1, &vertex_array);
|
||||
if (program != 0)
|
||||
gl.DeleteProgram(program);
|
||||
}
|
||||
}
|
||||
|
||||
bool Fail(const char *message) {
|
||||
if (!failure_logged) {
|
||||
std::fprintf(stderr, " [gles-overlay] %s\n", message);
|
||||
failure_logged = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint Compile(GLenum type, const char *code) {
|
||||
const GLuint shader = gl.CreateShader(type);
|
||||
if (shader == 0)
|
||||
return 0;
|
||||
gl.ShaderSource(shader, 1, &code, nullptr);
|
||||
gl.CompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
gl.GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_TRUE)
|
||||
return shader;
|
||||
char log[1024]{};
|
||||
gl.GetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
Fail(log);
|
||||
gl.DeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool CreateResources() {
|
||||
constexpr const char *vertex_source = R"(#version 300 es
|
||||
precision highp float;
|
||||
uniform vec4 bounds;
|
||||
out vec2 uv;
|
||||
void main() {
|
||||
vec2 corners[4] = vec2[4](vec2(0, 0), vec2(1, 0),
|
||||
vec2(0, 1), vec2(1, 1));
|
||||
uv = corners[gl_VertexID];
|
||||
gl_Position = vec4(mix(bounds.xy, bounds.zw, uv), 0, 1);
|
||||
}
|
||||
)";
|
||||
constexpr const char *fragment_source = R"(#version 300 es
|
||||
precision mediump float;
|
||||
uniform sampler2D image;
|
||||
in vec2 uv;
|
||||
out vec4 color;
|
||||
void main() { color = texture(image, uv); }
|
||||
)";
|
||||
const GLuint vertex = Compile(GL_VERTEX_SHADER, vertex_source);
|
||||
const GLuint fragment = Compile(GL_FRAGMENT_SHADER, fragment_source);
|
||||
if (vertex == 0 || fragment == 0) {
|
||||
if (vertex != 0)
|
||||
gl.DeleteShader(vertex);
|
||||
if (fragment != 0)
|
||||
gl.DeleteShader(fragment);
|
||||
return false;
|
||||
}
|
||||
program = gl.CreateProgram();
|
||||
if (program != 0) {
|
||||
gl.AttachShader(program, vertex);
|
||||
gl.AttachShader(program, fragment);
|
||||
gl.LinkProgram(program);
|
||||
}
|
||||
gl.DeleteShader(vertex);
|
||||
gl.DeleteShader(fragment);
|
||||
GLint linked = GL_FALSE;
|
||||
if (program != 0)
|
||||
gl.GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
char log[1024] = "unable to create the GLES text program";
|
||||
if (program != 0)
|
||||
gl.GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
return Fail(log);
|
||||
}
|
||||
bounds_location = gl.GetUniformLocation(program, "bounds");
|
||||
image_location = gl.GetUniformLocation(program, "image");
|
||||
gl.GenVertexArrays(1, &vertex_array);
|
||||
gl.GenTextures(1, &texture);
|
||||
gl.GetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
|
||||
return vertex_array != 0 && texture != 0 && bounds_location >= 0 &&
|
||||
image_location >= 0;
|
||||
}
|
||||
|
||||
GlesTextOverlaySource source;
|
||||
Gl gl;
|
||||
SDL_GLContext context = nullptr;
|
||||
GLuint program = 0, vertex_array = 0, texture = 0;
|
||||
GLint bounds_location = -1, image_location = -1, max_texture_size = 0;
|
||||
bool initialized = false, failed = false, failure_logged = false;
|
||||
bool uploaded = false, ready_logged = false;
|
||||
std::uint64_t revision = 0;
|
||||
std::uint32_t width = 0, height = 0;
|
||||
};
|
||||
|
||||
GlesTextOverlayCompositor::GlesTextOverlayCompositor(
|
||||
GlesTextOverlaySource source)
|
||||
: impl_(std::make_unique<Impl>(source)) {}
|
||||
GlesTextOverlayCompositor::~GlesTextOverlayCompositor() = default;
|
||||
|
||||
bool GlesTextOverlayCompositor::Draw(SDL_Window *window) {
|
||||
Impl &state = *impl_;
|
||||
const GlesTextOverlaySource &source = state.source;
|
||||
if (source.may_present != nullptr && !source.may_present())
|
||||
return true;
|
||||
if (source.query == nullptr || source.copy == nullptr)
|
||||
return true;
|
||||
MocktailTextOverlayFrameInfo frame{};
|
||||
if (!source.query(&frame) || frame.visible == 0)
|
||||
return true;
|
||||
if (!ValidFrame(frame))
|
||||
return state.Fail("invalid text overlay frame");
|
||||
int pixel_width = 0, pixel_height = 0;
|
||||
if (window == nullptr ||
|
||||
!SDL_GetWindowSizeInPixels(window, &pixel_width, &pixel_height) ||
|
||||
pixel_width <= 0 || pixel_height <= 0)
|
||||
return false;
|
||||
if (state.failed)
|
||||
return false;
|
||||
if (!state.initialized) {
|
||||
state.context = SDL_GL_GetCurrentContext();
|
||||
if (state.context == nullptr || !state.gl.Load()) {
|
||||
state.failed = true;
|
||||
return state.Fail("OpenGL ES 3.0 functions are unavailable");
|
||||
}
|
||||
} else if (SDL_GL_GetCurrentContext() != state.context) {
|
||||
return state.Fail("text overlay called with a different GL context");
|
||||
}
|
||||
Gl &gl = state.gl;
|
||||
SavedGlState saved(gl);
|
||||
if (!state.initialized) {
|
||||
if (!state.CreateResources()) {
|
||||
state.failed = true;
|
||||
return state.Fail("unable to initialize the GLES text overlay");
|
||||
}
|
||||
state.initialized = true;
|
||||
}
|
||||
if (frame.width > static_cast<std::uint32_t>(state.max_texture_size) ||
|
||||
frame.height > static_cast<std::uint32_t>(state.max_texture_size)) {
|
||||
return state.Fail("text overlay exceeds GL_MAX_TEXTURE_SIZE");
|
||||
}
|
||||
gl.BindTexture(GL_TEXTURE_2D, state.texture);
|
||||
gl.BindSampler(0, 0);
|
||||
if (!state.uploaded || state.revision != frame.revision ||
|
||||
state.width != frame.width || state.height != frame.height) {
|
||||
PixelCopy copy(static_cast<std::size_t>(frame.rgba_bytes));
|
||||
if (!source.copy(frame.revision, copy.pixels.data(), copy.pixels.size())) {
|
||||
return false;
|
||||
}
|
||||
gl.BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
gl.PixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
gl.PixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
gl.PixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, frame.width, frame.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, copy.pixels.data());
|
||||
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
state.revision = frame.revision;
|
||||
state.width = frame.width;
|
||||
state.height = frame.height;
|
||||
state.uploaded = true;
|
||||
}
|
||||
for (GLenum capability : kCapabilities)
|
||||
gl.Disable(capability);
|
||||
gl.Enable(GL_BLEND);
|
||||
gl.BlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
|
||||
gl.BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
|
||||
GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl.ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
const GLenum back = GL_BACK;
|
||||
gl.DrawBuffers(1, &back);
|
||||
gl.Viewport(0, 0, pixel_width, pixel_height);
|
||||
gl.UseProgram(state.program);
|
||||
gl.BindVertexArray(state.vertex_array);
|
||||
gl.Uniform1i(state.image_location, 0);
|
||||
const double x_scale = 2.0 / frame.coordinate_width;
|
||||
const double y_scale = 2.0 / frame.coordinate_height;
|
||||
gl.Uniform4f(state.bounds_location, frame.x * x_scale - 1.0,
|
||||
1.0 - frame.y * y_scale,
|
||||
(static_cast<double>(frame.x) + frame.width) * x_scale - 1.0,
|
||||
1.0 - (static_cast<double>(frame.y) + frame.height) * y_scale);
|
||||
gl.DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
if (!state.ready_logged) {
|
||||
std::fprintf(stderr,
|
||||
" [gles-overlay] same-surface TextBox compositor ready\n");
|
||||
state.ready_logged = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics
|
||||