mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-26 00:02:33 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a4e98709c | ||
|
|
4799a2d048 | ||
|
|
43383d62c4 | ||
|
|
c8a4da90e2 | ||
|
|
d54d35e43d |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
dfeee15b52a530aef0fc90f79140249522adf888d1c88fd57935dd8ce5257fa5 ./arm64-v8a/libVkLayer_fdm_darken.so
|
||||
82fecbaee846f1cff279d03747145f3fdb808f88e8ad424fb134ed06865f1f3b ./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
|
||||
|
||||
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,23 @@ 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).
|
||||
|
||||
|
||||
## Typing window takes the controllers: SteamVR theater mode (#476, #477, AC Nexus)
|
||||
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
+10
-10
@@ -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 |
|
||||
@@ -148,7 +148,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 +209,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 +602,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 +995,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 +1213,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.
|
||||
@@ -1960,7 +1960,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 +2863,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.
|
||||
|
||||
@@ -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,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
|
||||
+342
-11
@@ -17,6 +17,11 @@
|
||||
// (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).
|
||||
#define CENTRE_NAME "VK_LAYER_FRAMEPORT_fdm_centre"
|
||||
#define FP_FDM_MAX_VIEWS 4
|
||||
|
||||
@@ -32,6 +37,7 @@ 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
|
||||
|
||||
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;
|
||||
@@ -71,12 +77,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 +92,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 +123,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];
|
||||
@@ -255,6 +270,273 @@ static VKAPI_ATTR void VKAPI_CALL c_DestroyFramebuffer(VkDevice device, VkFrameb
|
||||
c_dev(device)->DestroyFramebuffer(device, fb, a);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 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;
|
||||
} 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, Valve 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->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) {
|
||||
@@ -279,6 +561,7 @@ static void c_begin(VkCommandBuffer cmd, uint32_t fbw, uint32_t fbh, VkImageView
|
||||
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;
|
||||
@@ -335,24 +618,27 @@ 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];
|
||||
float vx = c_valve_centre(fbw, mw, 0), vy = c_valve_centre(fbh, mh, 1);
|
||||
*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};
|
||||
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 +654,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
|
||||
@@ -386,6 +673,13 @@ static uint32_t c_offsets(VkCommandBuffer cmd, c_device *d, uint32_t in_count, c
|
||||
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,7 +822,33 @@ 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}};
|
||||
@@ -599,6 +919,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 +957,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
|
||||
|
||||
@@ -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 "
|
||||
|
||||
+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};
|
||||
@@ -139,6 +163,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 +251,54 @@ 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;
|
||||
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