Switch Emulation on a Snapdragon 8 Gen 2 Handheld: Why a Bigger Memory Layout Is Less Stable
My AYN Thor handheld runs a Snapdragon 8 Gen 2, and I mostly use it to play Switch games through Eden. For a stretch, Zelda: Tears of the Kingdom kept “dropping to the background” or crashing outright, on both the stable build and the nightly. My first guess was a memory shortage — the device has 15GB, so I pushed Eden’s memory layout all the way up to 8GB. The problem got worse.
This is the walk back from that counterintuitive result.
Dropping to the background and crashing are two separate problems
I treated the two symptoms as one issue until the logs showed entirely different causes. The test is simple: check whether the Eden process still exists. When it “drops to the background,” the process is alive and only its EmulationActivity was stopped. When it crashes, the process is gone and leaves a tombstone.
flowchart TB
A["Game drops to background or crashes"] --> B{"Is the Eden process still alive?"}
B -->|"Alive, pushed to background"| C["lmkd memory pressure<br/>kills the iiSU launcher<br/>launcher restarts and grabs foreground"]
B -->|"Process gone"| D["GPU pipeline crash<br/>shader cache mismatches current driver"]
C --> E["Memory layout 8GB → 6GB → 4GB"]
D --> F["Delete driver-specific shader cache<br/>confirm Turnip is loaded"]Once separated, each problem came with a clear chain of evidence.
Dropping to the background: lmkd kills the launcher, not the game
logcat kept repeating the same lines:
lowmemorykiller: Kill 'com.iisulauncher' (15071) ... reason: critical pressure and device is low on memory
lowmemorykiller: Kill 'com.iisulauncher' (16254) ... reason: critical pressure
lowmemorykiller: Kill 'com.iisulauncher' (16271) ... reason: critical pressureThe victim was the iiSU launcher, and it was a string of ever-changing pids. iiSU’s SecondaryHomeActivity was being relaunched by the system over and over, and each relaunch stopped Eden’s EmulationActivity (wm_stop_activity ... STOP_ACTIVITY_ITEM). What looked like “the game drops to the background and the launcher comes forward” was really memory pressure hitting the front end; the Eden process never died. That also explains why the nightly build didn’t help — its relationship with the launcher over memory is unchanged.
The thing to fix was the memory pressure, and the source of that pressure was the 8GB layout I had just chosen.
Why a bigger memory layout is less stable
Eden’s memory_layout_mode has three settings: 0 is 4GB, 1 is 6GB, 2 is 8GB, the cap on memory handed to the emulated “guest” Switch. Since the real Switch has 4GB, more sounds strictly better. But this device’s RAM is not all Eden’s: in practice the Eden process sat at roughly 9.2GB PSS, of which GPU shared memory (GL mtrack, the VRAM) regularly took 3.3GB, plus about 3.4GB of Other mmap (the guest’s 4GB mapping) and 1.9GB of Native heap. At the 8GB layout, guest memory, VRAM and Android itself pushed the whole machine to the edge.
The key fact is what lmkd actually measures: memory pressure (PSI / thrashing), not how much free memory remains. So even with a panel showing 11–12GB used and “plenty left,” lmkd still kills once the system hits critical pressure. Dropping the layout from 8GB to 6GB eased it immediately, and settling on 4GB (the Switch’s native size) stopped the launcher from being killed altogether. The conclusion is counterintuitive: the extra 4GB did not make the game smoother; it squeezed out the headroom the system needed to keep its foreground alive.
Crashes: the shader cache is driver-specific
The other problem was a real crash. Both tombstones stopped at the same point:
SIGSEGV vulkan.adreno.so qglinternal::vkCreateGraphicsPipelines(...)
← Vulkan::vk::Device::CreateGraphicsPipeline ← GraphicsPipeline::MakePipeline
the other: libllvm-qgl.so CreateQGLCShader(...) ← vulkan.adreno.soThe crash sessions reported Qualcomm Technologies Inc. Adreno Vulkan Driver 512.676.53, while healthy sessions reported PurpleVK public driver 26.0.99 — Turnip. The root cause is that the pipeline cache under Eden’s files/cache/shader/<titleid>/ is driver-specific. After switching or reverting the driver, the old cache no longer matches the new driver and the pipeline creation crashes. Tears of the Kingdom’s cache was 53MB, with a 117MB vulkan.bin beside it. Deleting cache/shader/0100f2c0115b6000/ to force a rebuild, and making sure the GPU driver was back on Turnip, ended the crashes.
There was a subtlety worth noting: the config’s [GpuDriver] driver_path pointed at T30, yet Adreno was actually loaded — the custom driver never took effect. Trusting the settings screen would have suggested Turnip was already in use; the reported driver name in the crash log is what exposed it.
Getting Tears of the Kingdom stable: driver and VRAM trade-offs
With the cache cleared and the driver back on Turnip, the crashes stopped, but Tears of the Kingdom is heavy for this SoC regardless. Measured frame rate was about 25–29 FPS, GPU-bound: at the 8GB layout the GPU sat pinned around 680MHz and 80% utilisation while the CPU stayed near 50%. The device performance mode matters too — “performance” pins clocks at 680MHz and then throttles under heat, making frame times less stable; switching to “normal/balanced” (performance_mode=0) lets clocks scale on demand and produces steadier pacing.
VRAM is the tighter resource. Enabling ASTC recompression for Tears of the Kingdom alone (astc_recompression\use_global=false plus astc_recompression=1 in config/custom/<TitleID>.ini) cut GL mtrack from 3.32GB to 2.07GB, Eden’s PSS from about 9.2GB to 7.82GB, and pushed system-available memory from 3.6GB back to 5.0GB, while frame rate held near the displayed ~60FPS (internally ~30 with frame generation, standard deviation 4–5ms). The trade-off is moving where decompression happens to regain VRAM headroom. Related: Tears of the Kingdom uses more VRAM after the 1.2.1 → 1.4.2 update, so the upgrade only stays stable paired with the 4GB layout.
Reading performance numbers without the on-screen overlay
For most of this I read the numbers directly over adb rather than trusting the emulator overlay:
# frame rate: find the game's SurfaceView layer, then compute actualPresent stamps
adb shell "dumpsys SurfaceFlinger --list | grep 'SurfaceView.*EmulationActivity'"
adb shell "dumpsys SurfaceFlinger --latency '<layer name>'"
# GPU: utilisation and current/max clock
adb shell "cat /sys/class/kgsl/kgsl-3d0/gpu_busy_percentage"
adb shell "cat /sys/class/kgsl/kgsl-3d0/gpuclk /sys/class/kgsl/kgsl-3d0/max_gpuclk"
# per-core CPU frequency
adb shell "for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq; do cat $c; done"
# memory: GL mtrack (VRAM) and Native heap
adb shell "dumpsys meminfo <eden_pid>"
# device performance mode (0 = normal/balanced)
adb shell "settings get system performance_mode"With these, “why is the frame rate unstable” stops being a guess: GPU near 100% with CPU idle points to a GPU bottleneck, while a wide frame-time standard deviation usually means throttling or memory pressure rather than insufficient CPU.
Two quieter bugs
While digging through memory I confirmed two more. One is games that refuse to launch: some per-game configs carry speed_limit=0, and the core timing code computes speed_scale = 100.f / SpeedLimit(), which divides by zero and takes the game down; setting it to 100 fixes it (grep -l '^speed_limit=0$' config/custom/*.ini finds them all). The other is asynchronous shaders, which reduce stutter on new effects but once crashed on a vulkan.adreno.so ← Vulkan::Scheduler::WorkerThread stack — stability traded for smoothness, decided per game.
In hindsight, the bottleneck for Switch games on this handheld was almost never raw compute; it was how memory and VRAM were allocated. That shrinking guest memory from 8GB back to 4GB made things more stable is the one result I want to remember.