Compare commits

...
Author SHA1 Message Date
spoopyghosty0 020a0251b1 Cpp2IL crashes aren't "code not found"; triage orders replayed logcat lines by date (#293)
- il2cpp._run: a non-zero exit or no .cs file under DiffableCs is an unreadable game. The release Cpp2IL dies on
  BattleTalent (Unity 2021.3.45f2, exit 134, IndexOutOfRangeException in CsFileUtils.GetTypeName) after making the
  folder tree; every class was recorded "missing" and cached.
- find_methods: any release failure gets one try with the development build (it reads BattleTalent), not only an
  unsupported metadata version; never with a caller's Cpp2IL or FRAMEPORT_CPP2IL. Errors are cached as errors,
  never as missing classes; cache entries with no class found and every wanted class missing are run again.
- Il2cppReturnPatch: Cpp2IL failures become the same WARN check as "code not found" but with Cpp2IL's error line;
  the method list in the WARN has no duplicates.
- triage: logcat times include the date (logcat replays kernel lines dated days earlier ahead of the game's; by time
  of day the "past midnight" guess gave a healthy run frames-stopped 83261 s). Year wrap handled.
2026-10-15 20:27:36 -04:00
spoopyghosty0 98aeff5263 Unity IL2CPP fixes warn when they find nothing; FrameBridge fills empty image rects (#293)
- Il2cppReturnPatch (unity_runtime_msaa_off, unity_multipass, unity_text_input, ...): a patch that is turned on
  but whose methods Cpp2IL doesn't find in the game now logs it and adds a WARN build check naming the patch
  ("not applied (<id>): code not found in this game ...") instead of skipping silently. ApkContext.checks =
  checks a patch reports while applying; apply_frame_fixes adds them to the build's checks (job/CLI output,
  build record). WARN only: the build still succeeds.
- FrameBridge rect_clamp: an empty projection/quad image rect (BattleTalent's first frames: 0,0 0x0 ->
  xrEndFrame -25 twice, then OVRPlugin stops submitting) becomes the whole swapchain image when its size is
  known; counted as swapped, logged ("rect_clamp: view N empty image rect -> full swapchain WxH"). Adapter
  revision unchanged; artifacts rebuilt.
- triage swapchain-rect-invalid, PLAYBOOK row, setting texts, PATCHES.md, i18n template.
2026-10-15 20:21:26 -04:00
18 changed files with 272 additions and 40 deletions

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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`) |
+26 -7
View File
@@ -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);
}
}
}
+20 -8
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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.": "",
+3
View File
@@ -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"
+3 -2
View File
@@ -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 "
+19 -8
View File
@@ -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)
+79 -3
View File
@@ -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"]
+22
View File
@@ -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")
+74
View File
@@ -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