Turnip renders each GMEM bin at the density the map has at the bin's centre, on
a grid anchored in map space, so the full-density area is the bins whose centre
falls in Valve's sharp texels: up to half a bin off the texels' centre the
centring layer aimed at (In Death 2768x2768: 192x224 bins, -80,+32 px in both
eyes). native/foveation/tu_bins.h recomputes Turnip's bin size per foveated pass
as Mesa does (tu_render_pass_gmem_config, tu_tiling_config_update_tile_layout,
the LRZ FDM limit; AVOID_CCU layout; Adreno 750 only, Mesa 26.3 window); the
layer tracks render-target images/views, models render passes and dynamic
rendering, and centres those bins on each eye. Unmodelled cases keep the texel
centre. Adapter settings fdm_bin_w/fdm_bin_h override the size (FrameBridge ->
fp_fdm_set_bin). No frame.adapter revision bump: games get it on rebuild.
Headless (darkening test, snapshot=15, Fixed), block centre vs optical centre:
In Death L -76,+31 / R -83,+35 px -> L +4,-1 / R -3,+3; AC Nexus (2592x2256,
computed 192x224) L +1,+5 / R +2,0; fdm_bin_w=288/fdm_bin_h=256 shifted the
block by the predicted -96,+48.
OutRun quits with "AppSW recommended buffers unavailable" whether XR_FB_space_warp is offered or hidden.
New per-game adapter setting space_warp_emul (off by default, native/adapter/space_warp.c): offers the
extension (emulated when the runtime lacks it, overrides hide_space_warp), reports a motion-vector size of
eye/4 (the Frame's runtime answers the full 1728x1728 eye), lists RGBA16F and depth formats after the
runtime's, serves motion-vector swapchains (RGBA16F/RG16F at the reported size or at most half the eye) and
their depth swapchains as FrameBridge-owned GL textures / Vulkan images (3 images, round robin, wait/release
in front of every other hook), and strips XrCompositionLayerSpaceWarpInfoFB at xrEndFrame (eye depth info is
now kept when space warp info isn't first in the chain, also for hide_space_warp).
Headless on the dev Frame with Pinball FX VR (UE5 + OVRPlugin, Vulkan): both 376x376 pairs served,
600/599/598 acquire/wait/release by FrameBridge, space warp info removed, 0 xrEndFrame failures, 72.2 fps like
its own recipe. Triage appsw-required, catalog entry for OutRun (fb_color_space + space_warp_emul),
PLAYBOOK row, DEVICE_NOTES section, host harness for the GL/Vulkan paths.
Valve's VALVE_fdm_injection layer gives every eye swapchain a density map covering the whole swapchain with its
FOVE_LEVEL rectangle at the swapchain centre; it never uses the submitted imageRect or the eye's optical axis, so games
with smaller or shifted eye rects (dynamic resolution, AC Nexus 0,0 / 287,0 1726x1728 in 2016x1728) got the sharp area
~14% of the view to the side. VK_LAYER_FRAMEPORT_fdm_centre (native/foveation/centre.h, second layer in
libVkLayer_fdm_darken.so, right below Valve's layer when FDM_DEBUG has "enable"; not for The Climb 2;
FRAMEPORT_FDM_CENTRE=0 turns it off) replaces the per-eye density-map offsets: the eye's optical centre (Fixed) or
Valve's gaze re-based on the eye's rect (Eye-tracked). FrameBridge reports the views (fdm_views.c).
Changes after the first version (b2be40af, reverted 2ce1c505) failed in the headset:
- Eye-tracked black: Valve declares fragmentDensityOffsetCount 1 for two-view passes while Turnip reads one offset
per view regardless, so the right eye's offset came from uninitialised stack memory. Every offset Turnip reads is
now written (view count from the pass's view mask, eye from the map view's layer, each eye's gaze from Valve's array).
- Fixed crash: the layer no longer flags images (it had flagged external-memory images Valve leaves alone). Valve's
own disable_offsets path also crashed AC Nexus without this layer (Turnip under fdm_injection_CmdBeginRendering), so
Fixed now drops disable_offsets in-process once the layer is in the chain and overrides Valve's gaze offsets with the
optical centre; if Valve read the option first its maps carry no offset flag and passes are left alone.
Headless on the dev Frame, AC Nexus, 130 s per run, darkening test, snapshots every 15 s: centring Fixed (2 runs) and
Eye-tracked ran without crash at 72 fps, both eyes' sharp area within 2% of the optical centre (Valve only: +14% Fixed,
+25% left eye Eye-tracked; Valve-only Fixed crashed in 1 of 2 runs). FrameBridge snapshot=N reads Vulkan eye images
back; FRAMEPORT_FOVEATION_TEST=2 paints 1/fragment area. Triage valve-fdm-render-crash. Host harness
tests/test_fdm_centre.py. The sharp area's size still snaps to Turnip's GMEM bins (driver design).
Valve's VALVE_fdm_injection layer gives every eye swapchain a density map that covers the whole swapchain and centres
its FOVE_LEVEL rectangle there; it never uses the submitted imageRect or the eye's optical axis. Games that submit a
smaller or shifted rect (dynamic resolution, asymmetric eye rects such as AC Nexus 0,0 / 287,0 1726x1728 in a
2016x1728 swapchain) got the sharp area ~14% of the view off to the side, towards the bottom for corner-anchored rects.
VK_LAYER_FRAMEPORT_fdm_centre (native/foveation/centre.h), a second layer in libVkLayer_fdm_darken.so inserted right
below Valve's layer when FDM_DEBUG has "enable" (not for The Climb 2; FRAMEPORT_FDM_CENTRE=0 turns it off), rewrites
the per-eye density-map offsets at the end of each pass: Fixed puts the sharp area on the eye's optical centre (it adds
the offsets and the offset image flag Valve leaves out with disable_offsets), Eye-tracked re-bases Valve's gaze offset
on the eye's rect. FrameBridge reports the views (fdm_views.c: swapchain size, layer, imageRect, optical centre).
FrameBridge snapshot=N now also reads Vulkan eye images back (both eyes, half size, rect + FOV logged), and
FRAMEPORT_FOVEATION_TEST=2 paints 1/fragment area. Headless on the dev Frame (AC Nexus, Fixed, darkening test):
sharp-area centre vs optical centre +203,+43 px -> +26,+11 at 1x, +276,+65 -> +12,+17 at 1.5x, +407,+87 -> +47,+31
at 2x. The size still snaps to Turnip's GMEM bins (density is chosen per bin), so Small equals Large at 1x and the
shape changes between render scales; that part is the driver's.
Docs: DEVICE_NOTES "Foveation: sharp area centre and size", FRAME_RUNTIME, PLAYBOOK row, native/README; triage
dynamic-resolution-foveation is quiet once the centring layer ran. Host harness tests/test_fdm_centre.py.
The in-place flip raced Unity, which finishes some panels after xrEndFrame without acquiring them again, and its
blits/readbacks waited for the GPU inside xrEndFrame. swapchain_pool.c now owns flips (also without swapchain_pool):
a flagged pooled panel's atlas copy is a blit with reversed rows, a flagged runtime swapchain's quad gets a per-frame
atlas area with an upside-down blit of its released image. The app's images are only read; no CPU waits. flip_dump
reads back what the runtime is given (async); layer_debug logs copies and waits per 5 s. The late-drawing
fingerprints, scratch image and in-place flip are gone. Headless: every intro panel upright as submitted, 72 fps.
The first device run (AC Nexus, Quest 3) pooled nothing and said nothing
about why. The pool declined any chained create info and any mipCount
other than 1; on Vulkan it now accepts a XrSwapchainCreateInfoFoveationFB
(irrelevant to a private image) and honours XrVulkanSwapchainCreateInfoMETA's
extra create flags and usage, and keeps the app's mip levels (barriers
cover every level, level 0 goes into the atlas). GLES keeps its rules.
Every decline is logged once per reason ("swapchain_pool: not pooled:
..."): session mismatch (also from the proactive check), array/faces,
samples, mips, usage, flags, size, format, chained struct type, Vulkan
device, GL context, slots, atlas refused, image allocation, atlas full;
plus one line when the 8-swapchain trigger starts pooling. The
xrCreateSwapchain log gains mips= and the first chained struct type.
Harness: the device sequence (1728 eye set, 2x2 and 1024x1024 usage 0x39,
2016 eye set, twelve 0x39 panels with foveation/META chains and a 4-mip
panel, flip-tracked runtime creates) pools every panel and the 2592x2256
set then fits under a 15-swapchain cap; decline reasons are checked.
swapchain_pool now also covers Vulkan sessions (XR_KHR_vulkan_enable/2):
from eight live app swapchains, single-layer format-43 colour panels get
private device-local VkImages (app usage + transfer, returned as
XrSwapchainImageVulkanKHR, UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL at the
first acquire) packed into up to two 4096x4096 sRGB runtime atlases.
xrEndFrame records one command buffer per frame on the binding's queue
(3-deep fence ring, submits under flip_lock): vkCmdCopyImage from each
referenced panel into the waited atlas image, which returns to
COLOR_ATTACHMENT_OPTIMAL before its release. Image ownership, crops and
layer ordering follow the GLES pool; the whole layer rewrite holds the
pool lock so a destroy cannot free an image with recorded copies.
Pooled Vulkan panels stay tracked by flip_vk.c (in-place flips keep
working). Other formats and arraySize-2 eye images keep runtime
swapchains. GLES behaviour is unchanged.
AC Nexus: Quest 3 identity again with swapchain_pool=1 (min_app 1.3.1),
frame.adapter revision 3 for it only. Settings text, triage diagnosis,
PLAYBOOK, DEVICE_NOTES, native README, PATCHES.md and the i18n template
updated. Host harness gains a recording Vulkan double (layouts, copy
regions, ring, failures, cleanup, AC Nexus swapchain count: 12 live with
the new eye set). Not verified on the device.
- Emulate XR_META_spatial_entity_discovery (xrDiscoverSpacesMETA, xrRetrieveSpaceDiscoveryResultsMETA) and XR_META_boundary_visibility
- Add system properties for space discovery and boundary visibility
- Fix semantic labels length and null termination
- Allow xrGetInstanceProcAddr with null handle for emulated procs in xrshim
- Trim composition layers exceeding runtime maxLayerCount in xrEndFrame and validate quad extents
- Hook xrshim when scene_emul is enabled in adapter settings
- Update prebuilt native artifacts and checksums
Add opt-in swapchain_pool with private GLES images and up to two packed
runtime atlases. Start at eight live app runtime swapchains to reserve
headroom before the session cap. Preserve image ownership, GL state,
source fences, crops and the existing cylinder/equirect layer ordering.
Enable for 4XVR, add count-limit triage without unknown suggestions for
older apps, and document the pending headset menu check. Leave default
recipes and adapter revisions unchanged. Rebuild arm64/arm32 artifacts.
Validation: 132 targeted tests passed (2 skipped), including the native
runtime/GL harness; Ruff and generated docs pass. Full host suite:
2095 passed, 137 skipped, 12 failures and 16 setup errors, all denied
socket creation in this sandbox. No headset validation or push.
Route OVRPlugin recommendations through the existing extension shim, retain native loader hooks, and log projection rect changes once per second per eye. Enable the fix for Batman, document verification, and rebuild both adapter architectures.
HardBullet's loading frames submit only a cube layer, which the Frame's runtime doesn't composite, so the loading
screen stayed black. Adapter setting cube_quads (default on) draws a cube-only frame's six faces as quad layers
around the viewer; frames with a projection layer are unchanged. Adapter rebuilt on current main.
Also: the foveation test expected a numeric FOVE_LEVEL to pass through (Valve's layer ignores numbers since #394),
a long description line and an import order in adapter_dialog.py failed lint.
On the Frame the atomic-branch detours never installed ("installation failed"): an mmap hint isn't honoured,
so the stub landed out of B range. Stubs now go into free gaps from /proc/self/maps within +-128 MiB of the
entry, mapped with MAP_FIXED_NOREPLACE (a relocated result is rejected); no gap = no hook. Every failure logs
the target and the failing step.
The detour helper from PR #34 (used by the metadata hook and PR #389's two geometry hooks) wrote a 16-byte
branch + literal in two steps while game threads may run the function (installation is retried from
xrPollEvent). Each hook now publishes a single aligned release-atomic AArch64 B to a stub within +-128 MiB;
the trampoline replays the one verified instruction and returns to entry+4. Every mprotect is checked;
failures before publication free the stub and trampoline and leave the function untouched; no stub in range =
no hook. Adapter artifacts rebuilt by us (the PR's binary isn't shipped).
Games saw a Quest 2: OVRPort's dispatcher hardcodes the system name "Oculus Quest2" and a Quest 2
headset UUID (XR_META_headset_id), and OVRPlugin decides the headset type from the UUID first, then the
name. device.headset_model (Customize; Quest 3 default for every Quest game, also Quest 3S, Quest Pro,
Quest 2) rewrites both in the dispatcher after checking the original bytes; Quest 2 leaves the build
byte-identical. FrameBridge reports the same name to games that call the runtime directly. A library
migration adds the default to existing recipes (installed games show Update on Frame).
Lepton's ro.product.model ("Lepton") can't be overridden by boot properties, so Build.MODEL is unchanged.
Verified on the Frame: OVRPlugin logs "systemName Meta Quest 3" (was "Oculus Quest2").
OutRun Quest VR quits before its VR session with "Rec.709 color space unavailable": the Frame's runtime
has no XR_FB_color_space. Adapter setting fb_color_space (off by default; triage color-space-missing
suggests it) offers the extension when the runtime lacks it, reports Rec.709 and accepts any colour
space without converting. OVRPort's dispatcher can't forward the new functions, so it isn't on for every
game. Not yet tried on the device.