You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Hard freeze (issue #19 signature) on Galaxy Z Fold 7 / Snapdragon 8 Elite after entering the first house #23
Device: Samsung Galaxy Z Fold 7 (SM-F966U), Snapdragon 8 Elite (SM8750), Adreno 830, 10.7 GB RAM, Android 17 (API 37), cover display 2520x1080
App: LAS Recomp for Android 1.2.0 (the log header still says "beta_4")
Game: detected as 1.0.0 (main build ID AE16F71E002AF8CB, address table v1_0_0), set up on the phone from an .xci
GPU driver: the phone's own Qualcomm Adreno Vulkan driver (not the bundled Turnip), Authentic 16:9, 1x, 30 fps (no generated frames in the log)
What happens
Title screen -> new game -> first house (MarinTarinHouse) -> talk to an NPC -> the game freezes. The app process stays alive (no crash), the screen is a still image and nothing responds to touch.
It happened every time I tried (3 runs), including after "Clear data" + a fresh on-device game build + a brand-new save.
Log evidence
Right after the level load:
[delegate] 0x12778af0c8 slot +0x28: no delegate at [this+0x10] (vtable 0x1001c51358, count 0x1, lr 0x10013c229c, owner 0x12778af000 done 0 lock 0) - skipped after 0 us (issue When I open the app, this appears. #19)
This is the "base vtable, no delegate, count 1, done 0, X unlocked" case described in delegate_null.c (second thread dump).
There is no "[delegate] frame job ... rebuild waited" line anywhere in the log, so the kick-off wait never acted.
~30 s later the hang dump:
[hang] ===== main guest thread in one kernel wait for 30 s; presenting stopped =====
[hang] render thread on; queued 12560, done 12560, running 0, presents pending 0, presents done 3479, game frames 3479
Main guest thread (thread 80, h 101): wait condvar addr 12778af204 mutex 12778af1f8 (mutex word 0), frames ... 10010cd0f8, 10010c6b58, 1000017734 ...
That is the condition variable of the same owner X = 0x12778af000 that the delegate guard reported (X+0x204, mutex X+0x1f8).
All worker threads are idle on their condvars (30-147 s), the render queue is empty. The audio threads keep running normally.
My reading (could be wrong)
rt_frame_job_kickoff only waits when X+0x1d8 & 1 is set and J is still live. At the time of the skipped release, done is 0 and J already has the base vtable, so that condition is not met and the late finish wipes the rebuilt J. This CPU is much faster and has 8 cores, which may open a window the S20 FE does not.
Question: would a fallback in rt_delegate_null be acceptable for this case (when a release of a wiped frame job is skipped and X+0x1d8 is 0: set X+0x1d8 = 1 and signal X+0x204, losing that frame job's work)? I can test any build you send, and I can attach more hang dumps.
A source-level fix proposal for this freeze signature, based on android-v1.2.0 (cca791a). This differs from the closed #24 proposal: it restores the synchronization hooks and prevents premature job reuse, rather than marking a stale frame job complete. Two issues need to be addressed:
The Android hook dispatcher compares addresses from different layouts.gen.py --shift emits rt_hook(c, site) with the runtime guest address, but hook_id() compares it with the unshifted Switch address stored in g_ga. For example, the frame-job kick-off runs at 0x1000015780 on Android, whereas the table stores 0x7000015780. The result is hook_id() == -1: the frame-job wait, owner-call notification guard, and other runtime hooks are silently ignored. Using the existing GA_AT() helper normalizes the table entry. Desktop behavior is unchanged because its shift is zero.
The frame-job wait's entry guard misses a partial finish. A worker can be preempted after storing the base vtable but before clearing the delegate. The current outer guard returns immediately once the vtable is no longer live, even though the delegate-clear store is still pending. That late store can erase the next frame's newly installed delegate. Accept both a live job and a base-vtable job with a nonempty delegate as unfinished; require the base vtable plus an empty delegate before reuse. This retains the existing bounded wait and does not synthesize completion or discard a job. The address fix above is necessary for this synchronization hook to execute on Android at all.
Minimal runtime-only diff:
diff --git a/runtime/hle/delegate_null.c b/runtime/hle/delegate_null.c
index b015ec4..518905f 100644
--- a/runtime/hle/delegate_null.c+++ b/runtime/hle/delegate_null.c@@ -80,13 +80,20 @@ void rt_frame_job_kickoff(uint64_t owner) {
}
uint64_t job = owner + 0xc8;
if (off || !owner || !(__atomic_load_n((uint8_t *)(uintptr_t)(owner + 0x1d8), __ATOMIC_ACQUIRE) & 1) ||
- rd64(job) != GA(DELEGATE_JOB_VTABLE_LIVE) || !frame_job(job))+ !frame_job(job))
return; // (the usual case: no run of J is still finishing)
+ uint64_t vt = rd64(job);+ if (vt != GA(DELEGATE_JOB_VTABLE_LIVE) &&+ (vt != GA(DELEGATE_JOB_VTABLE_BASE) || !rd64(job + 0x10)))+ return; // fully finished, or not this frame-job class
uint64_t t0 = plat_now_ns(), waited = 0;
bool finished;
// The finish (slot +0x00) is two stores: the base vtable, then [J+0x10] = 0. Wait for both - a worker preempted
// between them would otherwise clear the delegate of the J being rebuilt (the kick-off sets it non-zero).
- while (!(finished = rd64(job) != GA(DELEGATE_JOB_VTABLE_LIVE) && !rd64(job + 0x10)) &&+ // The entry guard must not require the live vtable: the worker may ALREADY have stored the base vtable but+ // still owe its delegate-clear store. Returning in that window allowed the next kick-off to rebuild J, only+ // for the old finish to clear the new delegate (Android #23; main then waits forever with done=0).+ while (!(finished = rd64(job) == GA(DELEGATE_JOB_VTABLE_BASE) && !rd64(job + 0x10)) &&
(waited = plat_now_ns() - t0) < 1000000000ull)
plat_sleep_until_ns(plat_now_ns() + 20000);
static int logged;
diff --git a/runtime/hle/hooks.c b/runtime/hle/hooks.c
index ebe9dec..941e2c1 100644
--- a/runtime/hle/hooks.c+++ b/runtime/hle/hooks.c@@ -63,7 +63,9 @@ static const size_t hook_field[] = {
static int hook_id(uint64_t site) {
for (size_t i = 0; i < sizeof hook_field / sizeof hook_field[0]; i++)
- if (*(const uint64_t *)((const char *)g_ga + hook_field[i]) == site) return (int)i;+ // gen.py passes the instruction's runtime address. Android shifts the+ // guest down by GA_SHIFT; the table itself retains Switch addresses.+ if (GA_AT(hook_field[i]) == site) return (int)i;
return -1;
}
Validation performed without publishing any game files:
Before the address fix, a dispatcher regression passes with the desktop address layout but fails with the Android layout: hook_id(GA_AT(hook_field[i])) != i.
After the fix, every hook ID maps correctly for both supported game-address tables. Calling all four frame-job kick-off sites through the actual rt_hook() reaches the actual synchronization function and waits for a simulated partial finish.
The deterministic partial-finish regression fails against the original wait guard and passes with the revised guard. Completed jobs, first kick-off, and unrelated objects retain their behavior.
The combined dispatcher/synchronization regressions pass with both address layouts on the host and as native ARM64 code on Android.
In a short on-device gameplay check, the previously failing transition completed. The frame-job wait was observed in the runtime diagnostics and normal rendering continued. Longer-session stability remains to be established.
These changes are confined to the runtime; the existing generated game code already contains the hook calls, so a game-code rebuild is not required.
This is the configuration I used and it worked extremely well for me.
-Aspect Ratio: 16:9
-Full Screen: Off
(These two are mandatory to avoid screen freezing and significantly reduce crashes)
Internal Resolution: Either one, but I recommend more of a 75% - 50% ratio.
-Upcaler: Bilinear (this is mandatory from what I've seen)
-Frame rate: 60 fps (This also seems to be mandatory)
-Driver: Turnip Adreno Driver T28-toasted (@Mr_Purple_666) 26.2.0-T28-toasted 1.4.348 (Of all of them, it worked best for me, but none of them remove insignificant visual errors.
-With this configuration, I completely eliminated screen freezes, although there are still minor crashes.
-It's impossible to go under the bridge to give the fisherman the hook; I recommend playing that part of the game on an emulator and continuing on the port for a better experience.
-Snapdragon 888
Activity
c-hui commentedon Oct 24, 2026
A source-level fix proposal for this freeze signature, based on android-v1.2.0 (
cca791a). This differs from the closed #24 proposal: it restores the synchronization hooks and prevents premature job reuse, rather than marking a stale frame job complete. Two issues need to be addressed:The Android hook dispatcher compares addresses from different layouts.
gen.py --shiftemitsrt_hook(c, site)with the runtime guest address, buthook_id()compares it with the unshifted Switch address stored ing_ga. For example, the frame-job kick-off runs at0x1000015780on Android, whereas the table stores0x7000015780. The result ishook_id() == -1: the frame-job wait, owner-call notification guard, and other runtime hooks are silently ignored. Using the existingGA_AT()helper normalizes the table entry. Desktop behavior is unchanged because its shift is zero.The frame-job wait's entry guard misses a partial finish. A worker can be preempted after storing the base vtable but before clearing the delegate. The current outer guard returns immediately once the vtable is no longer live, even though the delegate-clear store is still pending. That late store can erase the next frame's newly installed delegate. Accept both a live job and a base-vtable job with a nonempty delegate as unfinished; require the base vtable plus an empty delegate before reuse. This retains the existing bounded wait and does not synthesize completion or discard a job. The address fix above is necessary for this synchronization hook to execute on Android at all.
Minimal runtime-only diff:
Validation performed without publishing any game files:
hook_id(GA_AT(hook_field[i])) != i.rt_hook()reaches the actual synchronization function and waits for a simulated partial finish.These changes are confined to the runtime; the existing generated game code already contains the hook calls, so a game-code rebuild is not required.
sixela44916p commentedon Nov 1, 2026
This is the configuration I used and it worked extremely well for me.
-Aspect Ratio: 16:9
-Full Screen: Off
(These two are mandatory to avoid screen freezing and significantly reduce crashes)
Internal Resolution: Either one, but I recommend more of a 75% - 50% ratio.
-Upcaler: Bilinear (this is mandatory from what I've seen)
-Frame rate: 60 fps (This also seems to be mandatory)
-Driver: Turnip Adreno Driver T28-toasted (@Mr_Purple_666) 26.2.0-T28-toasted 1.4.348 (Of all of them, it worked best for me, but none of them remove insignificant visual errors.
-With this configuration, I completely eliminated screen freezes, although there are still minor crashes.
-It's impossible to go under the bridge to give the fisherman the hook; I recommend playing that part of the game on an emulator and continuing on the port for a better experience.
-Snapdragon 888