Files
InstanceBrick--nightcap/docs/DEVELOPMENT.md
T

11 KiB
Raw Blame History

Building and source layout

The installation guide 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, 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:

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:

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:

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. 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.