Foveation: put Valve's sharp area on each eye (#394); Vulkan eye snapshots

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.
This commit is contained in:
spoopyghosty0 committed 2026-10-24 10:28:42 -04:00
1 parent 3dc7d823e4
commit b2be40af35
23 files changed
+1197 -35

No files matched your search

+1
View File
@@ -618,6 +618,7 @@ Toy Master doesn't request a model after that (it uses its own), so the glb load
**Cube swapchains:** the runtime refuses `faceCount=6` → FrameBridge `cube_standin` (default on, GLES only, `native/adapter/cube_standin.c`). → docs/DEVICE_NOTES.md#cube-swapchains
**Hardware video decoding:** `native/hevc` OMX plugin (`OMX.frameport.{avc,hevc,vp9}.decoder`), installed once per Frame (`ensure_video_codec`, agent `video_codec_status`/`install_video_codec`/`video_codec_switch`); **bump `revision` in native/hevc/build.py with every artifact change**; per game `frame.hw_video_decode` (analysis `media_codec`), Settings `video.hw_decode`, `FRAMEPORT_NO_HW_VIDEO=1`; Batman migration `batman_video_patches` + `surface_native`. → docs/DEVICE_NOTES.md#hardware-video-decoding
**Vulkan shader dump:** adapter `vk_shader_dump=1` (`native/vkshim/shader_dump.h`) writes SPIR-V modules + `index.txt` to `files/fp_vk_shaders/`; `collect_diag` returns the newest session's modules (`shader_dumps`); triage `gpu-hang`, `triage.graphics_api`. → docs/DEVICE_NOTES.md#vulkan-shader-dump
**Foveation centre (#394):** Valve's FDM layer centres its density map on the whole swapchain (not the eye's rect or optical axis) and Turnip picks density per GMEM bin (the sharp area snaps to bins, so its size shifts with render resolution: driver design); `VK_LAYER_FRAMEPORT_fdm_centre` (native/foveation/centre.h, below Valve's layer, views from FrameBridge `fdm_views.c`) rewrites the per-eye density-map offsets; FrameBridge `snapshot=N` reads Vulkan eyes back. → docs/DEVICE_NOTES.md#foveation-sharp-area-centre-and-size-394
**Unreal OBB check (#159):** `frame.unreal_skip_obb_check` (analysis `unreal_verify_obb`) turns off Epic's OBB CRC pass; triage `unreal-obb-check-stuck`. → docs/DEVICE_NOTES.md#unreal-obb-check
**Issue round 2026-10-12:** Lepton breaks on unquoted paths with spaces → launchers give Lepton a `~/.local/share/frameport/lepton-paths/<pkg>` link (`app_dir_lines`/`set_app_dir`/`launcher_base`); `frame.eos_keychain_loop`, `frame.unity_tls_key_pool` (`libfp_tkeys.so`, `elf.rename_imports`), `frame.vermillion_scene_canvas`; `tools/cpp2il.py` falls back to Cpp2IL's dev build for metadata v107; Linux `desktop_icon`; `scripts/upload_bench.py`. → docs/DEVICE_NOTES.md#issue-round-2026-10-12
**OpenSSH uploads (#324/#242):** `native_upload.quote` escapes only backslash and `"`, Win32 local paths go as `C:/…`; `Frame.auth` records the login that worked (app_key / user_key from `ssh.extra_keys`, `frame ssh-keys`, agent, password); strict IdentitiesOnly first, relaxed retry. → docs/DEVICE_NOTES.md#openssh-uploads
+3 -3
View File
@@ -1,4 +1,4 @@
1a4dce5c4a6d0dfc5f20e55ac29e70c73cc2cc124c21ca6a7e1b316e6afb1c79 ./arm64-v8a/libVkLayer_fdm_darken.so
b7cf70618b132c0c97d1dc63790639afd12c8ddb7fb5fb1f1bf9804ad30a02c6 ./arm64-v8a/libVkLayer_fdm_darken.so
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
e63d93e086df3b7b29501188b80fb113879e2fd325d7659f68a31ac5bc87b561 ./arm64-v8a/liba32bridge.so
81b073b0cdf11fdb09d22dff682884c5046fe13801f18db9aed232e86f3bf173 ./arm64-v8a/liba32rt.so
@@ -14,11 +14,11 @@ e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/li
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
3af427bcb85fb1b1c8c6048b9ab4481d3d85d08e6a370a1c3f45674ac6281f62 ./arm64-v8a/libopenxr_loader_generic.so
2134844aab60928bb13305edd775239011e09cc900a200559b90211ea6d8e2c6 ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
c7a14d34e4ddf20ec943641f6431525867d6f030cd01160df1b24afd441af39f ./arm64-v8a/libvrapi.so
c7a14d34e4ddf20ec943641f6431525867d6f030cd01160df1b24afd441af39f ./arm64-v8a/libvrapi_foveation.so
78970b267d4db6e2f1c1a9c0c8ea275fcb42a086305529fb01215c9ae55de436 ./armeabi-v7a/libopenxr_loader_generic.so
61d57d82eba6682b7c2d781331fe209ae0df5b97ccaa438e1ce1b1280315a192 ./armeabi-v7a/libopenxr_loader_generic.so
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
55aae57d292e9536701f2f8a9dbaf1edcfdd766c5748d07a34da2f5ef197f212 ./hevc/libstagefrighthw.so
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -20,9 +20,10 @@ signatures:
suggest: [adapter.scene_emul]
- id: dynamic-resolution-foveation
pattern: 'projection view [01](?: rect change)?:[^\r\n]*rect=(?:[1-9]\d*,\d+|0,[1-9]\d*) '
unless: 'fixed_resolution: recommended'
# a rebuilt APK's foveation centring layer re-centres Valve's density map on the submitted rect (#394)
unless: 'fixed_resolution: recommended|fdm_centre: pass'
severity: info
diagnosis: The game submits an offset projection viewport. With Valve foveation, dynamic resolution can move the sharp centre or expose a white edge. Keeping full resolution may help; it increases GPU load and needs the updated adapter and OVRPlugin shim in a rebuilt APK.
diagnosis: The game submits an offset projection viewport. With Valve foveation, dynamic resolution can move the sharp centre or expose a white edge. A rebuilt APK (Update on Frame) puts the sharp area back on each eye; keeping full resolution may also help for the white edge, increases GPU load and needs the updated adapter and OVRPlugin shim in a rebuilt APK.
question: With foveated rendering enabled, was the sharp centre misplaced or was there a white line at the picture edge?
suggest: [adapter.fixed_resolution]
- id: static-openxr-loader-bypass
+58
View File
@@ -1002,3 +1002,61 @@ FrameBridge`, `fp_xrruntime: xrInitializeLoaderKHR forwarded to FrameBridge: 0`,
`new layer:` lines. The outer loader should log the private sdcard manifest, the
inner loader the real vendor manifest. Require RUNNING and submitted frames;
headset rendering and controls need separate confirmation.
## Foveation: sharp area centre and size (#394)
Reports: Valve foveation's sharp area sat towards the bottom right in both eyes (In Death: Unchained, The 7th Guest
VR), top right in Batman (dynamic resolution), and its size changed with FramePort's render resolution (RE4,
Asgard's Wrath 2: Large at 1x bigger than at 1.5x/2x; Small "different sizes and shapes").
**Mechanism** (Valve's `libVkLayer_VALVE_fdm_injection.so` disassembled 2026-10-24, Mesa Turnip source, AC Nexus on the
dev Frame): one library is both an implicit OpenXR API layer and a Vulkan layer. Its `xrCreateSwapchain` makes a
density map per eye swapchain (`detected FDM image 63x54[2]` for 2016x1728 array 2: `ceil(w/32) x ceil(h/32)`, one layer
per eye) and fills the `FOVE_LEVEL` preset rectangles in map-normalized coordinates, so every preset is centred at
about (0.51, 0.51) of the **whole swapchain**. Its `xrEndFrame` stores each view's imageRect, but nothing uses it for
the map. Eye-tracked offsets (`calc_offset`) are `foveationCenter NDC x 0.5 x render area`, also from the swapchain
centre; Fixed (`disable_offsets`) passes none and creates images without the density-map-offset flag. Turnip then
picks one density per GMEM **bin**, from the map texel under the bin's centre (`tu_calc_frag_area`); bins have a fixed
pixel size (GMEM / bytes per pixel), roughly 250-300 px for AC Nexus.
**Root causes.** Off-centre: games often submit a rect that isn't the swapchain (AC Nexus rect=0,0 / 287,0 1726x1728
inside 2016x1728 for its asymmetric eyes, then dynamic resolution rect=0,259 1467x1469; Asgard's Wrath 2 0,0 / 347,0
2237x2256 inside 2592x2256), and the eye's optical axis is off the rect centre anyway (FOV left/right -1.03/+0.88 rad,
up/down +0.86/-1.05). The swapchain-centred map then sits ~14% of the view width to one side (and towards the bottom
for corner-anchored dynamic-resolution rects). Size: the sharp area is the set of bins whose centre lies in Valve's
rectangle, so it snaps to whole bins: at 1x the bins are large relative to the eye, at 1.5x/2x smaller. At 1x Small
(hi) and Large (lo) gave the **same** 572x444 px area; at 1.5x they differ (764x444 vs 764x892, also in shape). The
rectangle is also a fraction of the swapchain, not of a smaller dynamic-resolution view.
**Fix (centre):** `VK_LAYER_FRAMEPORT_fdm_centre` (`native/foveation/centre.h`, a second layer in
`libVkLayer_fdm_darken.so`; its constructor inserts it right after Valve's layer in GraphicsEnv's debug-layer list when
`FDM_DEBUG` has `enable`, never for The Climb 2). Below Valve it sees the passes with Valve's map attached (render pass
+ framebuffer, or dynamic rendering) and the offsets at their end, and replaces each eye layer's offset so that Valve's
sharp centre (computed with Turnip's texel size and the preset) lands on the eye's optical centre (Fixed, or
Eye-tracked without a gaze: all-zero offsets) or on the gaze re-based on the eye's rect (Eye-tracked). In Fixed mode it
adds the offsets and gives 2D render targets and maps the offset flag, as Valve does when its offsets are on (Turnip
applies offsets only for a flagged map and sizes LRZ for offsets by the depth image's flag). FrameBridge
(`adapter/fdm_views.c`) reports per view: swapchain size, array layer, imageRect and optical centre (GLES: rows from
the bottom, Zink's memory order), through a weak `fp_fdm_set_views`. Double-wide single-layer eyes and passes of other
sizes are left alone. `FRAMEPORT_FDM_CENTRE=0` (as `LEPTON_ENV_`) turns it off. Logs: `FrameBridge: fdm views: …`,
`FoveationDarken: fdm_centre: pass 2016x1728 map 63x54 (preset 1, Valve centre 1024,880): offsets none (0,0) ->
-24,-184 / -24,-168`. Not changed: the bin quantization of the size (driver design) and the preset being a fraction of
the swapchain. Ships in the APK (FrameBridge + layer library): a game needs a rebuild (Update on Frame) to get it.
**Headless evidence** (AC Nexus, Fixed, darkening test, `snapshot=2`; its white start flash shows full density white,
coarse grey): FrameBridge now also reads Vulkan eye images back (`snapshot_vk.c`: both eyes, half size, the whole
swapchain layer, rect + FOV in the log). Sharp-area centre minus the left eye's optical centre, before -> after:
| scale | size | swapchain / left-eye rect | before | after | sharp area (of the rect) |
|---|---|---|---|---|---|
| 1.0 | Large (lo) | 2016x1728 / 0,259 1467x1469 | +203,+43 px (+14%,+3%) | +26,+11 (+2%,+1%) | 572x444 (39%x30%) |
| 1.0 | Small (hi) | same | +202,+43 | +26,+11 | 572x444 (same as Large) |
| 1.5 | Large (lo) | 2976x2592 / 0,389 2166x2203 | +276,+65 (+13%,+3%) | +12,+17 (+1%,+1%) | 764x892 (35%x40%) |
| 1.5 | Small (hi) | same | +276,+65 | +13,+17 | 764x444 (35%x20%) |
| 2.0 | Large (lo) | 4032x3456 / 0,519 2933x2937 | +407,+87 (+14%,+3%) | +47,+31 (+2%,+1%) | 1148x892 (39%x30%) |
| 2.0 | Small (hi) | same | (no flash frame caught) | -49,-81 (-2%,-3%) | 956x668 (33%x23%) |
Right eyes match (mirrored rects 244,259 / 360,389 / 488,519). The residual 1-2% is the bin grid. Not checked: GLES/Zink
games (the dynamic-rendering path is implemented, the GL row order is unverified), Eye-tracked with a real gaze
(headless has none), and the picture in the headset. Twice AC Nexus died in a freedreno thread right after a Vulkan
snapshot of its loading frame (not without snapshots): keep `snapshot` for diagnostics.
+30 -7
View File
@@ -513,11 +513,31 @@ The inspected arm64 `libVkLayer_VALVE_fdm_injection.so` has these semantics (vir
offset (`0x1b9b8`). These three flags do not resize the profile.
- `FDM_SWAPCHAIN_SIZE` is parsed by `atoi` (`0x1c518`), clamped to nonnegative, and
seeds a known swapchain size (`0x1ccf8`). It is a size/matching hint, not a radius.
The synthetic map is square with side `ceil(size/32)` (`0x19048–0x19088`); image
association also matches this tile size (`0x16c78`). Changing render resolution changes
the pixel extent, tile quantization and potentially the matched render target/map. This can
change the visible diagnostic area without making the ignored radius exports effective;
a matched device capture is needed to establish which path a particular game used.
The synthetic map (Zink/GLES dynamic-rendering path, `CmdBeginRendering`) is square with side
`ceil(size/32)`, size = the render area width (`0x19048–0x19088`); image association also matches
this tile size (`0x16c78`).
- Where the sharp area lands (device-checked 2026-10-24 with AC Nexus, GitHub #394; details in
DEVICE_NOTES.md "Foveation: sharp area centre and size"): for Vulkan games the layer's
`xrCreateSwapchain` creates a map of `ceil(w/32) x ceil(h/32)` texels per eye swapchain (one array
layer per eye) and fills the preset rectangles in map-normalized coordinates, so the sharp area is
centred on the **whole swapchain** (all presets centre at about 0.51, 0.51). It knows each view's
imageRect (`xrEndFrame` stores them) but doesn't use it for the map. Eye-tracked offsets
(`calc_offset`, `0x1b848`) are `gaze NDC x 0.5 x render-area size`, rounded to the offset
granularity (8), measured from that same swapchain centre; `disable_offsets` (Fixed) also leaves
every image without `VK_IMAGE_CREATE_FRAGMENT_DENSITY_MAP_OFFSET_BIT_EXT` (added at `0x16afc`
only when offsets are on). A game that submits a smaller or shifted rect (dynamic resolution,
asymmetric eyes: AC Nexus rect=0,0 / 287,0 1726x1728 in 2016x1728, Asgard's Wrath 2 0,0 / 347,0
2237x2256 in 2592x2256) gets the sharp area off its view's centre, and the eye's optical axis
(asymmetric FOV, e.g. -1.03/+0.88 rad) isn't the rect centre either. FramePort's
`VK_LAYER_FRAMEPORT_fdm_centre` (native/foveation/centre.h, below Valve's layer) rewrites the
per-view offsets so the area sits on each eye.
- Turnip (freedreno) applies a density map per GMEM **bin**: it reads the map on the CPU at the
bin's centre (`tu_calc_frag_area`, texel size `2^ceil(log2(fb/map))` >= 32 px) and renders the
whole bin at that density. The visible sharp area is therefore the set of bins whose centre falls
in Valve's rectangle: its size snaps to the bin grid (bins are ~150-200 px here and get relatively
smaller as render resolution grows), so the same preset looks bigger or smaller and changes
shape between render scales. That part is the driver's design, not a setting; FramePort can't
pick the bins.
Per-game library preferences now store `level`. Legacy radius choices are translated on read
(and written as `level` on Save): nearest old nominal .30 → lo, .25 → med, .20 → hi,
@@ -529,7 +549,8 @@ For device verification, hold game scene, render resolution and gaze constant; c
`lo`, `med`, `hi` across fresh game starts (Default should match lo absent another runtime override).
First use Fixed for a stable comparison, then Eye-tracked to check gaze following. Enable
**Darken coarse fragments (test)** and adapter `snapshot=N` to capture submitted eye images headlessly
every N seconds (GLES only, left eye at quarter size); compare the undarkened center and peripheral bands. Existing adapter
every N seconds (GLES: left eye at quarter size; Vulkan: both eyes at half size, the whole swapchain layer, with
imageRect and FOV in the log); compare the undarkened center and peripheral bands. Existing adapter
builds need diagnostic support installed. Turn the test and snapshots off before benchmarking.
Agent `FOVEATION_ENV_KEYS` must include `"FOVE_LEVEL"` for launcher updates via `set_settings`.
@@ -561,7 +582,9 @@ in Climb the same library also carries the compatibility correction.
High-resolution Deadpool samples crossed from about 45 to 90 FPS with injection enabled, but temperatures and
power rose as it rendered twice as many frames. Foveation does not guarantee lower temperature at uncapped FPS.
Preset export/migration has configuration coverage; these preset comparisons have not been device-tested here.
Preset export/migration has configuration coverage. Headless device check of the presets (2026-10-24, AC Nexus,
Fixed, darkening test + FrameBridge `snapshot=N`, which also reads Vulkan eye images back): DEVICE_NOTES.md
"Foveation: sharp area centre and size".
## Monitoring sources (probed 2026-10-07, SteamOS 0.4.3, kernel 6.18; used by the Monitor tab, agent `_monitor`)
+1 -1
View File
@@ -2331,7 +2331,7 @@ Saves what the game draws every few seconds, for testing without the headset.
- Game settings → Troubleshooting → "Eye-image snapshots (diagnostics)" (advanced).
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. OpenGL ES games only.
> Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. Vulkan games: both eyes at half size (fb_snap_vk_N_vV.ppm), the whole swapchain image with the submitted rect and FOV in the log.
### <a id="adapter-stable_local"></a>Keep menus in place
+1
View File
@@ -23,6 +23,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Updating a flat Android app after saving its recipe fails with `no OVRPort patches selected` (GitHub #271, Plezy) | older FramePort loads the saved recipe without restoring its unchanged-install setting or applying the flat-app build rules, then tries an empty VR conversion | update FramePort: saved recipes keep `as_is`, and flat apps still skip OVRPort; existing catalog-derived recipes refresh at the next app update. Until then, enable "Install as is" for an app whose APK needs no fixes |
| A launch test suggests a patch (for example triage `sdl-no-clipboard` → `frame.sdl_clipboard`) but the game page doesn't show it / lists it as not applicable; or a game added long ago lacks warnings newer ones get (Android version, web wrapper, missing OBB) (GitHub #104, Dramatic Shape) | the library entry was analyzed by an older FramePort: the analysis field the patch reads (`sdl_java`, `min_sdk`, `web_wrapper`, `expects_obb`, …) is missing, so `applies()` is false | entries whose `analysis.extra.analysis_version` is below `detect.ANALYSIS_VERSION` are analyzed again at the GUI's start (background) and before a build (`pipeline.refresh_analyses`); by hand: game menu → "Analyze again", then Update on Frame. An unreadable APK is marked `analysis_failed` (retried after the next version bump). Adding an analysis field: bump `ANALYSIS_VERSION` |
| Foveation full-resolution area looks identical at 10% and 45% (GitHub #394), while render resolution changes it | `FOVE_INNER`/`FOVE_OUTER` are neither passed through by Lepton nor read by Valve's layer; shared injection uses rectangular density presets | Game settings → Picture now exports named `FOVE_LEVEL=lo/med/hi`; Default preserves the runtime choice. Medium/high shrink the center and reduce peripheral density further. Legacy radius preferences map to the nearest old nominal preset. Compare Fixed + Darken coarse fragments with adapter `snapshot=N` at a fixed render resolution; see FRAME_RUNTIME.md for addresses and profile data. Agent launcher allowlist needs `FOVE_LEVEL`. |
| Valve foveation's sharp area sits off-centre in both eyes (bottom right in In Death: Unchained / The 7th Guest, towards the swapchain centre with dynamic resolution or asymmetric eye rects), Eye-tracked or Fixed (GitHub #394) | Valve's density map covers the whole swapchain with its sharp rectangle at the swapchain centre; the game's view is a smaller or shifted imageRect and the eye's optical axis isn't the rect centre (asymmetric FOV) | Rebuild (Update on Frame): the arm64 FrameBridge build's `VK_LAYER_FRAMEPORT_fdm_centre` (native/foveation/centre.h) sits below Valve's layer and moves each eye's map to its optical centre (Fixed) or re-bases the gaze on the eye's rect (Eye-tracked). launch.log: `FrameBridge: fdm views: …` and `fdm_centre: pass WxH map … -> x,y`. Check headlessly with Darken coarse fragments + `snapshot=N` (Vulkan: `fb_snap_vk_N_vV.ppm`). `FRAMEPORT_FDM_CENTRE=0` (launcher `LEPTON_ENV_`) turns it off for comparison. The area's size still snaps to Turnip's GMEM bins (density is chosen per bin), so it changes somewhat with render resolution; that part isn't fixable in FramePort |
| FDM layer loads but diagnostic logging reports `fdm_injection_CreateInstance: skipped` | Layer loading alone does not enable injection on the tested SteamOS 0.4.5 build; a skipped helper instance alone also does not establish game-wide inactivity | Opt-in `device.foveation` mode `tracked`: loads Valve's layer and sets `FDM_DEBUG=enable`. Batman probe reached foveated render passes with changing per-eye offsets. Validate graphics and matched gameplay GPU time; do not enable globally. Use `debug` only for brief diagnostics, preserving existing flags; never benchmark with debug spam. See FRAME_RUNTIME.md. |
| One eye (usually the right) shimmers or jitters in the 3D view while menus and panels look steady; pacing is clean (GitHub #69, e.g. RTCWQuest, Moss) | Valve's eye-tracked foveation layer (`VALVE_fdm_injection`) moves its low-density regions with each eye's gaze, and the right eye's gaze filter restarts often (`CEyePoseUKF R: Large dt … Bootstrapped` in `~/.local/share/Steam/logs/eyetracking.txt`) | Game settings → Picture → "Foveated rendering (Valve)": **Fixed** (`FDM_DEBUG=disable_offsets`; verify injection remains active on the installed runtime) or **Off** (`VK_INSTANCE_LAYERS=""`); catalog field `foveation:` |
| With `device.foveation` Eye-tracked/Fixed, the sharp area looks right but everything around it is black with scattered coloured blocks (menus on quads too); pacing fine (GitHub #303, Medieval Dynasty; Vampire Survivors reported the same) | Unity's Oculus XR plugin asks for SUBSAMPLED eye images (`XR_META_vulkan_swapchain_create_info`, `VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT`); under Valve's FDM their coarse areas stay in the compact subsampled layout, which the compositor reads as an ordinary image. `foveation_fix=1` only dropped the `XrSwapchainCreateInfoFoveationFB` nodes and passed the META node on. With `foveation_fix=0` + `foveation_storage_fix=1` the adapter logs `foveation_storage_fix: shared allocation SUBSAMPLED cleared` for the eye swapchains | `device.foveation` (tracked/fixed) now also sets `foveation_storage_fix=1`, which clears SUBSAMPLED whatever `foveation_fix` is (generic version of Batman's OVRPlugin allocation fix). Rebuild + reinstall (new adapter). Headless-checked only (72 fps, bit cleared); picture needs the headset |
+1
View File
@@ -36,6 +36,7 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
| `glshim/` | `arm64-v8a/libglshim.so` | Mesa GLSL compatibility for GLAD engines (hooks eglGetProcAddress): comments out `#pragma` before `#extension`, enables `GL_EXT_shader_implicit_conversions`, hides GL_OVR_multiview (`gl_hide_multiview`, default 1) and logs failed shaders. `-DGLSHIM_TRACE` adds per-FBO draw/error tracing. |
| `glmv/` | `arm64-v8a/libfpglmv.so` | Single-view draws of OVR_multiview programs (patch `frame.gl_multiview_fbo`, opt-in, GitHub #77). Mesa enforces OVR_multiview's rule that a draw's program declares as many views as the draw framebuffer has; engines that compile every shader with `num_views=2` and also draw into ordinary 2D framebuffers (Doom3Quest's HUD/PDA pool) lose those draws. The engine library gets it as first DT_NEEDED (direct `gl*` imports) and its `dlopen("libGLESv3.so")` string is rewritten to `libfpglmv.so` (same length; its `qgl*` dlsym table); linked `--no-as-needed` to libGLESv3 so dlsym on its handle finds everything it doesn't wrap. It records stage sources at glLinkProgram, caches the draw framebuffer's color-attachment view count per bind, and when a multiview program draws into a 0-view framebuffer it binds a lazily built twin (`glmv_rewrite.h`: num_views layout blanked, `gl_ViewID_OVR` -> `(0u)`; attribute locations + uniform block bindings copied, default-block uniforms copied before every such draw with a shadow to skip unchanged ones), draws, rebinds the original. Logcat tag `GLMV`; `gl_mv_debug=1`. The rewriter is host-tested on Doom3Quest's 19 shaders (`tests/test_gl_multiview_fbo.py`); untested on the device. |
| `tlskeys/` | `arm64-v8a/libfp_tkeys.so` | Thread-key pool for Unity (patch `frame.unity_tls_key_pool`, opt-in, GitHub #198; approach and the first helper by Klownicle). Bionic allows 128 pthread keys per process; Unity 6 plus large native libraries (Firebase, Gecko) and the Frame's own libraries use them up (Wander: `cannot create thread specific key for __cxa_get_globals()`). libunity.so's four key imports are renamed to `fp_tkey_create/_delete/_get/_set` (`elf.rename_imports`, same `.dynstr` bytes, versions set global) and this library is added to its DT_NEEDED: keys without a destructor become slots (512, generation-tagged, `0x40000000 + slot`) in a per-thread table under one real key; keys with a destructor and all other libraries keep real keys. Lock-free get/set. Logs `fp_tkeys: pooling Unity's thread keys` once (tag FrameBridge). Host-tested (rename); not yet run on the device. |
| `foveation/` | `arm64-v8a/libVkLayer_fdm_darken.so` (+ `libvrapi_foveation.so`, The Climb 2) | Two Vulkan layers in one library, linked (dormant) into every arm64 FrameBridge build. `VK_LAYER_FRAMEPORT_fdm_darken` (above Valve's `VALVE_fdm_injection`, only with the foveation test): dims coarse fragments (`FRAMEPORT_FOVEATION_TEST=1`) or paints 1/fragment area (`=2`, developers). `VK_LAYER_FRAMEPORT_fdm_centre` (`centre.h`, inserted right **below** Valve's layer whenever `FDM_DEBUG` has `enable`, i.e. Eye-tracked/Fixed; off with `FRAMEPORT_FDM_CENTRE=0`, never for The Climb 2): Valve's density map covers the whole swapchain with its sharp rectangle at the swapchain centre; this layer rewrites the per-view density-map offsets at the end of each pass (adds them in Fixed mode and flags render targets/maps with `FRAGMENT_DENSITY_MAP_OFFSET` as Valve does when its offsets are on) so the sharp area sits on each eye's optical centre inside its submitted rect, or on the gaze point re-based on that rect (GitHub #394). FrameBridge (`adapter/fdm_views.c`) reports the views via `fp_fdm_set_views`. Host harness: `tests/test_fdm_centre.py`. |
| `zinkfix/` | `arm64-v8a/libVkLayer_fp_shaderfix.so` | Shader-fix Vulkan layer `VK_LAYER_FP_shader_fix` for OpenGL ES games (patch `frame.zink_shader_fix`): sits between Zink (Mesa's GL on Vulkan) and the driver and inserts the words of `zink_shader_fix` (same format as the Vulkan shim's `vk_shader_fix`: size + SHA-256 match) into SPIR-V modules; `zink_shader_dump=1` writes every distinct module to `files/fp_spirv/`. The engine library loads it first (DT_NEEDED); its constructor puts the layer in front of Android's GraphicsEnv debug layer list (private Android 11 `getDebugLayers`/`setDebugLayers`, found through libvulkan.so's dependencies) before EGL starts, so the loader picks it up from the app's lib dir. Exports only the enumeration functions and `VK_LAYER_FP_shader_fixGet*ProcAddr` (never plain `vk*` entry points). Vader Immortal's lightspeed shaders (Klownicle, GitHub #49). |
| `xrlayer/` | `linux-arm64/libxr_frameport_timefix.so` + `XR_APILAYER_FRAMEPORT_timefix.json` | OpenXR API layer for Windows PC VR games under Proton on the Frame (glibc aarch64, built freestanding with the NDK's clang), on by default (patch `pcvr.xr_timefix`). Retries `xrCreateInstance` as an OpenXR 1.0 app when the runtime rejects 1.1 (the Frame's SteamVR runtime does, and Proton 11's VR helper asks for 1.1). Also emulates `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` if the runtime refuses them (the Frame's Android runtime does; its Linux runtime, used by Proton, supports them as of 2026-09-29 — so this part is only a fallback; Proton's wineopenxr needs them for `XR_KHR_win32_convert_performance_counter_time`, which Revive requires) with the adapter's xrWaitFrame-calibrated offset, and drops the extension from xrCreateInstance if the runtime rejects it. Enabled per game by the Proton launch.sh (`XR_API_LAYER_PATH`, `XR_ENABLE_API_LAYERS`). `tests/test_xrlayer.py` builds it for x86_64 and drives it via ctypes. |
| `oculushmd/` | `win-x64/fp_oculushmd.exe` | PC VR counterpart of overport's `patch_oculus_unreal` (patch `pcvr.oculus_unreal`, default for Unreal Rift games on the Frame). Unreal's OculusHMD and LibOVR's `ovr_Detect` only start when the Windows named event `OculusHMDConnected` exists and is signalled (the Oculus service creates it). `fp_oculushmd.exe <command line>` creates it (manual reset, signalled), runs the command (Revive's injector) in a job object and keeps the event until every process in the job — the game and its children — has exited (fallback without job support: 3 minutes); returns the command's exit code and logs `FramePort oculushmd: …` to stderr (launch.log). Freestanding Windows x64 console exe (no CRT, no Windows SDK): the NDK's clang + `lld-link /Brepro` with a kernel32 import library generated by `llvm-dlltool`, so it's byte-reproducible. Tested on Windows from WSL with a build whose event name is `-DEVENT_NAME=L"…"` (the Oculus service owns the real event on a PC with the Oculus app). |
+60
View File
@@ -0,0 +1,60 @@
// FramePort FrameBridge: tells FramePort's foveation centring layer (native/foveation/centre.h, in
// libVkLayer_fdm_darken.so, which this library links on arm64) where each eye's view is (GitHub #394). Every
// xrEndFrame: the first projection layer's views as swapchain size, array layer, imageRect and the eye's optical
// centre (the straight-ahead direction from the view's FOV angles; not the rect's centre when the FOV is
// asymmetric), in the rows of the Vulkan image the density map covers (GLES through Zink: GL's bottom row is the
// image's first row). Sent only when something changed.
#include <stddef.h>
typedef struct {
uint32_t width, height, layer;
int32_t x, y, w, h;
float cx, cy;
} fdm_view_info;
// One eye's view; 0 when the swapchain or the FOV is unusable.
static int fdm_view_of(const XrCompositionLayerProjectionView *pv, int gles, fdm_view_info *o) {
uint32_t w = 0, h = 0;
if (!swapchain_size(pv->subImage.swapchain, &w, &h)) return 0;
const XrRect2Di *r = &pv->subImage.imageRect;
float l = tanf(pv->fov.angleLeft), rt = tanf(pv->fov.angleRight);
float up = tanf(pv->fov.angleUp), dn = tanf(pv->fov.angleDown);
if (!(rt - l > 1e-4f) || !(up - dn > 1e-4f) || r->extent.width <= 0 || r->extent.height <= 0) return 0;
o->width = w, o->height = h, o->layer = pv->subImage.imageArrayIndex;
o->x = r->offset.x, o->y = r->offset.y, o->w = r->extent.width, o->h = r->extent.height;
o->cx = (float)r->offset.x + (float)r->extent.width * (-l / (rt - l));
o->cy = (float)r->offset.y + (float)r->extent.height * ((gles ? -dn : up) / (up - dn));
return 1;
}
// In libVkLayer_fdm_darken.so (a DT_NEEDED of the arm64 build); NULL without it (32-bit builds).
extern void fp_fdm_set_views(const fdm_view_info *views, uint32_t count) __attribute__((weak));
static void fdm_views_end_frame(const XrFrameEndInfo *info) {
void (*set_views)(const fdm_view_info *, uint32_t) = fp_fdm_set_views;
if (!set_views || !info) return;
const XrCompositionLayerProjection *proj = NULL;
for (uint32_t i = 0; i < info->layerCount && !proj; ++i)
if (info->layers[i] && info->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION)
proj = (const XrCompositionLayerProjection *)info->layers[i];
if (!proj) return; // keep the last views (a frame of quads only doesn't move the eyes)
fdm_view_info views[4];
uint32_t n = 0;
for (uint32_t v = 0; v < proj->viewCount && n < 4; ++v)
n += (uint32_t)fdm_view_of(&proj->views[v], !vk.device, &views[n]);
static fdm_view_info last[4];
static uint32_t last_n;
// the FOV moves a little every frame: resend when a centre moved by a pixel or more
int changed = n != last_n;
for (uint32_t i = 0; i < n && !changed; ++i)
changed = memcmp(&views[i], &last[i], offsetof(fdm_view_info, cx)) || fabsf(views[i].cx - last[i].cx) >= 1.0f ||
fabsf(views[i].cy - last[i].cy) >= 1.0f;
if (!changed) return;
memcpy(last, views, sizeof(views));
last_n = n;
set_views(views, n);
static int logged;
for (uint32_t i = 0; i < n && logged < 8; ++i, ++logged)
LOG("fdm views: %ux%u layer %u rect=%d,%d %dx%d optical centre %.0f,%.0f", views[i].width, views[i].height,
views[i].layer, views[i].x, views[i].y, views[i].w, views[i].h, views[i].cx, views[i].cy);
}
+6
View File
@@ -665,6 +665,7 @@ static void forget_swapchain(XrSwapchain handle) {
static void flip_on_create_swapchain(XrSwapchain handle, const XrSwapchainCreateInfo *info);
static int flip_wants_transfer(const XrSwapchainCreateInfo *info);
static int snap_vk_wants_transfer(const XrSwapchainCreateInfo *info);
static void clear_gl_errors(const char *where);
static void flip_on_destroy(XrSwapchain handle);
static void ab_on_destroy(XrSwapchain handle);
@@ -773,6 +774,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
// in-place flips of upside-down quads (flip_vk.c) blit from and back into these images
if (flip_wants_transfer(&fixed))
fixed.usageFlags |= XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
// Vulkan eye snapshots (snapshot_vk.c) copy from the shown images
if (snap_vk_wants_transfer(&fixed)) fixed.usageFlags |= XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT;
if (equirect_emul && swapchain && emul_virtual_create(session, &fixed, swapchain)) {
remember_swapchain(*swapchain);
emul_on_create_swapchain(*swapchain, &fixed);
@@ -1179,6 +1182,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
#include "layer_emul_gl.c"
#include "swapchain_pool.c"
#include "cube_standin.c"
#include "snapshot_vk.c"
#include "fdm_views.c"
#include "snapshot_gl.c"
#include "surface_swapchain.c"
#include "fade_quad.c"
@@ -1811,6 +1816,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
clear_gl_errors("xrEndFrame");
__atomic_add_fetch(&frame_ends, 1, __ATOMIC_RELAXED);
snap_end_frame(info);
fdm_views_end_frame(info);
if(surface_native)++sc_serial;
if (eye_debug) eye_debug_end_frame(session, info);
{ // frame pacing statistics every ~5 s: fps and submitted-vs-predicted display time
+6 -1
View File
@@ -1,4 +1,5 @@
// FramePort FrameBridge: eye-image snapshots for headless diagnostics (setting snapshot=<seconds>, GLES games only).
// FramePort FrameBridge: eye-image snapshots for headless diagnostics (setting snapshot=<seconds>; Vulkan games:
// snapshot_vk.c).
// Every <seconds>, at xrEndFrame, the left-eye image the game just submitted (projection layer, view 0: its swapchain
// image and array layer; without a projection layer the first quad/cylinder layer's) is read back on the game's own GL context and written at a quarter of its size as
// /sdcard/Android/data/<package>/files/fb_snap_<n>.ppm (n = 0-7, the oldest is overwritten). Nobody wears the headset
@@ -133,6 +134,10 @@ static void snap_end_frame(const XrFrameEndInfo *info) {
else if (layer && layer->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR)
sub = &((const XrCompositionLayerCylinderKHR *)layer)->subImage, type = layer->type;
}
if (vk.device) { // a Vulkan session: snapshot_vk.c reads both views back
snap_vk_end_frame(info, taken++ % 8);
return;
}
if (!sub || !snap_load_gl()) return;
if (!s_eglGetCurrentContext()) {
static int logged;
+121
View File
@@ -0,0 +1,121 @@
// FramePort FrameBridge: eye-image snapshots of Vulkan games (setting snapshot=<seconds>; snapshot_gl.c has the GLES
// half and the timing). At xrEndFrame both views of the first projection layer are read back from the image the
// layer shows (its whole swapchain array layer, not only the imageRect) and written at half size as
// files/fb_snap_vk_<n>_v<view>.ppm (n = 0-7); the log line gives the imageRect and FOV so the submitted rect and the
// eye's optical centre can be drawn over it. With "Darken coarse fragments (test)" this shows where Valve's foveation
// rendered at full density (GitHub #394). Diagnostics only: the copy waits for the GPU on the render thread, and
// projection swapchains get transfer-source usage while snapshots are on.
#define SNAP_VK_SCALE 2
static int snap_vk_wants_transfer(const XrSwapchainCreateInfo *info) {
return snapshot && vk.device && info && info->faceCount == 1 && info->sampleCount == 1 &&
(info->usageFlags & XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT);
}
static void snap_vk_write(const unsigned char *px, uint32_t w, uint32_t h, int bgra, int n, uint32_t view) {
char path[320];
snprintf(path, sizeof(path), "%s/fb_snap_vk_%d_v%u.ppm", snap_dir, n, view);
FILE *f = fopen(path, "wb");
if (!f) { LOG("snapshot: can't write %s", path); return; }
uint32_t ow = w / SNAP_VK_SCALE, oh = h / SNAP_VK_SCALE;
fprintf(f, "P6\n%u %u\n255\n", ow, oh);
unsigned char *row = malloc((size_t)ow * 3);
for (uint32_t y = 0; row && y < oh; ++y) {
const unsigned char *src = px + (size_t)y * SNAP_VK_SCALE * w * 4; // Vulkan rows are top-down
for (uint32_t x = 0; x < ow; ++x) {
const unsigned char *p = src + (size_t)x * SNAP_VK_SCALE * 4;
row[x * 3] = p[bgra ? 2 : 0], row[x * 3 + 1] = p[1], row[x * 3 + 2] = p[bgra ? 0 : 2];
}
fwrite(row, 1, (size_t)ow * 3, f);
}
free(row);
fclose(f);
}
static void snap_vk_end_frame(const XrFrameEndInfo *info, int n) {
const XrCompositionLayerProjection *proj = NULL;
for (uint32_t i = 0; i < info->layerCount && !proj; ++i)
if (info->layers[i] && info->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
((const XrCompositionLayerProjection *)info->layers[i])->viewCount)
proj = (const XrCompositionLayerProjection *)info->layers[i];
if (!proj || !vk_ready() || !vk_cmd_idle()) return;
uint32_t views = proj->viewCount < 2 ? proj->viewCount : 2;
for (uint32_t v = 0; v < views; ++v) {
const XrCompositionLayerProjectionView *pv = &proj->views[v];
const XrSwapchainSubImage *sub = &pv->subImage;
VkImage image = VK_NULL_HANDLE;
uint32_t w = 0, h = 0;
int64_t format = 0;
int why = 0;
pthread_mutex_lock(&flip_lock);
tracked_swapchain *t = find_tracked(sub->swapchain, 0);
uint32_t index = t ? shown_image(t) : 0;
if (!t || !t->image_count || index >= t->image_count) why = 1;
else if (!(t->info.usageFlags & XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT)) why = 2;
else if (sub->imageArrayIndex >= t->info.arraySize) why = 3;
else image = t->images[index], w = t->info.width, h = t->info.height, format = t->info.format;
pthread_mutex_unlock(&flip_lock);
int bgra = format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_B8G8R8A8_UNORM;
if (!why && !bgra && format != VK_FORMAT_R8G8B8A8_SRGB && format != VK_FORMAT_R8G8B8A8_UNORM) why = 4;
if (why) {
static unsigned logged;
if (!(logged & (1u << why))) {
logged |= 1u << why;
LOG("snapshot: Vulkan view %u not readable (%s)", v, why == 1 ? "unknown swapchain or images" :
why == 2 ? "no transfer usage" : why == 3 ? "array layer out of range" : "not an 8-bit RGBA format");
}
continue;
}
VkDeviceSize size = (VkDeviceSize)w * h * 4;
VkBuffer buf = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, NULL, 0, size, VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_SHARING_MODE_EXCLUSIVE, 0, NULL};
if (vk.CreateBuffer(vk.device, &bi, NULL, &buf) != VK_SUCCESS) return;
VkMemoryRequirements req;
vk.GetBufferMemoryRequirements(vk.device, buf, &req);
uint32_t type = UINT32_MAX;
const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
for (uint32_t i = 0; i < vk.memory.memoryTypeCount && type == UINT32_MAX; ++i)
if ((req.memoryTypeBits & (1u << i)) && (vk.memory.memoryTypes[i].propertyFlags & want) == want) type = i;
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, req.size, type};
if (type == UINT32_MAX || vk.AllocateMemory(vk.device, &ai, NULL, &mem) != VK_SUCCESS ||
vk.BindBufferMemory(vk.device, buf, mem, 0) != VK_SUCCESS) {
if (mem) vk.FreeMemory(vk.device, mem, NULL);
vk.DestroyBuffer(vk.device, buf, NULL);
return;
}
VkCommandBufferBeginInfo begin = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, NULL};
int ok = vk.ResetCommandBuffer(vk.cmd, 0) == VK_SUCCESS && vk.BeginCommandBuffer(vk.cmd, &begin) == VK_SUCCESS;
if (ok) {
// released swapchain images are in COLOR_ATTACHMENT_OPTIMAL (XR_KHR_vulkan_enable)
barrier(image, sub->imageArrayIndex, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy region = {0, 0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, sub->imageArrayIndex, 1}, {0, 0, 0},
{w, h, 1}};
vk.CmdCopyImageToBuffer(vk.cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &region);
barrier(image, sub->imageArrayIndex, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
ok = vk.EndCommandBuffer(vk.cmd) == VK_SUCCESS && vk_cmd_submit(2000000000ull) == 1;
}
void *mapped = NULL;
if (ok && vk.MapMemory(vk.device, mem, 0, size, 0, &mapped) == VK_SUCCESS) {
snap_vk_write(mapped, w, h, bgra, n, v);
vk.UnmapMemory(vk.device, mem);
const XrRect2Di *r = &sub->imageRect;
LOG("snapshot: vk view %u -> fb_snap_vk_%d_v%u.ppm (1/%d) image %ux%u array=%u rect=%d,%d %dx%d "
"fov=%.4f/%.4f/%.4f/%.4f", v, n, v, SNAP_VK_SCALE, w, h, sub->imageArrayIndex, r->offset.x,
r->offset.y, r->extent.width, r->extent.height, pv->fov.angleLeft, pv->fov.angleRight,
pv->fov.angleUp, pv->fov.angleDown);
} else {
static int logged;
if (logged++ < 3) LOG("snapshot: Vulkan readback of view %u failed", v);
}
if (vk.pending && !vk_cmd_idle()) return; // still running: leak the buffer rather than free it under the GPU
vk.FreeMemory(vk.device, mem, NULL);
vk.DestroyBuffer(vk.device, buf, NULL);
}
}
+1 -1
View File
@@ -384,7 +384,7 @@ def build_foveation(tc: Path):
run([exe(tc, "aarch64-linux-android29-clang"), "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror",
"-fvisibility=hidden", f"-ffile-prefix-map={src}=native/foveation",
"-Wl,-soname,libVkLayer_fdm_darken.so", "-Wl,-z,max-page-size=16384", "darken.c",
"-ldl", "-llog", "-o", ART / "arm64-v8a/libVkLayer_fdm_darken.so"], cwd=src)
"-ldl", "-llog", "-lm", "-o", ART / "arm64-v8a/libVkLayer_fdm_darken.so"], cwd=src)
def build_ovrpshim(tc: Path):
+605
View File
@@ -0,0 +1,605 @@
// SPDX-License-Identifier: GPL-3.0-only
// VK_LAYER_FRAMEPORT_fdm_centre (GitHub #394): puts the full-density area of Valve's foveation on each eye.
//
// Valve's VALVE_fdm_injection layer gives every eye swapchain a fragment density map sized to the WHOLE swapchain
// (ceil(w/32) x ceil(h/32) texels, one array layer per eye) and draws its preset rectangles centred on that map,
// i.e. on the swapchain's centre. The eye's view is the imageRect the game submits, often smaller than the swapchain
// and placed off-centre (dynamic resolution: a corner-anchored rect; asymmetric eyes: rect=0,0 / rect=287,0 inside a
// wider swapchain), and the eye's optical axis isn't the rect's centre either (asymmetric FOV, e.g. -1.03/+0.88 rad).
// So the sharp area sat towards the swapchain centre: off to one side or the bottom right of what the eye sees.
//
// This layer runs BELOW Valve's (inserted right after it in Android's debug-layer list), so it sees the render passes
// with Valve's density map attached and the per-view density-map offsets Valve passes at the end of the pass
// (VK_EXT/QCOM_fragment_density_map_offset: the map is shifted by the offset). It replaces each view's offset with
// one that puts the map's sharp centre on that eye's optical centre (Fixed: no gaze; Valve passes no offsets then and
// this layer adds them) or on the gaze point inside the eye's rect (Eye-tracked: Valve's gaze offset is measured from
// the swapchain centre and scaled by the render area; it is re-based on the rect). FrameBridge reports the views
// (fp_fdm_set_views, every xrEndFrame): swapchain size, array layer, imageRect and the optical centre, all in the
// Vulkan image's pixel rows. Passes are matched to views by framebuffer size and layer, like Valve matches maps.
// Off with FRAMEPORT_FDM_CENTRE=0 (developers: compare against Valve's placement).
#define CENTRE_NAME "VK_LAYER_FRAMEPORT_fdm_centre"
#define FP_FDM_MAX_VIEWS 4
typedef struct {
uint32_t width, height, layer; // swapchain image size, array layer
int32_t x, y, w, h; // imageRect in Vulkan image rows (top-down)
float cx, cy; // optical centre (straight-ahead direction) in the same pixels
} fp_fdm_view;
static fp_fdm_view c_views[FP_FDM_MAX_VIEWS];
static uint32_t c_view_count;
static int c_lock;
static int c_level = 1; // Valve's preset: 1 lo, 2 med, 3 hi (FOVE_LEVEL / FDM_DEBUG, as Valve reads them)
static int c_offsets_off; // FDM_DEBUG has disable_offsets (FramePort's Fixed mode)
static int c_disabled; // FRAMEPORT_FDM_CENTRE=0
EXPORT void fp_fdm_set_views(const fp_fdm_view *views, uint32_t count) {
if (count > FP_FDM_MAX_VIEWS) count = FP_FDM_MAX_VIEWS;
take_lock(&c_lock);
if (views && count) memcpy(c_views, views, count * sizeof(*views));
c_view_count = views ? count : 0;
drop_lock(&c_lock);
}
// Valve's full-density rectangle per preset (x, y, w, h in map-normalized coordinates; texel i is sampled at
// i / (texels - 1)), from libVkLayer_VALVE_fdm_injection.so (SteamOS 0.5.x, 2026-10).
static const float c_preset[4][4] = {{0, 0, 0, 0}, {.364f, .367f, .295f, .286f}, {.386f, .388f, .25f, .245f},
{.386f, .404f, .246f, .211f}};
// Where Turnip puts the centre of Valve's sharp texels, in framebuffer pixels: Turnip samples the map with a texel
// size of 2^ceil(log2(framebuffer / map)) pixels (at least 32), see tu_fragment_density_map_sample().
static float c_valve_centre(uint32_t fb, uint32_t texels, int axis) {
if (!texels) return fb / 2.0f;
uint32_t ratio = fb / texels, shift = 0;
while ((1u << shift) < ratio) ++shift;
if (shift < 5) shift = 5;
if (shift > 10) shift = 10;
float lo = c_preset[c_level][axis], hi = lo + c_preset[c_level][axis + 2];
float n = (float)(texels > 1 ? texels - 1 : 1);
int first = (int)ceilf(lo * n - 1e-4f), last = (int)floorf(hi * n + 1e-4f);
if (last < first) last = first;
return (float)(1u << shift) * ((float)(first + last + 1) / 2.0f);
}
// ---------------------------------------------------------------- object tracking (handles below Valve's layer)
#define C_SLOTS 2048
typedef struct { uint64_t key; uint64_t a, b, c; } c_slot; // a/b/c meaning per table
static c_slot c_images[C_SLOTS], c_ivs[C_SLOTS], c_rps[C_SLOTS], c_fbs[C_SLOTS], c_cmds[C_SLOTS];
static int c_obj_lock;
// Not the darkening layer's lock: it holds that one while asking the next layer (Valve's, then this one) for
// functions in vkCreateDevice.
static int c_dev_lock;
static c_slot *c_find(c_slot *t, uint64_t key, int create) {
if (!key) return NULL;
uint32_t h = (uint32_t)((key * 0x9E3779B97F4A7C15ull) >> 53) & (C_SLOTS - 1);
c_slot *free_slot = NULL;
for (uint32_t i = 0; i < C_SLOTS; ++i) {
c_slot *s = &t[(h + i) & (C_SLOTS - 1)];
if (s->key == key) return s;
if (s->key == 0) { if (!free_slot) free_slot = s; break; }
if (s->key == ~0ull && !free_slot) free_slot = s;
}
if (!create || !free_slot) return NULL;
memset(free_slot, 0, sizeof(*free_slot));
free_slot->key = key;
return free_slot;
}
static void c_forget(c_slot *t, uint64_t key) {
take_lock(&c_obj_lock);
c_slot *s = c_find(t, key, 0);
if (s) s->key = ~0ull; // tombstone
drop_lock(&c_obj_lock);
}
#define H(x) ((uint64_t)(uintptr_t)(x))
typedef void (VKAPI_PTR *PFN_fp_EndRendering2)(VkCommandBuffer, const void *);
typedef struct {
void *key;
VkDevice device;
PFN_vkGetDeviceProcAddr gdpa;
VkExtent2D gran;
PFN_vkCreateImage CreateImage;
PFN_vkDestroyImage DestroyImage;
PFN_vkCreateImageView CreateImageView;
PFN_vkDestroyImageView DestroyImageView;
PFN_vkCreateRenderPass CreateRenderPass;
PFN_vkCreateRenderPass2 CreateRenderPass2, CreateRenderPass2KHR;
PFN_vkDestroyRenderPass DestroyRenderPass;
PFN_vkCreateFramebuffer CreateFramebuffer;
PFN_vkDestroyFramebuffer DestroyFramebuffer;
PFN_vkCmdBeginRenderPass CmdBeginRenderPass;
PFN_vkCmdBeginRenderPass2 CmdBeginRenderPass2, CmdBeginRenderPass2KHR;
PFN_vkCmdEndRenderPass CmdEndRenderPass;
PFN_vkCmdEndRenderPass2 CmdEndRenderPass2, CmdEndRenderPass2KHR;
PFN_vkCmdBeginRendering CmdBeginRendering, CmdBeginRenderingKHR;
PFN_vkCmdEndRendering CmdEndRendering, CmdEndRenderingKHR;
PFN_fp_EndRendering2 CmdEndRendering2EXT, CmdEndRendering2KHR;
} c_device;
static struct { void *key; VkInstance instance; PFN_vkGetInstanceProcAddr gipa; } c_instances[MAX_HANDLES];
static c_device c_devices[MAX_HANDLES];
static c_device *c_dev(const void *handle) {
void *key = key_of(handle);
c_device *d = NULL;
take_lock(&c_dev_lock);
for (int i = 0; i < MAX_HANDLES && !d; i++)
if (c_devices[i].gdpa && c_devices[i].key == key) d = &c_devices[i];
drop_lock(&c_dev_lock);
return d;
}
static const VkBaseInStructure *c_chain(const void *next, VkStructureType type) {
for (const VkBaseInStructure *b = next; b; b = b->pNext)
if (b->sType == type) return b;
return NULL;
}
#define ST_FDM_RP_INFO ((VkStructureType)1000218002) // VkRenderPassFragmentDensityMapCreateInfoEXT
#define ST_FDM_RENDERING_INFO ((VkStructureType)1000044007) // VkRenderingFragmentDensityMapAttachmentInfoEXT
#define ST_FDM_OFFSET_END ((VkStructureType)1000425002) // VkRenderPassFragmentDensityMapOffsetEndInfoEXT
#define ST_RENDERING_END ((VkStructureType)1000619003) // VkRenderingEndInfoKHR
#define ST_RP_ATTACHMENT_BEGIN ((VkStructureType)1000108003)
#define ST_FDM_OFFSET_PROPS ((VkStructureType)1000425001)
#define FDM_USAGE 0x200u
#define FDM_OFFSET_FLAG 0x8000u
typedef struct { VkStructureType sType; const void *pNext; uint32_t count; const VkOffset2D *offsets; } c_offset_end;
typedef struct { VkStructureType sType; const void *pNext; } c_rendering_end;
// Fixed mode (FDM_DEBUG=disable_offsets): Valve then creates images without the density-map-offset flag, and Turnip
// only applies offsets to maps that have it (its tiling and LRZ also size depth attachments for offsets by the flag).
// Flag them as Valve does when its offsets are on (all 2D render targets and maps), so the fixed centre can be set.
#define C_OFFSET_USAGE (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | FDM_USAGE)
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateImage(VkDevice device, const VkImageCreateInfo *ci,
const VkAllocationCallbacks *a, VkImage *image) {
c_device *d = c_dev(device);
VkImageCreateInfo flagged;
VkResult r = VK_ERROR_FEATURE_NOT_PRESENT;
if (c_offsets_off && !c_disabled && ci && ci->imageType == VK_IMAGE_TYPE_2D && (ci->usage & C_OFFSET_USAGE) &&
!(ci->flags & FDM_OFFSET_FLAG)) {
flagged = *ci;
flagged.flags |= FDM_OFFSET_FLAG;
if ((r = d->CreateImage(device, &flagged, a, image)) == VK_SUCCESS) ci = &flagged;
}
if (r != VK_SUCCESS) r = d->CreateImage(device, ci, a, image); // unflagged, as asked (or the flag refused)
if (r == VK_SUCCESS && (ci->usage & FDM_USAGE)) {
take_lock(&c_obj_lock);
c_slot *s = c_find(c_images, H(*image), 1);
if (s) s->a = ci->extent.width, s->b = ci->extent.height, s->c = ci->flags;
drop_lock(&c_obj_lock);
static int logged;
if (logged++ < 4)
LOG("fdm_centre: density map %ux%u[%u] flags 0x%x", ci->extent.width, ci->extent.height, ci->arrayLayers,
ci->flags);
}
return r;
}
static VKAPI_ATTR void VKAPI_CALL c_DestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks *a) {
c_forget(c_images, H(image));
c_dev(device)->DestroyImage(device, image, a);
}
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateImageView(VkDevice device, const VkImageViewCreateInfo *ci,
const VkAllocationCallbacks *a, VkImageView *view) {
VkResult r = c_dev(device)->CreateImageView(device, ci, a, view);
if (r == VK_SUCCESS) {
take_lock(&c_obj_lock);
c_slot *img = c_find(c_images, H(ci->image), 0);
c_slot *s = img ? c_find(c_ivs, H(*view), 1) : NULL;
if (s) s->a = img->a, s->b = img->b, s->c = img->c;
drop_lock(&c_obj_lock);
}
return r;
}
static VKAPI_ATTR void VKAPI_CALL c_DestroyImageView(VkDevice device, VkImageView view, const VkAllocationCallbacks *a) {
c_forget(c_ivs, H(view));
c_dev(device)->DestroyImageView(device, view, a);
}
static void c_note_rp(const void *next, VkRenderPass rp) {
const VkRenderPassFragmentDensityMapCreateInfoEXT *f = (const void *)c_chain(next, ST_FDM_RP_INFO);
if (!f || f->fragmentDensityMapAttachment.attachment == VK_ATTACHMENT_UNUSED) return;
take_lock(&c_obj_lock);
c_slot *s = c_find(c_rps, H(rp), 1);
if (s) s->a = f->fragmentDensityMapAttachment.attachment;
drop_lock(&c_obj_lock);
}
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateRenderPass(VkDevice device, const VkRenderPassCreateInfo *ci,
const VkAllocationCallbacks *a, VkRenderPass *rp) {
VkResult r = c_dev(device)->CreateRenderPass(device, ci, a, rp);
if (r == VK_SUCCESS) c_note_rp(ci->pNext, *rp);
return r;
}
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateRenderPass2(VkDevice device, const VkRenderPassCreateInfo2 *ci,
const VkAllocationCallbacks *a, VkRenderPass *rp) {
VkResult r = c_dev(device)->CreateRenderPass2(device, ci, a, rp);
if (r == VK_SUCCESS) c_note_rp(ci->pNext, *rp);
return r;
}
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateRenderPass2KHR(VkDevice device, const VkRenderPassCreateInfo2 *ci,
const VkAllocationCallbacks *a, VkRenderPass *rp) {
VkResult r = c_dev(device)->CreateRenderPass2KHR(device, ci, a, rp);
if (r == VK_SUCCESS) c_note_rp(ci->pNext, *rp);
return r;
}
static VKAPI_ATTR void VKAPI_CALL c_DestroyRenderPass(VkDevice device, VkRenderPass rp, const VkAllocationCallbacks *a) {
c_forget(c_rps, H(rp));
c_dev(device)->DestroyRenderPass(device, rp, a);
}
// framebuffer: a = width | height << 32, b = density map view (0: imageless), c = map attachment index + 1
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateFramebuffer(VkDevice device, const VkFramebufferCreateInfo *ci,
const VkAllocationCallbacks *a, VkFramebuffer *fb) {
VkResult r = c_dev(device)->CreateFramebuffer(device, ci, a, fb);
if (r == VK_SUCCESS) {
take_lock(&c_obj_lock);
c_slot *rp = c_find(c_rps, H(ci->renderPass), 0);
if (rp) {
c_slot *s = c_find(c_fbs, H(*fb), 1);
if (s) {
s->a = ci->width | (uint64_t)ci->height << 32;
s->b = (!(ci->flags & VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT) && ci->pAttachments &&
rp->a < ci->attachmentCount) ? H(ci->pAttachments[rp->a]) : 0;
s->c = rp->a + 1;
}
}
drop_lock(&c_obj_lock);
}
return r;
}
static VKAPI_ATTR void VKAPI_CALL c_DestroyFramebuffer(VkDevice device, VkFramebuffer fb, const VkAllocationCallbacks *a) {
c_forget(c_fbs, H(fb));
c_dev(device)->DestroyFramebuffer(device, fb, a);
}
// command buffer state: a = framebuffer width | height << 32, b = map width | height << 32 (0: no map), c = map flags
static void c_begin(VkCommandBuffer cmd, uint32_t fbw, uint32_t fbh, VkImageView map) {
take_lock(&c_obj_lock);
c_slot *iv = map ? c_find(c_ivs, H(map), 0) : NULL;
c_slot *s = c_find(c_cmds, H(cmd), iv != NULL);
if (s) {
s->a = fbw | (uint64_t)fbh << 32;
s->b = iv ? (iv->a | iv->b << 32) : 0;
s->c = iv ? iv->c : 0;
}
drop_lock(&c_obj_lock);
}
static void c_begin_rp(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info) {
if (c_disabled || !info) return;
take_lock(&c_obj_lock);
c_slot *fb = c_find(c_fbs, H(info->framebuffer), 0);
uint64_t size = fb ? fb->a : 0, view = fb ? fb->b : 0, index = fb ? fb->c : 0;
drop_lock(&c_obj_lock);
if (fb && !view && index) { // imageless framebuffer: the views come with the begin
const VkRenderPassAttachmentBeginInfo *ab = (const void *)c_chain(info->pNext, ST_RP_ATTACHMENT_BEGIN);
if (ab && index - 1 < ab->attachmentCount) view = H(ab->pAttachments[index - 1]);
}
c_begin(cmd, (uint32_t)size, (uint32_t)(size >> 32), (VkImageView)(uintptr_t)view);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRenderPass(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info,
VkSubpassContents contents) {
c_begin_rp(cmd, info);
c_dev(cmd)->CmdBeginRenderPass(cmd, info, contents);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRenderPass2(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info,
const VkSubpassBeginInfo *sub) {
c_begin_rp(cmd, info);
c_dev(cmd)->CmdBeginRenderPass2(cmd, info, sub);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRenderPass2KHR(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info,
const VkSubpassBeginInfo *sub) {
c_begin_rp(cmd, info);
c_dev(cmd)->CmdBeginRenderPass2KHR(cmd, info, sub);
}
static void c_begin_rendering(VkCommandBuffer cmd, const VkRenderingInfo *info) {
if (c_disabled || !info) return;
const VkRenderingFragmentDensityMapAttachmentInfoEXT *f = (const void *)c_chain(info->pNext, ST_FDM_RENDERING_INFO);
// Turnip: a dynamic-rendering framebuffer ends where the render area ends
c_begin(cmd, (uint32_t)(info->renderArea.offset.x + (int32_t)info->renderArea.extent.width),
(uint32_t)(info->renderArea.offset.y + (int32_t)info->renderArea.extent.height), f ? f->imageView : VK_NULL_HANDLE);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRendering(VkCommandBuffer cmd, const VkRenderingInfo *info) {
c_begin_rendering(cmd, info);
c_dev(cmd)->CmdBeginRendering(cmd, info);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRenderingKHR(VkCommandBuffer cmd, const VkRenderingInfo *info) {
c_begin_rendering(cmd, info);
c_dev(cmd)->CmdBeginRenderingKHR(cmd, info);
}
static int c_round(float v, uint32_t gran) {
if (!gran) gran = 1;
return (int)lroundf(v / (float)gran) * (int)gran;
}
// The density-map offsets this pass ends with: in = Valve's (NULL: none), returns the count written to out (0: leave
// the pass as it is).
static uint32_t c_offsets(VkCommandBuffer cmd, c_device *d, uint32_t in_count, const VkOffset2D *in, VkOffset2D *out) {
take_lock(&c_obj_lock);
c_slot *s = c_find(c_cmds, H(cmd), 0);
uint64_t size = s ? s->a : 0, map = s ? s->b : 0, flags = s ? s->c : 0;
if (s) s->key = ~0ull; // one pass at a time per command buffer
drop_lock(&c_obj_lock);
if (!map || !(flags & FDM_OFFSET_FLAG)) return 0;
uint32_t fbw = (uint32_t)size, fbh = (uint32_t)(size >> 32), mw = (uint32_t)map, mh = (uint32_t)(map >> 32);
fp_fdm_view views[FP_FDM_MAX_VIEWS];
take_lock(&c_lock);
uint32_t nviews = c_view_count;
memcpy(views, c_views, sizeof(views));
drop_lock(&c_lock);
uint32_t count = in ? in_count : 0;
if (!count) { // Valve passed none (Fixed: disable_offsets): one per eye layer of this swapchain size
for (uint32_t i = 0; i < nviews; ++i)
if (views[i].width == fbw && views[i].height == fbh && views[i].layer + 1 > count) count = views[i].layer + 1;
}
if (!count || count > FP_FDM_MAX_VIEWS) return 0;
float vx = c_valve_centre(fbw, mw, 0), vy = c_valve_centre(fbh, mh, 1);
// Valve's offsets are all 0 when it has no gaze (no eye tracking, or not worn): then straight ahead
int gaze = 0;
for (uint32_t i = 0; in && !c_offsets_off && i < count; ++i) gaze |= in[i].x || in[i].y;
int matched = 0;
for (uint32_t layer = 0; layer < count; ++layer) {
out[layer] = in ? in[layer] : (VkOffset2D){0, 0};
const fp_fdm_view *v = NULL;
int candidates = 0;
for (uint32_t i = 0; i < nviews; ++i)
if (views[i].width == fbw && views[i].height == fbh && views[i].layer == layer) v = &views[i], ++candidates;
if (candidates != 1) continue; // e.g. both eyes side by side in one layer: one map can't serve both
float dx = v->cx, dy = v->cy;
if (gaze) {
// Valve: gaze NDC * render-area half size from the swapchain centre; the gaze is in the eye's view
dx = (float)v->x + (float)v->w / 2.0f + (float)in[layer].x * (float)v->w / (float)fbw;
dy = (float)v->y + (float)v->h / 2.0f + (float)in[layer].y * (float)v->h / (float)fbh;
}
out[layer].x = c_round(dx - vx, d->gran.width);
out[layer].y = c_round(dy - vy, d->gran.height);
++matched;
}
static int logged;
if (matched && logged++ < 6)
LOG("fdm_centre: pass %ux%u map %ux%u (preset %d, Valve centre %.0f,%.0f): offsets %s%d,%d%s -> %d,%d / %d,%d",
fbw, fbh, mw, mh, c_level, vx, vy, in ? "" : "none (", in ? in[0].x : 0, in ? in[0].y : 0, in ? "" : ")",
out[0].x, out[0].y, count > 1 ? out[1].x : 0, count > 1 ? out[1].y : 0);
return matched ? count : 0;
}
static VKAPI_ATTR void VKAPI_CALL c_end_rp2(VkCommandBuffer cmd, const VkSubpassEndInfo *info, int khr) {
c_device *d = c_dev(cmd);
PFN_vkCmdEndRenderPass2 next = khr && d->CmdEndRenderPass2KHR ? d->CmdEndRenderPass2KHR : d->CmdEndRenderPass2;
const c_offset_end *valve = info ? (const void *)c_chain(info->pNext, ST_FDM_OFFSET_END) : NULL;
VkOffset2D offsets[FP_FDM_MAX_VIEWS];
uint32_t n = c_disabled ? 0 : c_offsets(cmd, d, valve ? valve->count : 0, valve ? valve->offsets : NULL, offsets);
if (!n || (valve && info->pNext != (const void *)valve)) { // nothing to change, or not first in the chain
next(cmd, info);
return;
}
c_offset_end mine = {ST_FDM_OFFSET_END, valve ? valve->pNext : info->pNext, n, offsets};
VkSubpassEndInfo copy = *info;
copy.pNext = &mine;
next(cmd, &copy);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRenderPass2(VkCommandBuffer cmd, const VkSubpassEndInfo *info) {
c_end_rp2(cmd, info, 0);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRenderPass2KHR(VkCommandBuffer cmd, const VkSubpassEndInfo *info) {
c_end_rp2(cmd, info, 1);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRenderPass(VkCommandBuffer cmd) {
c_device *d = c_dev(cmd);
VkOffset2D offsets[FP_FDM_MAX_VIEWS];
uint32_t n = c_disabled || !d->CmdEndRenderPass2 ? 0 : c_offsets(cmd, d, 0, NULL, offsets);
if (!n) {
d->CmdEndRenderPass(cmd);
return;
}
c_offset_end mine = {ST_FDM_OFFSET_END, NULL, n, offsets};
VkSubpassEndInfo info = {VK_STRUCTURE_TYPE_SUBPASS_END_INFO, &mine};
d->CmdEndRenderPass2(cmd, &info);
}
static void c_end_rendering2(VkCommandBuffer cmd, const c_rendering_end *info, PFN_fp_EndRendering2 next) {
c_device *d = c_dev(cmd);
const c_offset_end *valve = info ? (const void *)c_chain(info->pNext, ST_FDM_OFFSET_END) : NULL;
VkOffset2D offsets[FP_FDM_MAX_VIEWS];
uint32_t n = c_disabled ? 0 : c_offsets(cmd, d, valve ? valve->count : 0, valve ? valve->offsets : NULL, offsets);
if (!n || (valve && info->pNext != (const void *)valve)) {
next(cmd, info);
return;
}
c_offset_end mine = {ST_FDM_OFFSET_END, valve ? valve->pNext : (info ? info->pNext : NULL), n, offsets};
c_rendering_end copy = {ST_RENDERING_END, &mine};
next(cmd, &copy);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRendering2EXT(VkCommandBuffer cmd, const void *info) {
c_end_rendering2(cmd, info, c_dev(cmd)->CmdEndRendering2EXT);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRendering2KHR(VkCommandBuffer cmd, const void *info) {
c_end_rendering2(cmd, info, c_dev(cmd)->CmdEndRendering2KHR);
}
static void c_end_rendering(VkCommandBuffer cmd, PFN_vkCmdEndRendering plain) {
c_device *d = c_dev(cmd);
PFN_fp_EndRendering2 next = d->CmdEndRendering2EXT ? d->CmdEndRendering2EXT : d->CmdEndRendering2KHR;
VkOffset2D offsets[FP_FDM_MAX_VIEWS];
uint32_t n = c_disabled || !next ? 0 : c_offsets(cmd, d, 0, NULL, offsets);
if (!n) {
plain(cmd);
return;
}
c_offset_end mine = {ST_FDM_OFFSET_END, NULL, n, offsets};
c_rendering_end info = {ST_RENDERING_END, &mine};
next(cmd, &info);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRendering(VkCommandBuffer cmd) {
c_end_rendering(cmd, c_dev(cmd)->CmdEndRendering);
}
static VKAPI_ATTR void VKAPI_CALL c_CmdEndRenderingKHR(VkCommandBuffer cmd) {
c_end_rendering(cmd, c_dev(cmd)->CmdEndRenderingKHR);
}
// ---------------------------------------------------------------- layer plumbing
static PFN_vkGetInstanceProcAddr c_instance_gipa(const void *handle, VkInstance *instance) {
void *key = key_of(handle);
PFN_vkGetInstanceProcAddr found = NULL, last = NULL;
take_lock(&c_dev_lock);
for (int i = 0; i < MAX_HANDLES; i++) {
if (!c_instances[i].gipa) continue;
last = c_instances[i].gipa;
if (instance) *instance = c_instances[i].instance;
if (c_instances[i].key == key) { found = c_instances[i].gipa; break; }
}
drop_lock(&c_dev_lock);
return found ? found : last;
}
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateInstance(const VkInstanceCreateInfo *ci, const VkAllocationCallbacks *a,
VkInstance *instance) {
VkLayerInstanceCreateInfo *link = (VkLayerInstanceCreateInfo *)ci->pNext;
while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO))
link = (VkLayerInstanceCreateInfo *)link->pNext;
if (!link || !link->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED;
PFN_vkGetInstanceProcAddr next = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
PFN_vkCreateInstance create = (PFN_vkCreateInstance)next(VK_NULL_HANDLE, "vkCreateInstance");
if (!create) return VK_ERROR_INITIALIZATION_FAILED;
VkResult r = create(ci, a, instance);
if (r != VK_SUCCESS) return r;
take_lock(&c_dev_lock);
int slot = 0;
for (int i = 0; i < MAX_HANDLES; i++)
if (!c_instances[i].gipa || c_instances[i].key == key_of(*instance)) { slot = i; break; }
c_instances[slot].key = key_of(*instance);
c_instances[slot].instance = *instance;
c_instances[slot].gipa = next;
drop_lock(&c_dev_lock);
return r;
}
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateDevice(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci,
const VkAllocationCallbacks *a, VkDevice *device) {
VkLayerDeviceCreateInfo *link = (VkLayerDeviceCreateInfo *)ci->pNext;
while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO))
link = (VkLayerDeviceCreateInfo *)link->pNext;
if (!link || !link->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED;
PFN_vkGetInstanceProcAddr next_gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
PFN_vkGetDeviceProcAddr next_gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr;
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
VkInstance instance = VK_NULL_HANDLE;
c_instance_gipa(pd, &instance);
PFN_vkCreateDevice create = (PFN_vkCreateDevice)next_gipa(instance, "vkCreateDevice");
if (!create) return VK_ERROR_INITIALIZATION_FAILED;
VkResult r = create(pd, ci, a, device);
if (r != VK_SUCCESS) return r;
c_device d;
memset(&d, 0, sizeof(d));
d.key = key_of(*device), d.device = *device, d.gdpa = next_gdpa, d.gran = (VkExtent2D){1, 1};
#define C_GET(field, name) d.field = (__typeof__(d.field))next_gdpa(*device, name);
C_GET(CreateImage, "vkCreateImage") C_GET(DestroyImage, "vkDestroyImage")
C_GET(CreateImageView, "vkCreateImageView") C_GET(DestroyImageView, "vkDestroyImageView")
C_GET(CreateRenderPass, "vkCreateRenderPass") C_GET(CreateRenderPass2, "vkCreateRenderPass2")
C_GET(CreateRenderPass2KHR, "vkCreateRenderPass2KHR") C_GET(DestroyRenderPass, "vkDestroyRenderPass")
C_GET(CreateFramebuffer, "vkCreateFramebuffer") C_GET(DestroyFramebuffer, "vkDestroyFramebuffer")
C_GET(CmdBeginRenderPass, "vkCmdBeginRenderPass") C_GET(CmdBeginRenderPass2, "vkCmdBeginRenderPass2")
C_GET(CmdBeginRenderPass2KHR, "vkCmdBeginRenderPass2KHR") C_GET(CmdEndRenderPass, "vkCmdEndRenderPass")
C_GET(CmdEndRenderPass2, "vkCmdEndRenderPass2") C_GET(CmdEndRenderPass2KHR, "vkCmdEndRenderPass2KHR")
C_GET(CmdBeginRendering, "vkCmdBeginRendering") C_GET(CmdBeginRenderingKHR, "vkCmdBeginRenderingKHR")
C_GET(CmdEndRendering, "vkCmdEndRendering") C_GET(CmdEndRenderingKHR, "vkCmdEndRenderingKHR")
C_GET(CmdEndRendering2EXT, "vkCmdEndRendering2EXT") C_GET(CmdEndRendering2KHR, "vkCmdEndRendering2KHR")
#undef C_GET
PFN_vkGetPhysicalDeviceProperties2 props2 =
(PFN_vkGetPhysicalDeviceProperties2)next_gipa(instance, "vkGetPhysicalDeviceProperties2");
struct { VkStructureType sType; void *pNext; VkExtent2D gran; } offset_props = {ST_FDM_OFFSET_PROPS, NULL, {0, 0}};
VkPhysicalDeviceProperties2 p2;
memset(&p2, 0, sizeof(p2));
p2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, p2.pNext = &offset_props;
if (props2) props2(pd, &p2);
if (offset_props.gran.width && offset_props.gran.height) d.gran = offset_props.gran;
take_lock(&c_dev_lock);
int slot = 0;
for (int i = 0; i < MAX_HANDLES; i++)
if (!c_devices[i].gdpa || c_devices[i].key == key_of(*device)) { slot = i; break; }
c_devices[slot] = d;
drop_lock(&c_dev_lock);
static int logged;
if (logged++ < 4)
LOG("fdm_centre: active on device %p (preset %d, offsets %s, granularity %ux%u)", (void *)*device, c_level,
c_offsets_off ? "off: Fixed" : "gaze", d.gran.width, d.gran.height);
return r;
}
static PFN_vkVoidFunction c_own(const char *name, const c_device *d);
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL c_GetDeviceProcAddr(VkDevice device, const char *name) {
if (name && !strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)c_GetDeviceProcAddr;
c_device *d = c_dev(device);
PFN_vkVoidFunction fn = c_own(name, d);
if (fn) return fn;
return d ? d->gdpa(device, name) : NULL;
}
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL c_GetInstanceProcAddr(VkInstance instance, const char *name) {
if (name && !strcmp(name, "vkCreateInstance")) return (PFN_vkVoidFunction)c_CreateInstance;
if (name && !strcmp(name, "vkCreateDevice")) return (PFN_vkVoidFunction)c_CreateDevice;
if (name && !strcmp(name, "vkGetInstanceProcAddr")) return (PFN_vkVoidFunction)c_GetInstanceProcAddr;
if (name && !strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)c_GetDeviceProcAddr;
if (name && !strncmp(name, "vkEnumerate", 11)) {
PFN_vkVoidFunction fn = own(name); // the library's layer enumeration
if (fn) return fn;
}
PFN_vkVoidFunction fn = c_own(name, NULL);
if (fn) return fn;
PFN_vkGetInstanceProcAddr next = instance ? c_instance_gipa(instance, NULL) : NULL;
return next ? next(instance, name) : NULL;
}
// Device functions are only ours where the next layer has them (a NULL stays NULL for the app).
static PFN_vkVoidFunction c_own(const char *name, const c_device *d) {
if (!name) return NULL;
#define C_OWN(fn, impl, field) if (!strcmp(name, fn)) return (!d || d->field) ? (PFN_vkVoidFunction)impl : NULL;
C_OWN("vkCreateImage", c_CreateImage, CreateImage) C_OWN("vkDestroyImage", c_DestroyImage, DestroyImage)
C_OWN("vkCreateImageView", c_CreateImageView, CreateImageView)
C_OWN("vkDestroyImageView", c_DestroyImageView, DestroyImageView)
C_OWN("vkCreateRenderPass", c_CreateRenderPass, CreateRenderPass)
C_OWN("vkCreateRenderPass2", c_CreateRenderPass2, CreateRenderPass2)
C_OWN("vkCreateRenderPass2KHR", c_CreateRenderPass2KHR, CreateRenderPass2KHR)
C_OWN("vkDestroyRenderPass", c_DestroyRenderPass, DestroyRenderPass)
C_OWN("vkCreateFramebuffer", c_CreateFramebuffer, CreateFramebuffer)
C_OWN("vkDestroyFramebuffer", c_DestroyFramebuffer, DestroyFramebuffer)
C_OWN("vkCmdBeginRenderPass", c_CmdBeginRenderPass, CmdBeginRenderPass)
C_OWN("vkCmdBeginRenderPass2", c_CmdBeginRenderPass2, CmdBeginRenderPass2)
C_OWN("vkCmdBeginRenderPass2KHR", c_CmdBeginRenderPass2KHR, CmdBeginRenderPass2KHR)
C_OWN("vkCmdEndRenderPass", c_CmdEndRenderPass, CmdEndRenderPass)
C_OWN("vkCmdEndRenderPass2", c_CmdEndRenderPass2, CmdEndRenderPass2)
C_OWN("vkCmdEndRenderPass2KHR", c_CmdEndRenderPass2KHR, CmdEndRenderPass2KHR)
C_OWN("vkCmdBeginRendering", c_CmdBeginRendering, CmdBeginRendering)
C_OWN("vkCmdBeginRenderingKHR", c_CmdBeginRenderingKHR, CmdBeginRenderingKHR)
C_OWN("vkCmdEndRendering", c_CmdEndRendering, CmdEndRendering)
C_OWN("vkCmdEndRenderingKHR", c_CmdEndRenderingKHR, CmdEndRenderingKHR)
C_OWN("vkCmdEndRendering2EXT", c_CmdEndRendering2EXT, CmdEndRendering2EXT)
C_OWN("vkCmdEndRendering2KHR", c_CmdEndRendering2KHR, CmdEndRendering2KHR)
#undef C_OWN
return NULL;
}
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL VK_LAYER_FRAMEPORT_fdm_centreGetInstanceProcAddr(VkInstance i, const char *n) {
return c_GetInstanceProcAddr(i, n);
}
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL VK_LAYER_FRAMEPORT_fdm_centreGetDeviceProcAddr(VkDevice d, const char *n) {
return c_GetDeviceProcAddr(d, n);
}
// Valve's preset and offset choice, read the way its layer reads them (FOVE_LEVEL wins over FDM_DEBUG lo/med/hi).
static int c_has_token(const char *list, const char *token) {
size_t n = strlen(token);
for (const char *p = list; p && *p;) {
while (*p && !((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) ++p;
const char *start = p;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_') ++p;
if ((size_t)(p - start) == n && !strncmp(start, token, n)) return 1;
}
return 0;
}
static void c_read_config(void) {
const char *flags = getenv("FDM_DEBUG"), *level = getenv("FOVE_LEVEL"), *off = getenv("FRAMEPORT_FDM_CENTRE");
c_offsets_off = c_has_token(flags, "disable_offsets");
c_level = c_has_token(flags, "hi") ? 3 : c_has_token(flags, "med") ? 2 : 1;
int explicit = c_has_token(level, "hi") ? 3 : c_has_token(level, "med") ? 2 : c_has_token(level, "lo") ? 1 : 0;
if (explicit) c_level = explicit;
c_disabled = off && !strcmp(off, "0");
}
+44 -14
View File
@@ -7,6 +7,7 @@ extern int __android_log_print(int,const char*,const char*,...);
#define ANDROID_LOG_INFO 4
#include <dlfcn.h>
#include <errno.h>
#include <math.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
@@ -142,20 +143,24 @@ static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_GetInstanceProcAddr(VkInst
// ---------------------------------------------------------------- layer enumeration (what the loader asks for)
static const VkLayerProperties properties = {LAYER_NAME, VK_MAKE_VERSION(1, 1, 0), 1, "FramePort coarse-fragment darkening"};
// Two layers in one library: the darkening diagnostic (above Valve's layer) and the density-map centring (below it,
// centre.h); the loader finds each one's entry points by its name.
static const VkLayerProperties properties[2] = {
{LAYER_NAME, VK_MAKE_VERSION(1, 1, 0), 1, "FramePort coarse-fragment darkening"},
{"VK_LAYER_FRAMEPORT_fdm_centre", VK_MAKE_VERSION(1, 1, 0), 1, "FramePort foveation centring"}};
EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceLayerProperties(uint32_t *count, VkLayerProperties *props) {
if (!props) { *count = 1; return VK_SUCCESS; }
if (*count < 1) return VK_INCOMPLETE;
props[0] = properties;
*count = 1;
return VK_SUCCESS;
if (!props) { *count = 2; return VK_SUCCESS; }
uint32_t n = *count < 2 ? *count : 2;
for (uint32_t i = 0; i < n; ++i) props[i] = properties[i];
*count = n;
return n < 2 ? VK_INCOMPLETE : VK_SUCCESS;
}
EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceExtensionProperties(const char *layer, uint32_t *count,
VkExtensionProperties *props) {
(void)props;
if (!layer || strcmp(layer, LAYER_NAME)) return VK_ERROR_LAYER_NOT_PRESENT;
if (!layer || (strcmp(layer, LAYER_NAME) && strcmp(layer, "VK_LAYER_FRAMEPORT_fdm_centre"))) return VK_ERROR_LAYER_NOT_PRESENT;
*count = 0;
return VK_SUCCESS;
}
@@ -180,6 +185,8 @@ EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL VK_LAYER_FRAMEPORT_fdm_darkenGet
return layer_GetDeviceProcAddr(d, n);
}
#include "centre.h"
static PFN_vkVoidFunction own(const char *name) {
if (!name) return NULL;
#define OWN(fn, impl) if (!strcmp(name, fn)) return (PFN_vkVoidFunction)impl;
@@ -220,9 +227,16 @@ static const char *cxx_chars(const void *s, size_t *len) {
__attribute__((constructor)) static void activate(void) {
climb_stereo_fix();
const char *enabled = getenv("FRAMEPORT_FOVEATION_TEST");
if (!enabled || strcmp(enabled, "1")) return;
/* This library is added only when device.foveation.test is selected. */
/* The launcher opt-in also keeps imported test builds dormant during normal play. */
int darken = enabled && (!strcmp(enabled, "1") || !strcmp(enabled, "2"));
fdm_paint = enabled && !strcmp(enabled, "2"); // 2 (developers): paint 1/fragment area instead of dimming
/* The darkening library is added only when device.foveation.test is selected (and dormant in every build):
* the launcher opt-in keeps imported test builds dormant during normal play. The centring layer follows
* FramePort's foveation modes: Valve's injection enabled (FDM_DEBUG=enable). */
c_read_config();
// The Climb 2 corrects Valve's offsets in its private bridge (climb_compat.h); leave it alone.
const char *climb = getenv("FRAMEPORT_CLIMB_STEREO_OFFSETS");
int centre = c_has_token(getenv("FDM_DEBUG"), "enable") && !c_disabled && !(climb && !strcmp(climb, "1"));
if (!darken && !centre) return;
void *vk = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
void *(*get_instance)(void) = vk ? (void *(*)(void))dlsym(vk, GET_INSTANCE) : NULL;
const void *(*get_layers)(void *) = vk ? (const void *(*)(void *))dlsym(vk, GET_LAYERS) : NULL;
@@ -242,17 +256,33 @@ __attribute__((constructor)) static void activate(void) {
char old[4097];
memcpy(old, current ? current : "", len);
old[len] = 0;
if (strstr(old, LAYER_NAME)) return; // loaded twice (the loader's own dlopen): already in the list
size_t n = strlen(LAYER_NAME) + (len ? 1 + len : 0);
// loaded twice (the loader's own dlopen): already in the list
if ((!darken || strstr(old, LAYER_NAME)) && (!centre || strstr(old, CENTRE_NAME))) return;
char list[4097 + 128];
snprintf(list, sizeof(list), "%s%s%s", darken && !strstr(old, LAYER_NAME) ? LAYER_NAME : "",
darken && !strstr(old, LAYER_NAME) && len ? ":" : "", old);
const char *valve = strstr(list, "VK_LAYER_VALVE_fdm_injection");
if (centre && !strstr(list, CENTRE_NAME)) {
if (!valve) {
centre = 0; // Valve's layer isn't loaded here: nothing to centre
} else { // right below Valve's layer: it sees the density map and offsets Valve hands down
size_t at = (size_t)(valve - list) + strlen("VK_LAYER_VALVE_fdm_injection");
char tail[sizeof(list)];
snprintf(tail, sizeof(tail), "%s", list + at);
snprintf(list + at, sizeof(list) - at, ":%s%s", CENTRE_NAME, tail);
}
}
if (!strcmp(list, old)) return;
size_t n = strlen(list);
size_t cap = (n + 16) & ~(size_t)15;
char *data = malloc(cap);
if (!data) return;
snprintf(data, cap, "%s%s%s", LAYER_NAME, len ? ":" : "", old);
memcpy(data, list, n + 1);
// passed by value: the callee gets a pointer to our temporary and copies it; freed below unless it was moved
struct cxx_string tmp = {cap | 1, n, data};
set_layers(env, &tmp);
if ((tmp.cap & 1) && tmp.data == data) free(data);
const char *now = cxx_chars(get_layers(env), &len);
LOG("foveation diagnostic: debug layers now \"%.*s\" (%d fix(es)%s)", (int)len, now ? now : "", nfixes,
LOG("foveation: debug layers now \"%.*s\" (%d fix(es)%s)", (int)len, now ? now : "", nfixes,
dump ? ", dumping modules" : "");
}
+11 -4
View File
@@ -2,8 +2,9 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* Diagnostic only: dim location-0 vec4 RGB where FragSizeEXT != (1,1).
/* Diagnostic only: dim location-0 vec4 RGB where FragSizeEXT != (1,1) (or paint it 1/area, fdm_paint).
* Unsupported interfaces pass through; alpha and all other outputs are retained. */
static int fdm_paint; /* FRAMEPORT_FOVEATION_TEST=2: RGB = 1/fragment area (white = full density), alpha kept */
static uint32_t *fdm_darken(const uint32_t *w,size_t n,size_t *out_n) {
if(n<5||n>4000000||w[0]!=0x07230203||!w[3]||w[3]>1000000)return NULL;
uint32_t bound=w[3],output=0,ptr=0,vec=0,flt=0; int entries=0,stores=0;
@@ -44,7 +45,7 @@ static uint32_t *fdm_darken(const uint32_t *w,size_t n,size_t *out_n) {
int nui=!ui,nbv=!bv,nv2=!v2,nip=!ip;
if(nui)ui=bound++;if(nbv)bv=bound++;if(nv2)v2=bound++;if(nip)ip=bound++;
uint32_t frag=bound++,one=bound++,bright=bound++,dark=bound++;
uint32_t *out=calloc(n+200+stores*64,sizeof(uint32_t));if(!out)return NULL;size_t k=0;
uint32_t *out=calloc(n+200+stores*72,sizeof(uint32_t));if(!out)return NULL;size_t k=0;
#define EMIT(...) do{const uint32_t a[]={__VA_ARGS__};memcpy(out+k,a,sizeof a);k+=sizeof a/4;}while(0)
memcpy(out,w,20);k=5;if(!ext_at)ext_at=cap_at;
for(size_t i=5;i<=n;) {
@@ -63,10 +64,16 @@ static uint32_t *fdm_darken(const uint32_t *w,size_t n,size_t *out_n) {
else if(op==62&&w[i+1]==output){
uint32_t sz=bound++,x=bound++,y=bound++,area=bound++,low=bound++,factor=bound++,factors=bound++,color=bound++;
EMIT((4u<<16)|61,v2,sz,frag);EMIT((5u<<16)|81,ui,x,sz,0);EMIT((5u<<16)|81,ui,y,sz,1);
EMIT((5u<<16)|132,ui,area,x,y);EMIT((5u<<16)|171,bv,low,area,one);
EMIT((5u<<16)|132,ui,area,x,y);
if(fdm_paint){uint32_t fa=bound++;
EMIT((4u<<16)|112,flt,fa,area);EMIT((5u<<16)|136,flt,factor,bright,fa);
EMIT((7u<<16)|80,vec,factors,factor,factor,factor,bright);
EMIT((9u<<16)|79,vec,color,factors,w[i+2],0,1,2,7);
}else{
EMIT((5u<<16)|171,bv,low,area,one);
EMIT((6u<<16)|169,flt,factor,low,dark,bright);
EMIT((7u<<16)|80,vec,factors,factor,factor,factor,bright);
EMIT((5u<<16)|133,vec,color,w[i+2],factors);
EMIT((5u<<16)|133,vec,color,w[i+2],factors);}
memcpy(out+k,w+i,len*4);out[k+2]=color;k+=len;
}else{memcpy(out+k,w+i,len*4);k+=len;}
i+=len;
+1 -1
View File
@@ -577,7 +577,7 @@
"Every 5 s, logs per located space how far the requested times are from the frame's predicted display time, and the runtime clock's offset from the monotonic clock. No effect on the game.": "",
"Every detail of the scan, in your browser.": "",
"Every key goes through": "",
"Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. OpenGL ES games only.": "",
"Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. Vulkan games: both eyes at half size (fb_snap_vk_N_vV.ppm), the whole swapchain image with the submitted rect and FOV in the log.": "",
"Every Scan a folder… adds its folder here. Rescan folders looks in them for new games, and for games you moved: a game whose files are gone from their old place is pointed at the same copy in its new place. Another version found there is only taken by Scan a folder…; Rescan leaves it for you to choose (Update with a newer version… on the game's page). Removing a folder only stops it being scanned: its games stay in the library.": "",
"Every shot you take in the headset": "",
"Every uploaded file is present with the right size.": "",
+2 -1
View File
@@ -25,7 +25,8 @@ SETTINGS = [
("snapshot", "int", 0, "Eye-image snapshots (diagnostics)",
"Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the "
"game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays "
"black; this shows what the game itself draws. OpenGL ES games only."),
"black; this shows what the game itself draws. Vulkan games: both eyes at half size (fb_snap_vk_N_vV.ppm), "
"the whole swapchain image with the submitted rect and FOV in the log."),
("gl_error_clear", "int", 0, "Clear the game's GL errors (GLES)",
"Clears OpenGL errors the game left pending before FrameBridge calls the Frame's runtime. The runtime's GL code "
"fails on any pending error; after one such failure at the start it allocated GPU memory every frame without "
+198
View File
@@ -0,0 +1,198 @@
// Host harness for native/foveation/centre.h (GitHub #394): a fake next layer records the density-map offsets the
// centring layer hands down for the render passes Valve's foveation layer would record.
#define _POSIX_C_SOURCE 200809L
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vulkan/vulkan.h>
#include <vulkan/vk_layer.h>
#define CHECK(c) \
do { \
if (!(c)) { \
fprintf(stderr, "line %d: %s\n", __LINE__, #c); \
return 1; \
} \
} while (0)
#define LOG(...) ((void)0)
#define EXPORT
#define MAX_HANDLES 16
static void take_lock(int *p) { while (__atomic_exchange_n(p, 1, __ATOMIC_ACQUIRE)) {} }
static void drop_lock(int *p) { __atomic_store_n(p, 0, __ATOMIC_RELEASE); }
static void *key_of(const void *handle) { return handle ? *(void *const *)handle : NULL; }
static PFN_vkVoidFunction own(const char *name) { (void)name; return NULL; }
#include "centre.h"
// ---- fake next layer
static void *dispatch_key = (void *)0x1234;
static struct { void *key; } device_obj = {(void *)0x1234}, cmd_obj = {(void *)0x1234};
static uintptr_t next_handle = 0x1000;
static VkImageCreateFlags last_image_flags;
static int ends;
static uint32_t got_count;
static VkOffset2D got[4];
static VKAPI_ATTR VkResult VKAPI_CALL n_CreateImage(VkDevice d, const VkImageCreateInfo *ci, const VkAllocationCallbacks *a,
VkImage *img) {
(void)d, (void)a;
last_image_flags = ci->flags;
*img = (VkImage)(next_handle += 8);
return VK_SUCCESS;
}
static VKAPI_ATTR VkResult VKAPI_CALL n_CreateImageView(VkDevice d, const VkImageViewCreateInfo *ci,
const VkAllocationCallbacks *a, VkImageView *v) {
(void)d, (void)ci, (void)a;
*v = (VkImageView)(next_handle += 8);
return VK_SUCCESS;
}
static VKAPI_ATTR VkResult VKAPI_CALL n_CreateRenderPass2(VkDevice d, const VkRenderPassCreateInfo2 *ci,
const VkAllocationCallbacks *a, VkRenderPass *rp) {
(void)d, (void)ci, (void)a;
*rp = (VkRenderPass)(next_handle += 8);
return VK_SUCCESS;
}
static VKAPI_ATTR VkResult VKAPI_CALL n_CreateFramebuffer(VkDevice d, const VkFramebufferCreateInfo *ci,
const VkAllocationCallbacks *a, VkFramebuffer *fb) {
(void)d, (void)ci, (void)a;
*fb = (VkFramebuffer)(next_handle += 8);
return VK_SUCCESS;
}
static VKAPI_ATTR void VKAPI_CALL n_CmdBeginRenderPass2(VkCommandBuffer c, const VkRenderPassBeginInfo *b,
const VkSubpassBeginInfo *s) {
(void)c, (void)b, (void)s;
}
static void record(const void *next) {
++ends;
got_count = 0;
const c_offset_end *o = (const void *)c_chain(next, ST_FDM_OFFSET_END);
if (o) {
got_count = o->count;
memcpy(got, o->offsets, o->count * sizeof(VkOffset2D));
}
}
static VKAPI_ATTR void VKAPI_CALL n_CmdEndRenderPass2(VkCommandBuffer c, const VkSubpassEndInfo *e) {
(void)c;
record(e ? e->pNext : NULL);
}
static VKAPI_ATTR void VKAPI_CALL n_CmdEndRenderPass(VkCommandBuffer c) {
(void)c;
record(NULL);
}
// One multiview pass into a w x h swapchain with Valve's ceil(w/32) x ceil(h/32) map; ends with `valve` offsets
// (NULL: none, as in Fixed mode).
static void pass(uint32_t w, uint32_t h, const VkOffset2D *valve, uint32_t count) {
VkDevice dev = (VkDevice)&device_obj;
VkCommandBuffer cmd = (VkCommandBuffer)&cmd_obj;
VkImageCreateInfo ici = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
ici.imageType = VK_IMAGE_TYPE_2D;
ici.extent = (VkExtent3D){(w + 31) / 32, (h + 31) / 32, 1};
ici.arrayLayers = 2;
ici.usage = FDM_USAGE;
ici.flags = c_offsets_off ? 0 : FDM_OFFSET_FLAG; // Valve flags maps itself when its offsets are on
VkImage map;
c_CreateImage(dev, &ici, NULL, &map);
VkImageViewCreateInfo vci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
vci.image = map;
VkImageView view;
c_CreateImageView(dev, &vci, NULL, &view);
VkRenderPassFragmentDensityMapCreateInfoEXT fdm = {ST_FDM_RP_INFO, NULL, {1, VK_IMAGE_LAYOUT_GENERAL}};
VkRenderPassCreateInfo2 rci = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2, &fdm};
VkRenderPass rp;
c_CreateRenderPass2(dev, &rci, NULL, &rp);
VkImageView atts[2] = {(VkImageView)0x99, view};
VkFramebufferCreateInfo fci = {VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fci.renderPass = rp, fci.attachmentCount = 2, fci.pAttachments = atts, fci.width = w, fci.height = h, fci.layers = 1;
VkFramebuffer fb;
c_CreateFramebuffer(dev, &fci, NULL, &fb);
VkRenderPassBeginInfo bi = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
bi.renderPass = rp, bi.framebuffer = fb, bi.renderArea.extent = (VkExtent2D){w, h};
c_CmdBeginRenderPass2(cmd, &bi, NULL);
if (valve) {
c_offset_end o = {ST_FDM_OFFSET_END, NULL, count, valve};
VkSubpassEndInfo e = {VK_STRUCTURE_TYPE_SUBPASS_END_INFO, &o};
c_CmdEndRenderPass2(cmd, &e);
} else {
c_CmdEndRenderPass(cmd);
}
}
static int near(int got_value, float want, int tolerance) { return fabsf((float)got_value - want) <= (float)tolerance; }
int main(void) {
(void)dispatch_key;
c_device d;
memset(&d, 0, sizeof(d));
d.key = device_obj.key, d.device = (VkDevice)&device_obj, d.gdpa = (PFN_vkGetDeviceProcAddr)1, d.gran = (VkExtent2D){8, 8};
d.CreateImage = n_CreateImage, d.CreateImageView = n_CreateImageView, d.CreateRenderPass2 = n_CreateRenderPass2;
d.CreateFramebuffer = n_CreateFramebuffer, d.CmdBeginRenderPass2 = n_CmdBeginRenderPass2;
d.CmdEndRenderPass2 = n_CmdEndRenderPass2, d.CmdEndRenderPass = n_CmdEndRenderPass;
c_devices[0] = d;
// Valve's lo centre on a 2016x1728 swapchain (63x54 map, 32 px texels): texels 23-40 and 20-34
CHECK(c_valve_centre(2016, 63, 0) == 1024.0f && c_valve_centre(1728, 54, 1) == 880.0f);
c_level = 3;
CHECK(c_valve_centre(2016, 63, 0) == 1024.0f); // hi: texels 24-39
c_level = 1;
// AC Nexus' asymmetric eyes: rect=0,0 / 287,0 1726x1728 inside 2016x1728, optical centres from the FOV
fp_fdm_view views[2] = {{2016, 1728, 0, 0, 0, 1726, 1728, 1002, 696}, {2016, 1728, 1, 287, 0, 1726, 1728, 1001, 709}};
fp_fdm_set_views(views, 2);
// Fixed (Valve passes no offsets): the layer adds one per eye layer, moving the map to the optical centre
c_offsets_off = 1;
pass(2016, 1728, NULL, 0);
CHECK(last_image_flags & FDM_OFFSET_FLAG); // flagged like Valve does when its offsets are on
CHECK(ends == 1 && got_count == 2);
CHECK(near(got[0].x, 1002 - 1024, 4) && near(got[0].y, 696 - 880, 4));
CHECK(near(got[1].x, 1001 - 1024, 4) && near(got[1].y, 709 - 880, 4));
CHECK(got[0].x % 8 == 0 && got[0].y % 8 == 0);
// Eye-tracked with no gaze (all 0): straight ahead = the optical centre too
c_offsets_off = 0;
VkOffset2D zero[2] = {{0, 0}, {0, 0}};
pass(2016, 1728, zero, 2);
CHECK(got_count == 2 && near(got[0].x, -22, 4) && near(got[1].y, -171, 4));
// Eye-tracked gaze: Valve's offset (gaze NDC * half the framebuffer, from the swapchain centre) is re-based on
// the eye's rect: gaze right by a quarter of the framebuffer = a quarter of the rect from the rect's centre
VkOffset2D gaze[2] = {{504, 0}, {-504, 432}};
pass(2016, 1728, gaze, 2);
CHECK(near(got[0].x, 863 + 504 * 1726.0f / 2016 - 1024, 4) && near(got[0].y, 864 - 880, 4));
CHECK(near(got[1].x, 287 + 863 - 504 * 1726.0f / 2016 - 1024, 4) && near(got[1].y, 864 + 432 - 880, 4));
// A pass of another size (no reported view): left alone
c_offsets_off = 1;
pass(1024, 1024, NULL, 0);
CHECK(got_count == 0);
// Both eyes side by side in one layer: one map can't serve both, left alone
fp_fdm_view wide[2] = {{3456, 1728, 0, 0, 0, 1728, 1728, 1000, 700}, {3456, 1728, 0, 1728, 0, 1728, 1728, 2450, 700}};
fp_fdm_set_views(wide, 2);
pass(3456, 1728, NULL, 0);
CHECK(got_count == 0);
// Off (FRAMEPORT_FDM_CENTRE=0): nothing changes
fp_fdm_set_views(views, 2);
c_disabled = 1;
last_image_flags = 0;
pass(2016, 1728, NULL, 0);
CHECK(got_count == 0 && !(last_image_flags & FDM_OFFSET_FLAG));
// configuration as Valve reads it: FOVE_LEVEL beats FDM_DEBUG's lo/med/hi
setenv("FDM_DEBUG", "enable,disable_offsets,med", 1);
unsetenv("FOVE_LEVEL");
unsetenv("FRAMEPORT_FDM_CENTRE");
c_read_config();
CHECK(c_offsets_off && c_level == 2 && !c_disabled);
setenv("FOVE_LEVEL", "hi", 1);
setenv("FDM_DEBUG", "enable", 1);
setenv("FRAMEPORT_FDM_CENTRE", "0", 1);
c_read_config();
CHECK(!c_offsets_off && c_level == 3 && c_disabled);
CHECK(c_has_token("enable,disable_offsets", "disable_offsets") && !c_has_token("enable", "disable_offsets"));
CHECK(c_has_token("enable,hi", "hi") && !c_has_token("enable,high", "hi"));
puts("ok");
return 0;
}
+44
View File
@@ -0,0 +1,44 @@
"""Host harness for the foveation centring layer (native/foveation/centre.h, GitHub #394)."""
import os
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
CC = shutil.which("cc") or shutil.which("clang")
def vulkan_headers():
candidates = [Path("/usr/include"), *(ROOT / "native/.cache").glob(
"ndk-*/toolchains/llvm/prebuilt/*/sysroot/usr/include")]
return next((p for p in candidates if (p / "vulkan/vulkan_core.h").is_file()
and (p / "vulkan/vk_layer.h").is_file()), None)
@pytest.mark.native
@pytest.mark.skipif(not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not CC,
reason="set FRAMEPORT_NATIVE_TESTS=1; needs a host C compiler and Vulkan headers")
def test_density_map_offsets_follow_the_eyes(tmp_path):
headers = vulkan_headers()
if headers is None:
pytest.skip("needs Vulkan headers (host development package or cached NDK)")
inc = tmp_path / "include"
shutil.copytree(headers / "vulkan", inc / "vulkan") # only these: the NDK sysroot shadows host libc headers
if (headers / "vk_video").is_dir():
shutil.copytree(headers / "vk_video", inc / "vk_video")
exe = tmp_path / "centre"
subprocess.run([CC, "-std=c11", "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers",
"-I", str(inc), "-I", str(ROOT / "native/foveation"),
str(ROOT / "tests/native/fdm_centre_harness.c"), "-lm", "-o", str(exe)], check=True)
out = subprocess.run([str(exe)], capture_output=True, text=True)
assert out.returncode == 0, out.stderr
assert out.stdout.strip() == "ok"
def test_fixed_and_tracked_launchers_keep_valve_flags():
# The centring layer reads Valve's own settings; FramePort's launcher exports stay as they were.
src = (ROOT / "native/foveation/darken.c").read_text()
assert '"VK_LAYER_VALVE_fdm_injection"' in src and "CENTRE_NAME" in src
assert "FRAMEPORT_CLIMB_STEREO_OFFSETS" in src # The Climb 2's private correction stays alone