Doom3Quest-based ports (PreyVR, #306) draw their menu on a curved
cylinder layer submitted once per eye. The Frame's runtime has no
cylinder layers, so FrameBridge shows each as ~7 flat quad strips; a
per-eye pair is ~14 quad layers in one frame, which the runtime rejects
whole (xrEndFrame -> XR_ERROR_LAYER_INVALID, -16). The menu then stays
black while the game runs at 72 fps.
Collapse the pair: drop the right-eye cylinder and show the left eye's
strips to both eyes (the runtime composites a quad to both eyes anyway).
That halves the layer count and drops the per-eye eyeVisibility, and the
runtime accepts the frame. Only frames with per-eye cylinders change --
exactly the ones that were being rejected; mono (BOTH) cylinder games
such as 4XVR are untouched.
Verified on the dev Frame (headless): PreyVR v1.2.4 rebuilt + installed,
the menu frame now submits (one transient -16 on the first frame before
the space is locatable, then clean), 72 fps. Visual confirmation of the
menu needs the headset. Triage signature cylinder-menu-layers-rejected.
No adapter revision bump (installed games are not marked outdated).
Diagnostics only (no behaviour change): the "new layer ... usable=0"
line prints a layer's swapchain pointer, but creation logged only the
result, so a dropped layer couldn't be matched to the xrCreateSwapchain
that made (or failed to make) its swapchain. Needed to pin why a quad
"screen layer" is dropped (#306 PreyVR: menu rendered to a quad whose
swapchain FrameBridge reports as not usable).
The GLES-only stand-in left Vulkan games (UNDERDOGS: VkFormat 43, faces=6) with the runtime's refusal, so OVRPlugin
crashed in ovrp_EndFrame4. When there's no GLES context, the adapter now makes a cube-compatible VkImage (6 layers,
the requested format/size/mips, usage from the XR flags) with the app's device/queue and a private transient pool,
transitions it to COLOR_ATTACHMENT_OPTIMAL, and enumerates it as a Vulkan image; its cube layers are still dropped.
GLES behaviour is unchanged. Adapter rebuilt deterministically (both ABIs).
Merged main (agent 90, ANALYSIS_VERSION 17/18 after main's 15/16) and rebuilt the bridge, foveation and a32
libraries from source (deterministic, no local paths).
Fixes on top:
- a32 loader: apply DT_ANDROID_RELR relocations (Journey of the Gods crashed at start)
- VrApi bridge: vrapi_Get/SetTrackingTransform (floor-level tracking; Shadow Point's player was below the floor)
- a32: never submit an untracked default head pose (hold up to 100 ms, repair or drop); pose consistency per display
time; submit/tracking counters
- VrApi bridge: wait again when xrWaitFrame gives a display time in the refresh just submitted (the runtime rejected
the second frame with AlreadySubmitted; Journey of the Gods ran at ~120 fps on a 90 Hz display and the scene slid
and snapped back); pacing counters
- VrApi bridge: GLES re-entry polls events before reusing the kept session (a lost or exiting session is replaced, a
stopped one waits for READY)
- VrApi bridge: STAGE is the runtime's STAGE space (play-area centre and orientation, unchanged by recentering);
LOCAL_FLOOR keeps the head-based origin
- signer certificate: pick the certificate the SignerInfo names, not the first in the PKCS#7 block
- flat 32-bit apps keep the bridge (it isn't a VR-only fix)
- Frida gadget loads are stripped before translation
- launcher restart test fixed; native/.cache is no longer committed
- catalog: Shadow Point and Journey of the Gods work (headset, 2026-10-14; min_app 1.1.2)
Rebuilt libvrapi.so and libvrapi_foveation.so without the local NDK paths a worktree build left in their debug info; a test checks that no artifact contains a local path.
Holding the Frame's menu button turned 32-bit (and other VrApi bridge)
games 90-120 degrees to the side. The bridge's app space is LOCAL plus
the head's yaw at the start; the runtime's recenter moves LOCAL to the
head, so that old yaw was applied again. On a LOCAL reference space
change (and on vrapi_RecenterPose, ignored before) the origin is taken
from the head again in the first frame after the change, and VrApi's
recenter count goes up. Owner-verified with Gunjack.
The upstream patches are regenerated against OVRPort 5e7df52 (they
were missing the FOV cache changes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sports Scramble (a 32-bit Santa Cruz build: UE 4.21, OVRPlugin on VrApi 1.21)
now plays in the headset with both controllers.
a32 bridge:
- AAsset_getBuffer: the host buffer lies above 4 GiB, so the guest gets a
copy in its own memory, freed in AAsset_close (Unreal read its
UE4CommandLine.txt this way and aborted on the NULL it got before)
- AKeyEvent_getFlags and the remaining AConfiguration getters
- Touch controllers for Quest builds: apps that require 6DoF head tracking
(uses-feature android.hardware.vr.headtracking version 1) get
assets/fp_a32_headset.txt = "quest" at conversion; the bridge then passes
both Frame controllers through (input devices, caps, state, tracking,
vibration; struct tails per VrApi minor) and sends no synthetic touchpad
events. Gear VR / Go games keep the remote emulation. A32_TOUCH=1/0
overrides.
Analysis: detect.requires_6dof, headset "quest" for such 32-bit apps
(ANALYSIS_VERSION 13).
Agent 82: Unreal restarts the app once after its first-run shader precompile
(the "optimizing" screen): it starts its own launcher activity and exits, and
Lepton ends the container with the first process. Skipping the restart or
turning it off (r.ProgramBinaryCache.RestartAndroidAfterPrecompile=0) left
the game on that screen for good. launch.sh now starts Lepton once more when
the system log shows the app's own LAUNCHER start right before its
System.exit(0) (_restart_requested; first run kept as
launch-before-restart.log; FRAMEPORT_NO_RESTART=1 turns it off).
upgrade_launchers adds it to existing launchers. Crashes, quitting from the
game and Steam's Exit game don't match.
Catalog: Sports Scramble works (owner, headset, 2026-10-13).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiled Gunjack and Spunky Dungeon on the Frame with perf and a new
A32_PERFMAP option (a perf map of the translated code: block bodies, exit
stubs, FPCR writes). FPCR switches turned out negligible (8 of 86k samples,
none in Gunjack). The cost was elsewhere:
- The run loop took the translation mutex on every dispatch and walked a hash
chain under it (Gunjack: jit_run 23.6 % of all samples, pthread_mutex_*
about 7 %, plus futex time in the kernel).
- The inline lookup table for indirect branches was only written at
translation, so a hot return target lost its slot for good to any block
translated later into the same slot and went through the run loop ever after.
- Every dispatch did an atomic add/sub on a counter all threads share (4.7 %).
- jit_invalidate scanned every block on each cacheflush that touched a code
page: Unity's Mono JIT does that constantly (72 % of Spunky Dungeon's main
thread in one run).
Now:
- the run loop looks blocks up without the lock (inline table first, then
the hash chains, published with release stores); the lock is only taken to
translate or to chain the exit it came from
- it refills the inline table with every block it dispatches; the refill and
invalidate_block are fenced against each other, so an invalidated block
never stays reachable
- HASH_BITS / LTAB_BITS 16 -> 18 (Gunjack translates well over 100k blocks)
- a per-thread in_jit_section flag + fence instead of the shared counter;
flush_cache sets flush_pending and waits for the flags (threads_any_in_jit)
- per-guest-page block lists: an invalidation visits only the blocks on the
affected pages and drops invalid ones from the lists on the way
Results on the Frame (same build otherwise, same launch conditions):
- Gunjack, 25-45 s after start with OVP_HEADLESS_TEST: 100,462 -> about
45,000 samples in 20 s (about 55 % less CPU), jit_run 23.6 % -> 1.6 %,
pthread_mutex_lock about 7 % -> 0.4 %
- Gunjack, 12-27 s: 72 fps in both, 11,306 -> 9,801 samples, jit_run
10.4 % -> 1.0 %
- Spunky Dungeon, 12-27 s: 72 fps in both; translated code is only 2-3 %
there, so no change. The jit_invalidate phase wasn't reproducible headless
under identical conditions: not yet measured in a played session.
- bench.c (single thread, few dispatches): unchanged.
Tests: run_programs.py (jit + interp, and A32_CACHE_KB=300), bridge harness,
fuzz.py arm-dp / t32 / arm-vfp / arm-neon (0 mismatches), pytest. Both games
pass their launch tests at 72 fps with the final build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gunjack (UE 4.10, Gear VR) has no separate reload or special-weapon input on
the touchpad: every swipe goes to UE4's Gesture_Flick, and
ATDPlayerController::OnFlick reads the flick angle. Below 45 or above 315
degrees (+X, a swipe towards the back of the Gear VR touchpad) it reloads;
135 to 225 degrees (-X, a swipe forward) fires the special weapon. Up and down
swipes do nothing. The bridge sent the stick's up/down as Y swipes, so in
Gunjack those directions did nothing.
For games that read their AInputQueue (NativeActivity), the right stick's
up/down now swipe along the touchpad's own X axis: up = swipe forward
(special weapon), down = swipe back (reload). Left/right are unchanged.
Unity games (AFFECTED, Spunky Dungeon) keep their mapping.
Rebuilt liba32bridge.so. Gunjack was rebuilt and installed on the dev Frame;
the headless launch test passes. Not yet tried in the headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gunjack (UE 4.10, Gear VR) imports no vrapi input functions: it reads the
Gear VR touchpad and back button as Android input events from its
AInputQueue (native_app_glue). The bridge delivered its controller events
through Activity.dispatchKeyEvent, which never reaches native code, so no
control worked at all.
input.c now queues synthetic events in front of the game's AInputQueue:
- AInputQueue_attachLooper (without a callback) registers the queue;
ALooper_pollAll/pollOnce return its ident while a synthetic event waits
(ALooper_wake ends a blocking poll), AInputQueue_getEvent hands it out,
finishEvent frees it.
- The AInputQueue/AInputEvent/AKeyEvent/AMotionEvent imports are written
by hand ("!" in imports.txt) and answer for synthetic events; real
events go to libandroid as before.
- Touch positions are scaled to the activity's window size
(native_activity.c records it).
Mapping (right controller, as for AFFECTED: The Manor): trigger = hold
the touchpad, A = tap, right stick = swipe, B / left menu = back. Unity
and other Java-delivered games are unchanged. A new input_start (VR
re-entry) ends the previous poll thread.
Also: the libandroid functions the host side calls directly are weak
references again, so the PC test harness links (it broke with the
AAsset calls of the signature loader); bridge/test/run.py passes.
Headless on the Frame: "controllers: the game reads its AInputQueue",
72 fps. Not yet verified in the headset.
Gunjack (CCP, UE 4.10, Gear VR) quit after loading because AndroidMain compares
GameActivity.AndroidThunkJava_GetSignature() with the original signing
certificate, and then crashed in the Frame's runtime when it entered VR again.
a32 bridge:
- frame.a32_bridge: original_signature() = Base64.DEFAULT(SHA-1(signing
certificate DER)) of the original APK (META-INF/*.RSA|DSA|EC), stored as
assets/fp_a32_signature.txt for libUE4.so games (convert(), shared with
bridge/convert.py)
- the bridge loads it at activity creation and answers guest calls on the
method ID of AndroidThunkJava_GetSignature()Ljava/lang/String; with it
(replaces the unused hex/OpenSSL prototype)
VrApi bridge: after loading, Unreal leaves VR mode and enters it again on the
same GL context; the first frame of a second OpenXR session there aborted in
vrclient (CSharedSemaphoreLinux::WaitFromGPU from the GL binding's
PreBeginFrame). vrapi_LeaveVrMode now keeps a running GLES session and
vrapi_EnterVrMode reuses it for the same context (another context still gets
a new session; Vulkan unchanged). upstream-patches/runtime.cpp.patch updated,
libvrapi.so and liba32bridge.so rebuilt.
Verified on the Frame: headless launch test passes (72 fps after re-entry);
owner-verified in the headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32-bit-only APKs (Gear VR / Oculus Go games) are no longer "unsupported": the
recipe engine suggests frame.a32_bridge, which converts the APK with the
translator bridge (convert() shared with bridge/convert.py) and ships
liba32bridge.so / liba32rt.so / liba32stub.so (native/build.py --only a32).
FramePort:
- engine._a32: overport off (OVRPort is 64-bit only; the patch ships OVRPort's
loader chain + its 32-bit platform dispatcher, overport.ensure_runtime_lib),
only frame.a32_bridge/adapter/launcher of the frame patches, 64-bit-only
adapter settings dropped, device.text_input_window for Unity; such patches
are hidden in the UI
- analysis extra.headset (ANALYSIS_VERSION 9): quest / go / gearvr ->
"Gear VR" / "Oculus Go" platform + Steam tag; catalog `headset:` field
- expects_obb ignores Unreal's OBB-inside-the-APK (assets/main.obb.png)
- catalog: com.fallen.affected, net.mobilemischief.spunkydungeon
Bridge / translator:
- VrApi 1.31 + Unity 2019 (no-config EGL contexts, LOCAL_FLOOR/STAGE tracking
space), GL buffer mappings per buffer object (fixes a ~10 MB/s guest leak and
garbled vertices), Go controller mapped to the right Frame controller
- native_app_glue imports (ALooper/AInputQueue/AConfiguration/AAsset*) for
UE4 NativeActivity games (Gunjack: runs, then quits on its own APK signature
check, which FramePort doesn't defeat)
- debugging aids: A32_BREAK, A32_STRACE to logcat, lifecycle/LeaveVrMode logs
vrapi-bridge: honours LOCAL_FLOOR/STAGE, picks a window/RGBA8 ES3 config for
no-config contexts; upstream patch updated, libvrapi.so rebuilt.
Verified on the Frame: Spunky Dungeon (Unity 2019.2, OVRPlugin 1.46) plays at
72 fps; AFFECTED The Manor still runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32-bit ARM (armeabi-v7a) libraries run inside a normal 64-bit Android app on the
Steam Frame, without Android's native-bridge mechanism (no Lepton changes): each
libX.so becomes libX.a32.so plus a 64-bit stub, next to liba32bridge.so (the
translator + host glue) and liba32rt.so (a 32-bit static-PIE runtime with bionic's
libc/libm/libz/libstdc++ and a loader for the game's libraries). Guest and host
talk through hypercalls (SVC 0, r7 = 0x0a32xxxx).
Bridge (native/a32translator/bridge):
- JNI both ways: generated + hand-written JNIEnv/JavaVM, RegisterNatives and
Java_* exports through AAPCS64 -> AAPCS32 trampolines, handles for references,
arrays/strings shared when below 4 GiB, else copied
- NativeActivity glue, libandroid imports (weak, generated from imports.txt)
- GLES 3.2 + extensions + EGL: 996 functions generated from the NDK headers
- OpenSL ES output implemented in the guest, played through AAudio
- VrApi 1.x (Gear VR era) translated to the 64-bit VrApi -> OpenXR bridge,
including OVRPlugin 1.12's frame layout and its wrapped (app texture) chains
- controller input as Gear VR touchpad/back/gamepad events (UnityPlayer.injectEvent)
- convert.py: prototype of the APK conversion (FramePort patch to follow)
- PC harness with a fake JVM (32 checks, qemu or native) and an on-device test APK
Translator fixes found on the way: guest signals during blocking system calls
and in host code (Boehm GC deadlock in Unity's Mono), static-PIE start files and
PT_TLS/PT_GNU_RELRO bias, host fault chaining, stderr to logcat in bridge mode.
vrapi-bridge: OVP_HEADLESS_TEST=1 skips the wait for a tracked head (automated
tests without a worn headset); upstream-patches/runtime.cpp.patch updated.
Verified on the Frame (2026-10-10): AFFECTED The Manor (Unity 5.6 + Mono,
OVRPlugin 1.12) runs in the headset with picture and sound; controls in progress.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interpreter + JIT for ARM, Thumb-2, VFPv4 and NEON, ARM EABI syscall
translation, signals, threads and self-modifying code. Runs static bionic
programs on the Frame. Tests: A64 encoders vs llvm-mc, a differential
instruction fuzzer and program tests against qemu-arm. Not shipped; the
README lists the steps towards running 32-bit-only APKs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opt-in game setting (Picture -> Draw fades over the picture). Batman: Arkham Shadow fades by dimming its projection
layer with XR_KHR_composition_layer_color_scale_bias; with fade_quad=1 FrameBridge adds a black, alpha-blended quad
right after the dimmed layer (VIEW space, 1 m ahead), opacity = 1 - scale in 16 steps. One 8x8 swapchain, refilled
only when the step changes (Vulkan clear on the app's queue / GLES clear in its context); one swapchain per step hit
XR_ERROR_LIMIT_REACHED. fade_quad=2 also removes the layer's own scale. Device (headless, Batman): swapchain created,
level 16/16 on the first frame, no xrEndFrame errors, 72 fps.
Bionic allows 128 pthread keys per process. Unity 6 takes about 40, and with Gecko, Firebase and the Frame's own
libraries Wander 0.3570 ran out: Firestore aborted at start ("cannot create thread specific key for
__cxa_get_globals()", #198, #202). Opt-in patch, suggested for Unity 6 with Firebase C++ or Gecko: libunity.so's
pthread_key_create/_delete and pthread_get/setspecific imports are renamed in place to fp_tkey_* (new
elf.rename_imports: undefined symbols only, shared names refused, versions set global) and libfp_tkeys.so
(native/tlskeys) is added to its DT_NEEDED; keys without a destructor become generation-tagged slots behind one
real key, everything else keeps real keys. Approach and the first helper by Klownicle (#198), reimplemented here.
Triage signature pthread-keys-exhausted, PLAYBOOK row, Wander's recipe updated to 0.3570.
Host OpenVR overlays are composited over Lepton (Quest) games on the Frame;
Windows overlay apps register through Proton's vrclient. Detect overlay apps
(bundled .vrmanifest with is_dashboard_overlay, or OpenVR + IVROverlay +
self-registration), patch pcvr.vr_overlay and the Linux entry fields
vr_overlay/vr_overlay_autostart, agent register/unregister/launch_vr_overlay
(IVRApplications through SteamVR's libopenvr_api.so; manifest with
binary_path_linux_arm, the only key the Frame's vrserver reads). Overlay apps
skip launch tests and the Linux Steam-parent watchdog and don't count as a
running game. Probe sources in native/vroverlay_probe.
Adapter setting vk_shader_dump=1: every distinct module once to files/fp_vk_shaders/<size>_<sha256>.spv, each
vkCreateShaderModule as one index.txt line (order, time), to find the shader behind a GPU hang for a vk_shader_fix.
Agent v72 collect_diag returns the newest dumped modules (Vulkan and OpenGL ES dumps); diagnostics bundle them.
Triage gpu-hang suggests the dump for the session's graphics API (FrameBridge's swapchain formats).
Iris returns an empty capture buffer (bytesused 0, no LAST flag, time 0) for every VP9 frame that isn't shown
(alt-ref frames split from superframes). FFmpeg's V4L2 wrapper takes any empty capture buffer during a drain
for the end of the stream, so the first hidden frame decoded after EOS ended it and the pictures still in the
driver were lost (two-pass VP9 4K: 573/600 and 617/640, always the last frames); outside a drain the empty
buffers were returned as pictures, which the component then guessed away by counting hidden frames.
build.py now patches v4l2_context.c to requeue empty capture buffers without LAST/ERROR; the drain ends at the
LAST buffer, or after a skipped empty buffer at one silent second, so it can't block. The component's
hidden-frame counting is removed. Dev Frame (Batman's container): two-pass VP9 4K 600/600 and 640/640 in
hardware with the same Y hashes as OMX.google.vp9.decoder (seek phase too), VP9 without hidden frames,
H.264 4K and HEVC 4K 600/600, VP9 surface output 600/600. Two builds (different dirs and NDK copies) give the
same libstagefrighthw.so 141de01f.
- Opt-in per game: only launchers of games whose recipe has frame.hw_video_decode (install stage, no APK change;
suggested from analysis media_codec, ANALYSIS_VERSION 8) get the codec line (deployment.json hw_video_decode,
finalize + upgrade_launchers). Games that had agent 70's per-game codec (Batman) keep it; their old
<base>/frameport-codec folders are removed afterwards. Batman: catalog + migration batman_video_patches
(frame.hw_video_decode + the hidden adapter setting surface_native, no more package check); 4XVR catalog.
- Off switch: Settings -> Installing "Hardware video decoding" (library video.hw_decode, default on) -> agent
video_codec_switch (video-codec/disabled, checked by the launcher line and the wrapper); FRAMEPORT_NO_HW_VIDEO=1
for one game.
- Connection: the codec is checked once per connection, also after a failed install; a Frame keeps the same or a
newer revision built elsewhere (logged) instead of flip-flopping between PCs.
- Wrapper: the merged media_codecs.xml goes to $XDG_RUNTIME_DIR/frameport-video, never into the version directory.
- Decoder: refused (busy) Iris sessions retry until 20 s after the plugin loaded (SteamVR's link holds a session
~12 s at game start), else 2 s, before the software fallback; VP9 capped at 4096x2304 (XML + component, larger
decodes in software); hidden VP9 frames' pictures dropped. Manifest revision 7.
- build.py builds from paths with spaces and from freshly extracted FFmpeg sources. Artifacts rebuilt here, identical
from two trees (ext4 and the NTFS repo path, two NDK copies); PR #128's own sources rebuild to its binary.
- AGENT_VERSION 71 (matches the connection's gate); triage hw-video-decoder-busy, PLAYBOOK row, docs.
Mesa enforces OVR_multiview's rule that a draw's program declares as many views as the draw framebuffer has and drops the draw silently; Doom3Quest compiles every vertex shader with layout(num_views=2) and draws its HUD/PDA into 2D-texture FBOs, which stay black on the Frame.
native/glmv (libfpglmv.so, same length as libGLESv3.so): the engine library's dlopen string is rewritten to it and it becomes the first DT_NEEDED. It records stage sources at link time and, when a multiview program draws into a framebuffer without views, draws with a lazily built single-view twin (num_views layout blanked, gl_ViewID_OVR -> 0u; attribute locations, block bindings and uniform values copied), then rebinds the original. Opt-in and experimental (analysis gl_multiview_libs, ANALYSIS_VERSION 6), setting gl_mv_debug, triage gl-multiview-twin-failed.
Host tests: the rewriter on Doom3Quest's 19 shaders (GPL-3.0 fixtures) and glmv.c against a stand-in GL that applies Mesa's rule. Untested on the device.
Klownicle's five fixes for Vader Immortal: Episode I, reimplemented as FramePort patches (found by Klownicle,
GitHub #49; their prebuilt APK is not used):
- frame.unreal_quest_precompile: UVRUtils::GetQuestShaderPrecompilePercent returned 0.0 on its non-Quest branch, so
the menu waited forever on the loading card; that branch now returns 1.0. Located by symbol, every instruction
around it checked (bl IsRunningOnSantaCruz; tbz w0,#0; fmov s0,wzr; ldp; ret); other builds stay untouched.
- frame.unreal_quest_keymap: the RPOC key selector (shared by AddAxisMapping and AddActionMapping, so both mapping
kinds were affected) picked the empty Gear VR key set for an Oculus HMD; the tbz to it becomes a nop. Located as
the common callee of both functions, pattern-checked.
- frame.zink_shader_fix + native/zinkfix (libVkLayer_fp_shaderfix.so): a Vulkan layer under Zink for GLES games
that applies zink_shader_fix entries (vk_shader_fix format) to the SPIR-V Zink generates; it adds itself to
GraphicsEnv's debug layer list from a constructor (the engine library loads it first). zink_shader_dump=1
captures modules. Vader's recipe carries the two lightspeed-shader fixes (12 OpStores each).
- adapter proximity_emul (FrameBridge): OVRPlugin's thumb/index proximity actions get bindings to the capacitive
touch inputs when the runtime lacks XR_FB_touch_controller_proximity (OVRPort offers the extension anyway):
generic replacement for the libUE4 ThumbUp patch.
- Vader's recipe: pose_time_fix + proximity_emul, status unknown (to test in the headset), min_app 0.12.1.
Analysis records unreal_quest_gates (ANALYSIS_VERSION 4). Star Wars: Tales from the Galaxy's Edge (same studio)
has neither gate (different engine build) and gets a catalog entry with pose_time_fix + proximity_emul.
Headless on the dev Frame (Lepton 3.0.5): the layer loads into Zink's instances, the proximity bindings are
accepted, and the game passes the loading card to the "guardian smaller than recommended, press any button" screen.
Budget Cuts Ultimate asks for a 2048x2048 cube swapchain (faces=6); the Frame's runtime has no cube layers and refuses it (-2), OVRPlugin carries on with no images and crashes in ovrp_EndFrame4 (memset). cube_standin (default on, GLES) serves it as a GL cube map in the game's context and drops its layers. Triage cube-swapchain-refused (supersedes unity-render-crash/native-crash). Verified headless: the game runs on at ~70 fps.
BattleSisters' hands lagged behind the controllers (also with ovrp_hold_physics on or off). pose_debug=1 (new,
FrameBridge: per 5 s and per located space, the requested time minus the predicted display time) showed on the dev
Frame (SteamOS 0.4.5): XrTime runs 2.56 s ahead of CLOCK_MONOTONIC, the hand spaces were located at -2564 ms (the
monotonic "now" passed on as an XrTime: OVRPort's dispatcher converts XR_KHR_convert_timespec_time 1:1 and
FrameBridge's emulation is never asked) and the head at XrTime 0.1 s (~1450 xrLocateViews/xrLocateSpace per 5 s).
pose_time_fix (default off; suggested for Unity built-in OVRPlugin games): in xrLocateSpace(s)/xrLocateViews a time
nearer the monotonic clock than XrTime's "now" is moved by the offset measured at xrWaitFrame, one more than 0.5 s
before the display time goes to "now". Headless with it on: every far-past request moved, located times -9.8..0 ms
from the display time. Host unit test for the time rules; PLAYBOOK row.
- native/ovrpshim: libunity.so's ovrp_BeginFrame/ovrp_EndFrame go through the shim; the frame wait waits <= 50 ms
for the last waited frame to begin and skips otherwise (the game deadlocked at its Init scene when Unity skipped a
begin); a begin/end of another index while a wait is outstanding gets the waited index. The physics-step
ovrp_Update2 is held back (prediction 0 located the controllers in the past on the Frame: the hands lagged).
frame.unity_oculus_check revision 5.
- native/xrshim: PCM vibrations (XrHapticPcmVibrationFB) become one plain vibration too; haptic_fix revision 3.
- Catalog: works with frame.unity_oculus_check + haptic_fix, min_app 0.12.1. Triage unity-frame-not-begun (info),
PLAYBOOK rows and how to get all-thread backtraces with gdbserver64 inside the container, test.
Owner's headset test (720p): heavy compression artifacts and many dropped-looking frames.
- Frames stamped on arrival (wall clock) bunched when ffmpeg read several at once (gaps of 0-10 ms and 45+ ms). The
input stays on the wall clock (read in step with pulse's audio; setts alone held output ~7 s), and setts now
numbers the output frames on fp_venc's fps grid: every gap exactly 1/fps on the Frame, audio aligned.
- fp_venc --peak: VBR capped at the peak (else CBR as before), level picked for the peak. The hardware path uses
1.5x peaks and higher targets (360p 1.5, 480p 2.5, 720p 5, 1080p 8, Full 10 Mbit/s); 3 Mbit/s CBR blurred every
head turn. The x264 fallback keeps its rates.
- fp_venc's info/stats lines go to the app log (late/skipped frames next time); its errors still explain a stop.
Verified on the Frame: probe with VBR (no refused control); end to end through livestream.start at 720p32: 403
frames all 31 ms apart, audio 0-12.57 s next to video 0-12.58 s, late viewer on its keyframe at once, nothing left
after stop. Picture quality with motion still needs the headset.
The box boxes repeat every 3 source pixels at these ratios, so the averages become rounding shifts (3:2: block
sizes 1, 2, 4) or an exact multiply (3:1: x7282 >> 16 = /9 for every sum), with no per-pixel scalar loop. Same
bytes as the generic path; 480p (852 px wide, irregular) keeps the generic path. Self-test adds 1158x648 -> 432/216
(columns left over after the 48-pixel steps) and times all four qualities.
Frame (2026-10-07): selftest ok (184 cases); 1920x1080 -> 720: 1.3-1.65 ms (was 5.6), -> 360: 0.7 ms,
-> 1080: 0.6-1.0 ms, -> 480: 2.8 ms.
- fp_venc: accept the source's buffer count (the Frame's v4l2loopback has max_buffers=2; asking for 3 failed the
probe); OUTPUT buffers remember which picture they hold, so repeats (standby, still scenes) skip the conversion
(720p: 35% -> 2.5% of a core with the headset asleep).
- Pipeline: raw H.264 stamped on arrival (-use_wallclock_as_timestamps, same clock as pulse) with a 32-byte probe:
-framerate was ignored, -fflags nobuffer lost the first seconds, frames numbered from 0 held all output ~7 s next
to the sound. frag_keyframe so a requested keyframe starts a fragment (a joining viewer starts there).
Verified on the Frame (headset asleep): selftest ok (104 cases), probe at 360p-1080p, end to end through
livestream.start: 720p36 + AAC in 0.1 s fragments, audio and video spans match, a second viewer starts on its
requested keyframe at once, nothing left running after stop; FP_VENC_DISABLE=1 falls back to x264.
native/venc/fp_venc: own clean-room driver of the Frame's iris encoder (V4L2 stateful encoder interface, written from
native/venc/SPEC.md = kernel docs + values measured on the Frame). Reads SteamVR's headset-view webcam, box-scales and
converts RGB24 to NV12 (NEON, bit-identical scalar reference), CBR H.264 with SPS/PPS on every IDR, one frame per slot
of an even fraction of the panel rate read through DRM (96 Hz -> 32 fps). stdin "k" = keyframe, EOF = stop;
--probe/--selftest. Static freestanding aarch64 binary in artifacts/linux-arm64-bin (reproducible).
livestream: the script probes fp_venc and pipes it into ffmpeg (-c:v copy + AAC), else the old x264 30 fps path; the
helper is synced to ~/.local/share/frameport/bin by sha256 at stream start (no agent change); a new viewer asks for a
keyframe and waits for it; the status line shows the encoder and fps. Tests: hardware/fallback script branches,
keyframe-on-join, helper sync; tests/test_venc.py compares convert.c with a Python reference (native marker).
Not yet run on the device.