mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-26 08:02:26 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
186d4f6290 | ||
|
|
fc8a28b3cd | ||
|
|
1a4e98709c | ||
|
|
4799a2d048 | ||
|
|
43383d62c4 | ||
|
|
c8a4da90e2 | ||
|
|
d54d35e43d |
No files matched your search
@@ -638,7 +638,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
|
||||
**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, centring the full-density *bins* (Turnip's bin size recomputed in `tu_bins.h`, Adreno 750 / Mesa 26.3; per-game override `fdm_bin_w`/`fdm_bin_h`; `FRAMEPORT_FDM_DIAG=1` logs it); FrameBridge `snapshot=N` reads Vulkan eyes back. → docs/DEVICE_NOTES.md#foveation-sharp-area-centre-and-size-394
|
||||
**Expansion file names (#450/#481):** OBBs with a download's copy suffix (`main.203.com.Armature.VR4-003.obb`) or another case are installed under the game's name (`quest_dump.obb_names`/`frame_manifest`, PC files untouched), other kinds too (#415: Crisis Brigade 2's `office.222.<pkg>.obb`, kind spelling kept, never an "another app" warning); another app's name (#129), duplicates and unclear cases stay warnings. → docs/DEVICE_NOTES.md#expansion-file-names-450-481
|
||||
**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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
dfeee15b52a530aef0fc90f79140249522adf888d1c88fd57935dd8ce5257fa5 ./arm64-v8a/libVkLayer_fdm_darken.so
|
||||
8c0393f9d913f835575406f2bb39e7017d07518b1563e3d4028077047e04bf63 ./arm64-v8a/libVkLayer_fdm_darken.so
|
||||
86974202436e9209e260a690ae3eb0082d7d0f88e17c621dd77c0788ea66bc6d ./arm64-v8a/libVkLayer_fp_shaderfix.so
|
||||
e63d93e086df3b7b29501188b80fb113879e2fd325d7659f68a31ac5bc87b561 ./arm64-v8a/liba32bridge.so
|
||||
81b073b0cdf11fdb09d22dff682884c5046fe13801f18db9aed232e86f3bf173 ./arm64-v8a/liba32rt.so
|
||||
ed529aac0e36806653e8dda1648007031776db3734bb0832cf39db068b551a90 ./arm64-v8a/liba32bridge.so
|
||||
0ee6a31c05c4348d4db9c4f8419ba80e165239c50283022d8b3d77d6f4b89d06 ./arm64-v8a/liba32rt.so
|
||||
18d8d917e83a6d87008f41908eb478dff635e798844e125abd1e8a5cf31805e2 ./arm64-v8a/liba32stub.so
|
||||
845905e8c2da617dddcb9625f4ed38e06f1d85f9c729b55dacb2c59488074c3e ./arm64-v8a/libfp_langpack.so
|
||||
69102b128e00fa1809489a7308e09c1505f563bf7091bbdd3fe5a58e4cc96c2c ./arm64-v8a/libfp_luajit.so
|
||||
@@ -14,11 +14,11 @@ e63d93e086df3b7b29501188b80fb113879e2fd325d7659f68a31ac5bc87b561 ./arm64-v8a/li
|
||||
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
|
||||
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
be863bb0001bc60e9bbd691753f5c5968664ae7917b29258dd15b7a9dd1c3ade ./arm64-v8a/libopenxr_loader_generic.so
|
||||
49ce3a091535744f0ec96768204da1d0c4f52caae7dae37dfe35fbb3b9bac09f ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
c7a14d34e4ddf20ec943641f6431525867d6f030cd01160df1b24afd441af39f ./arm64-v8a/libvrapi.so
|
||||
c7a14d34e4ddf20ec943641f6431525867d6f030cd01160df1b24afd441af39f ./arm64-v8a/libvrapi_foveation.so
|
||||
2f978a24bc78a33a4162f5d35ec13d722a5a965a67772bd6128cbd6f8b0b4b17 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
2465c28ee5905b847bd06d9e9c1f5e8351722ad525afc7a6c9967b24611a33c1 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
6fd59e6c6e533c0a17204e4f12adaf992ffa6cf58d6280aa9b37ca7ba20299d4 ./dex/browser-handoff.dex
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
package: com.sumalab.CrisisBrigade2reloaded
|
||||
title: Crisis Brigade 2 reloaded
|
||||
status: works
|
||||
notes: 'Its office.*.obb expansion files are installed under the game''s own package spelling from FramePort
|
||||
1.4.0 on (GitHub #415); older versions left the levels unloaded.'
|
||||
tested_version: '2.22'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.bhaptics_permission
|
||||
- frame.photon_voice_audio_route
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-25'
|
||||
known_good_sha256: 51ab71cc96cd10548275897c581054db12aa91bb1579c1798f6971a484f1f525
|
||||
app: 1.3.1.dev703
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261020.6124238'
|
||||
agent: 125
|
||||
issue: 499
|
||||
source_hint: crisis brigade
|
||||
min_app: 1.4.0
|
||||
@@ -70,6 +70,7 @@ two videos, then the island menu from +46 s). Still seen: `VideoPlayback audio b
|
||||
discarded` during the intros (the videos' sound may crackle; unchecked in the headset). The game hides everything
|
||||
behind "Return to play area" when the head is outside `PlayAreaManager`'s height limits (`PlayAreaSettings.heightData`
|
||||
min/max above the floor from GetTrackingTransform(3)).
|
||||
**Ghost Giant sound (GitHub #439, headless 2026-10-25):** distorted all the time. FMOD keeps two 10 ms OpenSL ES buffers; the bridge's player thread wrote each to AAudio with a blocking write into a 40 ms buffer, and the Frame's AudioFlinger takes 960 frames every 20 ms, so two buffers were read back to back per burst (callback intervals 0 / 20 ms). Enqueue to read was ~20 ms either way (the "read microseconds after enqueue" theory did not hold); the PCM the game handed over (`A32_AUDIO_DUMP`) had sample jumps at every 480-frame boundary 3-15x those inside a buffer, and Unity's video player logged 212 `VideoPlayback audio buffer overflow` lines. With reads paced on the clock (`host/audio_pacing.h`; intervals 9.5-10.5 ms) the boundary jumps equal the in-buffer ones and the overflows are gone (0), 72 fps, no underruns. Lesson: the Frame's legacy AAudio track (capacity 3848 frames) starts only once its buffer size is full: a paced stream with a 150 ms buffer never started, so pacing begins at the device's first read and the buffer grows only then. Not yet heard in the headset.
|
||||
**Headless snapshots of VrApi games** (`device.lepton_env` / launch.sh `LEPTON_ENV_OVP_HEADLESS_TEST=1` + adapter
|
||||
`snapshot=N`): without a worn headset the session stays SYNCHRONIZED (`shouldRender` false), the VrApi bridge ended
|
||||
every frame empty (`pacing 2s: … ends=145 (empty 145 …)`) and the a32 bridge dropped every projection with an
|
||||
@@ -1175,6 +1176,47 @@ black, so headset checks of the picture should be done with the test off. Headle
|
||||
straight-ahead gaze lands on the optical centre, which confirms the NDC re-basing). Not done: Khronos validation, a
|
||||
GLES game, a real gaze.
|
||||
|
||||
**Residual offset: the bin grid in map space (2026-10-25, In Death).** Report: with the centring layer the sharp
|
||||
area sits slightly towards the temple in In Death and The 7th Guest (mirrored optical centres), while Asgard's Wrath 2
|
||||
is fine. Suspected Turnip's shared-viewport fallback (a pipeline without the density-map flag, or one writing
|
||||
ViewportIndex, sets `rp.shared_viewport` for the pass: the bins stop sliding with the offsets and every view gets the
|
||||
finest area of all views, i.e. the union of both eyes' sharp bins). `FRAMEPORT_FDM_DIAG=1` (as `LEPTON_ENV_`) makes the
|
||||
centring layer log each pass's offsets as handed down and every pipeline/bind that would trigger it. Headless, In
|
||||
Death 1x Fixed Large, darkening test, `snapshot=15`: swapchain 2768x2768, rect 0,0 1730x1730, optical centres
|
||||
1004,697 / 715,709, map 87x87; 6000+ passes, **0 shared** (no pipeline without the map or writing ViewportIndex), and
|
||||
each eye's sharp area is its own 768x672 block, not a union. So that fallback isn't it. What is: Turnip samples the map
|
||||
at bin centres on a grid **anchored at 0 in map space** (bins slide with the offset: screen bin centre s samples
|
||||
s - offset), so the sharp area is the bins (here 192x224 px) whose map-space centre falls in Valve's sharp texels
|
||||
1024-1824: map 960-1728 x 1120-1792, centre 1344,1456 instead of the texel centre 1424,1424 the layer assumes. Shifted
|
||||
by the offsets that gives exactly the measured areas (left eye x 544-1312 y 392-1064, right eye x 248-1016 y 408-1080):
|
||||
both eyes -80,+32 px (-4.6%, +1.9% of the rect) off their optical centres, the same direction in both eyes. The error
|
||||
depends on the bin size (Turnip's tile0: GMEM, attachments, views) and is up to half a bin in each axis; AC Nexus's
|
||||
1-2% residual above is the same effect. Vulkan doesn't report Turnip's bin size (no VK_QCOM_tile_properties).
|
||||
|
||||
**Fix: the layer computes the bins (2026-10-25).** `native/foveation/tu_bins.h` follows Mesa: the driver on the Frame
|
||||
is Mesa 26.3.0-devel `git-e07916b45f` (Valve's build, not a public commit; the layer logs driver name + info at
|
||||
device creation), so the code follows upstream main of the 26.3 window (82b868960e), where `tu_util.cc`
|
||||
(`tu_tiling_config_update_tile_layout`), `tu_pass.cc` (`tu_render_pass_gmem_config`) and the LRZ limit
|
||||
(`fdl6_lrz_get_max_fdm_extra_size`) didn't change in substance. Passes with a density map use
|
||||
`TU_GMEM_LAYOUT_AVOID_CCU` (`tu_choose_gmem_layout`): Adreno 750 = 3 MiB GMEM - 6 x 0xc000 VPC attribute buffer - 6 x
|
||||
64 KiB / 8 colour CCU = 2801664 bytes for attachments; tile alignment 96x32, max 2016x2032. In Death's eye pass (4x
|
||||
RGBA8 + 4x D24S8 in GMEM, a 1x resolve target and the map outside) -> gmem_pixels 86016 -> 192x224 bins, as measured.
|
||||
The layer tracks render-target images/views and models render passes (v1/v2) and dynamic rendering; anything it
|
||||
doesn't model (another GPU or driver, MSRTSS, a GMEM attachment format not in the table, a depth view it didn't see
|
||||
created) keeps the texels' centre (the behaviour before). Per-game override: adapter settings `fdm_bin_w`/`fdm_bin_h`
|
||||
(both non-zero; FrameBridge passes them through `fp_fdm_set_bin`). No `frame.adapter` revision bump (like the first
|
||||
centring layer): games get it with their next rebuild. Headless, darkening test, `snapshot=15`, Fixed, block centre
|
||||
vs optical centre:
|
||||
|
||||
| game | swapchain / rect | bins | before (texel centre) | after |
|
||||
|---|---|---|---|---|
|
||||
| In Death 1x | 2768x2768 / 0,0 1730x1730 | 192x224 | L -76,+31 px, R -83,+35 | L +4,-1, R -3,+3 |
|
||||
| AC Nexus (start flash) | 2592x2256 / 0,521 1707x1735 (R 284,521) | 192x224 | (computed: -16,-80) | L +1,+5, R +2,0 |
|
||||
|
||||
Full-density blocks stay whole bins (768x672 in both games). AC Nexus' darker later snapshots are too dim for the
|
||||
automatic block detection but look centred by eye. Not checked: a GLES/Zink game (dynamic-rendering path), the
|
||||
picture in the headset.
|
||||
|
||||
|
||||
## Typing window takes the controllers: SteamVR theater mode (#476, #477, AC Nexus)
|
||||
|
||||
|
||||
@@ -573,7 +573,14 @@ The inspected arm64 `libVkLayer_VALVE_fdm_injection.so` has these semantics (vir
|
||||
in Valve's rectangle: the area's 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. The bin snapping is the driver's design, not a setting; FramePort can't
|
||||
pick the bins.
|
||||
pick the bins. With density-map offsets the bins slide but the sample grid stays anchored at 0 in map
|
||||
space (a screen bin centred at s samples s - offset), so the area's centre is the centre of the bins
|
||||
whose map-space centre lies in Valve's sharp texels, up to half a bin off the texels' centre. Turnip
|
||||
picks the bin size per framebuffer (`tu_tiling_config_update_tile_layout`: the GMEM budget split
|
||||
between the pass's attachments, fewest bins, FDM passes use the AVOID_CCU layout; Adreno 750 = 3 MiB
|
||||
GMEM, tile alignment 96x32); Vulkan doesn't report it (no `VK_QCOM_tile_properties`). The centring
|
||||
layer recomputes it (`native/foveation/tu_bins.h`, Mesa 26.3.0-devel `git-e07916b45f` on SteamOS
|
||||
0.5.x, logged by the layer); per-game override `fdm_bin_w`/`fdm_bin_h`.
|
||||
|
||||
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,
|
||||
|
||||
@@ -42,6 +42,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Cosmodread | Quest | ✅ Works | |
|
||||
| CrazyTaxiVR | Quest | ✅ Works | |
|
||||
| Creed | Quest | ✅ Works | |
|
||||
| Crisis Brigade 2 reloaded | Quest | ✅ Works | |
|
||||
| Crisis Zone VR | Quest | ✅ Works | |
|
||||
| cubism | Quest | ✅ Works | |
|
||||
| cubism | Quest | ✅ Works | |
|
||||
|
||||
+42
-12
@@ -2,7 +2,7 @@
|
||||
|
||||
# Patches
|
||||
|
||||
Every patch and game setting FramePort knows: 203 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
Every patch and game setting FramePort knows: 205 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
|
||||
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
|
||||
|
||||
@@ -12,7 +12,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
|
||||
- [Patches from OVRPort](#group-overport) (25)
|
||||
- [Steam Frame patches](#group-frame) (71)
|
||||
- [Game settings](#group-adapter) (82)
|
||||
- [Game settings](#group-adapter) (84)
|
||||
- [Files and settings on the Frame](#group-device) (12)
|
||||
- [PC VR (Proton and Revive)](#group-pcvr) (13)
|
||||
|
||||
@@ -50,7 +50,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Content files from the game's data](#frame-asset_files) | `frame.asset_files` | Steam Frame patches | 2 |
|
||||
| [Asset-file service bypass](#frame-asset_files_stub) | `frame.asset_files_stub` | Steam Frame patches | |
|
||||
| [Skip Meta avatars](#frame-avatar_stub) | `frame.avatar_stub` | Steam Frame patches | 1 |
|
||||
| [bHaptics: skip the Bluetooth permission request](#frame-bhaptics_permission) | `frame.bhaptics_permission` | Steam Frame patches | 12 |
|
||||
| [bHaptics: skip the Bluetooth permission request](#frame-bhaptics_permission) | `frame.bhaptics_permission` | Steam Frame patches | 13 |
|
||||
| [BlazeRush: controllers without Meta avatars, own shader cache](#frame-blazerush) | `frame.blazerush` | Steam Frame patches | 1 |
|
||||
| [Browser for sign-ins: open pages in Steam's browser](#frame-browser_handoff) | `frame.browser_handoff` | Steam Frame patches | |
|
||||
| [Cortopia games: don't wait for Meta's org-scoped id](#frame-cortopia_org_id) | `frame.cortopia_org_id` | Steam Frame patches | 1 |
|
||||
@@ -80,7 +80,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Platform compat (ovrMessageType_ToString)](#frame-ovrplatformcompat) | `frame.ovrplatformcompat` | Steam Frame patches | default |
|
||||
| [Stub missing Meta platform functions](#frame-ovrstubs) | `frame.ovrstubs` | Steam Frame patches | default |
|
||||
| [Unpaired pointer-authentication checks](#frame-pac_hints) | `frame.pac_hints` | Steam Frame patches | 1 |
|
||||
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | 3 |
|
||||
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | 4 |
|
||||
| [QuestCraft: count the Frame's network as online](#frame-pojlib_connection) | `frame.pojlib_connection` | Steam Frame patches | 1 |
|
||||
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 9 |
|
||||
| [Stress Level Zero: let Unity start Vulkan itself](#frame-slz_vulkan_hooks) | `frame.slz_vulkan_hooks` | Steam Frame patches | 1 |
|
||||
@@ -96,7 +96,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Unity: start VR without Meta's system apps](#frame-unity_oculus_check) | `frame.unity_oculus_check` | Steam Frame patches | 15 |
|
||||
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 34 |
|
||||
| [Unity: skip Meta's player-in-room check](#frame-unity_scene_active_rooms) | `frame.unity_scene_active_rooms` | Steam Frame patches | 1 |
|
||||
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 136 |
|
||||
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 137 |
|
||||
| [Unity: share thread keys (games that run out of them)](#frame-unity_tls_key_pool) | `frame.unity_tls_key_pool` | Steam Frame patches | 1 |
|
||||
| [Unity: headset always counts as worn](#frame-unity_user_presence) | `frame.unity_user_presence` | Steam Frame patches | 1 |
|
||||
| [GL shim for Unreal (no multisampled render-to-texture)](#frame-unreal_gl_shim) | `frame.unreal_gl_shim` | Steam Frame patches | 1 |
|
||||
@@ -135,6 +135,8 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Per-eye diagnostics (log)](#adapter-eye_debug) | `adapter.eye_debug` | Game settings | |
|
||||
| [Draw fades over the picture](#adapter-fade_quad) | `adapter.fade_quad` | Game settings | 2 |
|
||||
| [Colour space support](#adapter-fb_color_space) | `adapter.fb_color_space` | Game settings | 1 |
|
||||
| [Foveation bin height](#adapter-fdm_bin_h) | `adapter.fdm_bin_h` | Game settings | |
|
||||
| [Foveation bin width](#adapter-fdm_bin_w) | `adapter.fdm_bin_w` | Game settings | |
|
||||
| [Keep full resolution (no dynamic resolution)](#adapter-fixed_resolution) | `adapter.fixed_resolution` | Game settings | 2 |
|
||||
| [Upside-down panel images (diagnostics)](#adapter-flip_dump) | `adapter.flip_dump` | Game settings | |
|
||||
| [Fix upside-down menus](#adapter-flip_emul) | `adapter.flip_emul` | Game settings | |
|
||||
@@ -148,7 +150,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [GL shim: high-precision vertex shaders](#adapter-gl_highp_markers) | `adapter.gl_highp_markers` | Game settings | 1 |
|
||||
| [GL shim: integer vertex inputs](#adapter-gl_int_attribs) | `adapter.gl_int_attribs` | Game settings | 1 |
|
||||
| [Flat panel diagnostics (log)](#adapter-gl_mv_debug) | `adapter.gl_mv_debug` | Game settings | |
|
||||
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 122 |
|
||||
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 123 |
|
||||
| [Vibration strength](#adapter-haptic_scale) | `adapter.haptic_scale` | Game settings | |
|
||||
| [Turn off Quest Vulkan foveation](#adapter-hide_fb_foveation) | `adapter.hide_fb_foveation` | Game settings | |
|
||||
| [Turn off space warp](#adapter-hide_space_warp) | `adapter.hide_space_warp` | Game settings | 5 |
|
||||
@@ -209,7 +211,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Extra Lepton settings](#device-lepton_env) | `device.lepton_env` | Files and settings on the Frame | 3 |
|
||||
| [Share a folder with another game](#device-shared_folder) | `device.shared_folder` | Files and settings on the Frame | |
|
||||
| [Controllers as a gamepad (Steam Input)](#device-steam_gamepad) | `device.steam_gamepad` | Files and settings on the Frame | |
|
||||
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 135 |
|
||||
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 136 |
|
||||
| [Launch options (SteamVR or OpenXR)](#pcvr-launch_args) | `pcvr.launch_args` | PC VR (Proton and Revive) | 2 |
|
||||
| [Provide the Oculus runtime from Revive (Frame)](#pcvr-libovr_redirect) | `pcvr.libovr_redirect` | PC VR (Proton and Revive) | |
|
||||
| [No Unreal crash reporter](#pcvr-no_crash_reporter) | `pcvr.no_crash_reporter` | PC VR (Proton and Revive) | |
|
||||
@@ -602,7 +604,7 @@ Stops bHaptics from asking for a Bluetooth permission the Frame doesn't have (th
|
||||
- `frame.bhaptics_permission` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** bHaptics asks for an Android 12 Bluetooth permission at start; the permission screen interrupts the game on the Frame.
|
||||
- **Catalog games (12):** Darksword, Deisim, Epic Roller Coasters, Eternal Starlight, Quantum Void, Ragnarock, Smash Drums, SynthRiders and 4 more.
|
||||
- **Catalog games (13):** Crisis Brigade 2 reloaded, Darksword, Deisim, Epic Roller Coasters, Eternal Starlight, Quantum Void, Ragnarock, Smash Drums and 5 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `bhaptics-permission-prompt` (error): The game asked for Android 12's Bluetooth permission (bHaptics' vest SDK, for example Yupitergrad).
|
||||
- **Source:** [patches/frame/bhaptics_permission.py](../src/frameport/patches/frame/bhaptics_permission.py)
|
||||
@@ -995,7 +997,7 @@ Stops games with Photon Voice chat from crashing on the Frame when voice chat st
|
||||
- `frame.photon_voice_audio_route` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Photon Voice calls Android 12 audio methods, which Lepton (Android 11) lacks, and crashes when voice chat starts.
|
||||
- **Catalog games (3):** Deisim, Table Troopers, UNDERDOGS.
|
||||
- **Catalog games (4):** Crisis Brigade 2 reloaded, Deisim, Table Troopers, UNDERDOGS.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `photon-voice-api31` (fatal): The game's Photon Voice microphone plugin calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes when voice chat starts (for example Table Troopers at the tutorial).
|
||||
- **Source:** [patches/frame/photon_voice_audio_route.py](../src/frameport/patches/frame/photon_voice_audio_route.py)
|
||||
@@ -1213,7 +1215,7 @@ Lets you type into this app's text fields on the Frame (they'd close at once).
|
||||
- `frame.unity_text_input` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unity app with text fields (…): they close at once on the Frame because it has no system keyboard.
|
||||
- **Catalog games (136):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 128 more.
|
||||
- **Catalog games (137):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 129 more.
|
||||
- **Source:** [patches/frame/unity_text_input.py](../src/frameport/patches/frame/unity_text_input.py)
|
||||
|
||||
> Unity's TMP_InputField / InputField wait for Android's on-screen keyboard and close themselves a frame later when there is none (Lepton has no on-screen keyboard; Meta's system keyboard does this on a Quest), so a selected text field only flashes a caret. Rewrites TouchScreenKeyboardShouldBeUsed -> false and isKeyboardUsingEvents (TMP) / InPlaceEditing (uGUI) -> true in libil2cpp.so, found per game with Cpp2IL (downloaded on first use), so fields stay selected and take key presses: Steam's keyboard (with "Show the app window") or Type on Frame from the PC.
|
||||
@@ -1756,6 +1758,34 @@ For games that stop at start because the headset reports no colour space (for ex
|
||||
|
||||
> Offers Meta's colour-space extension when the Frame's runtime lacks it: the game is told the headset uses Rec.709 and may choose any colour space (no conversion is done). For games that quit before their VR session with "Rec.709 color space unavailable" (for example OutRun Quest VR, GitHub #336). Only reaches games that use their own OpenXR loader; OVRPort's dispatcher can't forward the new functions.
|
||||
|
||||
### <a id="adapter-fdm_bin_h"></a>Foveation bin height
|
||||
|
||||
Set together with the width. 0 = automatic.
|
||||
|
||||
- `adapter.fdm_bin_h`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Foveation bin height" (advanced): slider 0–2048 in steps of 32.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Height in pixels of the driver's GMEM bins for the eye passes, 0 = worked out automatically. See fdm_bin_w.
|
||||
|
||||
### <a id="adapter-fdm_bin_w"></a>Foveation bin width
|
||||
|
||||
Only if the full-resolution area of Valve's foveation sits off-centre in this game. 0 = automatic.
|
||||
|
||||
- `adapter.fdm_bin_w`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Foveation bin width" (advanced): slider 0–2016 in steps of 96.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Width in pixels of the GMEM bins the Frame's driver (Turnip) renders the eye passes in, 0 = worked out automatically. The driver gives each bin the density Valve's map has at the bin's centre, so the full-resolution area is a block of whole bins; FramePort centres that block on each eye and works out the bin size the way the driver does. Set this and fdm_bin_h (both non-zero) only when that comes out wrong for a game: the sizes are measured with "Darken coarse fragments (test)" and snapshot=N (multiples of 96 wide, 32 high; GitHub #394).
|
||||
|
||||
### <a id="adapter-fixed_resolution"></a>Keep full resolution (no dynamic resolution)
|
||||
|
||||
Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. May increase GPU load. Used at the next launch.
|
||||
@@ -1960,7 +1990,7 @@ For games that freeze the Frame when a controller vibrates (for example Lucky's
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Controllers → "Fix freezes when controllers vibrate" (advanced).
|
||||
- **Catalog games (122):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Barbaria, Battlemarked, BattleSisters and 114 more.
|
||||
- **Catalog games (123):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Barbaria, Battlemarked, BattleSisters and 115 more.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Routes OVRPlugin through FramePort's extension shim, which turns Meta's amplitude-envelope vibrations into plain ones: OVRPort's loader reads their nanosecond duration as seconds and allocates gigabytes, freezing the Frame when the game vibrates a controller (for example Lucky's Tale's save slots, GitHub #9). Needs a rebuild.
|
||||
@@ -2863,7 +2893,7 @@ Lets typing reach this app from Steam's on-screen keyboard or your PC's keyboard
|
||||
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
|
||||
- **Suggested (on):** The app opens a flat launcher that starts its VR part (…): Lepton runs VR apps headless, so the launcher is only seen in a shown Android window.
|
||||
- **Suggested (on):** App with text fields: key presses only reach a shown Android window.
|
||||
- **Catalog games (135):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Attack on Titan VR: Unbreakable, Audica and 127 more.
|
||||
- **Catalog games (136):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Attack on Titan VR: Unbreakable, Audica and 128 more.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Lepton runs VR apps headless: their Android window is never shown, so it never gets keyboard focus and no key press (Steam's on-screen keyboard, a USB/Bluetooth keyboard, Type on Frame) reaches the app's text fields. With Lepton's lepton-show-flatscreen marker the window is shown (behind the VR view, which keeps working) and Steam's keyboard opens for a selected text field. Steam lists the shown window as "Gamescope" (the Frame's compositor). While the game runs, FramePort turns off SteamVR's automatic theater mode for game windows: it put the Android window in front of the player with the controllers as a mouse pointer, and the game got no controller input. The window stays a page of the game in the dashboard. Pairs with "Make Unity text fields work without a system keyboard". Apps that open a flat launcher before their VR part (for example Xelee or WiiCompiled, whose launchers pick the game) need the window too: headless, the launcher is never seen and the VR part never starts. For those, theater mode stays on, so the launcher is in front of you; once the VR part runs, close the dashboard with the controller's button if it opened on the window.
|
||||
|
||||
+2
-1
@@ -25,7 +25,7 @@ longer investigations.
|
||||
| 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, run on Valve's offset path: Valve's own `disable_offsets` path crashed AC Nexus) or re-bases the gaze on the eye's rect (Eye-tracked); the layer never changes images and writes every offset Turnip reads (Valve declares 1 for two views). 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 the area changes somewhat with render resolution; the bin snapping isn't fixable in FramePort |
|
||||
| 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, run on Valve's offset path: Valve's own `disable_offsets` path crashed AC Nexus) or re-bases the gaze on the eye's rect (Eye-tracked); the layer never changes images and writes every offset Turnip reads (Valve declares 1 for two views). 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 the area changes somewhat with render resolution; the bin snapping isn't fixable in FramePort. A residual offset of up to half a bin (In Death -80,+32 px, same direction in both eyes) is the bin grid: the layer works out Turnip's bin size per pass (`native/foveation/tu_bins.h`, Adreno 750; log `fdm_centre: pass … bins 192x224 computed, sharp centre …`) and centres the full-density bins; `bins … unknown` = not modelled (texel centre). Wrong for a game: measure the bins with the darkening test + `snapshot=N` and set `fdm_bin_w`/`fdm_bin_h` (Game settings → Troubleshooting, or `frameport settings <pkg> fdm_bin_w=192 fdm_bin_h=224`) |
|
||||
| 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, for example 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 |
|
||||
@@ -105,6 +105,7 @@ longer investigations.
|
||||
| a32 bridge faults at a low Thumb PC while loading a library, with the same unrelocated pointer in `.init_array` (for example Journey of the Gods: `0x1259e9` in OVRPlugin) | The guest loader ignored the older `DT_ANDROID_RELR` / `DT_ANDROID_RELRSZ` tags, leaving relative pointers at their link addresses | Fixed in the guest loader: both Android and standard RELR tags use the existing RELR decoder. Rebuild the a32 runtime and APK; host regression tests cover both tag families, but game startup still needs a Frame run |
|
||||
| 32-bit game on the bridge quits right after its loading screen with `a32: block at 0x… overflowed the code buffer` (exit 127; Pinball FX2 VR, GitHub #342: OpenSSL's `sha1_block_data_order_armv8`, ~60 SHA-1 crypto instructions in a row, reached from `RAND_poll`) | Every instruction the JIT hands to the interpreter needs an exit stub; the translator only ended a block at 96 instructions or half its code buffer, so a dense run of such instructions overflowed the 64-entry stub table and `fatal()` ended the process | Fixed in the translator (jit.c `STUB_RESERVE`: a block ends while 8 stubs are left); rebuild the game. Find the code with the logged address minus the library's load address (`a32rt: loaded …`) and `A32_BREAK=<addr>` for its caller; `tests/programs/sha1ce.c` covers it |
|
||||
| 32-bit Unity game on the bridge: a video never shows (Ghost Giant's intro stays black, GitHub #439); log `AndroidMediaNDK could not load symbol AMediaFormat_new` then `GetDirectBufferAddress: buffer at 0x… is outside the guest's reach` / `AndroidVideoMedia: Error while getting codec input buffers` | Unity's video player needs libmediandk (AMediaFormat/AMediaCodec/AMediaExtractor); the bridge had none, and Unity's JNI fallback reads MediaCodec ByteBuffers in the host's heap, above the guest's 4 GiB | Bridge `host/media.c` + imports.txt: the NDK media functions (and `guest/media_keys.c`: the `AMEDIAFORMAT_KEY_*` data symbols Unity also looks up; without them it stops at `could not load symbol AMEDIAFORMAT_KEY_AAC_PROFILE`), format strings/buffers copied, codec buffers mirrored in guest memory (synced at `queueInputBuffer` / `getOutputBuffer`); triage `a32-video-ndk`; harness `fakemedia.c` + `libbtest4.c`. Headless check (2026-10-24, `OVP_HEADLESS_TEST=1` + `snapshot=3`): Ghost Giant's two intro videos play and its main menu renders |
|
||||
| 32-bit game on the bridge: distorted, choppy sound all the time (Ghost Giant, FMOD, GitHub #439); no underruns, rate/format right; Unity logs `VideoPlayback audio buffer overflow. N sample frames discarded` during videos | The bridge's OpenSL ES player wrote each buffer to AAudio with a blocking write, so the guest's buffers were taken in pairs at AudioFlinger's 20 ms bursts (callbacks 0 ms, then 20 ms apart); a two-buffer engine that mixes per callback then hands over discontinuous blocks (PCM dump: sample jumps at every 10 ms buffer boundary 3-15x those inside a buffer) | Bridge `host/audio_pacing.h`: reads paced on the clock (one buffer per its duration, 40 ms silence pre-roll, pacing from the device's first read, drift nudged out, re-anchor after stalls/underruns); `A32_AUDIO_PACING=0` = old writes, `A32_AUDIO_TRACE=1` = timing every 2 s, `A32_AUDIO_DUMP=<file>` = raw PCM (`LEPTON_ENV_<NAME>`). Headless check (2026-10-25): boundary jumps = in-buffer jumps, video overflow 212 -> 0, 72 fps |
|
||||
| 32-bit game on the bridge: framerate falls over minutes, purple/garbled geometry, then `Could not allocate memory: System out of memory!` (Spunky Dungeon) | The bridge's guest copies of mapped GL buffers were tracked per buffer target: a second buffer mapped on the same target lost the first one's copy (never written back: garbage vertices; never freed: ~10 MB/s until the guest's 4 GiB were full) | Fixed in the bridge (gl.c: mappings per buffer object). Check: the guest's `[anon:scudo:*]` maps below 4 GiB in /proc/<pid>/maps must stay flat |
|
||||
| 32-bit game on the bridge loads, leaves VR and its engine threads end, no crash, no finish() (Gunjack) | The game checks its own APK signature (UE4 `AndroidThunkJava_GetSignature` = Base64 of the signing certificate's SHA-1, compared in AndroidMain): every FramePort build is re-signed | `frame.a32_bridge` stores the original APK's value in `assets/fp_a32_signature.txt` (Unreal games) and the bridge answers the guest's call with it (log `AndroidThunkJava_GetSignature: original signature returned`). Find other such checks with `A32_BREAK` on the game's exit path (vrapi_LeaveVrMode logs its caller) |
|
||||
| 32-bit NativeActivity game on the bridge ignores every controller (Gunjack, UE 4.10 Gear VR: no vrapi input functions, it reads the Gear VR touchpad/back button as Android input events from its AInputQueue) | The bridge sent its controller events through `Activity.dispatchKeyEvent`, which a NativeActivity never passes to native code | input.c puts synthetic touchpad (motion, touchscreen source) and back-key events in front of the game's AInputQueue: `ALooper_pollAll/pollOnce` return the queue's ident while one waits, `AInputQueue_getEvent` hands it out, the AInputEvent/AKeyEvent/AMotionEvent imports answer for it. Right trigger = hold the touchpad, A = tap, right stick = swipe (up/down = swipe forward/back along the touchpad's X axis: Gunjack's `OnFlick` takes flick angles <45°/>315° as reload, 135-225° as special weapon), B/left menu = back (log `controllers: the game reads its AInputQueue`) |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 105 KiB |
+1
-1
@@ -36,7 +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 (never changing images: for Fixed it drops `disable_offsets` from `FDM_DEBUG` in-process so Valve runs its offset mode, then overrides Valve's gaze offsets; every offset Turnip reads is written, per view or per eye map layer) 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`. |
|
||||
| `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 (never changing images: for Fixed it drops `disable_offsets` from `FDM_DEBUG` in-process so Valve runs its offset mode, then overrides Valve's gaze offsets; every offset Turnip reads is written, per view or per eye map layer) 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`. Turnip renders whole GMEM bins at the density at each bin's centre: `tu_bins.h` recomputes Turnip's bin size per foveated pass (as Mesa does, Adreno 750 only) and the layer centres the full-density bins, not Valve's texels; adapter settings `fdm_bin_w`/`fdm_bin_h` override it (`fp_fdm_set_bin`); unmodelled passes keep the texel centre. `FRAMEPORT_FDM_DIAG=1` logs offsets, bins and shared-viewport causes. 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). |
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/* OpenSL ES for guest code: the part audio engines use for output (an engine, an output mix and PCM audio players
|
||||
* fed by an Android simple buffer queue), implemented here in the guest and played through the host's AAudio
|
||||
* (a32_audio_*, host/android.c). OpenSL's objects are tables of function pointers, so keeping them on the guest side
|
||||
* needs no thunks; each playing player has a guest thread that writes its queued buffers (blocking, which paces the
|
||||
* queue like a real device) and calls the buffer-queue callback. Recording and everything else report "not
|
||||
* supported". */
|
||||
* needs no thunks; each playing player has a guest thread that writes its queued buffers and calls the buffer-queue
|
||||
* callback. The write blocks until the buffer is due (host/audio_pacing.h: real-time pacing, so a two-buffer engine
|
||||
* like FMOD gets its callbacks evenly and has time to refill a buffer before it is read). Recording and everything
|
||||
* else report "not supported". */
|
||||
#include <SLES/OpenSLES.h>
|
||||
#include <SLES/OpenSLES_Android.h>
|
||||
#include <pthread.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "grt.h"
|
||||
|
||||
/* host/android.c (imports.txt) */
|
||||
@@ -55,7 +57,7 @@ typedef struct obj {
|
||||
slPlayCallback pcb;
|
||||
void *pctx;
|
||||
SLuint32 pmask;
|
||||
struct { const void *data; SLuint32 size; } q[QMAX];
|
||||
struct { const void *data; SLuint32 size; int64_t t; } q[QMAX];
|
||||
SLuint32 qhead, qcount, qindex;
|
||||
SLmillibel level;
|
||||
SLboolean mute;
|
||||
@@ -65,12 +67,39 @@ typedef struct obj {
|
||||
int quit, has_thread;
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t cond;
|
||||
/* A32_AUDIO_TRACE: enqueue → read times (ns) */
|
||||
int64_t tr_min, tr_max, tr_sum, tr_report;
|
||||
int tr_n;
|
||||
} obj;
|
||||
|
||||
#define OBJ(self, field) ((obj *)((char *)(self) - offsetof(obj, field)))
|
||||
|
||||
/* ---------------------------------------------------------------- the player thread */
|
||||
|
||||
static int trace = -1; /* A32_AUDIO_TRACE (host/android.c traces the pacing) */
|
||||
|
||||
static int64_t now_ns(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
|
||||
}
|
||||
|
||||
/* how long after its enqueue a buffer was read (the write returned), every 2 s */
|
||||
static void trace_read(obj *o, int64_t enqueued)
|
||||
{
|
||||
int64_t now = now_ns(), d = now - enqueued;
|
||||
if (!o->tr_n || d < o->tr_min) o->tr_min = d;
|
||||
if (d > o->tr_max) o->tr_max = d;
|
||||
o->tr_sum += d;
|
||||
o->tr_n++;
|
||||
if (!o->tr_report) o->tr_report = now;
|
||||
if (now - o->tr_report < 2000000000) return;
|
||||
grt_log(4, "a32sles", "enqueue to read 2s: n=%d min/avg/max=%.2f/%.2f/%.2f ms", o->tr_n, o->tr_min / 1e6,
|
||||
o->tr_sum / 1e6 / o->tr_n, o->tr_max / 1e6);
|
||||
o->tr_report = now, o->tr_n = 0, o->tr_sum = 0, o->tr_max = 0;
|
||||
}
|
||||
|
||||
static void *player_thread(void *arg)
|
||||
{
|
||||
obj *o = arg;
|
||||
@@ -80,9 +109,11 @@ static void *player_thread(void *arg)
|
||||
if (o->quit) break;
|
||||
const void *data = o->q[o->qhead].data;
|
||||
SLuint32 size = o->q[o->qhead].size;
|
||||
int64_t enqueued = o->q[o->qhead].t;
|
||||
pthread_mutex_unlock(&o->lock);
|
||||
int frame = o->channels * o->bits / 8;
|
||||
if (o->stream && frame) a32_audio_write(o->stream, data, (int)(size / (SLuint32)frame));
|
||||
if (trace > 0) trace_read(o, enqueued);
|
||||
pthread_mutex_lock(&o->lock);
|
||||
if (o->qcount && o->q[o->qhead].data == data) { /* not cleared meanwhile */
|
||||
o->qhead = (o->qhead + 1) % QMAX;
|
||||
@@ -258,6 +289,7 @@ static SLresult queue_enqueue(SLAndroidSimpleBufferQueueItf self, const void *bu
|
||||
logged++;
|
||||
SLuint32 at = (o->qhead + o->qcount) % QMAX;
|
||||
o->q[at].data = buf, o->q[at].size = size;
|
||||
o->q[at].t = trace > 0 ? now_ns() : 0;
|
||||
o->qcount++;
|
||||
pthread_cond_broadcast(&o->cond);
|
||||
pthread_mutex_unlock(&o->lock);
|
||||
@@ -380,6 +412,7 @@ static SLresult engine_create_player(SLEngineItf self, SLObjectItf *player, SLDa
|
||||
grt_log(5, "a32sles", "float PCM isn't supported");
|
||||
return SL_RESULT_CONTENT_UNSUPPORTED;
|
||||
}
|
||||
if (trace < 0) trace = getenv("A32_AUDIO_TRACE") != NULL;
|
||||
obj *o = new_obj(K_PLAYER);
|
||||
o->play = &play_itf;
|
||||
o->queue = &queue_itf;
|
||||
|
||||
@@ -33,6 +33,10 @@ void imp_ASensorManager_createEventQueue(a32_cpu *c, argreader *R)
|
||||
/* ---------------------------------------------------------------- audio output (guest/opensles.c) */
|
||||
|
||||
#include <aaudio/AAudio.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include "audio_pacing.h"
|
||||
|
||||
#define W(f) extern __typeof__(f) f __attribute__((weak))
|
||||
W(AAudio_createStreamBuilder); W(AAudioStreamBuilder_setSampleRate); W(AAudioStreamBuilder_setChannelCount);
|
||||
@@ -40,16 +44,53 @@ W(AAudioStreamBuilder_setFormat); W(AAudioStreamBuilder_setPerformanceMode); W(A
|
||||
W(AAudioStreamBuilder_delete); W(AAudioStream_requestStart); W(AAudioStream_requestPause);
|
||||
W(AAudioStream_requestFlush); W(AAudioStream_write); W(AAudioStream_close); W(AAudio_convertResultToText);
|
||||
W(AAudioStream_setBufferSizeInFrames); W(AAudioStream_getBufferCapacityInFrames); W(AAudioStream_getXRunCount);
|
||||
W(AAudioStream_getFramesWritten); W(AAudioStream_getFramesRead);
|
||||
#undef W
|
||||
|
||||
/* A32_AUDIO_PACING=0: the old blocking writes only (see audio_pacing.h); A32_AUDIO_TRACE=1: pacing statistics every
|
||||
* 2 s (and the guest's enqueue → read times, guest/opensles.c); A32_AUDIO_DUMP=<file>: append everything written to
|
||||
* the stream (raw PCM) to that file, e.g. /data/data/<package>/files/a32audio.pcm */
|
||||
static int audio_pacing = -1, audio_trace;
|
||||
static FILE *audio_dump;
|
||||
|
||||
typedef struct {
|
||||
AAudioStream *stream;
|
||||
int frame_bytes;
|
||||
int frame_bytes, rate;
|
||||
a32_pace pace;
|
||||
int running, primed; /* paced: the device has read since the start; the start's pre-roll was written */
|
||||
int64_t read_mark;
|
||||
/* trace */
|
||||
int64_t t_last, t_report, gap_min, gap_max, gap_sum, slept, level_min, level_max;
|
||||
int writes;
|
||||
} audio_out;
|
||||
|
||||
static int64_t audio_now(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * A32_PACE_NS + ts.tv_nsec;
|
||||
}
|
||||
|
||||
static int64_t audio_level(audio_out *o)
|
||||
{
|
||||
if (!AAudioStream_getFramesWritten || !AAudioStream_getFramesRead) return -1;
|
||||
int64_t w = AAudioStream_getFramesWritten(o->stream), r = AAudioStream_getFramesRead(o->stream);
|
||||
return w >= 0 && r >= 0 ? w - r : -1;
|
||||
}
|
||||
|
||||
void imp_a32_audio_open(a32_cpu *c, argreader *R)
|
||||
{
|
||||
int rate = (int)rd32(R), channels = (int)rd32(R), bits = (int)rd32(R);
|
||||
if (audio_pacing < 0) {
|
||||
const char *e = getenv("A32_AUDIO_PACING");
|
||||
audio_pacing = !(e && *e == '0');
|
||||
audio_trace = getenv("A32_AUDIO_TRACE") != NULL;
|
||||
const char *d = getenv("A32_AUDIO_DUMP");
|
||||
if (d && *d) {
|
||||
audio_dump = fopen(d, "ab");
|
||||
if (!audio_dump) bridge_log(ANDROID_LOG_WARN, "audio: can't write %s", d);
|
||||
}
|
||||
}
|
||||
AAudioStreamBuilder *b = NULL;
|
||||
if (!AAudio_createStreamBuilder || bits != 16 || AAudio_createStreamBuilder(&b) != AAUDIO_OK) {
|
||||
bridge_log(ANDROID_LOG_ERROR, "audio: can't open a %d Hz, %d channel, %d bit stream", rate, channels, bits);
|
||||
@@ -70,25 +111,100 @@ void imp_a32_audio_open(a32_cpu *c, argreader *R)
|
||||
return;
|
||||
}
|
||||
/* ~40 ms: writes block at the device's pace. A larger buffer let the player thread drain the guest's queue
|
||||
* faster than real time, and the guest's mixer (FMOD) fell behind: crackling */
|
||||
* faster than real time, and the guest's mixer (FMOD) fell behind: crackling. AudioFlinger also starts a legacy
|
||||
* track only once its buffer is full, so paced streams keep 40 ms until the device reads (the pre-roll fills
|
||||
* it) and only then take more room for the drift correction (audio_running) */
|
||||
if (AAudioStream_setBufferSizeInFrames) AAudioStream_setBufferSizeInFrames(s, rate / 25);
|
||||
audio_out *o = calloc(1, sizeof *o);
|
||||
o->stream = s;
|
||||
o->frame_bytes = channels * 2;
|
||||
bridge_log(ANDROID_LOG_INFO, "audio: %d Hz, %d channels through AAudio", rate, channels);
|
||||
o->rate = rate;
|
||||
a32_pace_init(&o->pace, rate);
|
||||
o->gap_min = INT64_MAX;
|
||||
o->level_min = INT64_MAX;
|
||||
bridge_log(ANDROID_LOG_INFO, "audio: %d Hz, %d channels through AAudio%s", rate, channels,
|
||||
audio_pacing ? ", paced" : "");
|
||||
ret32(c, ptr2h(o));
|
||||
}
|
||||
|
||||
/* blocking, like a full OpenSL buffer queue: this paces the guest's audio thread */
|
||||
static aaudio_result_t audio_put(audio_out *o, const void *data, int frames)
|
||||
{
|
||||
if (audio_dump) fwrite(data, (size_t)o->frame_bytes, (size_t)frames, audio_dump);
|
||||
return AAudioStream_write(o->stream, data, frames, 1000000000LL);
|
||||
}
|
||||
|
||||
static void audio_silence(audio_out *o, int64_t frames)
|
||||
{
|
||||
static const int16_t zeros[4096];
|
||||
int chunk = (int)sizeof zeros / o->frame_bytes;
|
||||
for (int64_t left = frames; left > 0; left -= chunk) audio_put(o, zeros, left < chunk ? (int)left : chunk);
|
||||
}
|
||||
|
||||
static void audio_trace_write(audio_out *o, int64_t now, int64_t level)
|
||||
{
|
||||
if (o->t_last) {
|
||||
int64_t gap = now - o->t_last;
|
||||
if (gap < o->gap_min) o->gap_min = gap;
|
||||
if (gap > o->gap_max) o->gap_max = gap;
|
||||
o->gap_sum += gap;
|
||||
}
|
||||
if (level >= 0 && level < o->level_min) o->level_min = level;
|
||||
if (level > o->level_max) o->level_max = level;
|
||||
o->t_last = now;
|
||||
o->writes++;
|
||||
if (!o->t_report) o->t_report = now;
|
||||
if (now - o->t_report < 2 * A32_PACE_NS) return;
|
||||
int n = o->writes > 1 ? o->writes - 1 : 1;
|
||||
bridge_log(ANDROID_LOG_INFO,
|
||||
"audio pacing 2s: writes=%d interval min/avg/max=%.1f/%.1f/%.1f ms level min/max=%lld/%lld "
|
||||
"slept=%lld ms anchors=%d shifts=%d silence=%lld frames",
|
||||
o->writes, o->gap_min / 1e6, o->gap_sum / 1e6 / n, o->gap_max / 1e6,
|
||||
(long long)(o->level_min == INT64_MAX ? -1 : o->level_min), (long long)o->level_max,
|
||||
(long long)(o->slept / 1000000), o->pace.anchors, o->pace.shifts, (long long)o->pace.silence_frames);
|
||||
if (audio_dump) fflush(audio_dump);
|
||||
o->t_report = now, o->writes = 0, o->gap_sum = 0, o->gap_min = INT64_MAX, o->gap_max = 0, o->slept = 0;
|
||||
o->level_min = INT64_MAX, o->level_max = 0;
|
||||
}
|
||||
|
||||
/* blocking, like a full OpenSL buffer queue; paced (audio_pacing.h): each buffer is read when the device needs it */
|
||||
void imp_a32_audio_write(a32_cpu *c, argreader *R)
|
||||
{
|
||||
audio_out *o = h2ptr(rd32(R));
|
||||
const void *data = g2p(rd32(R));
|
||||
int frames = (int)rd32(R);
|
||||
aaudio_result_t r = o ? AAudioStream_write(o->stream, data, frames, 1000000000LL) : 0;
|
||||
if (!o) {
|
||||
ret32(c, 0);
|
||||
return;
|
||||
}
|
||||
int x = AAudioStream_getXRunCount ? AAudioStream_getXRunCount(o->stream) : 0;
|
||||
if (audio_pacing && !o->running && AAudioStream_getFramesRead &&
|
||||
AAudioStream_getFramesRead(o->stream) > o->read_mark) { /* the device plays: pace from here */
|
||||
o->running = 1;
|
||||
a32_pace_restart(&o->pace);
|
||||
if (AAudioStream_setBufferSizeInFrames) AAudioStream_setBufferSizeInFrames(o->stream, o->rate * 3 / 20);
|
||||
}
|
||||
if (audio_pacing && !o->running && !o->primed) { /* fill the 40 ms with silence: that starts the device */
|
||||
int64_t level = audio_level(o);
|
||||
audio_silence(o, level > 0 ? o->pace.preroll - level : o->pace.preroll);
|
||||
o->primed = 1;
|
||||
}
|
||||
if (audio_pacing && o->running) {
|
||||
for (;;) {
|
||||
int64_t now = audio_now();
|
||||
a32_pace_step s = a32_pace_before(&o->pace, now, audio_level(o), x);
|
||||
audio_silence(o, s.silence);
|
||||
if (s.wait_ns <= 0) break;
|
||||
struct timespec ts = {(time_t)(s.wait_ns / A32_PACE_NS), (long)(s.wait_ns % A32_PACE_NS)};
|
||||
while (nanosleep(&ts, &ts) && errno == EINTR) {}
|
||||
o->slept += s.wait_ns;
|
||||
}
|
||||
}
|
||||
if (audio_trace) audio_trace_write(o, audio_now(), audio_level(o));
|
||||
aaudio_result_t r = audio_put(o, data, frames);
|
||||
if (audio_pacing && r > 0) a32_pace_after(&o->pace, r);
|
||||
static int xruns;
|
||||
if (o && AAudioStream_getXRunCount) {
|
||||
int x = AAudioStream_getXRunCount(o->stream);
|
||||
if (AAudioStream_getXRunCount) {
|
||||
x = AAudioStream_getXRunCount(o->stream);
|
||||
if (x > xruns && (xruns < 20 || x % 100 == 0)) bridge_log(ANDROID_LOG_WARN, "audio: %d underruns", x);
|
||||
xruns = x;
|
||||
}
|
||||
@@ -98,7 +214,13 @@ void imp_a32_audio_write(a32_cpu *c, argreader *R)
|
||||
void imp_a32_audio_start(a32_cpu *c, argreader *R)
|
||||
{
|
||||
audio_out *o = h2ptr(rd32(R));
|
||||
if (o) AAudioStream_requestStart(o->stream);
|
||||
if (!o) return;
|
||||
/* paced: a new pre-roll, then a new timeline once the device reads again */
|
||||
o->running = 0, o->primed = 0;
|
||||
o->read_mark = AAudioStream_getFramesRead ? AAudioStream_getFramesRead(o->stream) : 0;
|
||||
if (audio_pacing && AAudioStream_setBufferSizeInFrames) AAudioStream_setBufferSizeInFrames(o->stream, o->rate / 25);
|
||||
a32_pace_restart(&o->pace);
|
||||
AAudioStream_requestStart(o->stream);
|
||||
}
|
||||
|
||||
void imp_a32_audio_pause(a32_cpu *c, argreader *R)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/* Real-time pacing of the guest's OpenSL ES buffer queue (guest/opensles.c → host/android.c a32_audio_write).
|
||||
*
|
||||
* The device (AudioFlinger) takes data from the AAudio stream in bursts (~20 ms at 48 kHz). A blocking write alone
|
||||
* therefore releases the guest's buffers in bursts too: two 10 ms buffers back to back, then nothing for 20 ms, and the
|
||||
* buffer-queue callbacks follow the same rhythm. Audio engines that keep only two buffers and fill them from their own
|
||||
* mixer thread (FMOD: Ghost Giant, GitHub #439) are read before they have refilled them: distorted sound. A real
|
||||
* OpenSL ES device reads each buffer when it is due, so this paces the reads on the clock instead: buffer k is read at
|
||||
* anchor + k × its duration, with a short silence pre-roll in front so the device always has a cushion. The level
|
||||
* (frames written to the stream but not yet read by the device) keeps the timeline honest: an underrun or a level
|
||||
* below `low` (the guest stalled) starts again with a new pre-roll; a level that grows far past the cushion (the
|
||||
* device fell behind) shifts the timeline back; small clock drift either way is nudged out gradually.
|
||||
*
|
||||
* host/android.c starts pacing only once the device reads: AudioFlinger starts a legacy track when its buffer is full,
|
||||
* so the stream keeps a 40 ms buffer until then and the pre-roll is what fills it (pacing a stream that hasn't
|
||||
* started would wait for ever). A32_AUDIO_PACING=0 turns pacing off.
|
||||
*
|
||||
* Pure logic, no AAudio: tests/fixtures/src/a32_audio_pacing_test.c drives it with a simulated device. */
|
||||
#ifndef A32_AUDIO_PACING_H
|
||||
#define A32_AUDIO_PACING_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define A32_PACE_NS 1000000000LL
|
||||
|
||||
typedef struct {
|
||||
int rate;
|
||||
int64_t anchor_ns;
|
||||
int started; /* 0 = start again (stream started or resumed) */
|
||||
int checked; /* the timeline was corrected for the next write */
|
||||
int64_t frames; /* guest frames written since the anchor */
|
||||
int64_t preroll, low, slack; /* frames */
|
||||
int64_t stall_ns;
|
||||
int xruns;
|
||||
/* counters for the trace */
|
||||
int anchors, shifts;
|
||||
int64_t silence_frames;
|
||||
} a32_pace;
|
||||
|
||||
typedef struct {
|
||||
int64_t wait_ns; /* > 0: sleep this long, then ask again */
|
||||
int64_t silence; /* frames of silence to write before the guest's data */
|
||||
} a32_pace_step;
|
||||
|
||||
static inline void a32_pace_init(a32_pace *p, int rate)
|
||||
{
|
||||
*p = (a32_pace){0};
|
||||
p->rate = rate > 0 ? rate : 48000;
|
||||
p->preroll = (int64_t)p->rate * 40 / 1000; /* = the old 40 ms stream buffer: same latency as before */
|
||||
p->low = (int64_t)p->rate * 10 / 1000;
|
||||
p->slack = (int64_t)p->rate * 20 / 1000;
|
||||
p->stall_ns = 100000000LL;
|
||||
}
|
||||
|
||||
static inline void a32_pace_restart(a32_pace *p) { p->started = 0, p->checked = 0; }
|
||||
|
||||
static inline int64_t a32_pace_due(const a32_pace *p) { return p->anchor_ns + p->frames * A32_PACE_NS / p->rate; }
|
||||
|
||||
/* anchor the timeline so the next guest frame is due at `now` */
|
||||
static inline void a32_pace_anchor(a32_pace *p, int64_t now)
|
||||
{
|
||||
p->anchor_ns = now - p->frames * A32_PACE_NS / p->rate;
|
||||
p->anchors++;
|
||||
}
|
||||
|
||||
/* Before a guest write. now: CLOCK_MONOTONIC ns; level: frames buffered in the stream (< 0 = unknown); xruns: the
|
||||
* stream's underrun count. Write `silence` frames of silence first; with wait_ns > 0 sleep that long and call again
|
||||
* (the timeline is corrected once per write, the calls after a sleep only wait for the due time). */
|
||||
static inline a32_pace_step a32_pace_before(a32_pace *p, int64_t now, int64_t level, int xruns)
|
||||
{
|
||||
a32_pace_step s = {0, 0};
|
||||
if (p->checked) {
|
||||
int64_t wait = a32_pace_due(p) - now;
|
||||
s.wait_ns = wait > 0 ? wait : 0;
|
||||
return s;
|
||||
}
|
||||
p->checked = 1;
|
||||
int underrun = xruns > p->xruns;
|
||||
p->xruns = xruns;
|
||||
if (!p->started || underrun) { /* (re)start: build the cushion, then play from here */
|
||||
int64_t have = level > 0 ? level : 0;
|
||||
s.silence = have < p->preroll ? p->preroll - have : 0;
|
||||
p->silence_frames += s.silence;
|
||||
int64_t due = p->started ? a32_pace_due(p) : now; /* an underrun while the guest is on time: keep its pace */
|
||||
p->frames = 0;
|
||||
p->started = 1;
|
||||
a32_pace_anchor(p, due > now ? due : now);
|
||||
s.wait_ns = due > now ? due - now : 0;
|
||||
return s;
|
||||
}
|
||||
int64_t wait = a32_pace_due(p) - now;
|
||||
if (level < 0) { /* no level: the clock alone */
|
||||
if (-wait > p->stall_ns) a32_pace_anchor(p, now);
|
||||
s.wait_ns = wait > 0 ? wait : 0;
|
||||
return s;
|
||||
}
|
||||
/* the level expected when this buffer is due */
|
||||
int64_t proj = wait > 0 ? level - wait * p->rate / A32_PACE_NS : level;
|
||||
if (proj < p->low) { /* about to run dry (the guest stalled): rebuild the cushion, play from here */
|
||||
s.silence = level < p->preroll ? p->preroll - level : 0;
|
||||
p->silence_frames += s.silence;
|
||||
a32_pace_anchor(p, now);
|
||||
return s;
|
||||
}
|
||||
if (wait <= 0) { /* late: catch up only while the device is short of data, never in a burst otherwise */
|
||||
if (level > p->preroll || -wait > p->stall_ns) a32_pace_anchor(p, now);
|
||||
return s;
|
||||
}
|
||||
int64_t err = proj - p->preroll;
|
||||
if (err > p->slack) { /* the device fell behind our clock (or hasn't started yet): wait for it */
|
||||
p->anchor_ns += err * A32_PACE_NS / p->rate;
|
||||
p->shifts++;
|
||||
} else { /* clock drift either way: a gentle nudge (the level jumps by the device's bursts; this averages) */
|
||||
p->anchor_ns += err * A32_PACE_NS / p->rate / 64;
|
||||
}
|
||||
wait = a32_pace_due(p) - now;
|
||||
s.wait_ns = wait > 0 ? wait : 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
static inline void a32_pace_after(a32_pace *p, int frames)
|
||||
{
|
||||
p->frames += frames;
|
||||
p->checked = 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -29,10 +29,19 @@ static int fdm_view_of(const XrCompositionLayerProjectionView *pv, int gles, fdm
|
||||
|
||||
// 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));
|
||||
// Settings fdm_bin_w/fdm_bin_h (per game): the bin size Turnip uses for the eye passes, for when the layer's own
|
||||
// computation of it (centre.h) is wrong or undecided.
|
||||
extern void fp_fdm_set_bin(uint32_t width, uint32_t height) __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;
|
||||
static int bin_sent;
|
||||
if (!bin_sent && fdm_bin_w && fdm_bin_h && fp_fdm_set_bin) {
|
||||
fp_fdm_set_bin(fdm_bin_w, fdm_bin_h);
|
||||
LOG("fdm views: bin size %ux%u from fdm_bin_w/fdm_bin_h", fdm_bin_w, fdm_bin_h);
|
||||
}
|
||||
bin_sent = 1;
|
||||
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)
|
||||
|
||||
@@ -138,6 +138,7 @@ static int swapchain_fix = 1;
|
||||
static int cube_quads = 1;
|
||||
static int cube_standin = 1; // cube swapchains the runtime refuses: served by the adapter (cube_standin.c)
|
||||
static int rect_clamp = 1;
|
||||
static unsigned fdm_bin_w, fdm_bin_h; // fdm_bin_w/_h: Turnip's bin size for the foveation centring layer (#394)
|
||||
static int fixed_resolution;
|
||||
static int swapchain_pool = 0;
|
||||
static int fb_color_space; // fb_color_space: offer XR_FB_color_space when the runtime lacks it (#336)
|
||||
@@ -222,6 +223,8 @@ static void read_settings(const char *path) {
|
||||
if (sscanf(line, "cube_standin=%f", &value) == 1) cube_standin = value != 0;
|
||||
if (sscanf(line, "fixed_resolution=%f", &value) == 1) fixed_resolution = value != 0;
|
||||
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
|
||||
if (sscanf(line, "fdm_bin_w=%f", &value) == 1) fdm_bin_w = value > 0 && value <= 4096 ? (unsigned)value : 0;
|
||||
if (sscanf(line, "fdm_bin_h=%f", &value) == 1) fdm_bin_h = value > 0 && value <= 4096 ? (unsigned)value : 0;
|
||||
if (sscanf(line, "swapchain_pool=%f", &value) == 1) swapchain_pool = value != 0;
|
||||
if (sscanf(line, "fb_color_space=%f", &value) == 1) fb_color_space = value != 0;
|
||||
if (sscanf(line, "action_dedup=%f", &value) == 1) action_dedup = value != 0;
|
||||
|
||||
+661
-38
@@ -17,8 +17,21 @@
|
||||
// (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).
|
||||
// FRAMEPORT_FDM_DIAG=1 (as LEPTON_ENV_, off by default) logs, under the same tag, every view's offset as handed down
|
||||
// and which pipelines bound inside a foveated pass would make Turnip fall back to one shared viewport scale for all
|
||||
// views (pipeline without the density-map flag, or a pre-rasterization shader writing ViewportIndex): Turnip then
|
||||
// gives every view the finest fragment area of all views in each bin (tu_calc_frag_area, rp.shared_viewport), so
|
||||
// each eye gets the union of both eyes' sharp areas (GitHub #394, In Death / The 7th Guest).
|
||||
//
|
||||
// Bins (#394, second round): Turnip renders each GMEM bin at the density the map has at the bin's centre, on a grid
|
||||
// anchored in map space, so the full-density area is the bins whose centre falls in Valve's sharp texels; their centre
|
||||
// is off the texels' centre by up to half a bin (In Death: -80,+32 px). The layer computes Turnip's bin size for each
|
||||
// foveated pass the way Mesa does (tu_bins.h; Adreno 750 only) and centres those bins on the eye; the per-game setting
|
||||
// fdm_bin_w/fdm_bin_h (FrameBridge -> fp_fdm_set_bin) override it. Undecided (another GPU, an unmodelled pass): the texels'
|
||||
// centre as before.
|
||||
#define CENTRE_NAME "VK_LAYER_FRAMEPORT_fdm_centre"
|
||||
#define FP_FDM_MAX_VIEWS 4
|
||||
#include "tu_bins.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t width, height, layer; // swapchain image size, array layer
|
||||
@@ -32,6 +45,13 @@ 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_fixed; // FramePort's Fixed mode (FDM_DEBUG had disable_offsets): ignore Valve's gaze
|
||||
static int c_disabled; // FRAMEPORT_FDM_CENTRE=0
|
||||
static int c_diag; // FRAMEPORT_FDM_DIAG=1: offsets and shared-viewport causes in the log
|
||||
static const tb_gpu *c_gpu; // Turnip on a GPU tu_bins.h knows: bin sizes can be computed
|
||||
static uint32_t c_bin_w, c_bin_h; // fdm_bin_w/fdm_bin_h (per game, from FrameBridge): the bin size, not computed
|
||||
|
||||
EXPORT void fp_fdm_set_bin(uint32_t width, uint32_t height) {
|
||||
c_bin_w = width <= 4096 ? width : 0, c_bin_h = height <= 4096 ? height : 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;
|
||||
@@ -48,8 +68,8 @@ static const float c_preset[4][4] = {{0, 0, 0, 0}, {.364f, .367f, .295f, .286f},
|
||||
|
||||
// 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;
|
||||
// Valve's sharp texels in framebuffer pixels: [lo, hi)
|
||||
static void c_sharp_range(uint32_t fb, uint32_t texels, int axis, uint32_t *lo_px, uint32_t *hi_px) {
|
||||
uint32_t ratio = fb / texels, shift = 0;
|
||||
while ((1u << shift) < ratio) ++shift;
|
||||
if (shift < 5) shift = 5;
|
||||
@@ -58,7 +78,22 @@ static float c_valve_centre(uint32_t fb, uint32_t texels, int axis) {
|
||||
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);
|
||||
*lo_px = (uint32_t)first << shift, *hi_px = (uint32_t)(last + 1) << shift;
|
||||
}
|
||||
static float c_valve_centre(uint32_t fb, uint32_t texels, int axis) {
|
||||
if (!texels) return fb / 2.0f;
|
||||
uint32_t lo, hi;
|
||||
c_sharp_range(fb, texels, axis, &lo, &hi);
|
||||
return (float)(lo + hi) / 2.0f;
|
||||
}
|
||||
// The centre of the bins Turnip renders at full density (bin: Turnip's bin size along the axis, 0 = unknown); the
|
||||
// texels' centre when unknown or no bin centre falls inside the sharp texels.
|
||||
static float c_sharp_centre(uint32_t fb, uint32_t texels, int axis, uint32_t bin) {
|
||||
if (!texels || !bin) return c_valve_centre(fb, texels, axis);
|
||||
uint32_t lo, hi;
|
||||
c_sharp_range(fb, texels, axis, &lo, &hi);
|
||||
float c = tb_bins_centre(lo, hi, bin);
|
||||
return c >= 0 ? c : (float)(lo + hi) / 2.0f;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- object tracking (handles below Valve's layer)
|
||||
@@ -71,12 +106,12 @@ static int c_obj_lock;
|
||||
// functions in vkCreateDevice.
|
||||
static int c_dev_lock;
|
||||
|
||||
static c_slot *c_find(c_slot *t, uint64_t key, int create) {
|
||||
static c_slot *c_find_n(c_slot *t, uint32_t slots, uint64_t key, int create) {
|
||||
if (!key) return NULL;
|
||||
uint32_t h = (uint32_t)((key * 0x9E3779B97F4A7C15ull) >> 53) & (C_SLOTS - 1);
|
||||
uint32_t h = (uint32_t)((key * 0x9E3779B97F4A7C15ull) >> 40) & (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)];
|
||||
for (uint32_t i = 0; i < slots; ++i) {
|
||||
c_slot *s = &t[(h + i) & (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;
|
||||
@@ -86,6 +121,7 @@ static c_slot *c_find(c_slot *t, uint64_t key, int create) {
|
||||
free_slot->key = key;
|
||||
return free_slot;
|
||||
}
|
||||
static c_slot *c_find(c_slot *t, uint64_t key, int create) { return c_find_n(t, C_SLOTS, key, create); }
|
||||
static void c_forget(c_slot *t, uint64_t key) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(t, key, 0);
|
||||
@@ -116,6 +152,14 @@ typedef struct {
|
||||
PFN_vkCmdBeginRendering CmdBeginRendering, CmdBeginRenderingKHR;
|
||||
PFN_vkCmdEndRendering CmdEndRendering, CmdEndRenderingKHR;
|
||||
PFN_fp_EndRendering2 CmdEndRendering2EXT, CmdEndRendering2KHR;
|
||||
// FRAMEPORT_FDM_DIAG only
|
||||
PFN_vkCreateShaderModule CreateShaderModule;
|
||||
PFN_vkDestroyShaderModule DestroyShaderModule;
|
||||
PFN_vkCreateGraphicsPipelines CreateGraphicsPipelines;
|
||||
PFN_vkDestroyPipeline DestroyPipeline;
|
||||
PFN_vkCmdBindPipeline CmdBindPipeline;
|
||||
PFN_vkBeginCommandBuffer BeginCommandBuffer;
|
||||
PFN_vkCmdExecuteCommands CmdExecuteCommands;
|
||||
} c_device;
|
||||
static struct { void *key; VkInstance instance; PFN_vkGetInstanceProcAddr gipa; } c_instances[MAX_HANDLES];
|
||||
static c_device c_devices[MAX_HANDLES];
|
||||
@@ -147,6 +191,31 @@ static const VkBaseInStructure *c_chain(const void *next, VkStructureType type)
|
||||
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;
|
||||
|
||||
// Render targets (colour/depth images and their views), for Turnip's bin size (tu_bins.h): image a = width | height << 32,
|
||||
// b = samples | layers << 16 | format << 32, c = flags | usage << 32; view: the same with the view's format.
|
||||
#define C_RT_SLOTS 8192
|
||||
static c_slot c_rtimgs[C_RT_SLOTS], c_rtviews[C_RT_SLOTS];
|
||||
#define RT_USAGE (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
|
||||
|
||||
static int c_is_depth(VkFormat f) {
|
||||
return f == VK_FORMAT_D16_UNORM || f == VK_FORMAT_X8_D24_UNORM_PACK32 || f == VK_FORMAT_D32_SFLOAT ||
|
||||
f == VK_FORMAT_D16_UNORM_S8_UINT || f == VK_FORMAT_D24_UNORM_S8_UINT || f == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
}
|
||||
|
||||
// The tile-size limit a depth image with the density-map-offset flag sets (Turnip's max_tile_*_constraint_fdm)
|
||||
static void c_image_constraint(const c_slot *v, uint32_t *max_w, uint32_t *max_h) {
|
||||
uint32_t flags = (uint32_t)v->c, usage = (uint32_t)(v->c >> 32);
|
||||
VkFormat format = (VkFormat)(uint32_t)(v->b >> 32);
|
||||
if (!c_gpu || !(flags & FDM_OFFSET_FLAG) || !(usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) ||
|
||||
!c_is_depth(format))
|
||||
return;
|
||||
uint32_t w, h;
|
||||
tb_lrz_constraint(c_gpu, (uint32_t)v->a, (uint32_t)(v->a >> 32), (uint32_t)v->b & 0xffff,
|
||||
(uint32_t)(v->b >> 16) & 0xffff, &w, &h);
|
||||
if (w < *max_w) *max_w = w;
|
||||
if (h < *max_h) *max_h = h;
|
||||
}
|
||||
|
||||
// Images are never changed here: Valve flags them for offsets itself (all but external-memory ones) when its offsets
|
||||
// are on. Flagging more (imported Zink/AHardwareBuffer images, as a first version did for Fixed) crashed Turnip.
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateImage(VkDevice device, const VkImageCreateInfo *ci,
|
||||
@@ -162,10 +231,25 @@ static VKAPI_ATTR VkResult VKAPI_CALL c_CreateImage(VkDevice device, const VkIma
|
||||
LOG("fdm_centre: density map %ux%u[%u] flags 0x%x", ci->extent.width, ci->extent.height, ci->arrayLayers,
|
||||
ci->flags);
|
||||
}
|
||||
if (r == VK_SUCCESS && c_gpu && (ci->usage & RT_USAGE)) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find_n(c_rtimgs, C_RT_SLOTS, H(*image), 1);
|
||||
if (s)
|
||||
s->a = ci->extent.width | (uint64_t)ci->extent.height << 32,
|
||||
s->b = (ci->samples & 0xffff) | (uint64_t)(ci->arrayLayers & 0xffff) << 16 | (uint64_t)ci->format << 32,
|
||||
s->c = ci->flags | (uint64_t)ci->usage << 32;
|
||||
drop_lock(&c_obj_lock);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_DestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks *a) {
|
||||
c_forget(c_images, H(image));
|
||||
if (c_gpu) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find_n(c_rtimgs, C_RT_SLOTS, H(image), 0);
|
||||
if (s) s->key = ~0ull;
|
||||
drop_lock(&c_obj_lock);
|
||||
}
|
||||
c_dev(device)->DestroyImage(device, image, a);
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateImageView(VkDevice device, const VkImageViewCreateInfo *ci,
|
||||
@@ -178,22 +262,124 @@ static VKAPI_ATTR VkResult VKAPI_CALL c_CreateImageView(VkDevice device, const V
|
||||
// d = first array layer | layer count << 32: Valve gives a one-eye pass the map layer of that eye
|
||||
if (s) s->a = img->a, s->b = img->b, s->c = img->c,
|
||||
s->d = ci->subresourceRange.baseArrayLayer | (uint64_t)ci->subresourceRange.layerCount << 32;
|
||||
c_slot *rt = c_gpu ? c_find_n(c_rtimgs, C_RT_SLOTS, H(ci->image), 0) : NULL;
|
||||
c_slot *v = rt ? c_find_n(c_rtviews, C_RT_SLOTS, H(*view), 1) : NULL;
|
||||
if (v) v->a = rt->a, v->b = (rt->b & 0xffffffffull) | (uint64_t)ci->format << 32, v->c = rt->c, v->d = 0;
|
||||
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));
|
||||
if (c_gpu) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find_n(c_rtviews, C_RT_SLOTS, H(view), 0);
|
||||
if (s) s->key = ~0ull;
|
||||
drop_lock(&c_obj_lock);
|
||||
}
|
||||
c_dev(device)->DestroyImageView(device, view, a);
|
||||
}
|
||||
// render pass: a = map attachment index, b = the subpasses' view masks ORed (multiview)
|
||||
static void c_note_rp(const void *next, VkRenderPass rp, uint32_t view_mask) {
|
||||
|
||||
// A render pass as Turnip's GMEM setup sees it (tu_render_pass_gmem_config); 0: not modelled (no bin size then)
|
||||
typedef struct {
|
||||
tb_att atts[TB_MAX_ATTS];
|
||||
int known[TB_MAX_ATTS]; // format modelled
|
||||
uint32_t n, depth_mask; // depth_mask: attachments with a depth format
|
||||
int bad;
|
||||
} c_rp_model;
|
||||
static void c_model_init(c_rp_model *m, uint32_t n) {
|
||||
memset(m, 0, sizeof(*m));
|
||||
m->n = n;
|
||||
if (n > TB_MAX_ATTS) m->bad = 1;
|
||||
}
|
||||
static void c_model_att(c_rp_model *m, uint32_t i, VkFormat format, VkSampleCountFlagBits samples) {
|
||||
if (i >= m->n || m->bad) return;
|
||||
m->known[i] = tb_att_of(format, (uint32_t)samples, &m->atts[i]);
|
||||
m->atts[i].first = ~0u, m->atts[i].last = 0;
|
||||
if (c_is_depth(format)) m->depth_mask |= 1u << i;
|
||||
}
|
||||
static void c_model_range(c_rp_model *m, uint32_t a, uint32_t subpass, int gmem) {
|
||||
if (a == VK_ATTACHMENT_UNUSED || m->bad) return;
|
||||
if (a >= m->n) { m->bad = 1; return; }
|
||||
tb_att *t = &m->atts[a];
|
||||
if (subpass < t->first) t->first = subpass;
|
||||
if (subpass > t->last) t->last = subpass;
|
||||
if (gmem) {
|
||||
t->gmem = 1;
|
||||
if (!m->known[a]) m->bad = 1; // an attachment in GMEM with a format tu_bins.h doesn't know
|
||||
}
|
||||
}
|
||||
#define ST_MSRTSS ((VkStructureType)1000376002) // VkMultisampledRenderToSingleSampledInfoEXT
|
||||
#define ST_DS_RESOLVE ((VkStructureType)1000199001) // VkSubpassDescriptionDepthStencilResolve
|
||||
static int c_msrtss(const void *next) {
|
||||
const VkBaseInStructure *b = c_chain(next, ST_MSRTSS);
|
||||
return b && ((const struct { VkStructureType t; const void *n; VkBool32 on; } *)b)->on;
|
||||
}
|
||||
static void c_model2(const VkRenderPassCreateInfo2 *ci, c_rp_model *m) {
|
||||
c_model_init(m, ci->attachmentCount);
|
||||
for (uint32_t i = 0; i < ci->attachmentCount && !m->bad; ++i)
|
||||
c_model_att(m, i, ci->pAttachments[i].format, ci->pAttachments[i].samples);
|
||||
for (uint32_t i = 0; i < ci->subpassCount && !m->bad; ++i) {
|
||||
const VkSubpassDescription2 *sp = &ci->pSubpasses[i];
|
||||
if (c_msrtss(sp->pNext)) m->bad = 1;
|
||||
for (uint32_t j = 0; j < sp->inputAttachmentCount; ++j) c_model_range(m, sp->pInputAttachments[j].attachment, i, 0);
|
||||
for (uint32_t j = 0; j < sp->colorAttachmentCount; ++j) {
|
||||
c_model_range(m, sp->pColorAttachments[j].attachment, i, 1);
|
||||
if (sp->pResolveAttachments && sp->pResolveAttachments[j].attachment != VK_ATTACHMENT_UNUSED &&
|
||||
sp->pColorAttachments[j].attachment != VK_ATTACHMENT_UNUSED)
|
||||
c_model_range(m, sp->pResolveAttachments[j].attachment, i, 0);
|
||||
}
|
||||
if (sp->pDepthStencilAttachment) c_model_range(m, sp->pDepthStencilAttachment->attachment, i, 1);
|
||||
const VkSubpassDescriptionDepthStencilResolve *dr = (const void *)c_chain(sp->pNext, ST_DS_RESOLVE);
|
||||
if (dr && dr->pDepthStencilResolveAttachment && sp->pDepthStencilAttachment &&
|
||||
sp->pDepthStencilAttachment->attachment != VK_ATTACHMENT_UNUSED)
|
||||
c_model_range(m, dr->pDepthStencilResolveAttachment->attachment, i, 0);
|
||||
}
|
||||
}
|
||||
static void c_model1(const VkRenderPassCreateInfo *ci, c_rp_model *m) {
|
||||
c_model_init(m, ci->attachmentCount);
|
||||
for (uint32_t i = 0; i < ci->attachmentCount && !m->bad; ++i)
|
||||
c_model_att(m, i, ci->pAttachments[i].format, ci->pAttachments[i].samples);
|
||||
for (uint32_t i = 0; i < ci->subpassCount && !m->bad; ++i) {
|
||||
const VkSubpassDescription *sp = &ci->pSubpasses[i];
|
||||
for (uint32_t j = 0; j < sp->inputAttachmentCount; ++j) c_model_range(m, sp->pInputAttachments[j].attachment, i, 0);
|
||||
for (uint32_t j = 0; j < sp->colorAttachmentCount; ++j) {
|
||||
c_model_range(m, sp->pColorAttachments[j].attachment, i, 1);
|
||||
if (sp->pResolveAttachments && sp->pResolveAttachments[j].attachment != VK_ATTACHMENT_UNUSED &&
|
||||
sp->pColorAttachments[j].attachment != VK_ATTACHMENT_UNUSED)
|
||||
c_model_range(m, sp->pResolveAttachments[j].attachment, i, 0);
|
||||
}
|
||||
if (sp->pDepthStencilAttachment) c_model_range(m, sp->pDepthStencilAttachment->attachment, i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// render pass: a = map attachment index, b = the subpasses' view masks ORed (multiview), c = Turnip's GMEM budget
|
||||
// (gmem_pixels | tile_align_w << 32 | min_cpp << 48, 0: not modelled), d = depth-format attachments | samples of
|
||||
// attachment 0 << 32 (Turnip takes those for an imageless framebuffer's depth constraint)
|
||||
static void c_note_rp(const void *next, VkRenderPass rp, uint32_t view_mask, const c_rp_model *m,
|
||||
VkSampleCountFlagBits samples0) {
|
||||
const VkRenderPassFragmentDensityMapCreateInfoEXT *f = (const void *)c_chain(next, ST_FDM_RP_INFO);
|
||||
if (!f || f->fragmentDensityMapAttachment.attachment == VK_ATTACHMENT_UNUSED) return;
|
||||
tb_pass p = {0, 0, 0};
|
||||
int ok = c_gpu && m && !m->bad && tb_pass_config(c_gpu, m->atts, m->n, &p);
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(c_rps, H(rp), 1);
|
||||
if (s) s->a = f->fragmentDensityMapAttachment.attachment, s->b = view_mask;
|
||||
if (s) {
|
||||
s->a = f->fragmentDensityMapAttachment.attachment, s->b = view_mask;
|
||||
s->c = ok ? (p.gmem_pixels | (uint64_t)(p.tile_align_w & 0xffff) << 32 |
|
||||
(uint64_t)(p.min_cpp > 0xffff ? 0 : p.min_cpp) << 48) : 0;
|
||||
s->d = (m ? m->depth_mask : 0) | (uint64_t)samples0 << 32;
|
||||
}
|
||||
drop_lock(&c_obj_lock);
|
||||
static int logged;
|
||||
if (c_gpu && logged++ < 8) {
|
||||
char atts[160] = "";
|
||||
for (uint32_t i = 0; m && i < m->n && i < 8; ++i)
|
||||
snprintf(atts + strlen(atts), sizeof(atts) - strlen(atts), "%s%u%s", i ? "," : "", m->atts[i].cpp,
|
||||
m->atts[i].gmem ? "" : "-");
|
||||
LOG("fdm_centre: foveated render pass: attachments cpp %s (- = not in GMEM) -> %sgmem_pixels %u, tile "
|
||||
"align %u", atts, ok ? "" : "NOT MODELLED, ", p.gmem_pixels, p.tile_align_w);
|
||||
}
|
||||
}
|
||||
static uint32_t c_masks2(const VkRenderPassCreateInfo2 *ci) {
|
||||
uint32_t m = 0;
|
||||
@@ -206,30 +392,58 @@ static uint32_t c_masks1(const VkRenderPassCreateInfo *ci) {
|
||||
for (uint32_t i = 0; mv && mv->pViewMasks && i < mv->subpassCount; ++i) m |= mv->pViewMasks[i];
|
||||
return m;
|
||||
}
|
||||
static void c_note_rp2(const VkRenderPassCreateInfo2 *ci, VkRenderPass rp) {
|
||||
if (!c_chain(ci->pNext, ST_FDM_RP_INFO)) return;
|
||||
c_rp_model m;
|
||||
c_model2(ci, &m);
|
||||
c_note_rp(ci->pNext, rp, c_masks2(ci), &m, ci->attachmentCount ? ci->pAttachments[0].samples : VK_SAMPLE_COUNT_1_BIT);
|
||||
}
|
||||
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, c_masks1(ci));
|
||||
if (r == VK_SUCCESS && c_chain(ci->pNext, ST_FDM_RP_INFO)) {
|
||||
c_rp_model m;
|
||||
c_model1(ci, &m);
|
||||
c_note_rp(ci->pNext, *rp, c_masks1(ci), &m, ci->attachmentCount ? ci->pAttachments[0].samples : VK_SAMPLE_COUNT_1_BIT);
|
||||
}
|
||||
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, c_masks2(ci));
|
||||
if (r == VK_SUCCESS) c_note_rp2(ci, *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, c_masks2(ci));
|
||||
if (r == VK_SUCCESS) c_note_rp2(ci, *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,
|
||||
// d = layers | render pass view mask << 32
|
||||
|
||||
static uint32_t c_last_bit(uint32_t mask) {
|
||||
uint32_t n = 0;
|
||||
while (mask >> n) ++n;
|
||||
return n;
|
||||
}
|
||||
// Turnip's bin size for a framebuffer of a modelled pass: width | height << 16, 0 when undecided
|
||||
static uint32_t c_bin_size(uint64_t pass_c, uint32_t fb_w, uint32_t fb_h, uint32_t layers, uint32_t max_w,
|
||||
uint32_t max_h) {
|
||||
if (!c_gpu || !pass_c) return 0;
|
||||
tb_pass p = {(uint32_t)pass_c, (uint32_t)(pass_c >> 32) & 0xffff, (uint32_t)(pass_c >> 48)};
|
||||
if (!p.min_cpp) p.min_cpp = UINT32_MAX;
|
||||
uint32_t w = 0, h = 0;
|
||||
if (!tb_tile_size(c_gpu, &p, fb_w, fb_h, layers, max_w, max_h, &w, &h) || w > 0xffff || h > 0xffff) return 0;
|
||||
return w | h << 16;
|
||||
}
|
||||
|
||||
#define ST_FB_ATTACHMENTS ((VkStructureType)1000108001) // VkFramebufferAttachmentsCreateInfo
|
||||
// framebuffer: a = width | height << 32, b = density map view (0: imageless), c = map attachment index + 1 | Turnip's
|
||||
// bin size << 32 (width | height << 16, 0: undecided), d = layers | render pass view mask << 32
|
||||
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);
|
||||
@@ -237,12 +451,46 @@ static VKAPI_ATTR VkResult VKAPI_CALL c_CreateFramebuffer(VkDevice device, const
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *rp = c_find(c_rps, H(ci->renderPass), 0);
|
||||
if (rp) {
|
||||
uint32_t bin = 0;
|
||||
if (rp->c) {
|
||||
// the first depth attachment with the offset flag limits the tile size (tu_CreateFramebuffer)
|
||||
uint32_t max_w = ~0u, max_h = ~0u;
|
||||
int known = 1;
|
||||
if (ci->flags & VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT) {
|
||||
const VkFramebufferAttachmentsCreateInfo *fa = (const void *)c_chain(ci->pNext, ST_FB_ATTACHMENTS);
|
||||
for (uint32_t i = 0; fa && i < fa->attachmentImageInfoCount; ++i) {
|
||||
const VkFramebufferAttachmentImageInfo *ii = &fa->pAttachmentImageInfos[i];
|
||||
if ((ii->flags & FDM_OFFSET_FLAG) && (ii->usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)) {
|
||||
// Turnip takes the pass's first attachment's sample count here
|
||||
tb_lrz_constraint(c_gpu, ii->width, ii->height, (uint32_t)(rp->d >> 32), ii->layerCount,
|
||||
&max_w, &max_h);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (uint32_t i = 0; ci->pAttachments && i < ci->attachmentCount; ++i) {
|
||||
c_slot *v = c_find_n(c_rtviews, C_RT_SLOTS, H(ci->pAttachments[i]), 0);
|
||||
if (!v) {
|
||||
if (i < 32 && (rp->d >> i & 1)) known = 0; // a depth attachment not seen created
|
||||
continue;
|
||||
}
|
||||
if (((uint32_t)v->c & FDM_OFFSET_FLAG) &&
|
||||
((uint32_t)(v->c >> 32) & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)) {
|
||||
c_image_constraint(v, &max_w, &max_h);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t layers = ci->layers, views = c_last_bit((uint32_t)rp->b);
|
||||
if (views > layers) layers = views;
|
||||
if (known) bin = c_bin_size(rp->c, ci->width, ci->height, layers, max_w, max_h);
|
||||
}
|
||||
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;
|
||||
s->c = (rp->a + 1) | (uint64_t)bin << 32;
|
||||
s->d = ci->layers | rp->b << 32;
|
||||
}
|
||||
}
|
||||
@@ -255,6 +503,316 @@ static VKAPI_ATTR void VKAPI_CALL c_DestroyFramebuffer(VkDevice device, VkFrameb
|
||||
c_dev(device)->DestroyFramebuffer(device, fb, a);
|
||||
}
|
||||
|
||||
// Dynamic rendering: Turnip builds the pass per vkCmdBeginRendering (tu_setup_dynamic_render_pass/_framebuffer):
|
||||
// colour attachments, then depth/stencil in GMEM; the framebuffer ends at the render area's end.
|
||||
static uint32_t c_dynamic_bin(const VkRenderingInfo *info) {
|
||||
if (!c_gpu || c_msrtss(info->pNext)) return 0;
|
||||
tb_att atts[TB_MAX_ATTS];
|
||||
uint32_t n = 0;
|
||||
uint32_t max_w = ~0u, max_h = ~0u;
|
||||
take_lock(&c_obj_lock);
|
||||
int ok = info->colorAttachmentCount < TB_MAX_ATTS;
|
||||
for (uint32_t i = 0; ok && i < info->colorAttachmentCount; ++i) {
|
||||
VkImageView view = info->pColorAttachments[i].imageView;
|
||||
if (!view) continue;
|
||||
c_slot *v = c_find_n(c_rtviews, C_RT_SLOTS, H(view), 0);
|
||||
ok = v && tb_att_of((VkFormat)(uint32_t)(v->b >> 32), (uint32_t)v->b & 0xffff, &atts[n]);
|
||||
if (ok) atts[n].gmem = 1, atts[n].first = atts[n].last = 0, ++n;
|
||||
}
|
||||
const VkRenderingAttachmentInfo *ds = info->pDepthAttachment && info->pDepthAttachment->imageView ?
|
||||
info->pDepthAttachment : info->pStencilAttachment && info->pStencilAttachment->imageView ?
|
||||
info->pStencilAttachment : NULL;
|
||||
if (ok && ds) {
|
||||
c_slot *v = c_find_n(c_rtviews, C_RT_SLOTS, H(ds->imageView), 0);
|
||||
ok = v && tb_att_of((VkFormat)(uint32_t)(v->b >> 32), (uint32_t)v->b & 0xffff, &atts[n]);
|
||||
if (ok) atts[n].gmem = 1, atts[n].first = atts[n].last = 0, ++n;
|
||||
if (ok && info->pDepthAttachment && info->pDepthAttachment->imageView) {
|
||||
c_slot *dv = c_find_n(c_rtviews, C_RT_SLOTS, H(info->pDepthAttachment->imageView), 0);
|
||||
if (dv) c_image_constraint(dv, &max_w, &max_h);
|
||||
}
|
||||
}
|
||||
drop_lock(&c_obj_lock);
|
||||
tb_pass p;
|
||||
if (!ok || !tb_pass_config(c_gpu, atts, n, &p)) return 0;
|
||||
uint32_t layers = info->viewMask ? 1 : info->layerCount, views = c_last_bit(info->viewMask);
|
||||
if (views > layers) layers = views;
|
||||
uint64_t pass_c = p.gmem_pixels | (uint64_t)(p.tile_align_w & 0xffff) << 32 |
|
||||
(uint64_t)(p.min_cpp > 0xffff ? 0 : p.min_cpp) << 48;
|
||||
return c_bin_size(pass_c, (uint32_t)(info->renderArea.offset.x + (int32_t)info->renderArea.extent.width),
|
||||
(uint32_t)(info->renderArea.offset.y + (int32_t)info->renderArea.extent.height), layers, max_w,
|
||||
max_h);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- diagnostics (FRAMEPORT_FDM_DIAG=1)
|
||||
//
|
||||
// Turnip scales each view's viewport by that view's fragment area only when the bound pipeline has
|
||||
// program.per_view_viewport: the pre-rasterization stage doesn't write ViewportIndex, and the fragment shader was
|
||||
// compiled for a density map (render pass with a density-map attachment at pipeline creation, or dynamic rendering
|
||||
// with VK_PIPELINE_CREATE_RENDERING_FRAGMENT_DENSITY_MAP_ATTACHMENT_BIT_EXT), unless the app enabled
|
||||
// multiviewPerViewViewports. One draw with another pipeline sets rp.shared_viewport for the whole pass: the bins
|
||||
// aren't shifted by the offsets any more, each view's density is sampled at bin centre - its offset, and every view
|
||||
// gets the smallest fragment area of all views (tu_calc_frag_area). These counters say whether that happens.
|
||||
|
||||
#define C_PIPE_SLOTS 16384
|
||||
#define CP_FDM 1u // the driver sees a density map for this pipeline (fs.has_fdm)
|
||||
#define CP_VP 2u // a pre-rasterization shader writes ViewportIndex
|
||||
#define CP_LAYER 4u // ... writes Layer
|
||||
#define CP_DYN 8u // dynamic rendering
|
||||
#define CP_LIB 16u // pipeline library or linked from libraries
|
||||
#define CP_NOFS 32u // no fragment shader in this (library) pipeline
|
||||
#define CP_SCANNED 64u // the shader modules were seen
|
||||
#define CP_RISKY(b) (!((b) & CP_FDM) || ((b) & CP_VP))
|
||||
static c_slot c_mods[C_SLOTS], c_pdiag[C_SLOTS], c_pipes[C_PIPE_SLOTS];
|
||||
static int c_app_per_view_viewports; // the app enabled multiviewPerViewViewports (QCOM)
|
||||
static uint64_t c_n_pipes, c_n_pipes_nofdm, c_n_pipes_vp, c_n_passes, c_n_shared, c_n_logged_pipes;
|
||||
|
||||
// Output ViewportIndex (bit 0) / Layer (bit 1) of a SPIR-V module, plus 1 << (8 + execution model) per entry point.
|
||||
static uint32_t c_spirv_outputs(const uint32_t *code, size_t words) {
|
||||
if (!code || words < 5 || code[0] != 0x07230203u) return 0;
|
||||
enum { MAXID = 32 };
|
||||
uint32_t ids[MAXID], idbits[MAXID], nids = 0; // variables decorated BuiltIn ViewportIndex/Layer
|
||||
uint32_t structs[MAXID], sbits[MAXID], nstructs = 0; // block types with such a member
|
||||
uint32_t ptrs[MAXID], pbits[MAXID], nptrs = 0; // Output pointers to those blocks
|
||||
uint32_t out = 0;
|
||||
for (size_t i = 5; i < words;) {
|
||||
uint32_t op = code[i] & 0xffff, wc = code[i] >> 16;
|
||||
if (!wc || i + wc > words) break;
|
||||
const uint32_t *w = code + i;
|
||||
if (op == 15 && wc >= 2 && w[1] < 16) out |= 1u << (8 + w[1]); // OpEntryPoint
|
||||
uint32_t bit = 0;
|
||||
if (op == 71 && wc >= 4 && w[2] == 11) bit = w[3] == 10 ? 1 : w[3] == 9 ? 2 : 0; // OpDecorate BuiltIn
|
||||
if (op == 72 && wc >= 5 && w[3] == 11) bit = w[4] == 10 ? 1 : w[4] == 9 ? 2 : 0; // OpMemberDecorate
|
||||
if (bit && op == 71 && nids < MAXID) ids[nids] = w[1], idbits[nids++] = bit;
|
||||
if (bit && op == 72) {
|
||||
uint32_t k = 0;
|
||||
while (k < nstructs && structs[k] != w[1]) ++k;
|
||||
if (k == nstructs && nstructs < MAXID) structs[nstructs] = w[1], sbits[nstructs++] = 0;
|
||||
if (k < nstructs) sbits[k] |= bit;
|
||||
}
|
||||
if (op == 32 && wc >= 4 && w[2] == 3) // OpTypePointer Output
|
||||
for (uint32_t k = 0; k < nstructs; ++k)
|
||||
if (structs[k] == w[3] && nptrs < MAXID) ptrs[nptrs] = w[1], pbits[nptrs++] = sbits[k];
|
||||
if (op == 59 && wc >= 4 && w[3] == 3) { // OpVariable Output
|
||||
for (uint32_t k = 0; k < nids; ++k)
|
||||
if (ids[k] == w[2]) out |= idbits[k];
|
||||
for (uint32_t k = 0; k < nptrs; ++k)
|
||||
if (ptrs[k] == w[1]) out |= pbits[k];
|
||||
}
|
||||
i += wc;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateShaderModule(VkDevice device, const VkShaderModuleCreateInfo *ci,
|
||||
const VkAllocationCallbacks *a, VkShaderModule *module) {
|
||||
VkResult r = c_dev(device)->CreateShaderModule(device, ci, a, module);
|
||||
if (r == VK_SUCCESS) {
|
||||
uint32_t bits = c_spirv_outputs(ci->pCode, ci->codeSize / 4);
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(c_mods, H(*module), 1);
|
||||
if (s) s->a = bits | 0x80000000u; // high bit: seen
|
||||
drop_lock(&c_obj_lock);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_DestroyShaderModule(VkDevice device, VkShaderModule m, const VkAllocationCallbacks *a) {
|
||||
c_forget(c_mods, H(m));
|
||||
c_dev(device)->DestroyShaderModule(device, m, a);
|
||||
}
|
||||
|
||||
#define ST_PIPELINE_FLAGS2 ((VkStructureType)1000470005) // VkPipelineCreateFlags2CreateInfo
|
||||
#define ST_PIPELINE_RENDERING ((VkStructureType)1000044002) // VkPipelineRenderingCreateInfo
|
||||
#define ST_PIPELINE_LIBRARY ((VkStructureType)1000290000) // VkPipelineLibraryCreateInfoKHR
|
||||
#define PIPE_FDM_FLAG 0x400000ull // VK_PIPELINE_CREATE(_2)_RENDERING_FRAGMENT_DENSITY_MAP_ATTACHMENT_BIT_EXT
|
||||
#define PIPE_LIBRARY_FLAG 0x800ull
|
||||
typedef struct { VkStructureType sType; const void *pNext; uint64_t flags; } c_flags2;
|
||||
typedef struct { VkStructureType sType; const void *pNext; uint32_t count; const VkPipeline *libs; } c_libinfo;
|
||||
|
||||
// What Turnip will decide for this pipeline: bits CP_*, view mask << 32
|
||||
static uint64_t c_pipeline_bits(const VkGraphicsPipelineCreateInfo *ci) {
|
||||
uint64_t flags = ci->flags;
|
||||
const c_flags2 *f2 = (const void *)c_chain(ci->pNext, ST_PIPELINE_FLAGS2);
|
||||
if (f2) flags = f2->flags;
|
||||
uint32_t bits = 0, mask = 0, fs = 0;
|
||||
take_lock(&c_obj_lock);
|
||||
if (ci->renderPass) {
|
||||
c_slot *rp = c_find(c_rps, H(ci->renderPass), 0); // only passes with a density-map attachment are noted
|
||||
if (rp) bits |= CP_FDM, mask = (uint32_t)rp->b;
|
||||
} else {
|
||||
bits |= CP_DYN;
|
||||
if (flags & PIPE_FDM_FLAG) bits |= CP_FDM;
|
||||
const VkPipelineRenderingCreateInfo *pr = (const void *)c_chain(ci->pNext, ST_PIPELINE_RENDERING);
|
||||
if (pr) mask = pr->viewMask;
|
||||
}
|
||||
for (uint32_t i = 0; ci->pStages && i < ci->stageCount; ++i) {
|
||||
const VkPipelineShaderStageCreateInfo *st = &ci->pStages[i];
|
||||
uint32_t out = 0;
|
||||
if (st->module) {
|
||||
c_slot *m = c_find(c_mods, H(st->module), 0);
|
||||
if (m) out = (uint32_t)m->a, bits |= CP_SCANNED;
|
||||
} else {
|
||||
const VkShaderModuleCreateInfo *inl =
|
||||
(const void *)c_chain(st->pNext, VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO);
|
||||
if (inl) out = c_spirv_outputs(inl->pCode, inl->codeSize / 4), bits |= CP_SCANNED;
|
||||
}
|
||||
if (st->stage == VK_SHADER_STAGE_FRAGMENT_BIT) fs = 1;
|
||||
else bits |= (out & 1 ? CP_VP : 0) | (out & 2 ? CP_LAYER : 0);
|
||||
}
|
||||
const c_libinfo *libs = (const void *)c_chain(ci->pNext, ST_PIPELINE_LIBRARY);
|
||||
for (uint32_t i = 0; libs && libs->libs && i < libs->count; ++i) {
|
||||
c_slot *l = c_find_n(c_pipes, C_PIPE_SLOTS, H(libs->libs[i]), 0);
|
||||
if (l) {
|
||||
bits |= (uint32_t)l->a & (CP_FDM | CP_VP | CP_LAYER | CP_SCANNED);
|
||||
if (!((uint32_t)l->a & CP_NOFS)) fs = 1;
|
||||
if (!mask) mask = (uint32_t)(l->a >> 32);
|
||||
}
|
||||
}
|
||||
drop_lock(&c_obj_lock);
|
||||
if (libs || (flags & PIPE_LIBRARY_FLAG)) bits |= CP_LIB;
|
||||
if (!fs) bits |= CP_NOFS;
|
||||
return bits | (uint64_t)mask << 32;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL c_CreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, uint32_t count,
|
||||
const VkGraphicsPipelineCreateInfo *ci,
|
||||
const VkAllocationCallbacks *a, VkPipeline *pipes) {
|
||||
VkResult r = c_dev(device)->CreateGraphicsPipelines(device, cache, count, ci, a, pipes);
|
||||
for (uint32_t i = 0; r >= 0 && i < count; ++i) {
|
||||
if (!pipes[i]) continue;
|
||||
uint64_t b = c_pipeline_bits(&ci[i]);
|
||||
uint32_t bits = (uint32_t)b;
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find_n(c_pipes, C_PIPE_SLOTS, H(pipes[i]), 1);
|
||||
if (s) s->a = b, s->b = H(ci[i].renderPass);
|
||||
++c_n_pipes;
|
||||
if (!(bits & CP_FDM)) ++c_n_pipes_nofdm;
|
||||
if (bits & CP_VP) ++c_n_pipes_vp;
|
||||
int log_it = CP_RISKY(bits) && !(bits & CP_NOFS) && c_n_logged_pipes < 24;
|
||||
if (log_it) ++c_n_logged_pipes;
|
||||
unsigned long long n = c_n_pipes, nofdm = c_n_pipes_nofdm, vp = c_n_pipes_vp;
|
||||
drop_lock(&c_obj_lock);
|
||||
if (log_it || !s)
|
||||
LOG("fdm_diag: pipeline %p (%s, render pass %p subpass %u, view mask 0x%x, flags 0x%x): %s%s%s%s%s%s "
|
||||
"[pipelines so far %llu, without density map %llu, writing ViewportIndex %llu]", (void *)pipes[i],
|
||||
bits & CP_DYN ? "dynamic rendering" : "render pass", (void *)ci[i].renderPass, ci[i].subpass,
|
||||
(uint32_t)(b >> 32), (uint32_t)ci[i].flags, bits & CP_FDM ? "density map" : "NO density map",
|
||||
bits & CP_VP ? ", writes ViewportIndex" : "", bits & CP_LAYER ? ", writes Layer" : "",
|
||||
bits & CP_LIB ? ", library" : "", bits & CP_SCANNED ? "" : ", shaders not seen",
|
||||
s ? "" : ", NOT TRACKED (table full)", n, nofdm, vp);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_DestroyPipeline(VkDevice device, VkPipeline p, const VkAllocationCallbacks *a) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find_n(c_pipes, C_PIPE_SLOTS, H(p), 0);
|
||||
if (s) s->key = ~0ull;
|
||||
drop_lock(&c_obj_lock);
|
||||
c_dev(device)->DestroyPipeline(device, p, a);
|
||||
}
|
||||
|
||||
// pass diagnostics per command buffer: a = graphics binds, b = binds Turnip shares one viewport scale for, c = binds
|
||||
// writing ViewportIndex, d = bits of the first such pipeline | 1 << 31 (low half); 1 << 63: a secondary begun inside
|
||||
// a foveated render pass, 1 << 62: the pass executed secondaries
|
||||
static void c_pdiag_start(VkCommandBuffer cmd, int secondary) {
|
||||
if (!c_diag) return;
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(c_pdiag, H(cmd), 1);
|
||||
if (s) s->a = s->b = s->c = 0, s->d = secondary ? 1ull << 63 : 0;
|
||||
drop_lock(&c_obj_lock);
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_CmdBindPipeline(VkCommandBuffer cmd, VkPipelineBindPoint point, VkPipeline p) {
|
||||
if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(c_pdiag, H(cmd), 0);
|
||||
if (s) {
|
||||
c_slot *pp = c_find_n(c_pipes, C_PIPE_SLOTS, H(p), 0);
|
||||
uint32_t bits = pp ? (uint32_t)pp->a : 0;
|
||||
++s->a;
|
||||
if (pp && CP_RISKY(bits) && !c_app_per_view_viewports) {
|
||||
++s->b;
|
||||
if (!(s->d & 0xffffffffull)) s->d |= bits | 0x80000000u;
|
||||
}
|
||||
if (bits & CP_VP) ++s->c;
|
||||
}
|
||||
drop_lock(&c_obj_lock);
|
||||
}
|
||||
c_dev(cmd)->CmdBindPipeline(cmd, point, p);
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL c_BeginCommandBuffer(VkCommandBuffer cmd, const VkCommandBufferBeginInfo *info) {
|
||||
c_forget(c_pdiag, H(cmd));
|
||||
const VkCommandBufferInheritanceInfo *inh = info ? info->pInheritanceInfo : NULL;
|
||||
if (inh && inh->renderPass && (info->flags & VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT)) {
|
||||
take_lock(&c_obj_lock);
|
||||
int fdm = c_find(c_rps, H(inh->renderPass), 0) != NULL;
|
||||
drop_lock(&c_obj_lock);
|
||||
if (fdm) c_pdiag_start(cmd, 1);
|
||||
}
|
||||
return c_dev(cmd)->BeginCommandBuffer(cmd, info);
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_CmdExecuteCommands(VkCommandBuffer cmd, uint32_t n, const VkCommandBuffer *subs) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(c_pdiag, H(cmd), 0);
|
||||
for (uint32_t i = 0; s && subs && i < n; ++i) {
|
||||
c_slot *sub = c_find(c_pdiag, H(subs[i]), 0);
|
||||
if (!sub) continue;
|
||||
s->a += sub->a, s->b += sub->b, s->c += sub->c;
|
||||
if (!(s->d & 0xffffffffull)) s->d |= sub->d & 0xffffffffull;
|
||||
s->d |= 1ull << 62;
|
||||
}
|
||||
drop_lock(&c_obj_lock);
|
||||
c_dev(cmd)->CmdExecuteCommands(cmd, n, subs);
|
||||
}
|
||||
|
||||
// The pass's summary at its end: the offsets as handed down and whether Turnip shares one viewport scale
|
||||
typedef struct {
|
||||
uint32_t fbw, fbh, mw, mh, first, map_layers, n, flagged;
|
||||
int32_t eye[FP_FDM_MAX_VIEWS]; // index into views[] per offset, -1: no single matching view
|
||||
fp_fdm_view views[FP_FDM_MAX_VIEWS];
|
||||
float vx, vy; // where the full-density area is centred without offsets
|
||||
uint32_t bin_w, bin_h; // Turnip's bins (0: unknown)
|
||||
int bin_source; // 0 unknown, 1 computed, 2 fdm_bin_w/fdm_bin_h
|
||||
} c_pass_info;
|
||||
static void c_diag_pass(VkCommandBuffer cmd, const c_pass_info *pi, uint32_t in_count, const VkOffset2D *in,
|
||||
const VkOffset2D *out, uint32_t handed) {
|
||||
take_lock(&c_obj_lock);
|
||||
c_slot *s = c_find(c_pdiag, H(cmd), 0);
|
||||
uint64_t binds = s ? s->a : 0, shared = s ? s->b : 0, vp = s ? s->c : 0, d = s ? s->d : 0;
|
||||
if (s) s->key = ~0ull;
|
||||
uint64_t pass_no = ++c_n_passes;
|
||||
if (shared) ++c_n_shared;
|
||||
unsigned long long total_shared = c_n_shared, pipes = c_n_pipes, nofdm = c_n_pipes_nofdm, vpw = c_n_pipes_vp;
|
||||
drop_lock(&c_obj_lock);
|
||||
if (!(pass_no <= 40 || pass_no % 1000 == 0 || (shared && total_shared <= 20))) return;
|
||||
char text[640];
|
||||
int len = snprintf(text, sizeof(text), "fdm_diag: pass #%llu %ux%u map %ux%u layer %u+%u%s, bins %ux%u, sharp centre %.0f,%.0f: "
|
||||
"%llu pipeline bind(s)%s, %llu Turnip shares one viewport for%s (first: 0x%x), %llu writing "
|
||||
"ViewportIndex -> %s [%llu of %llu passes shared; pipelines created %llu, without density map "
|
||||
"%llu, writing ViewportIndex %llu]; offsets in %u:",
|
||||
(unsigned long long)pass_no, pi->fbw, pi->fbh, pi->mw, pi->mh, pi->first, pi->map_layers,
|
||||
pi->flagged ? "" : " (map without offset flag)", pi->bin_w, pi->bin_h, pi->vx, pi->vy,
|
||||
(unsigned long long)binds,
|
||||
d >> 62 ? " incl. secondaries" : "", (unsigned long long)shared,
|
||||
c_app_per_view_viewports ? " (app enabled per-view viewports)" : "", (uint32_t)d & 0x7fffffffu,
|
||||
(unsigned long long)vp, shared ? "SHARED VIEWPORT (each view gets all views' sharp bins)"
|
||||
: "per-view scale", total_shared,
|
||||
(unsigned long long)pass_no, pipes, nofdm, vpw, in_count);
|
||||
for (uint32_t v = 0; in && v < pi->n && v < FP_FDM_MAX_VIEWS && len > 0 && len < (int)sizeof(text); ++v)
|
||||
len += snprintf(text + len, sizeof(text) - (size_t)len, " %d,%d", in[v].x, in[v].y);
|
||||
if (len > 0 && len < (int)sizeof(text))
|
||||
len += snprintf(text + len, sizeof(text) - (size_t)len, "; handed down %u:", handed);
|
||||
for (uint32_t v = 0; v < handed && v < FP_FDM_MAX_VIEWS && len > 0 && len < (int)sizeof(text); ++v) {
|
||||
const fp_fdm_view *e = pi->eye[v] >= 0 ? &pi->views[pi->eye[v]] : NULL;
|
||||
if (e)
|
||||
len += snprintf(text + len, sizeof(text) - (size_t)len,
|
||||
" v%u %d,%d (layer %u rect %d,%d %dx%d centre %.0f,%.0f -> sharp at %.0f,%.0f)", v,
|
||||
out[v].x, out[v].y, e->layer, e->x, e->y, e->w, e->h, e->cx, e->cy, pi->vx + out[v].x,
|
||||
pi->vy + out[v].y);
|
||||
else
|
||||
len += snprintf(text + len, sizeof(text) - (size_t)len, " v%u %d,%d (no view)", v, out[v].x, out[v].y);
|
||||
}
|
||||
LOG("%s", text);
|
||||
}
|
||||
|
||||
// The number of density-map offsets Turnip reads at the end of a pass, whatever count it is given
|
||||
// (tu_fdm_num_layers): the multiview view count, else the framebuffer's layers (per-layer maps) or 1.
|
||||
static uint32_t c_pass_views(uint32_t view_mask, uint32_t layers) {
|
||||
@@ -266,19 +824,21 @@ static uint32_t c_pass_views(uint32_t view_mask, uint32_t layers) {
|
||||
return layers ? layers : 1;
|
||||
}
|
||||
|
||||
// command buffer state: a = framebuffer width | height << 32, b = map width | height << 32 (0: no map), c = map flags,
|
||||
// d = offsets Turnip reads | first map layer << 16 | map view's layer count << 32
|
||||
static void c_begin(VkCommandBuffer cmd, uint32_t fbw, uint32_t fbh, VkImageView map, uint32_t views) {
|
||||
// command buffer state: a = framebuffer width | height << 32, b = map width | height << 32 (0: no map), c = map flags
|
||||
// | Turnip's bin size << 32 (width | height << 16, 0: undecided), d = offsets Turnip reads | first map layer << 16 |
|
||||
// map view's layer count << 32
|
||||
static void c_begin(VkCommandBuffer cmd, uint32_t fbw, uint32_t fbh, VkImageView map, uint32_t views, uint32_t bin) {
|
||||
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;
|
||||
s->c = iv ? ((iv->c & 0xffffffffull) | (uint64_t)bin << 32) : 0;
|
||||
s->d = iv ? ((views & 0xffff) | (iv->d & 0xffff) << 16 | (iv->d >> 32) << 32) : 0;
|
||||
}
|
||||
drop_lock(&c_obj_lock);
|
||||
if (iv) c_pdiag_start(cmd, 0);
|
||||
}
|
||||
static void c_begin_rp(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info) {
|
||||
if (c_disabled || !info) return;
|
||||
@@ -286,12 +846,12 @@ static void c_begin_rp(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info) {
|
||||
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, lm = fb ? fb->d : 0;
|
||||
drop_lock(&c_obj_lock);
|
||||
if (fb && !view && index) { // imageless framebuffer: the views come with the begin
|
||||
if (fb && !view && (uint32_t)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]);
|
||||
if (ab && (uint32_t)index - 1 < ab->attachmentCount) view = H(ab->pAttachments[(uint32_t)index - 1]);
|
||||
}
|
||||
c_begin(cmd, (uint32_t)size, (uint32_t)(size >> 32), (VkImageView)(uintptr_t)view,
|
||||
c_pass_views((uint32_t)(lm >> 32), (uint32_t)lm));
|
||||
c_pass_views((uint32_t)(lm >> 32), (uint32_t)lm), (uint32_t)(index >> 32));
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRenderPass(VkCommandBuffer cmd, const VkRenderPassBeginInfo *info,
|
||||
VkSubpassContents contents) {
|
||||
@@ -314,7 +874,7 @@ static void c_begin_rendering(VkCommandBuffer cmd, const VkRenderingInfo *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,
|
||||
c_pass_views(info->viewMask, info->layerCount));
|
||||
c_pass_views(info->viewMask, info->layerCount), f && f->imageView ? c_dynamic_bin(info) : 0);
|
||||
}
|
||||
static VKAPI_ATTR void VKAPI_CALL c_CmdBeginRendering(VkCommandBuffer cmd, const VkRenderingInfo *info) {
|
||||
c_begin_rendering(cmd, info);
|
||||
@@ -335,24 +895,31 @@ static int c_round(float v, uint32_t gran) {
|
||||
// fragmentDensityOffsetCount says. Valve's layer computes one per view into its array but declares count 1 for a
|
||||
// two-view pass, so with Valve alone Turnip reads the right eye's offset beyond the count: in[v] is read for every
|
||||
// view here too (the same memory the driver reads), and every entry handed down is written.
|
||||
static uint32_t c_offsets(VkCommandBuffer cmd, c_device *d, uint32_t in_count, const VkOffset2D *in, VkOffset2D *out) {
|
||||
static uint32_t c_offsets_for(VkCommandBuffer cmd, c_device *d, uint32_t in_count, const VkOffset2D *in,
|
||||
VkOffset2D *out, c_pass_info *pi) {
|
||||
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, shape = s ? s->d : 0;
|
||||
if (s) s->key = ~0ull; // one pass at a time per command buffer
|
||||
drop_lock(&c_obj_lock);
|
||||
// A map Valve made without the offset flag (its offsets off): Turnip ignores offsets, change nothing.
|
||||
if (!map || !(flags & FDM_OFFSET_FLAG)) return 0;
|
||||
uint32_t n = (uint32_t)(shape & 0xffff), first = (uint32_t)((shape >> 16) & 0xffff);
|
||||
uint32_t map_layers = (uint32_t)(shape >> 32);
|
||||
if (!n || n > FP_FDM_MAX_VIEWS) 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];
|
||||
// Turnip's bins: the per-game setting, else computed for this pass (0: undecided -> the texels' centre)
|
||||
uint32_t bin = (uint32_t)(flags >> 32), bin_w = bin & 0xffff, bin_h = bin >> 16;
|
||||
int bin_source = bin ? 1 : 0;
|
||||
if (c_bin_w && c_bin_h) bin_w = c_bin_w, bin_h = c_bin_h, bin_source = 2;
|
||||
float vx = c_sharp_centre(fbw, mw, 0, bin_w), vy = c_sharp_centre(fbh, mh, 1, bin_h);
|
||||
*pi = (c_pass_info){fbw, fbh, mw, mh, first, map_layers, map ? n : 0, (flags & FDM_OFFSET_FLAG) != 0,
|
||||
{-1, -1, -1, -1}, {{0}}, vx, vy, bin_w, bin_h, bin_source};
|
||||
fp_fdm_view *views = pi->views;
|
||||
take_lock(&c_lock);
|
||||
uint32_t nviews = c_view_count;
|
||||
memcpy(views, c_views, sizeof(views));
|
||||
memcpy(views, c_views, sizeof(c_views));
|
||||
drop_lock(&c_lock);
|
||||
float vx = c_valve_centre(fbw, mw, 0), vy = c_valve_centre(fbh, mh, 1);
|
||||
// A map Valve made without the offset flag (its offsets off): Turnip ignores offsets, change nothing.
|
||||
if (!map || !(flags & FDM_OFFSET_FLAG)) return 0;
|
||||
if (!n || n > FP_FDM_MAX_VIEWS) return 0;
|
||||
if (!in_count) in = NULL; // no offsets from Valve at all
|
||||
// Valve's gaze (Eye-tracked); all zero when it has none (no eye tracking): straight ahead then
|
||||
int gaze = 0;
|
||||
@@ -368,6 +935,7 @@ static uint32_t c_offsets(VkCommandBuffer cmd, c_device *d, uint32_t in_count, c
|
||||
for (uint32_t i = 0; i < nviews; ++i)
|
||||
if (views[i].width == fbw && views[i].height == fbh && views[i].layer == layer) e = &views[i], ++candidates;
|
||||
if (candidates != 1) continue; // e.g. both eyes side by side in one layer: one map can't serve both
|
||||
pi->eye[v] = (int32_t)(e - views);
|
||||
float dx = e->cx, dy = e->cy;
|
||||
if (gaze) {
|
||||
// Valve: gaze NDC * render-area half size from the swapchain centre; the gaze is in the eye's view
|
||||
@@ -380,12 +948,21 @@ static uint32_t c_offsets(VkCommandBuffer cmd, c_device *d, uint32_t in_count, c
|
||||
}
|
||||
static int logged;
|
||||
if (matched && logged++ < 8)
|
||||
LOG("fdm_centre: pass %ux%u map %ux%u layer %u+%u (preset %d, Valve centre %.0f,%.0f, %s): %u view(s), %u "
|
||||
"offset(s) from Valve %d,%d -> %d,%d%s", fbw, fbh, mw, mh, first, map_layers, c_level, vx, vy,
|
||||
c_fixed ? "Fixed" : gaze ? "gaze" : "no gaze", n, in_count, in_count ? in[0].x : 0, in_count ? in[0].y : 0,
|
||||
out[0].x, out[0].y, n > 1 ? " (+ more views)" : "");
|
||||
LOG("fdm_centre: pass %ux%u map %ux%u layer %u+%u (preset %d, bins %ux%u %s, sharp centre %.0f,%.0f, texel "
|
||||
"centre %.0f,%.0f, %s): %u view(s), %u offset(s) from Valve %d,%d -> %d,%d%s", fbw, fbh, mw, mh, first,
|
||||
map_layers, c_level, bin_w, bin_h, bin_source == 2 ? "from fdm_bin_w/_h" : bin_source ? "computed" : "unknown",
|
||||
vx, vy, c_valve_centre(fbw, mw, 0), c_valve_centre(fbh, mh, 1), c_fixed ? "Fixed" : gaze ? "gaze" : "no gaze",
|
||||
n, in_count, in_count ? in[0].x : 0, in_count ? in[0].y : 0, out[0].x, out[0].y,
|
||||
n > 1 ? " (+ more views)" : "");
|
||||
return matched ? n : 0;
|
||||
}
|
||||
static uint32_t c_offsets(VkCommandBuffer cmd, c_device *d, uint32_t in_count, const VkOffset2D *in, VkOffset2D *out) {
|
||||
c_pass_info pi;
|
||||
memset(&pi, 0, sizeof(pi));
|
||||
uint32_t n = c_offsets_for(cmd, d, in_count, in, out, &pi);
|
||||
if (c_diag && pi.n) c_diag_pass(cmd, &pi, in_count, in_count ? in : NULL, n ? out : in, n ? n : in_count);
|
||||
return n;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR void VKAPI_CALL c_end_rp2(VkCommandBuffer cmd, const VkSubpassEndInfo *info, int khr) {
|
||||
c_device *d = c_dev(cmd);
|
||||
@@ -528,15 +1105,48 @@ static VKAPI_ATTR VkResult VKAPI_CALL c_CreateDevice(VkPhysicalDevice pd, const
|
||||
C_GET(CmdBeginRendering, "vkCmdBeginRendering") C_GET(CmdBeginRenderingKHR, "vkCmdBeginRenderingKHR")
|
||||
C_GET(CmdEndRendering, "vkCmdEndRendering") C_GET(CmdEndRenderingKHR, "vkCmdEndRenderingKHR")
|
||||
C_GET(CmdEndRendering2EXT, "vkCmdEndRendering2EXT") C_GET(CmdEndRendering2KHR, "vkCmdEndRendering2KHR")
|
||||
C_GET(CreateShaderModule, "vkCreateShaderModule") C_GET(DestroyShaderModule, "vkDestroyShaderModule")
|
||||
C_GET(CreateGraphicsPipelines, "vkCreateGraphicsPipelines") C_GET(DestroyPipeline, "vkDestroyPipeline")
|
||||
C_GET(CmdBindPipeline, "vkCmdBindPipeline") C_GET(BeginCommandBuffer, "vkBeginCommandBuffer")
|
||||
C_GET(CmdExecuteCommands, "vkCmdExecuteCommands")
|
||||
#undef C_GET
|
||||
if (c_diag) {
|
||||
// VkPhysicalDeviceMultiviewPerViewViewportsFeaturesQCOM: with it Turnip keeps per-view viewports for every
|
||||
// pipeline that doesn't write ViewportIndex
|
||||
const VkBaseInStructure *pv = c_chain(ci->pNext, (VkStructureType)1000488000);
|
||||
c_app_per_view_viewports = pv && ((const struct { VkStructureType t; const void *n; VkBool32 on; } *)pv)->on;
|
||||
uint32_t fdm_feature = 0;
|
||||
const VkBaseInStructure *fd = c_chain(ci->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT);
|
||||
if (fd) fdm_feature = ((const VkPhysicalDeviceFragmentDensityMapFeaturesEXT *)fd)->fragmentDensityMap;
|
||||
int multiview = 0;
|
||||
const VkBaseInStructure *mv = c_chain(ci->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES);
|
||||
if (mv) multiview = ((const VkPhysicalDeviceMultiviewFeatures *)mv)->multiview;
|
||||
const VkBaseInStructure *v11 = c_chain(ci->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES);
|
||||
if (v11) multiview |= ((const VkPhysicalDeviceVulkan11Features *)v11)->multiview;
|
||||
char exts[512] = "";
|
||||
for (uint32_t i = 0; i < ci->enabledExtensionCount; ++i)
|
||||
if (strstr(ci->ppEnabledExtensionNames[i], "density") || strstr(ci->ppEnabledExtensionNames[i], "multiview") ||
|
||||
strstr(ci->ppEnabledExtensionNames[i], "dynamic_rendering") || strstr(ci->ppEnabledExtensionNames[i], "library"))
|
||||
snprintf(exts + strlen(exts), sizeof(exts) - strlen(exts), " %s", ci->ppEnabledExtensionNames[i]);
|
||||
LOG("fdm_diag: device %p: multiviewPerViewViewports %s, fragmentDensityMap feature %u, multiview %d, "
|
||||
"extensions:%s", (void *)*device, c_app_per_view_viewports ? "ENABLED by the app" : "off", fdm_feature,
|
||||
multiview, exts);
|
||||
}
|
||||
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}};
|
||||
VkPhysicalDeviceDriverProperties driver;
|
||||
memset(&driver, 0, sizeof(driver));
|
||||
driver.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES;
|
||||
struct { VkStructureType sType; void *pNext; VkExtent2D gran; } offset_props = {ST_FDM_OFFSET_PROPS, &driver, {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;
|
||||
// Turnip's bin size is only modelled for the Frame's GPU (tu_bins.h); anything else keeps the texels' centre
|
||||
const tb_gpu *gpu = driver.driverID == VK_DRIVER_ID_MESA_TURNIP &&
|
||||
strstr(p2.properties.deviceName, "Adreno (TM) 750") ? &TB_ADRENO_750 : NULL;
|
||||
if (gpu) c_gpu = gpu;
|
||||
take_lock(&c_dev_lock);
|
||||
int slot = 0;
|
||||
for (int i = 0; i < MAX_HANDLES; i++)
|
||||
@@ -545,8 +1155,10 @@ static VKAPI_ATTR VkResult VKAPI_CALL c_CreateDevice(VkPhysicalDevice pd, const
|
||||
drop_lock(&c_dev_lock);
|
||||
static int logged;
|
||||
if (logged++ < 4)
|
||||
LOG("fdm_centre: active on device %p (preset %d, %s, granularity %ux%u)", (void *)*device, c_level,
|
||||
c_fixed ? "Fixed: optical centre" : "gaze", d.gran.width, d.gran.height);
|
||||
LOG("fdm_centre: active on device %p (preset %d, %s, granularity %ux%u; %s, %s %s: bins %s)", (void *)*device,
|
||||
c_level, c_fixed ? "Fixed: optical centre" : "gaze", d.gran.width, d.gran.height,
|
||||
p2.properties.deviceName, driver.driverName, driver.driverInfo,
|
||||
gpu ? "computed as Turnip does" : "unknown (texel centre)");
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -599,6 +1211,15 @@ static PFN_vkVoidFunction c_own(const char *name, const c_device *d) {
|
||||
C_OWN("vkCmdEndRenderingKHR", c_CmdEndRenderingKHR, CmdEndRenderingKHR)
|
||||
C_OWN("vkCmdEndRendering2EXT", c_CmdEndRendering2EXT, CmdEndRendering2EXT)
|
||||
C_OWN("vkCmdEndRendering2KHR", c_CmdEndRendering2KHR, CmdEndRendering2KHR)
|
||||
if (c_diag) {
|
||||
C_OWN("vkCreateShaderModule", c_CreateShaderModule, CreateShaderModule)
|
||||
C_OWN("vkDestroyShaderModule", c_DestroyShaderModule, DestroyShaderModule)
|
||||
C_OWN("vkCreateGraphicsPipelines", c_CreateGraphicsPipelines, CreateGraphicsPipelines)
|
||||
C_OWN("vkDestroyPipeline", c_DestroyPipeline, DestroyPipeline)
|
||||
C_OWN("vkCmdBindPipeline", c_CmdBindPipeline, CmdBindPipeline)
|
||||
C_OWN("vkBeginCommandBuffer", c_BeginCommandBuffer, BeginCommandBuffer)
|
||||
C_OWN("vkCmdExecuteCommands", c_CmdExecuteCommands, CmdExecuteCommands)
|
||||
}
|
||||
#undef C_OWN
|
||||
return NULL;
|
||||
}
|
||||
@@ -628,6 +1249,8 @@ static void c_read_config(void) {
|
||||
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");
|
||||
const char *diag = getenv("FRAMEPORT_FDM_DIAG");
|
||||
c_diag = diag && !strcmp(diag, "1");
|
||||
}
|
||||
|
||||
// Fixed mode: let Valve run with its offsets on (it then flags the images it should, and passes per-view offsets
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Turnip's bin (GMEM tile) size for a foveated pass, as Mesa's freedreno Vulkan driver picks it (GitHub #394).
|
||||
//
|
||||
// Turnip reads the density map once per bin, at the bin's centre, on a grid that stays anchored at 0 in map space
|
||||
// when density-map offsets move the bins (tu_calc_frag_area / tu_bin_offset: a screen bin centred at s samples the map
|
||||
// at s - offset). So the full-density area is the set of bins whose map-space centre falls inside Valve's sharp texels,
|
||||
// not the texels themselves: its centre is off the texel centre by up to half a bin in each axis (In Death at
|
||||
// 2768x2768: 192x224 bins, -80,+32 px). The centring layer (centre.h) needs the bin size to put that set on the eye.
|
||||
//
|
||||
// Vulkan doesn't report it (Turnip has no VK_QCOM_tile_properties), so this follows Mesa's own code: SteamOS 0.5.x
|
||||
// ships Mesa 26.3.0-devel (git-e07916b45f, a Valve build: logged as the GL renderer). That commit isn't public; the
|
||||
// functions below follow upstream main of the 26.3 development window (82b868960e, 2026-09-23), where they didn't
|
||||
// change in substance between 26.2's branch point and 27.0's:
|
||||
// tu_render_pass_gmem_config() (tu_pass.cc): GMEM split between the pass's attachments -> gmem_pixels
|
||||
// tu_tiling_config_update_tile_layout() (tu_util.cc): the tile size with the fewest bins for that budget
|
||||
// fdl6_lrz_get_max_fdm_extra_size() (freedreno_lrz_layout.h): a depth image with the density-map-offset flag caps
|
||||
// the tile size so LRZ fast clears still cover it
|
||||
// tu_choose_gmem_layout() (tu_clear_blit.cc): passes with a density map use TU_GMEM_LAYOUT_AVOID_CCU
|
||||
// fd6_calc_gmem_cache_offsets() (fd6_gmem_cache.h) + freedreno_devices.py: the GPU's numbers (Adreno 750 only)
|
||||
// Anything this doesn't model (MSRTSS, custom resolves, unknown formats, other GPUs) returns 0: the layer then keeps
|
||||
// the texel centre (the behaviour before), and the per-game settings fdm_bin_w/fdm_bin_h can give the size by hand.
|
||||
|
||||
typedef struct {
|
||||
uint32_t gmem_bytes; // GMEM size (KGSL/MSM report it to Turnip)
|
||||
uint32_t usable_avoid_ccu; // GMEM left for attachments with TU_GMEM_LAYOUT_AVOID_CCU
|
||||
uint32_t tile_align_w, tile_align_h; // fd_dev_info tile_align_w/h
|
||||
uint32_t tile_max_w, tile_max_h; // fd_dev_info tile_max_w/h
|
||||
uint32_t gmem_align_w, gmem_align_h; // fd_dev_info gmem_align_w/h (A6xxGPUInfo: 16, 4)
|
||||
uint32_t lrz_fc_size; // fd_lrzfc_layout<A7XX>::FC_SIZE
|
||||
} tb_gpu;
|
||||
|
||||
// Adreno 750 (FD750, a7xx gen3): 3 MiB GMEM; gen3 keeps 6 x 0xc000 for the VPC attribute buffer at the end of GMEM
|
||||
// and the colour CCU (6 CCUs x 64 KiB / 8 in GMEM mode) below it.
|
||||
static const tb_gpu TB_ADRENO_750 = {3u << 20, (3u << 20) - 6 * 0xc000 - 6 * 64 * 1024 / 8, 96, 32, 2016, 2032,
|
||||
16, 4, 1024};
|
||||
|
||||
// One attachment of the pass as Turnip sees it
|
||||
typedef struct {
|
||||
uint32_t cpp; // bytes per pixel = format block size x samples (D32S8: 4 x samples, stencil separate)
|
||||
uint32_t samples;
|
||||
int d32s8; // VK_FORMAT_D32_SFLOAT_S8_UINT
|
||||
int gmem; // used as a colour or depth/stencil attachment (input-only and resolve targets aren't)
|
||||
uint32_t first, last; // subpass range
|
||||
} tb_att;
|
||||
|
||||
#define TB_MAX_ATTS 16
|
||||
|
||||
typedef struct { uint32_t gmem_pixels, tile_align_w, min_cpp; } tb_pass;
|
||||
|
||||
// tu_render_pass_gmem_config() for TU_GMEM_LAYOUT_AVOID_CCU. 0: impossible (Turnip renders in sysmem then).
|
||||
static int tb_pass_config(const tb_gpu *g, const tb_att *atts, uint32_t n, tb_pass *out) {
|
||||
if (!g || !n || n > TB_MAX_ATTS) return 0;
|
||||
uint32_t block_align_shift = 3, tile_align_w = g->tile_align_w;
|
||||
const uint32_t gmem_align = (1u << block_align_shift) * tile_align_w * g->tile_align_h;
|
||||
struct { uint32_t cpp, first, last; } allocs[2 * TB_MAX_ATTS];
|
||||
uint32_t nalloc = 0;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const tb_att *a = &atts[i];
|
||||
if (!a->gmem) continue;
|
||||
int cpp1 = a->cpp == 1;
|
||||
for (int part = 0; part < (a->d32s8 ? 2 : 1); ++part) {
|
||||
uint32_t cpp = part ? a->samples : a->cpp;
|
||||
// tu_gmem_alloc(): reuse an allocation free during this subpass range, the same cpp or the closest larger
|
||||
int found = -1;
|
||||
for (uint32_t k = 0; k < nalloc; ++k) {
|
||||
if (!(allocs[k].first > a->last || allocs[k].last < a->first)) continue;
|
||||
if (allocs[k].cpp == cpp) { found = (int)k; break; }
|
||||
if (allocs[k].cpp > cpp && (found < 0 || allocs[found].cpp > allocs[k].cpp)) found = (int)k;
|
||||
}
|
||||
if (found >= 0) {
|
||||
if (a->first < allocs[found].first) allocs[found].first = a->first;
|
||||
if (a->last > allocs[found].last) allocs[found].last = a->last;
|
||||
} else {
|
||||
allocs[nalloc].cpp = cpp, allocs[nalloc].first = a->first, allocs[nalloc].last = a->last;
|
||||
++nalloc;
|
||||
}
|
||||
}
|
||||
if (a->d32s8) cpp1 = a->samples == 1;
|
||||
if (cpp1 && tile_align_w % 64 != 0) tile_align_w *= 2, block_align_shift -= 1;
|
||||
}
|
||||
uint32_t cpp_total = 0, min_cpp = UINT32_MAX;
|
||||
for (uint32_t k = 0; k < nalloc; ++k) {
|
||||
cpp_total += allocs[k].cpp;
|
||||
if (allocs[k].cpp < min_cpp) min_cpp = allocs[k].cpp;
|
||||
}
|
||||
out->tile_align_w = tile_align_w, out->min_cpp = min_cpp;
|
||||
if (!cpp_total) {
|
||||
out->gmem_pixels = 1024 * 1024;
|
||||
return 1;
|
||||
}
|
||||
uint32_t gmem_blocks = g->usable_avoid_ccu / gmem_align, pixels = UINT32_MAX;
|
||||
for (uint32_t k = 0; k < nalloc; ++k) {
|
||||
uint32_t align = allocs[k].cpp >> block_align_shift;
|
||||
if (align < 1) align = 1;
|
||||
uint32_t nblocks = (uint32_t)((uint64_t)gmem_blocks * allocs[k].cpp / cpp_total) & ~(align - 1);
|
||||
if (nblocks < align) nblocks = align;
|
||||
if (nblocks > gmem_blocks) return 0;
|
||||
gmem_blocks -= nblocks;
|
||||
cpp_total -= allocs[k].cpp;
|
||||
uint32_t p = nblocks * gmem_align / allocs[k].cpp;
|
||||
if (p < pixels) pixels = p;
|
||||
}
|
||||
out->gmem_pixels = pixels;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint32_t tb_align(uint32_t v, uint32_t a) { return (v + a - 1) / a * a; }
|
||||
|
||||
// fdl6_lrz_get_max_fdm_extra_size() (a7xx path): the largest tile a depth image with the density-map-offset flag
|
||||
// allows, so its LRZ fast-clear buffer still covers it.
|
||||
static int tb_lrz_covered(const tb_gpu *g, uint32_t w, uint32_t h, uint32_t samples, uint32_t layers) {
|
||||
switch (samples) { // fdl6_lrz_get_super_sampled_size()
|
||||
case 8: h *= 2; /* fallthrough */
|
||||
case 4: w *= 2; /* fallthrough */
|
||||
case 2: h *= 2; break;
|
||||
default: break;
|
||||
}
|
||||
uint32_t pitch = tb_align((w + 7) / 8, 32), height = tb_align((h + 7) / 8, 32);
|
||||
uint32_t layer_size = pitch * height * 2;
|
||||
uint32_t full = tb_align(layer_size >> 7, 512) / 8 * layers;
|
||||
return full <= g->lrz_fc_size;
|
||||
}
|
||||
static void tb_lrz_constraint(const tb_gpu *g, uint32_t w, uint32_t h, uint32_t samples, uint32_t layers,
|
||||
uint32_t *max_w, uint32_t *max_h) {
|
||||
*max_w = g->tile_max_w, *max_h = g->tile_max_h;
|
||||
if (!tb_lrz_covered(g, w, h, samples, layers)) return;
|
||||
uint32_t extra = g->tile_max_w < g->tile_max_h ? g->tile_max_w : g->tile_max_h;
|
||||
uint32_t step = g->gmem_align_w < g->gmem_align_h ? g->gmem_align_w : g->gmem_align_h;
|
||||
uint32_t min_extra = step > 192 ? step : 192; // MIN_TILE_SIZE_FOR_FDM_OFFSET
|
||||
while (extra > min_extra) {
|
||||
if (tb_lrz_covered(g, w + extra, h + extra, samples, layers)) {
|
||||
*max_w = extra / g->gmem_align_w * g->gmem_align_w;
|
||||
*max_h = extra / g->gmem_align_h * g->gmem_align_h;
|
||||
return;
|
||||
}
|
||||
extra -= step;
|
||||
}
|
||||
}
|
||||
|
||||
// tu_tiling_config_update_tile_layout() for a pass with a density map. max_w/max_h: the framebuffer's constraint
|
||||
// (~0u: none). 0: no GMEM tiling possible.
|
||||
static int tb_tile_size(const tb_gpu *g, const tb_pass *p, uint32_t fb_w, uint32_t fb_h, uint32_t layers,
|
||||
uint32_t max_w_c, uint32_t max_h_c, uint32_t *tile_w, uint32_t *tile_h) {
|
||||
if (!g || !p || !p->gmem_pixels || !fb_w || !fb_h || !layers || !p->tile_align_w) return 0;
|
||||
uint32_t align_w = p->tile_align_w, align_h = g->tile_align_h;
|
||||
if (layers > 1 && p->min_cpp && p->min_cpp != UINT32_MAX) {
|
||||
uint32_t stride = align_h * align_w * p->min_cpp;
|
||||
if (stride % 4096) { // the layer stride must be a multiple of 4 KiB: grow tile_align_h
|
||||
uint32_t tz = 0;
|
||||
while (!((stride >> tz) & 1)) ++tz;
|
||||
align_h <<= 12 - tz;
|
||||
}
|
||||
}
|
||||
uint32_t max_w = g->tile_max_w, max_h = g->tile_max_h;
|
||||
if (tb_align(fb_w, align_w) < max_w) max_w = tb_align(fb_w, align_w);
|
||||
if (max_w_c < max_w) max_w = max_w_c;
|
||||
if (tb_align(fb_h, align_h) < max_h) max_h = tb_align(fb_h, align_h);
|
||||
if (max_h_c < max_h) max_h = max_h_c;
|
||||
uint32_t best = UINT32_MAX, bw = UINT32_MAX, bh = UINT32_MAX;
|
||||
int possible = 0;
|
||||
for (uint32_t w = align_w; w <= max_w; w += align_w) {
|
||||
uint32_t h = p->gmem_pixels / (w * layers);
|
||||
if (h > max_h) h = max_h;
|
||||
h = h / align_h * align_h;
|
||||
if (!h) continue;
|
||||
uint32_t penalty = (w > h * 2 || h > w * 2) ? 1000 : 0; // FDM wants roughly square bins
|
||||
uint32_t cw = (fb_w + w - 1) / w, ch = (fb_h + h - 1) / h;
|
||||
h = tb_align((fb_h + ch - 1) / ch, align_h); // spread the rows evenly
|
||||
uint32_t total = cw * ch + penalty;
|
||||
int d_new = (int)w - (int)h, d_old = (int)bw - (int)bh;
|
||||
if (total < best || (total == best && abs(d_new) < abs(d_old))) {
|
||||
possible = 1, best = total, bw = w, bh = h;
|
||||
}
|
||||
}
|
||||
if (!possible) return 0;
|
||||
*tile_w = bw, *tile_h = bh;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Where the bins Turnip renders at full density are centred along one axis, in map-space pixels: the bins whose
|
||||
// centre (bin / 2 + j * bin) lies in the sharp texels [lo, hi). Negative: no bin centre falls inside.
|
||||
static float tb_bins_centre(uint32_t lo, uint32_t hi, uint32_t bin) {
|
||||
if (!bin || hi <= lo) return -1.0f;
|
||||
uint32_t half = bin / 2;
|
||||
uint32_t jmin = lo > half ? (lo - half + bin - 1) / bin : 0;
|
||||
if (hi <= half) return -1.0f;
|
||||
uint32_t jmax = (hi - 1 - half) / bin;
|
||||
if (jmax < jmin) return -1.0f;
|
||||
return (float)(jmin * bin + (jmax + 1) * bin) / 2.0f;
|
||||
}
|
||||
|
||||
// Bytes per pixel of the formats render targets use (vk_format_get_blocksize); 0: not modelled.
|
||||
static uint32_t tb_format_size(VkFormat f) {
|
||||
switch ((int)f) {
|
||||
case VK_FORMAT_R8_UNORM: case VK_FORMAT_R8_SNORM: case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT:
|
||||
case VK_FORMAT_R8_SRGB: case VK_FORMAT_S8_UINT:
|
||||
return 1;
|
||||
case VK_FORMAT_R8G8_UNORM: case VK_FORMAT_R8G8_SNORM: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT:
|
||||
case VK_FORMAT_R8G8_SRGB: case VK_FORMAT_R16_UNORM: case VK_FORMAT_R16_SNORM: case VK_FORMAT_R16_UINT:
|
||||
case VK_FORMAT_R16_SINT: case VK_FORMAT_R16_SFLOAT: case VK_FORMAT_D16_UNORM: case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
||||
case VK_FORMAT_B5G6R5_UNORM_PACK16: case VK_FORMAT_R4G4B4A4_UNORM_PACK16: case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
||||
case VK_FORMAT_R5G5B5A1_UNORM_PACK16: case VK_FORMAT_B5G5R5A1_UNORM_PACK16: case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
|
||||
return 2;
|
||||
case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SNORM: case VK_FORMAT_R8G8B8A8_UINT:
|
||||
case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM:
|
||||
case VK_FORMAT_B8G8R8A8_SRGB: case VK_FORMAT_A8B8G8R8_UNORM_PACK32: case VK_FORMAT_A8B8G8R8_SRGB_PACK32:
|
||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
|
||||
case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
||||
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: case VK_FORMAT_R16G16_UNORM: case VK_FORMAT_R16G16_SNORM:
|
||||
case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16_SFLOAT: case VK_FORMAT_R32_UINT:
|
||||
case VK_FORMAT_R32_SINT: case VK_FORMAT_R32_SFLOAT: case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_X8_D24_UNORM_PACK32:
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
return 4;
|
||||
case VK_FORMAT_R16G16B16A16_UNORM: case VK_FORMAT_R16G16B16A16_SNORM: case VK_FORMAT_R16G16B16A16_UINT:
|
||||
case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R16G16B16A16_SFLOAT: case VK_FORMAT_R32G32_UINT:
|
||||
case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32_SFLOAT:
|
||||
return 8;
|
||||
case VK_FORMAT_R32G32B32A32_UINT: case VK_FORMAT_R32G32B32A32_SINT: case VK_FORMAT_R32G32B32A32_SFLOAT:
|
||||
return 16;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// An attachment for tb_pass_config from its format and sample count; 0: format not modelled.
|
||||
static int tb_att_of(VkFormat format, uint32_t samples, tb_att *a) {
|
||||
memset(a, 0, sizeof(*a));
|
||||
if (!samples) samples = 1;
|
||||
a->samples = samples;
|
||||
if (format == VK_FORMAT_D32_SFLOAT_S8_UINT) {
|
||||
a->d32s8 = 1, a->cpp = 4 * samples;
|
||||
return 1;
|
||||
}
|
||||
uint32_t size = tb_format_size(format);
|
||||
a->cpp = size * samples;
|
||||
return size != 0;
|
||||
}
|
||||
@@ -94,6 +94,16 @@
|
||||
"title": "Colour space support",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.fdm_bin_h": {
|
||||
"anchor": "adapter-fdm_bin_h",
|
||||
"title": "Foveation bin height",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.fdm_bin_w": {
|
||||
"anchor": "adapter-fdm_bin_w",
|
||||
"title": "Foveation bin width",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.fixed_resolution": {
|
||||
"anchor": "adapter-fixed_resolution",
|
||||
"title": "Keep full resolution (no dynamic resolution)",
|
||||
|
||||
@@ -698,6 +698,8 @@
|
||||
"Found on your network": "",
|
||||
"Found: {path}": "",
|
||||
"Foveated rendering (Valve)": "",
|
||||
"Foveation bin height": "",
|
||||
"Foveation bin width": "",
|
||||
"fps": "",
|
||||
"fps isn't reported for PC VR and Linux games": "",
|
||||
"fps · perfect pacing": "",
|
||||
@@ -848,6 +850,7 @@
|
||||
"headset found": "",
|
||||
"Headset model": "",
|
||||
"Height": "",
|
||||
"Height in pixels of the driver's GMEM bins for the eye passes, 0 = worked out automatically. See fdm_bin_w.": "",
|
||||
"Hi there!": "",
|
||||
"Hide Android's navigation bar": "",
|
||||
"Hide details": "",
|
||||
@@ -1306,6 +1309,7 @@
|
||||
"Only FramePort's own key, and your SSH agent's keys.": "",
|
||||
"Only if 360° pictures show wrongly.": "",
|
||||
"Only if 360° pictures show wrongly: 1 = upside down, 2 = mirrored, 4 = turned around (add values to combine).": "",
|
||||
"Only if the full-resolution area of Valve's foveation sits off-centre in this game. 0 = automatic.": "",
|
||||
"Only install software from sites you trust. FramePort downloads it from {host} and installs it on your Frame.": "",
|
||||
"Only internal storage": "",
|
||||
"Only thin floating pieces up to this thickness are removed.": "",
|
||||
@@ -1710,6 +1714,7 @@
|
||||
"Send your boxes, doors and windows to the Frame. Games get them at their next start.": "",
|
||||
"Serve Meta scene and spatial entity APIs from the saved room (walls, furniture and mesh). Without a saved room, use a simple play-area box (for example Demeter).": "",
|
||||
"Set by you.": "",
|
||||
"Set together with the width. 0 = automatic.": "",
|
||||
"Set up a play area in SteamVR first. Valve's default circle does not describe your room.": "",
|
||||
"Set up a room in your current play area": "",
|
||||
"Set up for FramePort": "",
|
||||
@@ -2289,6 +2294,7 @@
|
||||
"Wide cover": "",
|
||||
"wide, 1920 × 620": "",
|
||||
"Width": "",
|
||||
"Width in pixels of the GMEM bins the Frame's driver (Turnip) renders the eye passes in, 0 = worked out automatically. The driver gives each bin the density Valve's map has at the bin's centre, so the full-resolution area is a block of whole bins; FramePort centres that block on each eye and works out the bin size the way the driver does. Set this and fdm_bin_h (both non-zero) only when that comes out wrong for a game: the sizes are measured with \"Darken coarse fragments (test)\" and snapshot=N (multiples of 96 wide, 32 high; GitHub #394).": "",
|
||||
"Window": "",
|
||||
"Windows": "",
|
||||
"Windows game (Proton)": "",
|
||||
|
||||
@@ -196,8 +196,8 @@ class A32Bridge(Patch):
|
||||
# the translator ended blocks before its exit-stub table ran out only from this release on (GitHub #342: OpenSSL's
|
||||
# ARMv8 SHA-1 rounds in Pinball FX2 VR killed the game): "Update on Frame" for the game that hit it
|
||||
# Ghost Giant (GitHub #439): Unity's video player needs libmediandk (AMediaFormat/AMediaCodec/AMediaExtractor
|
||||
# functions, then its AMEDIAFORMAT_KEY_* data symbols)
|
||||
package_revisions = {"com.zenstudios.PinballFX2VRQuest": 2, "com.zoink.ghostgiant": 3}
|
||||
# functions, then its AMEDIAFORMAT_KEY_* data symbols); 4: OpenSL ES buffers read on the clock (distorted sound)
|
||||
package_revisions = {"com.zenstudios.PinballFX2VRQuest": 2, "com.zoink.ghostgiant": 4}
|
||||
title = "32-bit ARM bridge (Gear VR / Go games)"
|
||||
description = (
|
||||
"Runs a 32-bit-only game (armeabi-v7a: Gear VR, Oculus Go and early Quest games) on the Frame, which has no "
|
||||
|
||||
@@ -296,6 +296,14 @@ SETTINGS = [
|
||||
"fragment-density-map foveation when the driver offers it and expects the density map from the headset, which the "
|
||||
"Frame doesn't provide: image views and barriers for a missing image, then a crash in the Frame's Vulkan driver "
|
||||
"(for example Metro Awakening). Valve's own foveation layer isn't affected."),
|
||||
("fdm_bin_w", "int", 0, "Foveation: bin width (Valve foveation)",
|
||||
"Width in pixels of the GMEM bins the Frame's driver (Turnip) renders the eye passes in, 0 = worked out "
|
||||
"automatically. The driver gives each bin the density Valve's map has at the bin's centre, so the full-resolution "
|
||||
"area is a block of whole bins; FramePort centres that block on each eye and works out the bin size the way the "
|
||||
"driver does. Set this and fdm_bin_h (both non-zero) only when that comes out wrong for a game: the sizes are "
|
||||
"measured with \"Darken coarse fragments (test)\" and snapshot=N (multiples of 96 wide, 32 high; GitHub #394)."),
|
||||
("fdm_bin_h", "int", 0, "Foveation: bin height (Valve foveation)",
|
||||
"Height in pixels of the driver's GMEM bins for the eye passes, 0 = worked out automatically. See fdm_bin_w."),
|
||||
("ovrp_begin_gate", "int", 0, "Unity frame loop: wait for skipped frames",
|
||||
"Unity's built-in Oculus frame loop (frame.unity_oculus_check): before each frame wait, wait up to 50 ms for the "
|
||||
"last waited frame to begin and skip the wait if it doesn't. Fixes a freeze at the first scene switch (for "
|
||||
@@ -547,6 +555,11 @@ UI: dict[str, dict] = {
|
||||
"vk_hide_fdm": dict(group="troubleshooting", level="advanced", label="Hide Vulkan foveation",
|
||||
help="For Unreal Engine 5 games that crash on the Frame (for example Metro Awakening).",
|
||||
control=("switch",)),
|
||||
"fdm_bin_w": dict(group="troubleshooting", level="advanced", label="Foveation bin width",
|
||||
help="Only if the full-resolution area of Valve's foveation sits off-centre in this game. 0 = "
|
||||
"automatic.", control=("slider", 0, 2016, 96, "{:.0f} px")),
|
||||
"fdm_bin_h": dict(group="troubleshooting", level="advanced", label="Foveation bin height",
|
||||
help="Set together with the width. 0 = automatic.", control=("slider", 0, 2048, 32, "{:.0f} px")),
|
||||
"ovrp_begin_gate": dict(group="troubleshooting", level="advanced",
|
||||
label="Unity frame loop: wait for skipped frames",
|
||||
help="For older Unity games that freeze at the first scene change (for example Sniper "
|
||||
@@ -683,6 +696,9 @@ class AdapterSetting(Patch):
|
||||
"ovrp_hold_physics": lambda a: a.engine == "Unity" and "libOVRPlugin.so" in a.libs,
|
||||
"vk_shader_fix": lambda a: a.engine == "Unreal", # read by the Vulkan shim, which only Unreal games get
|
||||
"vk_hide_fdm": lambda a: a.engine == "Unreal", # Unreal's Vulkan isn't always detected (Metro Awakening)
|
||||
# read by the foveation centring layer, which every arm64 FrameBridge build carries
|
||||
"fdm_bin_w": lambda a: "arm64-v8a" in a.abis,
|
||||
"fdm_bin_h": lambda a: "arm64-v8a" in a.abis,
|
||||
"vk_query_slots": lambda a: a.engine == "Unreal" and ap.is_vulkan(a),
|
||||
"vk_validation": lambda a: a.engine == "Unreal",
|
||||
"vk_pipeline_log": lambda a: a.engine in ("Unreal", "Other") and "arm64-v8a" in a.abis,
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/* a32 bridge audio pacing (native/a32translator/bridge/host/audio_pacing.h, GitHub #439): a simulated device that
|
||||
* takes 960 frames every 20 ms (AudioFlinger's bursts at 48 kHz) from a 150 ms stream buffer, fed by a guest that
|
||||
* queues 10 ms buffers. Paced, the reads come one per 10 ms (never two back to back) and the device never runs dry;
|
||||
* stalls, underruns and clock drift both ways re-anchor without bursts or underruns. */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "audio_pacing.h"
|
||||
|
||||
#define RATE 48000
|
||||
#define MS 1000000LL
|
||||
|
||||
typedef struct {
|
||||
int64_t written, read, cap;
|
||||
int xruns;
|
||||
int64_t next_burst, burst_ns; /* device clock: burst period in our ns */
|
||||
int started;
|
||||
} device;
|
||||
|
||||
/* advance the device to time t */
|
||||
static void device_run(device *d, int64_t t)
|
||||
{
|
||||
while (d->next_burst <= t) {
|
||||
if (!d->started) { /* AudioFlinger starts a track ~90 ms late with one big read (seen on the Frame) */
|
||||
d->started = 1;
|
||||
d->read += d->written - d->read < 2880 ? d->written - d->read : 2880;
|
||||
d->next_burst += d->burst_ns;
|
||||
continue;
|
||||
}
|
||||
if (d->written - d->read < 960) {
|
||||
d->xruns++;
|
||||
d->read = d->written; /* plays what there is, then silence */
|
||||
} else {
|
||||
d->read += 960;
|
||||
}
|
||||
d->next_burst += d->burst_ns;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int reads, xruns, min_gap_ms_x10, max_gap_ms_x10;
|
||||
int64_t last_read;
|
||||
} result;
|
||||
|
||||
/* the guest writes `n` 480-frame buffers; stall_at: buffer index after which the guest pauses for stall_ms */
|
||||
static result simulate_buf(int buf, int64_t burst_ns, int n, int stall_at, int stall_ms, int underrun_once)
|
||||
{
|
||||
device d = {0, 0, 3848, 0, 90 * MS, burst_ns, 0}; /* capacity 3848: the Frame's legacy AAudio stream */
|
||||
a32_pace p;
|
||||
a32_pace_init(&p, RATE);
|
||||
result r = {0, 0, 1000000, 0, -1};
|
||||
int64_t t = 0;
|
||||
for (int k = 0; k < n; k++) {
|
||||
if (k == stall_at) t += stall_ms * MS;
|
||||
if (underrun_once && k == n / 2) d.xruns++; /* a reported underrun: the pacer starts over with a pre-roll */
|
||||
for (;;) {
|
||||
device_run(&d, t);
|
||||
a32_pace_step s = a32_pace_before(&p, t, d.written - d.read, d.xruns);
|
||||
d.written += s.silence;
|
||||
if (s.wait_ns <= 0) break;
|
||||
t += s.wait_ns;
|
||||
}
|
||||
/* blocking write: what fits, then wait for room */
|
||||
for (int64_t left = buf; left > 0;) {
|
||||
int64_t room = d.cap - (d.written - d.read);
|
||||
if (room <= 0) {
|
||||
t = d.next_burst;
|
||||
device_run(&d, t);
|
||||
continue;
|
||||
}
|
||||
int64_t n = left < room ? left : room;
|
||||
d.written += n;
|
||||
left -= n;
|
||||
}
|
||||
a32_pace_after(&p, buf);
|
||||
if (k > 0 && r.last_read >= 0 && k != stall_at && !(underrun_once && k == n / 2)) {
|
||||
int g = (int)((t - r.last_read) * 10 / MS);
|
||||
if (g < r.min_gap_ms_x10) r.min_gap_ms_x10 = g;
|
||||
if (g > r.max_gap_ms_x10 && k > 20) r.max_gap_ms_x10 = g; /* the device starts ~90 ms late: a wait */
|
||||
}
|
||||
r.last_read = t;
|
||||
r.reads++;
|
||||
t += 50000; /* the guest's callback refills and re-enqueues: 50 us */
|
||||
}
|
||||
r.xruns = d.xruns - (underrun_once ? 1 : 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
static result simulate(int64_t burst_ns, int n, int stall_at, int stall_ms, int underrun_once)
|
||||
{
|
||||
return simulate_buf(480, burst_ns, n, stall_at, stall_ms, underrun_once);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
/* steady state: one read every 10 ms, no underruns */
|
||||
result r = simulate(20 * MS, 3000, -1, 0, 0);
|
||||
printf("steady: gaps %d..%d (x0.1 ms) xruns %d\n", r.min_gap_ms_x10, r.max_gap_ms_x10, r.xruns);
|
||||
assert(r.xruns == 0);
|
||||
assert(r.min_gap_ms_x10 >= 95 && r.max_gap_ms_x10 <= 105);
|
||||
|
||||
/* the device's clock 0.5 % slow / fast: corrections, never an underrun, never a burst of reads */
|
||||
r = simulate(20 * MS * 1005 / 1000, 6000, -1, 0, 0);
|
||||
printf("slow device: gaps %d..%d xruns %d\n", r.min_gap_ms_x10, r.max_gap_ms_x10, r.xruns);
|
||||
assert(r.xruns == 0 && r.min_gap_ms_x10 >= 80);
|
||||
r = simulate(20 * MS * 995 / 1000, 6000, -1, 0, 0);
|
||||
printf("fast device: gaps %d..%d xruns %d\n", r.min_gap_ms_x10, r.max_gap_ms_x10, r.xruns);
|
||||
assert(r.xruns == 0 && r.min_gap_ms_x10 >= 80);
|
||||
|
||||
/* the guest stalls for 300 ms: one underrun at most while it stalls, then no burst to catch up */
|
||||
r = simulate(20 * MS, 1000, 500, 300, 0);
|
||||
printf("stall: gaps %d..%d xruns %d\n", r.min_gap_ms_x10, r.max_gap_ms_x10, r.xruns);
|
||||
assert(r.xruns <= 15 && r.min_gap_ms_x10 >= 50);
|
||||
|
||||
/* a reported underrun: a new pre-roll, then paced again */
|
||||
r = simulate(20 * MS, 1000, -1, 0, 1);
|
||||
printf("underrun: gaps %d..%d xruns %d\n", r.min_gap_ms_x10, r.max_gap_ms_x10, r.xruns);
|
||||
assert(r.xruns == 0 && r.min_gap_ms_x10 >= 95 && r.max_gap_ms_x10 <= 105);
|
||||
|
||||
/* large buffers (85 ms; engines other than FMOD): one read per buffer duration, no underruns */
|
||||
r = simulate_buf(4096, 20 * MS, 500, -1, 0, 0);
|
||||
printf("4096-frame buffers: gaps %d..%d xruns %d\n", r.min_gap_ms_x10, r.max_gap_ms_x10, r.xruns);
|
||||
assert(r.xruns == 0 && r.min_gap_ms_x10 >= 750 && r.max_gap_ms_x10 <= 1050);
|
||||
|
||||
/* restart (stream started again): pre-roll again */
|
||||
a32_pace p;
|
||||
a32_pace_init(&p, RATE);
|
||||
a32_pace_step s = a32_pace_before(&p, 1 * MS, 0, 0);
|
||||
assert(s.silence == RATE * 40 / 1000 && s.wait_ns == 0);
|
||||
a32_pace_after(&p, 480);
|
||||
s = a32_pace_before(&p, 1 * MS + 100000, 1920 + 480, 0);
|
||||
assert(llabs(s.wait_ns - (10 * MS - 100000)) < 100000 && s.silence == 0);
|
||||
a32_pace_restart(&p);
|
||||
s = a32_pace_before(&p, 50 * MS, 600, 0);
|
||||
assert(s.silence == 1920 - 600 && s.wait_ns == 0);
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
@@ -15,7 +15,15 @@
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
#define LOG(...) ((void)0)
|
||||
#include <stdarg.h>
|
||||
static char last_log[1024];
|
||||
static void harness_log(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(last_log, sizeof(last_log), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
#define LOG(...) harness_log(__VA_ARGS__)
|
||||
#define EXPORT
|
||||
#define MAX_HANDLES 16
|
||||
static void take_lock(int *p) { while (__atomic_exchange_n(p, 1, __ATOMIC_ACQUIRE)) {} }
|
||||
@@ -58,6 +66,20 @@ static VKAPI_ATTR VkResult VKAPI_CALL n_CreateFramebuffer(VkDevice d, const VkFr
|
||||
*fb = (VkFramebuffer)(next_handle += 8);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL n_CreateGraphicsPipelines(VkDevice d, VkPipelineCache c, uint32_t n,
|
||||
const VkGraphicsPipelineCreateInfo *ci,
|
||||
const VkAllocationCallbacks *a, VkPipeline *p) {
|
||||
(void)d, (void)c, (void)ci, (void)a;
|
||||
for (uint32_t i = 0; i < n; ++i) p[i] = (VkPipeline)(next_handle += 8);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
static int binds_seen;
|
||||
static VKAPI_ATTR void VKAPI_CALL n_CmdBindPipeline(VkCommandBuffer c, VkPipelineBindPoint b, VkPipeline p) {
|
||||
(void)c, (void)b, (void)p;
|
||||
++binds_seen;
|
||||
}
|
||||
static VkPipeline bind_in_pass; // FRAMEPORT_FDM_DIAG tests: bound between begin and end
|
||||
static VkRenderPass last_rp;
|
||||
static VKAPI_ATTR void VKAPI_CALL n_CmdBeginRenderPass2(VkCommandBuffer c, const VkRenderPassBeginInfo *b,
|
||||
const VkSubpassBeginInfo *s) {
|
||||
(void)c, (void)b, (void)s;
|
||||
@@ -117,6 +139,8 @@ static void pass_ex(uint32_t w, uint32_t h, const VkOffset2D *valve, uint32_t co
|
||||
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);
|
||||
last_rp = rp;
|
||||
if (bind_in_pass) c_CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, bind_in_pass);
|
||||
if (valve) {
|
||||
c_offset_end o = {ST_FDM_OFFSET_END, NULL, count, valve};
|
||||
VkSubpassEndInfo e = {VK_STRUCTURE_TYPE_SUBPASS_END_INFO, &o};
|
||||
@@ -131,6 +155,57 @@ static void pass(uint32_t w, uint32_t h, const VkOffset2D *valve, uint32_t count
|
||||
|
||||
static int near(int got_value, float want, int tolerance) { return fabsf((float)got_value - want) <= (float)tolerance; }
|
||||
|
||||
// A two-view pass like In Death's eye pass: colour + depth/stencil (samples each) and Valve's density map, into a
|
||||
// w x h framebuffer whose depth image has the density-map-offset flag (Valve flags it). color: the colour format.
|
||||
static void eye_pass(uint32_t w, uint32_t h, VkFormat color, VkSampleCountFlagBits samples, 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 = FDM_OFFSET_FLAG, ici.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImage map, col, depth;
|
||||
c_CreateImage(dev, &ici, NULL, &map);
|
||||
ici.extent = (VkExtent3D){w, h, 1};
|
||||
ici.samples = samples, ici.format = color, ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
c_CreateImage(dev, &ici, NULL, &col);
|
||||
ici.format = VK_FORMAT_D24_UNORM_S8_UINT, ici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
c_CreateImage(dev, &ici, NULL, &depth);
|
||||
VkImageViewCreateInfo vci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
vci.subresourceRange.layerCount = 2;
|
||||
VkImageView views[3];
|
||||
vci.image = col, vci.format = color;
|
||||
c_CreateImageView(dev, &vci, NULL, &views[0]);
|
||||
vci.image = depth, vci.format = VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
c_CreateImageView(dev, &vci, NULL, &views[1]);
|
||||
vci.image = map, vci.format = VK_FORMAT_R8G8_UNORM;
|
||||
c_CreateImageView(dev, &vci, NULL, &views[2]);
|
||||
VkAttachmentDescription2 atts[3] = {{VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2}, {VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2},
|
||||
{VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2}};
|
||||
atts[0].format = color, atts[0].samples = samples;
|
||||
atts[1].format = VK_FORMAT_D24_UNORM_S8_UINT, atts[1].samples = samples;
|
||||
atts[2].format = VK_FORMAT_R8G8_UNORM, atts[2].samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkAttachmentReference2 cref = {VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2, NULL, 0}, dref = {VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2, NULL, 1};
|
||||
VkSubpassDescription2 sub = {VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2};
|
||||
sub.viewMask = 3, sub.colorAttachmentCount = 1, sub.pColorAttachments = &cref, sub.pDepthStencilAttachment = &dref;
|
||||
VkRenderPassFragmentDensityMapCreateInfoEXT fdm = {ST_FDM_RP_INFO, NULL, {2, VK_IMAGE_LAYOUT_GENERAL}};
|
||||
VkRenderPassCreateInfo2 rci = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2, &fdm};
|
||||
rci.attachmentCount = 3, rci.pAttachments = atts, rci.subpassCount = 1, rci.pSubpasses = ⊂
|
||||
VkRenderPass rp;
|
||||
c_CreateRenderPass2(dev, &rci, NULL, &rp);
|
||||
VkFramebufferCreateInfo fci = {VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
fci.renderPass = rp, fci.attachmentCount = 3, fci.pAttachments = views, 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);
|
||||
c_offset_end o = {ST_FDM_OFFSET_END, NULL, count, valve};
|
||||
VkSubpassEndInfo e = {VK_STRUCTURE_TYPE_SUBPASS_END_INFO, &o};
|
||||
c_CmdEndRenderPass2(cmd, &e);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
(void)dispatch_key;
|
||||
c_device d;
|
||||
@@ -139,6 +214,7 @@ int main(void) {
|
||||
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;
|
||||
d.CreateGraphicsPipelines = n_CreateGraphicsPipelines, d.CmdBindPipeline = n_CmdBindPipeline;
|
||||
c_devices[0] = d;
|
||||
|
||||
// Valve's lo centre on a 2016x1728 swapchain (63x54 map, 32 px texels): texels 23-40 and 20-34
|
||||
@@ -226,6 +302,95 @@ int main(void) {
|
||||
CHECK(!c_fixed && 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"));
|
||||
|
||||
// FRAMEPORT_FDM_DIAG: SPIR-V outputs. A vertex shader with a standalone Output ViewportIndex variable, one with
|
||||
// Layer in an output block, and a fragment shader reading Layer as an input (not counted).
|
||||
const uint32_t vs_vp[] = {0x07230203, 0x10000, 0, 20, 0,
|
||||
(4u << 16) | 15, 0, 1, 0, // OpEntryPoint Vertex %1 "" (name truncated)
|
||||
(4u << 16) | 71, 5, 11, 10, // OpDecorate %5 BuiltIn ViewportIndex
|
||||
(4u << 16) | 32, 4, 3, 2, // %4 = OpTypePointer Output %2
|
||||
(4u << 16) | 59, 4, 5, 3}; // %5 = OpVariable %4 Output
|
||||
CHECK(c_spirv_outputs(vs_vp, sizeof(vs_vp) / 4) == (1u | 1u << 8));
|
||||
const uint32_t vs_block[] = {0x07230203, 0x10000, 0, 20, 0,
|
||||
(5u << 16) | 72, 7, 1, 11, 9, // OpMemberDecorate %7 1 BuiltIn Layer
|
||||
(4u << 16) | 32, 8, 3, 7, // %8 = OpTypePointer Output %7
|
||||
(4u << 16) | 59, 8, 9, 3}; // %9 = OpVariable %8 Output
|
||||
CHECK(c_spirv_outputs(vs_block, sizeof(vs_block) / 4) == 2u);
|
||||
const uint32_t fs_in[] = {0x07230203, 0x10000, 0, 20, 0,
|
||||
(4u << 16) | 15, 4, 1, 0, // OpEntryPoint Fragment
|
||||
(4u << 16) | 71, 5, 11, 9, // OpDecorate %5 BuiltIn Layer
|
||||
(4u << 16) | 59, 4, 5, 1}; // %5 = OpVariable %4 Input
|
||||
CHECK(c_spirv_outputs(fs_in, sizeof(fs_in) / 4) == 1u << 12);
|
||||
CHECK(c_spirv_outputs(NULL, 0) == 0);
|
||||
|
||||
// FRAMEPORT_FDM_DIAG: a pipeline made for a render pass without a density map, bound inside a foveated pass,
|
||||
// makes Turnip share one viewport scale; one made for the foveated pass doesn't
|
||||
c_diag = 1, c_fixed = 0, c_level = 1, c_disabled = 0;
|
||||
fp_fdm_set_views(views, 2);
|
||||
pass(2016, 1728, zero, 2); // creates a foveated render pass (last_rp)
|
||||
VkPipelineShaderStageCreateInfo stages[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO},
|
||||
{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT, stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
VkGraphicsPipelineCreateInfo gci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
gci.stageCount = 2, gci.pStages = stages, gci.renderPass = (VkRenderPass)0x7777; // no density map
|
||||
VkPipeline plain, foveated;
|
||||
CHECK(c_CreateGraphicsPipelines((VkDevice)&device_obj, VK_NULL_HANDLE, 1, &gci, NULL, &plain) == VK_SUCCESS);
|
||||
CHECK(strstr(last_log, "NO density map"));
|
||||
gci.renderPass = last_rp;
|
||||
snprintf(last_log, sizeof(last_log), "-");
|
||||
CHECK(c_CreateGraphicsPipelines((VkDevice)&device_obj, VK_NULL_HANDLE, 1, &gci, NULL, &foveated) == VK_SUCCESS);
|
||||
CHECK(!strcmp(last_log, "-")); // not logged: nothing wrong with it
|
||||
bind_in_pass = foveated;
|
||||
pass(2016, 1728, zero, 2);
|
||||
CHECK(strstr(last_log, "per-view scale") && strstr(last_log, "1 pipeline bind(s)") && binds_seen == 1);
|
||||
bind_in_pass = plain;
|
||||
pass(2016, 1728, zero, 2);
|
||||
CHECK(strstr(last_log, "SHARED VIEWPORT") && binds_seen == 2);
|
||||
CHECK(strstr(last_log, "pipelines created 2, without density map 1, writing ViewportIndex 0"));
|
||||
CHECK(strstr(last_log, "handed down 2: v0 -24,-184"));
|
||||
bind_in_pass = VK_NULL_HANDLE;
|
||||
c_diag = 0;
|
||||
|
||||
// Turnip's bins (tu_bins.h, Adreno 750). In Death's eye pass: 4x RGBA8 + 4x D24S8, two views, 2768x2768.
|
||||
// Measured on the Frame (snapshots, 2026-10-25): 192x224 bins.
|
||||
tb_att idatts[3];
|
||||
CHECK(tb_att_of(VK_FORMAT_R8G8B8A8_SRGB, 4, &idatts[0]) && tb_att_of(VK_FORMAT_D24_UNORM_S8_UINT, 4, &idatts[1]));
|
||||
CHECK(!tb_att_of(VK_FORMAT_ASTC_4x4_UNORM_BLOCK, 1, &idatts[2])); // not modelled
|
||||
tb_att_of(VK_FORMAT_R8G8_UNORM, 1, &idatts[2]);
|
||||
idatts[0].gmem = idatts[1].gmem = 1; // the density map isn't in GMEM
|
||||
tb_pass tp;
|
||||
CHECK(tb_pass_config(&TB_ADRENO_750, idatts, 3, &tp) && tp.gmem_pixels == 86016 && tp.tile_align_w == 96);
|
||||
uint32_t tw = 0, th = 0, cw, ch;
|
||||
tb_lrz_constraint(&TB_ADRENO_750, 2768, 2768, 4, 2, &cw, &ch); // 4x: LRZ fast clear can't cover it anyway
|
||||
CHECK(cw == 2016 && ch == 2032);
|
||||
CHECK(tb_tile_size(&TB_ADRENO_750, &tp, 2768, 2768, 2, cw, ch, &tw, &th) && tw == 192 && th == 224);
|
||||
// a 1x depth image of that size does get a limit (a multiple of the GMEM alignment, at least 192)
|
||||
tb_lrz_constraint(&TB_ADRENO_750, 2768, 2768, 1, 2, &cw, &ch);
|
||||
CHECK(cw < 2016 && cw >= 192 && cw % 16 == 0 && ch % 4 == 0);
|
||||
// the bins whose centre lies in Valve's sharp texels 1024-1824 (lo preset, 87-texel map): 960-1728 / 1120-1792
|
||||
CHECK(tb_bins_centre(1024, 1824, 192) == 1344.0f && tb_bins_centre(1024, 1824, 224) == 1456.0f);
|
||||
CHECK(tb_bins_centre(1024, 1100, 400) < 0); // no bin centre inside: undecided
|
||||
CHECK(c_sharp_centre(2768, 87, 0, 192) == 1344.0f && c_sharp_centre(2768, 87, 0, 0) == 1424.0f);
|
||||
|
||||
// End to end: with the GPU known, the layer computes 192x224 for that pass and centres those bins on each eye
|
||||
fp_fdm_view indeath[2] = {{2768, 2768, 0, 0, 0, 1730, 1730, 1004, 697}, {2768, 2768, 1, 0, 0, 1730, 1730, 715, 709}};
|
||||
fp_fdm_set_views(indeath, 2);
|
||||
c_fixed = 1, c_level = 1;
|
||||
eye_pass(2768, 2768, VK_FORMAT_R8G8B8A8_SRGB, VK_SAMPLE_COUNT_4_BIT, zero, 2);
|
||||
CHECK(got_count == 2 && near(got[0].x, 1004 - 1424, 4) && near(got[0].y, 697 - 1424, 4)); // GPU unknown: texels
|
||||
c_gpu = &TB_ADRENO_750;
|
||||
eye_pass(2768, 2768, VK_FORMAT_R8G8B8A8_SRGB, VK_SAMPLE_COUNT_4_BIT, zero, 2);
|
||||
CHECK(got_count == 2 && near(got[0].x, 1004 - 1344, 4) && near(got[0].y, 697 - 1456, 4));
|
||||
CHECK(near(got[1].x, 715 - 1344, 4) && near(got[1].y, 709 - 1456, 4));
|
||||
// a colour format tu_bins.h doesn't model: undecided, the texels' centre as before
|
||||
eye_pass(2768, 2768, VK_FORMAT_ASTC_4x4_UNORM_BLOCK, VK_SAMPLE_COUNT_4_BIT, zero, 2);
|
||||
CHECK(near(got[0].x, 1004 - 1424, 4) && near(got[0].y, 697 - 1424, 4));
|
||||
// fdm_bin_w=256, fdm_bin_h=256 (FrameBridge -> fp_fdm_set_bin) wins over the computed size: bins 1024-1792 -> centre 1408
|
||||
fp_fdm_set_bin(256, 256);
|
||||
eye_pass(2768, 2768, VK_FORMAT_R8G8B8A8_SRGB, VK_SAMPLE_COUNT_4_BIT, zero, 2);
|
||||
CHECK(near(got[0].x, 1004 - 1408, 4) && near(got[0].y, 697 - 1408, 4));
|
||||
fp_fdm_set_bin(0, 0);
|
||||
c_gpu = NULL;
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
@@ -552,3 +552,28 @@ def test_jit_ends_blocks_before_the_stub_table_is_full():
|
||||
root = Path(__file__).resolve().parents[1] / "native" / "a32translator"
|
||||
assert "x->nstubs > MAX_STUBS - STUB_RESERVE" in (root / "src" / "jit.c").read_text()
|
||||
assert '"sha1ce": ("sha1ce.c"' in (root / "tests" / "run_programs.py").read_text()
|
||||
|
||||
|
||||
def test_audio_pacing(tmp_path):
|
||||
"""GitHub #439: the OpenSL ES player reads the guest's buffers on the clock (host/audio_pacing.h), one per buffer
|
||||
duration, instead of in pairs at AudioFlinger's 20 ms bursts (Ghost Giant's FMOD then mixed discontinuous
|
||||
blocks: distorted sound). The fixture simulates the Frame's device: steady state, clock drift both ways, a guest
|
||||
stall, an underrun, a late device start and buffers larger than the stream."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
|
||||
if not cc or os.name == "nt":
|
||||
pytest.skip("needs a POSIX host C compiler")
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
binary = tmp_path / "a32-audio-pacing-test"
|
||||
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-I", str(root / "native/a32translator/bridge/host"),
|
||||
str(root / "tests/fixtures/src/a32_audio_pacing_test.c"), "-o", str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True, timeout=60)
|
||||
host = (root / "native/a32translator/bridge/host/android.c").read_text()
|
||||
assert "a32_pace_before(" in host and 'getenv("A32_AUDIO_PACING")' in host
|
||||
Reference in new issue
Block a user