mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-17 16:02:22 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020a0251b1 | ||
|
|
98aeff5263 |
No files matched your search
@@ -903,7 +903,7 @@ writable). `frame.eos_keychain_loop` (opt-in, triage `eos-keychain-loop`) return
|
||||
0.3570 (Unity 6.5, Firebase, Gecko) ran out of bionic's 128 pthread keys → `frame.unity_tls_key_pool` (Klownicle's
|
||||
approach, #198: libunity's 4 key imports renamed `fp_tkey_*` via `elf.rename_imports`, `libfp_tkeys.so` pools
|
||||
destructor-less keys behind one real key); device: past Firestore, 72 fps. Cpp2IL releases can't read metadata v107
|
||||
(Unity 6.5) → `tools/cpp2il.py` falls back to the development CI build (nightly.link) on that error. Vermillion's canvas
|
||||
(Unity 6.5) → `tools/cpp2il.py` falls back to the development CI build (nightly.link) on that error, and since #293 on any release failure (BattleTalent, Unity 2021.3: the release dies exit 134 in CsFileUtils.GetTypeName after making the folders, no .cs; `il2cpp._run` = non-zero exit or no .cs is unreadable; never with FRAMEPORT_CPP2IL; caches holding "every class missing" are re-run). Vermillion's canvas
|
||||
is an OVROverlay quad (drawn above the hand) → `frame.vermillion_scene_canvas` (PaintableMesh getters → false;
|
||||
installed build checked, 72 fps headless; headset not yet). Linux apps: `desktop_icon` switch, a deleted desktop copy
|
||||
stays deleted (#212). Findings: OLAR (#201) RHIThread null deref in libUnreal (`ldr x8,[x19,#792]`, x19=0) not fixed by
|
||||
|
||||
@@ -13,11 +13,11 @@ e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/li
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
0f058921e9c1f5b9ef5847cc9e6b80a23e75028c76c1d3bb02d1610b535f81c9 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1a8590892a8cb8f01782bc1d245046014cce0f6db58ba5ba87769776fb65c738 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
9e8a7ce599b62a7c607b0d3637e4ec1b1ea1974c3373a7af3f5920ad2adc5e00 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
d7aae4b5d7b084bff5c8fdeab68c51ca4c87dce7abed86b2bc9694d355ebd8f5 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -107,7 +107,7 @@ signatures:
|
||||
- id: swapchain-rect-invalid
|
||||
pattern: 'xrEndFrame failed -25\b'
|
||||
severity: fatal
|
||||
diagnosis: The runtime rejects the game's frames, so it stops drawing (for example stuck at a "Waiting" box). An image rect reaches a few pixels past its swapchain on some Frames; FrameBridge from FramePort 0.11.0 clamps the rects, so rebuild and reinstall the game.
|
||||
diagnosis: The runtime rejects the game's frames, so it stops drawing (for example stuck at a "Waiting" box). An image rect reaches a few pixels past its swapchain on some Frames (FrameBridge from FramePort 0.11.0 clamps the rects), or is empty (0x0, for example BattleTalent's first frames right after Unity switches its MSAA level; a newer FrameBridge shows the whole swapchain image instead), so rebuild and reinstall the game.
|
||||
suggest: [frame.adapter]
|
||||
- id: cylinder-menu-layers-rejected
|
||||
pattern: 'xrEndFrame failed -16\b'
|
||||
|
||||
+2
-2
@@ -1846,7 +1846,7 @@ For games whose hands' thumbs never move when you touch the buttons or thumbstic
|
||||
|
||||
### <a id="adapter-rect_clamp"></a>Keep pictures in bounds
|
||||
|
||||
Stops games from hanging when their eye picture is a little too big (for example PowerWash Simulator). Leave it on.
|
||||
Stops games from hanging when their eye picture is a little too big or empty (for example PowerWash Simulator). Leave it on.
|
||||
|
||||
- `adapter.rect_clamp`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
@@ -1856,7 +1856,7 @@ Stops games from hanging when their eye picture is a little too big (for example
|
||||
- Game settings → Troubleshooting → "Keep pictures in bounds" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at "Waiting").
|
||||
> Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at "Waiting"). An empty rect (0x0, e.g. BattleTalent's first frames, GitHub #293) is rejected the same way and becomes the whole swapchain image.
|
||||
|
||||
### <a id="adapter-refresh_rate"></a>Refresh rate
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Video player / app finds no local videos; MediaProvider "Requested path /home/steamos/... doesn't appear under ..." | Lepton's /sdcard is a symlink to a host path, so Android's media index rejects every file | upload in the Files tab (Videos = /sdcard/Movies) and browse folders in the app |
|
||||
| Batman: Arkham Shadow closes during smoke-bomb effects; later input-tree corruption | Meta XR Audio Wwise deletes queued metadata still referenced by the current audio frame, then writes through the stale pointer | FrameBridge automatically retires metadata after current-frame references disappear, for the verified AArch64 SDK build only; see [AUDIO_METADATA.md](AUDIO_METADATA.md) |
|
||||
| Game stays on a loading or "Waiting" box although it reached FOCUSED; log `FrameBridge: xrEndFrame failed -25` then no more `pacing:` lines (for example PowerWash Simulator on some Frames, GitHub #39) | The eye image rect ends a few pixels past its swapchain (Unity rounds the swapchain width, the runtime's recommended size differs per Frame); SteamVR rejects every frame with XR_ERROR_SWAPCHAIN_RECT_INVALID | FrameBridge clamps every projection/quad image rect to its swapchain (`rect_clamp`, default on since 0.11.0; log `rect_clamp: view N image rect clamped`): rebuild + reinstall the game |
|
||||
| Black from the start, `projection view 0: ... rect=0,0 0x0` then `xrEndFrame failed -25` twice and OVRPlugin "Unable to submit Oculus frame" (BattleTalent, GitHub #293; right after Unity's "Switching to the recommended level" MSAA line) | The game submits its first projection views with an empty image rect; SteamVR rejects them and OVRPlugin gives up submitting | `rect_clamp` replaces an empty rect with the whole swapchain image (log `rect_clamp: view N empty image rect -> full swapchain WxH`): rebuild + reinstall. Check the build output for a WARN from the Unity IL2CPP fixes (`frame.unity_runtime_msaa_off` etc. "code not found in this game"): turned on, they may still change nothing |
|
||||
| Unity game (OVRPlugin, GLES or Vulkan) crashes ~10 s in: `FrameBridge: xrCreateSwapchain … faces=6 … result=-2`, OVRPlugin `CreateSwapchain for eye 0: 0x0, 0 stages`, then render-thread SIGSEGV in memset ← libOVRPlugin ← `ovrp_EndFrame4` (libgallium/ANR follow; for example Budget Cuts Ultimate, GitHub #107); triage `cube-swapchain-refused` | The game shows a cube-map layer (OVROverlay cubemap); the Frame's runtime has no cube layers and refuses cube swapchains (XR_ERROR_RUNTIME_FAILURE); OVRPlugin doesn't check and writes into the image list it never got | FrameBridge `cube_standin` (default on): a refused cube swapchain is served as a GL cube map in the game's context or a device-local Vulkan cube image transitioned to COLOR_ATTACHMENT_OPTIMAL, and its layers are dropped (log `cube_standin: runtime refused …`): rebuild + reinstall. Vulkan path covers UNDERDOGS (GitHub #296); host-tested, headset verification pending |
|
||||
| Game (Unity, GLES) freezes, `zink: DEVICE LOST` | multisampled render-to-texture hangs the GPU | `frame.unity_no_msaa`; if it persists: unfixable → PC version |
|
||||
| Unreal GLES game crashes ~3 s after start: `Fatal signal 11 … fault addr 0x10000 in tid … (RHIThread)`, `#01 … libgallium_dri.so` (GitHub #83 Star Wars Pinball VR, triage `unreal-msrtt-crash`) | Unreal's mobile MSAA renders through multisampled render-to-texture; Zink's `find_rp_state` then indexes `rendering_state_cache[6]` one past its end (sample count ≥ 32) and calls a junk hash function (0x10000). Mesa bug (no bounds check) | `frame.unreal_gl_shim` (GL shim with `gl_hide_msrtt`, multiview kept for Unreal: Unreal 4.25 only enables multiview with GL_OVR_multiview, GL_OVR_multiview2 **and** GL_OVR_multiview_multisampled_render_to_texture, so the shim keeps the last one visible for Unreal and maps glFramebufferTextureMultisampleMultiviewOVR to the single-sampled glFramebufferTextureMultiviewOVR; log `multiview multisampled render-to-texture (N samples) drawn single-sampled`) |
|
||||
|
||||
@@ -537,9 +537,18 @@ static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
|
||||
return found;
|
||||
}
|
||||
|
||||
// Clamp `r` to a w x h image; returns whether it changed.
|
||||
// Clamp `r` to a w x h image; returns whether it changed (2 = it was empty and now covers the whole image).
|
||||
static int clamp_rect(XrRect2Di *r, uint32_t w, uint32_t h) {
|
||||
XrRect2Di c = *r;
|
||||
// An empty rect (BattleTalent's first frames after Unity switched its MSAA level: 0,0 0x0, GitHub #293) is
|
||||
// rejected the same way, and the game stops submitting after two rejections: show the whole image instead.
|
||||
if (c.extent.width <= 0 || c.extent.height <= 0) {
|
||||
if (!w || !h || w > INT32_MAX || h > INT32_MAX) return 0;
|
||||
r->offset.x = r->offset.y = 0;
|
||||
r->extent.width = (int32_t)w;
|
||||
r->extent.height = (int32_t)h;
|
||||
return 2;
|
||||
}
|
||||
if (c.offset.x < 0) { c.extent.width += c.offset.x; c.offset.x = 0; }
|
||||
if (c.offset.y < 0) { c.extent.height += c.offset.y; c.offset.y = 0; }
|
||||
if ((uint32_t)c.offset.x >= w || (uint32_t)c.offset.y >= h) return 0; // nothing sensible to keep: leave it
|
||||
@@ -1878,9 +1887,14 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count]) projections[count] = *cur;
|
||||
if (projections[count].views != views[count])
|
||||
for (uint32_t v = 0; v < cur->viewCount; ++v) views[count][v] = cur->views[v];
|
||||
for (uint32_t v = 0; v < cur->viewCount; ++v)
|
||||
if (swapchain_size(views[count][v].subImage.swapchain, &w, &h) &&
|
||||
clamp_rect(&views[count][v].subImage.imageRect, w, h)) {
|
||||
for (uint32_t v = 0; v < cur->viewCount; ++v) {
|
||||
int how = swapchain_size(views[count][v].subImage.swapchain, &w, &h)
|
||||
? clamp_rect(&views[count][v].subImage.imageRect, w, h) : 0;
|
||||
if (how == 2) {
|
||||
static int logged_empty;
|
||||
if (logged_empty++ < 3)
|
||||
LOG("rect_clamp: view %u empty image rect -> full swapchain %ux%u", v, w, h);
|
||||
} else if (how) {
|
||||
static int logged;
|
||||
if (logged++ < 3)
|
||||
LOG("rect_clamp: view %u image rect clamped to %dx%d at %d,%d (swapchain %ux%u)", v,
|
||||
@@ -1889,6 +1903,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
views[count][v].subImage.imageRect.offset.x,
|
||||
views[count][v].subImage.imageRect.offset.y, w, h);
|
||||
}
|
||||
}
|
||||
projections[count].views = views[count];
|
||||
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
|
||||
++swapped;
|
||||
@@ -1896,13 +1911,17 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
} else if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
|
||||
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)layer;
|
||||
XrRect2Di r = q->subImage.imageRect;
|
||||
if (swapchain_size(q->subImage.swapchain, &w, &h) && clamp_rect(&r, w, h)) {
|
||||
int how = swapchain_size(q->subImage.swapchain, &w, &h) ? clamp_rect(&r, w, h) : 0;
|
||||
if (how) {
|
||||
if (layer != (const XrCompositionLayerBaseHeader *)&quads[count]) quads[count] = *q;
|
||||
quads[count].subImage.imageRect = r;
|
||||
layer = (const XrCompositionLayerBaseHeader *)&quads[count];
|
||||
++swapped;
|
||||
static int logged;
|
||||
if (!logged++) LOG("rect_clamp: quad image rect clamped (swapchain %ux%u)", w, h);
|
||||
static int logged, logged_empty;
|
||||
if (how == 2 && logged_empty++ < 3)
|
||||
LOG("rect_clamp: quad empty image rect -> full swapchain %ux%u", w, h);
|
||||
else if (how == 1 && !logged++)
|
||||
LOG("rect_clamp: quad image rect clamped (swapchain %ux%u)", w, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -67,19 +68,22 @@ def find_methods(lib: bytes, metadata: bytes, unity_version: str, wanted: dict[s
|
||||
cache_file = _cache_file(lib, metadata)
|
||||
from ..tools import cpp2il as tool
|
||||
|
||||
nightly = False # Cpp2IL's development build: for metadata newer than its release reads (Unity 6.5+)
|
||||
# a caller's own Cpp2IL or the user's FRAMEPORT_CPP2IL override: never swapped for another build
|
||||
explicit = cpp2il is not None or bool(os.environ.get("FRAMEPORT_CPP2IL"))
|
||||
nightly = False # Cpp2IL's development build: reads metadata (Unity 6.5+) and games the release crashes on
|
||||
try:
|
||||
cached = json.loads(cache_file.read_text())
|
||||
if cached.get("error"): # Cpp2IL can't read this build (e.g. an old Unity): don't run it again per patch
|
||||
if cpp2il is not None or cached.get("nightly") or not tool.unsupported_metadata(cached["error"]):
|
||||
if cached.get("error"): # Cpp2IL couldn't read this build: don't run it again per patch
|
||||
if explicit or cached.get("nightly"):
|
||||
raise RuntimeError(cached["error"])
|
||||
nightly = True # only the release couldn't: the development build gets one try
|
||||
elif _poisoned(cached, wanted):
|
||||
pass # an older FramePort took a Cpp2IL crash for "no such classes": run it again
|
||||
elif all(cls in cached.get("classes", {}) or cls in cached.get("missing", []) for cls in wanted):
|
||||
return {cls: {m: tuple(v) for m, v in cached["classes"][cls].items() if m in names}
|
||||
for cls, names in wanted.items() if cls in cached.get("classes", {})}
|
||||
except (OSError, ValueError, KeyError):
|
||||
pass
|
||||
explicit = cpp2il is not None
|
||||
exe = cpp2il or (tool.ensure_nightly() if nightly else tool.ensure())
|
||||
while True:
|
||||
try:
|
||||
@@ -87,7 +91,7 @@ def find_methods(lib: bytes, metadata: bytes, unity_version: str, wanted: dict[s
|
||||
break
|
||||
except _Unreadable as exc:
|
||||
error = "Cpp2IL couldn't read this game's code: " + str(exc)
|
||||
if not explicit and not nightly and tool.unsupported_metadata(error):
|
||||
if not explicit and not nightly: # e.g. BattleTalent (Unity 2021.3): the release crashes, nightly reads it
|
||||
nightly, exe = True, tool.ensure_nightly()
|
||||
continue
|
||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -99,8 +103,14 @@ def find_methods(lib: bytes, metadata: bytes, unity_version: str, wanted: dict[s
|
||||
return out
|
||||
|
||||
|
||||
def _poisoned(cached: dict, wanted: dict) -> bool:
|
||||
"""A cache entry written when Cpp2IL crashed after making its folders (GitHub #293): no class found at all and
|
||||
every wanted one recorded missing. A real game always has some of them (UnityEngine's QualitySettings)."""
|
||||
return not cached.get("classes") and bool(wanted) and all(c in cached.get("missing", []) for c in wanted)
|
||||
|
||||
|
||||
class _Unreadable(Exception):
|
||||
"""Cpp2IL wrote no output; the message is its last exception line."""
|
||||
"""Cpp2IL failed or wrote no class files; the message is its last exception line."""
|
||||
|
||||
|
||||
def _run(cpp2il: Path, lib: bytes, metadata: bytes, unity_version: str, wanted: dict[str, list[str]],
|
||||
@@ -116,8 +126,10 @@ def _run(cpp2il: Path, lib: bytes, metadata: bytes, unity_version: str, wanted:
|
||||
"--output-to", str(t / "out")], capture_output=True, text=True, errors="replace",
|
||||
timeout=timeout, cwd=tmp)
|
||||
root = t / "out" / "DiffableCs"
|
||||
if not root.is_dir():
|
||||
raise _Unreadable(_last_error(proc.stdout + proc.stderr))
|
||||
# a crash after Cpp2IL made the folder tree (exit 134, no .cs files: GitHub #293) isn't "no such classes"
|
||||
if proc.returncode != 0 or not root.is_dir() or next(root.rglob("*.cs"), None) is None:
|
||||
error = _last_error(proc.stdout + proc.stderr)
|
||||
raise _Unreadable(error if proc.returncode == 0 else f"{error} (exit code {proc.returncode})")
|
||||
out, missing = {}, []
|
||||
for cls, names in wanted.items():
|
||||
f = root / cls
|
||||
|
||||
@@ -54,7 +54,7 @@ def apply_frame_fixes(apk_in: Path, apk_out_unsigned: Path, analysis: Analysis,
|
||||
if patch.apply(ctx):
|
||||
applied.append(patch.id)
|
||||
reporter.log(f"applied {patch.id}" + (f" ({'; '.join(ctx.notes)})" if ctx.notes else ""))
|
||||
for name, ok, detail in patch.validate(ctx):
|
||||
for name, ok, detail in [*ctx.checks, *patch.validate(ctx)]:
|
||||
checks.append({"name": name, "ok": ok, "detail": detail})
|
||||
ws.write(apk_out_unsigned)
|
||||
return applied, checks, selection.superseded
|
||||
|
||||
@@ -785,7 +785,7 @@
|
||||
"Keep the same Meta user between starts": "",
|
||||
"Keep the signing keys: without them, updating a game loses its saves.": "",
|
||||
"Keeps a smoothing setting off that the game turns on itself and that can freeze the Frame.": "",
|
||||
"Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at \"Waiting\").": "",
|
||||
"Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at \"Waiting\"). An empty rect (0x0, e.g. BattleTalent's first frames, GitHub #293) is rejected the same way and becomes the whole swapchain image.": "",
|
||||
"Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.": "",
|
||||
"Keeps one label so the title is stable.": "",
|
||||
"Keeps one name for the game, so it shows the same everywhere.": "",
|
||||
@@ -1483,7 +1483,7 @@
|
||||
"Stops a start-up crash in Stress Level Zero games (for example BONELAB).": "",
|
||||
"Stops bHaptics from asking for a Bluetooth permission the Frame doesn't have (the game would freeze on its loading screen).": "",
|
||||
"Stops Epic Online Services from retrying its login keychain forever (the game would never finish starting).": "",
|
||||
"Stops games from hanging when their eye picture is a little too big (for example PowerWash Simulator). Leave it on.": "",
|
||||
"Stops games from hanging when their eye picture is a little too big or empty (for example PowerWash Simulator). Leave it on.": "",
|
||||
"Stops Meta's audio library from reporting to Meta (only matters on PC emulators).": "",
|
||||
"Stops Meta's old VR library from closing the game at start (for example Jurassic World Aftermath).": "",
|
||||
"Stops some Unreal games from closing themselves right after starting.": "",
|
||||
|
||||
@@ -48,6 +48,9 @@ class ApkContext:
|
||||
reporter: Reporter
|
||||
recipe_patches: dict # full selection, for patches that depend on others
|
||||
notes: list[str] = field(default_factory=list)
|
||||
# (name, ok, detail) a patch reports while applying, e.g. a WARN that it found nothing to change: they join the
|
||||
# build's checks (job/CLI output, the build record) like validate()'s
|
||||
checks: list[tuple[str, bool | None, str]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -150,14 +150,27 @@ class Il2cppReturnPatch(Patch):
|
||||
again, more = patch_field_loads(patched or lib, method, offset[0], value, name)
|
||||
patched, notes = again or patched, notes + more
|
||||
except Exception as exc: # noqa: BLE001 - optional fix: the game still builds, unchanged
|
||||
ctx.reporter.check(self.check_name, False, f"not fixed: {exc}")
|
||||
return False
|
||||
ctx.notes.extend(notes or [f"{self.check_name}: code not found in this game"])
|
||||
# e.g. "Cpp2IL couldn't read this game's code: System.IndexOutOfRangeException: … (exit code 134)"
|
||||
return self._not_applied(ctx, f"{exc}; built without this fix")
|
||||
if not notes: # turned on, but none of its methods is in this game: say so instead of a silent no-op
|
||||
wanted = dict.fromkeys(f"{c.rsplit('/', 1)[-1][:-3]}.{m}"
|
||||
for c, ms in {**self.targets, **self.field_loads}.items() for m in ms)
|
||||
return self._not_applied(ctx, f"code not found in this game (Cpp2IL found none of {', '.join(wanted)}); "
|
||||
"built without this fix")
|
||||
ctx.notes.extend(notes)
|
||||
if patched is not None:
|
||||
ws.put(lib_name, patched)
|
||||
return patched is not None
|
||||
|
||||
|
||||
def _not_applied(self, ctx: ApkContext, why: str) -> bool:
|
||||
"""A WARN build check (the build still succeeds) naming the patch, and the same line in the log."""
|
||||
detail = f"not applied ({self.id}): {why}"
|
||||
ctx.reporter.log(f"{self.check_name}: {detail}")
|
||||
ctx.checks.append((self.check_name, None, detail))
|
||||
return False
|
||||
|
||||
|
||||
class UnityTextInput(Il2cppReturnPatch):
|
||||
id = "frame.unity_text_input"
|
||||
title = "Make Unity text fields work"
|
||||
|
||||
@@ -173,7 +173,8 @@ SETTINGS = [
|
||||
("rect_clamp", "int", 1, "Clamp image rects",
|
||||
"Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image "
|
||||
"on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game "
|
||||
"stops drawing (for example PowerWash Simulator stuck at \"Waiting\")."),
|
||||
"stops drawing (for example PowerWash Simulator stuck at \"Waiting\"). An empty rect (0x0, e.g. BattleTalent's "
|
||||
"first frames, GitHub #293) is rejected the same way and becomes the whole swapchain image."),
|
||||
("swap_eyes", "int", 0, "Swap eyes", "Swap left/right views (diagnostic)."),
|
||||
("strip_depth", "int", 0, "Strip depth layers", "Remove XR_KHR_composition_layer_depth chains (diagnostic)."),
|
||||
("mutable_fix", "int", 0, "Mutable swapchain fix", "Experimental Vulkan mutable-format workaround."),
|
||||
@@ -393,7 +394,7 @@ UI: dict[str, dict] = {
|
||||
"vk_validation": dict(group="troubleshooting", level="advanced", label="Vulkan error checks (log)",
|
||||
help="Logs Vulkan mistakes the game makes. Needs a special test build.", control=("switch",)),
|
||||
"rect_clamp": dict(group="troubleshooting", level="advanced", label="Keep pictures in bounds",
|
||||
help="Stops games from hanging when their eye picture is a little too big (for example "
|
||||
help="Stops games from hanging when their eye picture is a little too big or empty (for example "
|
||||
"PowerWash Simulator). Leave it on.", control=("switch",)),
|
||||
"gl_hide_msrtt": dict(group="troubleshooting", level="advanced", label="Hide fast anti-aliasing (OpenGL ES)",
|
||||
help="For Unity games that crash when they turn on anti-aliasing (for example The Room "
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -209,8 +210,17 @@ def _forgive_headless_end(res: TriageResult, lines: list[str], markers: list[dic
|
||||
|
||||
|
||||
def _secs(line: str) -> float | None:
|
||||
"""A logcat line's time in seconds, its date included (as a day of a leap year): logcat replays old kernel
|
||||
lines dated days before the game's (BattleTalent, GitHub #293), so the time of day alone can't order them."""
|
||||
m = LOGCAT_TIME.match(line)
|
||||
return int(m[1]) * 3600 + int(m[2]) * 60 + int(m[3]) + int(m[4]) / 1000 if m else None
|
||||
if not m:
|
||||
return None
|
||||
month, day = int(m[1]), int(m[2])
|
||||
try:
|
||||
ordinal = date(2000, month, day).timetuple().tm_yday
|
||||
except ValueError:
|
||||
return None
|
||||
return (ordinal * 24 + int(m[3])) * 3600 + int(m[4]) * 60 + int(m[5]) + int(m[6]) / 1000
|
||||
|
||||
|
||||
MISSING_OBB = ("The game keeps its content in a data file (.obb) and none was installed: FramePort found no .obb "
|
||||
@@ -229,22 +239,23 @@ def add_missing_obb(res: TriageResult) -> TriageResult:
|
||||
return res
|
||||
|
||||
|
||||
LOGCAT_TIME = re.compile(r"^\d\d-\d\d (\d\d):(\d\d):(\d\d)\.(\d{3})")
|
||||
LOGCAT_TIME = re.compile(r"^(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\.(\d{3})")
|
||||
|
||||
|
||||
_YEAR = 366 * 86400
|
||||
_HALF_YEAR = _YEAR / 2
|
||||
|
||||
|
||||
def frames_stopped(lines: list[str], gap: float = 20.0) -> float | None:
|
||||
"""Seconds since the last pacing summary (or initial projection submission if none), when over `gap`.
|
||||
Twenty seconds allows several of FrameBridge's 5 s pacing windows before suspecting a stalled frame loop."""
|
||||
def secs(line):
|
||||
m = LOGCAT_TIME.match(line)
|
||||
return int(m[1]) * 3600 + int(m[2]) * 60 + int(m[3]) + int(m[4]) / 1000 if m else None
|
||||
last_projection = last_pacing = last = None
|
||||
for ln in lines: # logcat lines aren't strictly in time order: the latest time counts, not the last line
|
||||
t = secs(ln)
|
||||
t = _secs(ln)
|
||||
if t is None:
|
||||
continue
|
||||
if last is not None and t < last - 43200: # past midnight
|
||||
t += 86400
|
||||
if last is not None and t < last - _HALF_YEAR: # past New Year (12-31 -> 01-01)
|
||||
t += _YEAR
|
||||
last = t if last is None else max(last, t)
|
||||
if re.search(r"FrameBridge\s*: pacing:", ln):
|
||||
last_pacing = t if last_pacing is None else max(last_pacing, t)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Cpp2IL's development build for Unity metadata newer than its release reads (Unity 6.5: v107, GitHub #198)."""
|
||||
import json
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -58,6 +59,7 @@ def test_ensure_nightly_unpacks_the_executable(monkeypatch, tmp_path):
|
||||
def tools(monkeypatch, tmp_path):
|
||||
"""Release + development build stand-ins: the release can't read metadata v107."""
|
||||
monkeypatch.setattr(il2cpp, "cache_dir", lambda: tmp_path)
|
||||
monkeypatch.delenv("FRAMEPORT_CPP2IL", raising=False)
|
||||
release, nightly = Path("release/Cpp2IL"), Path("nightly/Cpp2IL")
|
||||
monkeypatch.setattr(cpp2il, "ensure", lambda progress=None: release)
|
||||
monkeypatch.setattr(cpp2il, "ensure_nightly", lambda progress=None: nightly)
|
||||
@@ -94,7 +96,9 @@ def test_cached_release_error_is_retried_once_with_the_development_build(tools,
|
||||
assert runs == [nightly]
|
||||
|
||||
|
||||
def test_other_errors_dont_fetch_the_development_build(tools, monkeypatch):
|
||||
def test_any_release_failure_tries_the_development_build_once(tools, monkeypatch):
|
||||
"""BattleTalent (Unity 2021.3, GitHub #293): the release crashes (IndexOutOfRangeException in GetTypeName), the
|
||||
development build reads it. Any failure of the release gets the one development-build try."""
|
||||
release, nightly, runs = tools
|
||||
|
||||
def run(exe, *a):
|
||||
@@ -102,6 +106,78 @@ def test_other_errors_dont_fetch_the_development_build(tools, monkeypatch):
|
||||
raise il2cpp._Unreadable("System.NullReferenceException: Object reference not set")
|
||||
|
||||
monkeypatch.setattr(il2cpp, "_run", run)
|
||||
with pytest.raises(RuntimeError):
|
||||
with pytest.raises(RuntimeError, match="couldn't read this game's code: System.NullReferenceException"):
|
||||
il2cpp.find_methods(b"lib", b"meta", "2022.3.1f1", WANTED)
|
||||
assert runs == [release]
|
||||
assert runs == [release, nightly]
|
||||
cached = json.loads(il2cpp._cache_file(b"lib", b"meta").read_text())
|
||||
assert cached["nightly"] is True and "missing" not in cached # an error, never "no such classes"
|
||||
with pytest.raises(RuntimeError): # both failed: not run again per patch
|
||||
il2cpp.find_methods(b"lib", b"meta", "2022.3.1f1", WANTED)
|
||||
assert runs == [release, nightly]
|
||||
|
||||
|
||||
def test_explicit_cpp2il_is_never_swapped(tools, monkeypatch):
|
||||
release, nightly, runs = tools
|
||||
mine = Path("mine/Cpp2IL")
|
||||
|
||||
def run(exe, *a):
|
||||
runs.append(exe)
|
||||
raise il2cpp._Unreadable("System.IndexOutOfRangeException: Index was outside the bounds of the array.")
|
||||
|
||||
monkeypatch.setattr(il2cpp, "_run", run)
|
||||
with pytest.raises(RuntimeError):
|
||||
il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED, cpp2il=mine)
|
||||
assert runs == [mine]
|
||||
il2cpp._cache_file(b"lib", b"meta").unlink()
|
||||
monkeypatch.setenv("FRAMEPORT_CPP2IL", str(release)) # the user's override (ensure() returns it)
|
||||
with pytest.raises(RuntimeError):
|
||||
il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED)
|
||||
assert runs == [mine, release]
|
||||
|
||||
|
||||
def test_poisoned_cache_is_run_again(tools):
|
||||
"""An older FramePort cached a Cpp2IL crash as "every class missing"; such an entry is run again."""
|
||||
release, nightly, runs = tools
|
||||
cache = il2cpp._cache_file(b"lib", b"meta")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text(json.dumps({"unity_version": "2021.3.45f2", "missing": list(WANTED), "classes": {}}))
|
||||
assert il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED) == {"Assembly-CSharp/X.cs": {"Run": (0x1000, 0x20)}}
|
||||
assert runs == [release, nightly]
|
||||
# a genuine result (some classes found) is kept
|
||||
assert il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED) and runs == [release, nightly]
|
||||
|
||||
|
||||
def _fake_cpp2il(tmp_path, script: str) -> Path:
|
||||
exe = tmp_path / "bin" / "Cpp2IL"
|
||||
exe.parent.mkdir(parents=True, exist_ok=True)
|
||||
exe.write_text("#!/bin/sh\n" + script)
|
||||
exe.chmod(0o755)
|
||||
return exe
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="shell-script stand-in for Cpp2IL")
|
||||
def test_cpp2il_crash_after_making_folders_is_unreadable(monkeypatch, tmp_path):
|
||||
"""The release's crash on BattleTalent: the DiffableCs folder tree exists, no .cs file, exit 134. That's an
|
||||
unreadable game (-> the development build), not "none of the classes are in it"."""
|
||||
monkeypatch.setattr(il2cpp, "cache_dir", lambda: tmp_path / "cache")
|
||||
monkeypatch.delenv("FRAMEPORT_CPP2IL", raising=False)
|
||||
crash = _fake_cpp2il(tmp_path / "release", """out=""; while [ $# -gt 0 ]; do [ "$1" = --output-to ] && out=$2; shift; done
|
||||
mkdir -p "$out/DiffableCs/Assembly-CSharp"
|
||||
echo "Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array." >&2
|
||||
echo " at Cpp2IL.Core.Utils.CsFileUtils.GetTypeName(...) in CsFileUtils.cs:line 282" >&2
|
||||
exit 134
|
||||
""")
|
||||
good = _fake_cpp2il(tmp_path / "nightly", """out=""; while [ $# -gt 0 ]; do [ "$1" = --output-to ] && out=$2; shift; done
|
||||
mkdir -p "$out/DiffableCs/Assembly-CSharp"
|
||||
cat > "$out/DiffableCs/Assembly-CSharp/X.cs" <<'CS'
|
||||
\t[Address(RVA = "0x5000", Offset = "0x1000", Length = "0x20")]
|
||||
\tpublic void Run() { }
|
||||
CS
|
||||
""")
|
||||
monkeypatch.setattr(cpp2il, "ensure", lambda progress=None: crash)
|
||||
monkeypatch.setattr(cpp2il, "ensure_nightly", lambda progress=None: good)
|
||||
with pytest.raises(il2cpp._Unreadable, match=r"IndexOutOfRangeException.*\(exit code 134\)"):
|
||||
il2cpp._run(crash, b"lib", b"meta", "2021.3.45f2", WANTED, 60)
|
||||
assert il2cpp.find_methods(b"lib", b"meta", "2021.3.45f2", WANTED) == {"Assembly-CSharp/X.cs": {"Run": (0x1000, 0x20)}}
|
||||
cached = json.loads(il2cpp._cache_file(b"lib", b"meta").read_text())
|
||||
assert cached["nightly"] is True and cached["classes"]
|
||||
@@ -206,6 +206,28 @@ def test_frames_stopped_and_unity_render_crash():
|
||||
assert triage.frames_stopped(["10-05 23:16:30.000 x FrameBridge: pacing: 72 fps", "10-05 23:16:40.000 y"]) is None
|
||||
|
||||
|
||||
def test_frames_stopped_orders_replayed_old_lines_by_date():
|
||||
"""BattleTalent (GitHub #293): logcat replays kernel lines dated days before the game's ahead of them; by time
|
||||
of day alone they looked like "past midnight" and a healthy run failed with frames-stopped (83261 s)."""
|
||||
from frameport.validate import triage
|
||||
|
||||
log = ("10-16 00:16:59.468 18 18 I hwservicemanager: hwservicemanager is ready now.\n"
|
||||
"10-14 00:40:12.674 16 16 W auditd : type=2000 audit(0.0:1): state=initialized\n"
|
||||
"10-14 18:25:54.684 0 0 I binder : 418000 RLIMIT_NICE not set\n"
|
||||
"10-14 23:59:59.000 0 0 I binder : 419099 RLIMIT_NICE not set\n"
|
||||
"10-15 02:10:00.000 0 0 I binder : 419099 RLIMIT_NICE not set\n"
|
||||
"10-15 23:50:00.000 0 0 I binder : 419099 RLIMIT_NICE not set\n"
|
||||
"10-16 00:17:33.725 1193 1274 I FrameBridge: pacing: 70.2 fps, displayTime vs predicted: avg 15 ms\n"
|
||||
"10-16 00:18:08.734 1193 1274 I FrameBridge: pacing: 72.2 fps, displayTime vs predicted: avg 13 ms\n"
|
||||
"10-16 00:18:13.837 1193 1274 I FrameBridge: layer_debug: frames: 361 xrBeginFrame\n")
|
||||
assert triage.frames_stopped(log.splitlines()) is None
|
||||
assert "frames-stopped" not in [f.id for f in triage.triage(log, "RUNNING").findings]
|
||||
# a real stop still counts, also across midnight and New Year
|
||||
assert triage.frames_stopped(["10-15 23:59:50.000 x FrameBridge: pacing: 72 fps", "10-16 00:00:30.000 y"]) == 40
|
||||
assert triage.frames_stopped(["12-31 23:59:50.000 x FrameBridge: pacing: 72 fps", "01-01 00:00:30.000 y"]) == 40
|
||||
assert triage.frames_stopped(["12-31 23:59:50.000 x FrameBridge: pacing: 72 fps", "01-01 00:00:05.000 y"]) is None
|
||||
|
||||
|
||||
def test_vrapi_called_before_init_suggests_the_stub_not_the_bridge():
|
||||
log = ("10-06 02:04:56.040 1154 1180 I OVRPlugin: OVRPlugin 1.89.1 initialized\n"
|
||||
"10-06 02:04:56.324 1154 1180 F VrApiLoader: vrapi_SetPropertyInt was called before vrapi_Initialize()!\n")
|
||||
|
||||
@@ -408,3 +408,77 @@ def test_vermillion_canvas_stays_in_the_scene():
|
||||
game = _analysis(package=V.PACKAGE, libs=["libil2cpp.so", "libunity.so"])
|
||||
assert p.applies(game) and p.detect(game) is None
|
||||
assert not p.applies(_analysis(package="com.other.game", libs=["libil2cpp.so"]))
|
||||
|
||||
|
||||
class _FakeWs:
|
||||
abi = "arm64-v8a"
|
||||
|
||||
def __init__(self, lib):
|
||||
self.files = {"lib/arm64-v8a/libil2cpp.so": lib, U.METADATA: b"meta"}
|
||||
|
||||
def lib(self, name):
|
||||
return f"lib/arm64-v8a/{name}"
|
||||
|
||||
def has(self, name):
|
||||
return name in self.files
|
||||
|
||||
def read(self, name):
|
||||
return self.files[name]
|
||||
|
||||
def put(self, name, data):
|
||||
self.files[name] = data
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
def write(self, path):
|
||||
pass
|
||||
|
||||
|
||||
def test_il2cpp_patch_with_no_code_found_warns_instead_of_a_silent_skip(monkeypatch):
|
||||
"""GitHub #293: BattleTalent's build had unity_runtime_msaa_off/multipass on, Cpp2IL found none of their methods
|
||||
and the build said nothing. Now a WARN check (not a failure) names the patch."""
|
||||
from frameport import build
|
||||
from frameport.core.events import Reporter
|
||||
|
||||
base.load_all()
|
||||
monkeypatch.setattr(il2cpp, "find_methods", lambda *a, **kw: {})
|
||||
events = []
|
||||
game = _analysis(extra={"unity_version": "2021.3.45f2"})
|
||||
ctx = base.ApkContext(_FakeWs(_lib()), game, {}, Reporter([events.append]), {})
|
||||
patch = base.get("frame.unity_runtime_msaa_off")
|
||||
assert patch.apply(ctx) is False and not ctx.notes
|
||||
(name, ok, detail), = ctx.checks
|
||||
assert name == patch.check_name and ok is None
|
||||
assert "frame.unity_runtime_msaa_off" in detail and "code not found" in detail
|
||||
assert "get_recommendedMSAALevel" in detail and detail.count("OVRDisplay.get_recommendedMSAALevel") == 1
|
||||
assert any("code not found" in e.message for e in events if e.kind == "log")
|
||||
|
||||
# the build turns it into a build check: a WARN, so BuildResult.ok stays true
|
||||
monkeypatch.setattr(build, "ApkWorkspace", lambda path: _FakeWs(_lib()))
|
||||
monkeypatch.setattr(build.upstream, "resolve", lambda ws, patches, reporter: build.upstream.Resolution(patches))
|
||||
recipe = type("Recipe", (), {"patches": {"frame.unity_runtime_msaa_off": {}}})()
|
||||
applied, checks, _ = build.apply_frame_fixes(Path("in.apk"), Path("out.apk"), game, recipe, Reporter())
|
||||
assert applied == [] and checks == [{"name": patch.check_name, "ok": None, "detail": detail}]
|
||||
|
||||
|
||||
def test_il2cpp_patch_says_when_cpp2il_couldnt_read_the_game(monkeypatch):
|
||||
"""Both Cpp2IL builds failed: the WARN names Cpp2IL's error, not "code not found" (GitHub #293)."""
|
||||
from frameport.core.events import Reporter
|
||||
|
||||
base.load_all()
|
||||
error = ("Cpp2IL couldn't read this game's code: System.IndexOutOfRangeException: Index was outside the bounds "
|
||||
"of the array. (exit code 134)")
|
||||
|
||||
def fail(*a, **kw):
|
||||
raise RuntimeError(error)
|
||||
|
||||
monkeypatch.setattr(il2cpp, "find_methods", fail)
|
||||
ctx = base.ApkContext(_FakeWs(_lib()), _analysis(extra={"unity_version": "2021.3.45f2"}), {}, Reporter(), {})
|
||||
patch = base.get("frame.unity_multipass")
|
||||
assert patch.apply(ctx) is False
|
||||
(name, ok, detail), = ctx.checks
|
||||
assert ok is None and "frame.unity_multipass" in detail and error in detail and "code not found" not in detail
|
||||
Reference in new issue
Block a user