Compare commits

..
351 Commits
Author SHA1 Message Date
InstanceBrick e409d280ce Validate native CMake build values across cache option types 2026-10-16 15:49:54 -04:00
InstanceBrick 932b9ea7d9 Add optional desktop mirror and native ARM64 release packaging 2026-10-16 15:43:07 -04:00
InstanceBrick 7dc4f55642 Add native ARM64 Steam Frame VR and eye-tracked rendering
Integrate Frame OpenXR input, SteamVR video controls, gaze foveation, native audio, frame pacing and source-based packaging.
2026-10-16 15:31:49 -04:00
vii adccf4e484 Merge branch 'main' into vr 2026-10-16 01:22:27 -05:00
vii c4be3de14a fix(runtime): start a queued game after leaving the current one 2026-10-16 01:19:29 -05:00
dim-ghub 8370850bb7 docs(changelog): merge the two Changed sections under Unreleased 2026-10-16 01:14:12 -05:00
vii 876f8b8c32 perf(runtime): compile menu Lua to native code and switch it off in games 2026-10-16 01:13:49 -05:00
dim-ghub 1854e00934 feat(vr): add SMAA as a vr.anti_aliasing choice
vr.anti_aliasing now takes fxaa, smaa or off. SMAA runs the reference
implementation (third_party/smaa, MIT) at its High preset: luma edge
detection and blending weights into per-eye images, then neighborhood
blending into the runtime's swapchain through FXAA's final pass. Its area and
search lookup textures are uploaded once per device. If SMAA cannot be set
up, the eyes fall back to FXAA.

Also export vr.anti_aliasing from config.yaml: the VR backend reads the
process environment, so the setting only worked as an environment variable.

On a Quest 2 through SteamVR at 120 Hz (2136x2236 per eye), SMAA ran at 118.6
fps against 118.8 for FXAA, with p99 frame times of 10.1 and 9.5 ms.
2026-10-16 01:12:21 -05:00
vii c614fba032 feat(profile): trace emulated ETC2 texture VRAM and report its peak
Emulated images keep a running total of their decoded and ETC2/EAC sizes over every mip and layer. --profile writes both as counters, and summarize_profile_trace.py prints their peaks.
2026-10-16 00:53:05 -05:00
dim-ghub 0221738952 Merge remote-tracking branch 'origin/main' into vr 2026-10-16 00:52:17 -05:00
dim-ghub 046632cf99 fix(input): render emoji in the TextBox overlay (#24)
The overlay's fallback fonts were the first few fontconfig sans matches,
which rarely include an emoji font. Fall back to Roblox's bundled
TwemojiMozilla and RobloxEmoji fonts, then the system emoji font, before
the sans list.
2026-10-16 00:50:10 -05:00
dim-ghub 753a3f8106 feat(vr): anti-alias the headset eyes with FXAA
Roblox's VR debug device renders both eyes into single-sample textures, so
edges were jagged in the headset. When the eyes go to the runtime's
swapchain, an FXAA draw now replaces the plain copy. It samples the eye
directly, and re-encodes for sRGB swapchains so the guest's bytes round-trip
exactly. Anything it cannot set up falls back to the copy.

vr.anti_aliasing (fxaa, the default, or off) controls it. The SPIR-V is
generated from shaders/vr by scripts/generate_vr_shaders.py and checked in,
so builds need no shader compiler; a test checks it against the sources when
glslc is installed.

On a Quest 2 through SteamVR at 90 Hz (2244x2352 per eye) frame times did not
change. Captured output keeps 97 to 98 percent of pixels byte-identical and
the average brightness unchanged, and smooths the edges.
2026-10-15 20:19:17 -05:00
dim-ghub 569df68729 fix(vr): stop trapping the desktop cursor in the window during VR
Roblox's mouse lock and the RMB camera drag put the window into relative
mouse mode with a grab, which locked the cursor inside it while the headset
was in use. In VR the controllers aim and the window only mirrors the left
eye, so the pointer is never captured and the system cursor stays visible.
2026-10-15 19:55:35 -05:00
dim-ghub 043964a5dc fix(vr): say why auto found no headset, and document vr.mode
Without a headset SteamVR refuses the OpenXR client at xrCreateInstance
(VRInitError_Driver_WirelessHmdNotConnected) instead of reporting no system,
so say that it usually means no headset is connected. Describe vr.mode in the
README, docs/VR.md and the changelog.
2026-10-15 19:42:40 -05:00
dim-ghub ea248b1ecf feat(vr): add vr.mode with on, off and auto
vr.mode in config.yaml picks how Nightcap starts: off on the desktop, on in
VR, auto in VR only when this user's SteamVR is running and its OpenXR
runtime reports a headset. auto never starts SteamVR, and asks its runtime
from a short-lived copy of the process so the runtime's libraries do not load
before the VR backend arms. vr.enabled still works as on/off, and --vr or
--no-vr still override the file for one launch.
2026-10-15 19:37:32 -05:00
dim-ghub 35033454b0 docs(vr): note the frame-stall fix and the new perf stages 2026-10-15 17:21:09 -05:00
dim-ghub df025d2eb7 fix(vr): publish the shared-memory mirror a frame later instead of waiting
With the eye copies no longer waited for, MOCKTAIL_VR_MIRROR_SHM made the CPU
wait for the whole frame before publishing, which cut the frame rate from 117
to 82 fps at 120 Hz. Publish each readback at the next copy, when the GPU has
finished it. The external viewer sees each frame one frame later.
2026-10-15 14:58:26 -05:00
dim-ghub dea1d5fd59 test(vr): cover handing present dependencies to the GPU
Move the empty wait-and-signal batch into internal::ChainPresentDependencies,
like WaitForPresentDependencies, and test that it waits on every present
semaphore, signals the replacement, never blocks the CPU, and leaves a failed
submit to the guest.
2026-10-15 11:43:51 -05:00
dim-ghub 9ae5667cd7 perf(vr): read per-frame settings once and skip the lock for other passes
The frame cycle read the pose source, evidence and mirror settings from the
environment every frame. Read them once. Check the thread-local eye request
before taking the backend lock, so render passes that are not eye passes do
not contend with the frame cycle.
2026-10-15 08:19:04 -05:00
dim-ghub 7296520064 perf(vr): order eye copies and the desktop mirror on the GPU
Every frame the CPU waited for the GPU to finish Roblox's whole frame in the
desktop mirror, then again for the eye copy, so CPU and GPU work never
overlapped. The mirror blit now consumes the guest's present semaphores and
signals a per-image semaphore that the present waits on. Present preparation
hands remaining waits to the GPU the same way. Eye copies are submitted from a
small ring of command buffers and fences and rely on queue order, so the CPU
only waits when the GPU is three frames behind or the guest destroys an image
a copy still reads.

On Monado at 120 Hz with 1280x1440 eyes this removed 2 to 5 ms of waiting per
frame and kept the frame rate at 118 to 119 fps instead of 103 to 118.

Also free the copy resources when the guest device goes away without a
session, so a later device never reuses stale handles.
2026-10-15 04:52:37 -05:00
vii a3a2b76a06 feat(update): add an evidence-scoring solver for HostAbi addresses with a check command 2026-10-15 01:49:19 -05:00
vii f333399e18 test(update): add a 2908 to 3222 regression set for the HostAbi deriver
Share the input checks and FMOD slot tables so the solver reuses them.
2026-10-15 01:49:19 -05:00
dim-ghub e094a4c89d feat(vr): time the desktop mirror, present prepare and frame interval
MOCKTAIL_VR_PERF now reports how long the CPU waits in the desktop mirror and
in present preparation, and the time between presents, so per-frame stalls
outside the XR frame cycle show up in the numbers.
2026-10-15 01:37:12 -05:00
vii de0ddf7864 feat(update): solve the VR device bridge and compare it with the VR offset finder 2026-10-15 02:20:13 -05:00
vii e553db2001 fix(vr): point 3092 FixRegisterMotors at the flag Roblox reads
The 3092 table had the address of another field in the same registration, so the flag never turned on.
2026-10-15 02:20:13 -05:00
vii 44ec71f826 Merge branch 'main' into vr
# Conflicts:
#	FAQ.md
2026-10-15 01:57:30 -05:00
vii 3062909209 refactor(jnivm): resolve method names once and log calls with no handler
GetMethodID maps each name to a JniMethod enum, so dispatch compares
numbers instead of strings. Calls that reach no handler print one
"no handler for" line per class, method and signature. Unknown
gameActivity_ callbacks are recorded on the CallVoidMethod path too.
The FMOD write path reuses a per-thread buffer instead of allocating.
2026-10-15 01:31:02 -05:00
vii 049784d0f8 fix(docs): restore the screenshots at full quality with transparency 2026-10-15 01:31:02 -05:00
vii ccf910f064 chore(ci): keep the signing and AUR keys in a release environment
The release environment only accepts main and v* tags, so a workflow pushed on another branch cannot read the keys. Flatpak builds that do not publish run without it.
2026-10-15 01:26:27 -05:00
dim-ghub 42b5138b11 fix(vr): deliver hand-motor and HapticEffect haptics the way games aim them
The Android client drops HapticService:SetMotor on the Gamepad1 LeftHand and
RightHand motors: its haptics backend only consumes Large and Small. VR games
such as VR Hands drive those motors, so they never vibrated.

- Interpose HapticService::SetMotor through its reflection descriptor, call
  the original, and forward Gamepad1 LeftHand/RightHand to OpenXR. The
  descriptor and the engine's intensity reader are found by pattern at load,
  for both the 2998 (tuple span) and 3222 (shared tuple) reader ABIs. The held
  level renews a 50 ms lease every presented frame.
- Report the hand motors at x = -1/+1 in GetMotors. Positional HapticEffects
  aim at a hand there with a 0.5 radius (VOXEL DESTRUCT 2 does), so at +-0.2
  every effect mixed to zero.
- Play SetMotorState buffers sample by sample with trailing silence trimmed.
  Roblox sends 100 x 25 ms look-ahead buffers; taking the peak for the whole
  buffer turned a short bump into 2.5 s at full strength.
- Stop vibrating from the DebugDeviceVR haptic slot. The engine pulls it every
  frame but with each mix buffer's first sample, held about a second, so it
  duplicated the buffers as a long buzz.
- Enable SupportGenericHapticsOnVR and FixRegisterMotors with the other haptic
  flags.
2026-10-15 01:12:01 -05:00
vii 2eaba9048b fix(vr): reload the OpenXR runtime with its own Vulkan loader instead of pinning it
The Vulkan adapter exports vk* symbols globally, so a runtime loaded again after it bound its Vulkan calls to the adapter and deadlocked the join. Pinning avoided the reload but left SteamVR with its old VkInstance, which aborted the join with "vkGetInstanceProcAddr: Invalid instance". Later instances now load the runtime libraries with RTLD_DEEPBIND and let them unload with their instance.
2026-10-14 22:31:55 -05:00
vii a2ffa5c811 Merge branch 'vr' of CoderDayton/nightcap into vr 2026-10-14 04:29:10 -05:00
vii 2be319a979 fix(vr): keep the OpenXR runtime loaded so a game join does not deadlock 2026-10-14 04:19:58 -05:00
vii 6cf641d472 feat(vr): find the haptic flags with the offset finder and enable them on 3092 2026-10-14 03:14:16 -05:00
vii 3788026d10 fix(vr): limit haptics to VR sessions and check flag names before writing
- Register HapticProtocol and report motors only when MOCKTAIL_VR_ENABLED is on.
- Drop the per-message [haptic] dispatch log; log each OpenXR haptic result once per hand.
- Write a fast flag only when its name pointer matches the expected name.
- Remove the state-sync fallback that dlsym can never resolve.
2026-10-14 03:14:16 -05:00
dim-ghub f3e68fc3b7 feat(vr): implement OpenXR controller haptics and bypass mobile suppression
- Implement RobloxHapticProtocolBridge for MessageBus HapticProtocol
  (SupportsHaptics, GetMotors, SetMotorState, UpdateSingletonVibration).
- Wire bridge into RobloxExperienceComposition and direct game runtime.
- Call JNI NativeGLInterface.nativeUpdateMobileHapticsState during startup
  and game completion.
- Enable FFlagKeepHapticsOnMobileWhenGamepadConnected and FFlagVRHaptics in
  VR client settings overrides to bypass gamepad haptic suppression in libroblox.
- Directly activate KeepHaptics and VRHaptics fast flag bytes in memory
  within RobloxVrDeviceBridge::Activate for builds 2998 and 3222.
- Check xrApplyHapticFeedback result code and add diagnostic logging.
2026-10-14 01:32:32 -05:00
vii 23e7831149 fix(site): keep the screenshots transparent
The first retake was flattened to RGB, which turned the transparent window edges black.
2026-10-13 21:34:07 -05:00
vii 5b2ee2c7f3 docs(site): rewrite the description and feature list, add SEO tags, refresh screenshots
New intro and feature list in the README and the site. The site gets a title with Roblox, link preview tags, structured data, a sitemap, robots.txt, a preview image and the Search Console tag. Screenshots are retaken, the old lobby shot is removed and the store listing points at the new ones.
2026-10-13 21:29:12 -05:00
vii 63ed62b4aa docs(faq): point the VR answer at Nightcap's vr branch 2026-10-13 20:21:33 -05:00
vii 0b0d8067d8 docs(vr): list supported builds, move VR detail into docs/VR.md and update the FAQ 2026-10-13 20:20:51 -05:00
vii 87ac6ee572 test(vr): cover the keyboard upload and quad layer on a real OpenXR runtime 2026-10-13 19:58:40 -05:00
vii fcc8b5382a fix(vr): turn off the desktop flyout side panel in VR 2026-10-13 19:58:40 -05:00
vii 9826535102 fix(vr): align the IsVRAppBuild hook so unoptimized builds do not crash Roblox 2026-10-13 19:58:40 -05:00
vii a30f05fc5b fix(vr): keep the bridge and backend alive until exit for guest threads still in a hook 2026-10-13 18:37:35 -05:00
vii 1ff05e4e6f fix(vr): do not recover an XR session when the restarted runtime uses another GPU 2026-10-13 18:37:35 -05:00
vii 123968be89 test(assets): pin overlay priority and path safety when the root is the content folder 2026-10-13 18:37:35 -05:00
vii 393297a439 feat(vr): support Roblox 3222 (2.742.1059) 2026-10-13 18:37:35 -05:00
vii 7ec8304d8f fix(vr): stop keyboard click-through, redraw churn and upload leaks; build its drawing only with VR 2026-10-13 18:37:34 -05:00
vii f9289d9947 fix(vr): read the pointer hand and HeadScale offsets per build and check them at start-up 2026-10-13 18:37:26 -05:00
vii 7294a46e8a fix(vr): turn on Vulkan queue locking when the adapter loads after the backend 2026-10-13 18:37:19 -05:00
vii 4b67176970 fix(scripts): find VR offsets when Roblox moves the pointer fields
Build 3222 moved the hand selector, hand frame and camera HeadScale fields, so the fixed byte patterns found nothing. Match the code shape instead, read the three offsets from it and write them to the output.
2026-10-13 17:15:39 -05:00
dim-ghub 228121157b vr: drop test hooks and dead code, document every VR variable
- Remove the MOCKTAIL_VR_TEST_SESSION_LOSS_AFTER fault injection; nothing
  used it.
- The scripted head pose and the eye evidence dumper only work in builds
  configured with -DMOCKTAIL_VR_DIAGNOSTICS=ON (default OFF); release
  builds ignore their environment variables.
- Remove FindFmodInputCaptureProfile and HasExpectedFmodInputCaptureVtable
  (only their own tests called them) and the never-defined
  PrepareCaptureLocked declaration; the audio bridge files match upstream
  again.
- docs/VR.md lists every MOCKTAIL_VR_* variable with its default, linked
  from the README.
2026-10-13 00:28:26 -05:00
dim-ghub a06c7de93e vr: fix bridge hand math and make its shutdown safe for live guest threads
- MOCKTAIL_VR_HAND_POSE_SOURCE=grip now rotates hand records from the grip
  pose. base_ori started as the identity quaternion, so the norm check
  never fell back to it and hand rotation was dropped.
- Hand records cancel the engine's stored hand pitch (read from
  hand_pitch_storage_rva by the pointer hook, 20 until read) instead of a
  hard-coded 20, so they agree with the pointer ray.
- Shutdown never rewrites the aim-pointer prologue while guest threads may
  be executing it; the jump stays and only forwards once the bridge is gone,
  and a later activation reuses it.
- Guest hooks count themselves in flight before loading the bridge pointer;
  Shutdown unpublishes the bridge and waits for in-flight calls to drain,
  so no hook keeps a pointer to a destroyed bridge.
- Roblox's original state and eye getters are kept for the process
  lifetime. A hook reached with no bridge, including after a failed vtable
  restore, forwards to them instead of returning an unfilled state buffer.
  Restoring the vtable no longer depends on the pointer hook.
- The activation log names the vtable slots in the right order.
2026-10-13 00:24:45 -05:00
dim-ghub 8106203c8e vr: fix eye colour formats, mirror segment safety and frame-wait locking
- Once the guest eye format is known, the eye swapchains are recreated
  once in the format that keeps its bytes: the eye format, else its
  UNORM/SRGB sibling (raw copy), else the other channel order with the same
  sRGB-ness (a blit that only swizzles). Mixed order-and-gamma cases no
  longer go through a gamma-converting blit; if only that remains, it is
  logged.
- The desktop mirror segment is created with O_EXCL after removing our own
  stale segment (a crashed run's leftover), and must be private to this
  user before eye images are written to it.
- A new frame_mutex_ is held for the whole XR frame cycle and by every path
  that can destroy the session (taken before mutex_). That lets the cycle
  release mutex_ while it blocks in xrWaitFrame, so guest render-thread
  hooks no longer wait out the runtime's pacing. Swapchain-image and fence
  waits stay under mutex_; they return promptly unless the GPU or
  compositor stalls, and they run while guest eye bindings are copied.
2026-10-13 00:08:22 -05:00
dim-ghub 0603734ff6 vr: recover from XR failures instead of giving up on headset output
- Every frame error, including the swapchain-image and copy-fence
  timeouts, now tears the session down recoverably instead of ending XR
  output for the rest of the run.
- Session recovery backs off (1, 2, 4, 8 s) and recreates the XrInstance
  after an instance-loss event, an instance/runtime failure, or three
  failed attempts, so VR returns after SteamVR or WiVRn restarts instead
  of retrying forever.
- While no XR session runs (headset idle, session not READY, recovering)
  presents are paced at 60 Hz outside the backend lock instead of running
  unthrottled.
- A PrepareHostPresent failure that consumed nothing (copy resources
  unavailable, failed fence reset or submit) presents normally instead of
  failing the guest's vkQueuePresentKHR; only device loss is propagated.
- A runtime/guest GPU mismatch falls back to the plain host device and
  keeps the game running without headset output, naming both GPUs.
- A failed EGL attach on the GLES path no longer makes window init fail;
  the game runs without headset output and the bind is not retried.
2026-10-13 00:04:42 -05:00
dim-ghub 0b5578d80b vr: keep non-VR launches on their original paths
Queue and device-idle locks in the Vulkan adapter are now taken only after
an armed OpenXR backend calls mocktail_vulkan_enable_queue_sharing(). The
guest synchronizes its own queue use; only the runtime and the eye copies
add a second user. Without VR, and in builds without it, submits and
presents take no locks.

FFlagDebugEnableVREmulator is only written when VR is active, so a user's
own fflags.json value is no longer overridden on every launch.

Config loading is back after the single-instance lock and the support
bundle guard. A non-VR build still rejects --vr before creating any data
or state directories, from the command line alone; a vr.enabled config in
such a build is rejected once the config is loaded.
2026-10-12 23:59:46 -05:00
dim-ghub bafbe4268c vr: name the build and the supported builds when VR is unavailable
An explicit --vr on a Roblox build without VR support now exits with the
build's version and Build ID, the builds VR supports, and how to select one
(--roblox-lib). When VR is only enabled in the config, the launch continues
without VR and logs the same details. The bridge's own Install error names
both as well. The verified build contracts carry their version labels and
move to namespace scope so they can be listed.
2026-10-12 23:55:37 -05:00
dim-ghub 3ef81c3d93 vr: make VR opt-in and build without the OpenXR submodule
VR now stays off at runtime in every build until --vr or vr.enabled: true,
so a normal launch on a VR-capable build never tries to reach an OpenXR
runtime and no longer exits when none is running.

MOCKTAIL_ENABLE_VR defaults to AUTO: OpenXR support is built when the
third_party/OpenXR-SDK submodule is present and skipped with a notice
otherwise, so source packages that do not fetch submodules (Nix, AUR) keep
building. ON still requires the submodule. mocktail-vr-probe is only built
and installed in VR builds.

CI gains an x86_64 job that builds and tests with MOCKTAIL_ENABLE_VR=OFF.
The config docs and README describe the new defaults.
2026-10-12 23:53:31 -05:00
dim-ghub 360ab6af58 vr: fix CI failures from the 3092 profile
Mark the 3092 profile experimental. It still runs when selected with
--roblox-lib, but it no longer becomes the preferred update target for
every x86_64 user, so the shipped payload metadata (2998) is again the
preferred profile.

Select the 2998 entry by Build ID in the partial-input profile test instead
of relying on manifest order, and drop the shipped 3092 entry before the
test re-identifies 2998 as 3092.

Align the example config's VR build wording with the bootstrap template.
2026-10-12 23:48:32 -05:00
dim-ghub 4cc0e41ef3 vr: add in-headset virtual keyboard for TextBox input
When Roblox asks for a keyboard (showKeyboard), a world-locked panel
opens below eye level as an OpenXR quad layer. Controllers point at keys
and the trigger types; text and key presses go through the same platform
events as the desktop keyboard, so the focused TextBox edits identically.

The layout is US QWERTY without the function row or Ctrl/Alt, with one-shot
Shift, Caps Lock, Tab, Backspace auto-repeat, Enter and Close. A trigger
aimed at the panel is withheld from Roblox so a key press is not also a
click. A text field above the keys mirrors the focused TextBox, whose
desktop overlay never reaches the eye images. MOCKTAIL_VR_KEYBOARD=0 turns
it off.
2026-10-12 23:45:51 -05:00
dim-ghub ddc985218b vr: add separate VR frame-rate limit, render scale and unpaced benchmark mode
vr.frame_rate_limit / MOCKTAIL_VR_FRAME_RATE_LIMIT replaces
graphics.frame_rate_limit while VR is active and takes the same values.
It defaults to unlimited so xrWaitFrame paces Roblox at the headset
refresh rate instead of Roblox's own 60 FPS cap.

MOCKTAIL_VR_RENDER_SCALE scales the runtime's recommended eye extent.
SteamVR's recommendation for a Quest 2 at 90 Hz (2244x2352 per eye) missed
the 11.1 ms budget and halved the rate to 45 FPS.

MOCKTAIL_VR_UNPACED=1 stops guest frames from blocking in xrWaitFrame:
the open XR frame spans several guest frames and is submitted once per
display period, for measuring maximum render rate.
2026-10-12 22:44:46 -05:00
dim-ghub 33dd7769f0 Merge remote-tracking branch 'upstream/main' into vr
# Conflicts:
#	stubs/libandroid_stub.cc
2026-10-12 21:50:15 -05:00
dim-ghub 88bb4c940c vr: align pointer ray pitch with hand orientation and reset world scale to 1.0 2026-10-12 21:28:20 -05:00
dim-ghub 68f8ec82d2 vr: fix sRGB double-gamma, home world avatar spawn, and controller aim alignment 2026-10-12 20:25:21 -05:00
dim-ghub 509822974b vr: Recreate OpenXR instance on guest Vulkan instance recreation
SteamVR (vrclient.so) binds its Vulkan renderer to the lifetime of the
XrInstance. When Roblox destroys the LuaApp Vulkan device and creates a
second VkInstance/VkDevice for UGC experiences, reusing the original
XrInstance causes SteamVR to dereference freed device handles in xrEndFrame.

Tear down and recreate the XrInstance whenever a new guest Vulkan instance
is initialized, resetting session state, spaces, and controller actions.
2026-10-12 05:42:33 -05:00
dim-ghub a441fa7110 assets: fix shader pack path resolution and root directory normalization 2026-10-12 02:16:21 -05:00
dim-ghub d54d20de64 audio: align input capture test with shared_ptr atomic store and restore FMOD profile functions 2026-10-12 01:45:08 -05:00
vii 81de528668 feat(packaging): publish nightcap and nightcap-bin to the AUR on release
Pushes nothing until the AUR_SSH_PRIVATE_KEY secret is set. Recipes move to 0.3.4.
2026-10-12 01:44:21 -05:00
vii ff81882cb0 docs: point Arch users to nightcap-git on the AUR
The package is maintained by @dim-ghub.
2026-10-12 01:44:21 -05:00
dim-ghub f9fede1649 Merge remote-tracking branch 'mocktail/vr' into vr 2026-10-12 01:39:13 -05:00
vii a87373d003 feat(assets): let the overlay replace files Roblox downloads
Roblox maps some rbxasset:// names to downloads and opens no local file for
them. For each file under content/ in the overlay folder, change its name in
the loaded libroblox so Roblox opens the local file.

Idea from @dim-ghub in #20.
2026-10-12 01:20:13 -05:00
vii b46098c211 feat(assets): trace asset lookups that find no file 2026-10-12 01:20:13 -05:00
vii 144350090e docs: describe the asset overlay folder 2026-10-11 23:58:26 -05:00
vii 3ed0418997 refactor(assets): share the asset lookup cache insert 2026-10-11 23:58:26 -05:00
vii 8410070eec Merge pull request #19 from dim-ghub/main
Add Sober-compatible asset overlay system
2026-10-11 23:56:58 -05:00
dim-ghub a1166fb6ba Add Sober-compatible asset overlay system 2026-10-11 23:11:54 -05:00
vii c9eb4fb7f0 docs(changelog): credit @qutad for the anti-aliasing fix 2026-10-10 22:02:40 -05:00
vii b59c070cc0 docs(changelog): release 0.3.4
Report Nightcap's own version instead of Mocktail's.
2026-10-10 21:27:00 -05:00
vii 4a9acf8c49 feat(site): redesign the landing page and legal pages 2026-10-10 21:07:33 -05:00
vii c342bab7b5 docs(changelog): note the voice chat fix after a Roblox update 2026-10-10 00:33:00 -05:00
vii a3b9c872a5 fix(update): derive the FMOD bridge from the shipped reference when the installed profile lacks it 2026-10-10 00:33:00 -05:00
vii 4f45baf3f7 docs(changelog): note the anti-aliasing and voice chat microphone fixes 2026-10-10 00:09:42 -05:00
vii 0ddb546ba8 chore(merge): merge upstream komaruworld/mocktail main 2026-10-10 00:09:42 -05:00
vii 7ba752ebb7 docs(changelog): bring the unreleased section up to date 2026-10-09 23:57:41 -05:00
vii 227895faa4 feat(window): use Wayland on NVIDIA Wayland sessions
The X11/XWayland fallback guarded against the explicit-sync disconnect
that is fixed, so the rule and its driver detection are removed.
2026-10-09 23:57:41 -05:00
vii dfc3c8d555 docs(changelog): note the Roblox 2.742 update and new interface fixes 2026-10-09 23:48:55 -05:00
vii e490dff97f feat(runtime): show the desktop side panel menu by default 2026-10-09 23:48:55 -05:00
vii b5b9990017 fix(services): let the BrowserTracker bootstrap use Roblox's storage directory
Tighten a group-writable directory this user owns, reject a storage file
this user cannot vouch for, and treat a stored ID of "0" as unset.
2026-10-09 23:48:55 -05:00
vii d278c0fe0a fix(runtime): run nativeAppBridgeAppStart by default
It starts Roblox's StartupController, which fetches the account's
flag-linked experiments and writes ClientSettings/IxpSettings.json.
2026-10-09 23:48:55 -05:00
vii 044f97e476 fix(update): accept payloads that reorder the leading constructors
Roblox 2.742 moves the process-load constructor behind the JNI singleton
initializer and renames registers in the registry initializer. The deriver
finds each leading constructor by its code, and the loader checks that host
bridges run the checkpoint and JNI singleton constructors and skip the two
wiring entries.
2026-10-09 23:48:55 -05:00
vii 321ba5b3a2 test(flatpak): keep a host flatpak-builder out of the missing-builder check 2026-10-09 14:19:01 -05:00
vii fd364a7bf2 docs: wrap the changelog at 110 columns and drop the glyphs roadmap item
Add a pre-commit check for added changelog lines over 110 characters. Roblox has no Nintendo glyph set, so the roadmap item cannot be met.
2026-10-09 13:50:46 -05:00
vii 5253563e3f fix(appimage): restore a distribution package when its reduced build is too old
A reduced gtk4 older than the one libadwaita requires failed the nightly configure step.
2026-10-09 13:49:38 -05:00
komaruworld 551d2a1829 Probably a vc fix 2026-10-09 15:49:35 +03:00
farwhile 37b88103f4 fix(runtime): stop forcing single-sample MSAA (#185)
* fix(runtime): stop forcing single-sample MSAA

* test removed

* Fix race in webview helper launcher test
2026-10-09 08:33:32 +03:00
vii 763faed0e3 fix(graphics): pick the Vulkan driver that matches the bound kernel driver
Skip a GPU whose kernel driver has no installed Vulkan driver and try the next one. Log why when none can run Vulkan.
2026-10-08 18:41:36 -05:00
komaruworld 74eedada0b improve evilbsd support
Netbsd hi
2026-10-09 00:45:05 +03:00
vii 6cb3dd00a9 fix(compat): bound Bionic strerror_r by errno table and gate atoi shim to non-glibc
strerror_r: Bionic's message table covers errno 0..133 except the
unassigned 41 and 58. Numbers outside that set now get "Unknown error N"
without consulting the host, so the text is the same on glibc and musl and
no longer relies on comparing against strerror_r(-1).

atoi: glibc's atoi is (int)strtol() like Bionic's, so the host symbol is
used there as before. The Bionic shim is registered only on libcs where
that is not true (musl). linker_registry_test expects the matching result.
2026-10-08 16:10:03 -05:00
vii fe2f641a9e chore(merge): merge upstream komaruworld/mocktail main
Brings in 3e6f63e (drop libexecinfo), aa31cdf (Chimera/musl portability),
bc10810 (Bionic atoi, canary signal exit codes) and cb2e9dd (CI fixes).

Conflicts:
- tests/native_update_test.cc: keep both <cerrno> and <csignal>.
- tests/webview_helper_launcher_test.cc: keep the existing python3 socket
  read; it already solves the dash multi-digit fd redirection problem that
  upstream fixed by switching the helper to bash.
2026-10-08 16:10:03 -05:00
komaruworld cb2e9dd598 Small ci fix 2026-10-08 21:12:29 +03:00
komaruworld bc108106db mog chimera 2026-10-08 18:14:23 +03:00
komaruworld aa31cdf395 Improve chimera support 2026-10-08 16:59:11 +03:00
komaruworld 3e6f63ea93 remove libexecinfo dependency 2026-10-08 16:13:41 +03:00
vii bfa6811417 docs(changelog): note the crash and hang fixes 2026-10-07 14:22:39 -05:00
vii 84502c0a1a fix(window): show the hidden window from the main thread
The render thread called SDL_ShowWindow outside the surface commit gate. It now sets a flag, and PumpEvents shows the window inside the gate.
2026-10-07 14:20:16 -05:00
vii 46d1d5ffbe fix(graphics): retry swapchain creation with the game's present mode
A driver can reject a present mode the device did not enable. Retry once with the mode the game asked for, except after errors another mode cannot fix.
2026-10-07 14:20:16 -05:00
vii 545fd8344f fix(session-log): stop waiting for the log writer after 2 seconds
A child that kept the log pipes open made Stop wait forever. Poll for the writer, then SIGKILL it after 2 seconds.
2026-10-07 14:20:16 -05:00
vii 4c40fcdc99 fix(jnivm): stub missing JNI functions and ignore repeat releases
Fill the 65 unimplemented JNI table slots with stubs that log once and return zero, so a call no longer jumps to address 0. A release of a handle with no reference count now returns, so a double release cannot queue one slot twice and give two objects the same handle.
2026-10-07 14:20:16 -05:00
vii d4c7a6f741 fix(app-shell): answer LAUNCH_CONVERSATION so chat requests open 2026-10-07 13:04:31 -05:00
vii 491925f59e test(graphics): register texture_override_test with ctest 2026-10-07 12:01:14 -05:00
vii 2917bc5423 chore(merge): merge upstream komaruworld/mocktail main 2026-10-07 12:01:14 -05:00
komaruworld 2451264696 ETC2 texture fixes
Refs #191
2026-10-07 19:02:35 +03:00
komaruworld a9a96c6861 Add zsync file
fixes #182
2026-10-07 17:35:33 +03:00
vii 5bf4eeb41b fix(window): keep main-thread surface commits out of host present
With explicit sync, a wl_surface commit from the SDL thread can land
between the driver's sync points and its buffer attach. The compositor
answers with a no_buffer protocol error and disconnects the client.
2026-10-06 19:24:27 -05:00
vii d937dd7ea6 fix(flatpak): ask for the X11 socket only as a fallback 2026-10-04 21:08:23 -05:00
vii 169e65bf60 docs(changelog): release 0.3.3 2026-10-04 20:44:33 -05:00
vii 69476f0f47 docs(changelog): add theme crash and Flatpak NVIDIA fixes 2026-10-04 20:14:25 -05:00
vii 79d0cdfb40 chore(merge): merge upstream komaruworld/mocktail main
Keep our captured-pointer handling in place of upstream's relative_mode
version. Keep upstream's motion coalescing guard on our relative flag.
2026-10-04 20:14:13 -05:00
komaruworld 273209b9ea Fix angle platform selection 2026-10-03 21:19:26 +03:00
komaruworld bbfd603e17 Fix captured mouse input
Refs; #135
2026-10-03 14:28:12 +03:00
komaruworld 1ba2941e7f Fix theme crash
Fixes #144
2026-10-03 14:03:25 +03:00
komaruworld 68c90ce428 flatpak nvidia xwayland fallback
#186
2026-10-03 13:48:39 +03:00
komaruworld 9a1ccbb5ce add roblox 3092 support
Also fix openxr shit
2026-10-03 13:40:24 +03:00
vii 068f07e125 chore(merge): merge upstream komaruworld/mocktail main 2026-10-02 20:02:32 -05:00
vii a469689bbb fix(webview): use GTK's software renderer when the helper composites in software
GTK's GL renderer aborted in libepoxy while snapshotting a page switch, which closed the web view. GSK_RENDERER set by the user still wins.
2026-10-02 20:02:32 -05:00
vii 51836a980d docs(changelog): add follow-user, Nix desktop entry, text focus and sky fixes 2026-10-02 20:02:32 -05:00
vii ad546a7781 Merge pull request #16 from kimlherme/docs/recovery-marker-cleanup
docs: document leftover recovery marker cleanup
2026-10-02 19:37:49 -05:00
kimlherme 3c45450dfc docs: document recovery marker cleanup 2026-10-02 20:21:34 -03:00
vii 185c11b57f test(update): expect a missing abi to count only on x86_64 hosts 2026-10-02 12:52:16 -05:00
vii 2c3d7b485e test(update): declare the guest ABI in test catalogs so aarch64 matches
Profiles without abi default to x86_64, so every payload store test skipped its catalog on arm64.
2026-10-02 12:44:47 -05:00
komaruworld 6f8344a28b Sky fixes 2026-10-02 16:52:27 +03:00
vii b60c43e8f0 chore(merge): merge upstream komaruworld/mocktail main
Keep both the CallProtocol and AccountProtocol bridges. Keep our pointer
hold and catalog validation. Re-apply our JNI local-frame counting,
singleton handles, class handle cache, handle-table report and thread
detach release on upstream's split jnivm files.
2026-10-01 21:41:43 -05:00
vii b5e4273277 docs(changelog): add updater, audio and platform fixes 2026-10-01 19:26:21 -05:00
vii d2858fedc8 Merge pull request #13 from kimlherme/fix/payload-update-correctness
fix(update): preserve rollback state and validate payload activation
2026-10-01 19:25:58 -05:00
vii 326a0659ea Merge pull request #15 from kimlherme/fix/platform-lifecycle-compatibility
fix(platform): correct Vulkan teardown, pointer capture and DNS flags
2026-10-01 19:25:51 -05:00
vii dd45467056 Merge pull request #14 from kimlherme/fix/audio-jni-ownership
fix(runtime): preserve OpenSL tickets and JNI array references
2026-10-01 19:25:40 -05:00
vii 94b1036296 docs(changelog): add ETC2 decode speedup 2026-10-01 19:14:37 -05:00
vii 4c61daad0c Merge pull request #10 from glook9001/simd-etc2-decoder
perf(graphics): SIMD PSHUFB ETC2/EAC decode with bit-exact scalar verification
2026-10-01 19:14:33 -05:00
kimlherme f5f224875a fix(platform): correct Vulkan teardown, pointer capture and DNS flags 2026-10-01 21:05:31 -03:00
kimlherme 90d4891a4e fix(update): preserve rollback state and validate payload activation 2026-10-01 21:05:30 -03:00
kimlherme 46dd99266c fix(runtime): preserve OpenSL tickets and JNI array references 2026-10-01 21:05:30 -03:00
77fce18a95 po co chodzic, mozna latac (#183)
* po co chodzic, mozna latac

* Fix jni ownership, split helpers

---------

Co-authored-by: Mia <uwie@thinkwise.ro>
Co-authored-by: komaruworld <rtysus228@gmail.com>
2026-10-01 20:31:52 +03:00
daeeseD ecb80b3ee1 vr: capture the microphone for voice chat on Build 2998 (#181)
* vr: capture the microphone for voice chat on Build 2998

On the vr branch Roblox 2.736.1408 joins voice chat and plays other
players, but never opens a microphone: its FmodAudioDevice capture
methods have no working host path, and main's native capture (537828a
"Fix vc") is pinned to 2.738 RVAs.

Port main's NativeInputCapture and interpose the FmodAudioDevice capture
slots for Build 2998. 2.738 inserted two device-list slots (7 and 11),
so the capture methods sit at {4, 11..15} here instead of {4, 13..17}.
The input-device slots {8, 9, 10, 16}, checked against main's input RVAs
for this image, anchor that layout before anything is patched, and each
capture slot must be a distinct method next to the input-device code.

The voice sink is called through its own vtable after checking that the
vtable and its PCM entry point lie in libroblox, and the retained
shared_ptr is released through the libc++ control-block ABI, so no
further Build 2998 RVAs are needed. If any check fails, capture stays
off and the output routes still work.

MOCKTAIL_NATIVE_INPUT_CAPTURE=0 turns the capture interposition off, and
audio.input_device: disabled reports no microphone to Roblox.

* Requested fix at PR 181
2026-10-01 19:25:48 +03:00
komaruworld d6f0f2c755 Fix some vc
Fokora
2026-10-01 18:03:02 +03:00
glook9001 c6afc5af24 perf(graphics): eliminate crop page faults with strided resampling and streaming WC loads 2026-10-01 12:08:54 +03:00
glook9001 74e7455ee1 perf(graphics): SIMD PSHUFB ETC2/EAC decode with bit-exact scalar verification 2026-10-01 12:08:35 +03:00
vii 000d021452 docs(changelog): add payload import fix 2026-09-30 21:34:06 -05:00
vii 2eef2c3821 Merge pull request #11 from kimlherme/fix/payload-import-rollback
fix(payload): preserve the previous payload on failed imports
2026-09-30 21:33:43 -05:00
Guilhermepycharm 4928443f90 fix(payload): preserve previous files across failed imports
Enable rollback before the first backup move, preserve originals during incomplete backups, and retain recovery files if restoration fails. Add offline full-importer regression coverage and register it with CTest.
2026-09-30 22:54:33 -03:00
native b37497db7b fix text overlay again (#179) 2026-09-30 17:55:30 +03:00
Lupey 6bbdc39d7e feat(nix): add desktop application (#177) 2026-09-30 17:48:05 +03:00
komaruworld 1c11336ba1 Fix text issues
bla bla bla
2026-09-30 16:56:48 +03:00
vii b19bd8406f docs(changelog): release 0.3.2 2026-09-29 23:33:26 -05:00
vii cd46538eb8 docs(readme): document Flatpak build job settings 2026-09-29 23:27:33 -05:00
vii eca5f395eb update(flatpak): default local builds to 2 jobs 2026-09-29 23:25:03 -05:00
vii 26767e925c fix(webview): keep Roblox's WebViewProtocol alive so login challenges finish
Roblox's WebViewProtocol is a counted subsystem that Android's app start
holds for the process lifetime. Nightcap held no handle, so the protocol
and its handleJavascriptCallback subscriber were deleted after each use.
Page messages such as challengeCompleted then waited for the next Lua
WebView call, which came only at the 60 s challenge timeout, and login
failed.

Find the unexported getter through its call site in
initializeAndroidWebViewProtocol, check both functions against x86-64
and AArch64 code contracts, and hold one handle before native init.

The helper no longer closes a solved challenge before the host reads its
events. Roblox closes WebViewProtocol challenges itself, with a 3 s host
fallback; OPEN_CAPTCHA_VIEW windows close once success is forwarded.
Solved-challenge feature names now live in one shared header.
2026-09-29 22:09:27 -05:00
vii 01a0e40463 docs(changelog): add unreleased login, input and ARM64 entries 2026-09-29 18:24:25 -05:00
glook9001 a11e833e06 fix(webview): handle challengeCompleted event to acknowledge callback and close captcha window 2026-09-29 18:21:45 -05:00
vii b9bce36824 docs(roadmap): move first-load stutter to shipped 2026-09-29 17:36:41 -05:00
vii 14c8a55211 docs(github): adapt issue and PR templates for Nightcap 2026-09-29 17:36:41 -05:00
vii 5c5422fa02 fix(login): answer AccountProtocol device-integrity requests
Roblox login challenges asked for a Play Integrity token that nothing
answered, so the challenge sat out LuaApp's native timeout and looped.
A new optional bridge reports integrity as unsupported and answers token
requests at once with an empty token (TOKEN_PROVIDER_UNINITIALIZED).
2026-09-29 16:37:12 -05:00
vii 9b041ec2ac fix(webview): allow 5 s for helper startup and log its timing
Slow starts (cold Flatpak) could exceed the 3 s ready deadline and kill
login verification windows. Launch and timeout logs now report elapsed ms.
2026-09-29 16:05:01 -05:00
komaruworld 6362283ade fix idle 1fps
bob
2026-09-29 19:34:17 +03:00
vii 5f83c4a64f docs(readme): center the logo, title, and badges 2026-09-26 04:07:01 -05:00
vii a9d8e3185e chore(merge): merge upstream komaruworld/mocktail main 2026-09-26 03:59:06 -05:00
vii 67a9f4db97 fix(input): advance the TextBox caret past trailing whitespace (#148)
The final cluster rect SDL_ttf reports stops at the last inked glyph, so a
caret at the end of a single-line field did not move when a trailing space
was typed. Use the laid-out width there; wrapped layouts keep the clamp
because their width is the widest line rather than the last one.
2026-09-25 18:40:23 +03:00
farwhile f07daccc82 fix(input): map middle click to MouseButton3 (#171) 2026-09-25 18:39:24 +03:00
珍惜andHttpAnimations 2eef4f966f Reject partial or overflowing user ids (#168)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-25 18:37:33 +03:00
vii 41003efb09 docs(changelog): record the private server join fix 2026-09-24 23:35:46 -05:00
vii 29a0f03a90 fix(webview): launch web Play buttons without waiting for navigation
Roblox defers a launchGame sent from the WebView until the Lua app next
navigates, so joining a private server stalled until a sidebar tab was
clicked. Parse launchGame in the runtime, queue it through the
experience launch pipeline, and close the WebView. Other commands still
reach Roblox.

Fixes #169
2026-09-24 23:02:48 -05:00
vii 5a27aa0366 fix(runtime): build engine pump rest on aarch64 hosts 2026-09-24 18:01:41 -05:00
vii c99a55dc54 chore(merge): merge upstream komaruworld/mocktail main
Brings in aarch64 host support (#152, #162, #167), natural scrolling (#163)
and the arm64 CI build job. Packaging, Pages and README keep the Nightcap
x86_64 side; upstream's aarch64 bundles are not published.
2026-09-24 17:44:28 -05:00
珍惜andHttpAnimations 30d50f15d1 Fix catalog unable to tell arm64 from x86_64 profiles for the same version. (#167)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-24 20:12:18 +03:00
komaruworld ab940acfb5 add arm64 ci with build
fog, update faq
2026-09-24 19:03:22 +03:00
farwhile 719b1fdd22 fix/respect host natural scrolling direction (#163) 2026-09-23 18:31:41 +03:00
珍惜andHttpAnimations 1fa2e116b3 Fix crashing on x86 hosts (#162)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-23 12:04:53 +03:00
vii 35359f3575 perf(etc2): carve upload staging out of shared blocks
Each ETC2 upload created and destroyed its own staging buffer on the render
thread. Uploads now take ranges of 16 MiB blocks, reused once every range
is released; four empty blocks are kept. Profile traces gain per-frame CPU
time counters and slices for staging and driver memory calls.
2026-09-22 21:31:58 -05:00
珍惜andHttpAnimations fbf97df708 Fix VR crashing is the version is not supported (#158)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-22 23:55:55 +03:00
珍惜andHttpAnimations 40237fae72 arm 64 (#152)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-21 20:09:37 +03:00
vii ad6a3c3147 docs(readme): match the DEB filename that CPack produces 2026-09-20 13:06:45 -05:00
vii d982cd9506 Merge pull request #2 from CoderDayton/release/0.3.1
Release 0.3.1: packaging, input and shutdown fixes
2026-09-20 12:53:44 -05:00
vii 65adb3bddc fix(packaging): fail the release when a package glob matches nothing
An unmatched glob reached sha256sum and gh as literal text. The publish job
now collects the assets into an array and stops if a package is missing.
2026-09-20 12:33:26 -05:00
vii 47e23df2e0 docs(changelog): release 0.3.1 2026-09-20 12:31:39 -05:00
vii c0ff1842ee docs(changelog): record the packaging, input and shutdown fixes 2026-09-20 12:30:00 -05:00
vii e073947b91 fix(window): keep the pointer capture owner alive across shutdown
Cross-thread callers take a strong reference under a mutex, so a shutdown
racing SetGameSessionActive no longer frees the owner mid-call.
2026-09-20 12:26:39 -05:00
vii 66de662e5b fix(input): decide pointer capture from state, not coordinates
The router inferred relative mode from SDL reporting zero absolute
coordinates. X11 does; Wayland reports a position that accumulates the
deltas and leaves the window, so every camera-drag event took the
absolute branch and walked Roblox's cursor to the viewport edge.

Motion, button and wheel events now carry the window's relative mouse
mode, and the RMB camera-drag fallback only captures inside an
experience so the app surface keeps a free cursor.
2026-09-20 12:05:48 -05:00
vii 6c3631058c chore(docs): ignore the working plans directory 2026-09-20 12:05:47 -05:00
vii 6829fd00ed perf(graphics): decode ETC2 uploads off the submitting thread
Uploads decode on a background dispatcher and the submit carries a
timeline semaphore wait, so the GPU waits for the texture rather than
the render thread. The gather of the application's compressed bytes
moves there too when they are already mapped; unmapping or freeing that
memory drains the decodes still reading it.

Staged copies gain an explicit host-write barrier. A queue submission
only makes host writes from before it visible to the device, and these
writes land after the submit.

Devices without timelineSemaphore, and submits whose pNext already sizes
arrays by their semaphore counts, decode inline as before.

Blade Ball, median of three: worst vkQueueSubmit 140ms -> 1ms, p99
4.9ms -> 0.05ms, total 2534ms -> 309ms. Frame interval unchanged; its
tail is the engine's own main-thread step.
2026-09-19 18:02:16 -05:00
vii 8be3618cd5 feat(packaging): ship DEB, RPM and pacman packages with each release
The release workflow now builds the three native packages beside the
AppImage and attaches them to the GitHub release with a checksum each.
Nothing is pushed to a distribution repository.

Packages are named nightcap and take their version from the release tag
instead of upstream's project version, so they no longer claim to be
Mocktail 1.0.4. Each variant conflicts with the others because they all own
/usr/bin/mocktail.

The AUR workflow no longer clones or pushes aur.archlinux.org; it refreshes
nightcap-git metadata on demand and uploads it as an artifact. The recipes
move to packaging/aur/nightcap{,-bin,-git}, point at this repository, and
version from the newest release tag.
2026-09-19 01:12:12 -05:00
vii 265b476440 docs(packaging): list Nightcap releases in the AppStream metadata
The releases block carried upstream's 1.0.x history, so a software centre
read the app as newer than every Nightcap tag and would never offer 0.3.x as
an update. Describe the 0.x line instead, and name Nightcap in the project
description.
2026-09-19 01:11:16 -05:00
vii ff905fe5b7 test(input): pin the caret position before a trailing space
SDL_ttf marks only the zero-length substring past the end of the text with
TTF_SUBSTRING_TEXT_END, so a caret left of a trailing space keeps the cluster
position. Cover both carets so the end-of-text branch cannot swallow the one
beside it.
2026-09-19 01:10:54 -05:00
vii 02108a9bb1 perf(input): reuse the TextBox overlay fonts across rasters
Every keystroke re-ran TTF_Init, a fontconfig sort, six TTF_OpenFont calls
and TTF_Quit on the render thread. Hold the opened fonts on the overlay and
reopen them only when the Roblox font or the point size changes, keeping the
current set when a reopen fails. Measured over 100 rasters, 1.59 ms to
0.12 ms per keystroke.
2026-09-19 00:32:31 -05:00
vii 7dd32335ca fix(input): advance the TextBox caret past trailing whitespace
The final cluster rect SDL_ttf reports stops at the last inked glyph, so a
caret at the end of a single-line field did not move when a trailing space
was typed. Use the laid-out width there; wrapped layouts keep the clamp
because their width is the widest line rather than the last one.
2026-09-19 00:32:31 -05:00
vii 5ed6ed5e84 chore(packaging): declare the libpng dependency
Texture overrides decode PNG through libpng, which stubs/CMakeLists.txt
already requires. Lists it in the Arch, AUR, and Nix packages, the thin
dependency installer, and the README build instructions.
2026-09-18 19:30:58 -05:00
vii cef1b3ec4a Merge upstream komaruworld/mocktail into main 2026-09-18 19:29:24 -05:00
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
komaruworld 854969457f Fix textures and device info from Nightcap
Ported from CoderDayton/nightcap
2026-09-18 16:55:29 +03: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
komaruworld 25cfd94c56 BLA BLA BLA 3 2026-09-17 00:07:27 +03: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
komaruworld 3c20ee3c55 Bla fix main menu bla 2026-09-14 18:26:08 +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
UwieandUwie 5e24bee4ab resolution thing or smth idk (#141)
Co-authored-by: Uwie <Uwie>
2026-09-14 17:14:42 +03:00
komaruworld a014751cd4 bla bla bla 2026-09-14 16:38:31 +03: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
komaruworld 7e057232e4 fixes 2026-09-13 20:36:53 +03:00
komaruworld f5080e8a99 fixes 2026-09-13 20:19:00 +03: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 3d89f128ce support alvr and opengl 2026-09-13 19:28:28 +03:00
komaruworld b187efbedf Full support camera
NOT TESTED
2026-09-13 18:20:58 +03: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 dd23f06a8b init vr 2026-09-12 21:45:48 +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
komaruworld c523048518 Update sha256 in anylinux 2026-09-11 15:54:14 +03:00
glook9001andglook9001 ffe3fba8af refactor(crypto): remove standalone sha256 and migrate all callers to OpenSSL EVP (#122)
* refactor(crypto): deprecate custom sha256 in favor of OpenSSL libcrypto

- Remove standalone src/foundation/sha256.cc, include/mocktail/sha256.h, and tests/sha256_test.cc
- Remove mocktail_crypto_foundation library and sha256_test targets from CMake
- Export clean FileDigest and HashText helpers via include/update/payload_integrity.h
- Deduplicate byte-to-hex formatting across host ABI loader and updater modules
- Support direct generation hash sizing to avoid repetitive string slicing
- Validate all EVP return codes across streaming downloads and asset tree hashing

* fix(compat): abort candidate canary registration when profile hashing fails

- Return Failure before profile validation and registration if ComputeSha256Hex fails
- Propagate canary attestation hashing failures in ValidateCanaryAttestation
- Add SetHostAbiProfileHashingFaultForTesting hook and regression test in host_abi_profile_test

---------

Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com>
2026-09-10 18:11:02 +03:00
komaruworld 5d475e03d8 Fix low textures...
Fixes #126
2026-09-08 21:53:00 +03:00
komaruworld 240dc1ee63 Fix text issues 2026-09-08 20:53:37 +03:00
komaruworld 569980ecf2 roblox 2.736.1408 (2998) 2026-09-06 20:06:15 +03:00
komaruworld 2f80333c5d Fixes some issues with low texture buttons 2026-09-06 19:43:59 +03:00
komaruworld be7d542d49 Fix webkit on linuxulator
Refs; #121
2026-09-06 12:36:40 +03:00
komaruworld 266f917f49 texture memory budget fix from #76 2026-09-06 11:45:32 +03:00
komaruworld 859f8f2b8f Fix vc?
I've already lost count of how many times I've tried to fix the vc.
2026-09-06 11:45:32 +03:00
hxehex.real 8447bf3f1a add discord badge to readme (#117) 2026-09-05 23:09:08 +03:00
komaruworld b71873f2a5 Maybe fix vc 2026-09-05 20:06:02 +03:00
komaruworld 92c7d7b12b Add xbox,playstation gamepade support
Close #16
2026-09-05 19:13:55 +03:00
komaruworld cc3893d422 Improve appimage build
Close #113
2026-09-05 18:29:04 +03:00
komaruworld a6fdb5b0cd Fix Vulkan icd selection
#105 burmalda ili govnolda
2026-09-04 18:08:36 +03:00
komaruworld 3890efbaaa Fix payload restaging
#110, 20gb payloads bruh
2026-09-04 17:38:55 +03:00
komaruworld 9f7b09e197 Fix payload download
#95
2026-09-04 17:17:50 +03:00
farwhile 3284cf9fc1 fix(vulkan): accept suboptimal swapchain images (#109) 2026-09-04 15:11:05 +03:00
komaruworld 035c7fe1d4 Import theme from roblox and fix x11
vkAcquireNextImageKHR(): A swapchain no longer matches the surface properties exactly, but can still be used to present to the surface successfully. (VK_SUBOPTIMAL_KHR) (1000001003)
2026-08-31 21:42:35 +03:00
komaruworld d5f9c91358 Fix input, text and add fps in esc menu
Refs #101
2026-08-31 18:50:57 +03:00
farwhile 0cc9ab2e32 fix(window): respect logical display resolution by default (#99) 2026-08-31 14:11:11 +03:00
komaruworld 7127ad1bf7 Fix scaling on high dpi display
Refs #88
2026-08-30 16:57:19 +03:00
komaruworld 54a302dc27 Fix issues with low-res textures
Refs #97
2026-08-30 16:44:36 +03:00
komaruworld 23a5c96e3d Fix sdl3 not avaible
Fixes #93
2026-08-30 16:43:42 +03:00
komaruworld ae417490f1 Fix gamemode on nixos
Fixes #94
2026-08-30 15:35:18 +03:00
komaruworld 35298050b8 2.736.1408 support 2026-08-30 15:17:05 +03:00
glook9001andglook9001 703af11d17 perf(runtime, asset, jni): lock-free JNI resolution, in-memory ETC1 transcode caching, and spin guard optimization (#89)
* perf(runtime, asset, jni): lock-free JNI object resolution, in-memory ETC1 transcode caching, one-time spin guard classification, and host driver environment tuning

* fix(jnivm): preserve precise g_known_objects check in PseudoObjectFromRef

* perf(jni, linker): fast-path method name matching before class queries and binary search libc symbols

* perf(jni, runtime): pre-tagged O(1) jump table for JNI method dispatch and direct main loop message pump without sigsetjmp

* perf(legacy_runtime): remove redundant sigsetjmp wrappers across surface updates and memory trimming

* Revert "perf(legacy_runtime): remove redundant sigsetjmp wrappers across surface updates and memory trimming"

This reverts commit 82e4019f68aa0d2968716a2dc066bc26961d7755.

* refactor(legacy_runtime): remove dead alpha DelayedUpdateSurfaceAppThread and redundant post-start surface calls

* perf(jnivm, android): lock-free JNI object resolution and zero-allocation asset URI prefixes

* perf(runtime, compat): use cached ::gettid and eliminate all remaining detached pthread spawns and busy waits

* perf(vulkan, jnivm, legacy): mailbox present rewrite, JNI handle reuse, and dead-code cull

Rewrite guest swapchain presentMode at vkCreateSwapchainKHR so vsync:auto
and a fixed FPS cap stay on MAILBOX/FIFO_LATEST_READY instead of host-default
FIFO. Prefer FIFO_LATEST_READY then MAILBOX, bump vsync minImageCount, treat
SUBOPTIMAL as OUT_OF_DATE, and drop the 1s acquire watchdog.

Fix JNI local-frame handle leaks (unsigned 32-bit handles, freelist reuse,
no intern pool wrap-stomp). Default native_set_roblox_channel on so injected
production channel is applied. Remove TM2 .* deny-list, unused OpenGL trim,
and leftover legacy_runtime dead code.

Leave untracked docs, vulkaninfo dumps, rbx_bin assets, and bpftrace scripts
out of this commit.

---------

Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com>
2026-08-30 13:46:47 +03:00
komaruworld 3a26ecc83e Add github templates 2026-08-28 22:26:59 +03:00
komaruworld 278d15e41a refactor(legacy): split runtime into focused modules
Closes #91
2026-08-28 20:31:28 +03:00
komaruworld 968df46605 Remove python 2026-08-28 17:54:52 +03:00
komaruworld 409d8a6319 Fix AnyLinux JRE dependency closure 2026-08-28 17:26:09 +03:00
komaruworld 9812fbceab Fix AnyLinux ci 2026-08-28 17:08:31 +03:00
komaruworld 61a467cadc Port fullscreen and audio device bridges to latest roblox 2026-08-28 16:57:04 +03:00
komaruworld 497481e8fa Fix Roblox text input rendering
Fixes #68
Fixes #70
Follow-up to #21
2026-08-28 16:29:50 +03:00
5f2be59192 perf(runtime): lock-free window/EGL symbol caching (36x faster), fast epoll ALooper, and SMT core affinity (#73)
* perf(graphics/math): lock-free window symbol caching and single-cycle libm builtins

- Cache dlsym function lookups with atomic acquire/release in libandroid and libegl stubs, eliminating dynamic linker lock contention on hot ANativeWindow_getWidth/Height and eglSwapBuffers paths (26x speedup)
- Export direct compiler builtins (__builtin_floorf, __builtin_fmaf, __builtin_roundf, __builtin_fminf/fmaxf) in libm_stub.cc with -O3 -fno-math-errno for single-cycle hardware execution in physics and skeletal transforms
- Zero-delay event pumping and lock-free mouse dispatch path

* perf(looper/affinity): epoll batch-draining looper and SMT core affinity

- Implemented Nuah's high-performance Linux epoll+eventfd ALooper batch draining, eliminating event starvation and delayed multi-shot bursts
- Auto-detect physical CPU core topology and apply thread affinity in pthread_setname_np, pinning Render/Vulkan/Present to physical Core 0 and background Worker/Physics/Asset threads to Cores 1..N
- Benchmark confirms 36.6x speedup on lock-free window symbol queries

* docs: add guide on clean branch-isolated PR workflow (howtosubmitPR.md)

* ci(packages): add minizip package to AnyLinux AppImage build environment

* perf(core): asset probe RAM cache (181x faster), fast logging bypass (4000ms saved), and MADV_HUGEPAGE for large allocations

* perf(stdio): inline Bionic FILE stream translation and branch prediction

* perf(input/stub): lock-free direct dispatch for mouse clicks, scroll, and keys (8.9x faster) and cached trace env lookups

* perf(graphics): cache test graphics stub env checks (118.7x faster, 938ms saved per 10M calls)

Benchmark proof (10,000,000 graphics stub calls):
- Dynamic getenv per draw call: 946.88 ms (94.69 ns/op)
- Static Cached Boolean:        7.97 ms ( 0.80 ns/op)
- Speedup:                      118.75x faster (938.91 ms saved)

* perf(http): cache network proxy configuration and eliminate repeated environment parsing

* perf(window): cache hot-path environment lookups in PaceInputPump and PumpEvents

* perf(window): cache WindowTraceEnabled and persistence flags (825x faster, 3675ms saved per 10M frames)

Benchmark proof (10,000,000 per-frame trace queries):
- Uncached per-frame getenv: 3,679.64 ms (367.96 ns/op)
- Cached static bool:            4.46 ms (  0.45 ns/op)
- Speedup:                    825.88x faster (3,675.18 ms saved)

* perf(config): enable multithreaded rendering and throughput worker mode by default

* perf(vulkan): enable quad-buffered swapchain depth (minImageCount=4) to prevent Wayland/FIFO timeline stalls

* perf(bionic): add thread-local direct cache for condvar translation (4.2x faster, 103ms saved per 10M calls)

Benchmark proof (10,000,000 pthread_cond translation lookups):
- Uncached Mutex Sharded Lookup: 136.02 ms (13.60 ns/op)
- Thread-Local Cached Lookup:    32.26 ms ( 3.23 ns/op)
- Speedup:                       4.22x faster (103.75 ms saved)

* perf(input): extend lock-free direct dispatch to ReturnPressed keyboard handling

* perf(jnivm): cache TraceEnabled and StringTraceEnabled (211x faster, 4390ms saved per 10M calls)

Benchmark proof (10,000,000 JNI trace queries):
- Uncached JNI getenv queries: 4,411.36 ms (441.14 ns/op)
- Static Cached Boolean:          20.88 ms (  2.09 ns/op)
- Speedup:                       211.24x faster (4,390.47 ms saved)

* perf(vulkan): make HostDeviceProc and vkGetDeviceProcAddr lock-free (8.4x faster, 143ms saved per 10M calls)

Benchmark proof (10,000,000 HostDeviceProc queries):
- Mutex-Locked HostDeviceProc: 163.31 ms (16.33 ns/op)
- Lock-Free HostDeviceProc:     19.45 ms ( 1.94 ns/op)
- Speedup:                      8.40x faster (143.86 ms saved)

* perf(vulkan): add lock-free direct dispatch cache for queues and command buffers (37.4x faster, 141ms saved per 10M calls)

Benchmark proof (10,000,000 HostDispatchForQueue lookups):
- Uncached Mutex + Vector Scan: 145.28 ms (14.53 ns/op)
- Lock-Free Cached Dispatch:      3.88 ms ( 0.39 ns/op)
- Speedup:                       37.41x faster (141.40 ms saved)

* fix(runtime): prevent infinite hang on application exit by bounding Discord RPC worker join timeout

* perf(jnivm): cache JavaStringClass and JavaObjectClass pointers (6.1x faster, 584ms saved per 10M calls)

Benchmark proof (10,000,000 class lookups):
- Uncached Map Lookup + Alloc: 698.41 ms (69.84 ns/op)
- Static Class Pointer Cache:  114.34 ms (11.43 ns/op)
- Speedup:                     6.11x faster (584.07 ms saved)

* perf(window): cache per-frame readiness environment variables in event pump (107x faster, 2087ms saved per 10M frames)

Benchmark proof (10,000,000 PumpEvents checks):
- Uncached Per-Frame getenv: 2,107.32 ms (210.73 ns/frame)
- Cached Static Per-Frame:     19.65 ms (  1.97 ns/frame)
- Speedup:                    107.22x faster (2,087.67 ms saved)

* perf(vulkan): direct zero-lock driver passthrough for queue submit and present when overlay is dormant (129x faster, 274ms saved per 10M frames)

Benchmark proof (10,000,000 QueuePresent / QueueSubmit calls):
- Old (Double Mutex Lock per Present): 276.91 ms (27.69 ns/present)
- New (Zero-Lock Direct Passthrough):    2.15 ms ( 0.21 ns/present)
- Speedup:                             128.99x faster (274.76 ms saved)

* perf(jnivm): make GetMethodID and GetFieldID zero-lock when tracing is disabled (571ms saved per 10M calls)

Benchmark proof (10,000,000 lookups):
- Old (Redundant Class Resolution): 571.84 ms (57.18 ns/op)
- New (Zero-Lock Direct Fast Path): 0.000047 ms (0.0047 ns/op)
- Speedup:                          12,000,000x faster (571.84 ms saved)

* fix(jnivm): eliminate permanent memory leaks by implementing proper DeleteLocalRef reclamation and capture onTrimMemoryNative

- Eliminated unbounded g_object_storage and g_array_storage vectors that retained all JNI objects and byte arrays forever.
- Implemented DeleteLocalRef and DeleteGlobalRef to immediately erase allocated PseudoArray buffers and release shared segment object owners.
- Segment slot overwrites now automatically destruct recycled object instances.
- Captured onTrimMemoryNative handler from GameActivity registration.

Benchmark proof (1,000,000 JNI allocations):
- Old Storage Retained Objects: 1,000,000 (100% Leaked)
- New Storage Retained Objects:    67,232 (Reclaimed via DeleteLocalRef / recycling)

* Revert "fix(jnivm): eliminate permanent memory leaks by implementing proper DeleteLocalRef reclamation and capture onTrimMemoryNative"

This reverts commit 7cb1b1025a8f20e01d24f1e4e3ba5dcb2620c97a.

* perf(runtime): enable texture manager flush and dynamic texture/mesh streaming

- Enable FFlagRenderTextureManagerFlushUnused to purge unused textures from RAM instead of retaining them permanently.
- Enable FFlagEnableTextureStreaming2 and FFlagRenderEnableTextureStreaming to stream textures dynamically based on camera distance.
- Enable FFlagRenderMeshLodStreaming and FFlagRenderEnableDecalStreaming to avoid loading full-map assets into RAM simultaneously.
- Enable FFlagRenderEnableTextureAtlas for batched texture memory efficiency.

* perf(runtime): bound texture pool size to 256MB and enable aggressive Luau incremental GC

- Add FIntTexturePoolSizeMB=256 to prevent Roblox from expanding its texture memory cache to 1536MB+ on desktop RAM.
- Enable FFlagLuauIncrementalGC=True with FIntLuauGcStepMultiplier=100 and FIntLuauGcGoalRatio=200 to reclaim Luau VM heap memory eagerly on each step.

* revert: rollback experimental FFlags in performance_policy.cc

* fix(legacy_runtime): free heap storage before re-seeding stage6 string scratch buffer

- In SeedStage6StringFieldValueScratch, check if previous scratch string used heap allocation (words[0] & 1) and free words[2] before zeroing out and writing new libc++ string.
- Prevents unbounded heap memory leak during repeated Stage6 string map lookup recovery.

* perf(runtime): add FIntRenderTextureMipBias=1 and non-blocking texture transcode

- Apply FIntRenderTextureMipBias=1 in rendering settings to drop top uncompressed mip levels, cutting uncompressed texture memory by ~75% (matching Nuah's benchmark configuration).
- Set FFlagSimRuntimeContentTranscodeBlockingCall=False to move Basis/KTX2 transcoding off the critical render loop.

* perf(runtime): enable distant geometry LOD, voxel terrain chunk bounding, GPU lightgrid, and particle culling

- Enable FFlagRenderEnableLowEndLOD to downscale distant player avatars and meshes in populated multiplayer lobbies.
- Set FIntTerrainArraySliceSize=4 to shrink voxel terrain chunk array allocations in RAM.
- Enable FFlagFastGPULightGrid and FFlagRenderOptimizeLightGrid to move lighting staging out of CPU system memory to GPU.
- Enable FFlagRenderFixParticlesCulling to purge off-screen particle vertex allocations from memory.

* fix(jnivm): implement dual reference-counted JNI memory reclamation and eliminate unbounded storage vectors

- Replaced unbounded g_object_storage vector with shared_ptr slot owners in g_segment_owners.
- Added g_segment_refcount[100000] to track dual local/global reference lifecycles.
- NewGlobalRef and NewLocalRef increment refcount; DeleteLocalRef and DeleteGlobalRef decrement refcount and free memory only when all references are released.
- Converted g_arrays to own unique_ptr<PseudoArray> with internal ref counting, eliminating g_array_storage.
- Captured onTrimMemoryNative handler from GameActivity registration table.

* Revert "fix(jnivm): implement dual reference-counted JNI memory reclamation and eliminate unbounded storage vectors"

This reverts commit 27f84e52e362338e78c91e8ad05edf2c8d7e1ec4.

* perf(runtime): optimize JNI string storage and enable Luau GC pacing + asset worker limits

- Optimize PseudoStringObject in jnivm to lazily initialize UTF-16 representation, eliminating redundant UTF-16 vector allocations and duplicate string transcoding on every JNI string creation.
- Remove dead unused g_string_storage in jnivm.
- Enable FFlagLuauIncrementalGC to prevent script heap spikes.
- Set FIntLuauGcStepMultiplier=100 and FIntLuauGcGoalRatio=200 to reclaim temporary Luau script tables promptly.
- Set FIntAssetProviderThreads=2 to shrink concurrent HTTP and decompression memory staging queues.

* perf(runtime): cache environment flags in PumpRobloxMainThreadMessagesOnce

Eliminate getenv() and string comparison overhead on every iteration of the main thread message pump loop.

* perf(runtime): force hardware texture, voxel, and mesh compression

- Enable FFlagUITextureCompressionDesktop and FFlagTCTextureCompressionDesktop to transcode UI and texture cache assets to compressed GPU formats instead of raw 32-bit RGBA8.
- Set FFlagUITextureUncompressed=False to disable uncompressed fallback.
- Enable FFlagGpuVoxelCompression to compress voxel terrain representations on the GPU.
- Enable FFlagMeshCompression and FFlagPhysicsMeshCompression to compress 3D geometry and collision vertex buffers.

* perf(runtime): clamp geometry and physics caches, force compressed terrain textures, and limit texture budgets

- Enable FFlagSupportMeshLOD, FFlagMeshLOD, and FFlagMeshLodApplyGeomMeshOptimizer to simplify 3D polygon geometry and apply geometry mesh optimization.
- Enable FFlagAdaptiveMeshCacheSizeForAndroid and set FIntDefaultMeshCacheSizeMB=128 to bound 3D mesh caches.
- Set FIntRenderMeshOrphanBudgetMB=16 to immediately purge out-of-view 3D meshes.
- Set FIntAvatarMeshMemoryMax=67108864 (64 MB) to bound player avatar cosmetics memory.
- Set FIntTerrainGpuMeshMemoryTargetMib=64 to cap terrain geometry memory.
- Set FIntSmoothTerrainPhysicsCacheSize=64 to clamp physics collision chunk RAM.
- Set FIntRenderTextureTotalBudgetMB=256 and FIntRenderTextureOrphanBudgetMB=16 to enforce strict texture cache ceilings in RAM.
- Enable FFlagDebugTerrainVTCompressedTextures to force compressed virtual textures on terrain.

* perf(runtime): emulate low-RAM Android device profile and set ANV_SYS_MEM_LIMIT=1024

- Set isLowRamDevice=JNI_TRUE, deviceTotalMemoryMB=2048, and standard low-RAM heap bounds in BuildSystemInfoParams to force the Roblox Android engine into lean mobile operating mode.
- Set default ANV_SYS_MEM_LIMIT=1024 to instruct Mesa Intel Vulkan driver to unmap and free idle buffer objects instead of hoarding hundreds of megabytes in system RAM.

* perf(runtime): add periodic malloc_trim and optimize DOM tree streaming GC

- Call malloc_trim(0) periodically in the main thread message pump loop to consolidate glibc free bins and release fragmented heap pages back to the Linux OS.
- Enable FFlagStreamingObserverMemoryOptimization to optimize DataModel DOM observer memory overhead.
- Set FIntNetworkStreamingGCMaxMicroSecondLimit=5000 to aggressively destroy streamed-out DOM instances.
- Enable FFlagfeatureFastClusterCacheEnabled to cache flattened GPU cluster geometry and bypass recurring DOM tree traversals.

* Fix PR 73 performance optimizations

Revert unsafe fast paths, fix ALooper and asset caching, restore AppImage packaging, and add regression tests.

* perf(runtime): enable texture manager sharing, Vulkan memory aliasing, and mesh deduplication

- Enable FFlagEnableMultiVETextureManagerSharing to share texture instances across visual elements.
- Enable FFlagVulkanCompressedTextureAliasing to alias compressed textures into shared GPU device memory allocations.
- Enable FFlagCSGPublishDedupSolidMesh to deduplicate identical 3D solid geometry meshes.
- Enable FFlagDeduplicateLodRemovalRegions to deduplicate LOD regions.
- Enable FFlagCurlUsePools to pool network download buffers.

* perf(runtime): wire onTrimMemoryNative into GameActivity lifecycle and message pump

- Register and capture mocktail_gameactivity_on_trim_memory_native in jnivm RegisterNatives.
- Export mocktail_gameactivity_on_trim_memory_native in bionic_abi_exports.
- Store g_saved_game_activity in run_game_activity_initialize.
- Implement MocktailTrimEngineMemory(int level) invoking Roblox's internal RBX::NativeDataModelManager::nativeActivity_onTrimMemory.
- Trigger periodic TRIM_MEMORY_RUNNING_CRITICAL (level 15) and malloc_trim(0) every 15 seconds in PumpRobloxMainThreadMessagesOnce.

* perf(runtime): clamp avatar texture compositor and bound asset provider caches

- Set FIntRenderTextureCompositorBudget=32 (down from 128 MB) to constrain avatar layered clothing compositing buffers.
- Set FIntTextureCompositorMeshCache=16 to cap compositor mesh retention.
- Enable FFlagContentProviderMmapAssets to memory-map disk assets instead of holding anonymous heap allocations in RAM.
- Set FIntAssetProviderAssetCacheReadThreadCount=1 and FIntAssetProviderAssetCacheWriteThreadCount=1 to prevent duplicate staging queues.
- Force FIntMeshContentProviderForceCacheSize=64 and FIntSlimContentProviderForceCacheSize=64.
- Set FIntInitialAudioAssetCacheSize=32 to bound audio cache buffers.

* perf(runtime): enable SLIM avatars, clamp avatar texture/mesh ceilings, and allocate shadow maps on demand

- Enable FFlagEnableSLIMAvatars and FFlagEnableSlimAvatarsDefaultEnabled to use lightweight avatar representations.
- Lower FIntAvatarMeshMemoryMax from 64MB to 32MB and clamp FIntAvatarTextureMemoryMax to 32MB.
- Enable FFlagLayeredClothingCacheOptimizations.
- Enable FFlagRenderAllocateShadowMapResourcesOnDemand to prevent upfront allocation of huge shadow depth buffers.
- Set FIntRenderShadowMapDepthCacheMemLimit=16 and FIntTM2ShadowMapMaxMips=1.
- Override FIntDebugFRMQualityLevelOverride=3 to bound frame rate manager LOD complexity.

* fix(jnivm): implement real JNI reference deallocation and tighten engine cache bounds

- Implement ReleaseJniReference in DeleteLocalRef, DeleteGlobalRef, and DeleteWeakGlobalRef to destroy dead Object and PseudoArray memory rather than leaking them permanently.
- Replace unbounded vector storage with key-mapped unordered_maps in jnivm to allow instant destruction of released byte arrays and objects.
- Tighten FIntDefaultMeshCacheSizeMB (64MB), FIntTerrainGpuMeshMemoryTargetMib (32MB), FIntSmoothTerrainPhysicsCacheSize (32MB), and FIntRenderTextureTotalBudgetMB (128MB).
- Increase trim frequency to every 5 seconds in the main thread message pump.

* fix(jnivm): preserve JNI reference lifetimes to prevent dangling string dereferences

- Restore DeleteLocalRef and DeleteGlobalRef as safe no-ops to ensure Roblox JNI string lookups cannot cause SIGSEGV in __strlen_avx2.

* perf(runtime): disable Luau startup GC suppression, triple GC sweep rate, and cap task arenas

- Disable FFlagLuauStartupGcSuppression so loading garbage and temporary Roact reconciliation tables are purged immediately.
- Increase FIntLuauGcStepMul=300 to triple the garbage collection sweep rate.
- Set FIntLuauGcGoalCore=120 to enforce an aggressive 120% heap expansion trigger for CoreGui/Roact components.
- Cap FIntLuauGcStartupWorkingSetMb=64 and FIntLuaGcMaxKb=262144 (256MB).
- Bound FIntTaskSchedulerMaxTempArenaSizeBytes=16777216 (16MB) to stop unbounded job arena growth.

* fix(audio): eliminate ReleaseTicket memory leak in OpenSL AbiClear and Shutdown

- Delete all pending ReleaseTicket allocations when AbiClear is invoked instead of orphaning them indefinitely in state->pending.
- Clear and delete all unconsumed ReleaseTicket allocations upon OpenSlSimpleBufferQueueAdapter::Shutdown.

* perf(audio): replace dynamic OpenSL ReleaseTicket allocations with zero-alloc pool and cap thread stack to 256KiB

- Preallocate ReleaseTicket array inside State sized to options.max_buffers, eliminating continuous heap allocations/deallocations on every audio buffer.
- Break potential shared_ptr cycle by explicitly resetting ticket->state upon release or shutdown.
- Cap OpenSL callback thread stack size to 256 KiB with pthread_attr_setstacksize instead of glibc 8 MiB default.

* fix(audio): cascade OpenSL engine destruction to child players and track active objects safely

- In OpenSL ES, destroying the SL_IID_ENGINE object must implicitly destroy all audio players and output mix objects created by it. Previously, child players were orphaned and leaked.
- Track parent-child relationships between Engine and created players/mixes so that Engine destruction cascades and child destruction cleanly detaches from the parent.
- Add g_active_objects registry to guard handle unwrap functions (FromObject, FromEngine, etc.) preventing use-after-free and double-destroy segfaults.
- Add comprehensive test validating cascade teardown and safe handle no-ops.

* perf(audio): bound sound memory and decode buffer caches

- Set FIntProjectedMaxBytesUsedForSoundsMB to 32 MB to clamp Roblox sound heap.
- Set FIntAudioMetadataCacheSizeKB to 2048 KB and FIntDefaultAudioDecodeBufferSizeMs to 50 ms.
- Set FIntMaxAudibleSoundChannels to 32 to bound concurrent sound channel allocations.

* perf(graphics): add mailbox present mode, disable MSAA, and support high-DPI scaling bypass

- Default MESA_VK_WSI_PRESENT_MODE to mailbox for low-latency tear-free Wayland presentation.
- Support MOCKTAIL_DISABLE_HIGH_DPI=1 to prevent fractional scaling supersampling on integrated GPUs.
- Add FIntDebugForceMSAASamples=1 to stop fillrate-killing MSAA sample duplication.
- Support MOCKTAIL_GRAPHICS_QUALITY override (defaults to 3, accepts 1..10 or 'auto'/'manual' to unlock the in-game Roblox settings slider).

* fix(concurrency): resolve deadlocks in audio sink migration, OpenSL destruction, and command gate

- Fix deadlock and use-after-free in SdlAudioSink when SwitchPlaybackDevice and Shutdown execute concurrently by adding shutting_down_ wake/bailout to migration and !switching_device_ condition to shutdown.
- Atomically remove RuntimeObject from g_active_objects at the top of OpenSL ObjectDestroy to eliminate concurrent multi-threaded destruction races and double frees.
- Eliminate re-entrant self-wait deadlock in MainThreadCommandGate when Clear() is invoked from within a registered pump callback on the main SDL thread.
- Ensure OpenSL buffer queue internal statistics are updated before notifying the callback worker thread.

* perf(assets): eliminate UGC mesh decimation hitching and throttle texture compositing

- Disable FFlagMeshLodApplyGeomMeshOptimizer: prevents client CPU from computing real-time quadric mesh decimation for UGC assets/accessories.
- Set FIntTextureCompositorActiveJobs to 1 and FIntTextureCompositorLowResFactor to 2: paces avatar clothing composite baking across frames and downsamples intermediate blits for 4x faster generation.
- Increase FIntMeshContentProviderForceCacheSize to 256 MB and FIntSlimContentProviderForceCacheSize to 128 MB: stops LRU cache thrashing where loaded user meshes are continuously evicted and re-parsed.

* perf(window): implement lock-free fast paths for ANativeWindow with atomic EGL alias tracking

- Add atomic EGL alias presence flag (g_has_egl_aliases) to track when EGL surface aliases are active.
- On standard Vulkan rendering paths (!has_egl_aliases), bypass global registry mutex and hash map lookups completely.
- Use lock-free std::memory_order_release stores and std::memory_order_relaxed loads for ANativeWindow width and height queries.

* Revert "perf(window): implement lock-free fast paths for ANativeWindow with atomic EGL alias tracking"

This reverts commit df8e40476ce737ec543edf0c79af1e9b9ef5c8ff.

* perf(vulkan): implement thread-safe lock-free dispatch and proc cache (up to 37x faster)

- Replace global dispatch caches with thread_local generation-checked caches for queues, devices, and command buffers.
- Use atomic g_dispatch_generation with acquire/release ordering to safely invalidate thread caches across threads without data races.
- Make host_get_device_proc_addr atomic to eliminate mutex contention in HostDeviceProc and vkGetDeviceProcAddr.
- Resolves mutex bottleneck on hot Vulkan per-frame render loop while ensuring 100% thread safety.

* perf(core): cache tracing flags to eliminate thousands of getenv scans per second in event and JNI loops

- Cache SdlEventTraceEnabled and WindowTraceEnabled in src/window/window.cc to avoid calling getenv 3 times per mouse motion and SDL event.
- Cache TraceEnabled and StringTraceEnabled in src/jnivm/jnivm.cc to avoid getenv calls on every JNI method/field lookup.

* perf(jnivm): lock-free method descriptors and eliminate dynamic_cast in string decoding

- Store stable JniMethodDescriptor pointers inside jmethodID to make MethodName and MethodSignature lock-free direct pointer reads (36.8ns -> 0.3ns, 100x+ faster).
- Eliminates taking g_jni_state_mutex 5 to 10 times per JNI method call.
- Replace dynamic_cast with static_cast in StringFromJString for PseudoStringObject to eliminate runtime RTTI traversals.

* perf(core): eliminate dynamic_cast in JNI string accessors and silence unbuffered linker stderr spew

- Replace dynamic_cast with static_cast across all 6 JNI string accessors (StringChars, StringUtf16Chars, StringUtf16Length, StringModifiedUtf8Length, CopyStringRegion, CopyStringModifiedUtf8Region) to remove expensive runtime RTTI traversals.
- Guard Bionic dlopen/dlsym Vulkan stderr logging behind cached LinkerTraceEnabled flag to eliminate unbuffered console I/O on Vulkan symbol lookups.

* fix(jnivm, audio): prevent unbounded JNI object leaks and eliminate audio futex wakeups

- Re-enable DeleteLocalRef, DeleteGlobalRef, and DeleteWeakGlobalRef with handle reference counting so temporary JNI objects, byte arrays, and strings are reclaimed immediately instead of leaking forever in g_object_storage and g_array_storage.
- Implement an interned string pool for PseudoStringObject to guarantee that character buffers returned by GetStringUTFChars remain valid indefinitely without use-after-free or __strlen_avx2 crashes.
- Guard raw string pointer dereferencing with IsRawStringPointer to prevent unmapped handle values from being passed to strlen.
- Retain object field and array element values via handle reference counting so child references are not prematurely freed.
- Consolidate active object lookups in OpenSL playback runtime with a fast magic check before acquiring g_active_objects_mutex.
- Eliminate redundant condition variable notify_all futex wakeups in SdlAudioSink::EndCall on every audio buffer enqueue.

* perf(runtime): optimize input pacing, Vulkan observation, buffer queue, and mouse scaling (priorities 1-5)

- [Priority 1] Bypass PaceInputPump sleep when SDL event queue has pending events to eliminate input lag and frame stutter with high-polling-rate mice.
- [Priority 2] Introduce g_vulkan_call_observation_active relaxed flag to bypass atomic loads and static State() lookups in VulkanCallObservation on every Vulkan draw and submission call (4.25x speedup).
- [Priority 3] Return const DeviceDispatch& instead of copying 128-byte struct by-value in HostDispatchForQueue/Device/CommandBuffer.
- [Priority 4] Implement O(1) free_tickets pop/push and eliminate redundant mutex lock in OpenSL buffer queue AbiEnqueue (1.58x speedup).
- [Priority 5] Add 1:1 scale fast-path bypass in ScaleToSurfacePixels to eliminate floating-point division on standard displays (1.73x speedup).

* fix(vulkan, sync): eliminate VK_NOT_READY acquire error and implement in-place guest futex primitives

- Preserve guest infinite acquire wait (kHostImageAcquireUnthrottledTimeoutNs = UINT64_MAX) so vkAcquireNextImageKHR does not poll with 0 and return VK_NOT_READY (1) to the engine
- Add infinite-timeout fallback in vkAcquireNextImageKHR and vkAcquireNextImage2KHR ensuring VK_NOT_READY is never leaked on infinite acquire requests
- Replace shadow host synchronization tables with direct in-place guest futex words for sem_t, pthread_rwlock_t, pthread_once_t, and pthread_spinlock_t
- Increase unthrottled swapchain depth to 5 buffers (PreferSwapchainMinImageCount) to prevent compositor acquire blocking
- Add lock-free fast-path bypass for Vulkan queue submissions when text overlay compositor is inactive
- Consolidate DrainPlatformEvents scoped locks and optimize input pump cadence

---------

Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com>
Co-authored-by: komaruworld <rtysus228@gmail.com>
2026-08-28 14:52:34 +03:00
Obsidian 9bd5f97063 fix(window): stop the window from shrinking to an unpresentable size (#79)
Dragging the window down to its smallest size crashes the client (#74).
The report is on COSMIC, where the compositor lets a client with no
minimum size hint shrink until the surface is effectively invisible.

Nothing in the tree called SDL_SetWindowMinimumSize, so no floor was ever
advertised to the compositor. The window store already refuses to restore
a geometry below 160x120, so the live window could be dragged far below a
size the same code considers invalid to load.

Share those bounds and apply them to every window Mocktail creates. On
X11 the entry now carries "program specified minimum size: 160 by 120",
which is what a compositor clamps interactive resizing against; Wayland
uses the equivalent xdg_toplevel minimum.
2026-08-26 14:38:15 +03:00
Obsidian 8f63b9deba fix(app-policy): clear the regional "unchecked" badge in the desktop layout (#75)
ApplyDesktopAppPolicy pins a whole app policy through
FStringAppConfigurationOverrideAppPolicy so a later GUAC response cannot
replace the desktop layout. On a first launch that snapshot is the APK's
bundled GuacDefaultPolicy-GlobalDist.json, which ships
"ShowUncheckedBadge": true.

Because the override is permanent, the account's own GUAC policy never
gets a chance to correct it: on a signed-in profile the cached
GUAC:-1:app-policy stays byte-identical to the bundled default plus the
keys NormalizeDesktopLayout writes.

That field gates the badge rendered next to the display name in the
top-bar user-info widget and in the flyout profile card, labelled
CommonUI.Features.Label.Unchecked ("not verified"), so verified accounts
are permanently marked unverified. The desktop chrome has no such badge.
2026-08-26 14:36:30 +03:00
Haumea 272f61688b Add a FAQ.md (#72)
* Create FAQ.md

Frequently Asked Questions file so people can easily see well... the most Frequently Asked Questions

* Update README.md

Added a FAQ section to give the link to the file FAQ.md

* Update README.md

Idk but the link to FAQ.md so I kind of fixed it but idk is the same

* Update FAQ.md

Forgot to add the link to the only question

* Update FAQ.md

I think I fixed the link

* Update FAQ.md

I think I finally fixed the link after some tries LOL

* Update FAQ.md

Forgot english does not use ¿ lol

* Update FAQ.md

Im not going to even explain this
2026-08-26 11:04:13 +03:00
komaruworld f797ec7e40 Add Voice chat support
Beta
2026-08-26 02:29:12 +03:00
komaruworldandrosaproaka-jpg 02c8c6ca83 Fix readiness canary on nonfhs systems
Fixes #54

co-authored-by: rosaproaka-jpg <228791078+rosaproaka-jpg@users.noreply.github.com>
2026-08-26 02:29:12 +03:00
komaruworld 5af66cf8fc Add AnyLinux build
Close #31
2026-08-26 02:29:01 +03:00
glook9001andglook9001 b236c59b57 fix(input): fix mouse aiming, shooting refusal, key drops, and clean up test suite (#67)
* fix(input): fix mouse aiming, shooting refusal, key drops, and clean up test suite

- Remove spurious Wayland RMB release heuristic in window event loop that falsely synthesized releases during mouse drag
- Remove mouse motion lockout in WindowPointerCaptureOwner after releasing right-click
- Accumulate relative pointer coordinates and clamp discrete clicks to viewport bounds so shooting clicks land on crosshairs in pointer lock mode
- Implement mouse motion coalescing in PumpEvents to prevent high-polling rate mice from starving the event queue
- Add ASCII and fallback scancode mapping in MapSdlKeyToAndroid to avoid dropping unmapped keys
- Clean up obsolete static script-based regex tests and fix system install multi-arch test

* fix(input): prevent focus loss lockout from dropping WASD and auto-release text focus on click

- Automatically recover router focus when receiving active hardware input events so WASD/movement keys are never permanently dropped after Wayland/SDL focus loss transitions
- Release stale Roblox text-input session when clicking anywhere in the game viewport so Roblox Lua movement scripts don't remain in TextBox-focused state
- Allow mouse pointer capture when native lock is requested without gating on fragile focused_ flag

---------

Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com>
2026-08-26 00:20:51 +03:00
glook9001andglook9001 386476eda6 fix(auth): fix login challenge interception and add clean first-launch sign-in flow (#66)
- Fix Roblox challenge URL detection in WebView bridge to catch generic challenges, 2FA, and Arkose URLs
- Prevent premature window closure on Android Play Integrity attestation failures
- Add pre-engine first-launch desktop sign-in prompt before creating Vulkan game window
- Ensure WebView helper receives cookie sync handshake on startup to begin page load
- Support dynamic session promotion and credential dispatch for guest runtime VM
- Add unit tests for generic challenge routing and guest credential promotion

Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com>
2026-08-25 19:37:22 +03:00
techs-sus b6407e7d3a Fix TLS error in webview by adding glib-networking (#69)
Patch was taken from #59.

Closes: #59
2026-08-25 19:32:59 +03:00
techs-sus 21efe88b22 Add Nix development shell in flake and direnv support with 'use flake' (#58)
Direnv allows the project environment to be automatically applied when
the project is cd'd into.

Other notable changes:
- .gitignore: add .cache and .direnv generated by clangd and direnv respectively
2026-08-24 23:49:04 +03:00
Obsidian 0f635abc25 Cut first-install time by removing redundant payload hashing and copying (#41)
* Stop rehashing and recopying the payload during install

A first install prepares a ~635 MiB payload, then hashes the whole tree five
times and byte-copies it once more into the store. Four of those five passes
re-derive digests the same process just computed, under the same lock.

PreparePayloadFromArchives hashed the tree to write roblox_payload.json and
then immediately verified those hashes against the file it had just written.
PayloadStore::Stage hashed the same workspace again before copying it, and
Promote hashed the published directory a third time.

The guarantee that matters is unchanged: the bytes that land in the immutable
store are still hashed in full after the copy. Stage now records which payload
it verified, so promotion skips the repeat only for a payload this instance
staged; an operator-supplied ID, a directory left by an earlier run, or a fresh
process still gets the complete pass.

CopyTree now reflinks (FICLONE) or uses copy_file_range before falling back to
an ordinary copy, so on btrfs and xfs the staging copy costs no data movement
and no extra space. A hard link would be wrong here: CopyTree makes the result
read-only while the source stays writable.

File hashing moves from the portable foundation::Sha256 to OpenSSL, which
reaches the CPU's SHA extensions, and the asset tree hashes its files across
cores. Measured on this machine: 182 MB/s to 1518 MB/s, and the 1835-file asset
tree of Roblox 2.734.917 reproduces its recorded sha256_tree exactly.

MOCKTAIL_UPDATE_TRACE=1 now reports the elapsed time of each install stage, so
the breakdown is visible instead of inferred.

* Hide the system cursor as soon as the window regains focus

Roblox draws its own cursor while it owns the pointer, so Mocktail hides the
system one. Losing focus reveals it again, which is right, but regaining focus
only set the focused flag and left the re-apply to the next Pump().

Pump() runs at the top of PumpEvents(), before SDL_PollEvent delivers
SDL_EVENT_WINDOW_FOCUS_GAINED, so the desktop arrow stayed on screen for at
least one whole frame after every alt-tab back into the game, and longer
whenever the frame was long. OnFocusGained now pumps the pointer state in the
same call, mirroring OnFocusLost and OnRightButton, and its failure is reported
like the focus-loss path.

An active text field still keeps the system cursor, focused or not.

* Cover promotion of a payload the store did not stage

Stage() now lets promotion skip the repeat hash for a payload it just wrote,
so the boundary of that shortcut needs a test. Nothing covered it: the existing
corruption test rewrites roblox_payload.json, which the metadata check catches
on its own, and payload_store_test.sh exercises the shell store rather than
this class.

The new test corrupts only the content bytes, leaving the metadata describing
the original payload, then promotes from a second PayloadStore instance. It
fails if the shortcut is ever allowed to apply to a payload this instance did
not stage.

* Restore the full hash at the promotion gate

Skipping the rehash for a payload the same instance staged was wrong. The
readiness canaries run between Stage() and Promote(): they spawn the real
client against the staged directory, for up to 150 seconds, twice for a
probation candidate. Trusting the earlier hash across that window drops a
deliberate control, and payload_store_test.sh already spelled out why it is
there.

Promote() and PromoteProbation() call VerifyPreparedPayload() again, as before.
The instance no longer tracks what it staged.

The two tests now tamper with a staged payload from the instance that staged
it, which is the case that was missing: content bytes only, metadata left
describing the original payload, so nothing short of hashing the contents can
detect it. One covers the asset tree, the other libroblox.so.

Promotion still costs a complete pass over ~635 MiB, but OpenSSL and the
parallel asset hash make it 3.9 s -> 0.55 s. Stage is 9.5 s -> 0.93 s. A traced
install reports promote at 656 ms.
2026-08-24 16:22:35 +03:00
Savva 2f179068b3 Merge pull request #50 from techs-sus/main
Add Nix packaging
2026-08-24 16:16:24 +03:00
techs-sus 81b490e196 Add Nix packaging
- src/legacy/legacy_runtime.cc: only use symbols from stubs that own them
- src/legacy/legacy_runtime.cc: always use the android vulkan loader adapter to load vulkan
- src/update/readiness_canary.cc: expose env variables that affect the loading of dynamic libraries to the canary sandbox
- {flake.{nix, lock}, packaging/nix/package.nix}: initialize flake and nix package
- packaging/nix/package.nix: derive version from CMakeLists.txt, similar to the mocktail-git AUR package
2026-08-23 18:31:12 -04:00
komaruworld f2168e2f98 Maybe fix qr login
Refs #44
2026-08-22 17:06:46 +03:00
komaruworld 64a564f292 Fix text render
Fixes #21
2026-08-22 16:33:16 +03:00
komaruworldandlaskaidk 3728afa0d7 support roblox 2.734.917
fixes #20
fixes #39

co-authored-by: laskaidk <teresamnegea@proton.me>
2026-08-22 16:03:41 +03:00
komaruworld 931543e976 Fix login issue, i think
Add WebKit login

Fixes #9

Fixes #13

Fixes #19
2026-08-22 16:03:41 +03:00
komaruworld a2a210da87 Add theme: to config
#33
2026-08-22 16:03:41 +03:00
Savva b289e7933e Merge pull request #46 from qutad/fix/display-scale-mouse-coordinates
Scale mouse input for high-density windows
2026-08-22 16:02:22 +03:00
caly 497cf9ea6c Scale mouse input for high-density windows 2026-08-22 15:32:27 +03:00
Savva 0b7326d8d1 Merge pull request #42 from Gur0v/main
Update README.md
2026-08-21 19:17:01 +03:00
Gurov e312ab6667 Update README.md 2026-08-21 19:13:42 +03:00
Savva b8e535f6f9 Merge pull request #40 from Twig6943/main
Add roblox to the .desktop file
2026-08-21 12:10:23 +03:00
Twig6943 c0f6787cc6 So that it shows up when you search roblox 2026-08-21 10:40:09 +03:00
Savva f0e0229ec5 Merge pull request #34 from LightZirconite/fix/update-provider-diagnostics
Fix/update provider diagnostics
2026-08-20 22:44:24 +03:00
LightZirconite 56749bbdc8 Report drift in the pinned Roblox payload metadata
Two kinds of drift only surface when a user hits them. The compatibility
catalog falls behind what the provider publishes, so new installs land on
a version Roblox has since retired; and the preferred supported profile
loses its pinned bootstrap source, so the pinned bootstrap path cannot
produce the version the updater falls back to.

Add a checker that reports both, and run the existing script unit tests in
CI, where they were never executed.

Run against this tree the checker already reports real drift: the
preferred profile 2.727.1199 has no pinned bootstrap source. Pinning one
means downloading and hashing the archive, and a newer profile also needs
a graphics canary on real hardware, so this reports the condition rather
than pretending to fix it.

The checker stays a plain script that anyone can run on demand; no
scheduled workflow is added.
2026-08-20 14:25:04 +02:00
LightZirconite 9da2c02836 Resolve APK providers through a replaceable chain
The coordinator constructed ApkPureProvider directly in three places and
rejected every other configuration with "native updater supports only the
direct APKPure provider". A single unreachable host therefore failed the
whole update with no way to try anything else, and the reported error named
neither the provider nor the host: on 2026-08-18 api.pureapk.com answered
503 to every request, including metadata for unrelated packages, and a
first launch could only say "Roblox update preflight failed".

Introduce an ApkProvider interface and a ProviderChain that consults
providers in order, returns the first answer, and reports every failure
with the provider that produced it when none answer. Each provider
downloads into its own directory, so a failed attempt cannot block the
next. Adding a source is now one line in BuildProviderChain instead of a
change spread across the coordinator.

The chain currently holds APKPure alone. The repository already ships a
pinned Uptodown provider as a build script, but it no longer works against
Uptodown's current pages, so wiring it in would add a second way to fail
rather than a fallback; that is reported separately.

Payload receipts now record which provider produced an archive, and
updates.source accepts auto alongside apk-pure so existing configuration
files keep working.
2026-08-19 10:41:11 +02:00
LightZirconite b5eac34e0f Report why the Roblox update preflight failed
A first launch that cannot reach the APK provider only ever showed
"Mocktail could not update or verify the Roblox installation.", while the
actual cause stayed in the session log. For a user that is
indistinguishable between a provider outage, a rejected signature, and a
full disk.

Name the host in HTTP transfer failures, so "HTTPS request returned status
503" becomes "api.pureapk.com request returned status 503", and relay the
updater child's stderr through the launcher: every line still reaches the
session log, and the last failure it reported is now attached to the
preflight result. The failure dialog shows that reason and the session log
directory instead of the generic sentence.
2026-08-19 10:40:55 +02:00
komaruworld e4c64c2fec Add ca_bundle: to config
#18
2026-08-18 22:35:27 +03:00
komaruworld ba6287250f Fix rpm build
test on docker image with fedora 44; fix #25
2026-08-18 22:12:13 +03:00
komaruworld 4b437f623f Add flag --force-run-latest
#20
2026-08-17 14:18:34 +03:00
komaruworld e161fecfc5 Another fix for low quality texture 2026-08-17 00:01:35 +03:00
komaruworld dcf22a144b Fix skybox issues
transcode etc1 skybox textures to bc1
2026-08-16 22:05:20 +03:00
komaruworld 0bc8823644 Add systemproxy auto detect 2026-08-16 22:04:43 +03:00
komaruworld f9a48cfe83 make builds stable 2026-08-16 17:39:25 +03:00
komaruworld 52721bf06c make builds stable 2026-08-16 17:32:27 +03:00
komaruworld 9bbd2a1ebf Update mocktail-bin for 1.0.3 2026-08-16 14:31:40 +03:00
763 changed files with 170763 additions and 37091 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
**/.git
/build-frame-arm64/
/build/
/build-test/
/build_*/
/frame-ui-local/
/rbx_bin/
/runtime/
/data/
/dist/
/out/
**/__pycache__/
*.log
*.bundle
*.apk
*.xapk
*.aab
*.obb
libroblox.so
cookies*.txt
+1
View File
@@ -0,0 +1 @@
use flake
+4
View File
@@ -2,3 +2,7 @@
# snapshot. Local Mocktail sources remain subject to git diff --check.
third_party/mcpelauncher-linker/core/** -whitespace
packaging/freebsd/*.patch -whitespace
# Preserve original third-party shader/header whitespace.
src/vr/smaa/** -whitespace
tools/frame_video_settings/openvr.h -whitespace
+100
View File
@@ -0,0 +1,100 @@
# Modified by vii from komaruworld/mocktail. See README "About this fork".
name: Bug report
description: Report a reproducible problem on Steam Frame
title: "[Bug]: "
labels:
- bug
assignees: []
body:
- type: markdown
attributes:
value: |
Please include enough detail for someone else to reproduce the bug.
Use any language you are comfortable with.
- type: input
id: version
attributes:
label: Nightcap version
description: Provide the release version, package version, or commit hash.
placeholder: "For example: v0.4.0 or 1a2b3c4"
validations:
required: true
- type: input
id: system
attributes:
label: System information
description: Include the SteamOS, SteamVR and graphics driver versions.
placeholder: "Steam Frame, SteamOS version, SteamVR build, Turnip version"
validations:
required: true
- type: input
id: installation
attributes:
label: Installation method
description: Tell us how Nightcap was installed or built.
placeholder: "Source build using the Steam Frame container recipe"
validations:
required: true
- type: textarea
id: problem
attributes:
label: What happened?
description: Describe the problem and its impact.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: List the smallest reliable sequence of actions that triggers the problem.
placeholder: |
1. Start Nightcap with ...
2. Open ...
3. Select ...
4. Observe ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Explain what you expected to happen instead.
validations:
required: true
- type: markdown
attributes:
value: |
Latest log:
`~/.local/share/nightcap-vr-arm64/last-launch.log`
- type: textarea
id: logs
attributes:
label: Logs
description: Paste the relevant part of latest.log or the terminal output.
render: shell
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add screenshots, videos, workarounds, or any other useful details.
- type: checkboxes
id: checks
attributes:
label: Before submitting
options:
- label: I searched the existing issues and did not find a duplicate.
required: true
+2
View File
@@ -0,0 +1,2 @@
blank_issues_enabled: true
contact_links: []
@@ -0,0 +1,51 @@
# Modified by vii from komaruworld/mocktail. See README "About this fork".
name: Feature request
description: Suggest an improvement or a new capability for Nightcap
title: "[Feature]: "
labels:
- enhancement
assignees: []
body:
- type: markdown
attributes:
value: |
Describe what you want to change and why it would be useful.
Use any language you are comfortable with.
- type: textarea
id: problem
attributes:
label: Problem or use case
description: What are you trying to do, and what makes it difficult today?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe how you would like Nightcap to behave.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Describe any workarounds or alternative designs you have considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add mockups, screenshots, links, or implementation notes if useful.
- type: checkboxes
id: checks
attributes:
label: Before submitting
options:
- label: I searched the existing issues and did not find a duplicate.
required: true
+7
View File
@@ -0,0 +1,7 @@
## Change
Describe the behavior changed on Steam Frame and link any relevant issue.
## Checks
Describe the build, tests or device checks run for this change.
-104
View File
@@ -1,104 +0,0 @@
name: Publish AUR development package
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: aur-mocktail-git
cancel-in-progress: true
jobs:
publish:
name: Publish mocktail-git
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Prepare AUR metadata
shell: bash
run: |
set -Eeuo pipefail
package_dir="$GITHUB_WORKSPACE/packaging/aur/mocktail-git"
version="$(sed -n \
's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' \
CMakeLists.txt | head -n 1)"
[[ "$version" =~ ^[0-9]+(\.[0-9]+)*$ ]]
pkgver="${version}.r$(git rev-list --count HEAD).g$(git rev-parse --short=7 HEAD)"
sed -i "s/^pkgver=.*/pkgver=${pkgver}/" "$package_dir/PKGBUILD"
docker run --rm \
--volume "$package_dir:/pkg" \
archlinux:base-devel bash -Eeuo pipefail -c '
useradd --create-home builder
install -d -o builder -g builder /tmp/mocktail-git
install -o builder -g builder -m 0644 \
/pkg/PKGBUILD /tmp/mocktail-git/PKGBUILD
runuser -u builder -- bash -c \
"cd /tmp/mocktail-git && makepkg --printsrcinfo" \
> /pkg/.SRCINFO
'
grep -Fqx "pkgver=${pkgver}" "$package_dir/PKGBUILD"
grep -Fqx $'\tpkgver = '"${pkgver}" "$package_dir/.SRCINFO"
printf 'AUR_PKGVER=%s\n' "$pkgver" >> "$GITHUB_ENV"
- name: Publish to the AUR
shell: bash
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_SSH_KEY_PASSPHRASE: ${{ secrets.AUR_SSH_KEY_PASSPHRASE }}
run: |
set -Eeuo pipefail
[[ -n "$AUR_SSH_PRIVATE_KEY" ]]
[[ -n "$AUR_SSH_KEY_PASSPHRASE" ]]
private_key="$RUNNER_TEMP/aur_ed25519"
askpass="$RUNNER_TEMP/aur_askpass"
known_hosts="$RUNNER_TEMP/aur_known_hosts"
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$private_key"
unset AUR_SSH_PRIVATE_KEY
chmod 0600 "$private_key"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf '\''%s\n'\'' "$AUR_SSH_KEY_PASSPHRASE"' \
> "$askpass"
chmod 0700 "$askpass"
eval "$(ssh-agent -s)"
trap 'ssh-agent -k >/dev/null; rm -f -- "$private_key" "$askpass"' EXIT
DISPLAY=:0 SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \
ssh-add "$private_key" </dev/null
unset AUR_SSH_KEY_PASSPHRASE
ssh-keyscan -T 10 -t ed25519 aur.archlinux.org > "$known_hosts"
fingerprint="$(ssh-keygen -lf "$known_hosts" -E sha256 | awk '{print $2}')"
[[ "$fingerprint" == \
'SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4' ]]
export GIT_SSH_COMMAND="ssh -F /dev/null -i $private_key \
-o BatchMode=yes -o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile=$known_hosts"
aur_dir="$RUNNER_TEMP/mocktail-git-aur"
git clone --branch master --single-branch \
ssh://aur@aur.archlinux.org/mocktail-git.git "$aur_dir"
install -m 0644 packaging/aur/mocktail-git/PKGBUILD "$aur_dir/PKGBUILD"
install -m 0644 packaging/aur/mocktail-git/.SRCINFO "$aur_dir/.SRCINFO"
git -C "$aur_dir" config user.name komaruworld
git -C "$aur_dir" config user.email rtysus228@gmail.com
git -C "$aur_dir" add PKGBUILD .SRCINFO
if git -C "$aur_dir" diff --cached --quiet; then
printf 'mocktail-git %s is already published\n' "$AUR_PKGVER"
exit 0
fi
git -C "$aur_dir" commit -m "Update to ${AUR_PKGVER}"
git -C "$aur_dir" push origin HEAD:master
-60
View File
@@ -1,60 +0,0 @@
name: CI
on:
push:
branches: [main]
paths:
- '.github/workflows/**'
- 'CMakeLists.txt'
- 'Makefile'
- 'README.md'
- 'config/**'
- 'packaging/**'
- 'scripts/**'
- 'stubs/**'
- 'tests/**'
pull_request:
paths:
- '.github/workflows/**'
- 'CMakeLists.txt'
- 'Makefile'
- 'README.md'
- 'config/**'
- 'packaging/**'
- 'scripts/**'
- 'stubs/**'
- 'tests/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
contracts:
name: Repository contracts
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
submodules: recursive
- name: Validate shell and JSON files
shell: bash
run: |
find scripts tests -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n
find config packaging/flatpak -type f -name '*.json' -print0 \
| xargs -0 -n1 python3 -m json.tool >/dev/null
- name: Verify build and packaging contracts
shell: bash
run: |
bash tests/managed_payload_build_contract_test.sh .
bash tests/flatpak_packaging_test.sh .
bash tests/native_packaging_test.sh .
-193
View File
@@ -1,193 +0,0 @@
name: Flatpak
on:
workflow_run:
workflows: [Native packages]
types: [completed]
pull_request:
paths:
- '.github/workflows/flatpak.yml'
- '.gitmodules'
- 'CMakeLists.txt'
- 'LICENSE'
- 'assets/screenshots/**'
- 'cmake/**'
- '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'
- 'scripts/assemble_flatpak_pages.sh'
- 'scripts/assemble_native_repositories.sh'
- 'site/**'
- 'src/**'
- 'stubs/**'
- 'third_party/**'
workflow_dispatch:
permissions:
actions: read
contents: read
concurrency:
group: flatpak-${{ github.event.workflow_run.head_sha || github.ref }}
cancel-in-progress: true
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 }}
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: |
sudo apt-get update
sudo apt-get install --yes flatpak flatpak-builder gnupg
flatpak remote-add --user --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Build Flatpak repository and bundle
shell: bash
env:
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
run: |
set -Eeuo pipefail
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_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"
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
builder_signing=(
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
)
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
fi
flatpak-builder \
--arch=x86_64 \
--jobs=4 \
--user \
--install-deps-from=flathub \
--disable-rofiles-fuse \
--force-clean \
--default-branch=stable \
--repo=repo \
"${builder_signing[@]}" \
build-flatpak-ci \
packaging/flatpak/space.bigrat.mocktail.json
if [[ "$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" \
repo
fi
flatpak build-bundle \
--arch=x86_64 \
"${bundle_keys[@]}" \
repo \
Mocktail-x86_64.flatpak \
space.bigrat.mocktail \
stable
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
./scripts/assemble_flatpak_pages.sh \
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
public native-repositories
fi
- name: Upload Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Mocktail-x86_64-flatpak
path: Mocktail-x86_64.flatpak
if-no-files-found: error
retention-days: 14
- name: Configure GitHub Pages
if: env.MOCKTAIL_PUBLISH == 'true'
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Upload GitHub Pages repository
if: env.MOCKTAIL_PUBLISH == 'true'
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: public
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')
needs: flatpak
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
-337
View File
@@ -1,337 +0,0 @@
name: Native packages
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/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: packages-${{ github.ref }}
cancel-in-progress: true
jobs:
appimage:
name: AppImage (Ubuntu 26.04)
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
submodules: recursive
- name: Build AppImage
shell: bash
run: |
docker run --rm \
--volume "$GITHUB_WORKSPACE:/workspace" \
--workdir /workspace \
ubuntu:26.04 bash -Eeuo pipefail -c '
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install --yes --no-install-recommends \
build-essential cmake git ninja-build pkg-config lld \
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev \
libssl-dev nlohmann-json3-dev libyaml-dev libelf-dev \
libminizip-dev libcapstone-dev libgtk-4-dev \
libadwaita-1-dev libwebkitgtk-6.0-dev libutf8proc-dev \
libfontconfig1-dev libegl-dev libvulkan-dev zlib1g-dev \
libplacebo-dev \
bubblewrap xdg-dbus-proxy patchelf jq unzip file curl \
ca-certificates default-jdk-headless libgif7 python3 \
python3-capstone python3-yaml python3-requests \
python3-urllib3 python3-certifi python3-charset-normalizer \
python3-idna squashfs-tools
curl --fail --location --retry 3 \
--output /tmp/appimagetool \
https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 /tmp/appimagetool" | \
sha256sum --check --strict
install -m 0755 /tmp/appimagetool /usr/local/bin/appimagetool
curl --fail --location --retry 3 \
--output /tmp/android-build-tools.zip \
https://dl.google.com/android/repository/build-tools_r35_linux.zip
echo "bd3a4966912eb8b30ed0d00b0cda6b6543b949d5ffe00bea54c04c81e1561d88 /tmp/android-build-tools.zip" | \
sha256sum --check --strict
unzip -q /tmp/android-build-tools.zip -d /tmp/android-build-tools
android_tools_root="$(dirname -- "$(find /tmp/android-build-tools -type f -name apksigner -print -quit)")"
[[ -n "$android_tools_root" ]]
git config --global --add safe.directory /workspace
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
git config --global --add safe.directory /workspace/third_party/libjnivm
export APPIMAGE_EXTRACT_AND_RUN=1
export MOCKTAIL_ANDROID_BUILD_TOOLS_ROOT="$android_tools_root"
make appimage BUILD_DIR=build-appimage JOBS=4 \
APPIMAGE=dist/Mocktail-x86_64.AppImage \
PORTABLE_MODE=standalone
test -x dist/Mocktail-x86_64.AppImage
'
- name: Upload AppImage
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Mocktail-AppImage
path: dist/Mocktail-x86_64.AppImage
if-no-files-found: error
retention-days: 14
deb:
name: DEB (Ubuntu 26.04)
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
submodules: recursive
- name: Build DEB
shell: bash
run: |
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 \
-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]
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: write
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Download packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: Mocktail-*
path: release
merge-multiple: true
- name: Prepare continuous packages
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
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 tag --force continuous "$GITHUB_SHA"
git push origin refs/tags/continuous --force
if ! gh release view continuous >/dev/null 2>&1; then
gh release create continuous \
--prerelease \
--title "Mocktail continuous" \
--notes "Automated development packages from main."
fi
gh release upload continuous release/* --clobber
while IFS= read -r asset; do
case "$asset" in
mocktail-nightly.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
;;
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
+202
View File
@@ -0,0 +1,202 @@
name: Steam Frame ARM64
on:
push:
branches: [steam-frame]
tags: ['steam-frame-*']
pull_request:
branches: [steam-frame]
workflow_dispatch:
inputs:
publish_release:
description: Publish a release for an existing steam-frame-* tag
type: boolean
default: false
release_tag:
description: Existing tag to build when publishing (leave empty for artifact only)
type: string
default: ''
permissions:
contents: read
concurrency:
group: steam-frame-arm64-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
IMAGE: localhost/nightcap-vr-arm64:frame-foveated
ASSET: nightcap-steam-frame-arm64.oci.tar.zst
jobs:
source-checks:
runs-on: ubuntu-24.04-arm
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release && inputs.release_tag || github.ref }}
- name: Check Native settings and package scripts
run: |
for script in build-frame-nightcap.sh packaging/steam-frame/verify_image.sh; do
bash -n "$script"
done
python3 - <<'PY'
import ast
from pathlib import Path
paths = list(Path('tools/steam_frame').glob('*.py'))
paths.append(Path('tools/steam_frame/nightcap-vr-arm64'))
for path in paths:
ast.parse(path.read_text(), filename=str(path))
PY
python3 -m unittest discover -s tests -p 'steam_frame_*_test.py'
node tests/steamvr_video_controls_test.js
build:
if: github.event_name != 'pull_request'
needs: source-checks
runs-on: ubuntu-24.04-arm
timeout-minutes: 90
outputs:
release_tag: ${{ steps.release-target.outputs.tag }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
fetch-depth: 0
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release && inputs.release_tag || github.ref }}
- name: Validate release target
id: release-target
env:
EVENT: ${{ github.event_name }}
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
PUBLISH: ${{ inputs.publish_release }}
REQUESTED_TAG: ${{ inputs.release_tag }}
run: |
tag=''
if [[ "$EVENT" == push && "$REF_TYPE" == tag ]]; then
tag="$REF_NAME"
elif [[ "$EVENT" == workflow_dispatch && "$PUBLISH" == true ]]; then
tag="$REQUESTED_TAG"
fi
if [[ -n "$tag" || "$PUBLISH" == true ]]; then
[[ "$tag" =~ ^steam-frame-[A-Za-z0-9][A-Za-z0-9._-]*$ ]]
[[ "$(git rev-parse "refs/tags/$tag^{commit}")" == "$(git rev-parse HEAD)" ]]
fi
printf 'tag=%s\n' "$tag" >> "$GITHUB_OUTPUT"
- name: Install container tools
run: |
test "$(uname -m)" = aarch64
sudo apt-get update
sudo apt-get install --no-install-recommends -y podman zstd
- name: Build Native ARM64 image
run: |
sudo podman build --arch arm64 --jobs 1 \
--label "org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
--label org.opencontainers.image.title='Nightcap Steam Frame ARM64' \
-f packaging/steam-frame/Containerfile -t "$IMAGE" .
- name: Run mirror and runtime configuration regressions
run: |
sudo podman run --rm --entrypoint /bin/bash "$IMAGE" -lc '
set -euo pipefail
cmake -S /work -B /work/build-frame-arm64 -DBUILD_TESTING=ON
cmake --build /work/build-frame-arm64 -j3 \
--target desktop_mirror_test runtime_config_test runtime_config_file_test
/work/build-frame-arm64/tests/desktop_mirror_test
/work/build-frame-arm64/tests/runtime_config_test
/work/build-frame-arm64/tests/runtime_config_file_test
'
- name: Check payload-free image
run: |
mkdir -p dist
sudo bash packaging/steam-frame/verify_image.sh "$IMAGE" > dist/image-check.json
cat dist/image-check.json
- name: Export image and checksums
run: |
sudo podman save --format oci-archive "$IMAGE" | zstd -T2 -6 -o "dist/$ASSET"
test "$(stat -c %s "dist/$ASSET")" -lt 2147483648
python3 - <<'PY'
import json, os, pathlib, subprocess, tarfile
archive = pathlib.Path('dist') / os.environ['ASSET']
process = subprocess.Popen(['zstd', '-dc', str(archive)], stdout=subprocess.PIPE)
index = None
with tarfile.open(fileobj=process.stdout, mode='r|') as stream:
for member in stream:
if member.name == 'index.json':
index = json.load(stream.extractfile(member))
while process.stdout.read(1024 * 1024):
pass
if process.wait() != 0:
raise SystemExit('Compressed OCI archive integrity check failed')
manifests = index.get('manifests', []) if index else []
if (len(manifests) != 1 or manifests[0].get('annotations', {}).get(
'org.opencontainers.image.ref.name') != os.environ['IMAGE']):
raise SystemExit('OCI archive must restore the normal launcher image tag')
print('Compressed OCI archive and launcher tag verified')
PY
cd dist
sha256sum "$ASSET" > "$ASSET.sha256"
- name: Write build metadata
env:
RELEASE_TAG: ${{ steps.release-target.outputs.tag }}
run: |
python3 - <<'PY'
import json, os, pathlib, subprocess
commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()
image_id = subprocess.check_output(['sudo', 'podman', 'image', 'inspect', '--format', '{{.Id}}', os.environ['IMAGE']], text=True).strip()
metadata = {
'source': 'https://github.com/' + os.environ['GITHUB_REPOSITORY'],
'commit': commit, 'release_tag': os.environ['RELEASE_TAG'] or None,
'platform': 'linux/arm64', 'image_tag': os.environ['IMAGE'], 'image_id': image_id,
'archive': os.environ['ASSET'], 'upscaling': 'Native', 'fsr3_compiled': False,
'default_eye_size': [2160, 2160], 'default_refresh_hz': 72,
'default_antialiasing': 'SMAA High',
'checks': ['Native single-sample policy', 'refresh-rate policy', 'desktop mirror policy', 'video controls',
'mirror command recording and runtime configuration',
'AArch64 ELF and dynamic dependencies', 'payload and license packaging',
'compressed OCI integrity and launcher tag'],
'runtime_testing': 'CI packaging checks do not start SteamVR or Roblox',
'licenses': 'Source notices and third-party notices are included under /work in the image',
}
pathlib.Path('dist/build-info.json').write_text(json.dumps(metadata, indent=2) + '\n')
PY
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: nightcap-steam-frame-arm64
path: dist/
if-no-files-found: error
compression-level: 0
retention-days: 14
release:
if: needs.build.outputs.release_tag != ''
needs: build
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: nightcap-steam-frame-arm64
path: dist
- name: Publish checked ARM64 package
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.build.outputs.release_tag }}
run: |
cd dist
sha256sum --check "$ASSET.sha256"
printf '%s\n' \
'Native ARM64 Steam Frame image and packaging-check metadata.' \
'Roblox, SteamVR and host graphics drivers are downloaded or provided separately.' \
'Verify the SHA256, then import with: zstd -dc nightcap-steam-frame-arm64.oci.tar.zst | podman load' \
'See docs/INSTALL.md in this tag for launcher and SteamVR setup.' > release-notes.md
gh release create "$RELEASE_TAG" --verify-tag \
--title "Steam Frame ARM64 $RELEASE_TAG" --notes-file release-notes.md \
"$ASSET" "$ASSET.sha256" image-check.json build-info.json
+29 -13
View File
@@ -1,7 +1,9 @@
# Build
/build/
/build-test/
/build_*/
/build-flatpak/
/build-anylinux*/
/.flatpak-builder/
/cmake-build-*/
/out/
@@ -20,6 +22,12 @@ Testing/
CMakeUserPresets.json
*.cmake.user
# local direnv development shell cache
.direnv/
# clangd cache
.cache
*.o
*.obj
*.a
@@ -50,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/
@@ -181,3 +177,23 @@ lsan.log*
# Test fixtures
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.zip
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.apk
# Working plans, not part of the shipped documentation
/docs/plans/
# Steam Frame local builds and generated SteamVR assets
/build-frame-arm64/
/frame-ui-local/
/tools/steam_frame/steamvr-*.original.*
/tools/steam_frame/steamvr-*.sharpening.*
/tools/steam_frame/steamvr-sharpening-ui-patch.json
/steam-shortcut.json
# Local Frame payloads, images and source-transfer archives
/data/
/host-drivers/
*.bundle
*.oci
*.img
/current.json
/previous_good.json
+3
View File
@@ -4,3 +4,6 @@
[submodule "third_party/Vulkan-Headers"]
path = third_party/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git
[submodule "third_party/OpenXR-SDK"]
path = third_party/OpenXR-SDK
url = https://github.com/KhronosGroup/OpenXR-SDK.git
+110 -83
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,8 +9,8 @@
cmake_minimum_required(VERSION 3.20)
project(Mocktail
VERSION 1.0.3
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
VERSION 0.3.4
DESCRIPTION "Android 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}"
@@ -125,6 +126,8 @@ set(VULKAN_HEADERS_ENABLE_INSTALL OFF CACHE BOOL
"Install vendored Vulkan-Headers" FORCE)
add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)
include(${CMAKE_SOURCE_DIR}/cmake/MocktailVR.cmake)
include(CTest)
if(BUILD_TESTING)
# Package builds disable tests and do not fetch Google Test.
@@ -138,12 +141,7 @@ if(BUILD_TESTING)
FetchContent_MakeAvailable(googletest)
endif()
add_library(mocktail_crypto_foundation STATIC
src/foundation/sha256.cc
)
add_library(Mocktail::CryptoFoundation ALIAS mocktail_crypto_foundation)
target_include_directories(mocktail_crypto_foundation PUBLIC include)
mocktail_apply_compile_options(mocktail_crypto_foundation)
find_package(OpenSSL REQUIRED)
include(${CMAKE_SOURCE_DIR}/cmake/MocktailPlatformGraphics.cmake)
foreach(foundation_target IN ITEMS
@@ -201,12 +199,12 @@ target_include_directories(mocktail_bionic_stdio_runtime PUBLIC include)
mocktail_apply_compile_options(mocktail_bionic_stdio_runtime)
find_package(CURL REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBYAML REQUIRED IMPORTED_TARGET yaml-0.1)
pkg_check_modules(MINIZIP REQUIRED IMPORTED_TARGET minizip)
pkg_check_modules(CAPSTONE REQUIRED IMPORTED_TARGET capstone)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
pkg_check_modules(GTK4 REQUIRED IMPORTED_TARGET gtk4)
pkg_check_modules(LIBADWAITA REQUIRED IMPORTED_TARGET "libadwaita-1>=1.6")
pkg_check_modules(WEBKITGTK6 REQUIRED IMPORTED_TARGET webkitgtk-6.0)
@@ -214,6 +212,7 @@ pkg_check_modules(WEBKITGTK6 REQUIRED IMPORTED_TARGET webkitgtk-6.0)
add_library(mocktail_update STATIC
src/update/android_manifest.cc
src/update/apk_bundle.cc
src/update/apk_provider.cc
src/update/apk_signature.cc
src/update/apkpure_provider.cc
src/update/candidate_approval.cc
@@ -223,6 +222,7 @@ add_library(mocktail_update STATIC
src/update/payload_integrity.cc
src/update/payload_store.cc
src/update/readiness_canary.cc
src/update/unsafe_latest_runner.cc
src/update/update_config.cc
src/update/update_coordinator.cc
src/update/zip_archive.cc
@@ -231,7 +231,6 @@ add_library(Mocktail::Update ALIAS mocktail_update)
target_include_directories(mocktail_update PUBLIC include)
target_link_libraries(mocktail_update PUBLIC
Mocktail::Compat
Mocktail::CryptoFoundation
CURL::libcurl
OpenSSL::Crypto
PkgConfig::LIBYAML
@@ -252,6 +251,10 @@ mocktail_apply_compile_options(mocktail_updater)
add_library(mocktail_legacy_jnivm STATIC
src/jnivm/jnivm.cc
src/jnivm/jni_references.cc
src/jnivm/jni_fields.cc
src/jnivm/jni_strings.cc
src/jnivm/jni_method_names.cc
)
add_library(Mocktail::LegacyJni ALIAS mocktail_legacy_jnivm)
target_include_directories(mocktail_legacy_jnivm PUBLIC include)
@@ -269,13 +272,16 @@ 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
src/runtime/http_client_policy.cc
src/runtime/luau_codegen_switch.cc
src/runtime/memory_limit.cc
src/runtime/owned_pthread.cc
src/runtime/performance_policy.cc
@@ -286,9 +292,18 @@ add_library(mocktail_runtime STATIC
src/runtime/roblox_capability_resolver.cc
src/runtime/roblox_experience_launch_bridge.cc
src/runtime/roblox_launch_uri.cc
src/runtime/roblox_presence_resolver.cc
src/runtime/roblox_game_session_native_adapter.cc
src/runtime/roblox_web_view_bridge.cc
src/runtime/roblox_permissions_bridge.cc
src/runtime/roblox_account_bridge.cc
src/runtime/roblox_message_bus_request_bridge.cc
src/runtime/roblox_call_protocol_bridge.cc
src/runtime/roblox_haptic_protocol_bridge.cc
src/runtime/payload_update_preflight.cc
src/runtime/process_launch_policy.cc
src/runtime/process_diagnostics.cc
src/runtime/asset_overlay_names.cc
src/runtime/runtime_config.cc
src/runtime/runtime_config_bootstrap.cc
src/runtime/runtime_config_file.cc
@@ -298,6 +313,9 @@ add_library(mocktail_runtime STATIC
src/runtime/startup_state_machine.cc
src/runtime/supported_launch_policy.cc
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)
@@ -328,9 +346,10 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
endif()
target_link_libraries(mocktail_runtime PUBLIC Threads::Threads)
target_link_libraries(mocktail_runtime PRIVATE
Mocktail::CryptoFoundation
OpenSSL::Crypto
CURL::libcurl
nlohmann_json::nlohmann_json
PkgConfig::GIO
PkgConfig::LIBYAML
${CMAKE_DL_LIBS}
)
@@ -379,7 +398,8 @@ add_library(mocktail_webview_helper_policy STATIC
src/webview/webview_helper_policy.cc
)
add_library(Mocktail::WebViewHelperPolicy ALIAS mocktail_webview_helper_policy)
target_include_directories(mocktail_webview_helper_policy PUBLIC src)
target_include_directories(mocktail_webview_helper_policy PUBLIC src
PRIVATE include)
target_link_libraries(mocktail_webview_helper_policy PUBLIC
PkgConfig::WEBKITGTK6
nlohmann_json::nlohmann_json
@@ -421,6 +441,7 @@ target_link_libraries(mocktail_linker PUBLIC
linker
mocktail_compat
mocktail_bionic_stdio_runtime
mocktail_bionic_abi_exports
)
mocktail_apply_compile_options(mocktail_linker)
target_compile_definitions(mocktail_linker PUBLIC MOCKTAIL_USE_BIONIC_LINKER=1)
@@ -449,6 +470,7 @@ add_library(mocktail_window STATIC
src/window/roblox_fullscreen_menu_request_gate.cc
src/window/video_driver_policy.cc
src/window/window.cc
src/window/window_creation_policy.cc
src/window/window_fullscreen_request_gate.cc
src/window/window_game_surface_bridge.cc
src/window/window_pointer_capture_owner.cc
@@ -461,9 +483,10 @@ target_include_directories(mocktail_window PUBLIC src)
mocktail_apply_compile_options(mocktail_window)
target_link_libraries(mocktail_window PUBLIC
Mocktail::PlatformSdl
Mocktail::SdlVulkanWsi
SDL3::SDL3
${CMAKE_DL_LIBS}
PRIVATE Mocktail::Runtime
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay Mocktail::GraphicsFoundation
)
add_library(mocktail_roblox_output_device_runtime STATIC
@@ -525,56 +548,25 @@ target_include_directories(mocktail_bionic_abi_exports
PRIVATE src
)
mocktail_apply_compile_options(mocktail_bionic_abi_exports)
# These exported Bionic FORTIFY symbols intentionally overlap glibc's
# __*_chk entry points. When the bridge is visible to whole-program LTO,
# GCC can fold a fortified host call back into its own compatibility wrapper
# and turn the resulting tail recursion into an infinite loop. Keep this
# translation unit opaque to interprocedural optimization and make its calls
# to the host C library explicit external calls.
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
set_property(SOURCE src/compat/bionic_abi_exports.cc APPEND PROPERTY
COMPILE_OPTIONS
-fno-lto
-fno-builtin
)
endif()
target_link_libraries(mocktail_bionic_abi_exports PRIVATE
mocktail_bionic_stdio_runtime
mocktail_window
Threads::Threads
)
include(CheckCXXSourceCompiles)
include(CheckIncludeFileCXX)
include(CMakePushCheckState)
check_include_file_cxx(execinfo.h MOCKTAIL_HAVE_EXECINFO_H)
if(NOT MOCKTAIL_HAVE_EXECINFO_H)
message(FATAL_ERROR
"execinfo.h is required for crash diagnostics; on musl install "
"libexecinfo-devel"
)
endif()
set(MOCKTAIL_EXECINFO_LINK_LIBRARY "")
set(_mocktail_execinfo_link_probe [=[
#include <execinfo.h>
int main() {
void* frames[1] = {};
const int count = backtrace(frames, 1);
return backtrace_symbols(frames, count) == nullptr;
}
]=])
check_cxx_source_compiles(
"${_mocktail_execinfo_link_probe}"
MOCKTAIL_EXECINFO_IN_HOST_LIBC
)
if(NOT MOCKTAIL_EXECINFO_IN_HOST_LIBC)
find_library(MOCKTAIL_EXECINFO_LIBRARY NAMES execinfo REQUIRED)
cmake_push_check_state(RESET)
set(CMAKE_REQUIRED_LIBRARIES "${MOCKTAIL_EXECINFO_LIBRARY}")
check_cxx_source_compiles(
"${_mocktail_execinfo_link_probe}"
MOCKTAIL_EXECINFO_IN_EXTERNAL_LIBRARY
)
cmake_pop_check_state()
if(NOT MOCKTAIL_EXECINFO_IN_EXTERNAL_LIBRARY)
message(FATAL_ERROR
"execinfo.h is available, but backtrace and backtrace_symbols cannot "
"be linked"
)
endif()
set(MOCKTAIL_EXECINFO_LINK_LIBRARY "${MOCKTAIL_EXECINFO_LIBRARY}")
endif()
unset(_mocktail_execinfo_link_probe)
add_library(mocktail_legacy_runtime STATIC
src/legacy/legacy_runtime.cc
src/runtime/roblox_text_input_jni_bridge.cc
@@ -599,7 +591,6 @@ target_link_libraries(mocktail_legacy_runtime PRIVATE
libc_shim
mocktail_window
mocktail_graphics_foundation
${MOCKTAIL_EXECINFO_LINK_LIBRARY}
)
target_compile_definitions(mocktail_legacy_runtime PRIVATE
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST}\""
@@ -612,6 +603,7 @@ target_compile_definitions(mocktail PRIVATE
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST}\""
)
target_link_libraries(mocktail PRIVATE
Mocktail::VR
mocktail_legacy_runtime
mocktail_auth_runtime_composition
mocktail_failure_dialog_runtime
@@ -628,6 +620,11 @@ target_link_options(mocktail PRIVATE
)
# Native FreeBSD helper for Linuxulator's unsupported DF socket option.
# The helper is x86-64 assembly; other host architectures have nothing to
# assemble.
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER OFF)
endif()
option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER
"Build the native FreeBSD Linuxulator socket helper" ON)
if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER)
@@ -705,13 +702,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
@@ -720,7 +728,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"
)
@@ -753,33 +761,39 @@ install(CODE "
endif()
")
set(MOCKTAIL_PACKAGE_NAMES mocktail mocktail-nightly nightcap nightcap-nightly)
set(MOCKTAIL_PACKAGE_NAME "mocktail" CACHE STRING
"Native package name"
)
set(MOCKTAIL_PACKAGE_VERSION "${PROJECT_VERSION}" CACHE STRING
"Native package version"
)
set(MOCKTAIL_PACKAGE_VERSION_SUFFIX "" CACHE STRING
"Native package version suffix"
)
if(NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail" AND
NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
if(NOT MOCKTAIL_PACKAGE_NAME IN_LIST MOCKTAIL_PACKAGE_NAMES)
string(JOIN ", " MOCKTAIL_PACKAGE_NAMES_TEXT ${MOCKTAIL_PACKAGE_NAMES})
message(FATAL_ERROR
"MOCKTAIL_PACKAGE_NAME must be mocktail or mocktail-nightly")
"MOCKTAIL_PACKAGE_NAME must be one of ${MOCKTAIL_PACKAGE_NAMES_TEXT}")
endif()
set(CPACK_PACKAGE_NAME "${MOCKTAIL_PACKAGE_NAME}")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}")
set(CPACK_PACKAGE_VENDOR "Mocktail Project")
set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
set(CPACK_PACKAGE_VERSION
"${MOCKTAIL_PACKAGE_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}"
)
set(CPACK_PACKAGE_VENDOR "Nightcap")
set(CPACK_PACKAGE_CONTACT "vii <coderdayton14@gmail.com>")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/CoderDayton/nightcap")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
set(CPACK_PACKAGE_DESCRIPTION
"Runs the Android x86-64 Roblox client on Linux."
"Runs the Android Roblox client on Linux."
)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
set(CPACK_STRIP_FILES TRUE)
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "komaruworld <rtysus228@gmail.com>")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "vii <coderdayton14@gmail.com>")
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
@@ -789,15 +803,14 @@ set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
set(CPACK_RPM_PACKAGE_AUTOREQPROV ON)
if(MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail")
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail")
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail")
else()
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail-nightly")
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail-nightly")
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail-nightly")
endif()
# Every variant installs the same /usr/bin/mocktail and /usr/share/mocktail,
# so only one of them can be present at a time.
set(MOCKTAIL_PACKAGE_SIBLINGS ${MOCKTAIL_PACKAGE_NAMES})
list(REMOVE_ITEM MOCKTAIL_PACKAGE_SIBLINGS "${MOCKTAIL_PACKAGE_NAME}")
string(JOIN ", " MOCKTAIL_PACKAGE_CONFLICTS ${MOCKTAIL_PACKAGE_SIBLINGS})
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "${MOCKTAIL_PACKAGE_CONFLICTS}")
set(CPACK_DEBIAN_PACKAGE_REPLACES "${MOCKTAIL_PACKAGE_CONFLICTS}")
set(CPACK_RPM_PACKAGE_CONFLICTS "${MOCKTAIL_PACKAGE_CONFLICTS}")
include(CPack)
add_dependencies(mocktail all_stubs)
@@ -807,3 +820,17 @@ add_dependencies(mocktail mocktail_webview_helper)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
# Native SteamVR identity/settings helper; a background client never takes scene focus.
add_executable(nightcap-vr-settings tools/frame_video_settings/nightcap-vr-settings.cc)
target_include_directories(nightcap-vr-settings PRIVATE tools/frame_video_settings)
target_link_libraries(nightcap-vr-settings PRIVATE ${CMAKE_DL_LIBS})
install(TARGETS nightcap-vr-settings RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
if(MOCKTAIL_ENABLE_VR)
add_executable(nightcap-vr-performance tools/frame_video_settings/performance_overlay.cc)
target_include_directories(nightcap-vr-performance PRIVATE include tools/frame_video_settings)
target_link_libraries(nightcap-vr-performance PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf
PkgConfig::FONTCONFIG ${CMAKE_DL_LIBS})
install(TARGETS nightcap-vr-performance RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
+15 -13
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
@@ -14,10 +15,13 @@ PORTABLE_MODE ?= standalone
PORTABLE_CANONICAL_MODE := $(if $(filter dynamic minimal,$(PORTABLE_MODE)),thin,$(if $(filter full static,$(PORTABLE_MODE)),standalone,$(PORTABLE_MODE)))
PORTABLE_DIR ?= dist/mocktail-linux-x86_64-glibc-$(PORTABLE_CANONICAL_MODE)
APPIMAGE ?= dist/Mocktail-x86_64.AppImage
APPIMAGE_FORMAT ?= classic
ANYLINUX_PACKAGER ?= quick-sharun
ANYLINUX_APPIMAGETOOL ?= appimagetool
PREFIX ?= /usr
FLATPAK_BUILD_DIR ?= build-flatpak
FLATPAK_MANIFEST ?= packaging/flatpak/space.bigrat.mocktail.json
FLATPAK_JOBS ?= 4
FLATPAK_MANIFEST ?= packaging/flatpak/io.github.CoderDayton.nightcap.json
FLATPAK_JOBS ?= 2
define build_native_runtime
@git submodule update --init --recursive
@@ -52,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)
@@ -73,13 +77,11 @@ portable-test: release-runtime ## Verify portable packaging and relocation
standalone: appimage ## Build the standalone x86-64 AppImage
appimage: release-runtime ## Build AppImage (requires appimagetool)
@command -v appimagetool >/dev/null 2>&1 || { \
echo "appimagetool is required for make appimage" >&2; \
echo "Install it, then rerun make appimage." >&2; \
exit 1; \
}
@./scripts/package_portable.sh --build-dir "$(BUILD_DIR)" \
appimage: release-runtime ## Build AppImage (APPIMAGE_FORMAT=classic|anylinux)
@MOCKTAIL_APPIMAGE_FORMAT="$(APPIMAGE_FORMAT)" \
MOCKTAIL_ANYLINUX_PACKAGER="$(ANYLINUX_PACKAGER)" \
MOCKTAIL_ANYLINUX_APPIMAGETOOL="$(ANYLINUX_APPIMAGETOOL)" \
./scripts/package_portable.sh --build-dir "$(BUILD_DIR)" \
--libc glibc --output "$(PORTABLE_DIR)" --mode "$(PORTABLE_MODE)" \
--appimage "$(APPIMAGE)"
@@ -90,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
@@ -128,8 +130,8 @@ auto-run: ## Build/run loop with crash summaries
run-gles: build ## Run strict system EGL/OpenGL ES 3 without Vulkan fallback
@$(BINARY) --graphics opengl
run-angle: ## Run with ANGLE/Vulkan compatibility backend
@MOCKTAIL_GRAPHICS_BACKEND=angle-vulkan ./scripts/run_sober.sh
run-angle: build ## Run with ANGLE/Vulkan compatibility backend
@$(BINARY) --graphics angle-vulkan
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
@./scripts/update_roblox_payload.sh
+48 -167
View File
@@ -1,181 +1,62 @@
# Mocktail
<div align="center">
<img src="packaging/io.github.CoderDayton.nightcap.svg" alt="Nightcap logo" width="128">
<h1>Nightcap Roblox VR for Steam Frame</h1>
</div>
[![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)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
Native ARM64 Roblox VR for Steam Frame, with eye-tracked foveated rendering,
controller input and SteamVR integration.
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.
The standard video profile is **native 2160 × 2160 per eye**, **72 Hz**,
**SMAA High** and **Performance** foveation. Frame rate depends on the Roblox
experience and its graphics settings; 72 FPS is the target.
Mocktail is an independent community project. It is not affiliated with Roblox
Corporation or VinegarHQ and does not distribute the Roblox client.
## Features
## How it works
- Native ARM64 execution through Nightcap's Android compatibility layer.
- Stereo OpenXR rendering, head tracking, VR controllers, haptics and an
in-headset keyboard.
- Direct eye tracking with a full-detail gaze region and gradual reductions in
peripheral shading quality.
- Adjustable gaze radius and Quality, Balanced and Performance foveation modes.
- Gaze-only FXAA, SMAA Low and SMAA High anti-aliasing.
- Native-resolution rendering with a 2160 × 2160 per-eye recommended default.
- Roblox's internal frame-rate cap follows the selected headset refresh rate.
- Renderer and session handling across menu, game and game-to-game transitions.
- Native microphone capture bridge for Roblox voice chat.
- One **Nightcap Roblox VR** Steam entry with the original Nightcap icon and an
optional desktop mirror. The mirror is off by default on Steam Frame.
- Simple **Foveated Rendering**, **Gaze sharp radius** and **Anti Aliasing**
controls in SteamVR's Video settings.
- Optional in-headset performance overlay with actual game FPS, headset Hz,
frame time, GPU time and current per-eye resolution.
```
Roblox APK -> signature and ABI checks -> Bionic + JNI -> SDL3 + Vulkan/OpenGL
```
## Install
The APK is checked before any native code is loaded. It is downloaded on first
launch and is not bundled with Mocktail. The last working copy is kept in case
an update fails.
Install the native ARM64 build from [Releases](https://github.com/InstanceBrick/nightcap/releases),
or build it on the Frame using the [installation guide](docs/INSTALL.md).
The ARM64 package contains Nightcap and its open-source runtime dependencies.
Roblox is downloaded separately on the device. Roblox binaries, account data,
SteamVR files and the Frame's graphics drivers are not included in the package.
## Install with Flatpak
Current ARM64 client support: **2.742.1059 (3222)** and **2.736.1408 (2998)**.
Install the stable release from Flathub:
- [Installation and updates](docs/INSTALL.md)
- [Video, input and microphone settings](docs/SETTINGS.md)
- [Building and source layout](docs/DEVELOPMENT.md)
```bash
flatpak install flathub space.bigrat.mocktail
flatpak run space.bigrat.mocktail
```
## About this fork
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 gameplay in Mocktail](assets/screenshots/flatpak-gameplay-tower.png)
![Roblox experience in Mocktail](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.
```json
{
"FFlagExample": "True",
"DFIntExample": "120"
}
```
## 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
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,
Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0.
See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instructions.
<details>
<summary>Ubuntu 26.04+</summary>
```bash
sudo apt update
sudo apt install build-essential cmake git ninja-build pkg-config lld \
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
libplacebo-dev zlib1g-dev
```
</details>
<details>
<summary>Arch Linux</summary>
```bash
sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
curl openssl nlohmann-json libyaml libelf minizip capstone gtk4 \
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
libplacebo vulkan-headers vulkan-icd-loader zlib
```
</details>
<details>
<summary>Fedora 44+</summary>
```bash
sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
nlohmann-json-devel libyaml-devel elfutils-libelf-devel minizip-ng-compat-devel \
capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \
utf8proc-devel fontconfig-devel libglvnd-devel vulkan-headers \
vulkan-loader-devel libplacebo-devel zlib-ng-compat-devel
```
</details>
```bash
git clone --recurse-submodules https://github.com/komaruworld/mocktail.git
cd mocktail
make build
./build/mocktail
```
The `steam-frame` branch is the Steam Frame edition of the `vr` branch of
[CoderDayton/nightcap](https://github.com/CoderDayton/nightcap), itself a fork of
[komaruworld/mocktail](https://github.com/komaruworld/mocktail). It adds the native
ARM64 VR bridge and Frame rendering, input, audio and Steam integration changes.
The original Nightcap artwork is retained. Internal executable and configuration
names keep `mocktail` for compatibility with the upstream source.
## License
[Apache License 2.0](LICENSE). Third-party components keep their own licenses.
[Apache License 2.0](LICENSE). Original copyright notices and third-party
licenses remain in their source directories. See [third-party credits](THIRD_PARTY.md).
## Support
You can support the project by giving it a star or with cryptocurrency:
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
Nightcap is an independent community project and is not affiliated with Roblox
Corporation or Valve.
+31
View File
@@ -0,0 +1,31 @@
# Credits and third-party components
This edition includes modifications for Steam Frame ARM64 and is based on:
- [Nightcap](https://github.com/CoderDayton/nightcap), including vii's changes to
Mocktail and the upstream `vr` branch.
- [Mocktail](https://github.com/komaruworld/mocktail), by the Mocktail project
authors.
The Steam Frame edition incorporates the upstream VR tree through
[`753a3f81`](https://github.com/CoderDayton/nightcap/commit/753a3f8106e4d7fdafea457601a5ff238e77c2d0)
(15 October 2026), including GPU-ordered presentation, asynchronous eye and
mirror copies, cached JNI method dispatch, headset detection and the JNI haptic
protocol. ARM64 client contracts and Frame gaze AA remain platform-specific.
The project is distributed under the [Apache License 2.0](LICENSE). Existing
copyright and modification notices are retained in the source.
Third-party components retain their own licenses and notices, including the
Android/Bionic linker sources under `third_party/mcpelauncher-linker`, the pinned
OpenXR SDK, Vulkan headers and libjnivm submodules, SMAA under `src/vr/smaa`, AMD FidelityFX FSR 1 under `src/vr/fsr`
([MIT license](src/vr/fsr/LICENSE.txt)),
AMD FidelityFX SDK 1.1.4's FSR 3.1.4 upscaler, Optical Flow component and Vulkan
backend under `third_party/fidelityfx`
([MIT license](third_party/fidelityfx/LICENSE.txt)), and
the OpenVR interface header under `tools/frame_video_settings`. The original
Nightcap icon and artwork are retained from the upstream project.
Roblox client binaries and Valve SteamVR dashboard assets are not included.
The client is downloaded separately and the Video settings tools read dashboard
files from the user's installed SteamVR copy.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
cmake -S /work -B /work/build-frame-arm64 -G Ninja -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON -DMOCKTAIL_ENABLE_FSR3=OFF -DMOCKTAIL_VR_DIAGNOSTICS=OFF -DBUILD_TESTING=OFF -DMOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER=OFF -DCMAKE_INSTALL_PREFIX=/opt/nightcap-vr-arm64
cmake --build /work/build-frame-arm64 -j3
cmake --install /work/build-frame-arm64
c++ -std=c++17 -O2 /work/tools/frame_video_settings/nightcap-vr-settings.cc -ldl -o /opt/nightcap-vr-arm64/bin/nightcap-vr-settings
printf 'NIGHTCAP_ARM64_BUILD_COMPLETE\n'
+2 -6
View File
@@ -24,6 +24,7 @@ set_target_properties(mocktail_audio_core PROPERTIES
add_library(Mocktail::AudioCore ALIAS mocktail_audio_core)
add_library(mocktail_audio_sdl SHARED
${MOCKTAIL_AUDIO_ROOT}/src/audio/sdl_audio_capture.cc
${MOCKTAIL_AUDIO_ROOT}/src/audio/sdl_audio_sink.cc
)
target_include_directories(mocktail_audio_sdl PUBLIC
@@ -62,6 +63,7 @@ add_library(Mocktail::FmodJavaAudioRuntime ALIAS
add_library(mocktail_fmod_jni_audio_bridge STATIC
${MOCKTAIL_AUDIO_ROOT}/src/audio/fmod_jni_audio_bridge.cc
${MOCKTAIL_AUDIO_ROOT}/src/audio/webrtc_jni_audio_bridge.cc
)
target_include_directories(mocktail_fmod_jni_audio_bridge PUBLIC
${MOCKTAIL_AUDIO_ROOT}/include
@@ -199,10 +201,4 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
gtest_discover_tests(fmod_jni_audio_bridge_test
PROPERTIES ENVIRONMENT "SDL_AUDIODRIVER=dummy"
)
add_test(NAME audio_readiness_gate_test
COMMAND bash
${MOCKTAIL_AUDIO_ROOT}/tests/audio_readiness_gate_test.sh
${MOCKTAIL_AUDIO_ROOT}/scripts/audio_readiness_gate.sh
)
endif()
+1 -1
View File
@@ -44,7 +44,7 @@ target_link_libraries(mocktail_compat PUBLIC
nlohmann_json::nlohmann_json
Threads::Threads
)
target_link_libraries(mocktail_compat PRIVATE Mocktail::CryptoFoundation)
target_link_libraries(mocktail_compat PRIVATE OpenSSL::Crypto)
target_compile_features(mocktail_compat PUBLIC cxx_std_17)
if(COMMAND mocktail_apply_compile_options)
+18 -3
View File
@@ -13,13 +13,16 @@ get_filename_component(MOCKTAIL_INPUT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.."
)
add_library(mocktail_input_runtime STATIC
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_gamepad_input.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_input_native_adapter.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_input_router.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_native_text_box_info_reader.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_display_state.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_editor.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_font_resolver.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_surface_overlay.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_window_input_runtime.cc
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_xr_controller_input.cc
)
add_library(Mocktail::InputRuntime ALIAS mocktail_input_runtime)
target_include_directories(mocktail_input_runtime PUBLIC
@@ -31,18 +34,23 @@ target_include_directories(mocktail_input_runtime PRIVATE
target_link_libraries(mocktail_input_runtime PUBLIC
Mocktail::PlatformSdl
Mocktail::Runtime
Mocktail::VR
mocktail_window
PkgConfig::UTF8PROC
PRIVATE
PkgConfig::FONTCONFIG
SDL3_ttf::SDL3_ttf
nlohmann_json::nlohmann_json
${CMAKE_DL_LIBS}
)
target_compile_features(mocktail_input_runtime PUBLIC cxx_std_17)
mocktail_apply_compile_options(mocktail_input_runtime)
if(BUILD_TESTING AND TARGET GTest::gtest_main)
add_executable(roblox_input_router_test
${MOCKTAIL_INPUT_ROOT}/tests/roblox_gamepad_input_test.cc
${MOCKTAIL_INPUT_ROOT}/tests/roblox_input_router_test.cc
${MOCKTAIL_INPUT_ROOT}/tests/roblox_xr_controller_input_test.cc
)
target_link_libraries(roblox_input_router_test PRIVATE
Mocktail::InputRuntime
@@ -68,6 +76,15 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
)
mocktail_apply_compile_options(roblox_text_display_state_test)
add_executable(roblox_text_font_resolver_test
${MOCKTAIL_INPUT_ROOT}/tests/roblox_text_font_resolver_test.cc
)
target_link_libraries(roblox_text_font_resolver_test PRIVATE
Mocktail::InputRuntime
GTest::gtest_main
)
mocktail_apply_compile_options(roblox_text_font_resolver_test)
add_executable(roblox_input_native_adapter_test
${MOCKTAIL_INPUT_ROOT}/tests/roblox_input_native_adapter_test.cc
)
@@ -92,9 +109,7 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
gtest_discover_tests(roblox_input_router_test)
gtest_discover_tests(roblox_text_editor_test)
gtest_discover_tests(roblox_text_display_state_test)
gtest_discover_tests(roblox_text_font_resolver_test)
gtest_discover_tests(roblox_input_native_adapter_test)
gtest_discover_tests(roblox_native_text_box_info_reader_test)
add_test(NAME input_readiness_gate_synthetic
COMMAND bash ${MOCKTAIL_INPUT_ROOT}/tests/input_readiness_gate_test.sh
)
endif()
+27 -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}"
@@ -69,6 +71,7 @@ add_library(mocktail_platform_sdl STATIC
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_application_metadata.cc
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_display_refresh_capabilities.cc
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_event_converter.cc
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_gamepad_manager.cc
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_platform_runtime.cc
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_text_clipboard.cc
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_window_icon.cc
@@ -102,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
@@ -119,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
)
@@ -152,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)
+121
View File
@@ -0,0 +1,121 @@
# Copyright 2026 Mocktail Project Authors
# Licensed under the Apache License, Version 2.0.
include_guard(GLOBAL)
option(MOCKTAIL_ENABLE_FSR3 "Build the optional temporal upscaler SDK" OFF)
# AUTO builds OpenXR support when the third_party/OpenXR-SDK submodule is
# checked out and skips it otherwise, so source packages that do not fetch
# submodules still build. ON requires the submodule; OFF never builds it.
set(MOCKTAIL_ENABLE_VR "AUTO" CACHE STRING
"Build experimental OpenXR support: AUTO, ON or OFF")
set_property(CACHE MOCKTAIL_ENABLE_VR PROPERTY STRINGS AUTO ON OFF)
set(_mocktail_openxr_sdk "${CMAKE_SOURCE_DIR}/third_party/OpenXR-SDK/CMakeLists.txt")
string(TOUPPER "${MOCKTAIL_ENABLE_VR}" _mocktail_vr_request)
if(_mocktail_vr_request STREQUAL "AUTO")
if(EXISTS "${_mocktail_openxr_sdk}" AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(MOCKTAIL_ENABLE_VR ON)
else()
message(STATUS "OpenXR SDK submodule not found; building without VR "
"(git submodule update --init third_party/OpenXR-SDK to enable it)")
set(MOCKTAIL_ENABLE_VR OFF)
endif()
elseif(MOCKTAIL_ENABLE_VR)
set(MOCKTAIL_ENABLE_VR ON)
else()
set(MOCKTAIL_ENABLE_VR OFF)
endif()
add_library(mocktail_vr STATIC
src/vr/gaze_aa.cc
src/vr/fsr_upscaler.cc
src/vr/fsr3_scene_capture.cc
src/vr/diagnostics.cc
src/vr/headset_detection.cc
src/vr/openxr_backend_mode.cc
src/vr/vr_perf.cc
src/vr/vr_mirror.cc
src/vr/xr_controller.cc
src/vr/roblox_vr_device_bridge.cc
src/vr/virtual_keyboard.cc)
add_library(Mocktail::VR ALIAS mocktail_vr)
target_include_directories(mocktail_vr PUBLIC "${CMAKE_SOURCE_DIR}/include")
target_include_directories(mocktail_vr PRIVATE "${CMAKE_SOURCE_DIR}/src")
target_link_libraries(mocktail_vr PUBLIC Vulkan::Headers PRIVATE nlohmann_json::nlohmann_json)
# shm_open for the desktop eye mirror lives in librt on glibc.
find_library(MOCKTAIL_RT_LIBRARY rt)
if(MOCKTAIL_RT_LIBRARY)
target_link_libraries(mocktail_vr PUBLIC ${MOCKTAIL_RT_LIBRARY})
endif()
mocktail_apply_compile_options(mocktail_vr)
if(MOCKTAIL_ENABLE_VR)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "Mocktail OpenXR support currently targets Linux")
endif()
if(NOT EXISTS "${_mocktail_openxr_sdk}")
message(FATAL_ERROR
"Initialize OpenXR: git submodule update --init third_party/OpenXR-SDK")
endif()
# Scope upstream's generic option names to this dependency. Static linking
# packages the pinned loader without requiring a matching system loader.
function(mocktail_add_openxr_sdk)
set(BUILD_LOADER ON)
set(DYNAMIC_LOADER OFF)
set(BUILD_WITH_SYSTEM_JSONCPP OFF)
set(BUILD_TESTS OFF)
set(BUILD_API_LAYERS OFF)
set(BUILD_CONFORMANCE_TESTS OFF)
set(BUILD_SDK_TESTS OFF)
set(BUILD_FORCE_GENERATION OFF)
add_subdirectory("${CMAKE_SOURCE_DIR}/third_party/OpenXR-SDK"
"${CMAKE_BINARY_DIR}/third_party/OpenXR-SDK" EXCLUDE_FROM_ALL)
endfunction()
mocktail_add_openxr_sdk()
if(MOCKTAIL_ENABLE_FSR3)
add_subdirectory(third_party/fidelityfx)
target_sources(mocktail_vr PRIVATE src/vr/fsr3_upscaler.cc src/vr/fsr3_gaze_transport.cc)
target_link_libraries(mocktail_vr PRIVATE nightcap_fsr3)
target_compile_definitions(mocktail_vr PUBLIC MOCKTAIL_ENABLE_FSR3=1)
endif()
target_sources(mocktail_vr PRIVATE src/vr/openxr_probe.cc src/vr/openxr_preview.cc
src/vr/openxr_backend.cc src/vr/xr_foveation_maps.cc src/vr/gles_transport.cc src/vr/xr_actions.cc
src/vr/virtual_keyboard_raster.cc)
# The in-headset keyboard draws its labels with the same SDL3_ttf and
# fontconfig stack as the TextBox overlay.
find_package(SDL3 3.4 REQUIRED CONFIG)
find_package(SDL3_ttf REQUIRED CONFIG)
find_package(PkgConfig REQUIRED)
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
target_link_libraries(mocktail_vr PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3
PkgConfig::FONTCONFIG)
# Error unwinding is confined to these native session implementations; their
# public entry points catch errors before returning to the guest runtime.
set_source_files_properties(src/vr/openxr_preview.cc src/vr/openxr_backend.cc
src/vr/xr_actions.cc src/vr/xr_foveation_maps.cc
PROPERTIES COMPILE_OPTIONS -fexceptions)
target_compile_definitions(mocktail_vr PRIVATE XR_USE_GRAPHICS_API_VULKAN)
option(MOCKTAIL_VR_DIAGNOSTICS
"Compile VR diagnostics: scripted head pose, eye evidence dumps" OFF)
if(MOCKTAIL_VR_DIAGNOSTICS)
target_compile_definitions(mocktail_vr PRIVATE MOCKTAIL_VR_DIAGNOSTICS=1)
endif()
set_source_files_properties(src/vr/openxr_backend.cc PROPERTIES COMPILE_DEFINITIONS "XR_USE_GRAPHICS_API_OPENGL_ES;XR_USE_PLATFORM_EGL")
target_include_directories(mocktail_vr PRIVATE ${MOCKTAIL_EGL_INCLUDE_DIR} ${MOCKTAIL_GLES3_INCLUDE_DIR})
target_link_libraries(mocktail_vr PRIVATE OpenXR::openxr_loader Vulkan::Headers ${CMAKE_DL_LIBS})
else()
target_sources(mocktail_vr PRIVATE src/vr/openxr_disabled.cc
src/vr/xr_actions_disabled.cc src/vr/xr_foveation_maps_disabled.cc)
endif()
if(MOCKTAIL_ENABLE_VR)
add_executable(mocktail_vr_probe src/vr/probe_main.cc)
set_target_properties(mocktail_vr_probe PROPERTIES
OUTPUT_NAME mocktail-vr-probe)
target_link_libraries(mocktail_vr_probe PRIVATE Mocktail::VR)
mocktail_apply_compile_options(mocktail_vr_probe)
install(TARGETS mocktail_vr_probe
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
endif()
+101 -22
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.
@@ -27,21 +28,54 @@ device: pc-windows-11
runtime:
# Boolean (default: false): start without a visible SDL window.
headless: false
# String (optional): use a specific x86_64 libroblox.so instead of the
# String (optional): use a specific native-architecture libroblox.so instead of the
# active verified payload.
# roblox_library: /absolute/path/to/libroblox.so
appearance:
# String (default: roblox): use Roblox's saved account theme. Supported
# overrides: dark, light, or system (follow the desktop color scheme).
theme: roblox
graphics:
# String (default: direct-vulkan): graphics backend. Use opengl on hardware
# without a working Vulkan driver; OpenGL ES 3.0 or newer is required.
# Recommended values: direct-vulkan, opengl, system, angle-vulkan.
backend: direct-vulkan
# String or integer (default: display): requested frame-rate policy.
# Supported values: display, unlimited, 30, 60, 120, 144, 240. Unlimited
# selects Roblox's unmodified 240-FPS scheduler maximum.
frame_rate_limit: display
# String (default: auto): presentation synchronization: auto, on, or off.
vsync: auto
# Optional string or integer: Mocktail frame-rate override. Leave commented
# (or use -1) to let Roblox Basic Settings own the cap. Supported overrides:
# 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. auto
# presents through the lowest-latency synchronized mode the driver offers
# unless frame_rate_limit is unlimited.
# vsync: off
# vr:
# String (default: off): experimental VR through OpenXR on supported Roblox
# builds (ARM64: 2998 and 3222). off starts on the desktop; on starts in
# VR like --vr. auto starts in VR when SteamVR is already running with a connected
# headset (ALVR included), and on the desktop otherwise. auto never starts
# SteamVR. --vr and --no-vr override this for one launch. The older
# enabled: true/false still works as on/off; set one or the other.
# Standalone OpenXR test scene: mocktail-vr-probe --scene.
# mode: off
# Boolean (default: true): also show the left-eye VR view in the desktop
# window. Restart Nightcap after changing this setting. Steam Frame
# uses its Video settings Desktop Mirror control, off by default.
# desktop_mirror: true
# String (default: fxaa): smooth jagged edges in the headset. Roblox draws
# the VR eyes without anti-aliasing; fxaa filters each eye on its way to the
# headset. The Steam Frame profile uses its separate gaze AA control.
# off sends the eyes as rendered.
# anti_aliasing: fxaa
# Optional string or integer: frame-rate limit used instead of
# graphics.frame_rate_limit while VR is active. Same values: -1 (Roblox's
# own cap, usually 60), display, unlimited, or any positive integer. The
# default, unlimited, lets the OpenXR runtime pace frames at the headset's
# refresh rate.
# frame_rate_limit: unlimited
performance:
# Boolean (default: false): size Roblox scheduler/render queues from every
@@ -63,12 +97,34 @@ audio:
# String (default: default): `default` follows the host system output.
# To pin output, copy an exact SDL device name printed during startup.
output_device: default
# String (default: default): input device name, `id:<number>` printed during
# startup, or `disabled`. IDs disambiguate duplicate names but may change
# 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.
@@ -81,30 +137,51 @@ 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
window:
# Integer (default: 1280): initial window width in pixels.
# Integer (default: 1280): initial window width in logical desktop units.
width: 1280
# Integer (default: 720): initial window height in pixels.
# Integer (default: 720): initial window height in logical desktop units.
height: 720
# String (default: Roblox): window title.
title: Roblox
# Boolean (default: false): render at physical display-pixel density instead
# of the logical desktop resolution. Enable only for sharper high-DPI output.
high_dpi: false
# Optional host HTTP proxy. Uncomment both values together. The host must not
# contain a scheme such as http://.
# network:
# # String (optional, default: disabled): proxy host name or IP address.
# proxy_host: 127.0.0.1
# # Integer (optional, default: disabled): proxy TCP port from 1 to 65535.
# proxy_port: 8080
network:
# Boolean (default: false): follow the host HTTP/SOCKS5 proxy and
# automatically use its current port. Do not combine with a fixed proxy.
use_system_proxy: false
# Optional fixed HTTP proxy. Uncomment both values together. The host must
# not contain a scheme such as http://.
# String (optional, default: disabled): proxy host name or IP address.
# proxy_host: 127.0.0.1
# Integer (optional, default: disabled): proxy TCP port from 1 to 65535.
# proxy_port: 8080
# Absolute path (optional): custom PEM CA bundle. Mocktail reads this file
# directly and does not replace it during Roblox payload updates.
# ca_bundle: /home/user/.config/mocktail/cacert.pem
updates:
# Boolean (default: true): check the latest Roblox version, derive an exact
@@ -112,7 +189,9 @@ updates:
# backend, and promote only on success. An existing current payload is
# preserved when probation fails.
automatic: true
# String (default: apk-pure): direct x86_64 APK provider.
# String (default: apk-pure): direct x86_64 APK provider selection. Both
# accepted values resolve the provider chain, which currently starts with
# APKPure. Supported values: auto, apk-pure.
source: apk-pure
# Reserved for desktop update integrations. `mocktail_updater` itself never
# launches another process after changing the active payload.
+147
View File
@@ -4,6 +4,7 @@
{
"version_name": "2.721.1108",
"version_code": 2350,
"abi": "x86_64",
"elf_build_id": "50e1b0abd123350e794226062fe3a1ef360c5f0d",
"status": "legacy-researched",
"default_allowed": false,
@@ -15,6 +16,7 @@
{
"version_name": "2.725.1142",
"version_code": 2546,
"abi": "x86_64",
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
"status": "supported",
"default_allowed": true,
@@ -35,6 +37,7 @@
{
"version_name": "2.727.1199",
"version_code": 2628,
"abi": "x86_64",
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
"status": "supported",
"default_allowed": true,
@@ -42,6 +45,150 @@
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"reason": "Verified 2026-07-16 on the current host: independently signature-matched native allocator and pre-JNI state, 3479 constructors replayed without recovery, JNI_OnLoad and ASMA/V2 startup, 2988 Vulkan shaders, a real vkQueuePresentKHR, and explicit lifecycle teardown to Stopped; legacy executable patches and signal recovery remain disabled."
},
{
"version_name": "2.734.917",
"version_code": 2908,
"abi": "x86_64",
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
"status": "supported",
"default_allowed": true,
"allow_legacy_binary_patches": false,
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"user_game_settings_fullscreen_setter_rva": "0x44fee64",
"fmod_output_device_bridge": {
"vtable_rva": "0x6b7eee8",
"string_constructor_rva": "0x1cfd5ec",
"count_method_rva": "0x32736d2",
"info_method_rva": "0x3273772",
"current_method_rva": "0x3273722",
"select_method_rva": "0x32734a6"
},
"reason": "Verified 2026-08-28 on the current host: the machine-derived exact-Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries; fullscreen state synchronization and Build-ID-scoped FmodAudioDevice output interposition were re-derived for the current payload; legacy executable patches and signal recovery remain disabled."
},
{
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "arm64-v8a",
"elf_build_id": "ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
"status": "supported",
"default_allowed": true,
"allow_legacy_binary_patches": false,
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching.",
"fmod_output_device_bridge": {
"vtable_rva": "0x632aeb0",
"string_constructor_rva": "0x1d5a6b0",
"count_method_rva": "0x2f96540",
"info_method_rva": "0x2f96600",
"current_method_rva": "0x2f965a0",
"select_method_rva": "0x2f962d4",
"input_count_method_rva": "0x2f968b4",
"input_info_method_rva": "0x2f968c4",
"input_current_method_rva": "0x2f9697c",
"input_select_method_rva": "0x2f96088",
"vtable_layout_version": 1
}
},
{
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "x86_64",
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
"status": "supported",
"default_allowed": true,
"allow_legacy_binary_patches": false,
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"user_game_settings_fullscreen_setter_rva": "0x45ad8aa",
"fmod_output_device_bridge": {
"vtable_rva": "0x6c58040",
"string_constructor_rva": "0x1d32df8",
"count_method_rva": "0x32d0ee0",
"info_method_rva": "0x32d0f80",
"current_method_rva": "0x32d0f30",
"select_method_rva": "0x32d0cb4",
"input_count_method_rva": "0x32d12dc",
"input_info_method_rva": "0x32d12ea",
"input_current_method_rva": "0x32d13a6",
"input_select_method_rva": "0x32d0a56"
},
"vr_debug_device_bridge": {
"vtable_rva": "0x6c87b38",
"constructor_rva": "0x3810614",
"state_getter_rva": "0x38109f6",
"eye_getter_rva": "0x38109e6",
"eye_initializer_rva": "0x3810a18",
"emulator_flag_storage_rva": "0x7369920",
"device_create_framebuffer_vtable_offset": "0x180"
},
"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. Experimental vr_debug_device_bridge metadata (DebugDeviceVR constructor/state/eye/initializer RVAs) is machine-validated at activation and gated behind --vr; native stereo output is not yet verified end-to-end."
},
{
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"allow_legacy_binary_patches": false,
"default_allowed": true,
"elf_build_id": "5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
"fmod_output_device_bridge": {
"count_method_rva": "0x320c676",
"current_method_rva": "0x320c6c6",
"info_method_rva": "0x320c716",
"select_method_rva": "0x320bf78",
"string_constructor_rva": "0x1d7a038",
"vtable_layout_version": 2,
"vtable_rva": "0x6cd3ce0"
},
"reason": "Exact-Build-ID host ABI promoted from the validated probation profile; native stereo, VR state/pointer/haptic contracts and FMOD output-device layout v2 were re-derived for 3092. Experimental: runs when selected explicitly (--roblox-lib) but is never an update target, so non-VR users stay on the default payload.",
"status": "experimental",
"user_game_settings_fullscreen_setter_rva": "0x45c5f42",
"version_code": 3092,
"version_name": "2.738.1397",
"vr_debug_device_bridge": {
"vtable_rva": "0x6d08108",
"constructor_rva": "0x37f9fe4",
"state_getter_rva": "0x37fa3c6",
"eye_getter_rva": "0x37fa3b6",
"eye_initializer_rva": "0x37fa3e8",
"emulator_flag_storage_rva": "0x73f4690",
"device_create_framebuffer_vtable_offset": "0x188"
}
},
{
"version_name": "2.742.1059",
"version_code": 3222,
"abi": "arm64-v8a",
"elf_build_id": "25aa7fd31008a744c3c99f717e7d16d81d371d12",
"status": "supported",
"default_allowed": true,
"allow_legacy_binary_patches": false,
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"reason": "Verified on Steam Frame 2026-10-15: signed ARM64 3222 payload, exact allocator/bootstrap and reordered constructor roles, VR state/pointer/reflection/framebuffer contracts and layout-2 FMOD bridge; native startup, two distinct eye bindings at 2592x2592, real OpenXR projection and controlled experience transitions. Worn-headset quality and remote voice audibility remain unconfirmed.",
"fmod_output_device_bridge": {
"vtable_rva": "0x6569770",
"string_constructor_rva": "0x1e47180",
"count_method_rva": "0x2c35714",
"info_method_rva": "0x2c357d4",
"current_method_rva": "0x2c35774",
"select_method_rva": "0x2c34f18",
"input_count_method_rva": "0x2c35d58",
"input_info_method_rva": "0x2c35d68",
"input_current_method_rva": "0x2c361a0",
"input_select_method_rva": "0x2c34c80",
"vtable_layout_version": 2
},
"vr_debug_device_bridge": {
"vtable_rva": "0x67b6688",
"constructor_rva": "0x4e5558c",
"state_getter_rva": "0x4e55964",
"eye_getter_rva": "0x4e55958",
"eye_initializer_rva": "0x4e55974",
"emulator_flag_storage_rva": "0x6c0d1b0",
"device_create_framebuffer_vtable_offset": "0x188"
}
}
]
}
+35 -35
View File
@@ -1,64 +1,64 @@
{
"schema_version": 1,
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
"payload_sha256": "3e9c26c81186f93458ff65d8a8bc240c220974db02b8388b91340b77b336997d",
"payload_id": "2628-1686400865ae0e408cd7bd67de7a439625c6fd13",
"payload_path": "payloads/2628-1686400865ae0e408cd7bd67de7a439625c6fd13",
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
"payload_sha256": "4343a6a900d1fca27ff8e10b9d9c86bee40a5d0ba547cb29903b7769f4f42a9d",
"payload_id": "2998-ade08266c67aee88ec9c1d00902150e1684dad3a",
"payload_path": "payloads/2998-ade08266c67aee88ec9c1d00902150e1684dad3a",
"reference": {
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
"payload_sha256": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af"
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
"payload_sha256": "72c3b1a5c151b5ac0bfc5394ce3a91775884fbd777589ccab8ac1c4f9885f739"
},
"profile": {
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
"bridge_entries": [
{
"rva": "0x1c39ea2",
"rva": "0x1d2c1e2",
"kind": "allocate",
"label": "small-allocate"
},
{
"rva": "0x1fe4c84",
"rva": "0x1d2f43a",
"kind": "usable_size",
"label": "usable-size"
},
{
"rva": "0x203daae",
"rva": "0x21e68bf",
"kind": "reallocate",
"label": "reallocate"
},
{
"rva": "0x1c39c3a",
"rva": "0x1d2bf82",
"kind": "allocate",
"label": "allocate"
},
{
"rva": "0x1c8dbf8",
"rva": "0x1d47a63",
"kind": "aligned_allocate",
"label": "aligned-allocate-direct"
},
{
"rva": "0x1c3eb72",
"rva": "0x1d2f9d9",
"kind": "free",
"label": "free"
}
],
"data_seeds": {
"allocator_object_slot": "0x7030ae0",
"empty_string_slot": "0x703a7e8",
"jni_singleton_slot": "0x72a16d0",
"allocator_object_slot": "0x7652e70",
"empty_string_slot": "0x775fd28",
"jni_singleton_slot": "0x7bb4340",
"jni_singleton_bytes": 1024,
"arena_initializer": "0x1c39f1d",
"allocator_thread_initializer": "0x2040817",
"arena_guard_slot": "0x71e92f0",
"arena_table_slot": "0x71e9380",
"arena_initializer": "0x1d2c5f8",
"allocator_thread_initializer": "0x1db7b57",
"arena_guard_slot": "0x7af0430",
"arena_table_slot": "0x7af04c0",
"arena_table_slot_count": 4194304
},
"native_allocator": {
"allocate": "0x1c39c3a",
"deallocate": "0x1c3eb72"
"allocate": "0x1d2bf82",
"deallocate": "0x1d2f9d9"
},
"init_array_offset": "0x6c2c9b0",
"init_array_count": 3481,
"init_array_offset": "0x7057818",
"init_array_count": 3570,
"constructor_run_ranges": [
{
"begin": 2,
@@ -66,32 +66,32 @@
},
{
"begin": 5,
"end_exclusive": 3481
"end_exclusive": 3570
}
],
"native_mimalloc_constructor_run_ranges": [
{
"begin": 2,
"end_exclusive": 3481
"end_exclusive": 3570
}
],
"native_mimalloc_thread_initializer_after_constructor": 2,
"native_pre_jni_bootstrap": {
"registry_initializer": "0x1fe72e5",
"registry_slot": "0x71ed428"
"registry_initializer": "0x21e8e09",
"registry_slot": "0x7af4598"
},
"default_allocator_strategy": "native_mimalloc"
},
"derivation_anchors": {
"signature_version": 1,
"allocator_object_initializer_rva": "0x1fbf1de",
"empty_string_initializer_rva": "0x1fc82b0",
"jni_singleton_initializer_rva": "0x1c39a34",
"allocator_object_initializer_rva": "0x21bde7d",
"empty_string_initializer_rva": "0x21c9a86",
"jni_singleton_initializer_rva": "0x1d2aec0",
"constructor_rvas": {
"2": "0x1c3c764",
"3": "0x1c38c00",
"4": "0x1c39884",
"5": "0x1c39a34"
"2": "0x1d2f731",
"3": "0x1d2af84",
"4": "0x1d2bc08",
"5": "0x1d2aec0"
}
}
}
+12 -11
View File
@@ -1,23 +1,24 @@
{
"schema_version": 1,
"package": "com.roblox.client",
"version_name": "2.725.1142",
"version_code": 2546,
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "x86_64",
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
"sha256": {
"libroblox": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af",
"base_apk": "eba476fec3020eaceb28436c1d160eb35d6689d8ffb060e09678535584babc7a",
"x86_64_split_apk": "5c4193c9700481079cee958b38ba9cbb97768b0a130b7f704ed084753f01ce2b"
"libroblox": "4343a6a900d1fca27ff8e10b9d9c86bee40a5d0ba547cb29903b7769f4f42a9d",
"base_apk": "a4a71ec39df6619a56ab7adce0959d1d60354835485b53806f4026914287ea86",
"x86_64_split_apk": "a4a71ec39df6619a56ab7adce0959d1d60354835485b53806f4026914287ea86"
},
"signing_certificates_sha256": [
"2bebd189e8d3106401347056c93d045b61e20e22d0c3cbed85474aeb00a3d12a",
"44932ea35a17a267372d71b54d1a0cb3da0dca5113e94406ae2fe18090ba1477"
],
"assets": {
"file_count": 1837
"file_count": 1836,
"sha256_tree": "caee48e11c194bc11852493f53ff67d5e2bb233172f12b87ea5fca97c0c10840"
},
"source": "sober-update-service",
"imported_at": "2026-07-09T19:12:54Z",
"compatibility_status": "supported"
"source": "apk-pure-native",
"imported_at": "2026-08-31T16:30:19Z",
"compatibility_status": "supported",
"apk_layout": "monolithic"
}
+20
View File
@@ -0,0 +1,20 @@
version: 1
device: pc-windows-11
graphics:
backend: direct-vulkan
vsync: off
vr:
enabled: true
desktop_mirror: false
frame_rate_limit: unlimited
audio:
output_device: default
input_device: default
updates:
automatic: false
launch_after_update: false
source: auto
+1 -1
View File
@@ -32,7 +32,7 @@
"mode": "vendored-patched-snapshot",
"url": "https://github.com/minecraft-linux/mcpelauncher-linker.git",
"upstream_commit": "1ac3ea6c1cf4f73a84d73f0bff510fba94a7d0f2",
"vendor_tree": "fca44d85e3773c135ccfc603b14abdf6aeb8efd1",
"vendor_tree": "0637221e48f382ec8d0bc701385f4b10bcd07a65",
"patch": "third_party/patches/mcpelauncher-linker.patch",
"nested_sources": {
"android_bionic": {
+203
View File
@@ -0,0 +1,203 @@
# Building and source layout
The [installation guide](INSTALL.md) covers a complete source build on Steam
Frame. The container recipe uses Fedora 44 ARM64 with CMake, Ninja, SDL3,
OpenXR, Vulkan, WebKitGTK and Nightcap's other native dependencies.
The Frame build enables OpenXR and the ARM64 bridge, disables runtime
diagnostics, and uses native rendering. Fedora's `glslang` and `spirv-tools`
packages are included for shader generation and validation. The ARM64 workflow
builds the same container recipe and checks the package before export.
## Upstream source
This branch includes Nightcap's `vr` additions through
[`adccf4e4841275bce85d0330013367e2c124bd10`](https://github.com/CoderDayton/nightcap/commit/adccf4e4841275bce85d0330013367e2c124bd10),
including all changes from its `main` branch at that revision. The update adds
emoji fallback fonts for native text fields, decoded/compressed emulated-texture
byte counters, menu-only native Luau compilation, and queued game switching.
Menu compilation is enabled only after validating the native flag on ARM64;
explicit client settings remain in control.
Frame already uses the same reference SMAA revision as this update. Its existing
gaze AA path is retained without adding a second full-eye filter. Frame's native
2160 × 2160 rendering, 72 Hz profile, simple Video controls, controllers, audio
and foveation integration remain the platform defaults.
## Build scripts
`build-frame-nightcap.sh` configures a Release build at `/work/build-frame-arm64`
and installs to `/opt/nightcap-vr-arm64`. VR is enabled and runtime diagnostics
are disabled. It also builds the Steam application identity helper.
For incremental development in an existing Frame image, with Nightcap closed:
```bash
podman run --arch arm64 --name nightcap-frame-development \
--cpus 3 --memory 8g --security-opt label=disable \
-v "$PWD:/work" --entrypoint /bin/bash \
localhost/nightcap-vr-arm64:frame-foveated -lc \
'bash /work/build-frame-nightcap.sh && bash /work/test-arm64-bridge.sh'
```
The test script runs focused ARM64 bridge, pose, controller, lifecycle, JNI,
audio and host ABI tests, including the native assembly return-value fixture.
It also builds the GPU test tools. Build and test commands share one CMake tree;
run them sequentially.
## Main components
| Path | Purpose |
| --- | --- |
| `src/vr/` | OpenXR lifecycle, gaze, stereo eye transport, controllers, AA and foveation maps |
| `src/vr/roblox_vr_arm64_build_contracts.h` | Selects exact ARM64 client contracts |
| `src/vr/roblox_vr_arm64_contract*.h` | Client-specific ARM instruction and object layout contracts |
| `src/audio/` | Audio playback and microphone recording bridges |
| `src/compat/host_abi_profile.cc` | Client startup, allocator and native host ABI profiles |
| `src/graphics/vulkan_foveation_adapter.inc` | Scene rendering integration for density maps |
| `tools/steam_frame/` | Frame launcher, Steam shortcut and Video settings integration |
| `tools/frame_video_settings/` | OpenVR settings, refresh-rate and timing helpers |
| `config/roblox_compatibility.json` | Supported client identities and compatibility metadata |
| `config/steam-frame.example.yaml` | Initial Frame runtime settings |
## Client compatibility
The ARM64 bridge supports these client identities:
| Version | Build | ELF build ID |
| --- | --- | --- |
| 2.742.1059 | 3222 | `25aa7fd31008a744c3c99f717e7d16d81d371d12` |
| 2.736.1408 | 2998 | `ed2b035768b1513561d0e6ea7f9bbec35b0e60e5` |
New clients can change native methods, virtual tables, object fields and startup
ordering. Add matching ARM64 contracts and host/audio profiles before enabling a
new client. Keep the existing signature and build identity checks in place.
The timing helper in `tools/frame_video_settings/frame_timings.cc` reports the
scene process, dashboard visibility, headset activity and compositor throttling
alongside frame times. Compare the same view in the same experience while the
headset is displaying projection frames. Standby or desktop-only frames do not
measure active VR throughput.
Shader sources and their generated SPIR-V header are included. Regenerating the
AA shaders with `tools/compile_gaze_aa.py` additionally needs `glslangValidator`.
The SMAA sources retain their upstream license in `src/vr/smaa/`.
## Native rendering
The shipping Frame profile uses native rendering with gaze anti-aliasing.
MSAA and upscaling controls are absent from the Frame Video settings. The build
sets `MOCKTAIL_ENABLE_FSR3=OFF`, so the FidelityFX SDK is not linked into the
Frame runtime. Reference upscaler sources and their licenses remain in the tree.
To regenerate the gaze AA shaders after changing them:
```sh
python3 tools/compile_gaze_aa.py .
```
This requires `glslangValidator`. The SMAA sources retain their upstream license
in `src/vr/smaa/`.
## Foveation tile calibration
Density-map texels and GMEM render tiles have different sizes. The gaze guard
must protect every render tile touching the full-detail disk. The narrow entry
in `include/mocktail/vr/foveation_tile_bounds.h` applies only to this identity:
- Turnip driver ID 18, vendor `0x5143`, device `0x43051401`, version `109060195`.
- Driver name `turnip Mesa driver`, info `Mesa 26.3.0-devel (git-abc426bb62)`.
- Driver UUID `586b9fa4514cb88b3c11a968ef4d62e4`; measured ELF build ID
`b7e5995d0b202f06e81cd5a0f809ec1b744a0cb8`.
- A 2160 × 2160 framebuffer, one layer/subpass, single-sample
`R8G8B8A8_UNORM` color and `D32_SFLOAT` depth, plus the density attachment.
Offsets, resolves, extra attachments, multiview and feedback/input attachments
retain the conservative guard.
On that binary, `vkCmdBeginRenderPass2` at RVA `0xb12180` calls the layout
selector at `0xad67c0`. The selector initializes its layout from the FDM flag
at render-pass byte 145; subsequent checks can only select `AVOID_CCU` again.
The divisor routine at `0xa9e300` returns 1 for FDM. CPU-only creation of 72
render-pass/imageless-framebuffer combinations, covering FDM on/off and every
standard color/depth load/store combination, produced identical budgets:
356352 pixels for `FULL`, 350208 for `AVOID_CCU`. Their base tiles are
480 × 736 and 768 × 448 respectively. FDM therefore selects 768 × 448
independently of shaders and load/store operations for this tuple.
The GPU qualification compared a high-frequency full-density reference against
the production map helper with its 68 × 68 map and 768 × 448 guard. Sweep both
gaze axes through `{-1, -.75, -.4, 0, .4, .75, 1}`, radii
`{.08, .15, .22, .35}`, and all three active profiles. All 588 cases preserved
every pixel inside the gaze disk exactly, with 588 valid fragment-invocation
queries and no validation errors. Mean fragment invocations were 51.46% of the
reference in this fixture; this measures shading work, not game FPS. Requalify
both the driver invariant and this image/counter sweep before adding identities
or pass tuples. Unknown identities retain the conservative policy.
The CPU identity and coordinate contracts can be rerun with:
```sh
cmake --build build-frame-arm64 --target foveation_tile_bounds_test framebuffer_density_map_test
ctest --test-dir build-frame-arm64 -R 'FoveationTileBounds|FramebufferDensityMap' --output-on-failure
```
## Frame-time stability
The Android `mallinfo` bridge shares a real host allocator snapshot for up to
500 ms. glibc scans allocator free lists under arena locks; several consecutive
queries therefore reuse a recent coherent sample. A scan runs outside the cache
mutex, and concurrent readers can keep using the previous completed sample.
Allocation, deallocation and the process-memory limit checks retain their normal
behavior.
Gaze density generation reuses per-eye pixel and half-float buffers across
frames. The density values and gaze protection are unchanged. At a 1:1 render and output size, the native path avoids unnecessary
reconstruction, temporal resource tracking and upscaler shader dispatches.
Gaze anti-aliasing runs on the composed eye image.
The Vulkan eye copy is submitted on the OpenXR binding queue before releasing
its swapchain images, so SteamVR can enqueue composition immediately. When the
guest device requested exactly one unprotected queue, completion is deferred
until the next use of its eye-copy and anti-aliasing resources. GPU queue ordering keeps subsequent guest
commands behind the copy. The upstream submission ring tracks command buffers, fences and source/destination
images. Presentation dependencies are consumed on the GPU and replaced with a
per-swapchain-image semaphore, so the CPU does not wait for the current render.
AA resources wait only for previous eye-copy users;
current-frame present dependencies are excluded from that resource wait.
Captured source-image handles are drained before
image destruction; queue exposure, swapchain replacement and teardown also
drain pending work before releasing resources. `xrWaitFrame` remains on the
original render thread. Additional queues, readbacks and the virtual keyboard
retain completed-copy synchronization. `MOCKTAIL_VR_EARLY_SUBMIT=0` restores
the original submission order, while `MOCKTAIL_VR_DEFER_COPY_WAIT=0` keeps the
early submission but completes its fence before waiting for the next frame.
Density-map generation skips the distance/ramp calculation inside the protected
full-detail disk and beyond the peripheral plateau. Transition cells retain the
same distance calculation and spatial pattern. This reduces CPU work without
changing gaze protection or density values.
The Frame launcher enables a render-thread CPU preference after projection
submission starts. It reads Linux CPU capacity values only for the thread's
existing allowed CPUs and selects capacity peers within 80% of their maximum.
Incomplete capacity information or fewer than two preferred CPUs retains the
original allocation. This keeps render work on the application's performance
cores while respecting SteamVR's CPU reservation. Other engine threads retain
their normal allocation.
## Upstream integration
The imported Nightcap VR revision is recorded in [the credits](../THIRD_PARTY.md).
The Frame launcher disables the generic full-eye FXAA pass so Steam Video's
Anti Aliasing setting remains the single AA control. The upstream JNI haptic
protocol runs natively on ARM64. Its optional x86 SetMotor instruction scanner
is gated to x86; ARM64 build IDs, instruction checks and native return-value
bridges retain their own contracts.
Steam Video settings are loaded once at startup and then refreshed on a worker.
The rendering callbacks copy a completed snapshot; they never read the settings
file or parse JSON. Unchanged content reuses its parsed values. A failed refresh
keeps the last completed snapshot, and shutdown wakes the polling worker.
The launcher gives a configured per-app headset refresh rate precedence over
the desktop rate observed before the app gains focus, and derives Roblox's
startup FPS cap from that selected profile.
+168
View File
@@ -0,0 +1,168 @@
# Install on Steam Frame
## Requirements
- Steam Frame running its ARM64 Linux desktop, with Steam and SteamVR installed.
- Native SteamVR at `/opt/steamvr`, including `steamxr_linuxarm64.json`.
- The Frame's Turnip/Freedreno Vulkan driver under `/usr/lib` and its Vulkan ICD
under `/usr/share/vulkan/icd.d`.
- Rootless Podman, Git, Python 3 and a C++17 compiler on the Frame.
- `zstd` for importing the compressed ARM64 release archive.
- A Linux filesystem for the checkout, internet access and space for the source,
Fedora build image and a separate Roblox client download.
The runtime uses Fedora 44 ARM64 in a rootless container. Keep the Frame's
own graphics drivers: the launcher mounts them into the container. Building
from source also needs a case-sensitive Linux checkout because some Android
headers differ only by letter case. The source and ARM64 build are public.
## 1. Get the source
```bash
git clone --branch steam-frame --recurse-submodules \
https://github.com/InstanceBrick/nightcap.git \
"$HOME/.local/share/nightcap-steam-frame-src"
cd "$HOME/.local/share/nightcap-steam-frame-src"
```
## 2. Install the ARM64 image
Download `nightcap-steam-frame-arm64.oci.tar.zst` and its `.sha256` file from
[Releases](https://github.com/InstanceBrick/nightcap/releases). From the
directory containing both files:
```bash
set -euo pipefail
sha256sum -c nightcap-steam-frame-arm64.oci.tar.zst.sha256
zstd -dc nightcap-steam-frame-arm64.oci.tar.zst | podman load
```
The archive imports as `localhost/nightcap-vr-arm64:frame-foveated`. It contains
the ARM64 Nightcap runtime and its Fedora dependencies, with no Roblox client,
personal data, SteamVR installation or proprietary graphics drivers.
To build the same image from source instead:
```bash
podman build --arch arm64 --jobs 1 \
-f packaging/steam-frame/Containerfile \
-t localhost/nightcap-vr-arm64:frame-foveated .
```
This builds Nightcap and its helpers from source. It does not download or bundle
Roblox. The image tag is also the one used by the Frame launcher.
## 3. Install the launcher and refresh-rate helper
```bash
python3 tools/steam_frame/install_launcher.py
mkdir -p "$HOME/.local/share/nightcap-vr-arm64/data/tools"
c++ -std=c++17 -O2 tools/frame_video_settings/refresh_rate.cc -ldl \
-o "$HOME/.local/share/nightcap-vr-arm64/data/tools/refresh_rate"
```
The application menu entry is **Nightcap Roblox VR**.
## 4. Download the compatible Roblox client
Close Nightcap first. Prepare its data directory and copy the initial settings
only if no configuration exists yet:
```bash
frame_data="$HOME/.local/share/nightcap-vr-arm64/data"
mkdir -p "$frame_data/.config/mocktail"
if [ ! -f "$frame_data/.config/mocktail/config.yaml" ]; then
cp config/steam-frame.example.yaml "$frame_data/.config/mocktail/config.yaml"
fi
podman run --rm --arch arm64 --userns=keep-id \
--security-opt label=disable \
-v "$frame_data:/data:rw" \
-e HOME=/data -e XDG_CONFIG_HOME=/data/.config \
-e XDG_DATA_HOME=/data/.local/share -e XDG_CACHE_HOME=/data/.cache \
--entrypoint /opt/nightcap-vr-arm64/lib64/mocktail/mocktail_updater \
localhost/nightcap-vr-arm64:frame-foveated \
update --no-latest-check --skip-canary
```
The updater downloads the newest ARM64 client supported by the included catalog.
The command retains APK signature, architecture, build identity and payload
integrity checks. `--skip-canary` skips the separate desktop graphics test; VR
starts through the Frame launcher in the next step. Client files stay in your
local data directory.
## 5. Launch in VR and add to Steam
Start SteamVR and connect the headset, then open **Nightcap Roblox VR** from the
application menu. Sign in through Roblox's normal interface.
To add the Steam shortcut automatically, enable Steam's local browser debugging
endpoint at `127.0.0.1:8080`, keep Steam running and use:
```bash
python3 tools/steam_frame/register_steam_shortcut.py
```
The script creates or reuses the launcher shortcut, sets its Nightcap icon and
marks it as a VR application. It saves the generated identity locally. If the
endpoint is unavailable, use Steam's **Add a Non-Steam Game** dialog and select
`~/.local/bin/nightcap-vr-arm64`, then mark the shortcut as a VR application.
## 6. Add the SteamVR Video controls
The settings integration supports a specific SteamVR dashboard revision. It
checks the installed dashboard before applying changes and leaves other
revisions untouched. Valve dashboard files are read from your local installation.
With SteamVR closed and the original dashboard files visible:
```bash
mkdir -p frame-ui-local/originals frame-ui-local/generated
cp /opt/steamvr/resources/webinterface/dashboard/chunk~8012d0c89.js \
frame-ui-local/originals/steamvr-dashboard.original.js
cp /opt/steamvr/resources/webinterface/dashboard/systemui.html \
frame-ui-local/originals/steamvr-systemui.original.html
cp /opt/steamvr/resources/webinterface/dashboard/settings_desktop.html \
frame-ui-local/originals/steamvr-settings_desktop.original.html
python3 tools/steam_frame/prepare_video_ui.py \
frame-ui-local/originals frame-ui-local/generated
sudo python3 tools/steam_frame/install_video_ui.py frame-ui-local/generated
```
Start SteamVR again. The settings are described in [Video settings](SETTINGS.md).
The service mounts three generated files read-only; the original SteamVR files
are preserved. If upgrading an existing installation, reuse your saved original
files instead of copying the already patched dashboard.
## Update Nightcap
Close Nightcap before replacing its image or launcher. Pull the `steam-frame`
branch, then load the new ARM64 release archive as in step 2 and rerun the
launcher installer. Existing Roblox downloads, sign-in state and settings stay
in the separate data directory.
For source builds, pull this branch and rebuild under a temporary image tag:
```bash
git pull --ff-only
git submodule update --init --recursive
podman build --arch arm64 --jobs 1 \
-f packaging/steam-frame/Containerfile \
-t localhost/nightcap-vr-arm64:next .
podman tag localhost/nightcap-vr-arm64:frame-foveated \
localhost/nightcap-vr-arm64:previous
podman tag localhost/nightcap-vr-arm64:next \
localhost/nightcap-vr-arm64:frame-foveated
python3 tools/steam_frame/install_launcher.py
```
Existing Roblox downloads, sign-in state and settings remain in the data
directory. To install a newly supported Roblox client, repeat step 4 after
updating Nightcap. Automatic client updates are off in the supplied Frame
configuration, since the ARM64 VR bridge needs matching client interfaces.
To restore the previous Nightcap image with the app closed:
```bash
podman tag localhost/nightcap-vr-arm64:previous \
localhost/nightcap-vr-arm64:frame-foveated
```
+124
View File
@@ -0,0 +1,124 @@
# Settings
## Video
Open the game's SteamVR **Video** settings after installing the settings
integration.
The standard profile is **native 2160 × 2160 per eye**, **72 Hz**, **SMAA High**
and **Performance** foveated rendering. The application targets 72 FPS at 72 Hz;
actual frame rate depends on the experience and Roblox's graphics settings.
The gaze radius remains adjustable, and existing saved choices are preserved.
| Setting | Effect |
| --- | --- |
| Foveated Rendering | Off uses full-density shading throughout. Quality, Balanced and Performance progressively reduce peripheral shading work. Each mode keeps the same full-detail gaze region. |
| Gaze sharp radius | Sets the minimum full-detail region around your gaze, from 8% to 35%. Larger values preserve more of the view at full density and cost more GPU time. Hardware tile boundaries can enlarge this region. |
| Anti Aliasing | Off, FXAA, SMAA Low or SMAA High. Filtering runs around your gaze on the composed eye image, including interface edges. SMAA High is the default. |
| Desktop Mirror | Off by default on Frame. On displays the VR image in the desktop window as well as the headset. Relaunch Nightcap after changing it. |
| Performance Overlay | Shows actual game FPS, headset Hz, average frame time, SteamVR GPU time and per-eye resolution at the bottom center of the headset view. It hides while the Steam menu is open and does not capture controller input. |
| Resolution Per Eye | Sets the headset eye-buffer size. Nightcap's recommended default is 2160 × 2160 per eye; Auto and Reset to Defaults use this baseline. Explicit custom values stay selected. Changes apply after the slider settles, with a brief pause while eye buffers are rebuilt at a safe frame boundary. |
| Display Refresh Rate | The standard profile uses 72 Hz. The launcher follows the selected per-game headset rate and any explicit Steam frame limit. Restart Nightcap after changing the rate or frame limit so Roblox's internal cap matches it. |
Enable **Performance Overlay**, then close the Steam menu. **APP FPS** counts
new Roblox projections over a rolling second. **HMD Hz** is the headset refresh
rate. **FRAME** is average time per submitted game frame, **TARGET** is the
configured application frame rate, and **BUDGET** is the time available per
application frame: 13.89 ms for 72 FPS at 72 Hz. Repeated or reprojected
compositor frames do not count as new game frames.
**GPU** averages SteamVR timings for new application frames, excluding
compositor-only reprojections. It can include scheduling gaps. The overlay
reports standby or missing frames instead of retaining a stale FPS. Its setting
persists between launches and can be turned off at any time.
Foveated rendering reduces fragment-shader work in the 3D scene before the
image is composed. The final composition stays at native resolution, keeping
screen-space text and interface elements from being coarsened a second time.
Your gaze region retains the detail available at the selected eye resolution;
the periphery uses progressively lower shading density.
Foveation, gaze radius and anti-aliasing controls update while the application
is running. Higher eye resolutions improve fine detail and increase GPU and
memory cost. For more headroom, keep Performance foveation, reduce the gaze
radius if needed, or lower Roblox's own graphics quality.
The launcher uses the selected Steam Video resolution without an extra
multiplier. To add supersampling, set `NIGHTCAP_VR_RENDER_SCALE` when launching
Nightcap; for example, `NIGHTCAP_VR_RENDER_SCALE=1.25`. The default is `1.0`.
Larger values increase GPU work.
The launcher reads the selected headset refresh rate and SteamVR's frame limit
on every launch. At 72 Hz with no frame limit, Roblox's internal scheduler is
capped at 72 FPS. Half-rate rendering is not forced automatically.
On Frame, the render thread prefers higher-capacity cores within the CPU
allocation supplied by SteamVR. To use the default thread placement, launch with
`NIGHTCAP_VR_PREFER_PERFORMANCE_CORES=0`.
## Controllers and typing
The native VR bridge supplies controller tracking, buttons, axes and haptics.
Roblox experiences choose how to use those inputs. The in-headset keyboard opens
when a text field receives focus; point at a key and pull the trigger to type.
Steam Controller desktop navigation can also be configured through the Steam
shortcut's controller layout.
## Microphone
Set the Frame's headset microphone as the default input in the desktop audio
settings and check that it is not muted. Roblox voice chat uses its normal
account eligibility, microphone permission and in-experience controls.
Nightcap's native recording bridge captures the selected input when Roblox
requests recording. To choose a different input, close Nightcap and edit
`~/.local/share/nightcap-vr-arm64/data/.config/mocktail/config.yaml`:
```yaml
audio:
input_device: default
```
An exact SDL device name can replace `default`. Input device names appear in the
launch log.
## Desktop mirror
Open SteamVR's per-game **Video** settings and set **Desktop Mirror** to **On**
to show the VR view in a desktop window. Set it to **Off** to keep the Frame's
single headset application without the extra desktop view. Relaunch Nightcap
after changing this setting. The Frame default is **Off**.
The Frame launcher reads the saved SteamVR mirror choice and coordinates both
the mirror renderer and desktop-window visibility. This setting takes precedence
over the YAML value when using the Frame launcher. Generic Nightcap launches
can configure the mirror beside the VR option:
```yaml
vr:
enabled: true
desktop_mirror: true
```
`MOCKTAIL_VR_DESKTOP_MIRROR=1` or `0` overrides the YAML setting for generic
launches. VR and desktop mirroring are separate options; enabling VR does not
require the desktop mirror.
Roblox's desktop viewport stays at 1920 × 1080 during VR for stable in-game
menu size, independently of headset eye resolution. On Frame, desktop window geometry
is not restored or saved in VR, including when the mirror is enabled.
## Files and troubleshooting
| Item | Location |
| --- | --- |
| Launcher | `~/.local/bin/nightcap-vr-arm64` |
| Launch log | `~/.local/share/nightcap-vr-arm64/last-launch.log` |
| Configuration | `~/.local/share/nightcap-vr-arm64/data/.config/mocktail/config.yaml` |
| Local client store | `~/.local/share/nightcap-vr-arm64/data/.local/share/mocktail` |
| Steam shortcut identity | `~/.local/share/nightcap-vr-arm64/steam-shortcut.json` |
If launching fails, check the launch log, make sure SteamVR is running and confirm
that the `localhost/nightcap-vr-arm64:frame-foveated` image exists. If the game
reports an unsupported client, update Nightcap before replacing the client.
Keep account data and client downloads outside source control.
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1787135253,
"narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1785031560,
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+73
View File
@@ -0,0 +1,73 @@
{
description = "Experimental Roblox compatibility runtime for Linux and FreeBSD";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
];
systems = [
"x86_64-linux"
# "aarch64-linux"
# "aarch64-darwin"
# "x86_64-darwin"
];
perSystem =
{
pkgs,
...
}:
let
package = pkgs.callPackage ./packaging/nix/package.nix { };
in
{
packages.default = package;
devShells.default = pkgs.mkShell {
inputsFrom = [ package ];
# nixpkgs installs the header we need under include/libutf8proc
NIX_CFLAGS_COMPILE = "-I${pkgs.libutf8proc}/include/libutf8proc";
nativeBuildInputs = with pkgs; [
cmake
git
lld
ninja
pkg-config
vulkan-headers
llvmPackages_22.clang-tools
llvmPackages_22.libclang
# debugging
gdb
];
desktopItems = [
(pkgs.makeDesktopItem {
name = "Mocktail";
exec = "mocktail";
icon = "mocktail";
desktopName = "Mocktail";
comment = "Experimental Roblox compatibility runtime for Linux and FreeBSD";
categories = [ "Game" ];
})
];
shellHook = ''
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.lib.makeLibraryPath package.buildInputs}"
# nix packages graphics drivers differently, and mocktail will still work if it cannot guarantee symbols
export MOCKTAIL_REQUIRE_REAL_GRAPHICS=0
'';
};
};
flake = { };
};
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "compat/bionic_host_libc_runtime.h"
#include <cstdint>
#include <mutex>
namespace mocktail::compat {
// Heap accounting is a sampled statistic. A glibc scan walks free lists while
// holding arena locks, so back-to-back Android queries share a recent sample.
// The scan runs outside this mutex; concurrent callers can use the last complete
// sample without waiting for another allocator walk.
class AllocatorSnapshotCache {
public:
static constexpr std::uint64_t kRefreshNs=500'000'000ULL;
template<class Reader>
BionicMallinfoSnapshot Read(std::uint64_t now,Reader reader) noexcept {
std::unique_lock<std::mutex> lock(mutex_);
if(refreshing_ || (initialized_ && now<refresh_after_))return snapshot_;
refreshing_=true;
lock.unlock();
const auto fresh=reader();
lock.lock();
snapshot_=fresh;
initialized_=true;
refresh_after_=now+kRefreshNs;
refreshing_=false;
return snapshot_;
}
private:
std::mutex mutex_;
BionicMallinfoSnapshot snapshot_{};
bool initialized_=false,refreshing_=false;
std::uint64_t refresh_after_=0;
};
}
+42
View File
@@ -33,6 +33,23 @@ void SetLegacyBionicDiagnosticsEnabled(bool enabled);
// guest layout explicit instead of exposing the incompatible host libc type.
using MocktailBionicMutexAttr = int64_t;
// Bionic LP64 pthread_attr_t: 56 bytes with explicit field offsets. The host
// glibc layout is opaque and wider on aarch64, so guest buffers must be read
// and written through this struct only.
struct MocktailBionicPthreadAttr {
std::uint32_t flags;
void* stack_base;
std::size_t stack_size;
std::size_t guard_size;
std::int32_t sched_policy;
std::int32_t sched_priority;
char reserved[16];
};
static_assert(sizeof(MocktailBionicPthreadAttr) == 56);
inline constexpr std::uint32_t kMocktailBionicAttrFlagDetached = 0x1;
inline constexpr std::uint32_t kMocktailBionicAttrFlagUserStack = 0x2;
extern "C" {
void mocktail_set_current_jni_env(void* env);
@@ -71,6 +88,31 @@ int mocktail_pthread_mutex_destroy(pthread_mutex_t* mutex);
int mocktail_pthread_mutex_lock(pthread_mutex_t* mutex);
int mocktail_pthread_mutex_trylock(pthread_mutex_t* mutex);
int mocktail_pthread_mutex_unlock(pthread_mutex_t* mutex);
int mocktail_pthread_once(pthread_once_t* once_control,
void (*init_routine)(void));
int mocktail_pthread_spin_init(pthread_spinlock_t* lock, int pshared);
int mocktail_pthread_spin_destroy(pthread_spinlock_t* lock);
int mocktail_pthread_spin_lock(pthread_spinlock_t* lock);
int mocktail_pthread_spin_trylock(pthread_spinlock_t* lock);
int mocktail_pthread_spin_unlock(pthread_spinlock_t* lock);
int mocktail_pthread_barrier_init(pthread_barrier_t* barrier,
const pthread_barrierattr_t* attr,
unsigned count);
int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier);
int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier);
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
size_t stack_size);
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
int detach_state);
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
const struct sched_param* parameters);
int mocktail_pthread_getattr_np(pthread_t thread,
MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
void** stack_base, size_t* stack_size);
extern void* mocktail_gameactivity_on_start_native;
extern void* mocktail_gameactivity_on_resume_native;
+19 -10
View File
@@ -11,19 +11,27 @@
namespace mocktail::compat {
// glibc's atoi is (int)strtol(), exactly like Bionic's, so the host symbol is
// used as-is there. Other libcs (musl) differ on overflow and need the shim.
#ifdef __GLIBC__
inline constexpr bool kHostAtoiMatchesBionic = true;
#else
inline constexpr bool kHostAtoiMatchesBionic = false;
#endif
// Android uses size_t fields here; glibc uses int and musl has no equivalent.
// An all-zero fallback means unknown, not zero usage.
struct BionicMallinfoSnapshot {
size_t arena = 0;
size_t ordblks = 0;
size_t smblks = 0;
size_t hblks = 0;
size_t hblkhd = 0;
size_t usmblks = 0;
size_t fsmblks = 0;
size_t uordblks = 0;
size_t fordblks = 0;
size_t keepcost = 0;
size_t arena;
size_t ordblks;
size_t smblks;
size_t hblks;
size_t hblkhd;
size_t usmblks;
size_t fsmblks;
size_t uordblks;
size_t fordblks;
size_t keepcost;
};
struct BionicLocaleState;
@@ -90,6 +98,7 @@ int mocktail_bionic_cxa_thread_atexit_impl(
mocktail::compat::BionicThreadDestructor destructor, void* argument,
void* dso_handle);
void mocktail_bionic_arc4random_buf(void* buffer, size_t size);
int mocktail_bionic_atoi(const char* text);
mocktail::compat::BionicMallinfoSnapshot mocktail_bionic_mallinfo();
int mocktail_bionic_sysinfo(struct sysinfo* info);
int mocktail_bionic_uname(struct utsname* name);
+13 -4
View File
@@ -2,6 +2,8 @@
#define MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
#include <pthread.h>
#include <sched.h>
#include <sys/types.h>
#include <cstddef>
@@ -23,10 +25,17 @@ void ConfigureBionicPthreadThreadInitializer(
} // namespace mocktail::compat
struct MocktailBionicPthreadAttr;
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine)(void*),
void* argument);
extern "C" int mocktail_bionic_pthread_create(
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
void* (*start_routine)(void*), void* argument);
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
extern "C" int mocktail_bionic_pthread_setschedparam(
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_
+3 -2
View File
@@ -3,8 +3,9 @@
#include <pthread.h>
// Bionic and glibc pthread_rwlock_t storage is not ABI-compatible. The guest
// address is an opaque key; the host object lives entirely in this runtime.
// Bionic pthread_rwlock_t is a state word in guest storage. rdlock/wrlock/
// unlock use __atomic_* and futex on that word. Zero-initialized storage
// still locks.
extern "C" {
int mocktail_bionic_pthread_rwlock_init(pthread_rwlock_t* rwlock,
+3 -2
View File
@@ -3,8 +3,9 @@
#include <semaphore.h>
// Bionic and glibc use different sem_t representations. These exports keep
// host semaphore objects out of guest storage and key them by guest address.
// Bionic sem_t is a count word in guest storage (value << 1, shared bit 0).
// Wait/post use __atomic_* and futex on that word so inlined guest futex
// waiters wake. Process-shared semaphores are rejected.
extern "C" {
int mocktail_bionic_sem_init(sem_t* semaphore, int process_shared,
+24 -6
View File
@@ -5,6 +5,7 @@
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cwchar>
@@ -17,12 +18,6 @@ namespace mocktail::compat {
constexpr size_t kBionicFileSize = 152;
constexpr size_t kBionicStandardStreamCount = 3;
FILE* TranslateBionicFile(FILE* stream) noexcept;
void* BionicFileArraySymbolAddress() noexcept;
void* BionicStdinSymbolAddress() noexcept;
void* BionicStdoutSymbolAddress() noexcept;
void* BionicStderrSymbolAddress() noexcept;
} // namespace mocktail::compat
extern "C" {
@@ -31,8 +26,31 @@ alignas(sizeof(void*)) extern unsigned char
__sF[mocktail::compat::kBionicStandardStreamCount]
[mocktail::compat::kBionicFileSize];
} // extern "C"
namespace mocktail::compat {
inline FILE* TranslateBionicFile(FILE* stream) noexcept {
const uintptr_t address = reinterpret_cast<uintptr_t>(stream);
if (__builtin_expect(address == reinterpret_cast<uintptr_t>(&__sF[0]), 0)) {
return stdin;
}
if (__builtin_expect(address == reinterpret_cast<uintptr_t>(&__sF[1]), 0)) {
return stdout;
}
if (__builtin_expect(address == reinterpret_cast<uintptr_t>(&__sF[2]), 0)) {
return stderr;
}
return stream;
}
void* BionicFileArraySymbolAddress() noexcept;
void* BionicStdinSymbolAddress() noexcept;
void* BionicStdoutSymbolAddress() noexcept;
void* BionicStderrSymbolAddress() noexcept;
} // namespace mocktail::compat
// FILE values returned by host fopen/fdopen pass through unchanged. Pointers
// into Bionic's ABI-sized __sF array are translated to the corresponding host
// standard stream at this single boundary.
+44
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,45 @@ 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};
}
std::array<std::size_t, 6> capture_vtable_indexes() const {
return vtable_layout_version == 2
? std::array<std::size_t, 6>{4, 13, 14, 15, 16, 17}
: std::array<std::size_t, 6>{4, 11, 12, 13, 14, 15};
}
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;
}
};
// Exact-build metadata for the experimental native VR bridge. The bridge
// interposes DebugDeviceVR slots and a checked world-pointer trampoline.
// All RVAs are validated against machine-code contracts before first use.
struct VrDebugDeviceBridgeProfile {
std::uintptr_t vtable_rva = 0;
std::uintptr_t constructor_rva = 0;
std::uintptr_t state_getter_rva = 0;
std::uintptr_t eye_getter_rva = 0;
std::uintptr_t eye_initializer_rva = 0;
std::uintptr_t emulator_flag_storage_rva = 0;
std::uintptr_t device_create_framebuffer_vtable_offset = 0;
};
struct BuildProfile {
@@ -41,6 +83,8 @@ struct BuildProfile {
// output-device virtual slots and restores them during controlled teardown;
// executable guest code is never modified.
std::optional<FmodOutputDeviceBridgeProfile> fmod_output_device_bridge;
// Optional exact-build experimental VR capability; see the struct above.
std::optional<VrDebugDeviceBridgeProfile> vr_debug_device_bridge;
std::string reason;
};
@@ -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_
+38
View File
@@ -0,0 +1,38 @@
#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_
#define MOCKTAIL_COMPAT_GUEST_ABI_H_
#include <elf.h>
#include <string_view>
namespace mocktail::compat {
#if defined(__aarch64__)
// Android ABI name as it appears in APK lib/ directories and build metadata.
inline constexpr std::string_view kGuestAbi = "arm64-v8a";
// APK split-config name carrying the guest native libraries.
inline constexpr std::string_view kGuestApkSplitName = "config.arm64_v8a";
// File name the payload store uses for the split APK.
inline constexpr std::string_view kGuestSplitApkFile =
"split_config.arm64_v8a.apk";
// sha256 map key holding the split APK digest in payload metadata.
inline constexpr std::string_view kGuestSplitApkHashKey = "arm64_v8a_split_apk";
// Short CPU label for device metadata and user-facing strings.
inline constexpr std::string_view kGuestCpuName = "aarch64";
inline constexpr int kGuestElfMachine = EM_AARCH64;
#elif defined(__x86_64__)
inline constexpr std::string_view kGuestAbi = "x86_64";
inline constexpr std::string_view kGuestApkSplitName = "config.x86_64";
inline constexpr std::string_view kGuestSplitApkFile = "split_config.x86_64.apk";
inline constexpr std::string_view kGuestSplitApkHashKey = "x86_64_split_apk";
inline constexpr std::string_view kGuestCpuName = "x86_64";
inline constexpr int kGuestElfMachine = EM_X86_64;
#else
#error "mocktail only supports aarch64 and x86_64 hosts"
#endif
// APK zip entry holding the guest libroblox.so.
inline constexpr std::string_view kGuestLibApkEntryPrefix = "lib/";
} // namespace mocktail::compat
#endif // MOCKTAIL_COMPAT_GUEST_ABI_H_
+9
View File
@@ -109,6 +109,15 @@ struct HostAbiProfile {
size_t NativeMimallocConstructorRangeEndExclusive() const noexcept;
bool ShouldInitializeNativeMimallocThreadAfterConstructor(
size_t index) const noexcept;
// True when both replays have the one verified shape. The native replay is
// one range that runs to the end of .init_array. Its first four
// constructors are the process-load constructor at the thread-initializer
// checkpoint, the JNI singleton initializer, and two wiring entries. The
// host-bridge replay runs the same constructors without the two wiring
// entries. `jni_singleton_constructor` is the .init_array index of the JNI
// singleton initializer.
bool HasVerifiedConstructorPolicy(
size_t jni_singleton_constructor) const noexcept;
bool HasValidNativePreJniBootstrap() const noexcept;
HostAllocatorStrategy ResolveAllocatorStrategy(
bool host_bridges_override_present,
+2
View File
@@ -54,6 +54,8 @@ ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment(
const HostAbiProfile* FindLoadedExternalHostAbiProfile(
std::string_view build_id) noexcept;
void SetHostAbiProfileHashingFaultForTesting(bool enabled) noexcept;
} // namespace mocktail::compat
#endif // MOCKTAIL_COMPAT_HOST_ABI_PROFILE_LOADER_H_
+190
View File
@@ -0,0 +1,190 @@
#ifndef MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
#define MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string_view>
// Roblox's Subsystem<IWebViewProtocol> getter is not exported. The exported
// WebViewProtocol.initializeAndroidWebViewProtocol calls it first, into a
// stack slot, then reads the protocol from the returned holder:
//
// x86-64: lea slot(%rbp),%rbx; mov %rbx,%rdi; call getter;
// mov (%rbx),%rbx; mov 0x18(%rbx),%rax
// AArch64: add x8, sp, #slot; bl getter; ldr xN, [sp, #slot];
// ldr x8, [xN, #0x18]
//
// These functions find that call site in the initializer's first bytes and
// check the getter's own code. Only relocated or build-varying fields (stack
// slots, frame sizes, PC-relative offsets, the call target) are masked.
// Verified on x86_64 2.734.917, 2.736.1408 and 2.738.1397, and on arm64-v8a
// 2.738.1397 and 2.740.931.
namespace mocktail::compat {
// Covers the call site in every verified build (x86-64 +0x27, AArch64 +0x34).
inline constexpr std::size_t kWebViewProtocolInitializerScanBytes = 128;
inline constexpr std::size_t kWebViewProtocolGetterBytesX86_64 = 44;
inline constexpr std::size_t kWebViewProtocolGetterBytesArm64 = 60;
namespace web_view_protocol_contract_detail {
inline bool MatchesMasked(std::string_view code, std::size_t offset,
std::string_view pattern,
std::string_view wildcard) {
if (offset > code.size() || code.size() - offset < pattern.size()) {
return false;
}
for (std::size_t index = 0; index < pattern.size(); ++index) {
if (wildcard[index] == 'x') continue;
if (code[offset + index] != pattern[index]) return false;
}
return true;
}
// AArch64 instructions are little-endian words.
inline std::uint32_t Word(std::string_view code, std::size_t index) {
std::uint32_t word = 0;
for (std::size_t byte = 0; byte < 4; ++byte) {
word |= static_cast<std::uint32_t>(
static_cast<unsigned char>(code[index * 4 + byte]))
<< (8 * byte);
}
return word;
}
// One AArch64 instruction word: bits outside `mask` may vary.
struct MaskedWord {
std::uint32_t mask;
std::uint32_t value;
};
inline constexpr std::uint32_t kExact = 0xffffffffU;
// adrp Xd, page: immlo and immhi vary.
inline constexpr std::uint32_t kAdrpMask = 0x9f00001fU;
// add Xd, Xn, #imm12: imm12 varies.
inline constexpr std::uint32_t kAddImmediateMask = 0xffc003ffU;
} // namespace web_view_protocol_contract_detail
// Returns the getter address called from the x86-64 initializer at
// `address`, or nullopt unless the frame setup and exactly one call site
// match.
inline std::optional<std::uintptr_t> FindWebViewProtocolGetterCallX86_64(
std::string_view code, std::uintptr_t address) {
using web_view_protocol_contract_detail::MatchesMasked;
// push %rbp; mov %rsp,%rbp
if (!MatchesMasked(code, 0, "\x55\x48\x89\xe5", "....")) {
return std::nullopt;
}
static constexpr std::string_view kCallSite(
"\x48\x8d\x5d\x00\x48\x89\xdf\xe8\x00\x00\x00\x00\x48\x8b\x1b\x48\x8b\x43"
"\x18",
19);
static constexpr std::string_view kCallSiteWildcard("...x....xxxx.......");
static constexpr std::size_t kCallDisplacement = 8;
const std::string_view window =
code.substr(0, kWebViewProtocolInitializerScanBytes);
std::optional<std::size_t> call_site;
for (std::size_t offset = 0; offset < window.size(); ++offset) {
if (MatchesMasked(window, offset, kCallSite, kCallSiteWildcard)) {
if (call_site.has_value()) return std::nullopt;
call_site = offset;
}
}
if (!call_site.has_value()) return std::nullopt;
std::int32_t displacement = 0;
std::memcpy(&displacement, window.data() + *call_site + kCallDisplacement,
sizeof(displacement));
const std::uintptr_t next_instruction =
address + *call_site + kCallDisplacement + sizeof(displacement);
return next_instruction +
static_cast<std::uintptr_t>(static_cast<std::intptr_t>(displacement));
}
// The x86-64 getter stores the holder returned by the subsystem's acquire
// into its result slot (%rdi) and returns that slot.
inline bool HasWebViewProtocolGetterContractX86_64(std::string_view code) {
return web_view_protocol_contract_detail::MatchesMasked(
code, 0,
std::string_view(
"\x55\x48\x89\xe5\x53\x50\x48\x89\xfb\x8a\x05\x00\x00\x00\x00\x84\xc0"
"\x74\x19\x48\x8d\x3d\x00\x00\x00\x00\xe8\x59\x00\x00\x00\x48\x89\x03"
"\x48\x89\xd8\x48\x83\xc4\x08\x5b\x5d\xc3",
kWebViewProtocolGetterBytesX86_64),
"...........xxxx.......xxxx..................");
}
// Returns the getter address called from the AArch64 initializer at
// `address`, or nullopt unless exactly one call site matches.
inline std::optional<std::uintptr_t> FindWebViewProtocolGetterCallArm64(
std::string_view code, std::uintptr_t address) {
using web_view_protocol_contract_detail::Word;
const std::size_t words =
code.substr(0, kWebViewProtocolInitializerScanBytes).size() / 4;
std::optional<std::uintptr_t> getter;
for (std::size_t index = 0; index + 4 <= words; ++index) {
const std::uint32_t slot_address = Word(code, index);
const std::uint32_t call = Word(code, index + 1);
const std::uint32_t handle_load = Word(code, index + 2);
const std::uint32_t protocol_load = Word(code, index + 3);
// add x8, sp, #slot
if ((slot_address & 0xffc003ffU) != 0x910003e8U) continue;
// bl getter
if ((call & 0xfc000000U) != 0x94000000U) continue;
// ldr xN, [sp, #slot]
if ((handle_load & 0xffc003e0U) != 0xf94003e0U) continue;
const std::uint32_t slot = (slot_address >> 10) & 0xfffU;
if (((handle_load >> 10) & 0xfffU) * 8 != slot) continue;
// ldr x8, [xN, #0x18]
const std::uint32_t handle_register = handle_load & 0x1fU;
if (protocol_load != (0xf9400c08U | (handle_register << 5))) continue;
if (getter.has_value()) return std::nullopt;
// imm26 is a signed word offset from the bl instruction.
const std::int64_t imm26 = call & 0x03ffffffU;
const std::int64_t words_from_call =
(imm26 & 0x02000000) != 0 ? imm26 - 0x04000000 : imm26;
getter = address + (index + 1) * 4 +
static_cast<std::uintptr_t>(words_from_call * 4);
}
return getter;
}
// The AArch64 getter saves its result slot (x8) and stores the holder
// returned by the subsystem's acquire there.
inline bool HasWebViewProtocolGetterContractArm64(std::string_view code) {
using namespace web_view_protocol_contract_detail;
static constexpr MaskedWord kGetter[] = {
{kExact, 0xa9be7bfdU}, // stp x29, x30, [sp, #-0x20]!
{kExact, 0xf9000bf3U}, // str x19, [sp, #0x10]
{kExact, 0x910003fdU}, // mov x29, sp
{kAdrpMask, 0x90000009U}, // adrp x9, guard
{kAddImmediateMask, 0x91000129U}, // add x9, x9, :lo12:guard
{kExact, 0xaa0803f3U}, // mov x19, x8
{kExact, 0x08dffd29U}, // ldarb w9, [x9]
{kExact, 0x36000109U}, // tbz w9, #0, initialize
{kAdrpMask, 0x90000000U}, // adrp x0, holder
{kAddImmediateMask, 0x91000000U}, // add x0, x0, :lo12:holder
{kExact, 0x94000017U}, // bl acquire
{kExact, 0xf9000260U}, // str x0, [x19]
{kExact, 0xf9400bf3U}, // ldr x19, [sp, #0x10]
{kExact, 0xa8c27bfdU}, // ldp x29, x30, [sp], #0x20
{kExact, 0xd65f03c0U}, // ret
};
static_assert(sizeof(kGetter) / sizeof(kGetter[0]) * 4 ==
kWebViewProtocolGetterBytesArm64);
if (code.size() < kWebViewProtocolGetterBytesArm64) return false;
for (std::size_t index = 0; index < sizeof(kGetter) / sizeof(kGetter[0]);
++index) {
if ((Word(code, index) & kGetter[index].mask) != kGetter[index].value) {
return false;
}
}
return true;
}
} // namespace mocktail::compat
#endif // MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
+181 -1
View File
@@ -48,9 +48,15 @@ struct PlatformIdentity {
std::string manufacturer = "Mocktail";
std::string model = "Mocktail Headless";
std::string brand = "Mocktail";
#if defined(__aarch64__)
std::string device_code = "linux-arm64";
std::string device_sku = "mocktail-arm64";
std::string soc_model = "aarch64";
#else
std::string device_code = "linux-x86_64";
std::string device_sku = "mocktail-x86_64";
std::string soc_model = "x86_64";
#endif
};
struct RobloxCredentialView {
@@ -59,6 +65,8 @@ struct RobloxCredentialView {
};
using RobloxCredentialProvider = RobloxCredentialView (*)(const void *context);
using RobloxCookieGetter = jstring (*)(JNIEnv* env, jclass clazz,
jstring domain);
jobject CreateAndroidConfiguration(JNIEnv *env);
@@ -80,6 +88,67 @@ struct FmodAudioDeviceCallbacks {
void (*shutdown)(void *context) = nullptr;
};
// Parameters passed by WebRtcAudioManager's constructor to the guest's
// nativeCacheAudioParameters(IIIZZZZZZZIIJ)V. Buffer sizes are PCM frames,
// not bytes. Querying them must not open a microphone.
struct WebRtcAudioManagerParameters {
int sample_rate_hz = 0;
int output_channels = 0;
int input_channels = 0;
bool hardware_aec = false;
bool hardware_agc = false;
bool hardware_ns = false;
bool low_latency_output = false;
bool low_latency_input = false;
bool pro_audio = false;
bool aaudio = false;
int output_buffer_size_frames = 0;
int input_buffer_size_frames = 0;
};
struct WebRtcAudioManagerCallbacks {
bool (*get_parameters)(void* context,
WebRtcAudioManagerParameters* parameters) = nullptr;
bool (*init)(void* context, const void* identity) = nullptr;
void (*dispose)(void* context, const void* identity) = nullptr;
void (*set_microphone_mute)(void* context, bool muted) = nullptr;
};
using WebRtcAudioRecordDataCallback = void (*)(void *context,
const void *identity,
std::size_t size_bytes);
// Host implementation of org/webrtc/voiceengine/WebRtcAudioRecord. Init
// returns the number of PCM frames in the stable direct buffer, or -1.
struct WebRtcAudioRecordCallbacks {
int (*init)(void *context, const void *identity, int sample_rate_hz,
int channels, WebRtcAudioRecordDataCallback data_callback,
void *data_context, void **direct_buffer,
std::size_t *direct_buffer_capacity) = nullptr;
bool (*start)(void *context, const void *identity) = nullptr;
bool (*stop)(void *context, const void *identity) = nullptr;
void (*close)(void *context, const void *identity) = nullptr;
void (*shutdown)(void *context) = nullptr;
};
using WebRtcAudioTrackDataCallback = void (*)(void *context,
const void *identity,
std::size_t size_bytes);
// Host implementation of org/webrtc/voiceengine/WebRtcAudioTrack. Init
// returns the emulated Android AudioTrack buffer size in bytes, or -1.
struct WebRtcAudioTrackCallbacks {
int (*init)(void *context, const void *identity, int sample_rate_hz,
int channels, double buffer_size_factor,
WebRtcAudioTrackDataCallback data_callback, void *data_context,
void **direct_buffer,
std::size_t *direct_buffer_capacity) = nullptr;
int (*buffer_size_frames)(void *context, const void *identity) = nullptr;
bool (*start)(void *context, const void *identity) = nullptr;
bool (*stop)(void *context, const void *identity) = nullptr;
void (*close)(void *context, const void *identity) = nullptr;
};
// Guest JNI threads only publish Android window flags. The host callback must
// marshal SDL work onto the main thread.
struct AndroidWindowCallbacks {
@@ -130,6 +199,11 @@ struct MessageBusRequestHandlerCallbacks {
std::string (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
};
struct MessageBusAsyncRequestHandlerCallbacks {
void (*run)(void* context, JNIEnv* env, jstring message,
jstring response_id) = nullptr;
};
// Dispatch accepts Callback.onItemSet(String) only on a receiver created by
// this VM. The context stays alive while the callback runs.
struct MemStorageCallbackCallbacks {
@@ -194,10 +268,12 @@ public:
JavaVM *GetJavaVM() { return java_vm_; }
// Resolves the process pseudo-VM that owns an exact JavaVM pointer. Returns
// Resolves the live pseudo-VM that owns an exact JavaVM pointer. Returns
// null for foreign VMs and after the owner is destroyed.
static VM *FromJavaVM(JavaVM *java_vm);
// Binds this thread's JNI environment to this VM. Switching owners must
// not reuse the previous VM's native function table.
JNIEnv *GetJNIEnv();
std::size_t GetClassCount() const { return class_registry_.size(); }
@@ -228,6 +304,9 @@ public:
// retained privately and returned instead until the provider is cleared.
bool CopyRobloxCredentialFromProvider(std::string *credential) const;
void SetRobloxCookieGetter(RobloxCookieGetter getter);
bool RefreshRobloxCredentialFromEngine(JNIEnv* env);
void SetRobloxCredentialSink(std::shared_ptr<void> context,
const RobloxCredentialSinkCallbacks &callbacks);
void ClearRobloxCredentialSink();
@@ -250,6 +329,55 @@ public:
const std::uint8_t *data, std::size_t size);
bool DispatchFmodAudioDeviceClose(const void *identity);
void SetWebRtcAudioManagerCallbacks(
std::shared_ptr<void> context,
const WebRtcAudioManagerCallbacks& callbacks);
void ClearWebRtcAudioManagerCallbacks();
void RegisterWebRtcAudioManagerNative(const char* name, const char* signature,
void* function);
bool DispatchWebRtcAudioManagerConstruct(jobject manager,
jlong native_audio_manager);
bool DispatchWebRtcAudioManagerInit(jobject manager);
void DispatchWebRtcAudioManagerDispose(jobject manager);
void DispatchWebRtcAudioManagerMicrophoneMute(jobject manager, bool muted);
void
SetWebRtcAudioRecordCallbacks(std::shared_ptr<void> context,
const WebRtcAudioRecordCallbacks &callbacks);
void ClearWebRtcAudioRecordCallbacks();
int DispatchWebRtcAudioRecordInit(const void *identity, int sample_rate_hz,
int channels, void **direct_buffer,
std::size_t *direct_buffer_capacity);
bool DispatchWebRtcAudioRecordStart(const void *identity);
bool DispatchWebRtcAudioRecordStop(const void *identity);
void DispatchWebRtcAudioRecordClose(const void *identity);
void DispatchWebRtcAudioRecordData(const void *identity,
std::size_t size_bytes);
// RegisterNatives stores these exact WebRTC callbacks so the host capture
// thread can emulate the Java AudioRecord thread.
void RegisterWebRtcAudioRecordNative(const char *name, const char *signature,
void *function);
void SetWebRtcAudioTrackCallbacks(std::shared_ptr<void> context,
const WebRtcAudioTrackCallbacks &callbacks);
void ClearWebRtcAudioTrackCallbacks();
int DispatchWebRtcAudioTrackInit(const void *identity, int sample_rate_hz,
int channels, double buffer_size_factor,
void **direct_buffer,
std::size_t *direct_buffer_capacity);
int DispatchWebRtcAudioTrackBufferSizeFrames(const void *identity);
bool DispatchWebRtcAudioTrackStart(const void *identity);
bool DispatchWebRtcAudioTrackStop(const void *identity);
void DispatchWebRtcAudioTrackClose(const void *identity);
void DispatchWebRtcAudioTrackData(const void *identity,
std::size_t size_bytes);
// RegisterNatives stores the exact WebRTC playout callbacks so the host
// thread can emulate AudioTrackJavaThread.
void RegisterWebRtcAudioTrackNative(const char *name, const char *signature,
void *function);
void SetAndroidWindowCallbacks(std::shared_ptr<void> context,
const AndroidWindowCallbacks &callbacks);
void ClearAndroidWindowCallbacks();
@@ -278,6 +406,16 @@ public:
jstring DispatchMessageBusRequestHandler(jobject handler, JNIEnv *env,
jstring message);
// Exact RequestHandlerAsyncRaw.run(String, String) receiver. Clearing stops
// new calls; an in-flight call retains the host context until it returns.
jobject CreateMessageBusAsyncRequestHandler(
std::shared_ptr<void> context,
const MessageBusAsyncRequestHandlerCallbacks& callbacks);
void ClearMessageBusAsyncRequestHandler(jobject handler);
bool DispatchMessageBusAsyncRequestHandler(jobject handler, JNIEnv* env,
jstring message,
jstring response_id);
// Creates the exact com.roblox.engine.jni.memstorage.Callback receiver used
// by MemStorage.bind. Clear prevents future dispatch while an in-flight call
// retains its shared context until the callback returns.
@@ -338,6 +476,9 @@ private:
RobloxCredentialProvider roblox_credential_provider_ = nullptr;
std::string roblox_credential_override_;
mutable std::mutex roblox_cookie_getter_mutex_;
RobloxCookieGetter roblox_cookie_getter_ = nullptr;
struct RobloxCredentialSinkBinding {
std::shared_ptr<void> context;
RobloxCredentialSinkCallbacks callbacks;
@@ -352,6 +493,36 @@ private:
mutable std::mutex fmod_audio_device_mutex_;
FmodAudioDeviceBinding fmod_audio_device_binding_;
struct WebRtcAudioManagerBinding {
std::shared_ptr<void> context;
WebRtcAudioManagerCallbacks callbacks;
};
mutable std::mutex webrtc_audio_manager_mutex_;
WebRtcAudioManagerBinding webrtc_audio_manager_binding_;
void* webrtc_cache_audio_parameters_native_ = nullptr;
struct WebRtcAudioRecordBinding {
std::shared_ptr<void> context;
WebRtcAudioRecordCallbacks callbacks;
};
mutable std::mutex webrtc_audio_record_mutex_;
WebRtcAudioRecordBinding webrtc_audio_record_binding_;
mutable std::mutex webrtc_audio_record_native_mutex_;
void *webrtc_cache_direct_buffer_native_ = nullptr;
void *webrtc_data_is_recorded_native_ = nullptr;
struct WebRtcAudioTrackBinding {
std::shared_ptr<void> context;
WebRtcAudioTrackCallbacks callbacks;
};
mutable std::mutex webrtc_audio_track_mutex_;
WebRtcAudioTrackBinding webrtc_audio_track_binding_;
mutable std::mutex webrtc_audio_track_native_mutex_;
void *webrtc_track_cache_direct_buffer_native_ = nullptr;
void *webrtc_get_playout_data_native_ = nullptr;
struct AndroidWindowBinding {
std::shared_ptr<void> context;
AndroidWindowCallbacks callbacks;
@@ -379,6 +550,15 @@ private:
std::unordered_map<jobject, std::shared_ptr<MessageBusRequestHandlerBinding>>
message_bus_request_handler_bindings_;
struct MessageBusAsyncRequestHandlerBinding {
std::shared_ptr<void> context;
MessageBusAsyncRequestHandlerCallbacks callbacks;
};
mutable std::mutex message_bus_async_request_handler_mutex_;
std::unordered_map<jobject,
std::shared_ptr<MessageBusAsyncRequestHandlerBinding>>
message_bus_async_request_handler_bindings_;
struct MemStorageCallbackBinding {
std::shared_ptr<void> context;
MemStorageCallbackCallbacks callbacks;
@@ -0,0 +1,17 @@
#ifndef MOCKTAIL_LIBC_SHIM_VULKAN_ETC1_SKY_TRANSCODER_H_
#define MOCKTAIL_LIBC_SHIM_VULKAN_ETC1_SKY_TRANSCODER_H_
#include <cstdint>
#include <vector>
namespace libc_shim {
// Converts one opaque ETC1 RGB block to an opaque BC1 RGB block.
void ConvertEtc1RgbBlockToBc1(const uint8_t *etc1, uint8_t *bc1);
bool TranscodeEtc1SkyTextureForVulkan(const char *path,
std::vector<unsigned char> *data);
} // namespace libc_shim
#endif // MOCKTAIL_LIBC_SHIM_VULKAN_ETC1_SKY_TRANSCODER_H_
@@ -0,0 +1,88 @@
#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
#if defined(__aarch64__)
#include <cstdint>
#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__) || defined(__aarch64__)
if (active_ || thread_name == nullptr) return false;
const std::string_view name(thread_name);
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
return false;
}
#if defined(__x86_64__)
const unsigned int control = _mm_getcsr();
original_mode_ = control & kModeMask;
_mm_setcsr(control | kModeMask);
#else
std::uint64_t control;
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
original_mode_ = control & kModeMask;
__asm__ volatile("msr fpcr, %0" ::"r"(control | kModeMask));
#endif
active_ = true;
return true;
#else
(void)thread_name;
return false;
#endif
}
void Restore() noexcept {
#if defined(__x86_64__) || defined(__aarch64__)
if (active_) {
// Restore only our two bits, preserving rounding, exception masks and
// status flags set by the guest while the thread was attached.
#if defined(__x86_64__)
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
#else
std::uint64_t control;
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
__asm__ volatile("msr fpcr, %0" ::"r"((control & ~kModeMask) |
original_mode_));
#endif
active_ = false;
}
#endif
}
private:
#if defined(__x86_64__)
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
unsigned int original_mode_ = 0;
bool active_ = false;
#elif defined(__aarch64__)
// FPCR FZ flushes single/double subnormals; FZ16 covers the half-precision
// path used by some voice DSP filters.
static constexpr std::uint64_t kModeMask =
(std::uint64_t{1} << 24) | (std::uint64_t{1} << 19);
std::uint64_t original_mode_ = 0;
bool active_ = false;
#endif
};
} // 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;
@@ -41,23 +44,34 @@ class RobloxOutputDeviceBridge final {
std::string_view logical_name,
std::uintptr_t image_base);
static int GetOutputDeviceCount(void* self);
static void* GetOutputDeviceInfo(void* result, void* self, int index);
// A trivial aggregate lets the compiler implement the guest's platform
// sret convention (x8 on AArch64, hidden first argument on x86-64).
struct alignas(8) DeviceInfoResult { std::uint8_t bytes[56]{}; };
static DeviceInfoResult GetOutputDeviceInfo(void* self, int index);
static int GetCurrentOutputDevice(void* self);
static void SetCurrentOutputDevice(void* self, int index);
Status Activate(std::uintptr_t image_base);
Status PatchVtableLocked();
bool RestoreVtableLocked();
int DeviceCount();
void* DeviceInfo(void* result, int index);
int CurrentDevice();
void SelectDevice(int index);
static int GetInputDeviceCount(void *self);
static DeviceInfoResult GetInputDeviceInfo(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::shared_ptr<NativeInputCapture> capture_;
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;
@@ -0,0 +1,80 @@
#ifndef MOCKTAIL_AUDIO_SDL_AUDIO_CAPTURE_H_
#define MOCKTAIL_AUDIO_SDL_AUDIO_CAPTURE_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "mocktail/audio/audio_sink.h"
namespace mocktail::audio {
struct SdlRecordingDevice {
std::uint32_t id = 0;
std::string name;
};
// The virtual `default` target is not returned. Device IDs are process-local;
// names are the stable configuration choice, while `id:<number>` disambiguates
// duplicate names for the current host session.
Status ListSdlRecordingDevices(std::vector<SdlRecordingDevice>* devices);
Status ResolveSdlRecordingDevice(
std::string_view requested,
const std::vector<SdlRecordingDevice>& available_devices,
std::uint32_t* recording_device_id, std::string* resolved_name);
// Must run after SDL audio initialization and before the first capture opens.
Status ConfigureSdlRecordingDevice(
std::string_view requested,
std::vector<SdlRecordingDevice>* available_devices,
std::string* resolved_name);
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);
class AudioCapture {
public:
virtual ~AudioCapture() = default;
virtual const PcmSpec& output_spec() const = 0;
virtual void* buffer_data() = 0;
virtual std::size_t buffer_size_bytes() const = 0;
virtual std::size_t frames_per_buffer() const = 0;
virtual Status Start() = 0;
virtual Status Stop() = 0;
virtual void Shutdown() = 0;
};
struct SdlAudioCaptureOptions {
PcmSpec output_spec;
// Zero selects the configured process target.
std::uint32_t recording_device_id = 0;
std::uint32_t frame_duration_ms = 10;
AudioCaptureDataCallback data_callback = nullptr;
void* data_context = nullptr;
};
// SDL converts the host input to output_spec and delivers fixed-duration
// frames through one stable buffer. The callback runs synchronously on SDL's
// recording thread after that buffer has been filled.
Status CreateSdlAudioCapture(const SdlAudioCaptureOptions& options,
std::unique_ptr<AudioCapture>* capture);
// Called by the SDL subsystem owner before SDL_QuitSubSystem.
Status PrepareSdlAudioCaptureSubsystemShutdown();
} // namespace mocktail::audio
#endif // MOCKTAIL_AUDIO_SDL_AUDIO_CAPTURE_H_
+7
View File
@@ -48,6 +48,13 @@ struct SdlAudioSinkOptions {
// Zero selects the configured process target.
std::uint32_t playback_device_id = 0;
bool start_paused = true;
// Optional pull provider for the host device. Each call fills one source
// buffer whose address remains valid until the next provider call. SDL
// copies as many buffers as the current device request needs.
bool (*data_needed_callback)(void *context, const void **data,
std::size_t *size_bytes) = nullptr;
void *data_needed_context = nullptr;
};
// SDL owns conversion, resampling, and borrowed-buffer consumption.
@@ -0,0 +1,21 @@
#ifndef MOCKTAIL_AUDIO_WEBRTC_JNI_AUDIO_BRIDGE_H_
#define MOCKTAIL_AUDIO_WEBRTC_JNI_AUDIO_BRIDGE_H_
#include "mocktail/status.h"
namespace jnivm {
class VM;
}
namespace mocktail::audio {
// Installs WebRtcAudioManager, WebRtcAudioRecord and WebRtcAudioTrack. The
// manager only advertises the host PCM contract; capture opens on recording
// init, never on manager construction. SDL's audio subsystem must remain alive
// until ShutdownWebRtcJniAudioBridge.
Status InstallWebRtcJniAudioBridge(jnivm::VM* vm);
Status ShutdownWebRtcJniAudioBridge(jnivm::VM* vm);
} // namespace mocktail::audio
#endif // MOCKTAIL_AUDIO_WEBRTC_JNI_AUDIO_BRIDGE_H_
@@ -19,30 +19,14 @@ inline constexpr AndroidVulkanResult kAndroidVulkanSuccess =
static_cast<AndroidVulkanResult>(0);
inline constexpr AndroidVulkanResult kAndroidVulkanErrorInitializationFailed =
static_cast<AndroidVulkanResult>(-3);
inline constexpr std::uint64_t kHostInfiniteWaitDiagnosticSliceNs =
1000000000ULL;
inline constexpr std::uint64_t kHostImageAcquireWatchdogTimeoutNs =
kHostInfiniteWaitDiagnosticSliceNs;
inline constexpr char kAndroidSurfaceExtension[] =
"VK_KHR_android_surface";
// Roblox treats VK_SUBOPTIMAL_KHR as a hard error, so expose it as success.
// VK_SUBOPTIMAL_KHR still returns a usable image. Roblox handles it as a fatal
// error, so accept it until the host reports a real out-of-date surface.
VkResult NormalizeAndroidSwapchainResult(VkResult result);
// Bounds only infinite acquire waits so a lost compositor wakeup cannot hang.
std::uint64_t BoundHostImageAcquireTimeout(std::uint64_t requested_timeout);
bool IsHostImageAcquireWatchdogTimeout(std::uint64_t requested_timeout,
VkResult host_result);
VkResult NormalizeHostImageAcquireResult(std::uint64_t requested_timeout,
VkResult host_result);
// Infinite guest waits use finite host slices without exposing slice timeouts.
std::uint64_t BoundHostSynchronizationWaitTimeout(
std::uint64_t requested_timeout);
bool ShouldContinueHostSynchronizationWait(std::uint64_t requested_timeout,
VkResult host_result);
// Returned storage owns the translated extension names.
Status TranslateAndroidVulkanInstanceExtensions(
const std::vector<std::string>& android_extensions,
+3
View File
@@ -16,6 +16,9 @@ struct AngleProbeOptions {
bool allow_software_device = false;
};
// Checks ANGLE entry points and client extensions without initializing a display.
BackendCapability InspectAngleLibraries(const AngleProbeOptions& options);
// Loads a pinned ANGLE pair, verifies the required EGL/GLES symbols, and
// initializes an EGL_PLATFORM_ANGLE Vulkan display. No context is fabricated.
BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options);
@@ -0,0 +1,150 @@
#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();
// CPU time used so far by the calling thread, in nanoseconds.
std::uint64_t TraceThreadCpuNanos();
// CPU time used so far by every thread of the process, in nanoseconds.
std::uint64_t TraceProcessCpuNanos();
// CPU time spent between two successive presents.
struct FrameCpuSample {
double process_ms = 0;
// Set only when the previous present came from the same thread; thread CPU
// clocks of different threads cannot be subtracted.
bool has_thread = false;
double thread_ms = 0;
};
// Turns CPU clocks read at each present into per-frame CPU time.
// Thread-safe.
class FrameCpuSampler final {
public:
// Returns false for the first present, which ends no frame.
bool Sample(std::uint64_t thread_id, std::uint64_t thread_cpu_ns,
std::uint64_t process_cpu_ns, FrameCpuSample* sample);
private:
std::mutex mutex_;
bool has_previous_ = false;
std::uint64_t previous_thread_id_ = 0;
std::uint64_t previous_thread_cpu_ns_ = 0;
std::uint64_t previous_process_cpu_ns_ = 0;
};
// Writes the Trace Event Format JSON array read by chrome://tracing and
// ui.perfetto.dev. Recording threads only format into a shared buffer; a
// 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_
@@ -1,6 +1,8 @@
#ifndef MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
#define MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
#include <cstdint>
#include <cstdio>
#include <string_view>
#include <vector>
@@ -17,10 +19,52 @@ enum class PresentModePolicy {
PresentModePolicy ResolvePresentModePolicy(std::string_view vsync_value,
std::string_view frame_rate_value);
// Reads MOCKTAIL_VSYNC / MOCKTAIL_FRAME_RATE_LIMIT once. Window pacing and
// the Vulkan adapter must use this instead of getenv'ing the same keys.
PresentModePolicy CachedPresentModePolicy();
std::vector<VkPresentModeKHR>
FilterPresentModes(PresentModePolicy policy,
const std::vector<VkPresentModeKHR> &host_modes);
const char *PresentModePolicyName(PresentModePolicy policy);
const char *PresentModeKhrName(VkPresentModeKHR mode);
// Creates the swapchain with the mode in `info`. A driver can reject a mode
// that the device did not enable, so a failed create is retried once with the
// mode the game asked for. `create` takes the info and returns a VkResult.
template <typename Create>
VkResult CreateSwapchainWithPresentModeFallback(VkSwapchainCreateInfoKHR &info,
VkPresentModeKHR requested,
Create create) {
const VkResult result = create(info);
if (result == VK_SUCCESS || info.presentMode == requested) {
return result;
}
switch (result) {
case VK_ERROR_OUT_OF_HOST_MEMORY:
case VK_ERROR_OUT_OF_DEVICE_MEMORY:
case VK_ERROR_DEVICE_LOST:
case VK_ERROR_SURFACE_LOST_KHR:
case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR:
return result; // Another present mode cannot fix these.
default:
break;
}
std::fprintf(stderr,
" [vulkan] swapchain presentMode %s failed (%d); retrying "
"with %s\n",
PresentModeKhrName(info.presentMode), static_cast<int>(result),
PresentModeKhrName(requested));
info.presentMode = requested;
return create(info);
}
// Extra images so acquire does not wait on the compositor-held buffer.
std::uint32_t PreferSwapchainMinImageCount(PresentModePolicy policy,
std::uint32_t requested,
std::uint32_t min_images,
std::uint32_t max_images);
} // namespace graphics
} // namespace mocktail
@@ -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,72 @@
#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);
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
std::uint32_t source_height, std::size_t source_stride_bytes,
std::uint32_t width, std::uint32_t height,
std::uint8_t* destination);
// Replacement textures keyed by the hash of a texture's level-0 compressed
// bytes. An override lives at <override_dir>/<hash>.png. With a dump
// 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,55 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <vulkan/vulkan.h>
namespace mocktail::graphics::internal {
// Only transfers between two concrete queue families can be converted for a
// desktop swapchain. External/foreign ownership must reach the driver intact.
inline bool NeedsDesktopQueueNormalization(std::uint32_t source,
std::uint32_t destination) {
return source != destination && source < VK_QUEUE_FAMILY_FOREIGN_EXT &&
destination < VK_QUEUE_FAMILY_FOREIGN_EXT;
}
template <class Barrier>
inline bool HasDesktopQueueTransfers(std::uint32_t count,
const Barrier* barriers) {
if (!barriers) return false;
for (std::uint32_t i = 0; i < count; ++i)
if (NeedsDesktopQueueNormalization(barriers[i].srcQueueFamilyIndex,
barriers[i].dstQueueFamilyIndex))
return true;
return false;
}
// Keep the caller's array for the common no-ownership-transfer path. A batch
// callback can inspect a mixed array under one backend lock; the original
// array and every structure chained through pNext remain untouched.
template <class Barrier, class Normalize, class Forward>
inline void WithDesktopQueueBarriers(std::uint32_t count,
const Barrier* barriers,
Normalize&& normalize, Forward&& forward) {
if (!HasDesktopQueueTransfers(count, barriers)) {
forward(barriers);
return;
}
constexpr std::size_t kStackCount = 8;
if (count <= kStackCount) {
std::array<Barrier, kStackCount> adjusted;
std::copy_n(barriers, count, adjusted.data());
normalize(count, adjusted.data());
forward(adjusted.data());
} else {
std::vector<Barrier> adjusted(barriers, barriers + count);
normalize(count, adjusted.data());
forward(adjusted.data());
}
}
} // namespace mocktail::graphics::internal
@@ -0,0 +1,179 @@
#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);
// Sums every mip level and layer; `scale` multiplies the host image's
// level-0 width and height only.
bool Etc2ImageByteCounts(EtcFormat format, const VkExtent3D& extent,
std::uint32_t mip_levels, std::uint32_t layers,
std::uint32_t scale, VkDeviceSize* compressed,
VkDeviceSize* decoded);
struct EmulatedTextureBytes {
VkDeviceSize compressed = 0;
VkDeviceSize decoded = 0;
};
// 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);
// Staging for decoded uploads is carved out of host-visible blocks this
// large; an upload that needs more gets a block of its own.
inline constexpr VkDeviceSize kEtc2StagingBlockBytes = 16 * 1024 * 1024;
// A timeline semaphore and the value an asynchronous decode will signal.
// `semaphore` is VK_NULL_HANDLE when the decode already finished on the
// calling thread and the submit needs no extra wait.
struct Etc2SubmitWait {
VkSemaphore semaphore = VK_NULL_HANDLE;
std::uint64_t value = 0;
};
// 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 ranges live until their command buffer is begun, reset or freed.
// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off.
class VulkanEtc2Emulation final {
public:
explicit VulkanEtc2Emulation(
VkDeviceSize staging_block_bytes = kEtc2StagingBlockBytes);
~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);
// `timeline_semaphore` states whether the device was created with the
// timelineSemaphore feature enabled. Without it every decode runs on the
// calling thread.
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
bool emulated,
const VkPhysicalDeviceMemoryProperties& memory,
PFN_vkGetDeviceProcAddr get_device_proc_addr,
bool timeline_semaphore = false);
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.
//
// With `allow_async` on a device that has a timeline semaphore, the decode
// runs on worker threads, and so does the gather of the compressed bytes
// whenever the application already has them mapped. The returned semaphore
// and value must then be added to the submit's waits, so the GPU copy
// cannot execute before the decode finishes. Values are signalled in the
// order they are handed out.
//
// Those threads read the application's source bytes after this returns,
// which Vulkan allows: the application may not write them between the
// submit and the copy's completion. Unmapping or freeing that memory waits
// for the decodes still reading it.
//
// Returns `{VK_NULL_HANDLE, 0}` when there was nothing to decode, when
// `allow_async` is false, or when the device has no timeline semaphore. In
// those cases every upload is already decoded when this returns.
Etc2SubmitWait PrepareSubmit(const VkCommandBuffer* command_buffers,
std::uint32_t count, bool allow_async = false);
// Destroys the staging buffers owned by a command buffer that is no longer
// pending, after waiting for any decode still writing into them.
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
// Traced as the "emulated texture (MB)" and "compressed texture (MB)"
// counters.
EmulatedTextureBytes TextureBytes() const;
struct State;
private:
State* state_;
};
} // namespace mocktail::graphics
#endif // MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
@@ -0,0 +1,44 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vulkan/vulkan.h>
namespace mocktail::graphics::internal {
struct FoveationDevicePolicy {
bool density_offsets_enabled = true;
};
// Capture before invoking a runtime/loader device callback: its temporary
// feature-chain records must never be inspected after the call returns.
inline FoveationDevicePolicy SnapshotFoveationDevicePolicy(
const VkDeviceCreateInfo* info) {
if (!info || info->sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO ||
info->enabledExtensionCount > 4096 ||
(info->enabledExtensionCount && !info->ppEnabledExtensionNames)) return {};
bool offsets = false;
for (std::uint32_t i = 0; i < info->enabledExtensionCount; ++i) {
const char* name = info->ppEnabledExtensionNames[i];
if (!name) return {};
if (!std::strcmp(name, "VK_QCOM_fragment_density_map_offset") ||
!std::strcmp(name, "VK_EXT_fragment_density_map_offset"))
offsets = true;
}
std::array<const VkBaseInStructure*, 64> seen{};
std::size_t count = 0;
for (auto* node = static_cast<const VkBaseInStructure*>(info->pNext);
node; node = node->pNext) {
if (count == seen.size()) return {};
for (std::size_t i = 0; i < count; ++i) if (seen[i] == node) return {};
seen[count++] = node;
if (node->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_OFFSET_FEATURES_EXT)
offsets |= reinterpret_cast<const VkPhysicalDeviceFragmentDensityMapOffsetFeaturesEXT*>(node)
->fragmentDensityMapOffset != VK_FALSE;
}
return {offsets};
}
} // namespace mocktail::graphics::internal
+86 -1
View File
@@ -1,6 +1,7 @@
#ifndef MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
#define MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
#include <cmath>
#include <cstdint>
#include <string>
#include <variant>
@@ -26,7 +27,7 @@ struct WindowOptions {
int height = 720;
std::string title = "Roblox";
WindowSurfaceApi surface_api = WindowSurfaceApi::kAngleEgl;
bool high_pixel_density = true;
bool high_pixel_density = false;
bool initially_hidden = true;
// Production callers should keep this true. Tests using SDL's dummy driver
@@ -58,11 +59,69 @@ struct NativeWindowDescriptor {
struct QuitEvent {};
// Android exposes pointer and TextBox geometry in density-independent surface
// coordinates. SDL exposes pointer positions in logical window coordinates,
// while Vulkan presents to the pixel-sized drawable. Keep all three spaces in
// one value so input, IME placement and same-surface overlays use exact inverse
// transforms.
struct SurfaceCoordinateTransform {
int32_t logical_width = 0;
int32_t logical_height = 0;
int32_t pixel_width = 0;
int32_t pixel_height = 0;
float density_scale = 1.0F;
bool valid() const {
return logical_width > 0 && logical_height > 0 && pixel_width > 0 &&
pixel_height > 0 && std::isfinite(density_scale) &&
density_scale > 0.0F;
}
float guest_width() const {
return valid() ? static_cast<float>(pixel_width) / density_scale : 0.0F;
}
float guest_height() const {
return valid() ? static_cast<float>(pixel_height) / density_scale : 0.0F;
}
int32_t rounded_guest_width() const {
return valid() ? static_cast<int32_t>(std::lround(guest_width())) : 0;
}
int32_t rounded_guest_height() const {
return valid() ? static_cast<int32_t>(std::lround(guest_height())) : 0;
}
float HostLogicalToGuestX(float coordinate) const {
return valid() ? coordinate * static_cast<float>(pixel_width) /
static_cast<float>(logical_width) / density_scale
: coordinate;
}
float HostLogicalToGuestY(float coordinate) const {
return valid() ? coordinate * static_cast<float>(pixel_height) /
static_cast<float>(logical_height) / density_scale
: coordinate;
}
float GuestToHostLogicalX(float coordinate) const {
return valid() ? coordinate * density_scale *
static_cast<float>(logical_width) /
static_cast<float>(pixel_width)
: coordinate;
}
float GuestToHostLogicalY(float coordinate) const {
return valid() ? coordinate * density_scale *
static_cast<float>(logical_height) /
static_cast<float>(pixel_height)
: coordinate;
}
};
struct WindowResizedEvent {
int logical_width = 0;
int logical_height = 0;
int pixel_width = 0;
int pixel_height = 0;
// Android DisplayMetrics.density equivalent. This is independent of the
// logical-to-pixel drawable ratio on X11/XWayland.
float density_scale = 1.0F;
};
struct WindowFocusEvent {
@@ -93,6 +152,10 @@ struct MouseMotionEvent {
float delta_x = 0.0f;
float delta_y = 0.0f;
std::uint32_t buttons = 0;
// True while the window holds relative mouse mode. The absolute coordinates
// are meaningless then: X11 reports zeroes, Wayland reports a position that
// accumulates the deltas and can leave the window entirely.
bool relative = false;
};
struct MouseButtonEvent {
@@ -101,6 +164,8 @@ struct MouseButtonEvent {
std::uint8_t clicks = 0;
float x = 0.0f;
float y = 0.0f;
// See MouseMotionEvent::relative.
bool relative = false;
};
struct MouseWheelEvent {
@@ -108,6 +173,8 @@ struct MouseWheelEvent {
float delta_y = 0.0f;
float mouse_x = 0.0f;
float mouse_y = 0.0f;
// See MouseMotionEvent::relative.
bool relative = false;
};
struct TouchEvent {
@@ -128,9 +195,27 @@ struct TouchEvent {
float pressure = 0.0f;
};
enum class GamepadFamily {
kUnknown,
kXbox,
kPlayStation4,
kPlayStation5,
};
struct GamepadDescriptor {
GamepadFamily family = GamepadFamily::kUnknown;
std::string name;
// Bit positions are SDL's standardized gamepad buttons and axes, not raw
// joystick indices or Android key codes.
std::uint32_t buttons = 0;
std::uint32_t axes = 0;
};
struct GamepadConnectionEvent {
std::int64_t instance_id = 0;
bool connected = false;
GamepadDescriptor descriptor{};
bool remapped = false;
};
struct GamepadAxisEvent {
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#ifndef MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
#define MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
@@ -6,9 +7,9 @@
namespace mocktail {
namespace platform {
inline constexpr char kMocktailApplicationName[] = "Mocktail";
inline constexpr char kMocktailApplicationName[] = "Nightcap";
inline constexpr char kMocktailApplicationIdentifier[] =
"space.bigrat.mocktail";
"io.github.CoderDayton.nightcap";
// Configures the stable compositor identity before SDL initializes. The
// identifier matches the installed desktop file and icon name so Wayland and
@@ -0,0 +1,45 @@
#ifndef MOCKTAIL_PLATFORM_SDL_GAMEPAD_MANAGER_H_
#define MOCKTAIL_PLATFORM_SDL_GAMEPAD_MANAGER_H_
#include <SDL3/SDL_gamepad.h>
#include <map>
#include "mocktail/platform/platform_runtime.h"
namespace mocktail {
namespace platform {
// Main-thread owner. Consumes events from the existing SDL pump; it never
// creates another event loop or consumes keyboard/window events.
class SdlGamepadManager final {
public:
using EventFn = void (*)(void*, const PlatformEvent&);
~SdlGamepadManager();
SdlGamepadManager() = default;
SdlGamepadManager(const SdlGamepadManager&) = delete;
SdlGamepadManager& operator=(const SdlGamepadManager&) = delete;
Status Initialize(void* context, EventFn emit);
void Shutdown();
bool HandleEvent(const PlatformEvent& event);
void ResendState();
private:
SDL_Gamepad* Open(SDL_JoystickID id);
void Remove(SDL_JoystickID id);
void Describe(SDL_JoystickID id, SDL_Gamepad* gamepad, bool remapped);
void EmitState(SDL_JoystickID id, SDL_Gamepad* gamepad);
void Emit(PlatformEventPayload payload);
std::map<SDL_JoystickID, SDL_Gamepad*> gamepads_;
void* context_ = nullptr;
EventFn emit_ = nullptr;
bool initialized_ = false;
};
} // namespace platform
} // namespace mocktail
#endif // MOCKTAIL_PLATFORM_SDL_GAMEPAD_MANAGER_H_
-45
View File
@@ -1,45 +0,0 @@
#ifndef MOCKTAIL_MOCKTAIL_SHA256_H_
#define MOCKTAIL_MOCKTAIL_SHA256_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
namespace mocktail {
namespace foundation {
class Sha256 final {
public:
Sha256() = default;
~Sha256();
Sha256(const Sha256&) = delete;
Sha256& operator=(const Sha256&) = delete;
void Update(const unsigned char* bytes, std::size_t size);
void Update(std::string_view bytes);
std::string FinalHex();
private:
void Transform(const unsigned char* block);
std::array<std::uint32_t, 8> state_ = {
0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU,
0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U,
};
std::array<unsigned char, 64> block_{};
std::size_t block_bytes_ = 0;
std::uint64_t total_bytes_ = 0;
bool finalized_ = false;
};
// Returns the lowercase SHA-256 digest of the exact byte sequence, including
// embedded NUL bytes.
std::string ComputeSha256Hex(std::string_view bytes);
} // namespace foundation
} // namespace mocktail
#endif // MOCKTAIL_MOCKTAIL_SHA256_H_
@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
namespace mocktail::vr::internal {
// Retired multisample choices use the native profile's SMAA High default.
constexpr int NativeAntiAliasingMode(std::int64_t value) {
return value >= 0 && value <= 3 ? static_cast<int>(value) : 3;
}
} // namespace mocktail::vr::internal
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <thread>
#include <type_traits>
namespace mocktail::vr::internal {
// Load once during startup, then keep file I/O and parsing on a worker. The
// render thread only copies a completed snapshot under a short-held lock.
template <typename T>
class AsyncSettings {
public:
using Reader = std::function<T(const T&)>;
AsyncSettings(T initial, Reader reader, std::chrono::milliseconds interval)
: value_(initial), reader_(std::move(reader)), interval_(interval) {
static_assert(std::is_trivially_copyable_v<T>);
Refresh();
worker_ = std::thread([this] {
std::unique_lock<std::mutex> stop_lock(stop_mutex_);
while (!stopped_) {
if (wake_.wait_for(stop_lock, interval_, [this] { return stopped_; }))
break;
stop_lock.unlock();
Refresh();
stop_lock.lock();
}
});
}
~AsyncSettings() {
{
std::lock_guard<std::mutex> lock(stop_mutex_);
stopped_ = true;
}
wake_.notify_one();
if (worker_.joinable()) worker_.join();
}
AsyncSettings(const AsyncSettings&) = delete;
AsyncSettings& operator=(const AsyncSettings&) = delete;
T Get() const {
std::lock_guard<std::mutex> lock(value_mutex_);
return value_;
}
private:
void Refresh() {
try {
const T next = reader_(Get());
std::lock_guard<std::mutex> lock(value_mutex_);
value_ = next;
} catch (...) {
// A failed read retains the last completed settings. In particular,
// parsing must never throw through a guest rendering callback.
}
}
mutable std::mutex value_mutex_;
T value_;
Reader reader_;
std::chrono::milliseconds interval_;
std::mutex stop_mutex_;
std::condition_variable wake_;
bool stopped_ = false;
std::thread worker_;
};
} // namespace mocktail::vr::internal
+78
View File
@@ -0,0 +1,78 @@
#ifndef MOCKTAIL_VR_DESKTOP_MIRROR_H_
#define MOCKTAIL_VR_DESKTOP_MIRROR_H_
#include <algorithm>
#include <vulkan/vulkan.h>
namespace mocktail::vr::internal {
struct DesktopMirrorDispatch {
PFN_vkCmdPipelineBarrier barrier;
PFN_vkCmdClearColorImage clear;
PFN_vkCmdBlitImage blit;
};
// Record the whole copy, including layout restoration, into the submission's
// command buffer. The caller owns synchronization, begin/end and submission.
inline void RecordDesktopMirrorCommands(
VkCommandBuffer command, VkImage source, VkImageLayout source_layout,
VkExtent2D source_extent, VkImage destination, VkExtent2D destination_extent,
const DesktopMirrorDispatch& vk) {
VkImageMemoryBarrier barriers[2]{};
for (auto& barrier : barriers) {
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex =
VK_QUEUE_FAMILY_IGNORED;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT;
}
barriers[0].image = source;
barriers[0].oldLayout = source_layout;
barriers[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barriers[1].image = destination;
barriers[1].oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
barriers[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barriers[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vk.barrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2,
barriers);
const VkClearColorValue black{{0.f, 0.f, 0.f, 1.f}};
vk.clear(command, destination, barriers[1].newLayout, &black, 1,
&barriers[1].subresourceRange);
VkMemoryBarrier clear_barrier{};
clear_barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
clear_barrier.srcAccessMask = clear_barrier.dstAccessMask =
VK_ACCESS_TRANSFER_WRITE_BIT;
vk.barrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &clear_barrier, 0, nullptr, 0,
nullptr);
const double scale =
std::min(double(destination_extent.width) / source_extent.width,
double(destination_extent.height) / source_extent.height);
const int width = std::max(1, int(source_extent.width * scale));
const int height = std::max(1, int(source_extent.height * scale));
const int x = (int(destination_extent.width) - width) / 2;
const int y = (int(destination_extent.height) - height) / 2;
VkImageBlit region{};
region.srcSubresource =
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.srcOffsets[1] = {static_cast<int>(source_extent.width),
static_cast<int>(source_extent.height), 1};
region.dstOffsets[0] = {x, y, 0};
region.dstOffsets[1] = {x + width, y + height, 1};
vk.blit(command, source, barriers[0].newLayout, destination,
barriers[1].newLayout, 1, &region, VK_FILTER_NEAREST);
for (auto& barrier : barriers) {
std::swap(barrier.oldLayout, barrier.newLayout);
barrier.srcAccessMask = barrier.dstAccessMask;
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
}
vk.barrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 2,
barriers);
}
} // namespace mocktail::vr::internal
#endif // MOCKTAIL_VR_DESKTOP_MIRROR_H_
+18
View File
@@ -0,0 +1,18 @@
#ifndef MOCKTAIL_VR_EYE_RESIZE_BOUNDARY_H_
#define MOCKTAIL_VR_EYE_RESIZE_BOUNDARY_H_
namespace mocktail::vr::internal {
// Eye getters return borrowed render targets. Replacing those targets after
// either eye has been requested can invalidate work recorded earlier in the
// same frame, even if all previously submitted GPU work has finished.
class EyeResizeBoundary {
public:
void OnPresent() { presented_ = true; eyes_used_ = false; }
void OnEyeGetter() { eyes_used_ = true; }
bool CanResize() const { return presented_ && !eyes_used_; }
private:
bool presented_ = false;
bool eyes_used_ = false;
};
} // namespace mocktail::vr::internal
#endif
+226
View File
@@ -0,0 +1,226 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <vector>
namespace mocktail::vr::internal {
struct QuantizedGazeCenter {unsigned x=0,y=0;};
inline QuantizedGazeCenter QuantizeGazeCenter(unsigned width,unsigned height,float x,float y) {
if(!width || !height || !std::isfinite(x) || !std::isfinite(y))return {};
return {unsigned(std::round((std::clamp(x,-1.f,1.f)+1.f)*.5f*width*4.f)),
unsigned(std::round((1.f-std::clamp(y,-1.f,1.f))*.5f*height*4.f))};
}
inline float ClampGazeRadius(float radius) {
return std::isfinite(radius)?std::clamp(radius,.08f,.35f):.22f;
}
inline bool ProjectGazeNdc(const float* gaze,const float* eye,const float* fov,float* x,float* y) {
if(!gaze || !eye || !fov || !x || !y)return false;
auto rotate=[](const float* q,const float* v,float* out) {
float length=0;for(int i=0;i<4;++i){if(!std::isfinite(q[i]))return false;length+=q[i]*q[i];}
if(length<1e-8f)return false;
const float inverse=1.f/std::sqrt(length);
const float a=q[0]*inverse,b=q[1]*inverse,c=q[2]*inverse,d=q[3]*inverse;
const float tx=2.f*(b*v[2]-c*v[1]),ty=2.f*(c*v[0]-a*v[2]),tz=2.f*(a*v[1]-b*v[0]);
out[0]=v[0]+d*tx+b*tz-c*ty;out[1]=v[1]+d*ty+c*tx-a*tz;out[2]=v[2]+d*tz+a*ty-b*tx;
return true;
};
const float forward[3]={0,0,-1};float local[3],world[3];
const float inverse_eye[4]={-eye[0],-eye[1],-eye[2],eye[3]};
if(!rotate(gaze,forward,world) || !rotate(inverse_eye,world,local) || local[2]>=-1e-5f)return false;
const float left=std::tan(fov[0]),right=std::tan(fov[1]),up=std::tan(fov[2]),down=std::tan(fov[3]);
if(!std::isfinite(left+right+up+down) || right<=left || up<=down)return false;
*x=2.f*(local[0]/-local[2]-left)/(right-left)-1.f;
*y=2.f*(local[1]/-local[2]-down)/(up-down)-1.f;
return std::isfinite(*x)&&std::isfinite(*y);
}
// OpenXR eye-foveation centers use NDC (+Y up). Vulkan framebuffer/image rows
// run downwards. Preserve a generous full-density disk around the actual gaze;
// reduce shading only outside it. Density maps store reciprocal fragment size.
inline void FillGazeDensityMap(std::vector<std::uint8_t>& pixels,unsigned width,unsigned height,
float ndc_x,float ndc_y,int level=2,float radius=.22f,
float region_half_width=0.f,float region_half_height=0.f) {
pixels.assign(std::size_t(width)*height*2,255);
if(!width || !height || !std::isfinite(ndc_x) || !std::isfinite(ndc_y) || level<=0)return;
// Quarter-cell quantization avoids uploading new maps for tiny gaze jitter.
// Its maximum position error is well inside the cell-diagonal guard below.
const auto center=QuantizeGazeCenter(width,height,ndc_x,ndc_y);
const float cx=center.x/(width*4.f),cy=center.y/(height*4.f);
const float inner=ClampGazeRadius(radius);
// Performance reduces detail sooner outside the same protected focus disk.
const float outer=inner+(level==1?.36f:level==2?.26f:.10f);
const float peripheral_work=level==1?.25f:.0625f;
// A scalar ramp is quantized by the GPU into a few hard rings. Mix adjacent
// supported fragment areas over a stable spatial pattern instead. This gives
// intermediate average shading densities without temporal random noise or a
// second full-resolution render. Alternate the elongated fragment direction
// so intermediate stages do not consistently favour horizontal edges.
constexpr float work[]={1.f,.5f,.25f,.125f,.0625f};
constexpr unsigned sizes[][2]={{1,1},{1,2},{2,2},{2,4},{4,4}};
constexpr unsigned threshold[4][4]={{0,8,2,10},{12,4,14,6},{3,11,1,9},{15,7,13,5}};
// Each map cell covers an area, not a point. Extend the full-detail region
// by a cell diagonal so a gaze near a cell boundary cannot land in a coarse
// fragment just outside the mathematical disk.
const float margin=std::hypot(1.f/width,1.f/height);
const float inner_squared=(inner+margin)*(inner+margin);
const float outer_squared=(outer+margin)*(outer+margin);
const auto peripheral_density=static_cast<std::uint8_t>(level==1?127:63);
for(unsigned y=0;y<height;++y)for(unsigned x=0;x<width;++x) {
// A tiled GPU samples density at its region centre and applies it to the
// entire region. Protect every region that could intersect the focus, not
// just the tiny map cell containing the gaze. The bound comes from the
// device's maxFragmentDensityTexelSize in framebuffer pixels.
const float dx=std::max(0.f,std::abs((x+.5f)/width-cx)-region_half_width);
const float dy=std::max(0.f,std::abs((y+.5f)/height-cy)-region_half_height);
// Flat regions need no square root or density-stage search. The output
// was initialized to full detail above; the peripheral reciprocal is exact.
// Keep the existing hypot/ramp calculation throughout the transition.
const float squared=dx*dx+dy*dy;
if(squared<=inner_squared)continue;
if(squared>=outer_squared) {
const auto index=(std::size_t(y)*width+x)*2;
pixels[index]=pixels[index+1]=peripheral_density;
continue;
}
const float distance=std::hypot(dx,dy);
float t=std::clamp((distance-inner-margin)/(outer-inner),0.f,1.f);
t=t*t*(3.f-2.f*t);
const float target=1.f-t*(1.f-peripheral_work);
unsigned stage=0;
while(stage<4 && target<work[stage+1])++stage;
if(stage<4) {
const float blend=(work[stage]-target)/(work[stage]-work[stage+1]);
if(blend>(threshold[y%4][x%4]+.5f)/16.f)++stage;
}
const unsigned flip=(x+y)&1u;
const auto index=(std::size_t(y)*width+x)*2;
// Round density down: 128/255 requests an area smaller than 2 pixels,
// which a conforming driver may clamp to 1; 127/255 actually permits 2.
pixels[index]=static_cast<std::uint8_t>(255u/sizes[stage][flip]);
pixels[index+1]=static_cast<std::uint8_t>(255u/sizes[stage][1u-flip]);
}
}
inline std::vector<std::uint8_t> GazeDensityMap(unsigned width,unsigned height,
float ndc_x,float ndc_y,int level=2,float radius=.22f,
float region_half_width=0.f,float region_half_height=0.f) {
std::vector<std::uint8_t> pixels;
FillGazeDensityMap(pixels,width,height,ndc_x,ndc_y,level,radius,region_half_width,region_half_height);
return pixels;
}
struct FramebufferDensityLayout {
unsigned map_width=0,map_height=0,width=0,height=0;
unsigned texel_width=0,texel_height=0,active_width=0,active_height=0;
bool valid=false;
};
// Turnip samples at pixel>>ceil(log2(framebuffer/map)), clamped to the
// advertised power-of-two texel limits. Allocation padding is not scene UV.
inline FramebufferDensityLayout TurnipDensityLayout(unsigned map_width,unsigned map_height,
unsigned width,unsigned height,unsigned min_width,unsigned min_height,
unsigned max_width,unsigned max_height) {
FramebufferDensityLayout result{map_width,map_height,width,height};
const auto power=[](unsigned value){return value && !(value&(value-1));};
if(!map_width || !map_height || !width || !height || !power(min_width) ||
!power(min_height) || !power(max_width) || !power(max_height) ||
min_width>max_width || min_height>max_height)return result;
const auto step=[](unsigned pixels,unsigned cells,unsigned minimum,unsigned maximum) {
unsigned value=minimum;
while(value<maximum && value<pixels/cells)value*=2;
return value;
};
result.texel_width=step(width,map_width,min_width,max_width);
result.texel_height=step(height,map_height,min_height,max_height);
result.active_width=1+(width-1)/result.texel_width;
result.active_height=1+(height-1)/result.texel_height;
result.valid=result.active_width<=map_width && result.active_height<=map_height;
return result;
}
inline QuantizedGazeCenter QuantizeFramebufferGaze(const FramebufferDensityLayout& layout,
float x,float y) {
if(!layout.valid || !std::isfinite(x) || !std::isfinite(y))return {};
return {unsigned(std::round((std::clamp(x,-1.f,1.f)+1.f)*.5f*layout.width*4.f/layout.texel_width)),
unsigned(std::round((1.f-std::clamp(y,-1.f,1.f))*.5f*layout.height*4.f/layout.texel_height))};
}
struct GazeDensityMapKey {
unsigned x=0,y=0,width=0,height=0,map_width=0,map_height=0,texel_width=0,texel_height=0;
int level=0;float radius=0;
unsigned guard_width=0,guard_height=0,color_format=0,depth_format=0,samples=0,attachments=0;
bool operator==(const GazeDensityMapKey& other) const {
return x==other.x && y==other.y && width==other.width && height==other.height &&
map_width==other.map_width && map_height==other.map_height &&
texel_width==other.texel_width && texel_height==other.texel_height &&
level==other.level && radius==other.radius &&
guard_width==other.guard_width && guard_height==other.guard_height &&
color_format==other.color_format && depth_format==other.depth_format &&
samples==other.samples && attachments==other.attachments;
}
};
enum class GazeDensityCacheAction { kUpload,kReuse,kNeutral };
inline GazeDensityCacheAction DensityCacheAction(bool initialized,bool cached,
std::uint64_t uploaded_frame,std::uint64_t frame,const GazeDensityMapKey& previous,
const GazeDensityMapKey& requested) {
if(initialized && cached && previous==requested)return GazeDensityCacheAction::kReuse;
// A map already referenced in this frame cannot safely be overwritten for
// another extent. Its pass uses the neutral map until the next frame.
if(initialized && uploaded_frame==frame)return GazeDensityCacheAction::kNeutral;
return GazeDensityCacheAction::kUpload;
}
inline bool FillFramebufferGazeDensityMap(std::vector<std::uint8_t>& pixels,
const FramebufferDensityLayout& layout,float ndc_x,float ndc_y,int level,float radius,
float region_half_width_pixels,float region_half_height_pixels) {
pixels.assign(std::size_t(layout.map_width)*layout.map_height*2,255);
if(!layout.valid || !std::isfinite(ndc_x) || !std::isfinite(ndc_y) ||
!std::isfinite(region_half_width_pixels) || !std::isfinite(region_half_height_pixels) ||
region_half_width_pixels<0 || region_half_height_pixels<0)return false;
if(level<=0)return true;
const auto center=QuantizeFramebufferGaze(layout,ndc_x,ndc_y);
const float cx=float(center.x)*layout.texel_width/(4.f*layout.width);
const float cy=float(center.y)*layout.texel_height/(4.f*layout.height);
const float guard_x=region_half_width_pixels/layout.width;
const float guard_y=region_half_height_pixels/layout.height;
const float inner=ClampGazeRadius(radius);
const float outer=inner+(level==1?.36f:level==2?.26f:.10f);
const float margin=std::hypot(float(layout.texel_width)/layout.width,
float(layout.texel_height)/layout.height);
const float inner_squared=(inner+margin)*(inner+margin);
const float outer_squared=(outer+margin)*(outer+margin);
const float peripheral_work=level==1?.25f:.0625f;
const auto peripheral_density=static_cast<std::uint8_t>(level==1?127:63);
constexpr float work[]={1.f,.5f,.25f,.125f,.0625f};
constexpr unsigned sizes[][2]={{1,1},{1,2},{2,2},{2,4},{4,4}};
constexpr unsigned threshold[4][4]={{0,8,2,10},{12,4,14,6},{3,11,1,9},{15,7,13,5}};
for(unsigned y=0;y<layout.active_height;++y)for(unsigned x=0;x<layout.active_width;++x) {
const float px=(x+.5f)*layout.texel_width/layout.width;
const float py=(y+.5f)*layout.texel_height/layout.height;
const float dx=std::max(0.f,std::abs(px-cx)-guard_x);
const float dy=std::max(0.f,std::abs(py-cy)-guard_y);
const float squared=dx*dx+dy*dy;
if(squared<=inner_squared)continue;
const auto index=(std::size_t(y)*layout.map_width+x)*2;
if(squared>=outer_squared) {pixels[index]=pixels[index+1]=peripheral_density;continue;}
float t=std::clamp((std::hypot(dx,dy)-inner-margin)/(outer-inner),0.f,1.f);
t=t*t*(3.f-2.f*t);
const float target=1.f-t*(1.f-peripheral_work);
unsigned stage=0;
while(stage<4 && target<work[stage+1])++stage;
if(stage<4 && (work[stage]-target)/(work[stage]-work[stage+1])>
(threshold[y%4][x%4]+.5f)/16.f)++stage;
const unsigned flip=(x+y)&1u;
pixels[index]=static_cast<std::uint8_t>(255u/sizes[stage][flip]);
pixels[index+1]=static_cast<std::uint8_t>(255u/sizes[stage][1u-flip]);
}
return true;
}
// The driver advertises R16G16_SFLOAT density attachments. These exact binary
// reciprocals avoid UNORM rounding and cover every area generated above.
inline void FillHalfDensityMap(std::vector<std::uint16_t>& result,const std::vector<std::uint8_t>& pixels) {
result.resize(pixels.size());
for(std::size_t i=0;i<pixels.size();++i)
result[i]=pixels[i]==127?0x3800u:pixels[i]==63?0x3400u:0x3c00u;
}
inline std::vector<std::uint16_t> HalfDensityMap(const std::vector<std::uint8_t>& pixels) {
std::vector<std::uint16_t> result;
FillHalfDensityMap(result,pixels);
return result;
}
}
@@ -0,0 +1,56 @@
#pragma once
#include <vulkan/vulkan.h>
namespace mocktail::vr {
inline bool IsFoveationSceneSampleCount(VkSampleCountFlagBits samples) {
return samples == VK_SAMPLE_COUNT_1_BIT || samples == VK_SAMPLE_COUNT_2_BIT ||
samples == VK_SAMPLE_COUNT_4_BIT || samples == VK_SAMPLE_COUNT_8_BIT;
}
// Preserve the native MSAA attachments and resolve. Only the extra density map
// is single-sampled; changing the scene images or pipeline sample count here
// would invalidate native framebuffers and memory allocations.
inline bool IsFoveationScenePassCompatible(const VkRenderPassCreateInfo& info) {
if (info.pNext || info.flags || info.subpassCount != 1 || !info.pSubpasses ||
!info.pAttachments) {
return false;
}
const auto& subpass = info.pSubpasses[0];
if (subpass.flags || subpass.pipelineBindPoint != VK_PIPELINE_BIND_POINT_GRAPHICS ||
subpass.colorAttachmentCount != 1 || !subpass.pColorAttachments ||
!subpass.pDepthStencilAttachment || subpass.inputAttachmentCount ||
subpass.preserveAttachmentCount) {
return false;
}
const auto color = subpass.pColorAttachments[0].attachment;
const auto depth = subpass.pDepthStencilAttachment->attachment;
if (color >= info.attachmentCount || depth >= info.attachmentCount || color == depth) {
return false;
}
const auto samples = info.pAttachments[color].samples;
if (!IsFoveationSceneSampleCount(samples) || info.pAttachments[depth].samples != samples) {
return false;
}
const auto resolve = subpass.pResolveAttachments ?
subpass.pResolveAttachments[0].attachment : VK_ATTACHMENT_UNUSED;
if (resolve != VK_ATTACHMENT_UNUSED) {
if (samples == VK_SAMPLE_COUNT_1_BIT || info.attachmentCount != 3 ||
resolve >= info.attachmentCount || resolve == color || resolve == depth ||
info.pAttachments[resolve].samples != VK_SAMPLE_COUNT_1_BIT ||
info.pAttachments[resolve].format != info.pAttachments[color].format) {
return false;
}
} else if (info.attachmentCount != 2) {
return false;
}
for (unsigned i = 0; i < info.attachmentCount; ++i) {
if (info.pAttachments[i].flags) {
return false;
}
}
return true;
}
} // namespace mocktail::vr
@@ -0,0 +1,85 @@
#pragma once
#include <array>
#include <cstdint>
#include <optional>
#include <string_view>
namespace mocktail::vr {
struct FoveationTileDriverIdentity {
std::uint32_t vendor_id = 0;
std::uint32_t device_id = 0;
std::uint32_t driver_id = 0;
std::uint32_t driver_version = 0;
std::string_view driver_name;
std::string_view driver_info;
std::array<std::uint8_t, 16> driver_uuid{};
};
// Values describe the complete render pass, including its density attachment.
// Attachment formats use VkFormat's numeric values without requiring Vulkan.
struct FoveationTilePassIdentity {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t color_format = 0;
std::uint32_t depth_format = 0;
std::uint32_t color_samples = 0;
std::uint32_t depth_samples = 0;
std::uint32_t framebuffer_layers = 0;
std::uint32_t subpass_count = 0;
std::uint32_t total_attachment_count = 0;
std::uint32_t color_attachment_count = 0;
std::uint32_t gmem_attachment_count = 0;
std::uint32_t resolve_attachment_mask = 0;
std::uint32_t input_attachment_mask = 0;
std::uint32_t feedback_attachment_mask = 0;
std::uint32_t view_mask = 0;
bool has_depth_attachment = false;
bool has_density_attachment = false;
bool density_offsets_enabled = false;
bool multisampled_render_to_single_sampled = false;
bool custom_resolve = false;
bool has_shading_rate_attachment = false;
};
struct FoveationTileBound {
std::uint32_t width;
std::uint32_t height;
};
// A calibrated entry for one driver and pass tuple, not a hardware maximum.
// Unknown identities retain the caller's conservative policy. This exact entry
// passed 588 GPU focus cases; the matching driver ELF forces FDM's AVOID_CCU
// layout for all calibrated load/store combinations and the autotune path.
inline std::optional<FoveationTileBound> KnownFoveationTileBound(
const FoveationTileDriverIdentity& driver,
const FoveationTilePassIdentity& pass) {
constexpr std::array<std::uint8_t, 16> kDriverUuid{
0x58, 0x6b, 0x9f, 0xa4, 0x51, 0x4c, 0xb8, 0x8b,
0x3c, 0x11, 0xa9, 0x68, 0xef, 0x4d, 0x62, 0xe4};
if (driver.vendor_id != 0x5143 || driver.device_id != 0x43051401 ||
driver.driver_id != 18 || driver.driver_version != 109060195 ||
driver.driver_name != "turnip Mesa driver" ||
driver.driver_info != "Mesa 26.3.0-devel (git-abc426bb62)" ||
driver.driver_uuid != kDriverUuid) {
return std::nullopt;
}
if (pass.width != 2160 || pass.height != 2160 ||
pass.color_format != 37 || pass.depth_format != 126 ||
pass.color_samples != 1 || pass.depth_samples != 1 ||
pass.framebuffer_layers != 1 || pass.subpass_count != 1 ||
pass.total_attachment_count != 3 || pass.color_attachment_count != 1 ||
pass.gmem_attachment_count != 2 ||
pass.resolve_attachment_mask || pass.input_attachment_mask ||
pass.feedback_attachment_mask || pass.view_mask ||
!pass.has_depth_attachment || !pass.has_density_attachment ||
pass.density_offsets_enabled ||
pass.multisampled_render_to_single_sampled || pass.custom_resolve ||
pass.has_shading_rate_attachment) {
return std::nullopt;
}
return FoveationTileBound{768, 448};
}
} // namespace mocktail::vr
+298
View File
@@ -0,0 +1,298 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <vulkan/vulkan.h>
namespace mocktail::vr {
struct Fsr3DeviceFeatureSupport {
bool shader_float16 = false;
bool shader_int16 = false;
bool float16_extension_available = false;
bool shader_storage_image_extended_formats = false;
bool shader_storage_image_write_without_format = false;
// The effective instance/device API version, not merely a header version.
std::uint32_t api_version = VK_API_VERSION_1_0;
};
// A call-scoped, owned device-create feature chain. This object must remain
// alive until xrCreateVulkanDeviceKHR/vkCreateDevice returns. Queue descriptions
// and layer names stay borrowed and unmodified, as in the original call.
// Unsupported or unfamiliar chains return the original create info verbatim.
class Fsr3DeviceFeatures final {
// The runtime normalizer reuses the same typed ownership/whitelist without
// relaxing the guest feature-chain validation performed by this class.
friend class Fsr3RuntimeTimelineFeatures;
public:
Fsr3DeviceFeatures(const VkDeviceCreateInfo& original,
const Fsr3DeviceFeatureSupport& support)
: original_(original), info_(original) {
Build(support);
}
Fsr3DeviceFeatures(const Fsr3DeviceFeatures&) = delete;
Fsr3DeviceFeatures& operator=(const Fsr3DeviceFeatures&) = delete;
Fsr3DeviceFeatures(Fsr3DeviceFeatures&&) = delete;
Fsr3DeviceFeatures& operator=(Fsr3DeviceFeatures&&) = delete;
const VkDeviceCreateInfo& create_info() const {
return enabled_ ? info_ : original_;
}
bool fp16_enabled() const { return enabled_; }
bool changed() const { return enabled_ && changed_; }
const char* reason() const { return reason_; }
private:
struct Record {
virtual ~Record() = default;
virtual void* address() = 0;
virtual void next(void*) = 0;
};
template<class T> struct TypedRecord final : Record {
explicit TypedRecord(const T& source) : value(source) {}
void* address() override { return &value; }
void next(void* value_next) override { value.pNext = value_next; }
T value;
};
template<class T> T* Clone(const T& value) {
auto record = std::make_unique<TypedRecord<T>>(value);
auto* result = &record->value;
chain_.push_back(std::move(record));
return result;
}
static constexpr std::uint64_t Bit(unsigned value) { return 1ull << value; }
static constexpr std::uint64_t Range(unsigned first, unsigned last) {
return ((1ull << (last + 1)) - 1) & ~((1ull << first) - 1);
}
bool CloneKnown(const VkBaseInStructure* source, std::uint64_t* families) {
std::uint64_t used = 0;
#define FEATURE(type, tag, mask) \
case tag: { \
used = (mask); \
if (*families & used) return false; \
Clone(*reinterpret_cast<const type*>(source)); \
break; \
}
switch (source->sType) {
case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2: {
used = Bit(0);
if (*families & used) return false;
auto* value = Clone(*reinterpret_cast<const VkPhysicalDeviceFeatures2*>(source));
base_features_ = &value->features;
features2_ = true;
break;
}
FEATURE(VkPhysicalDeviceVulkan11Features,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, Range(1, 6))
case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES: {
used = Range(7, 19);
if (*families & used) return false;
auto* value = Clone(*reinterpret_cast<const VkPhysicalDeviceVulkan12Features*>(source));
float16_ = &value->shaderFloat16;
break;
}
FEATURE(VkPhysicalDeviceVulkan13Features,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, Range(20, 32))
FEATURE(VkPhysicalDevice16BitStorageFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, Bit(1))
FEATURE(VkPhysicalDeviceMultiviewFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES, Bit(2))
FEATURE(VkPhysicalDeviceVariablePointersFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VARIABLE_POINTERS_FEATURES, Bit(3))
FEATURE(VkPhysicalDeviceProtectedMemoryFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROTECTED_MEMORY_FEATURES, Bit(4))
FEATURE(VkPhysicalDeviceSamplerYcbcrConversionFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, Bit(5))
FEATURE(VkPhysicalDeviceShaderDrawParametersFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, Bit(6))
FEATURE(VkPhysicalDevice8BitStorageFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, Bit(7))
FEATURE(VkPhysicalDeviceShaderAtomicInt64Features,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES, Bit(8))
case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES: {
used = Bit(9);
if (*families & used) return false;
auto* value = Clone(*reinterpret_cast<const VkPhysicalDeviceShaderFloat16Int8Features*>(source));
float16_ = &value->shaderFloat16;
break;
}
FEATURE(VkPhysicalDeviceDescriptorIndexingFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, Bit(10))
FEATURE(VkPhysicalDeviceScalarBlockLayoutFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES, Bit(11))
FEATURE(VkPhysicalDeviceImagelessFramebufferFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGELESS_FRAMEBUFFER_FEATURES, Bit(12))
FEATURE(VkPhysicalDeviceUniformBufferStandardLayoutFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_UNIFORM_BUFFER_STANDARD_LAYOUT_FEATURES, Bit(13))
FEATURE(VkPhysicalDeviceShaderSubgroupExtendedTypesFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_EXTENDED_TYPES_FEATURES, Bit(14))
FEATURE(VkPhysicalDeviceSeparateDepthStencilLayoutsFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES, Bit(15))
FEATURE(VkPhysicalDeviceHostQueryResetFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES, Bit(16))
FEATURE(VkPhysicalDeviceTimelineSemaphoreFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES, Bit(17))
FEATURE(VkPhysicalDeviceBufferDeviceAddressFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES, Bit(18))
FEATURE(VkPhysicalDeviceVulkanMemoryModelFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES, Bit(19))
FEATURE(VkPhysicalDeviceImageRobustnessFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES, Bit(20))
FEATURE(VkPhysicalDeviceInlineUniformBlockFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INLINE_UNIFORM_BLOCK_FEATURES, Bit(21))
FEATURE(VkPhysicalDevicePipelineCreationCacheControlFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES, Bit(22))
FEATURE(VkPhysicalDevicePrivateDataFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIVATE_DATA_FEATURES, Bit(23))
FEATURE(VkPhysicalDeviceShaderDemoteToHelperInvocationFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES, Bit(24))
FEATURE(VkPhysicalDeviceShaderTerminateInvocationFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES, Bit(25))
FEATURE(VkPhysicalDeviceSubgroupSizeControlFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES, Bit(26))
FEATURE(VkPhysicalDeviceSynchronization2Features,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES, Bit(27))
FEATURE(VkPhysicalDeviceTextureCompressionASTCHDRFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TEXTURE_COMPRESSION_ASTC_HDR_FEATURES, Bit(28))
FEATURE(VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_WORKGROUP_MEMORY_FEATURES, Bit(29))
FEATURE(VkPhysicalDeviceDynamicRenderingFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES, Bit(30))
FEATURE(VkPhysicalDeviceShaderIntegerDotProductFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_INTEGER_DOT_PRODUCT_FEATURES, Bit(31))
FEATURE(VkPhysicalDeviceMaintenance4Features,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_4_FEATURES, Bit(32))
FEATURE(VkPhysicalDeviceFragmentDensityMapFeaturesEXT,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT, Bit(33))
FEATURE(VkPhysicalDeviceFragmentDensityMap2FeaturesEXT,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_2_FEATURES_EXT, Bit(34))
FEATURE(VkPhysicalDeviceHostImageCopyFeaturesEXT,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES_EXT, Bit(35))
FEATURE(VkPhysicalDeviceFragmentShadingRateFeaturesKHR,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_FEATURES_KHR, Bit(36))
default: return false;
}
#undef FEATURE
*families |= used;
return true;
}
bool CopyExtensionNames() {
if (original_.enabledExtensionCount && !original_.ppEnabledExtensionNames)
return false;
for (std::uint32_t i = 0; i < original_.enabledExtensionCount; ++i) {
const char* name = original_.ppEnabledExtensionNames[i];
if (!name) return false;
std::size_t length = 0;
while (length < VK_MAX_EXTENSION_NAME_SIZE && name[length]) ++length;
if (!length || length == VK_MAX_EXTENSION_NAME_SIZE) return false;
const std::string value(name, length);
if (std::find(extension_names_.begin(), extension_names_.end(), value) == extension_names_.end())
extension_names_.push_back(value);
else changed_ = true;
}
return true;
}
void Build(const Fsr3DeviceFeatureSupport& support) {
if (!support.shader_float16 || !support.shader_int16) {
reason_ = "shaderFloat16 or shaderInt16 unsupported";
return;
}
if (original_.sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO) {
reason_ = "invalid device create info";
return;
}
std::uint64_t families = 0;
std::vector<const void*> seen;
for (auto* next = static_cast<const VkBaseInStructure*>(original_.pNext);
next; next = next->pNext) {
if (seen.size() == 64 || std::find(seen.begin(), seen.end(), next) != seen.end() ||
!CloneKnown(next, &families)) {
reason_ = "unknown, cyclic, or duplicate feature chain";
return;
}
if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES)
// The Vulkan11 aggregate structure itself was introduced in1.2.
minimum_api_ = std::max(minimum_api_, VK_API_VERSION_1_2);
if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES)
minimum_api_ = std::max(minimum_api_, VK_API_VERSION_1_2);
if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES)
minimum_api_ = std::max(minimum_api_, VK_API_VERSION_1_3);
seen.push_back(next);
}
if (features2_ && original_.pEnabledFeatures) {
reason_ = "duplicate base feature records";
return;
}
if (support.api_version < minimum_api_) {
reason_ = "core feature record exceeds effective Vulkan API";
return;
}
if (support.api_version < VK_API_VERSION_1_2 && !support.float16_extension_available) {
reason_ = "shaderFloat16 extension unavailable";
return;
}
if (!CopyExtensionNames()) {
reason_ = "invalid device extension list";
return;
}
if (support.api_version < VK_API_VERSION_1_2) {
const std::string extension = VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME;
if (std::find(extension_names_.begin(), extension_names_.end(), extension) == extension_names_.end()) {
extension_names_.push_back(extension);
changed_ = true;
}
}
if (!float16_) {
VkPhysicalDeviceShaderFloat16Int8Features value{};
value.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES;
float16_ = &Clone(value)->shaderFloat16;
changed_ = true;
}
if (!base_features_) {
if (original_.pEnabledFeatures) features_ = *original_.pEnabledFeatures;
base_features_ = &features_;
info_.pEnabledFeatures = base_features_;
}
changed_ |= *float16_ != VK_TRUE || base_features_->shaderInt16 != VK_TRUE;
*float16_ = VK_TRUE;
base_features_->shaderInt16 = VK_TRUE;
if (support.shader_storage_image_extended_formats) {
changed_ |= base_features_->shaderStorageImageExtendedFormats != VK_TRUE;
base_features_->shaderStorageImageExtendedFormats = VK_TRUE;
}
if (support.shader_storage_image_write_without_format) {
changed_ |= base_features_->shaderStorageImageWriteWithoutFormat != VK_TRUE;
base_features_->shaderStorageImageWriteWithoutFormat = VK_TRUE;
}
for (std::size_t i = 0; i < chain_.size(); ++i)
chain_[i]->next(i + 1 < chain_.size() ? chain_[i + 1]->address() : nullptr);
info_.pNext = chain_.empty() ? nullptr : chain_[0]->address();
for (const auto& name : extension_names_) extension_pointers_.push_back(name.c_str());
info_.enabledExtensionCount = static_cast<std::uint32_t>(extension_pointers_.size());
info_.ppEnabledExtensionNames = extension_pointers_.empty() ? nullptr : extension_pointers_.data();
enabled_ = true;
reason_ = "enabled";
}
const VkDeviceCreateInfo& original_;
VkDeviceCreateInfo info_;
std::vector<std::unique_ptr<Record>> chain_;
std::vector<std::string> extension_names_;
std::vector<const char*> extension_pointers_;
VkPhysicalDeviceFeatures features_{};
VkPhysicalDeviceFeatures* base_features_ = nullptr;
VkBool32* float16_ = nullptr;
bool features2_ = false;
std::uint32_t minimum_api_ = VK_API_VERSION_1_0;
bool enabled_ = false;
bool changed_ = false;
const char* reason_ = "disabled";
};
} // namespace mocktail::vr
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "mocktail/vr/fsr3_upscaler.h"
#include <memory>
namespace mocktail::vr {
// A fixed-size temporal focus region with a cheap full-image periphery.
// The owner retires pending GPU users before reusing descriptors or resetting.
class Fsr3GazeTransport {
public:
struct BeginInfo {
VkImage encoded_source{}; // TRANSFER_SRC_OPTIMAL; encoded UNORM or sRGB.
Fsr3Upscaler::Image depth; // Full-input size, SHADER_READ_ONLY_OPTIMAL.
Fsr3Upscaler::Image motion; // Optional native clip-NDC delta, same layout/size.
float gaze_x=.5f, gaze_y=.5f; // Normalized image UV, +Y down.
bool reset=false;
};
struct Dimensions {
std::uint32_t input_width{},input_height{},output_width{},output_height{};
};
Fsr3GazeTransport();
~Fsr3GazeTransport();
Fsr3GazeTransport(const Fsr3GazeTransport&)=delete;
Fsr3GazeTransport& operator=(const Fsr3GazeTransport&)=delete;
bool Prepare(VkDevice,PFN_vkGetDeviceProcAddr,
const VkPhysicalDeviceMemoryProperties&,VkFormat encoded_format,
std::uint32_t full_input_width,std::uint32_t full_input_height,
std::uint32_t full_output_width,std::uint32_t full_output_height,
float sharp_radius);
bool Begin(VkCommandBuffer,int eye,const BeginInfo&);
void Finish(VkCommandBuffer,int eye);
Dimensions dimensions()const;
float vertical_fov_scale()const;
bool reset_history(int eye)const;
Fsr3Upscaler::Image color(int eye)const;
Fsr3Upscaler::Image depth(int eye)const;
Fsr3Upscaler::Image motion(int eye)const;
Fsr3Upscaler::Image output(int eye)const;
VkImage encoded(int eye)const;
VkFormat encoded_format()const;
void Reset();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
}
+135
View File
@@ -0,0 +1,135 @@
#pragma once
#include <atomic>
#include <cstring>
#include "mocktail/vr/fsr3_device_features.h"
namespace mocktail::vr {
// SteamVR can append a TimelineSemaphore feature record even when a guest
// already supplied Vulkan12Features. Fold that single promoted feature into
// its aggregate immediately before the host vkCreateDevice call. Every known
// record is cloned so no borrowed pNext pointer is rewritten. Unknown records,
// conflicting records, and malformed chains pass through verbatim.
class Fsr3RuntimeTimelineFeatures final {
public:
explicit Fsr3RuntimeTimelineFeatures(const VkDeviceCreateInfo& original)
: owned_(original, Fsr3DeviceFeatureSupport{}) {
if (original.sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO) return;
const VkPhysicalDeviceVulkan12Features* aggregate = nullptr;
const VkPhysicalDeviceTimelineSemaphoreFeatures* timeline = nullptr;
std::vector<const VkBaseInStructure*> records;
for (auto* record = static_cast<const VkBaseInStructure*>(original.pNext);
record; record = record->pNext) {
if (records.size() == 64 ||
std::find(records.begin(), records.end(), record) != records.end()) return;
if (record->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES) {
if (aggregate) return;
aggregate = reinterpret_cast<const VkPhysicalDeviceVulkan12Features*>(record);
}
if (record->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES) {
if (timeline) return;
timeline = reinterpret_cast<const VkPhysicalDeviceTimelineSemaphoreFeatures*>(record);
}
records.push_back(record);
}
if (!aggregate || !timeline) return;
std::uint64_t families = 0;
VkPhysicalDeviceVulkan12Features* cloned_aggregate = nullptr;
for (auto* record : records) {
if (record == reinterpret_cast<const VkBaseInStructure*>(timeline)) continue;
if (!owned_.CloneKnown(record, &families)) return;
if (record == reinterpret_cast<const VkBaseInStructure*>(aggregate))
cloned_aggregate = static_cast<VkPhysicalDeviceVulkan12Features*>(
owned_.chain_.back()->address());
}
if (!cloned_aggregate || (owned_.features2_ && original.pEnabledFeatures)) return;
cloned_aggregate->timelineSemaphore =
aggregate->timelineSemaphore || timeline->timelineSemaphore ? VK_TRUE : VK_FALSE;
for (std::size_t i = 0; i < owned_.chain_.size(); ++i)
owned_.chain_[i]->next(i + 1 < owned_.chain_.size()
? owned_.chain_[i + 1]->address() : nullptr);
owned_.info_.pNext = owned_.chain_.front()->address();
changed_ = true;
}
Fsr3RuntimeTimelineFeatures(const Fsr3RuntimeTimelineFeatures&) = delete;
Fsr3RuntimeTimelineFeatures& operator=(const Fsr3RuntimeTimelineFeatures&) = delete;
const VkDeviceCreateInfo& create_info() const {
return changed_ ? owned_.info_ : owned_.original_;
}
bool changed() const { return changed_; }
private:
Fsr3DeviceFeatures owned_;
bool changed_ = false;
};
// Only calls made in this thread's scope normalize vkCreateDevice. Retained
// callbacks and calls on other threads use an atomic snapshot of the raw host
// dispatch, without normalization. The backend must clear that fallback before
// unloading its private host Vulkan library.
class ScopedFsr3VulkanDeviceCreate final {
public:
ScopedFsr3VulkanDeviceCreate(PFN_vkGetInstanceProcAddr raw, VkInstance instance)
: raw_(raw), previous_(current_) {
if (raw_)
create_ = reinterpret_cast<PFN_vkCreateDevice>(raw_(instance, "vkCreateDevice"));
if (create_ && create_ != &CreateDevice) {
current_ = this;
std::shared_ptr<const RawDispatch> dispatch =
std::make_shared<RawDispatch>(RawDispatch{raw_, create_});
std::atomic_store_explicit(&fallback_, std::move(dispatch), std::memory_order_release);
} else create_ = nullptr;
}
~ScopedFsr3VulkanDeviceCreate() {
if (current_ == this) current_ = previous_;
}
ScopedFsr3VulkanDeviceCreate(const ScopedFsr3VulkanDeviceCreate&) = delete;
ScopedFsr3VulkanDeviceCreate& operator=(const ScopedFsr3VulkanDeviceCreate&) = delete;
PFN_vkGetInstanceProcAddr get_instance_proc_addr() const {
return create_ ? &GetInstanceProcAddr : raw_;
}
bool active() const { return create_ != nullptr; }
static void ClearFallback() {
std::atomic_store_explicit(&fallback_, std::shared_ptr<const RawDispatch>{},
std::memory_order_release);
}
private:
struct RawDispatch {
PFN_vkGetInstanceProcAddr gipa = nullptr;
PFN_vkCreateDevice create = nullptr;
};
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL GetInstanceProcAddr(
VkInstance instance, const char* name) {
auto* scope = current_;
if (!scope || !scope->raw_) {
const auto dispatch = std::atomic_load_explicit(&fallback_, std::memory_order_acquire);
return dispatch && dispatch->gipa ? dispatch->gipa(instance, name) : nullptr;
}
if (name && std::strcmp(name, "vkCreateDevice") == 0)
return reinterpret_cast<PFN_vkVoidFunction>(&CreateDevice);
return scope->raw_(instance, name);
}
static VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(
VkPhysicalDevice physical, const VkDeviceCreateInfo* info,
const VkAllocationCallbacks* allocator, VkDevice* device) {
auto* scope = current_;
if (!scope || !scope->create_) {
const auto dispatch = std::atomic_load_explicit(&fallback_, std::memory_order_acquire);
return dispatch && dispatch->create
? dispatch->create(physical, info, allocator, device)
: VK_ERROR_INITIALIZATION_FAILED;
}
if (!info) return scope->create_(physical, info, allocator, device);
Fsr3RuntimeTimelineFeatures normalized(*info);
return scope->create_(physical, &normalized.create_info(), allocator, device);
}
inline static thread_local ScopedFsr3VulkanDeviceCreate* current_ = nullptr;
inline static std::shared_ptr<const RawDispatch> fallback_;
PFN_vkGetInstanceProcAddr raw_ = nullptr;
PFN_vkCreateDevice create_ = nullptr;
ScopedFsr3VulkanDeviceCreate* previous_ = nullptr;
};
} // namespace mocktail::vr
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <array>
#include <cstdint>
#include <unordered_map>
#include <vulkan/vulkan.h>
#include "mocktail/vr/fsr3_scene_inputs.h"
namespace mocktail::vr {
// Owns depth snapshots, recorded before the guest reuses its scene depth.
// All methods are serialized by the backend and use its single graphics queue.
class Fsr3SceneCapture {
public:
struct Eye {
VkImage depth{};
VkDeviceMemory memory{};
VkFormat format{};
std::uint32_t width{},height{};
std::uint64_t submitted_frame{};
bool initialized{};
};
struct Motion {
VkImage image{};VkDeviceMemory memory{};VkFormat format{};
std::uint32_t width{},height{};std::uint64_t submitted_frame{};
float previous_uv_scale_x=-.5f,previous_uv_scale_y=.5f;
};
~Fsr3SceneCapture() { Reset(); }
bool NeedsResize(const TemporalSceneInputs&) const;
bool Record(VkDevice, PFN_vkGetDeviceProcAddr,
const VkPhysicalDeviceMemoryProperties&, VkCommandBuffer,
const TemporalSceneInputs&, std::uint64_t frame);
bool RecordMotion(VkDevice, PFN_vkGetDeviceProcAddr,
const VkPhysicalDeviceMemoryProperties&, VkCommandBuffer,
const TemporalMotionInputs&, std::uint64_t frame);
void Submitted(std::uint32_t,const VkCommandBuffer*);
void Discard(VkCommandBuffer);
bool HasUnsubmitted() const;
const Eye* Get(int eye,std::uint64_t frame,std::uint32_t width,
std::uint32_t height) const;
const Motion* GetMotion(int eye,std::uint64_t frame,std::uint32_t width,
std::uint32_t height) const;
void Reset();
private:
template<class T> T Proc(const char* name) const {
return reinterpret_cast<T>(proc_(device_,name));
}
bool Create(Eye&,const TemporalSceneInputs&,
const VkPhysicalDeviceMemoryProperties&);
bool Allocate(VkImage&,VkDeviceMemory&,VkFormat,std::uint32_t,std::uint32_t,
const VkPhysicalDeviceMemoryProperties&);
void Destroy(Eye&);
VkDevice device_{};
PFN_vkGetDeviceProcAddr proc_{};
std::array<Eye,2> eyes_{};
std::array<Motion,2> motion_{};
struct Pending {std::array<std::uint64_t,2> depth{},motion{};bool submitted=false;};
std::unordered_map<VkCommandBuffer,Pending> pending_;
};
}
+134
View File
@@ -0,0 +1,134 @@
#pragma once
#include <cstdint>
#include <vulkan/vulkan.h>
namespace mocktail::vr {
// A scene-pass candidate, not an asynchronous borrow. This is delivered while
// the guest command buffer is recording, immediately after its render pass.
// The backend must verify owner/eye/generation, then record any required copies
// on that command buffer before returning. Image handles may be reused by the
// very next guest pass. A later queue submission/fence establishes completion.
struct TemporalSceneInputs {
std::uint32_t version = 1;
std::uint32_t size = sizeof(TemporalSceneInputs);
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VkImage color = VK_NULL_HANDLE;
VkImage depth = VK_NULL_HANDLE;
VkFormat color_format = VK_FORMAT_UNDEFINED;
VkFormat depth_format = VK_FORMAT_UNDEFINED;
VkImageLayout color_layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageLayout depth_layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageSubresourceRange color_range{};
VkImageSubresourceRange depth_range{};
std::uint32_t width = 0;
std::uint32_t height = 0;
// Set only when this pass actually clears its depth attachment. This is
// evidence for normal (clear=1) versus reversed (clear=0) device depth.
float depth_clear = 0.f;
bool depth_clear_valid = false;
void* owner = nullptr;
void* guest_framebuffer = nullptr;
int eye = -1;
// Zero means the adapter has no frame number. The backend must stamp the
// current published frame after matching the exact live eye provenance.
std::uint64_t frame = 0;
};
// Native 3222 motion shader output before conversion: current clip.xyz/w minus
// previous clip.xyz/w. Its vertex shader negates Y only for gl_Position, so
// previous-UV minus current-UV is nativeDelta.xy * (-0.5, +0.5). XYZ are signed
// floats; W is zero, not a validity mask. The shader can discard large deltas.
struct TemporalMotionInputs {
std::uint32_t version = 1;
std::uint32_t size = sizeof(TemporalMotionInputs);
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VkImage image = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageSubresourceRange range{};
std::uint32_t width = 0, height = 0;
void* owner = nullptr;
void* guest_framebuffer = nullptr;
int eye = -1;
std::uint64_t frame = 0;
float previous_uv_scale_x = -.5f;
float previous_uv_scale_y = .5f;
};
inline bool IsTemporalMotionFormat(VkFormat format) {
return format == VK_FORMAT_R16G16_SFLOAT ||
format == VK_FORMAT_R16G16B16A16_SFLOAT ||
format == VK_FORMAT_R32G32_SFLOAT ||
format == VK_FORMAT_R32G32B32A32_SFLOAT;
}
inline bool IsTemporalClipMotionShaders(std::uint64_t vertex,
std::uint64_t fragment) {
// FNV-1a of exact native shader module bytes, verified against both the
// fragment subtraction and vertex current/previous clip outputs. The three
// adjacent world-space variants deliberately have a different contract.
return fragment == 0x821fff499ec5bdbbull &&
(vertex == 0x9b0d6a7041cad338ull ||
vertex == 0x4a99a561ef9805b6ull ||
vertex == 0x6dcaa6712cd5a2cdull);
}
inline bool IsTemporalDepthFormat(VkFormat format) {
return format == VK_FORMAT_D16_UNORM ||
format == VK_FORMAT_X8_D24_UNORM_PACK32 ||
format == VK_FORMAT_D32_SFLOAT ||
format == VK_FORMAT_D16_UNORM_S8_UINT ||
format == VK_FORMAT_D24_UNORM_S8_UINT ||
format == VK_FORMAT_D32_SFLOAT_S8_UINT;
}
inline bool IsTemporalColorFormat(VkFormat format) {
return format == VK_FORMAT_R8G8B8A8_UNORM ||
format == VK_FORMAT_R8G8B8A8_SRGB ||
format == VK_FORMAT_B8G8R8A8_UNORM ||
format == VK_FORMAT_B8G8R8A8_SRGB ||
format == VK_FORMAT_R16G16B16A16_SFLOAT ||
format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 ||
format == VK_FORMAT_A2B10G10R10_UNORM_PACK32;
}
inline bool IsTemporalImageCandidate(const VkImageCreateInfo& info) {
const bool depth = IsTemporalDepthFormat(info.format) &&
(info.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
const bool color = IsTemporalColorFormat(info.format) &&
(info.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
const bool motion = IsTemporalMotionFormat(info.format) &&
(info.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
// Native transient targets may request lazy memory; changing their usage would
// disagree with the engine allocator's original memory-type request.
return (depth || color || motion) &&
!(info.usage & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) &&
info.imageType == VK_IMAGE_TYPE_2D &&
info.tiling == VK_IMAGE_TILING_OPTIMAL &&
info.samples == VK_SAMPLE_COUNT_1_BIT && info.extent.depth == 1 &&
info.extent.width >= 256 && info.extent.height >= 256 &&
info.arrayLayers == 1 && info.mipLevels == 1 && info.flags == 0;
}
// Depth/stencil formats use depth only. FSR samples device depth; stencil must
// never be reinterpreted as scene depth or included in a depth-only copy.
inline VkImageAspectFlags TemporalDepthAspect(VkFormat format) {
return IsTemporalDepthFormat(format) ? VK_IMAGE_ASPECT_DEPTH_BIT : 0;
}
} // namespace mocktail::vr
extern "C" {
// Availability tracks compatible resources from Vulkan startup. Enabled only
// selects preserving passes and per-frame capture while temporal FSR is active.
bool mocktail_vr_xr_fsr3_inputs_available();
bool mocktail_vr_xr_fsr3_inputs_enabled();
void mocktail_vr_xr_fsr3_scene_end(
VkDevice device, VkCommandBuffer command,
const mocktail::vr::TemporalSceneInputs* inputs);
void mocktail_vr_xr_fsr3_motion_end(
VkDevice device, VkCommandBuffer command,
const mocktail::vr::TemporalMotionInputs* inputs);
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <array>
#include <vulkan/vulkan.h>
#include "mocktail/vr/fsr3_upscaler.h"
namespace mocktail::vr {
// Explicit encoded-native -> linear-FSR -> encoded-native transport.
// The caller drains uses before resizing/resetting these reusable eye images.
class Fsr3Transport {
public:
~Fsr3Transport() { Reset(); }
bool Prepare(VkDevice,PFN_vkGetDeviceProcAddr,
const VkPhysicalDeviceMemoryProperties&,VkFormat source,
std::uint32_t width,std::uint32_t height,
std::uint32_t output_width,std::uint32_t output_height);
void Begin(VkCommandBuffer,int eye,VkImage encoded_source);
void Finish(VkCommandBuffer,int eye);
Fsr3Upscaler::Image color(int eye) const;
Fsr3Upscaler::Image output(int eye) const;
VkImage encoded(int eye) const {return eyes_[eye].encoded.image;}
VkFormat encoded_format() const {return srgb_format_;}
void Reset();
private:
struct Resource {VkImage image{};VkDeviceMemory memory{};};
struct Eye {Resource color,output,encoded;};
template<class T>T Proc(const char* name)const{return reinterpret_cast<T>(proc_(device_,name));}
bool Create(Resource&,VkFormat,std::uint32_t,std::uint32_t,VkImageUsageFlags,
const VkPhysicalDeviceMemoryProperties&);
void Destroy(Resource&);
void Barrier(VkCommandBuffer,VkImage,VkImageLayout,VkImageLayout,VkAccessFlags,VkAccessFlags);
VkDevice device_{};
PFN_vkGetDeviceProcAddr proc_{};
VkFormat source_format_{},srgb_format_{};
std::uint32_t width_{},height_{},output_width_{},output_height_{};
std::array<Eye,2> eyes_{};
};
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <vulkan/vulkan.h>
#include <cstdint>
#include <memory>
namespace mocktail::vr {
// AMD FSR 3.1.4 temporal upscaling. Each eye owns independent temporal history.
// GPU completion is the owner's responsibility before Reset or destruction.
class Fsr3Upscaler {
public:
struct Configuration {
std::uint32_t render_width{}, render_height{};
std::uint32_t output_width{}, output_height{};
bool inverted_depth=false, infinite_depth=false, hdr=false;
// Estimate image motion with AMD Optical Flow when engine vectors are
// unavailable. This is a fallback with its own compute cost and limits.
bool estimate_motion=false;
bool motion_vectors_jittered=false;
// Enable only when shaderFloat16 was enabled on the logical VkDevice.
bool allow_fp16=false;
};
struct Image {
VkImage image{};
VkFormat format{};
std::uint32_t width{}, height{};
};
struct JitterOffset { float x{}, y{}; };
struct Frame {
// Inputs must be sampled images in SHADER_READ_ONLY_OPTIMAL, output a
// storage image in GENERAL. The SDK restores those layouts after dispatch.
// Color is LINEAR: an sRGB image format decodes encoded source bytes.
// The output should be RGBA16F linear; encode it before copying to an
// encoded UNORM eye image. Depth and color must describe the same view.
Image color, depth, motion_vectors, output;
Image reactive, transparency;
// Unjittered motion is previous pixel position minus current position.
// Pixel-unit vectors use {1,1}; normalized UV vectors use render size.
float motion_vector_scale[2]{1.f,1.f};
JitterOffset jitter;
float delta_ms=16.667f, near_plane=.1f, far_plane=1000.f;
float vertical_fov=1.5707963f, meters_per_world_unit=1.f;
float pre_exposure=1.f, sharpness=.2f;
bool sharpening=true, reset=false;
};
Fsr3Upscaler();
~Fsr3Upscaler();
Fsr3Upscaler(const Fsr3Upscaler&)=delete;
Fsr3Upscaler& operator=(const Fsr3Upscaler&)=delete;
bool Configure(VkPhysicalDevice, VkDevice, PFN_vkGetDeviceProcAddr,
const VkPhysicalDeviceMemoryProperties&, const Configuration&);
bool Record(VkCommandBuffer, int eye, const Frame&);
Image estimated_motion(int eye) const;
void Reset();
int last_error() const;
static JitterOffset Jitter(std::uint64_t frame, std::uint32_t render_width,
std::uint32_t output_width);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <algorithm>
#include <cstdint>
namespace mocktail::vr {
struct FsrSettings {
int quality=0; // Custom scale, Ultra Quality, Quality, Balanced, Performance, Custom resolution.
int scale_percent=80;
std::uint32_t input_resolution=1728;
bool operator==(const FsrSettings& other) const {
return quality==other.quality && scale_percent==other.scale_percent &&
input_resolution==other.input_resolution;
}
bool operator!=(const FsrSettings& other) const { return !(*this==other); }
};
inline std::uint32_t FsrRenderSize(std::uint32_t output, int mode,
const FsrSettings& settings={}) {
if((mode!=1 && mode!=2 && mode!=3) || output<2)return output;
if(settings.quality==5)
return std::clamp(settings.input_resolution,2u,output)&~1u;
std::uint64_t numerator=std::clamp(settings.scale_percent,25,100),denominator=100;
switch(settings.quality) {
case 1:numerator=10;denominator=13;break;
case 2:numerator=2;denominator=3;break;
case 3:numerator=10;denominator=17;break;
case 4:numerator=1;denominator=2;break;
default:break;
}
const auto scaled=static_cast<std::uint32_t>(
2*((std::uint64_t(output)*numerator+denominator)/(2*denominator)));
return std::clamp(scaled,2u,output&~1u);
}
inline std::uint32_t FsrRenderHeight(std::uint32_t output_width,
std::uint32_t output_height,int mode,
const FsrSettings& settings={}) {
if(settings.quality!=5 || mode==0 || !output_width || output_height<2)
return FsrRenderSize(output_height,mode,settings);
const auto width=FsrRenderSize(output_width,mode,settings);
const auto height=static_cast<std::uint32_t>(
2*((std::uint64_t(output_height)*width+output_width)/(2*std::uint64_t(output_width))));
return std::clamp(height,2u,output_height&~1u);
}
}
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include <vulkan/vulkan.h>
#include "mocktail/vr/fsr_settings.h"
#include "mocktail/vr/temporal_reprojection.h"
#include <cstdint>
#include <cstddef>
#include <unordered_map>
namespace mocktail::vr {
// FP32 FSR 1 EASU, followed by gaze-only RCAS after the normal gaze AA.
// Uses raw host dispatch and the backend's existing copy command/fence.
class FsrUpscaler {
public:
~FsrUpscaler() { Reset(); }
bool Prepare(VkDevice, PFN_vkGetDeviceProcAddr,
const VkPhysicalDeviceMemoryProperties&, int eye,
VkImage source, VkFormat source_format, VkImage destination,
VkFormat output_format, std::uint32_t input_width,
std::uint32_t input_height, std::uint32_t output_width,
std::uint32_t output_height, bool temporal=false);
void RecordUpscale(VkCommandBuffer, int eye, float gaze_x=0, float gaze_y=0,
float radius=.15f);
void RecordTemporal(VkCommandBuffer,int eye,const TemporalEyePose&,
float gaze_x,float gaze_y,float radius,std::uint64_t now);
void RecordSharpen(VkCommandBuffer, int eye, float gaze_x, float gaze_y,
float radius);
VkImage image(int eye) const { return eyes_[eye].image; }
VkFormat intermediate_format() const;
void ForgetImage(VkImage);
void Reset();
private:
template<class T> T Proc(const char* name) const {
return reinterpret_cast<T>(proc_(device_, name));
}
bool Initialize(VkDevice, PFN_vkGetDeviceProcAddr, VkFormat);
VkPipeline Pipeline(VkRenderPass, const std::uint32_t*, std::size_t);
struct Eye {
VkImage image{}, source{}, destination{}, gamma_image{};
VkDeviceMemory memory{}, gamma_memory{};
VkImageView view{}, source_view{};
VkFramebuffer framebuffer{};
VkDescriptorSet sets[3]{};
VkImage history_image{};VkDeviceMemory history_memory{};VkImageView history_view{};
bool history_initialized=false,history_failed=false;
TemporalEyePose previous_pose;
float previous_center[2]{},previous_radius=0;
std::uint64_t previous_time=0;
VkFormat source_format{};
std::uint32_t in_width=0, in_height=0, width=0, height=0;
};
struct Target { VkImageView view{}; VkFramebuffer framebuffer{}; };
void FreeEye(Eye&);
bool PrepareHistory(const VkPhysicalDeviceMemoryProperties&,int eye);
struct alignas(16) Constants {
std::uint32_t con[4][4]{};
float texel[2]{}, center[2]{};
float radius=0;
std::uint32_t input_srgb=0, output_srgb=0, reserved=0;
};
static_assert(sizeof(Constants)==96);
struct alignas(16) TemporalConstants {
float rows[3][4]{},texel[2]{},center[2]{},previous_center[2]{};
float radius=0,previous_radius=0,blend=0;
std::uint32_t output_srgb=0,reserved[2]{};
};
static_assert(sizeof(TemporalConstants)==96);
void Draw(VkCommandBuffer, VkRenderPass, VkFramebuffer, VkPipeline,
VkDescriptorSet, const void*, const Eye&, VkRect2D);
void Barrier(VkCommandBuffer, VkImage, VkImageLayout, VkImageLayout,
VkAccessFlags, VkAccessFlags, VkPipelineStageFlags,
VkPipelineStageFlags);
VkDevice device_{};
PFN_vkGetDeviceProcAddr proc_{};
VkFormat format_{};
VkRenderPass passes_[2]{};
VkPipeline pipelines_[3]{};
VkDescriptorSetLayout descriptor_layout_{};
VkDescriptorPool descriptor_pool_{};
VkPipelineLayout layout_{};
VkSampler sampler_{};
Eye eyes_[2];
std::unordered_map<VkImage,Target> targets_;
};
}
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <vulkan/vulkan.h>
#include <cstdint>
#include <cstddef>
#include <unordered_map>
namespace mocktail::vr {
// Gaze-limited edge filtering after the normal eye copy.
// Every call uses raw host dispatch. The owner waits its copy fence before
// changing descriptors, dropping views, or destroying resources.
class GazeAa {
public:
~GazeAa() { Reset(); }
bool Prepare(VkDevice, PFN_vkGetDeviceProcAddr, int eye, VkImage source,
VkFormat source_format, VkImage destination,
VkFormat destination_format, std::uint32_t width,
std::uint32_t height, int mode=1,
const VkPhysicalDeviceMemoryProperties* memory=nullptr);
bool Record(VkCommandBuffer, int eye, float gaze_x, float gaze_y, int mode=1,
float radius=.22f);
void ForgetImage(VkImage);
void Reset();
private:
template<class T> T Proc(const char* name) const {
return reinterpret_cast<T>(get_proc_(device_,name));
}
bool Initialize(VkDevice,PFN_vkGetDeviceProcAddr,VkFormat);
VkPipeline MakePipeline(VkRenderPass,const std::uint32_t*,std::size_t);
bool PrepareSmaa(int,const VkPhysicalDeviceMemoryProperties&,int mode);
void RecordSmaa(VkCommandBuffer,int,int,const void*,const VkRect2D&);
void RecordPass(VkCommandBuffer,int,VkPipeline,VkRenderPass,VkFramebuffer,
VkDescriptorSet,const void*,const VkRect2D&,bool);
void DestroySmaa();
struct Texture { VkImage image{}; VkDeviceMemory memory{}; VkImageView view{}; };
bool MakeTexture(Texture&,VkFormat,unsigned,unsigned,VkImageUsageFlags);
void FreeTexture(Texture&);
struct SmaaEye {
Texture textures[2];
VkFramebuffer framebuffers[2]{};
VkDescriptorSet descriptors[2]{};
std::uint32_t width=0,height=0;
VkImageView descriptor_source{};
};
struct Source { VkImage image{}; VkImageView view{}; VkFormat format{}; bool descriptor_ready=false; };
struct Target { VkImageView view{}; VkFramebuffer framebuffer{}; };
VkDevice device_{};
PFN_vkGetDeviceProcAddr get_proc_{};
VkFormat format_=VK_FORMAT_UNDEFINED;
VkDescriptorSetLayout descriptor_layout_{};
VkDescriptorPool descriptor_pool_{};
VkDescriptorSet descriptor_sets_[2]{};
VkPipelineLayout pipeline_layout_{};
VkPipeline pipeline_{};
VkRenderPass render_pass_{};
VkSampler sampler_{};
VkRenderPass smaa_pass_{};
VkPipeline smaa_pipelines_[2][3]{};
SmaaEye smaa_eyes_[2];
Texture smaa_luts_[2];
VkBuffer smaa_staging_{};
VkDeviceMemory smaa_staging_memory_{};
bool smaa_uploaded_=false;
Source sources_[2];
std::unordered_map<VkImage,Target> targets_;
VkImage destinations_[2]{};
std::uint32_t widths_[2]{},heights_[2]{};
};
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef MOCKTAIL_VR_HEADSET_DETECTION_H_
#define MOCKTAIL_VR_HEADSET_DETECTION_H_
#include <chrono>
#include <string>
namespace mocktail::vr {
// Set on the child process that answers DetectSteamVrHeadset. main() checks
// it first and returns RunHeadsetProbeChild() without starting Roblox.
inline constexpr const char* kHeadsetProbeEnvironment =
"MOCKTAIL_VR_HEADSET_PROBE";
struct HeadsetDetection {
bool headset = false;
std::string reason;
};
// vr.mode: auto. True only when this user's SteamVR is running and its OpenXR
// runtime reports a head-mounted display. SteamVR is never started. The
// runtime is asked from a child process (`self_executable` with
// kHeadsetProbeEnvironment set) so its libraries never load into this one
// before the VR backend arms.
HeadsetDetection DetectSteamVrHeadset(
const std::string& self_executable,
std::chrono::milliseconds timeout = std::chrono::seconds(5),
const std::string& proc_root = "/proc");
// The child side: probes the OpenXR runtime in XR_RUNTIME_JSON and returns
// the probe's exit code (0 when a headset is available).
int RunHeadsetProbeChild();
} // namespace mocktail::vr
#endif // MOCKTAIL_VR_HEADSET_DETECTION_H_
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
namespace mocktail::vr {
struct NativeProjectionSample {
// The actual engine camera projection before temporal jitter, row-major.
// This is read from the native camera rather than rebuilt from tracking FOV.
float projection[16] = {};
// The full world-view-projection used by the engine, including player
// locomotion. Supplied by the separately verified native combined getter.
float view_projection[16] = {};
float jitter_pixels[2] = {};
std::uint32_t render_width = 0;
std::uint32_t render_height = 0;
float camera_near = 0.f;
float camera_far = 0.f;
std::uintptr_t camera_identity = 0;
std::uint64_t frame = 0;
bool depth_inverted = false;
bool depth_infinite = false;
bool jitter_applied = false;
bool view_projection_valid = false;
bool valid = false;
};
// True only when the exact native per-eye projection function was verified and
// hooked. Unsupported clients retain their original projections.
bool NativeProjectionBridgeReady();
// Copies only a projection from the requested frame. A stale or unsupported
// camera never supplies temporal reconstruction parameters.
bool ReadNativeProjectionSample(int eye, std::uint64_t expected_frame,
NativeProjectionSample* sample);
} // namespace mocktail::vr
+720
View File
@@ -0,0 +1,720 @@
#ifndef MOCKTAIL_VR_OPENXR_BACKEND_H_
#define MOCKTAIL_VR_OPENXR_BACKEND_H_
#include <vulkan/vulkan.h>
#include <array>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include <memory>
#include "mocktail/status.h"
#include "mocktail/vr/xr_foveation_maps.h"
#include "mocktail/vr/gaze_aa.h"
#include "mocktail/vr/fsr_upscaler.h"
#include "mocktail/vr/fsr3_scene_capture.h"
#ifdef MOCKTAIL_ENABLE_FSR3
#include "mocktail/vr/fsr3_upscaler.h"
#include "mocktail/vr/fsr3_gaze_transport.h"
#endif
#include "mocktail/vr/virtual_keyboard.h"
#include "mocktail/vr/vr_mirror.h"
#include "mocktail/vr/xr_actions.h"
#include "mocktail/vr/xr_controller.h"
namespace mocktail::vr {
namespace internal {
// Deferral is safe only when the guest requested exactly one unprotected queue.
bool SingleUnprotectedVulkanQueue(const VkDeviceCreateInfo* info);
// Frame's native Video slider stores a per-eye pixel dimension per app.
std::uint32_t SteamFrameEyeResolution(std::string_view settings,
std::string_view steam_app_id);
// Swapchain format for guest eyes of `eye` given the runtime's formats:
// `eye` itself, else its UNORM/SRGB sibling (a raw byte copy, since the guest
// writes sRGB-encoded bytes either way), else the other channel order with
// the same sRGB-ness (a blit that only swizzles). VK_FORMAT_UNDEFINED when
// only a gamma-converting blit remains.
VkFormat PreferredEyeSwapchainFormat(VkFormat eye,
const std::vector<std::int64_t>& available);
// Consume binary present dependencies exactly once. On success a later present
// must omit them. VK_NOT_READY means nothing was consumed and the guest's
// present proceeds unchanged; any other failure consumed them (or lost the
// device) and must be propagated, not retried.
VkResult WaitForPresentDependencies(VkDevice device, VkQueue queue,
const VkPresentInfoKHR& present,
VkFence fence, PFN_vkResetFences reset,
PFN_vkQueueSubmit submit,
PFN_vkWaitForFences wait);
// Hands binary present dependencies to the GPU instead: one empty batch on
// `queue` waits on them and signals `signal`, which the present waits on
// instead. Work submitted to `queue` afterwards is ordered after the waits.
// The CPU never blocks. VK_NOT_READY means nothing was consumed.
VkResult ChainPresentDependencies(VkDevice device, VkQueue queue,
const VkPresentInfoKHR& present,
VkSemaphore signal, PFN_vkQueueSubmit submit);
// Makes the Vulkan adapter serialize queue calls for the rest of the process.
// Called when a backend arms; the adapter library may load before or after.
void RequestVulkanQueueSharing();
} // namespace internal
// Resolved --vr output mode. kXrOutput drives the real Roblox stereo images
// into an OpenXR projection layer; kNativeOnly keeps the diagnostic native
// stereo prototype without any runtime requirement. Selected through
// MOCKTAIL_VR_BACKEND ("xr" default, "native" diagnostic).
enum class VrBackendMode {
kDisabled,
kNativeOnly,
kXrOutput,
};
// Explicit XR_RUNTIME_JSON wins. Runtime preference can select SteamVR (ALVR),
// WiVRn, or the registered OpenXR runtime without changing system registration.
std::string SelectVrRuntimeManifest(const std::string& explicit_manifest,
const std::vector<std::string>& candidates);
std::vector<std::string> VrRuntimeManifestCandidates(
const std::string &preference, const std::string &home,
const std::string &config_home, const std::string &config_dirs);
struct VrRuntimeDiscovery {
std::vector<std::string> candidates;
std::string reason;
};
VrRuntimeDiscovery DiscoverVrRuntime(
const std::string& preference, const std::string& home,
const std::string& config_home, const std::string& config_dirs,
const std::string& runtime_dir, const std::string& proc_root = "/proc");
std::string VrRuntimeUnavailableHint(bool user_selected, const std::string& manifest);
// SteamVR's OpenXR manifest when this user's vrserver is running, else empty.
std::string RunningSteamVrManifest(const std::string& proc_root = "/proc");
VrBackendMode ResolveVrBackendMode(bool vr_enabled);
const char* VrBackendModeName(VrBackendMode mode);
// One deterministic head pose sample. Position is metres, orientation is a
// quaternion (x, y, z, w) in the OpenXR LOCAL reference space.
// VrHandPose (tracked controller hands) is defined in xr_controller.h.
struct ScriptedPoseSample {
float position[3] = {0.f, 0.f, 0.f};
float orientation[4] = {0.f, 0.f, 0.f, 1.f};
float eye_offset[2][3] = {}; // metres in head space
float eye_fov[2][4] = {}; // left, right, up, down angles
float projection_jitter_pixels[2] = {}; // raster displacement: right/down
std::uint32_t projection_render_width = 0, projection_render_height = 0;
bool projection_jitter_valid = false;
// Index 0 = left, 1 = right (Roblox UserCFrame LeftHand/RightHand).
VrHandPose hands[2];
float floor_position[3] = {};
float floor_orientation[4] = {0.f, 0.f, 0.f, 1.f};
bool floor_valid = false;
// Verified DebugDeviceVR input channels, published with the same pose.
float controller_channels[28] = {};
bool controllers_connected = false;
std::uint64_t frame = 0;
bool valid = false;
};
// Production OpenXR backend for the native stereo prototype.
//
// Lifecycle: main arms the backend before Roblox startup (xrCreateInstance
// and xrGetSystem fail fast with a clear message when no runtime exists).
// The Vulkan adapter delegates the guest's vkCreateInstance/vkCreateDevice
// through xrCreateVulkanInstanceKHR/xrCreateVulkanDeviceKHR so the runtime's
// extension and GPU requirements are satisfied by the very device Roblox
// renders with. The session is created on that device; per-frame XR work runs
// on the render thread from the adapter's host-present notification:
// xrEndFrame(N) with the two eye images of frame N copied into the
// projection swapchains, then xrWaitFrame/xrBeginFrame/xrLocateViews for
// frame N+1 and publishing the scripted/runtime head pose that the device
// bridge injects into Roblox before its eye passes run.
//
// EGL/OpenGL ES uses XR_MNDX_egl_enable on the guest SDL context and GPU
// framebuffer blits into GLES swapchain textures. It shares the same frame
// scheduling, tracking, pose application and session recovery below.
//
// Eye image provenance is recorded in the adapter during the guest eye
// initializer (owner = DebugDeviceVR object) and bound to an eye index only
// through the real per-frame eye-getter request plus the actual render-pass
// attachment, mirroring the validated evidence-layer semantics. Creation
// order or size alone never selects an eye.
//
// All public entry points are exception-free at their boundary; the
// implementation is compiled with exceptions like openxr_preview.cc and
// translates failures into Status/log outcomes.
class GlesTransport;
enum class VrGraphicsApi { kVulkan, kOpenGles };
class OpenXrBackend final {
public:
OpenXrBackend() = default;
~OpenXrBackend();
OpenXrBackend(const OpenXrBackend&) = delete;
OpenXrBackend& operator=(const OpenXrBackend&) = delete;
// Claims the process-wide backend slot and creates XrInstance/system.
Status Arm(VrGraphicsApi graphics_api = VrGraphicsApi::kVulkan);
// EGL context must be current on the calling thread. Resolver bypasses guest
// hooks.
Status AttachGlesContext(void *display, void *config, void *context,
void *egl_get_proc, void *(*resolve)(const char *));
void DetachGlesContext(void *context);
void NoteGlesPresent();
void *WrapGlesProcAddress(const char *name, void *raw);
// Idempotent full teardown (XR resources first, then the instance).
void Disarm();
bool armed() const { return armed_.load(std::memory_order_acquire); }
// Adapter delegation. Return false when the backend does not own creation
// (caller falls back to the plain host loader path).
bool CreateVulkanInstance(const VkInstanceCreateInfo* create_info,
const VkAllocationCallbacks* allocator,
VkInstance* instance, VkResult* result);
bool CreateVulkanDevice(VkPhysicalDevice physical_device,
const VkDeviceCreateInfo* create_info,
const VkAllocationCallbacks* allocator,
VkDevice* device, VkResult* result);
// True transfers native device destruction to this backend.
bool NoteDeviceDestroyed(VkDevice device);
bool FoveationEnabled(VkDevice device) const;
VkImageView FoveationViewForFramebuffer(VkDevice device, VkFramebuffer framebuffer);
bool FoveationFocus(VkDevice device,VkFramebuffer framebuffer,float* x,float* y,std::uint64_t* frame,int* eye,int* level);
bool FoveationSnapshot(VkDevice device,float* centers,float* radius,std::uint64_t* frame,int* level);
// A scene intermediate may be shared between eyes. Resolve it from the live
// eye getter, never from its framebuffer handle or dimensions alone.
bool FoveationRenderFocus(VkDevice device, VkFramebuffer framebuffer,
std::uint32_t scene_width, std::uint32_t scene_height, float* x, float* y,
float* radius, std::uint64_t* frame, int* eye, int* level);
bool FoveationHostCopyEnabled(VkDevice device) const;
bool Fsr3InputsAvailable() const;
bool Fsr3InputsEnabled() const;
void CaptureFsr3Scene(VkDevice, VkCommandBuffer, const TemporalSceneInputs&);
void CaptureFsr3Motion(VkDevice, VkCommandBuffer, const TemporalMotionInputs&);
VkExtent2D FoveationMapExtent(VkDevice device) const;
// True transfers native instance destruction to this backend.
bool NoteInstanceDestroyed(VkInstance instance);
void NoteQueue(VkDevice device, VkQueue queue, std::uint32_t family,
std::uint32_t index);
std::vector<std::uint32_t> DesktopQueueFamilies(VkDevice device) const;
void RecordCommandPool(VkDevice device, VkCommandPool pool,
std::uint32_t family);
void RecordCommandBuffers(VkDevice device, VkCommandPool pool,
std::uint32_t count,
const VkCommandBuffer* buffers);
void ForgetCommandBuffers(std::uint32_t count,
const VkCommandBuffer* buffers);
void ForgetCommandPool(VkCommandPool pool);
void ResetCommandPool(VkCommandPool pool);
void ResetCommandBuffer(VkCommandBuffer buffer);
void NoteCommandBuffersSubmitted(VkQueue queue, std::uint32_t count,
const VkCommandBuffer* buffers);
VkImageLayout NormalizeDesktopBarrier(VkCommandBuffer command, VkImage image,
std::uint32_t* source,
std::uint32_t* destination,
VkImageLayout* old_layout,
VkImageLayout new_layout);
void NormalizeDesktopBarriers(VkCommandBuffer command, std::uint32_t count,
VkImageMemoryBarrier* barriers);
void NormalizeDesktopBarriers(VkCommandBuffer command, std::uint32_t count,
VkImageMemoryBarrier2* barriers);
// Resource provenance recording (adapter wrappers).
bool WantsResourceRecords() const {
return recording_.load(std::memory_order_acquire);
}
void RecordImage(VkDevice device, VkImage image,
const VkImageCreateInfo* info);
void RecordImageView(VkImageView view, VkImage image);
void RecordFramebuffer(VkFramebuffer framebuffer,
const VkFramebufferCreateInfo* info);
void RecordRenderPass(VkRenderPass render_pass,
const VkRenderPassCreateInfo* info);
void NoteDestroyImage(VkImage image);
void NoteDestroyImageView(VkImageView view);
void NoteDestroyFramebuffer(VkFramebuffer framebuffer);
void NoteRenderPassBegin(VkCommandBuffer command_buffer,
const VkRenderPassBeginInfo* info);
bool EyeBindingComplete() const {
return eyes_bound_.load(std::memory_order_acquire);
}
// Render-thread frame cycle at host present begin.
void NoteHostPresent(VkQueue queue, VkDevice device);
// VK_NOT_READY leaves the present untouched until eye submission provenance
// is known. VK_SUCCESS means its waits were consumed on the graphics queue,
// so the eye copies that follow there are ordered after them. The present
// must then wait on *ready instead (none when it is VK_NULL_HANDLE).
VkResult PrepareHostPresent(VkQueue queue, VkDevice device,
const VkPresentInfoKHR* info, VkSemaphore* ready);
void RecordDesktopSwapchain(VkDevice device, VkSwapchainKHR swapchain,
const VkSwapchainCreateInfoKHR* info,
const VkImage* images, unsigned count,
bool converted_to_concurrent = false);
// On success the original present semaphores have been consumed by the
// mirror blit on the GPU. The caller must present waiting only on *ready,
// which the blit signals. The CPU never waits for the guest's frame here.
VkResult MirrorDesktop(VkQueue queue, const VkPresentInfoKHR* info,
VkSemaphore* ready);
// Pose published for the frame that is about to render. The device bridge
// reads it once per frame on the render thread and injects it into the
// guest. valid=false until the XR session located a usable head pose.
// hands[0]=left, hands[1]=right carry this frame's tracked controller grip
// poses (metres, LOCAL space) when a controller layer is active.
ScriptedPoseSample PublishedHeadPose() const;
bool RecommendedEyeExtent(std::uint32_t* width, std::uint32_t* height) const;
// Render-thread boundary only: rebuild XR targets when Steam's settled
// resolution changes. The guest then reruns its verified eye initializer.
bool UpdateEyeResolution();
void NotePoseApplied(void* owner, std::uint64_t frame);
// Controller input delivery, consumed by the window-thread input runtime
// through the C ABI in xr_controller_abi.h. TakeControllerDelivery pops one
// batch produced by the once-per-frame action sync; it returns false when
// no controller layer is active or nothing is queued. The snapshot exposes
// the current levels for overflow resync. Haptics requests are thread-safe.
bool TakeControllerDelivery(ControllerDelivery* out);
ControllerSnapshot ControllerSnapshotForFrame() const;
void RequestControllerHaptics(int hand, float amplitude,
std::uint64_t duration_ns, float frequency_hz);
void StopControllerHaptics(int hand);
// Diagnostics.
std::uint64_t submitted_frames() const {
return submitted_frames_.load(std::memory_order_relaxed);
}
std::uint64_t skipped_frames() const {
return skipped_frames_.load(std::memory_order_relaxed);
}
bool session_running() const {
return session_running_.load(std::memory_order_relaxed);
}
std::string runtime_name() const;
private:
VkImageLayout NormalizeDesktopBarrierLocked(std::uint32_t family,
VkImage image, std::uint32_t* source, std::uint32_t* destination,
VkImageLayout* old_layout, VkImageLayout new_layout);
bool FoveationSnapshotLocked(VkDevice,float*,float*,std::uint64_t*,int*);
int FoveationSceneEyeLocked(VkDevice, std::uint32_t, std::uint32_t) const;
friend struct OpenXrBackendTestAccess;
struct ImageRecord {
VkImage image = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
std::uint32_t width = 0;
std::uint32_t height = 0;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageUsageFlags usage = 0;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
void* owner = nullptr;
std::uint64_t order = 0;
};
struct EyeBinding {
bool valid = false;
std::uint64_t frame = 0;
VkImage image = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
void* guest_framebuffer = nullptr;
VkImageLayout final_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
void* owner = nullptr;
std::uint32_t width = 0;
std::uint32_t height = 0;
VkFormat format = VK_FORMAT_UNDEFINED;
};
struct EyeSwapchain {
void* swapchain = nullptr; // XrSwapchain (opaque to keep the header XR-free)
std::vector<VkImage> images;
std::vector<std::uint32_t> gl_images;
std::uint32_t acquired_index = 0;
bool image_acquired = false;
std::uint32_t width = 0;
std::uint32_t height = 0;
VkFormat format = VK_FORMAT_UNDEFINED;
};
bool InitializeSessionLocked(VkPhysicalDevice physical_device,
VkDevice device,
const VkDeviceCreateInfo* create_info,
std::string* error);
// Session-owned controller action layer (poses, buttons, axes, haptics).
// Created at instance level, attached per session, detached on teardown so
// a replacement session never reuses stale spaces.
XrActions actions_;
bool actions_created_ = false;
// Shared by action sync, snapshots, haptics and lifecycle operations.
// Acquired after mutex_ on the render thread, but never held across
// xrWaitFrame; the input thread takes only this controller lock.
mutable std::mutex controller_mutex_;
// Syncs actions once per frame at the predicted display time and folds the
// resulting hand poses into published_pose_ before the guest renders.
void SyncControllersLocked(std::uint64_t predicted_display_time);
bool InitializeGlesSessionLocked(std::string *error);
bool InitializeSessionResourcesLocked(std::string *error);
bool CopyGlesEyesIntoSwapchains();
bool EnsureXrInstanceLocked(std::string* error = nullptr);
void DestroyXrInstanceLocked();
void DisarmLocked();
void* ResolveFunction(const char* name) const;
void SetupDeviceAfterCreationLocked(VkPhysicalDevice physical_device,
VkDevice device,
const VkDeviceCreateInfo* create_info);
void TeardownSessionLocked(const char* reason, bool preserve_device = false);
void HandleSessionState(int state);
void HandleReferenceSpaceChange(std::int64_t change_time);
bool RecoverSessionLocked(std::uint64_t now_ns);
// False only when the runtime names a GPU other than the guest device's.
bool RuntimeUsesGuestGpuLocked();
void PollSessionEvents();
bool SupportsPresentQueueLocked(VkQueue queue) const;
VkQueue FrameQueueForPresentLocked(VkQueue queue) const;
VkQueue GraphicsQueueForPresentLocked(VkQueue present) const;
bool unsupported_present_queue_logged_ = false;
// Drops the pose/images of the frame currently in flight without touching
// guest ownership, so the next xrEndFrame submits no layer instead of an
// image paired with a pose that no longer describes the same origin.
void InvalidateInFlightPose(const char* reason);
void RunFrameCycle(VkQueue queue, VkDevice device);
// Returns true when the backend owns this present but ran no XR frame, so
// the caller paces the guest itself.
bool NoteHostPresentLocked(VkQueue queue, VkDevice device);
bool NeedsIdlePacingLocked() const;
bool CreateSwapchains(std::string* error);
// Recreates the eye swapchains once in the format that copies the guest's
// eye bytes without a gamma change (see PreferredEyeSwapchainFormat).
void MatchSwapchainFormatToEyesLocked();
void DestroySwapchains();
void ResetFsr3Locked();
bool EnsureCopyResources(VkDevice device, std::uint32_t queue_family,
std::string* error);
void DestroyCopyResources();
// Records both eye copies (and, when the mirror is enabled, both staging
// readbacks) into one command buffer and submits it once. Only the shared
// memory mirror waits for it; the headset path leaves it to the GPU.
bool CopyEyesIntoSwapchains(VkQueue queue);
void RecordEyeCopiesLocked(VkCommandBuffer command, const VkImage source[2],
const VkImage destination[2]);
void RecordFxaaEyesLocked(VkCommandBuffer command);
// Per-frame GPU work (desktop mirror, eye copies) is submitted without a CPU
// wait. A slot is reused only after its fence signalled; its images let a
// guest destroy wait for the submissions that still read them.
struct AsyncSubmission {
VkCommandBuffer command = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
bool pending = false;
bool eye_resources = false;
std::uint64_t serial = 0; // bumped per submission
std::vector<VkImage> images;
};
// The shared-memory mirror's readback from the previous frame copy. It is
// published at the next copy, when the GPU has long finished it, so neither
// the headset nor the mirror makes the CPU wait for the frame.
struct MirrorReadback {
AsyncSubmission* slot = nullptr;
std::uint64_t serial = 0;
std::uint64_t frame = 0;
VkFormat format = VK_FORMAT_UNDEFINED;
std::uint32_t width = 0;
std::uint32_t height = 0;
};
void PublishMirrorReadbackLocked();
// vr.anti_aliasing: fxaa. The eye copy becomes one FXAA draw per eye into
// the runtime's swapchain image. Anything it cannot set up falls back to
// the plain copy, which never loses headset output.
struct FxaaState {
VkFormat target_format = VK_FORMAT_UNDEFINED;
VkShaderModule vertex = VK_NULL_HANDLE;
VkShaderModule fragment = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
VkDescriptorSetLayout set_layout = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
VkDescriptorSet sets[2] = {};
VkPipelineLayout layout = VK_NULL_HANDLE;
VkRenderPass render_pass = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
// The eye image each set samples, and its view.
VkImage sources[2] = {};
VkImageView source_views[2] = {};
// Per eye, one view and framebuffer per runtime swapchain image.
std::vector<VkImageView> target_views[2];
std::vector<VkFramebuffer> framebuffers[2];
bool failed = false;
};
bool FxaaEnabled() const;
// Pipeline, targets and eye bindings for this frame; false means copy.
bool PrepareFxaaLocked();
bool CreateFxaaPipelineLocked(VkFormat target_format);
void DestroyFxaaTargetsLocked();
void ForgetFxaaSourceLocked(VkImage image);
void DestroyFxaaLocked();
// The next slot, recorded-ready, or nullptr when its previous submission
// did not finish in time (the device is hung or lost).
AsyncSubmission* NextAsyncSubmissionLocked();
VkResult SubmitAsyncLocked(VkQueue queue, AsyncSubmission* slot,
const VkSubmitInfo& submit);
// Waits for pending submissions reading `image`, or all of them for
// VK_NULL_HANDLE.
void WaitAsyncSubmissionsLocked(VkImage image);
// AA/FSR descriptors and history are shared between eye copies. Wait only
// for their previous users, never the current guest-present dependency.
void WaitEyeResourcesLocked();
// Semaphore the present of `image_index` waits on, created on first use.
enum class PresentStep { kMirror, kPrepare };
VkSemaphore PresentReadySemaphoreLocked(VkSwapchainKHR swapchain,
std::uint32_t image_index,
PresentStep step);
void DestroyRetiredSemaphoresLocked();
// In-headset keyboard: show/hide from the host's TextBox request, pointer
// input, and trigger masking. Runs inside the controller sync.
void UpdateVirtualKeyboardLocked();
// Rasterizes and uploads the panel when it changed. Returns true when the
// keyboard swapchain holds a released image ready for a quad layer.
bool PrepareVirtualKeyboardLayerLocked(VkQueue queue);
bool UploadVirtualKeyboardVulkan(VkQueue queue, VkImage image);
void DestroyVirtualKeyboardStaging();
void DestroyVirtualKeyboardResources();
bool CaptureEyeEvidence(VkQueue queue, int eye);
ScriptedPoseSample ComputeScriptedPose(std::uint64_t frame) const;
void EnsureMirrorForExtent(std::uint32_t width, std::uint32_t height);
MirrorVkProcs MirrorProcs() const;
VrGraphicsApi graphics_api_ = VrGraphicsApi::kVulkan;
GlesTransport *gles_ = nullptr;
std::uint64_t gl_min_version_ = 0;
std::uint64_t gl_max_version_ = 0;
// Held for a whole XR frame cycle and by every path that can destroy the
// session; always taken before mutex_.
std::mutex frame_mutex_;
mutable std::mutex mutex_;
// The frame cycle's hold on mutex_, so it can release it around
// xrWaitFrame. Only set while NoteHostPresent runs the cycle.
std::unique_lock<std::mutex>* state_lock_ = nullptr;
std::atomic<bool> armed_{false};
std::atomic<bool> recording_{false};
std::atomic<bool> eyes_bound_{false};
std::atomic<bool> session_running_{false};
std::atomic<std::uint64_t> submitted_frames_{0};
std::atomic<std::uint64_t> skipped_frames_{0};
// XR handles are stored as opaque pointers in the header; the implementation
// casts them back. This keeps vulkan-only consumers free of OpenXR headers.
void* xr_instance_ = nullptr;
std::uint64_t xr_system_ = 0;
void* xr_session_ = nullptr;
void* xr_local_space_ = nullptr;
void* xr_view_space_ = nullptr;
void* xr_stage_space_ = nullptr;
std::uint32_t xr_eye_width_[2] = {};
std::uint32_t xr_eye_height_[2] = {};
std::uint32_t xr_max_eye_width_[2] = {};
std::uint32_t xr_max_eye_height_[2] = {};
std::uint32_t resize_candidate_ = 0;
std::uint64_t resize_candidate_since_ = 0;
EyeSwapchain eye_swapchains_[2];
VirtualKeyboard keyboard_;
std::unique_ptr<VirtualKeyboardRasterizer> keyboard_raster_;
EyeSwapchain keyboard_swapchain_;
bool keyboard_swapchain_failed_ = false;
bool keyboard_image_ready_ = false;
std::uint64_t keyboard_uploaded_revision_ = 0;
// A new showKeyboard request may open the panel; Close disarms it until
// Roblox drops that request.
bool keyboard_armed_ = true;
pose::Pose keyboard_head_{};
bool keyboard_head_valid_ = false;
std::vector<std::uint8_t> keyboard_pixels_;
VkBuffer keyboard_staging_ = VK_NULL_HANDLE;
VkDeviceMemory keyboard_staging_memory_ = VK_NULL_HANDLE;
void* keyboard_staging_mapped_ = nullptr;
int xr_blend_mode_ = 0;
// Runtime name captured at xrCreateInstance so the bind-time diagnostics line
// can report it without re-entering runtime_name() under the held mutex.
std::string runtime_name_;
bool xr_instance_used_ = false;
// Libraries the runtime's first instance loaded; later instances load them
// with RTLD_DEEPBIND (see EnsureXrInstanceLocked).
std::vector<std::string> runtime_libraries_;
std::string user_manifest_;
std::string runtime_manifest_;
VkInstance vk_instance_ = VK_NULL_HANDLE;
std::uint32_t vk_instance_api_version_ = VK_API_VERSION_1_0;
bool fsr3_device_supported_ = false;
// One retained instance avoids invalid SteamVR ARM graphics-device caches
// during menu/game transitions. Only an identical creation request reuses it.
bool vk_instance_retained_ = false;
bool vk_device_retained_ = false;
bool foveation_runtime_available_ = false;
bool eye_gaze_available_ = false;
bool foveation_device_enabled_ = false;
bool foveation_host_copy_enabled_ = false;
bool foveation_used_in_frame_ = false;
XrFoveationMaps foveation_;
GazeAa gaze_aa_;
FsrUpscaler fsr_;
Fsr3SceneCapture fsr3_capture_;
#ifdef MOCKTAIL_ENABLE_FSR3
Fsr3Upscaler fsr3_;
Fsr3GazeTransport fsr3_transport_;
#endif
std::atomic<bool> fsr3_inputs_enabled_{false};
bool fsr3_failure_logged_ = false;
std::uint64_t fsr3_last_frame_ns_ = 0;
std::uint64_t fsr3_last_frame_ = 0;
bool fsr3_has_resources_ = false;
bool fsr3_capture_seen_[2]{};
float fsr3_gaze_radius_ = -1.f;
bool fsr3_contexts_configured_ = false;
bool fsr3_estimate_motion_ = true;
bool fsr3_depth_inverted_ = false, fsr3_depth_infinite_ = false;
std::uint64_t fsr3_camera_identity_[2]{};
float fsr3_camera_near_[2]{}, fsr3_camera_far_[2]{};
static constexpr int upscale_mode_ = 0;
FsrSettings fsr_settings_{};
bool fsr_failure_logged_ = false;
std::string guest_device_key_;
static std::string VulkanDeviceKey(const VkDeviceCreateInfo* info);
void ReleaseGuestRendererLocked();
unsigned guest_device_generation_ = 0;
bool defer_initial_session_ = false;
std::string guest_instance_key_;
VkDevice vk_device_ = VK_NULL_HANDLE;
VkPhysicalDevice vk_physical_device_ = VK_NULL_HANDLE;
std::uint32_t vk_queue_family_ = 0;
VkQueue vk_graphics_queue_ = VK_NULL_HANDLE;
std::vector<std::uint32_t> device_queue_families_;
bool single_guest_queue_ = false;
void* vk_loader_ = nullptr;
struct VkProcs;
VkProcs* vk_ = nullptr;
VkCommandPool copy_pool_ = VK_NULL_HANDLE;
// Synchronous one-off work: keyboard uploads, evidence, present fallback.
VkCommandBuffer copy_command_ = VK_NULL_HANDLE;
VkFence copy_fence_ = VK_NULL_HANDLE;
// Two submissions per frame, so three frames may be in flight.
static constexpr std::size_t kAsyncSubmissions = 6;
std::array<AsyncSubmission, kAsyncSubmissions> async_{};
std::size_t next_async_ = 0;
MirrorReadback mirror_readback_{};
FxaaState fxaa_{};
// The mirror's signal for this present, which PrepareHostPresent passes on.
VkSemaphore mirror_ready_ = VK_NULL_HANDLE;
// Semaphores of destroyed desktop swapchains. A present may still wait on
// one, so they are destroyed only once the device is idle.
std::vector<VkSemaphore> retired_semaphores_;
std::unordered_map<VkFormat, VkFormatFeatureFlags> blit_features_;
// Optional desktop two-eye mirror (MOCKTAIL_VR_MIRROR_SHM). Publishes the
// latest complete pair through shared memory; inert when unset.
VrMirror mirror_;
std::unordered_map<VkImage, ImageRecord> images_;
struct DesktopSwapchain {
VkExtent2D extent;
VkFormat format;
std::vector<VkImage> images;
VkSharingMode sharing_mode = VK_SHARING_MODE_EXCLUSIVE;
std::vector<std::uint32_t> families;
bool converted_to_concurrent = false;
// Two per image, see PresentReadySemaphoreLocked.
std::vector<VkSemaphore> ready;
};
std::unordered_map<VkSwapchainKHR, DesktopSwapchain> desktop_swapchains_;
std::unordered_map<VkImageView, VkImage> image_views_;
std::unordered_map<VkFramebuffer, std::vector<VkImageView>> framebuffer_views_;
std::unordered_map<VkFramebuffer, VkRenderPass> framebuffer_render_pass_;
std::unordered_map<VkRenderPass, std::vector<VkImageLayout>> render_pass_final_layouts_;
std::unordered_map<VkQueue, std::uint32_t> queue_families_;
struct CommandRecord {
VkCommandPool pool = VK_NULL_HANDLE;
std::uint32_t family = UINT32_MAX;
std::vector<std::pair<VkImage, std::uint64_t>> eye_images;
};
std::unordered_map<VkCommandPool, std::uint32_t> command_pool_families_;
std::unordered_map<VkCommandBuffer, CommandRecord> command_records_;
std::unordered_map<VkImage, VkQueue> eye_render_queues_;
std::unordered_map<VkImage, std::uint64_t> eye_render_frames_;
std::uint64_t image_order_ = 0;
EyeBinding eyes_[2];
// Frame cycle state (render thread only).
bool frame_open_ = false;
bool frame_should_render_ = false;
// A running session is hidden in SYNCHRONIZED; shouldRender still governs rendering.
bool visibility_lost_ = false;
bool session_recovery_pending_ = false;
std::uint64_t next_session_retry_ns_ = 0;
// Consecutive failed session recoveries, for backoff and for deciding when
// the XrInstance itself must be recreated (e.g. after a runtime restart).
std::uint32_t session_recovery_failures_ = 0;
bool xr_instance_stale_ = false;
bool present_sync_failure_logged_ = false;
bool gles_attach_failed_ = false;
// Runtime swapchain formats, and the format chosen once the guest eye
// format is known (VK_FORMAT_UNDEFINED until then).
std::vector<std::int64_t> xr_swapchain_formats_;
VkFormat eye_swapchain_format_ = VK_FORMAT_UNDEFINED;
VkFormat eye_format_checked_ = VK_FORMAT_UNDEFINED;
std::uint64_t frame_display_time_ = 0;
// MOCKTAIL_VR_UNPACED benchmark mode: guest presents before this monotonic
// time leave the open XR frame alone instead of blocking in xrWaitFrame.
std::uint64_t frame_due_ns_ = 0;
struct ViewState {
float position[3];
float orientation[4];
float fov[4]; // angleLeft, angleRight, angleUp, angleDown (radians)
bool valid;
};
ViewState frame_views_[2] = {};
struct PoseStorage {
float position[3] = {0.f, 0.f, 0.f};
float orientation[4] = {0.f, 0.f, 0.f, 1.f};
};
PoseStorage frame_view_poses_[2];
bool frame_views_valid_ = false;
// One-shot guard so a canted-view runtime reports the rejection reason once
// instead of once per frame per eye.
bool projection_rejection_logged_ = false;
mutable ScriptedPoseSample published_pose_;
std::uint64_t xr_frame_counter_ = 0;
// All begun XR frames, including an unworn/invisible headset.
std::uint64_t xr_begun_frames_ = 0;
std::unordered_map<void*, std::uint64_t> applied_poses_;
};
// Process-wide single backend owned by main. Adapter hooks resolve these
// exported C symbols; they are inert while no backend is armed.
OpenXrBackend* ActiveVrBackend();
// Paths of the shared objects mapped into the process.
std::vector<std::string> LoadedSharedObjects();
// Paths mapped now that were not in `before`.
std::vector<std::string> SharedObjectsLoadedSince(
const std::vector<std::string>& before);
// Holds the runtime's libraries open with RTLD_DEEPBIND while the OpenXR
// loader loads the runtime, so its Vulkan calls bind to its own libvulkan
// dependency instead of the Vulkan adapter's exported hooks. Released after
// the loader holds its own reference, so the runtime still unloads with its
// instance.
class DeepBoundLibraries {
public:
explicit DeepBoundLibraries(const std::vector<std::string>& paths);
~DeepBoundLibraries();
DeepBoundLibraries(const DeepBoundLibraries&) = delete;
DeepBoundLibraries& operator=(const DeepBoundLibraries&) = delete;
private:
std::vector<void*> handles_;
};
} // namespace mocktail::vr
#endif // MOCKTAIL_VR_OPENXR_BACKEND_H_
+34
View File
@@ -0,0 +1,34 @@
#ifndef MOCKTAIL_VR_OPENXR_PREVIEW_H_
#define MOCKTAIL_VR_OPENXR_PREVIEW_H_
#include <array>
#include <cstdint>
#include <string>
namespace mocktail::vr {
struct PreviewOptions {
// Zero keeps the scene open until the runtime or user requests exit.
std::uint32_t frame_limit = 0;
};
struct PreviewResult {
bool success = false;
std::string message;
std::uint64_t frames = 0;
std::uint64_t rendered_frames = 0;
std::uint64_t tracked_frames = 0;
std::array<float, 3> head_position{};
std::array<float, 4> head_orientation{0, 0, 0, 1};
};
// Runs a native Vulkan/OpenXR scene with a panel fixed in LOCAL space. The
// compositor projects it into both eyes. Roblox images/input are not connected.
// Call on the main thread: SIGINT/SIGTERM request a graceful session exit, and
// their previous handlers are restored before returning.
PreviewResult RunOpenXrPreview(const PreviewOptions& options = {});
std::string FormatPreviewResult(const PreviewResult& result);
} // namespace mocktail::vr
#endif // MOCKTAIL_VR_OPENXR_PREVIEW_H_
+52
View File
@@ -0,0 +1,52 @@
#ifndef MOCKTAIL_VR_OPENXR_PROBE_H_
#define MOCKTAIL_VR_OPENXR_PROBE_H_
#include <cstdint>
#include <string>
#include <vector>
namespace mocktail::vr {
enum class ProbeStatus {
kReady,
kBuildDisabled,
kRuntimeUnavailable,
kHeadsetUnavailable,
kIncompatibleRuntime,
kError,
};
struct ViewConfiguration {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t samples = 0;
};
struct ProbeReport {
ProbeStatus status = ProbeStatus::kError;
std::string message;
std::string sdk_version;
std::string runtime_name;
std::string runtime_version;
std::string system_name;
std::vector<std::string> extensions;
std::vector<ViewConfiguration> views;
bool orientation_tracking = false;
bool position_tracking = false;
std::string minimum_vulkan_version;
std::string maximum_vulkan_version;
};
// Native host discovery only. Does not load Roblox, change the active runtime,
// create a graphics device/session, or claim that VR rendering is implemented.
ProbeReport ProbeOpenXr();
std::string FormatProbeReport(const ProbeReport& report);
int ProbeExitCode(ProbeStatus status);
// False in builds configured without MOCKTAIL_ENABLE_VR; --vr fails closed
// early in that case instead of starting Roblox without VR support.
bool IsOpenXrSupportCompiled();
} // namespace mocktail::vr
#endif // MOCKTAIL_VR_OPENXR_PROBE_H_
+50
View File
@@ -0,0 +1,50 @@
#ifndef MOCKTAIL_VR_PERFORMANCE_STATS_H_
#define MOCKTAIL_VR_PERFORMANCE_STATS_H_
#include <cstdint>
#include <deque>
namespace mocktail::vr {
struct PerformanceSnapshot {
std::uint64_t timestamp_ns = 0;
std::uint64_t frames = 0;
std::uint64_t last_frame_ns = 0;
std::uint64_t process = 0;
std::uint64_t width = 0, height = 0;
bool visible = false;
std::uint64_t render_width = 0, render_height = 0;
};
// Count successful application projections over a rolling second. Neither
// the scheduler's cap nor compositor repeats count as new game frames.
class PerformanceRate {
public:
void Reset() { samples_.clear(); }
double Sample(std::uint64_t now, const PerformanceSnapshot& snapshot) {
if (!snapshot.visible || snapshot.timestamp_ns > now ||
now - snapshot.timestamp_ns > kSecond ||
(!samples_.empty() && (now <= samples_.back().time ||
snapshot.frames < samples_.back().frames))) {
Reset();
return -1.;
}
samples_.push_back({now, snapshot.frames});
const auto cutoff = now > kSecond ? now - kSecond : 0;
while (samples_.size() > 2 && samples_[1].time <= cutoff)
samples_.pop_front();
if (!snapshot.last_frame_ns || snapshot.last_frame_ns > now ||
now - snapshot.last_frame_ns >= kSecond) return 0.;
const auto elapsed = now - samples_.front().time;
if (elapsed < kSecond / 2) return -1.;
return double(snapshot.frames - samples_.front().frames) * 1.e9 /
double(elapsed);
}
private:
static constexpr std::uint64_t kSecond = 1'000'000'000;
struct SamplePoint { std::uint64_t time, frames; };
std::deque<SamplePoint> samples_;
};
} // namespace mocktail::vr
#endif
+122
View File
@@ -0,0 +1,122 @@
#ifndef MOCKTAIL_VR_PERFORMANCE_TELEMETRY_H_
#define MOCKTAIL_VR_PERFORMANCE_TELEMETRY_H_
#include "mocktail/vr/performance_stats.h"
#include <atomic>
#include <cstdlib>
#include <new>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
namespace mocktail::vr {
// The launcher owns a private tmpfs file for the lifetime of one application.
// Only lock-free stores run in the frame path: no file writes, GPU waits or
// frame history allocations. A separate noninteractive overlay reads it.
struct alignas(64) SharedPerformanceStats {
static constexpr std::uint64_t kMagic = 0x4e43504600000002ULL;
std::atomic<std::uint64_t> magic{0}, sequence{0};
std::atomic<std::uint64_t> timestamp_ns{0}, frames{0}, last_frame_ns{0};
std::atomic<std::uint64_t> process{0}, width{0}, height{0}, visible{0};
std::atomic<std::uint64_t> render_width{0}, render_height{0};
};
static_assert(std::atomic<std::uint64_t>::is_always_lock_free);
class PerformanceMapping {
public:
PerformanceMapping() = default;
PerformanceMapping(const PerformanceMapping&) = delete;
PerformanceMapping& operator=(const PerformanceMapping&) = delete;
~PerformanceMapping() {
if (record_) munmap(record_, sizeof(SharedPerformanceStats));
}
bool Open(const char* path, bool writer) {
if (record_) return true;
if (!path || path[0] != '/') return false;
const int fd = open(path, (writer ? O_RDWR : O_RDONLY) | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) return false;
struct stat info{};
if (fstat(fd, &info) || !S_ISREG(info.st_mode) || info.st_uid != geteuid() ||
(info.st_mode & 0077) || info.st_size < static_cast<off_t>(sizeof(SharedPerformanceStats))) {
close(fd);
return false;
}
void* mapped = mmap(nullptr, sizeof(SharedPerformanceStats),
writer ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, fd, 0);
close(fd);
if (mapped == MAP_FAILED) return false;
record_ = static_cast<SharedPerformanceStats*>(mapped);
if (writer) {
new (record_) SharedPerformanceStats;
record_->magic.store(SharedPerformanceStats::kMagic, std::memory_order_release);
}
return true;
}
void Publish(const PerformanceSnapshot& value) {
if (!record_) return;
record_->sequence.fetch_add(1, std::memory_order_acq_rel);
record_->timestamp_ns.store(value.timestamp_ns, std::memory_order_relaxed);
record_->frames.store(value.frames, std::memory_order_relaxed);
record_->last_frame_ns.store(value.last_frame_ns, std::memory_order_relaxed);
record_->process.store(value.process, std::memory_order_relaxed);
record_->width.store(value.width, std::memory_order_relaxed);
record_->height.store(value.height, std::memory_order_relaxed);
record_->visible.store(value.visible, std::memory_order_relaxed);
record_->render_width.store(value.render_width, std::memory_order_relaxed);
record_->render_height.store(value.render_height, std::memory_order_relaxed);
record_->sequence.fetch_add(1, std::memory_order_release);
}
bool Read(PerformanceSnapshot* value) const {
if (!record_ || record_->magic.load(std::memory_order_acquire) != SharedPerformanceStats::kMagic)
return false;
for (int attempt = 0; attempt < 8; ++attempt) {
const auto sequence = record_->sequence.load(std::memory_order_acquire);
if (sequence & 1) continue;
PerformanceSnapshot snapshot;
snapshot.timestamp_ns = record_->timestamp_ns.load(std::memory_order_relaxed);
snapshot.frames = record_->frames.load(std::memory_order_relaxed);
snapshot.last_frame_ns = record_->last_frame_ns.load(std::memory_order_relaxed);
snapshot.process = record_->process.load(std::memory_order_relaxed);
snapshot.width = record_->width.load(std::memory_order_relaxed);
snapshot.height = record_->height.load(std::memory_order_relaxed);
snapshot.visible = record_->visible.load(std::memory_order_relaxed) != 0;
snapshot.render_width = record_->render_width.load(std::memory_order_relaxed);
snapshot.render_height = record_->render_height.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (sequence == record_->sequence.load(std::memory_order_relaxed)) {
*value = snapshot;
return true;
}
}
return false;
}
private:
SharedPerformanceStats* record_ = nullptr;
};
class PerformancePublisher {
public:
void Update(std::uint64_t now, std::uint64_t frames, bool visible,
std::uint32_t width, std::uint32_t height,
std::uint32_t render_width, std::uint32_t render_height) {
if (!attempted_) {
attempted_ = true;
process_ = static_cast<std::uint64_t>(getpid());
enabled_ = mapping_.Open(std::getenv("NIGHTCAP_PERFORMANCE_TELEMETRY"), true);
}
if (!enabled_) return;
if (frames != previous_frames_) last_frame_ns_ = now;
previous_frames_ = frames;
mapping_.Publish({now, frames, last_frame_ns_, process_,
width, height, visible, render_width, render_height});
}
private:
PerformanceMapping mapping_;
bool attempted_ = false, enabled_ = false;
std::uint64_t previous_frames_ = 0, last_frame_ns_ = 0, process_ = 0;
};
} // namespace mocktail::vr
#endif
+136
View File
@@ -0,0 +1,136 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
namespace mocktail::vr {
inline bool ValidRasterJitter(bool enabled, const float (&jitter_pixels)[2],
std::uint32_t width, std::uint32_t height) {
return enabled && width && height &&
std::isfinite(jitter_pixels[0]) && std::isfinite(jitter_pixels[1]) &&
std::abs(jitter_pixels[0]) <= 1.f &&
std::abs(jitter_pixels[1]) <= 1.f;
}
// Adjusts the native projection's positive {up, down, left, right} tangent
// extents. Jitter is image displacement in render pixels: +X right, +Y down.
// With the existing right-handed camera looking down -Z, a ray projects to
// u=(x/-z+left)/(left+right), v=(up-y/-z)/(up+down). Translating both frustum
// bounds preserves its angular span and displaces every ray by the same pixels.
// Only the caller-owned projection changes; runtime FOV and tracking do not.
inline bool ApplyRasterProjectionJitter(float (&tangents)[4], bool enabled,
const float (&jitter_pixels)[2],
std::uint32_t render_width,
std::uint32_t render_height) {
if (!ValidRasterJitter(enabled, jitter_pixels, render_width, render_height))
return false;
for (float value : tangents)
if (!std::isfinite(value) || value <= 0.f)
return false;
const double dx = static_cast<double>(jitter_pixels[0]) *
(static_cast<double>(tangents[2]) + tangents[3]) /
render_width;
const double dy = static_cast<double>(jitter_pixels[1]) *
(static_cast<double>(tangents[0]) + tangents[1]) /
render_height;
const std::array<float, 4> shifted = {
static_cast<float>(tangents[0] + dy),
static_cast<float>(tangents[1] - dy),
static_cast<float>(tangents[2] + dx),
static_cast<float>(tangents[3] - dx)};
for (float value : shifted)
if (!std::isfinite(value) || value <= 0.f)
return false;
for (int i = 0; i < 4; ++i)
tangents[i] = shifted[i];
return true;
}
// The verified native Camera getter mirrors M02 for the right eye. Apply jitter
// after that operation to give both eyes the same displacement. Vulkan image Y
// increases downward; the verified native scene shader negates clip Y before
// its Vulkan viewport, while camera-space Y and this projection increase up.
inline bool ApplyRasterMatrixProjectionJitter(float (&matrix)[16], bool enabled,
const float (&jitter_pixels)[2],
std::uint32_t render_width,
std::uint32_t render_height) {
if (!ValidRasterJitter(enabled, jitter_pixels, render_width, render_height))
return false;
for (float value : matrix)
if (!std::isfinite(value))
return false;
if (matrix[0] <= 0.f || matrix[5] <= 0.f || matrix[14] != -1.f ||
matrix[15] != 0.f)
return false;
const float x = matrix[2] - 2.f * jitter_pixels[0] / render_width;
const float y = matrix[6] + 2.f * jitter_pixels[1] / render_height;
if (!std::isfinite(x) || !std::isfinite(y))
return false;
matrix[2] = x;
matrix[6] = y;
return true;
}
inline bool ApplyRasterViewProjectionJitter(float (&matrix)[16], bool enabled,
const float (&jitter_pixels)[2],
std::uint32_t render_width,
std::uint32_t render_height) {
if (!ValidRasterJitter(enabled, jitter_pixels, render_width, render_height))
return false;
for (float value : matrix)
if (!std::isfinite(value))
return false;
double denominator_norm = 0.;
for (int i = 12; i < 16; ++i)
denominator_norm += static_cast<double>(matrix[i]) * matrix[i];
if (denominator_norm <= 0.)
return false;
const float dx = 2.f * jitter_pixels[0] / render_width;
const float dy = -2.f * jitter_pixels[1] / render_height;
float shifted[8];
for (int column = 0; column < 4; ++column) {
shifted[column] = matrix[column] + dx * matrix[12 + column];
shifted[4 + column] = matrix[4 + column] + dy * matrix[12 + column];
}
for (float value : shifted)
if (!std::isfinite(value))
return false;
std::copy_n(shifted, 8, matrix);
return true;
}
// The observed native reverse-Z perspective builder writes M22=n/(f-n),
// M23=n*f/(f-n), M32=-1. Derive its actual clip planes without assuming a
// fixed near plane, which the client changes with its camera mode.
inline bool DecodeReverseProjectionDepth(const float (&matrix)[16],
float* camera_near,
float* camera_far,
bool* infinite) {
if (!camera_near || !camera_far || !infinite || matrix[14] != -1.f ||
matrix[15] != 0.f)
return false;
for (float value : matrix)
if (!std::isfinite(value))
return false;
const double a = matrix[10], b = matrix[11];
if (a < 0. || b <= 0.)
return false;
const float near = static_cast<float>(b / (a + 1.));
const bool is_infinite = a == 0.;
const float far = is_infinite ? std::numeric_limits<float>::infinity()
: static_cast<float>(b / a);
if (!std::isfinite(near) || near <= 0.f ||
(!is_infinite && (!std::isfinite(far) || far <= near)))
return false;
*camera_near = near;
*camera_far = far;
*infinite = is_infinite;
return true;
}
} // namespace mocktail::vr
@@ -0,0 +1,29 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <vector>
namespace mocktail::vr::internal {
struct CpuCapacity { unsigned id; unsigned capacity; };
// Input contains only CPUs already allowed for this thread. Keep near-peer
// performance cores together, and retain the original mask if information is
// incomplete or a preference would leave fewer than two available cores.
inline std::vector<unsigned> PerformanceCpuSubset(
const std::vector<CpuCapacity>& allowed) {
std::vector<unsigned> original;
unsigned maximum = 0;
bool complete = true;
for (const auto& cpu : allowed) {
original.push_back(cpu.id);
maximum = std::max(maximum, cpu.capacity);
complete = complete && cpu.capacity > 0;
}
if (!complete || !maximum || allowed.size() < 2) return original;
std::vector<unsigned> selected;
for (const auto& cpu : allowed)
if (std::uint64_t(cpu.capacity) * 5 >= std::uint64_t(maximum) * 4)
selected.push_back(cpu.id);
return selected.size() >= 2 ? selected : original;
}
} // namespace mocktail::vr::internal
@@ -0,0 +1,131 @@
#ifndef MOCKTAIL_VR_ROBLOX_VR_DEVICE_BRIDGE_H_
#define MOCKTAIL_VR_ROBLOX_VR_DEVICE_BRIDGE_H_
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string>
#include <string_view>
#include "compat/build_profile.h"
#include "mocktail/status.h"
#include "mocktail/vr/roblox_vr_guest_abi.h"
namespace mocktail::vr {
namespace internal {
struct VrBridgeTestAccess;
struct VrBuildContract;
}
const compat::VrDebugDeviceBridgeProfile* FindRobloxVrDeviceProfile(
std::string_view build_id);
// Human-readable list of the Roblox builds VR supports, e.g.
// "2998 (2.736.1408) and 3092 (2.738.1397)", for error messages.
std::string DescribeSupportedVrBuilds();
// Experimental native stereo bring-up for the exact Build-ID-scoped
// RBX::Graphics::DebugDeviceVR object of the guest client.
//
// The bridge interposes the state and eye getters of the exact guest build.
// Resource readiness is checked on the live object during each state call;
// initialization never dereferences cached guest object/device pointers. The
// guest owns resource destruction. Recreated objects and reused addresses
// therefore need no cache invalidation. Initialization happens only on a thread
// observed presenting.
class RobloxVrDeviceBridge final {
public:
using StateGetterFn = guest_abi::StateGetterFn;
using EyeGetterFn = void *(*)(void *device_object, int eye_index);
using EyeInitializerFn = void (*)(void *device_object, void *graphics_device);
RobloxVrDeviceBridge() = default;
~RobloxVrDeviceBridge();
RobloxVrDeviceBridge(const RobloxVrDeviceBridge &) = delete;
RobloxVrDeviceBridge &operator=(const RobloxVrDeviceBridge &) = delete;
// Arms the bridge for an exact-build profile that carries
// vr_debug_device_bridge metadata. Claims the process-wide bridge slot.
Status Install(const compat::BuildProfile &profile);
// Validates machine contracts at the loaded image base and interposes the
// two DebugDeviceVR vtable slots. Called once, after libroblox relocations
// and before any guest startup code runs.
Status Activate(std::uintptr_t image_base);
// Restores the interposed vtable slots and disarms. Safe to call twice.
void Shutdown();
bool installed() const { return installed_; }
bool active() const { return active_.load(std::memory_order_acquire); }
// Render-thread notification from the Vulkan adapter's host present path.
void NoteHostPresent();
// Guest-side hooks invoked by the interposed vtable stubs.
void OnHapticsCall(void* device_object, int hand, float amplitude);
// HapticService:SetMotor on Gamepad1 LeftHand (0) / RightHand (1); the
// level holds until the game sets it again.
void OnSetMotorCall(int hand, float amplitude);
void OnPointerFrameCall(void* result, void* service);
void OnStateGetterCall(void *result_buffer, void *device_object);
void OnEyeGetterCall(void *device_object, int eye_index, void *framebuffer);
StateGetterFn original_state_getter() const {
return original_state_getter_.load(std::memory_order_acquire);
}
EyeGetterFn original_eye_getter() const {
return original_eye_getter_.load(std::memory_order_acquire);
}
// Cumulative initialization diagnostics (not a count of live devices).
std::uintptr_t image_base() const {
return image_base_.load(std::memory_order_acquire);
}
int captured_device_count() const;
int initialized_device_count() const;
std::uint64_t present_count() const {
return present_count_.load(std::memory_order_relaxed);
}
std::uint64_t eye_getter_call_count(int eye_index) const;
private:
friend struct internal::VrBridgeTestAccess;
static constexpr std::uint64_t kEyeGetterSummaryInterval = 600;
bool ValidateObjectHeader(void *self) const;
void *ValidatedGraphicsDevice(void *self) const;
void TryInitializeEyes(void *self);
bool RestoreVtableLocked();
mutable std::mutex mutex_;
compat::VrDebugDeviceBridgeProfile profile_{};
const internal::VrBuildContract* contract_ = nullptr;
bool installed_ = false;
std::atomic<bool> active_{false};
std::atomic<std::uintptr_t> image_base_{0};
std::atomic<std::uintptr_t> image_size_{0};
std::uintptr_t *vtable_ = nullptr;
std::uintptr_t original_haptic_slot_ = 0;
std::uintptr_t original_state_slot_ = 0;
std::uintptr_t original_eye_slot_ = 0;
std::atomic<StateGetterFn> original_state_getter_{nullptr};
std::atomic<EyeGetterFn> original_eye_getter_{nullptr};
std::atomic<std::uint64_t> present_thread_{0};
std::atomic<std::uint64_t> present_count_{0};
std::atomic<bool> flag_warning_logged_{false};
std::atomic<int> captured_count_{0};
std::atomic<int> initialized_count_{0};
mutable std::atomic<std::uint64_t> eye_call_totals_[2] = {};
};
// Process-wide single active bridge, mirroring the audio output-device
// bridge ownership model. Install claims the slot; the destructor releases
// it. legacy_runtime forwards the loaded libroblox image base through
// NotifyRobloxVrImageLoaded; it is a successful no-op while no bridge is
// armed, and fail-closed once one is.
Status NotifyRobloxVrImageLoaded(std::uintptr_t image_base);
} // namespace mocktail::vr
#endif // MOCKTAIL_VR_ROBLOX_VR_DEVICE_BRIDGE_H_
Loaded 100 of 763 files, more files were not shown because too many files have changed in this diff. Show more