Compare commits

...
75 Commits
Author SHA1 Message Date
vii 8bde1a1c0d Merge pull request #1 from CoderDayton/release/0.3.0
Release 0.3.0
2026-09-18 19:19:48 -05:00
vii 60e9c5c3ef docs(changelog): release 0.3.0 2026-09-18 19:06:08 -05:00
vii d76e1b6f54 docs(roadmap): move profiling and Discord presence to shipped, add first-load stutter 2026-09-18 19:06:02 -05:00
vii 33c06c063e docs(changelog): note the Sober-style Discord card defaults 2026-09-18 18:54:57 -05:00
vii 9e8afb233a feat(discord-rpc): show the Roblox badge as the default small image
Defaults images.small to the roblox_small asset on the bundled Discord
application, with "Roblox" as its hover text.
2026-09-18 18:54:12 -05:00
vii 7fda1eb239 feat(discord-rpc): show the experience creator and title the card Roblox
Default title is Roblox, details "Playing {place_name}", and state
"by {creator_name}". The creator comes from the games API response
already fetched for the place name. A text field that expands an
empty placeholder is left out, so the state never shows a bare "by".
2026-09-18 18:45:11 -05:00
vii f128e28b83 docs(perf): describe the main-thread pump rest modes and GameMode's governor permission 2026-09-18 18:26:32 -05:00
vii 718e67f9e1 perf(runtime): rest the engine pump between polls instead of spinning
The main thread polls nativeCallMessagesFromMainThread 146k times a second
with nothing to do. Between polls it now rests 100 us: nanosleep when the
governor is performance, TPAUSE (C0.2) when the CPU has WAITPKG, a spin
otherwise. Sleeping under a power-saving governor lets the render thread's
core clock down and drops frames, which is why TPAUSE is the default there.
MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin overrides. The profile trace
gains a slice for pumps that ran a message.
2026-09-18 15:36:54 -05:00
vii c6c354fdb1 perf(graphics): emit ETC2 uploads on the decode pool and stop the trace writer after a failed write
EmitPass reuses the decoder's persistent workers through RunOnDecodeWorkers
instead of creating and joining threads per batch. A failed trace write stops
the writer thread rather than leaving it ticking every 250 ms.
2026-09-18 12:42:36 -05:00
vii 25930d14ff fix(graphics, runtime): honor ETC2 source stride and cut main-loop overhead
ETC2 uploads read bufferRowLength/bufferImageHeight instead of assuming
packed rows. MakeFileObject retains the path field. The main-thread pump
caches its JNI attach, but not the GetJNIEnv fallback, so a VM swap retries.
An idle launch-broker drain skips both locks.
2026-09-18 12:21:29 -05:00
vii 65030d8df1 fix(input): hold the pointer position during relative mouse capture
Relative-mode motion carries zero absolute coordinates. The router used
to accumulate the deltas into its pointer position, so a long camera turn
parked the reported position at the viewport edge and Roblox drew its
cursor there when the capture ended. The position now stays where it was
captured while deltas pass through. Zero-coordinate wheel and button
events keep it too, and an unknown position defaults to the viewport
center.
2026-09-18 02:51:03 -05:00
vii 0be5e808c5 fix(runtime): restore the affinity of every thread, not just the caller
sched_setaffinity binds one thread, so the fallback that undid a GameMode pin
only freed the thread that called Start(); the worker threads the fix exists to
protect stayed on the daemon's cores. Walk /proc/self/task instead, and treat a
readdir failure partway through as a failed restore rather than a finished one.

Also stop chrome_trace_writer emitting a valueless "ts" on to_chars failure, or
a bare inf/nan, either of which costs the whole trace instead of one field.
2026-09-18 02:00:58 -05:00
vii 1b030a57eb perf(runtime, graphics): release core-pinning gamemode and parallelize texture uploads
Release the GameMode request when the daemon narrows the CPU mask; the pin
concentrates Roblox's worker pool and is re-applied every few seconds, so
ending the request is the only thing that lifts it. `gamemode: on` keeps it.

Emit texture uploads on worker threads instead of serially inside
vkQueueSubmit, in two passes so mip 0 lands before the levels scaled from it.

Decode ETC2 through a persistent pool and lower the parallel floor to one
128x128 image, so small batches stop running single-threaded.

Document all three in docs/PERFORMANCE.md.
2026-09-18 00:47:57 -05:00
vii 2f5527b8b5 perf(libc_shim): cut path translation cost per file call
Mappings are kept sorted longest prefix first, unmapped paths skip allocation, and the path trace switch is read once.
2026-09-16 21:22:31 -05:00
vii 579cedbf1d perf(textures): pool ETC2 staging buffers and speed up resampling
Staging buffers stay mapped and are reused instead of allocated per upload. Box resampling precomputes column spans and copies repeated rows, with identical output.
2026-09-16 21:22:31 -05:00
vii a7b90e5919 fix(jnivm): stop leaking JNI handles until the table fills
FindClass and GetObjectClass reuse one handle per class, detached threads release their local refs, and local ref deletes and singleton checks no longer scan. A full table logs the classes holding handles. Stale handles above slot 4096 no longer read as C strings.
2026-09-16 21:22:31 -05:00
vii 3bba795ea3 feat(profile): add --profile Chrome trace of Vulkan adapter work
Records presents, GPU waits, submits, ETC2 decode, and pipeline and shader creation for ui.perfetto.dev. Adds a benchmark guide and a trace summary script.
2026-09-16 21:22:30 -05:00
vii 5ce4487d4f feat(discord): add custom presence title, state, and icons
text.title sets the activity name, images.* set the large and small icons and hover text, and text.state may be empty to hide it. Text and image fields expand {place_name} and {place_icon}; fields that render empty are omitted. show_place_name: false now also hides the place thumbnail.
2026-09-16 11:45:30 -05:00
vii c544389dd0 fix(textures): make the crosshair arms symmetric
Move the right and bottom arms out 1px so every arm reaches 12px from the center pixel with a 4px gap.
2026-09-16 11:11:06 -05:00
vii bdb84142d7 docs(textures): ship a crosshair override people can copy in
Adds textures/ with the crosshair PNG named by its ETC2 hash and a README
covering install, naming and how to dump hashes for other textures. Nothing
installs or loads it; copying into ~/.config/mocktail/textures is opt-in.
2026-09-16 04:53:53 -05:00
vii 40698efb7d docs(changelog): release 0.2.0 2026-09-16 03:25:39 -05:00
vii 3d2a2b3d98 docs(changelog): record the input, storage, and packaging changes 2026-09-16 02:56:11 -05:00
vii 78a314b761 ci(packages): reduce to a branded nightly AppImage
The workflow built DEB, RPM and pacman packages on every push to main and
every pull request, then published them as "Mocktail continuous" with
assets named Mocktail-* and a continuous tag pushed as komaruworld.

Nightcap ships the AppImage and the Flatpak, so the three native package
jobs are removed along with the APT and RPM repository step, which could
not work here regardless: it downloads .deb and .rpm from this repository's
latest release, and the release workflow publishes neither.

What remains runs nightly and on demand rather than per commit, skips the
build when the continuous tag already points at HEAD, publishes
Nightcap-nightly-x86_64.AppImage with a checksum beside it, and tags as
github-actions[bot]. A manual run from another branch builds and
smoke-tests without touching the release.
2026-09-16 02:51:20 -05:00
vii 2d1dd3788b ci(release): pass GH_TOKEN to the AppImage dependency installer
install_anylinux_dependencies.sh now queries the GitHub API for the rolling
release's asset IDs and digests. Without a token it uses the unauthenticated
rate limit, which shared Actions runner IPs exhaust. packages.yml already
passes the token; the release workflow runs the same script in the same
container and did not.
2026-09-16 02:42:06 -05:00
vii 56d3160641 Merge upstream komaruworld/mocktail into main
Takes upstream's AnyLinux dependency rework: the checked-in digest list is
replaced by a name list, and the installer snapshots GitHub asset IDs and
SHA256 digests together at build time, verifying each archive before
installation. This removes the manual hash refresh a998754 had to do.

Keeps Nightcap's FAQ.md over the upstream copy; ours is the same document
with Nightcap paths, RPC on by default, and the Discord play time section.

Keeps TextureManager2 enabled rather than upstream's revert to TM1. Their
mip regression is described on the mobile MipPackStream path, and this fork
already reports real host memory and a PC device profile. Hours of play
show no texture problem, so the workaround is not adopted without evidence
it is needed here.
2026-09-16 02:37:08 -05:00
vii 5db99041bc docs: rewrite the input roadmap items and trim the donation list
Discord Rich Presence item 1 is custom presence fields; the private-server
join item is dropped. The profiling item names the Chrome trace format
instead of Perfetto.

Item 2 covered hot-plug and input release on disconnect, which already
work, so it is now only the button glyph gap. Raw mouse and the pointer
fix move to Shipped.

Drops the BCH donation address from the README.
2026-09-16 02:29:07 -05:00
vii 9ec56338c3 test: fix umask, dash, and scheduling assumptions in three suites
native_update_test wrote approval artifacts with the process umask, which
candidate_approval rejects when group writable. The helper now chmods 0600.

webview_helper_launcher_test drove its fake helper with `dd <&198` under
#!/bin/sh. dash rejects multi-digit file descriptors, so the script never
ran and the packet file stayed empty. It now reads the control socket in
python3, as the adjacent tests in the file already do.

audio_foundation_test asserted the poller thread had run before stopping
it, without waiting for it to be scheduled; it failed about one run in two.
It now waits for the first poll under a bounded deadline.
2026-09-16 02:28:09 -05:00
vii 894ebf8eb7 fix(services): create BrowserTracker storage directories private and usable
The service created its own credential directory with create_directories
and then refused it, because IsPrivateDirectory rejects a group-writable
path and mkdir masks its mode with the process umask. On a umask of 002
that failed every first run; a umask carrying owner bits left the
directory unwritable by its owner instead.

CreatePrivateDirectories creates each missing component and tightens it to
0700 through a descriptor opened O_DIRECTORY | O_NOFOLLOW. Components that
already exist keep their mode and are still judged by IsPrivateDirectory.
2026-09-16 02:28:02 -05:00
vii 6bec8a6c5c feat(input): add input.raw_mouse and keep the pointer inside the viewport
Relative-mode motion reports zero absolute coordinates, so the router
accumulates deltas. The accumulator was never clamped, only the copy sent
to the engine, so a long turn pushed it far outside the viewport and every
reversal was swallowed until the pointer travelled all the way back.

input.raw_mouse sends look deltas in host units instead of guest surface
units. Those differ only when the desktop display scale and the window's
pixel density disagree, as under fractional scaling. Default off.
2026-09-16 02:27:56 -05:00
vii b9e7bc761b docs(roadmap): swap RPC join items for custom presence fields
Name the Chrome trace format for --profile instead of Perfetto.
2026-09-15 22:53:19 -05:00
komaruworld b54c896498 ci hate me? 2026-09-15 19:52:24 +03:00
komaruworld 1cd7b7db8f Fuck ci 2026-09-15 19:42:56 +03:00
vii f87aaca2fb feat(discord): enable Rich Presence by default under Nightcap's own application
Adds Terms and Privacy pages to the site for the Discord application.
2026-09-15 10:34:50 -05:00
vii 901ba615ab fix(runtime): keep fflags.json values over the performance preset instead of failing launch 2026-09-15 10:34:50 -05:00
komaruworld cdbf9ac1eb revert commit fdce534 2026-09-15 18:26:13 +03:00
vii f098bc1b5d docs(faq): add FAQ from upstream with Nightcap paths and link it
Discord RPC config key, log locations, and the upstream vr branch.
2026-09-15 09:12:38 -05:00
vii e1f7c8c06c docs(changelog): add CHANGELOG.md and use it for GitHub release notes
Release and Flatpak workflows take the release body from the tag's
changelog section and fail early if it is missing.
2026-09-15 08:23:11 -05:00
vii a9987546d5 fix(packaging): refresh AnyLinux debloated package hashes to 2026-09-15
pkgforge rebuilt six of the eight rolling packages, so the AppImage build
failed on the ffmpeg-mini checksum.
2026-09-15 08:10:55 -05:00
vii 6a39887e8f docs(roadmap): add Nightcap roadmap and link it from the README 2026-09-15 08:04:17 -05:00
vii 349e314850 feat(packaging): rename app ID to io.github.CoderDayton.nightcap and publish Flatpak
Rename the desktop entry, metainfo, icons, Flatpak manifest, and SDL app
identity so Nightcap installs side by side with upstream. Build a signed
Flatpak on v* tags, publish the OSTree repo and landing page to GitHub
Pages, and attach the bundle to the release. Release creation tolerates
the AppImage and Flatpak workflows racing on the same tag.
2026-09-15 07:10:26 -05:00
komaruworld f383fbbff4 update faq.md 2026-09-15 12:08:18 +03:00
vii b1112d8cdc feat(graphics): default small texture upscale to 4
Unset or non-numeric MOCKTAIL_SMALL_TEXTURE_UPSCALE now means 4, so the
upscale applies on every launch path. Drop the variable from the desktop
entry Exec line.
2026-09-15 03:49:13 -05:00
vii 27c383a5d9 feat(branding): recolor app icon to the Nightcap purple palette
Indigo #2F2A6B, violet #6C4FD1, lavender #A78BFA. PNGs rendered with resvg.
2026-09-14 22:26:10 -05:00
vii 7cf24feef6 feat(branding): rename fork to Nightcap and add AppImage release workflow
Show Nightcap as the app name in the desktop entry, window title,
dialogs, and metainfo. Binary, config paths, and desktop ID keep the
mocktail name. Publish Nightcap-x86_64.AppImage to GitHub Releases on
v* tags. README points at the fork's releases instead of upstream
packages.
2026-09-14 22:19:19 -05:00
vii 4443766d39 chore(hooks): add lefthook config mirroring CI repository checks 2026-09-14 18:19:15 -05:00
vii ea2166b09a docs(license): mark files modified from upstream per Apache 2.0
Add a modified-by notice to every upstream file this fork changes.
Ignore build-test/.
2026-09-14 18:08:12 -05:00
vii 9e4e69be1c docs(readme): describe fork changes and replace dead FAQ link with settings 2026-09-14 16:48:00 -05:00
vii dd0655bfde Merge branch 'pc-improvements' into main
Resolve src/update/host_abi_deriver.cc: keep upstream's FMOD vtable layout
versions and input-method derivation inside the optional output-device
bridge, so a mismatch omits the bridge instead of failing derivation.
2026-09-14 16:09:33 -05:00
vii d2d275706b perf(graphics): present through fifo-latest-ready by default to cut input latency
Roblox requests FIFO with 3 swapchain images, which queues up to two frames
before display. The default vsync policy (auto, unmanaged frame cap) now
resolves to the vsync policy so the lowest-latency synchronized present mode
the host offers is used.
2026-09-14 15:51:35 -05:00
komaruworld d76d75b7f8 rollback to native login
not fully test
2026-09-14 19:03:36 +03:00
vii b7b63dd30f feat(packaging): launch with MOCKTAIL_SMALL_TEXTURE_UPSCALE=4 from the desktop entry 2026-09-14 10:10:40 -05:00
vii aacab4036c feat(graphics): replace and upscale small ETC2 textures
Level-0 uploads are hashed against the override store; MOCKTAIL_SMALL_TEXTURE_UPSCALE
creates colour images up to 64px N times larger, snaps block-rounded regions to the
host mip, fills every host mip from level 0 and turns image copies touching scaled
images into blits. MOCKTAIL_ADVERTISE_ETC2=0 hides the feature bit.
2026-09-14 10:10:40 -05:00
vii e104b1d936 feat(graphics): add PNG texture override and dump store keyed by upload hash
Adds libpng, MOCKTAIL_TEXTURE_OVERRIDE_DIR (default ~/.config/mocktail/textures)
and MOCKTAIL_TEXTURE_DUMP_DIR.
2026-09-14 10:10:40 -05:00
komaruworld 537828acc2 Fix vc
Fixes #128 Fixes #132
2026-09-14 16:36:35 +03:00
komaruworld 5be2f68a55 Support input devices
Fix ci, some shit with webkit on wayland
2026-09-14 15:24:04 +03:00
komaruworld f7e519bae1 I forgot to add changelog 2026-09-14 14:50:12 +03:00
komaruworld ad8fbfcf65 Add 1.0.4 release to flathub metadata 2026-09-14 14:36:49 +03:00
komaruworld 96e1966d16 sigxcpu crashes fix
Refs #84
2026-09-14 13:15:54 +03:00
komaruworld becaee71c0 Add fleasion support
Close #18
2026-09-14 13:05:24 +03:00
vii bf6a6b417d fix(packaging): write the install path into the installed desktop entry 2026-09-14 04:53:50 -05:00
vii dfd35b8cc8 feat(packaging): launch on Wayland from the desktop entry 2026-09-14 04:44:25 -05:00
vii 1632681e6c fix(jnivm): answer NativeQuoteInterface.requestResponse so servers stop kicking with 319
The client asks Java for a keystore attestation quote after joining. With no
handler the call returned null, the server never got an answer and dropped
the session with error 319. Reply with the no-keystore form [1, 0, message].
2026-09-14 04:44:25 -05:00
komaruworld ab4944a907 Fix shit with sigxcpu
Refs #84
2026-09-14 12:34:09 +03:00
vii 88d10d53f2 perf(graphics): decode ETC2 uploads off mapped memory on worker threads
Compressed bytes are copied to heap scratch, decoded heap to heap across
up to 8 threads in block-row bands, and streamed into the staging buffer.
Decoding straight from uncached Vulkan memory caused 200-460 ms hitches.
The FPS trace now reports the longest frame gap per window.
2026-09-14 04:18:43 -05:00
vii eae9fa7030 feat(graphics): emulate ETC2/EAC textures on Vulkan
Hosts without native ETC2 now report ETC2/EAC support. Uploads into emulated images are decoded to RGBA8/R16 staging buffers at submit. MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns it off.
2026-09-13 11:45:28 -05:00
vii ad76f68224 fix(runtime): report real host memory and display size
DeviceParams now carry host RAM instead of a low-RAM device profile, and screen, window and configuration sizes follow the host display and window with a 1920x1080 fallback.
2026-09-13 11:45:28 -05:00
vii d783793b4e fix(update): make the FMOD output-device bridge optional
Candidate payloads without a matching FMOD output-device signature now derive without the bridge instead of failing activation.
2026-09-13 11:45:28 -05:00
komaruworld 017f79398d Fix crash in resize windows 2026-09-13 14:32:41 +03:00
komaruworld fc120e8285 support rbx 2.738 with fix charts 2026-09-13 13:02:25 +03:00
komaruworld 117b9b2016 Update mocktail package in aur
😢
2026-09-12 23:21:27 +03:00
komaruworld 1376ab2474 Ci fixes sha256
i skoro sha snesy naher
2026-09-12 23:13:55 +03:00
komaruworld fdce53421f Fix low textures
THX HIPPO! <3
2026-09-12 20:52:25 +03:00
komaruworld 770ed086d5 Fix aur bin
Close #132

:(
2026-09-12 19:27:53 +03:00
komaruworld c48db09f13 Fix chat text render on opengl 2026-09-12 15:59:03 +03:00
komaruworld 0548ebd253 Fix chat text render and chat shiftlock issues
Fixes 106, Fixes #127, Refs #116
2026-09-12 14:45:29 +03:00
207 changed files with 14776 additions and 1491 deletions

No files matched your search

+3 -2
View File
@@ -1,5 +1,6 @@
# Modified by vii from komaruworld/mocktail. See README "About this fork".
name: Bug report
description: Report a reproducible problem in Mocktail
description: Report a reproducible problem in Nightcap
title: "[Bug]: "
labels:
- bug
@@ -75,7 +76,7 @@ body:
value: |
Latest log:
- Flatpak: `~/.var/app/space.bigrat.mocktail/.local/state/mocktail/logs/latest.log`
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
- Other installs: `~/.local/state/mocktail/logs/latest.log`
Older logs are in the `sessions` directory next to `latest.log`.
+1
View File
@@ -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 .
+67 -68
View File
@@ -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
+58 -220
View File
@@ -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
+168
View File
@@ -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
+2 -13
View File
@@ -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/
+141
View File
@@ -0,0 +1,141 @@
# Changelog
All notable changes to Nightcap. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Release notes on
GitHub are taken from the matching section here.
## [Unreleased]
## [0.3.0] - 2026-09-18
### Added
- Custom Discord presence. `integrations.discord_rpc.text.title` replaces the
name in "Playing Nightcap", and `integrations.discord_rpc.images` sets the
large and small icons and their hover text. Text and image fields accept
`{place_name}`, `{place_icon}`, and `{creator_name}`. A field that expands
an empty placeholder is left out, and `text.state: ""` hides the state line.
- The default Discord card now matches Sober: the title is "Roblox", the
details line is "Playing <experience>", the state is "by <creator>", and
the Roblox badge sits inside the experience icon. The creator comes from the
same Roblox games request that already fetches the experience name. With
`show_place_name: false` the creator is hidden too, so the card shows only
the title and timer.
- `--profile <file>` writes a Chrome-format trace of the Vulkan adapter's
work: presents, GPU waits, submits, ETC2 texture decode, and pipeline and
shader creation. Open it in ui.perfetto.dev. `docs/BENCHMARKING.md`
describes the benchmark runs, and `scripts/summarize_profile_trace.py`
turns traces into a before and after table.
### Fixed
- Texture uploads stalled the frame they were submitted on. ETC2 batches
decoded on worker threads but were then resampled one upload at a time on
the submitting thread, which at the default 4x upscale is sixteen times the
decoded texels. Resampling now runs on the same worker pool, in two passes so
mip level 0 still resolves before the mips built from it. In a 70k-part
place the worst `vkQueueSubmit` fell from 167ms to 92ms.
- Most ETC2 batches decoded on a single thread. Workers were created and
joined per batch, so the parallel floor had to cover that cost and sat at a
1024x1024 image; batches under it ran on the caller, which was 54% of all
decode time in a 70k-part place. Decode workers are now a persistent pool,
and the floor drops to a 128x128 image. Mean decode time in that place fell
from 8.2ms to 4.7ms, and 11.8ms before both texture fixes.
- On CPUs with a wide core-speed spread, Feral GameMode pinned the process to
the few fastest cores, concentrating Roblox's worker pool onto them instead
of spreading it. With `performance.gamemode: auto`, a request that narrows
the CPU affinity is now released again, which releases the pin. Set
`pin_cores=no` in `~/.config/gamemode.ini` to keep GameMode without the
pinning, or `performance.gamemode: on` to accept it. See
`docs/PERFORMANCE.md`.
- With `show_place_name: false`, Discord no longer shows the experience
thumbnail or its name on hover.
- The host main thread no longer spins on the engine's message pump, which
cost about half a core and 146,000 empty calls a second. It now rests 100 µs
between polls: sleeping when the governor is `performance`, parking the
core with TPAUSE on CPUs that have it, spinning otherwise. With GameMode
setting the governor, process CPU in a 60k-part place fell from 216% to
107% at the same 120 fps. `MOCKTAIL_ENGINE_PUMP_REST` forces a mode; see
`docs/PERFORMANCE.md`.
- ETC2 uploads with a `bufferRowLength` or `bufferImageHeight` larger than
the image read the padded rows as packed data and decoded garbage.
- ETC2 resampling created and joined worker threads on every batch. It now
runs on the persistent decode pool.
- A failed trace write left the profile writer thread waking every 250 ms
for the rest of the session. It now exits.
- The `--profile` trace gains a `pump` slice for the engine's main-thread
step.
## [0.2.0] - 2026-09-16
### Added
- FAQ covering Discord RPC, log locations, and VR, adapted from upstream.
- `input.raw_mouse` in `config.yaml` sends mouse look deltas in the host's
own units. Turn it on when first-person aiming feels too slow or too fast
under fractional display scaling. Off by default.
- Nightly AppImage builds, published as the `continuous` prerelease. They are
untested; use a tagged release for normal play.
### Changed
- Values in `fflags.json` now win over the performance preset instead of
stopping the launch. Startup prints each key it kept.
- Discord Rich Presence uses Nightcap's own Discord application, so it shows
as "Playing Nightcap". Terms and privacy pages for it live on the
project site.
- Discord Rich Presence is on by default. Set
`integrations.discord_rpc.enabled: false` to turn it off.
- The AppImage build verifies its reduced dependency packages against digests
taken at build time, rather than a digest list kept in the repository.
### Fixed
- Mouse look keeps responding after a long turn. The tracked pointer position
could drift past the edge of the view, which swallowed every reversal until
the mouse travelled all the way back.
- Nightcap no longer refuses to start with `cannot lock appStorage`. It
created its own credential directory using the process umask, then rejected
that directory for being group writable. Systems with a permissive umask,
such as `002`, failed on first run.
## [0.1.0] - 2026-09-15
First Nightcap release, forked from komaruworld/mocktail.
### Added
- ETC2 and EAC texture emulation on Vulkan, so games that use compressed
mobile textures render correctly on desktop GPUs. Decoding runs on worker
threads to avoid stutter.
- PNG texture overrides in `~/.config/mocktail/textures`, keyed by upload
hash, with a dump mode to find the texture you want to replace.
- Small textures are upscaled 4x by default. Set
`MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn it off.
- AppImage and signed Flatpak builds on every release tag. The Flatpak repo
is published at https://coderdayton.github.io/nightcap/.
- Roadmap in `docs/ROADMAP.md`.
### Changed
- Frames are presented as soon as they are ready instead of waiting in a
queue, which lowers input latency.
- Roblox now sees the real host RAM and display size instead of a low-end
phone profile.
- App ID is `io.github.CoderDayton.nightcap`, so Nightcap installs next to
upstream Mocktail. The binary and config paths keep the `mocktail` name.
- New name, icon, and colors.
- The desktop entry launches on Wayland and points at the installed binary.
### Fixed
- Servers no longer kick with error 319 shortly after joining.
- The FMOD output-device bridge is optional, so the updater no longer fails
without it.
- The AppImage build no longer fails on stale checksums for the slimmed
Arch packages.
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.0
[0.2.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.2.0
[0.1.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.1.0
+27 -9
View File
@@ -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}"
@@ -265,9 +266,11 @@ add_library(mocktail_runtime STATIC
src/runtime/crash_report_policy.cc
src/runtime/discord_rpc.cc
src/runtime/device_profile.cc
src/runtime/engine_pump_rest.cc
src/runtime/environment.cc
src/runtime/external_launch_broker.cc
src/runtime/frame_rate_policy.cc
src/runtime/fleasion.cc
src/runtime/game_mode.cc
src/runtime/game_session_coordinator.cc
src/runtime/graphics_launch_policy.cc
@@ -286,6 +289,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 +302,7 @@ add_library(mocktail_runtime STATIC
src/runtime/support_bundle.cc
src/runtime/system_proxy.cc
src/runtime/texture_memory_policy.cc
src/runtime/device_memory_profile.cc
)
add_library(Mocktail::Runtime ALIAS mocktail_runtime)
target_include_directories(mocktail_runtime PUBLIC include)
@@ -327,6 +333,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 +472,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 +727,24 @@ install(FILES
config/roblox_signing_certificates.json
DESTINATION "${MOCKTAIL_INSTALL_DATA_DIR}/metadata"
)
install(FILES packaging/space.bigrat.mocktail.desktop
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications"
)
install(FILES packaging/space.bigrat.mocktail.metainfo.xml
# The installed desktop entry names the binary by its install path, so it
# launches from sessions whose PATH lacks the install prefix. Uses the prefix
# given at install time, not the configure-time one.
install(CODE "
file(READ \"${CMAKE_SOURCE_DIR}/packaging/io.github.CoderDayton.nightcap.desktop\"
MOCKTAIL_DESKTOP_ENTRY)
string(REPLACE \"Exec=env SDL_VIDEODRIVER=wayland,x11 mocktail %u\"
\"Exec=env SDL_VIDEODRIVER=wayland,x11 \${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/mocktail %u\"
MOCKTAIL_DESKTOP_ENTRY \"\${MOCKTAIL_DESKTOP_ENTRY}\")
file(WRITE \"${CMAKE_BINARY_DIR}/io.github.CoderDayton.nightcap.desktop\"
\"\${MOCKTAIL_DESKTOP_ENTRY}\")
file(INSTALL \"${CMAKE_BINARY_DIR}/io.github.CoderDayton.nightcap.desktop\"
DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications\")
")
install(FILES packaging/io.github.CoderDayton.nightcap.metainfo.xml
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo"
)
install(FILES packaging/space.bigrat.mocktail.svg
install(FILES packaging/io.github.CoderDayton.nightcap.svg
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps"
)
set(MOCKTAIL_HICOLOR_ICON_SIZES
@@ -735,7 +753,7 @@ set(MOCKTAIL_HICOLOR_ICON_SIZES
# Generated icons are committed, so ImageMagick is not a build dependency.
foreach(icon_size IN LISTS MOCKTAIL_HICOLOR_ICON_SIZES)
install(FILES
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/space.bigrat.mocktail.png"
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/io.github.CoderDayton.nightcap.png"
DESTINATION
"${CMAKE_INSTALL_DATADIR}/icons/hicolor/${icon_size}x${icon_size}/apps"
)
+53 -8
View File
@@ -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
```
+4 -3
View File
@@ -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
+105 -91
View File
@@ -1,19 +1,71 @@
# Mocktail
<!-- Modified by vii from komaruworld/mocktail. See "About this fork" below. -->
# Nightcap
[![CI](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml/badge.svg)](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
[![Stars](https://img.shields.io/github/stars/komaruworld/mocktail?style=flat&logo=github)](https://github.com/komaruworld/mocktail/stargazers)
[![Downloads](https://img.shields.io/github/downloads/komaruworld/mocktail/total?logo=github)](https://github.com/komaruworld/mocktail/releases/latest)
[![CI](https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml/badge.svg)](https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml)
[![Stars](https://img.shields.io/github/stars/CoderDayton/nightcap?style=flat&logo=github)](https://github.com/CoderDayton/nightcap/stargazers)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Discord](https://img.shields.io/discord/1543955607869063269?label=discord)](https://discord.gg/rhgpfcmFSD)
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
the Linux side.
Nightcap is Mocktail, tuned for playing on a real PC. It runs the Android
`x86_64` Roblox client on Linux, including a Linux userspace hosted by
FreeBSD's Linuxulator. It provides the Android ABI and JNI pieces the client
expects, then connects them to SDL3 and Vulkan or OpenGL on the Linux side.
Mocktail is an independent community project. It is not affiliated with Roblox
Nightcap is an independent community project. It is not affiliated with Roblox
Corporation or VinegarHQ and does not distribute the Roblox client.
## About this fork
Nightcap is a fork of [komaruworld/mocktail](https://github.com/komaruworld/mocktail)
with extra fixes for playing on a PC. The program and config paths keep the
`mocktail` name so upstream fixes still apply cleanly. The app ID is
`io.github.CoderDayton.nightcap`, so Nightcap and upstream can be installed
side by side.
- **Lower input lag.** Frames are shown as soon as they are ready instead of
waiting in a queue.
- **Sharper textures.** Small textures are upscaled 4x by default, and you
can replace any texture with your own PNG in `~/.config/mocktail/textures`.
A ready-made crosshair is in [`textures/`](textures/README.md). Set
`MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn the upscale off.
- **ETC2 textures on any GPU.** Games that use compressed mobile textures now
render correctly on desktop GPUs, without stutter.
- **No more error 319 kicks.** Servers no longer drop you shortly after
joining.
- **Correct device info.** Roblox sees your real RAM and screen size.
- **Better desktop launcher.** Runs on Wayland and always finds the installed
binary.
Everything else works the same as upstream. See the
[roadmap](docs/ROADMAP.md) for what is coming next, the
[changelog](CHANGELOG.md) for what each release changed,
[performance](docs/PERFORMANCE.md) for CPU load, heat and the knobs that change
them, and the [FAQ](FAQ.md) for Discord RPC, logs, and VR.
## Install with Flatpak
```bash
flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
flatpak run io.github.CoderDayton.nightcap
```
Updates arrive through `flatpak update`. Nightcap is not on Flathub yet.
## AppImage
Grab the AppImage from the
[latest release](https://github.com/CoderDayton/nightcap/releases/latest),
make it executable, and run it:
```bash
chmod +x Nightcap-x86_64.AppImage
./Nightcap-x86_64.AppImage
```
Or build and install it from source using the steps under
[Building](#building). There are no AUR, DEB, or RPM packages of Nightcap.
Upstream ships those for unmodified Mocktail, see
[komaruworld/mocktail](https://github.com/komaruworld/mocktail#readme).
## How it works
```
@@ -21,91 +73,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>
![Roblox home in Mocktail](assets/screenshots/flatpak-home.png)
![Roblox home in Nightcap](assets/screenshots/flatpak-home.png)
![Roblox gameplay in Mocktail](assets/screenshots/flatpak-gameplay-tower.png)
![Roblox gameplay in Nightcap](assets/screenshots/flatpak-gameplay-tower.png)
![Roblox experience in Mocktail](assets/screenshots/flatpak-gameplay-lobby.png)
![Roblox experience in Nightcap](assets/screenshots/flatpak-gameplay-lobby.png)
</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 +102,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 +160,34 @@ sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
</details>
```bash
git clone --recurse-submodules https://github.com/komaruworld/mocktail.git
cd mocktail
git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
cd nightcap
make build
./build/mocktail
```
To install it for your user, with an app menu entry and Roblox link handling:
```bash
make install PREFIX=~/.local
```
Use `sudo make install` instead to install system-wide under `/usr`. To make
Roblox website links open the build tree copy without installing, run
`make register-url-handler`.
To run the same checks as CI before each commit and push, install
[lefthook](https://github.com/evilmartians/lefthook) and run once:
```bash
lefthook install
```
## Known limitations
Mocktail does not currently work with `hardened_malloc`. Using
`hardened_malloc` may cause Mocktail to fail to start or crash during runtime.
Run Mocktail without `hardened_malloc` enabled.
Nightcap does not currently work with `hardened_malloc`. Using
`hardened_malloc` may cause Nightcap to fail to start or crash during runtime.
Run Nightcap without `hardened_malloc` enabled.
## License
@@ -186,7 +195,12 @@ Run Mocktail without `hardened_malloc` enabled.
## Support
You can support the project by giving it a star or with cryptocurrency:
Support this fork by giving it a star or with cryptocurrency:
- LTC: `LdABR2ELRYrUESWZWrEk1uNx38gHHMuHUU`
- SOL: `tjeDnPoWeyW8zfTf8c5CVAm5QTV8vFPaPouaScAdwfT`
Support the upstream project, which does most of the heavy lifting:
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
+26 -1
View File
@@ -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)
+39 -6
View File
@@ -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.
@@ -91,12 +112,24 @@ integrations:
public_servers_only: true
# button_label: Join Server
# English defaults. Uncomment only the lines you want to customize.
# Text and image fields accept {place_name}, {place_icon}, and
# {creator_name}. A field that expands an empty placeholder is left out,
# and a "" title shows the app name.
# title, state, and the images fields may be "" to hide them.
# text:
# browsing: Browsing experiences
# joining: Joining an experience
# playing: "{place_name}"
# state: Playing Roblox
# playing: "Playing {place_name}"
# state: "by {creator_name}"
# unknown_place: Unknown experience
# title: Roblox
# Images take an https:// URL, or an asset key uploaded to application_id.
# large: "" hides both images; the small image needs a large one.
# images:
# large: "{place_icon}"
# large_text: "{place_name}"
# small: roblox_small
# small_text: Roblox
# Advanced: overrides the application bundled by the package maintainer.
# application_id: 123456789012345678
+5 -1
View File
@@ -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."
}
+98
View File
@@ -0,0 +1,98 @@
# Benchmarking
A pull request that claims a speedup shows before and after numbers taken
the way this guide describes. Numbers from different machines are not
compared with each other. Each run is compared with a run on the same
machine.
## Two layers, two tools
Roblox measures its own work. Nightcap measures the layer between Roblox and
your driver.
| Layer | What it covers | Tool |
| --- | --- | --- |
| Roblox | Scripts, physics, rendering jobs, network | Roblox's stats overlays, Shift+F4 and Shift+F5 in game |
| Nightcap | Vulkan adapter: present, swapchain and GPU waits, submits, ETC2 texture decode, pipeline and shader creation | `--profile` |
A Nightcap change should move the `--profile` numbers. If it only moves the
Roblox overlay numbers, say so in the pull request, since the cause is then
less direct.
## Recording a trace
```sh
mocktail --profile ~/nightcap-before.json
```
The Vulkan adapter writes a Chrome-format trace to that file, a few times a
second. The file is valid JSON after every write, so a session that crashes
or is killed still leaves a readable trace. Exit from the Roblox menu when you are done. Open the file
in [ui.perfetto.dev](https://ui.perfetto.dev) or `chrome://tracing`.
Expect about 130 bytes per event, around 15 MB per minute at 240 FPS.
Profiling is off unless `--profile` is given, and `MOCKTAIL_PROFILE_TRACE`
does the same thing as the flag.
### What the trace contains
Each slice is named after the call it times, on the thread that made it.
| Category | Slices | Arguments |
| --- | --- | --- |
| `present` | `vkQueuePresentKHR`: the whole adapter present, including the text overlay | `result` |
| `present` | `host present`: the host driver's present, nested inside the slice above | |
| `wait` | `vkAcquireNextImageKHR`, `vkAcquireNextImage2KHR`, `vkWaitForFences`, `vkWaitSemaphores`, `vkWaitSemaphoresKHR`, `vkQueueWaitIdle`, `vkDeviceWaitIdle` | |
| `submit` | `vkQueueSubmit`, `vkQueueSubmit2`, `vkQueueSubmit2KHR` | `submits` |
| `texture` | `etc2 decode`: CPU decode of ETC2 textures before a submit | `uploads`, `compressed_bytes`, `decoded_bytes` |
| `pipeline` | `vkCreateGraphicsPipelines`, `vkCreateComputePipelines` | `count`, `cache` (1 when a pipeline cache was passed), `result` |
| `pipeline` | `vkCreateShaderModule` | `bytes`, `result` |
| `pipeline` | `vkCreatePipelineCache` | `initial_bytes`, `result` |
| `pump` | `nativeCallMessagesFromMainThread`: the engine's main-thread step, on the host main thread. Only calls of 20 µs or more are recorded; the empty polls between them are not. | |
The `frame interval (ms)` counter is the time between successive adapter
presents. `vkQueuePresentKHR` time minus `host present` time is the adapter's
own cost per frame.
## The benchmark runs
Run every scenario before and after the change, on the same build type
(`make build`), with the same `config.yaml`. Close other GPU-heavy apps.
Record `graphics.frame_rate_limit` and `graphics.vsync` in the pull request,
because a frame cap hides frame time gains.
| Scenario | How to start | What to do | What it shows |
| --- | --- | --- | --- |
| Home screen, cold | `mocktail --profile home-cold.json` with an empty shader cache (below) | Wait 30 s on the home screen, exit | First-load stutter: `pipeline` and `texture` slices |
| Tower of Hell | `mocktail --profile toh.json --launch-uri "roblox://experiences/start?placeId=1962086868"` | Stand still at spawn for 60 s, exit | Steady-state frame time on a light scene |
| Murder Mystery 2 | `mocktail --profile mm2.json --launch-uri "roblox://experiences/start?placeId=142823291"` | Stay in the lobby for 60 s, exit | Frame time with many players and a busier scene |
For the cold run, point the driver's shader cache at an empty directory, so
your normal cache is left alone:
```sh
cache=$(mktemp -d)
MESA_SHADER_CACHE_DIR="$cache" __GL_SHADER_DISK_CACHE_PATH="$cache" \
mocktail --profile home-cold.json
```
`MESA_SHADER_CACHE_DIR` applies to Mesa drivers (AMD, Intel) and
`__GL_SHADER_DISK_CACHE_PATH` to the NVIDIA driver.
Run each scenario three times and keep the median run, since one run can
catch a background stall.
## Comparing runs
```sh
scripts/summarize_profile_trace.py toh-before.json toh-after.json
```
This prints a Markdown table with one column per trace: frame count, frame
interval mean, p50, p95, p99 and max, then count, total, mean, p99 and max
for every slice name, largest total first. Paste the table into the pull
request and attach both traces.
For frame time, compare p95 and p99 as well as the mean. Stutter shows up in
the tail first. For first-load work, compare the `total (ms)` rows of the
`pipeline` and `texture` slices from the cold run.
+147
View File
@@ -0,0 +1,147 @@
# Performance and CPU load
What Nightcap does to your CPU and GPU, which knobs change it, and how to
measure rather than guess.
## GameMode core pinning
Nightcap asks Feral GameMode for a performance session at startup
(`performance.gamemode`, default `auto`). GameMode raises the CPU governor and
the process priority, which helps.
On a CPU with a wide spread of per-core maximum frequencies — Intel P-core and
E-core parts, AMD parts with preferred cores — GameMode also pins the process
to the fastest cores. On a 32-thread host that can mean 4 usable threads out of
32. Roblox spreads its work across a worker pool, so the pin concentrates that
pool onto a couple of physical cores instead of spreading it. Those cores sit
near their boost ceiling and run hot while the rest of the chip idles.
GameMode re-applies the pin roughly every 5 seconds, so widening the mask from
inside the process does not hold. Releasing the request does release the pin.
Under `auto`, Nightcap therefore checks its CPU affinity immediately after the
request. If GameMode narrowed it, Nightcap ends the request and prints:
```
[gamemode] released: it pinned this process to a subset of the CPUs, ...
```
The trade is that GameMode's governor and priority boost go with it. To keep
those and lose only the pinning, turn pinning off for every game on the host:
```ini
# ~/.config/gamemode.ini
[cpu]
pin_cores=no
park_cores=no
```
GameMode reads `$XDG_CONFIG_HOME`, `/usr/share/gamemode` and `/etc`, in that
order. It does not read anything under Nightcap's own config directory, so this
file has to live in one of those three places. The daemon reads it when it
starts, so restart it after editing: `systemctl --user restart gamemoded`.
The governor switch runs through polkit. Debian and Ubuntu only allow it for
members of the `gamemode` group; without that, the daemon's journal shows
`cpugovctl set performance: Not authorized` and the governor stays put while
the session is otherwise active. Add yourself and log in again:
```bash
sudo usermod -aG gamemode "$USER"
```
Fedora and Arch allow any logged-in local user.
Setting `performance.gamemode: on` accepts the pinning instead: `on` is an
explicit request for GameMode, and Nightcap keeps the session whatever it does
to the affinity. `off` never contacts the daemon.
To see which CPUs the process may use:
```bash
taskset -pc $(pgrep -x mocktail)
```
## The main-thread pump
Roblox posts its per-frame main-thread step at a time of its own choosing,
and nothing signals the host when it does. The host main thread therefore
polls `nativeCallMessagesFromMainThread`, and a post that waits more than a
fraction of a millisecond costs a whole frame. Between polls the thread rests
for 100 µs, and how it rests decides the CPU clock:
| Mode | When | What it costs |
| --- | --- | --- |
| `sleep` | The governor is `performance`, which GameMode sets | The thread idles at about 3% of a core. The governor holds the clock. |
| `tpause` | Otherwise, on a CPU with WAITPKG (Intel 12th gen and newer) | The core stays awake in a light C0 state. Frame rate holds; the OS books the time as busy, so the thread shows around 70% of a core, at lower power than a spin. |
| `spin` | Otherwise | The core stays awake at full power. |
Under a power-saving governor the CPU clocks a bursty core down unless some
core stays awake, and Roblox's render thread then runs slower and drops
frames. That is why `sleep` is not the default there. Startup prints the
chosen mode:
```
[main] engine pump rest: tpause (MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin)
```
`MOCKTAIL_ENGINE_PUMP_REST` forces a mode. `sleep` without the governor's help
runs cooler for a few frames per second less; measure before keeping it.
## Small texture upscaling
`MOCKTAIL_SMALL_TEXTURE_UPSCALE` (default `4`) redraws ETC2 textures of 64px or
smaller at N times their width and height. At the default that is 16 times the
pixels per affected texture.
The cost lands in two places. Decoding and resampling is CPU work on up to 8
threads while textures load, which shows up as a burst rather than a steady
load. The enlarged textures then stay in GPU memory for the session and cost
extra sampling bandwidth on every frame that uses them.
Set it to `1` to turn upscaling off:
```bash
MOCKTAIL_SMALL_TEXTURE_UPSCALE=1 mocktail
```
## Frame rate and present mode
`graphics.frame_rate_limit` (default `-1`) leaves Roblox's own in-game
framerate cap in charge; Nightcap sets no `DFIntTaskSchedulerTargetFps`
override. A fixed value forwards that number to the scheduler.
`graphics.vsync` (default `auto`) selects the presentation mode. `auto` and
`on` both pick the lowest-latency synchronized mode the driver offers, in
order: `FIFO_LATEST_READY`, `MAILBOX`, `FIFO_RELAXED`, `FIFO`. The first two do
not block the render thread on the display refresh, so the frame rate is
governed by the in-game cap rather than by presentation. `off` selects
`IMMEDIATE` and does not synchronize at all.
If the frame rate is higher than your refresh rate, the extra frames are work
you cannot see. Lower the in-game cap to your refresh rate first.
## Measuring
Shift+F4 in game shows Roblox's own scheduler jobs, with a millisecond cost and
a percentage for each. It does not show Nightcap's threads — the Vulkan
adapter, the ETC2 decode workers, the libc shim — so it is a reading on the
game, not on the process.
For the whole process:
```bash
top -H -p $(pgrep -x mocktail) # per-thread CPU, sorted
ps -L -o psr=,pcpu=,comm= -p $(pgrep -x mocktail) # which core each thread is on
nvidia-smi --query-gpu=utilization.gpu,temperature.gpu,power.draw --format=csv
```
A thread pinned near 100% while the frame rate is steady is a busy-wait. The
one exception is the host main thread in `tpause` mode, which the OS reports
as busy while the core is parked (see above). Load spread across many
`RBX Worker` threads is the game's own scheduler, and its cost tracks the
place you are in — part count, moving parts and joints all show in the
Shift+F4 World section.
`--profile <file>` writes a Chrome trace of the Vulkan adapter's work for
ui.perfetto.dev. See [BENCHMARKING.md](BENCHMARKING.md).
+90
View File
@@ -0,0 +1,90 @@
# Nightcap roadmap
What Nightcap is working toward, in priority order. Items move to
[Shipped](#shipped) when they land on `main`. Open an issue to propose a
change to this list.
## 1. Settings UI
Nightcap is configured by editing `~/.config/mocktail/config.yaml`. Most
people never open it, so they never find the FPS unlock or the vsync switch.
Goal: a small GTK4 settings window, reachable from the app menu and from a
`--settings` flag, that reads and writes the same `config.yaml`.
- Graphics tab: backend, frame rate limit, vsync, small texture upscale.
- Input tab: the `input.raw_mouse` toggle and controller options.
- Advanced tab: FFlag overrides with a plain text editor for `fflags.json`.
- Discord tab: every field in the `discord_rpc` block, so a friend can see
what you are playing and you can switch it all off in one click.
- Changes apply on next launch. A restart button restarts Nightcap.
Done when every option in `config/mocktail.example.yaml` that a player would
touch can be set without a text editor.
## 2. Controller button glyphs
Upstream added Xbox and PlayStation gamepad support through SDL. Controllers
already hot-plug and release their held inputs when unplugged. One rough edge
is left.
- Correct button glyphs for the connected pad. `SDL_GetGamepadType` is only
read for Xbox and PlayStation pads, so a Switch Pro, a GameCube pad, or any
generic pad SDL reports as `SDL_GAMEPAD_TYPE_STANDARD` reaches Roblox as
type 0 and draws generic glyphs. `SDL_GetRealGamepadType`, and the vendor
and product IDs, are available and unused.
Done when a plugged-in pad shows its own buttons.
## 3. First-load stutter
The `--profile` trace now shows shader compiles and pipeline creation, so
the stutter on first entering an experience can be measured.
- Cut the stutter the trace shows, and prove it with a before and after
trace per [BENCHMARKING.md](BENCHMARKING.md).
Done when entering a fresh experience no longer drops frames on shader
compiles.
## Shipped
- Lower input latency: frames are presented as soon as they are ready.
- Controllers hot-plug without a restart, and a pad that is unplugged mid-game
releases its held buttons and sticks instead of leaving them stuck.
- Mouse look keeps responding after a long turn, instead of going dead until
the pointer travels back from past the screen edge.
- Optional raw mouse input: `input.raw_mouse` in `config.yaml` sends look
deltas in host units, so fractional display scaling no longer changes
first-person sensitivity.
- Small textures upscaled 4x by default, with PNG overrides in
`~/.config/mocktail/textures`.
- ETC2 textures decoded on desktop GPUs without stutter.
- No more error 319 kicks after joining a server.
- Roblox sees the real host RAM and screen size.
- Desktop launcher that works on Wayland and finds the installed binary.
- Discord Rich Presence on by default, with the experience name, creator,
elapsed time, the experience icon with a Roblox badge, and a join button
when the server is public. See the [FAQ](../FAQ.md) to switch it off.
- Custom Discord presence: title, details, state, and large and small icons
from `config.yaml`, with `{place_name}`, `{place_icon}`, and
`{creator_name}` placeholders.
- `--profile` writes a Chrome-format trace of the Vulkan adapter's work,
including shader and pipeline creation, readable in `ui.perfetto.dev`.
[BENCHMARKING.md](BENCHMARKING.md) fixes the experiences to measure and
`scripts/summarize_profile_trace.py` builds the before and after table.
- Texture uploads no longer stall the frame they land on: ETC2 decode and
resampling run on a persistent worker pool.
- The host main thread rests between engine polls instead of spinning, and
GameMode's core pinning is released, so Roblox's workers spread across
every core. See [PERFORMANCE.md](PERFORMANCE.md).
- Own app ID `io.github.CoderDayton.nightcap`, so Nightcap installs next to
upstream.
- AppImage and signed Flatpak releases on every `v*` tag, with the Flatpak
repo on GitHub Pages.
## Not planned
- Replacing the `mocktail` binary and config names. Keeping them makes
upstream merges clean.
- DEB, RPM, and AUR packages. Upstream ships those for unmodified Mocktail.
@@ -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_
+24
View File
@@ -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,121 @@
#ifndef MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
#define MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <initializer_list>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
namespace mocktail {
namespace graphics {
// Environment variable naming the trace file the Vulkan adapter writes.
inline constexpr char kProfileTraceEnvironment[] = "MOCKTAIL_PROFILE_TRACE";
struct TraceArg {
const char* key;
std::int64_t value;
};
// CLOCK_MONOTONIC in nanoseconds.
std::uint64_t TraceClockNanos();
// Writes the Trace Event Format JSON array read by chrome://tracing and
// ui.perfetto.dev. Recording threads only format into a shared buffer; a
// background thread owns file writes, so disk stalls do not land inside the
// measured calls. Events are written in arrival order. Each write ends the
// file with the closing bracket, so a killed session leaves a loadable trace
// holding everything written before it died.
// Names, categories, and keys must outlive the call.
class ChromeTraceWriter final {
public:
static std::unique_ptr<ChromeTraceWriter> Open(
const std::filesystem::path& path, std::string* error);
~ChromeTraceWriter();
ChromeTraceWriter(const ChromeTraceWriter&) = delete;
ChromeTraceWriter& operator=(const ChromeTraceWriter&) = delete;
// A complete ("X") slice on the calling thread. Slices on one thread must
// nest, which scoped timing gives.
void Slice(const char* name, const char* category, std::uint64_t start_ns,
std::uint64_t end_ns, std::initializer_list<TraceArg> args = {});
void Slice(const char* name, const char* category, std::uint64_t start_ns,
std::uint64_t end_ns, const TraceArg* args,
std::size_t arg_count);
// A counter ("C") sample; each name becomes one counter track.
void Counter(const char* name, std::uint64_t timestamp_ns, double value);
// Returns once every event recorded before the call is on disk.
void Flush();
// Flushes, closes the file, and drops later events. Idempotent.
void Close();
private:
explicit ChromeTraceWriter(std::FILE* file);
void Append(const char* event, std::size_t size);
void WriterLoop();
void WritePending();
std::FILE* file_;
const int owner_pid_;
std::mutex file_mutex_; // Guards file_ and spare_.
std::string spare_;
std::mutex mutex_;
std::condition_variable wake_;
std::string pending_;
bool first_event_ = true;
bool closed_ = false;
bool stopping_ = false;
std::thread writer_;
};
// The process trace named by MOCKTAIL_PROFILE_TRACE, opened on first use.
// Null when profiling is off or the file could not be opened. The writer is
// never destroyed; it is closed when the process exits normally.
ChromeTraceWriter* ActiveProfileTrace();
// Times its own lifetime as one slice. A null writer records nothing.
class TraceScope final {
public:
TraceScope(ChromeTraceWriter* writer, const char* name,
const char* category)
: writer_(writer),
name_(name),
category_(category),
start_ns_(writer != nullptr ? TraceClockNanos() : 0) {}
~TraceScope() {
if (writer_ != nullptr) {
writer_->Slice(name_, category_, start_ns_, TraceClockNanos(), args_,
arg_count_);
}
}
TraceScope(const TraceScope&) = delete;
TraceScope& operator=(const TraceScope&) = delete;
// Keeps the first kMaxArgs arguments.
void Arg(const char* key, std::int64_t value) {
if (writer_ != nullptr && arg_count_ < kMaxArgs) {
args_[arg_count_++] = {key, value};
}
}
private:
static constexpr std::size_t kMaxArgs = 4;
ChromeTraceWriter* writer_;
const char* name_;
const char* category_;
std::uint64_t start_ns_;
TraceArg args_[kMaxArgs]{};
std::size_t arg_count_ = 0;
};
} // namespace graphics
} // namespace mocktail
#endif // MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
+79
View File
@@ -0,0 +1,79 @@
#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
#include <cstddef>
#include <cstdint>
namespace mocktail::graphics {
// ETC2 and EAC block encodings from the OpenGL ES 3.0 / Vulkan core formats.
// sRGB variants share the unorm encoding; the colour space belongs to the
// destination format.
enum class EtcFormat {
kEtc2Rgb8,
kEtc2Rgb8A1,
kEtc2Rgba8,
kEacR11,
kEacR11Signed,
kEacRg11,
kEacRg11Signed,
};
// Compressed bytes per 4x4 block.
std::size_t EtcBlockBytes(EtcFormat format);
// Decoded bytes per texel: 4 (RGBA8) for ETC2 formats, 2 per channel
// (16-bit, little-endian) for EAC formats.
std::size_t EtcDecodedTexelBytes(EtcFormat format);
// Decodes a width x height image stored as ceil(width/4) x ceil(height/4)
// blocks into row-major texels. Returns false, writing nothing, when a buffer
// is smaller than the image requires.
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
std::size_t source_bytes, std::uint32_t width,
std::uint32_t height, std::uint8_t* destination,
std::size_t destination_bytes);
// Decodes block rows [first_block_row, first_block_row + block_row_count) of
// the image DecodeEtcImage describes, writing only the texel rows those blocks
// cover. Bands that together cover every block row produce DecodeEtcImage's
// output. Returns false, writing nothing, when a buffer is too small or the
// rows lie outside the image.
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
std::size_t source_bytes, std::uint32_t width,
std::uint32_t height,
std::uint32_t first_block_row,
std::uint32_t block_row_count,
std::uint8_t* destination,
std::size_t destination_bytes);
// One DecodeEtcImage call. Jobs must not share destination bytes.
struct EtcDecodeJob {
EtcFormat format = EtcFormat::kEtc2Rgb8;
const std::uint8_t* source = nullptr;
std::size_t source_bytes = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint8_t* destination = nullptr;
std::size_t destination_bytes = 0;
// Set to DecodeEtcImage's result.
bool ok = false;
};
// Decodes every job before returning. Large batches are split into block-row
// bands decoded on up to worker_count threads, the caller included; small
// batches decode on the caller only.
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
unsigned worker_count);
// Calls run(context) worker_count times, once on the caller and the rest on
// pooled decode workers that run alongside it, and returns when every call
// has returned. `run` must be safe to call from several threads at once, so
// it should drain a shared queue. When the pool is busy with another batch,
// or no worker could be started, run is called on the caller alone.
void RunOnDecodeWorkers(void (*run)(void*), void* context,
unsigned worker_count);
} // namespace mocktail::graphics
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
@@ -0,0 +1,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
+2
View File
@@ -33,6 +33,8 @@ struct CommandLineOptions {
// Requests one explicit launch of the provider latest without promoting it
// into the managed payload state.
bool force_run_latest = false;
// Chrome trace file the Vulkan adapter writes; empty when not profiling.
std::string profile_trace_path;
};
struct CommandLineParseResult {
+28
View File
@@ -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_
+5 -1
View File
@@ -13,11 +13,14 @@ namespace mocktail {
namespace runtime {
struct DiscordRpcActivity {
std::string name;
std::string details;
std::string state;
std::optional<int64_t> start_timestamp;
std::string large_image;
std::string large_text;
std::string small_image;
std::string small_text;
std::string button_label;
std::string button_url;
};
@@ -27,7 +30,8 @@ std::string BuildDiscordJoinUrl(const RobloxExperienceLaunchRequest& request);
DiscordRpcActivity BuildDiscordRpcActivity(
const DiscordRpcConfig& config, RobloxExperiencePresencePhase phase,
const RobloxExperienceLaunchRequest* request, std::string place_name,
int64_t session_started_at, std::string place_icon_url = {});
int64_t session_started_at, std::string place_icon_url = {},
std::string creator_name = {});
// Maintains a direct, unauthenticated Discord Desktop RPC connection. The
// worker owns all network and IPC work; lifecycle notifications only replace
+73
View File
@@ -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_
+65
View File
@@ -0,0 +1,65 @@
#ifndef MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
#define MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
#include <cstdint>
namespace mocktail::runtime {
// The engine posts its per-frame main-thread step at a time of its own
// choosing, with no signal the host can block on, so the host main thread
// polls nativeCallMessagesFromMainThread. An empty poll costs a few
// microseconds; polled back to back it burns a core.
//
// Between polls the thread rests for kEnginePumpRestNs. A post is then picked
// up at most kEnginePumpRestNs + timer slack + wake latency late, far under
// the engine's sub-millisecond slack.
//
// How it rests decides the clock. Under a power-saving governor the CPU clocks
// a bursty core down unless some core stays awake, and the engine's render
// thread then runs slower and drops frames. So:
// kSleep nanosleep. The core idles. Correct only when the governor holds
// the clock up on its own (performance, which GameMode sets).
// kTpause TPAUSE in C0.2. The core stays awake at reduced power, the clock
// holds, and the OS books the time as busy. Needs WAITPKG.
// kSpin No rest. The core stays awake at full power.
inline constexpr uint64_t kEnginePumpRestNs = 100'000;
// Timer slack the calling thread is set to; the kernel default of 50 us would
// otherwise stretch every sleep.
inline constexpr uint64_t kEnginePumpTimerSlackNs = 10'000;
// Shallow C-state exit plus scheduler latency on the host, for the bound.
inline constexpr uint64_t kShallowWakeNs = 50'000;
enum class EnginePumpRestMode { kSleep, kTpause, kSpin };
const char* EnginePumpRestModeName(EnginePumpRestMode mode);
struct EnginePumpRestInputs {
bool governor_performance = false;
bool cpu_has_waitpkg = false;
// MOCKTAIL_ENGINE_PUMP_REST: "sleep", "tpause" or "spin". Null, empty or
// unknown defers to the policy; "tpause" without WAITPKG does too.
const char* override = nullptr;
};
EnginePumpRestMode ChooseEnginePumpRestMode(const EnginePumpRestInputs& inputs);
// True when the file holds "performance". `path` is a cpufreq
// scaling_governor file; cpu0's stands for the machine.
bool CpuGovernorIsPerformance(const char* path);
bool CpuHasWaitPkg();
// The mode chosen from the host on first use.
EnginePumpRestMode ActiveEnginePumpRestMode();
// Rests the calling thread in the given mode and returns the rest length, 0
// for kSpin. The first call lowers the thread's timer slack to
// kEnginePumpTimerSlackNs.
uint64_t RestAfterEnginePump(EnginePumpRestMode mode);
inline uint64_t RestAfterEnginePump() {
return RestAfterEnginePump(ActiveEnginePumpRestMode());
}
} // namespace mocktail::runtime
#endif // MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
+28
View File
@@ -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
+34
View File
@@ -1,6 +1,8 @@
#ifndef MOCKTAIL_RUNTIME_GAME_MODE_H_
#define MOCKTAIL_RUNTIME_GAME_MODE_H_
#include <sched.h>
#include <string>
#include <string_view>
@@ -22,6 +24,7 @@ enum class GameModeSessionState {
kDisabled,
kUnavailable,
kRequestFailed,
kDeclinedCorePinning,
kAlreadyActive,
kActive,
kStopped,
@@ -43,9 +46,37 @@ struct GameModeClientApi {
ErrorFn error_string = nullptr;
};
// The CPU affinity calls, injectable so tests exercise the restore without
// moving the threads they run on.
struct CpuAffinityApi {
// Reads the calling thread's permitted CPUs. GameMode pins every thread
// alike, so one thread's mask is enough to detect the pin.
using GetFn = int (*)(cpu_set_t* mask);
// Applies `mask` to every thread of this process, not only the caller.
// Returns 0 when each live thread took it.
using SetFn = int (*)(const cpu_set_t* mask);
GetFn get = nullptr;
SetFn set = nullptr;
};
CpuAffinityApi HostCpuAffinityApi();
// True when `after` permits a strict subset of the CPUs `before` permits.
// An equal, wider or merely relocated set is not a narrowing.
bool CpuAffinityNarrowed(const cpu_set_t& before, const cpu_set_t& after);
// Owns at most one ref-counted GameMode request. Missing host support and
// daemon rejection remain nonfatal because GameMode is an optimization, not a
// correctness dependency.
//
// GameMode pins the calling process to the host's fastest cores when it sees a
// wide core-speed spread. Roblox spreads its own work over a worker pool, so
// that pin concentrates the load onto a couple of cores instead of helping,
// and the daemon re-applies it every few seconds, so correcting the mask in
// place does not hold. Under `auto`, a request that narrows the affinity is
// released again: ending the request is what releases the pin. `on` is an
// explicit choice and keeps the pinned session.
class GameModeSession final {
public:
GameModeSession() = default;
@@ -59,6 +90,8 @@ class GameModeSession final {
static GameModeSession Start(GameModePolicy policy);
static GameModeSession StartWithClientForTesting(
GameModePolicy policy, GameModeClientApi api);
static GameModeSession StartWithClientForTesting(
GameModePolicy policy, GameModeClientApi api, CpuAffinityApi affinity);
GameModeSessionState state() const { return state_; }
const std::string& detail() const { return detail_; }
@@ -73,6 +106,7 @@ class GameModeSession final {
private:
static GameModeSession StartBound(GameModePolicy policy,
GameModeClientApi api,
CpuAffinityApi affinity,
void* library_handle);
void CloseLibrary();
+16 -10
View File
@@ -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,
+15
View File
@@ -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_
+16
View File
@@ -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);
+15
View File
@@ -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 {
@@ -218,6 +223,10 @@ class RobloxInputRouter final {
Status DisconnectGamepadLocked(ActiveGamepad& gamepad);
Status DisconnectGamepadsLocked();
Status ReleasePressedInputsLocked();
// Keeps mouse_x_/mouse_y_ where they are for an event that carries no
// usable position, clamped to the current viewport. Before any absolute
// position has been seen, the held position is the viewport center.
void HoldPointerLocked(const platform::SurfaceCoordinateTransform& transform);
int32_t AllocatePointerIdLocked() const;
std::vector<ActiveTouch>::iterator FindTouchIteratorLocked(int64_t touch_id,
int64_t finger_id);
@@ -233,8 +242,14 @@ class RobloxInputRouter final {
std::vector<ActiveTouch> active_touches_;
std::vector<ActiveKey> active_keys_;
std::vector<ActiveGamepad> gamepads_;
// Last known pointer position in guest units. Relative-mode motion, and
// wheel or button events with zero coordinates, do not move it: the pointer
// stays where it was captured while the deltas pass through.
float mouse_x_ = 0.0f;
float mouse_y_ = 0.0f;
// True once an absolute position has been received since Activate.
bool pointer_anchored_ = false;
bool raw_mouse_ = false;
};
const char* RobloxInputDispatchStateName(RobloxInputDispatchState state);
@@ -36,6 +36,7 @@ class RobloxWindowInputRuntime final {
Status UpdateTextFocusProperties(uint64_t generation,
const RobloxTextFocusProperties& properties);
RobloxInputSnapshot Snapshot() const;
void SetRawMouseEnabled(bool enabled);
private:
static void PlatformEventCallback(void* context,
+34 -3
View File
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#ifndef MOCKTAIL_RUNTIME_RUNTIME_CONFIG_H_
#define MOCKTAIL_RUNTIME_RUNTIME_CONFIG_H_
@@ -38,6 +39,10 @@ struct InputCapabilityConfig {
bool touch_enabled = false;
bool mouse_enabled = true;
bool keyboard_enabled = true;
// Mouse look deltas keep their host units instead of being scaled into guest
// surface units. The two differ only when the desktop display scale and the
// window's pixel density disagree, as under fractional scaling.
bool raw_mouse = false;
};
struct NetworkProxyConfig {
@@ -46,16 +51,29 @@ struct NetworkProxyConfig {
int port = 0;
};
// Text and image fields expand {place_name}, {place_icon}, and
// {creator_name}. A field that expands an empty placeholder, or renders
// empty, is left out of the activity.
struct DiscordRpcTextConfig {
std::string title = "Roblox";
std::string browsing = "Browsing experiences";
std::string joining = "Joining an experience";
std::string playing = "{place_name}";
std::string state = "Playing Roblox";
std::string playing = "Playing {place_name}";
std::string state = "by {creator_name}";
std::string unknown_place = "Unknown experience";
};
// Images are an https URL or a Discord application asset key. Discord shows
// the small image only next to a large one.
struct DiscordRpcImagesConfig {
std::string large = "{place_icon}";
std::string large_text = "{place_name}";
std::string small = "roblox_small";
std::string small_text = "Roblox";
};
struct DiscordRpcConfig {
bool enabled = false;
bool enabled = true;
bool show_place_name = true;
bool show_elapsed_time = true;
bool join_enabled = true;
@@ -63,6 +81,7 @@ struct DiscordRpcConfig {
std::string join_button_label = "Join Server";
std::string application_id;
DiscordRpcTextConfig text;
DiscordRpcImagesConfig images;
};
std::optional<NetworkProxyConfig> ParseNetworkProxyConfig(
@@ -119,6 +138,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 +178,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;
+23
View File
@@ -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 .
-11
View File
@@ -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
+12
View File
@@ -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
+5 -5
View File
@@ -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
+5 -4
View File
@@ -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
+3 -3
View File
@@ -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
+2 -3
View File
@@ -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",
+8 -8
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

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>
-12
View File
@@ -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
View File
Whitespace-only changes.
+25 -40
View File
@@ -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"
+2 -1
View File
@@ -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() {
+121 -35
View File
@@ -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(
+3 -2
View File
@@ -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
+116 -30
View File
@@ -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
+3 -2
View File
@@ -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"
+11 -10
View File
@@ -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}"
+7 -4
View File
@@ -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
+30
View File
@@ -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}"
-127
View File
@@ -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}" "$@"
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# Copyright 2026 Mocktail Project Authors
# Licensed under the Apache License, Version 2.0.
"""Summarizes `--profile` traces as a Markdown table for pull requests.
Usage: summarize_profile_trace.py TRACE [TRACE ...]
Each trace gets its own column, so `before.json after.json` compares them.
Frame times come from the "frame interval (ms)" counter; every other row is
a slice name recorded by the Vulkan adapter.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import math
from pathlib import Path
import sys
from typing import Iterable
FRAME_COUNTER = "frame interval (ms)"
@dataclass(frozen=True)
class Distribution:
count: int
total: float
mean: float
p50: float
p95: float
p99: float
max: float
@dataclass(frozen=True)
class Summary:
frames: Distribution | None
slices: dict[str, Distribution]
def _nearest_rank(ordered: list[float], percent: float) -> float:
rank = max(1, math.ceil(percent / 100 * len(ordered)))
return ordered[rank - 1]
def _distribution(values: list[float]) -> Distribution:
ordered = sorted(values)
total = sum(ordered)
return Distribution(count=len(ordered), total=total,
mean=total / len(ordered),
p50=_nearest_rank(ordered, 50),
p95=_nearest_rank(ordered, 95),
p99=_nearest_rank(ordered, 99), max=ordered[-1])
def summarize(events: Iterable[dict]) -> Summary:
frames: list[float] = []
slices: dict[str, list[float]] = {}
for event in events:
phase = event.get("ph")
if phase == "C" and event.get("name") == FRAME_COUNTER:
frames.append(float(event["args"]["value"]))
elif phase == "X":
slices.setdefault(event["name"], []).append(event["dur"] / 1000)
return Summary(
frames=_distribution(frames) if frames else None,
slices={name: _distribution(values)
for name, values in slices.items()})
def _row(label: str, cells: list[str]) -> str:
return "| " + " | ".join([label, *cells]) + " |"
def _cell(distribution: Distribution | None, field: str) -> str:
if distribution is None:
return "-"
if field == "count":
return str(distribution.count)
return f"{getattr(distribution, field):.2f}"
def _metric_row(label: str, distributions: list[Distribution | None],
field: str) -> str:
return _row(label, [_cell(item, field) for item in distributions])
def to_markdown(traces: list[tuple[str, Summary]]) -> str:
summaries = [summary for _, summary in traces]
lines = [_row("metric", [name for name, _ in traces]),
_row("---", ["---:"] * len(traces))]
frames = [summary.frames for summary in summaries]
lines.append(_metric_row("frames", frames, "count"))
if any(frames):
lines += [_metric_row(f"frame interval {field} (ms)", frames, field)
for field in ("mean", "p50", "p95", "p99", "max")]
# Largest total time first, taken from whichever trace spent the most.
totals: dict[str, float] = {}
for summary in summaries:
for name, distribution in summary.slices.items():
totals[name] = max(totals.get(name, 0.0), distribution.total)
for name in sorted(totals, key=lambda name: -totals[name]):
column = [summary.slices.get(name) for summary in summaries]
lines.append(_metric_row(f"{name} count", column, "count"))
lines += [_metric_row(f"{name} {field} (ms)", column, field)
for field in ("total", "mean", "p99", "max")]
return "\n".join(lines)
def main(arguments: list[str]) -> int:
if not arguments:
print(__doc__.strip(), file=sys.stderr)
return 2
traces = []
for argument in arguments:
path = Path(argument)
traces.append((path.name, summarize(json.loads(path.read_text()))))
print(to_markdown(traces))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+35 -54
View File
@@ -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 &gt;/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 &amp;&amp; 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>
+43
View File
@@ -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>
+17 -11
View File
@@ -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;
}
+36
View File
@@ -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>
+225
View File
@@ -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
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more