Reference reconciled: 2026-09-10 America/Chicago.
Authority: this file is a detailed tuning/reference catalog, not an independent SSOT. Read facts.md first and project.md second; when this file conflicts with them, the authority records govern until validated newer evidence is used to update those records.
Hardware target: Apple M4 Mac mini (Mac16,10), 10 physical CPU cores (4 performance + 6 efficiency), 10 GPU cores, 16 GiB unified memory, macOS 26.6.2 / build 25G83. Fixed CPU/GPU MHz values are not project-authoritative.
Current DEV execution: TFTMAC DEV 2.3.0 build 8, 1920×1080 / 320 dpi / 60 Hz, 8 vCPU / 6144 MiB, host GPU/CoreAudio, OpenGL ES through ANGLE, current winner DEV-B8-WIN-01. vCPUs are virtual scheduling resources; they do not reserve or dedicate an equal number of physical M4 host cores.
DEVELOPMENT TARGET LOCK — 2026-09-04: The protected Control app at
/Applications/TFTMAC.appis the stable known-good launcher and is not a development target. All new settings, UI, graphics, runtime, and experiment changes go to/Applications/TFTMAC DEV.app/advanced_diagnosticsfirst. DEV may be promoted into a full production release only through a separate explicit Flash-authorized release process after acceptance. Until then, no DEV build/install step may overwrite or mutate Control.
The graphics and gameplay pipeline operates as four serial stages:
Every bottleneck discovered in testing maps directly to one of these four operational layers.
┌──────────────────────────────────────────────────────────────────────────────┐
│ Layer 1: Unreal Engine (Inside TFT APK) │
│ • DeviceProfiles.ini • GameUserSettings.ini • CVars │
└──────────────────────────────────────┬───────────────────────────────────────┘
│ BLASTBufferQueue
┌──────────────────────────────────────▼───────────────────────────────────────┐
│ Layer 2: Android Guest OS & Compositor (Android 16 Emulator) │
│ • SurfaceFlinger • Hardware Display HAL • Process Nice Priority │
└──────────────────────────────────────┬───────────────────────────────────────┘
│ gfxstream ASG Ring Buffer & gRPC
┌──────────────────────────────────────▼───────────────────────────────────────┐
│ Layer 3: Virtualization & Hardware Bus (QEMU / Apple Hypervisor) │
│ • PCI BAR • ASG Shared Memory • -vsync-rate Timer │
└──────────────────────────────────────┬───────────────────────────────────────┘
│ streamScreenshot RPC
┌──────────────────────────────────────▼───────────────────────────────────────┐
│ Layer 4: Host Native App & Metal Presenter (macOS) │
│ • SwiftNIO HTTP/2 • Frame Mailbox • CAMetalLayer Presenter │
└──────────────────────────────────────────────────────────────────────────────┘
Riot's current TFT mobile client is verified to use Unreal Engine. This project does not claim a specific Unreal major version without direct current evidence. On Android, the observed configuration path uses Unreal Device Profiles and user settings.
- Target Guest File:
/sdcard/Android/data/com.riotgames.league.teamfighttactics/files/UnrealGame/TFT/TFT/Saved/Config/Android/DeviceProfiles.ini - Provisioned By:
TFTMACRuntime.swift -> provisionTFTDeviceProfiles
| Setting / CVar | Stock Android Value | TFTMAC High-Perf Value | Operational Impact & Rationale |
|---|---|---|---|
tft.DefaultFrameRateLimit |
30 |
60 |
Unlocks 60 FPS nominal budget during boot, splash, menus, and asset loading (bypasses mobile battery-saving throttle). |
t.MaxFPS |
0 or 30 |
60 |
Hard clamp on the engine rendering loop; matches the 60 Hz hardware VSYNC clock. |
r.VSync |
0 (or dynamic) |
1 |
Forces strict VSYNC pacing; eliminates tearing and prevents CPU spin-waiting. |
a.StripOddFramesWhenFrameStripping |
1 |
0 |
Restores full 60 FPS champion animations. Stock profile strips every odd animation frame to save mobile CPU. |
a.StripFramesOnCompression |
1 |
0 |
Disables animation compression frame stripping. |
r.SkeletalMeshForceLOD |
1 |
0 |
Prevents forced low-detail LOD on champion and Little Legend 3D models. |
r.Streaming.PoolSize |
300 |
1000 |
Expands texture streaming memory pool to 1 GB (utilizes M4 unified memory). |
r.Streaming.PoolSizeForMeshes |
25 |
250 |
Eliminates combat hitching. Stock 25 MB pool forced constant disk paging during 8-player combat; 250 MB keeps all units cached. |
r.RenderTargetPoolMin |
0 |
100 |
Pre-allocates 100 MB of render targets (3x mobile forward-shading requirement) while freeing 300 MB of guest RAM for OS disk/asset page caching. |
r.pso.PrecompileThreadPoolSize |
historical 4 |
current value must be read from the verified DEV profile/receipt | Controls background PSO compilation concurrency. It does not permanently reserve physical or virtual cores for GameThread/RHIThread; validate any change as a one-factor DEV candidate. |
p.ClothPhysics |
0 |
1 |
Enables cape and cloth simulation physics on champions (~0.5 ms CPU budget under <=30 champions). |
grass.Enable |
0 |
1 |
Enables 3D interactive arena foliage and grass. |
r.MaterialQualityLevel |
0 (Low) |
1 (High) |
Enables full PBR material shaders and normal maps. |
tft.Audio.DeviceTier |
Low |
High |
Restores full-fidelity audio assets and spatial effects. |
tft.Audio.PlayOnlyOneArenaAtATime |
true |
false |
Enables full board audio staging without artificial sound muting. |
tft.Audio.RestrictNumberOfAmbientSounds |
true |
false |
Restores high-definition ambient environment audio. |
r.MobileContentScaleFactor |
0.0 (dynamic) |
1.0 |
Locks rendering scale 1:1 with the physical 1080p display buffer. |
r.DynamicRes.FrameTimeBudget |
Dynamic | 16.666666 |
Anchors dynamic resolution scaling directly to a 16.67 ms (60 FPS) frame budget. |
r.DynamicRes.MinScreenPercentage |
70 |
85 |
Provides emergency 15% load-shedding relief floor during particle bursts while rendering at 100% (native 1080p) during standard play. |
Configured automatically over adb during the boot and gameplay preparation phase.
| Setting / Property | Command / Property | Value | Impact & Bottleneck Resolved |
|---|---|---|---|
| System Min Refresh Rate | settings put system min_refresh_rate |
60.0 |
Prevents Android 16 display scheduler from dropping refresh rate when UI is idle. |
| System Peak Refresh Rate | settings put system peak_refresh_rate |
60.0 |
Prevents SurfaceFlinger from phase-jittering between 60 Hz and 120 Hz. |
| SurfaceFlinger Timestamping | setprop service.sf.present_timestamp |
1 |
Enables hardware presentation timestamps required for accurate jitter/latency measurement. |
| SurfaceFlinger Debug Overlay | setprop debug.sf.showupdates |
0 |
Disables debug visual updates that waste GPU rasterization. |
| TFT Process Priority | renice -n -20 -p <pid> |
requested negative nice value | Requests higher CFS scheduling preference for the TFT process. nice -20 is not Linux real-time scheduling and does not guarantee that background work cannot preempt game threads. |
| Background Wellbeing Daemon | pm disable-user com.google.android.apps.wellbeing |
Disabled | Eliminates boot-time CPU storms from GMS app wellbeing and usage tracking scanners. |
| Virtual AC Power | dumpsys battery set ac 1 |
1 |
Forces virtual AC power state; stops Android from entering battery saver mode. |
| Stay Awake While Plugged In | settings put global stay_on_while_plugged_in |
7 |
Prevents screen timeout or lockscreen activation during benchmarks. |
| Low-Latency Audio Sink | setprop debug.stagefright.audio.sink |
1 |
Routes game audio through low-latency stagefright direct sink. |
| Audio Fast Track Multiplier | setprop af.fast_track_multiplier |
2 |
Expands fast-track mixer ring buffer to eliminate audio crackling. |
| Deep Audio Buffer | setprop audio.deep_buffer.media |
1 |
Prevents audio underruns during burst disk I/O. |
| SurfaceFlinger Latch Unsignaled | setprop debug.sf.latch_unsignaled |
1 |
Bypasses fence wait locks when GPU finishes ahead of compositor latch. |
| SurfaceFlinger GL Backpressure | setprop debug.sf.enable_gl_backpressure |
0 |
Disables compositor backpressure stalls. |
The translation layer between the guest Linux kernel and the macOS host hardware.
| Setting / Parameter | Location | Value | Critical Hardware Constraint |
|---|---|---|---|
| ASG Write Buffer Size | asgWriteBufferSize in AVD config |
1_048_576 (1 MiB) |
HARDWARE PINNED: Exceeding 1 MiB causes fatal QEMU PCI BAR crash (External address size too small). Must never exceed 1 MiB. |
| Hardware VSYNC Rate | Emulator argument -vsync-rate |
60 |
Hardware interrupt clock for the virtual display controller. Aligns 1:1 with 16.666 ms frame intervals. |
| GPU Acceleration Mode | Emulator argument -gpu |
current DEV host |
Uses the stock emulator host-accelerated gfxstream path. Current TFT selects OpenGL ES through ANGLE; Vulkan/gfxstream/MoltenVK/Metal are downstream layers. |
| Guest vCPU Allocation | effective launch profile / QEMU -cores |
8 current DEV |
Gives the guest 8 virtual CPUs scheduled by the hypervisor/host. This does not dedicate eight physical cores or reserve two physical cores for macOS. The sealed AVD restoration file may still contain hw.cpu.ncore=6. |
| Guest RAM Allocation | AVD config.ini (hw.ramSize) |
6144 (6 GiB) |
Gives Android adequate heap without pressuring macOS unified RAM. |
| Crash Report Consent | Emulator argument -crash-report-mode |
disabled |
Bypasses modal Google crash reporting consent dialogs on boot. |
| Boot snapshot policy | effective current DEV QEMU command | cold / -no-snapshot |
Current verified DEV/LKG test launches are cold/no-snapshot for reproducibility. Historical quickboot experiments do not describe the current optimization baseline. |
The native Swift app presenting frames to the physical Mac mini display.
| Component / Setting | Implementation | Design Function |
|---|---|---|
| Single-Frame Mailbox | FrameMailbox.swift |
Single-slot overwrite buffer. Completely prevents buffer queue backlog; if host drops a tick, it presents the freshest frame rather than lagging behind. |
| Metal Render Pipeline | EmbeddedEmulatorView.swift |
Fullscreen oversized single triangle vertex shader with bilinear clamp-to-edge sampling into bgra8Unorm_srgb CAMetalLayer. |
| Multi-Activity Aware Sampler | GameFrameTelemetry.swift |
Automatically differentiates between MobileFREWebViewActivity (Riot web login) and GameActivity (3D game engine). Prevents false 0 FPS alarms. |
| Damage-Driven Stream Awareness | updatePerformanceOverlay() |
Displays TFT LOGIN (IDLE) and PIPE IDLE when Android posts 0 dirty rectangles during static authentication forms. |
These settings cannot be modified. They represent strict safety, stability, hardware, and architectural boundaries where deviation results in immediate crash, memory corruption, or fatal desynchronization.
| Invariant / Setting | Hard-Locked Value | Failure Signature if Violated | Architectural Root Cause |
|---|---|---|---|
| ASG Write Buffer Maximum |
1_048_576 bytes (1 MiB) |
Fatal QEMU abort: External address size too small
|
QEMU's PCI BAR address window allocation for Apple Silicon hypervisor is strictly bounded to 1 MiB. |
| ASG Data Ring Size |
32_768 bytes (32 KiB) |
Ring synchronization stall / deadlocks | Larger rings exceed the virtio-gpu-asg kernel driver's lockless single-producer ring assumptions. |
| ASG Write Step Size |
16_384 bytes (16 KiB) |
Command stream underruns or pipeline hitching | Empirically proven optimal balance between packet dispatch overhead and buffer exhaustion. |
| gRPC Message Ingress Cap |
16_777_216 bytes (16 MiB) |
gRPC error: RESOURCE_EXHAUSTED (Socket teardown) |
Standard gRPC limit is 4 MiB. A 1080p uncompressed frame is 8,294,400 bytes ( |
| Display Interrupt Clock | -vsync-rate 60 |
Phase beat jitter ( |
Physical QEMU virtual display timer. Setting Android to 120 Hz with a 60 Hz hardware clock creates permanent micro-stutter. |
| Riot Performance Mode Beta | OFF (Hard Locked) | Tail latency collapse ( |
Historical testing proved Riot's mobile beta throttles Unreal simulation threads unpredictably. |
| MoltenVK Synchronous Queue |
0 (Disabled / Async) |
40% overall frame rate collapse | Synchronous submits block the host render thread waiting for GPU completion, destroying pipeline overlap. |
| MoltenVK Max Command Buffers | 64 |
Metal command buffer pool contention | Values of 128/256 increased memory pool churn without improving throughput. |
| Dual Runtime Port Isolation | Control: 5038/5582/8554DEV: 5041/5586/8556
|
Address already in use or ADB session theft |
Guarded by atomic runtime lease. Both runtimes can never share ports or run simultaneously. |
| ADB Session Launch Chain |
/usr/bin/open via RuntimeHost
|
ADB status unauthorized
|
Launching emulator from background daemons or injecting ADB_VENDOR_KEYS breaks the macOS user security session. |
| Riot Signed Binary Integrity | Stock Google Play APK | Anti-cheat kick / App launch crash | Riot verifies package signatures (signatures=PackageSignatures). APK code or shaders must never be tampered with. |
| Metal Texture Pixel Format | .bgra8Unorm_srgb |
High CPU overhead from software pixel conversion | Must match CAMetalLayer native scanout format on Apple Silicon. |
Standard diagnostic tools repeatedly failed to provide truthful data because of the hybrid virtualization stack. Here is the forensic record of how each logging mechanism was discovered and built:
- Why Standard Tools Failed: Android developers normally use
dumpsys gfxinfo. However,gfxinfoonly tracks View-based UI (ViewRootImpl). Because Unreal Engine bypasses the Android View hierarchy and renders directly to a native VulkanSurfaceView(SurfaceView[com.riotgames...GameActivity](BLAST)),gfxinforeturned completely empty data. - How We Discovered the Solution: We analyzed the Android compositor source code and found that SurfaceFlinger maintains a circular 128-entry hardware presentation ring buffer for every active BLAST layer:
- Line 0: Nanosecond VSYNC refresh period (e.g.
16666667for 60 Hz). - Lines 1–128: Triples of
[desiredPresentTime, actualPresentTime, frameReadyTime].
- Line 0: Nanosecond VSYNC refresh period (e.g.
- The Math: By parsing the actual presentation timestamps, filtering out system sentinels (
0,0x7FFFFFFFFFFFFFFF), and tracking intervals between consecutive actual presentation timestamps, we constructed an exact 1-second rolling telemetry window measuringeffectiveFPS,1% low,p95,p99,jankCount, andmissedVsyncEquivalents.
- The Mystery: During initial boot and login, our telemetry reported 0 FPS with status
AVAILABLE, logging 990 seconds of total failure and pulling the average down to 0.21 FPS. - How We Discovered the Cause: We investigated
dumpsys SurfaceFlinger --listand discovered that when Riot presents account login, it launchesMobileFREWebViewActivity(a Chromium HTML webview) on top of Unreal Engine. Android automatically putsGameActivityintoPlayerBase::pause(). Unreal literally stopped rendering frames. Because our sampler was hardcoded to only look forGameActivity, it reported the paused surface as a 0 FPS crash. - The Fix: We updated
GameFrameTelemetrySamplerto detectMobileFREWebViewActivity. When active, it returns.unavailable(.loginPromptActive)and the HUD displaysTFT LOGIN (IDLE), keeping the database free of false degradation records.
- The Mystery: When frame drops occurred during 8-player combat, was the bottleneck in QEMU's ASG decoder, the Vulkan driver, MoltenVK translation, or the Apple M4 Metal GPU?
-
How We Discovered the Solution:
- We designed
PipelineEventV1: an exact 96-byte C++ binary event structure with SHA-256 segment chaining. - We instrumented
gfxstream-backendandMoltenVKusing Vulkan 64-bit timeline semaphores (VK_KHR_timeline_semaphore). - In live run
causal-hook-timeline-20260903-r6, across 99,480 events and 10,796 frames, total host graphics latency averaged 0.792 ms mean (<5% of a 16.67 ms frame):- ASG Decode
$\to$ MoltenVK Entry:0.004 ms - Host Submit (Site 1002):
0.019 ms - MoltenVK Translation (Site 2003):
0.106 ms - Metal GPU Execution (Site 2005):
0.683 ms
- ASG Decode
- Bounded finding: that instrumented span measured low host graphics latency for the sampled work and shifted suspicion upstream. It did not prove that 100% of all combat latency is guest shader compilation or disk I/O across every current run.
- We designed
- The Mystery: Early automated telemetry runs suffered worse combat stuttering than unmonitored runs.
- Historical profiling finding: one earlier 6-vCPU match generated 4,253 child processes from aggressive polling and also used a bounded Perfetto/ftrace capture. That combination was considered intrusive and was removed from the preferred measurement path. Do not generalize it as proof that tracing always completely starves RHIThread/AudioFlinger; current profiling must remain bounded.
- The Fix: We restricted
dumpsys SurfaceFlinger --listto run only when the active layer is lost or reports 0 frames, cachedcurrentGamePID, and disabled automatic Perfetto dumps during normal interactive play (TFTMAC_ENABLE_AUTO_PERFETTO=1required).
- Hardware boundary: the Apple M4 Mac mini has 10 CPU cores (4 performance + 6 efficiency) and 10 GPU cores sharing unified memory. This project does not treat fixed P/E-core MHz values as an authoritative runtime fact because macOS dynamically manages frequency.
- GPU scheduling boundary: the Android guest does not directly allocate physical M4 GPU cores. The current path ultimately submits work to Metal, and macOS/Metal own GPU scheduling. A prior bounded instrumented sample measured about 0.68 ms of Metal GPU work for its sampled frames; that does not prove every 1080p frame finishes in 0.68 ms or that every workload continuously occupies all 10 GPU cores.
-
CPU/quality tuning history:
- Current DEV uses 8 vCPUs, not 6. vCPU count does not dedicate or reserve physical host cores; it changes the guest scheduler's available virtual CPUs.
- PSO worker-count experiments can reduce or increase contention, but a worker count does not permanently reserve vCPUs for GameThread/RHIThread and cannot be claimed to eliminate context-switch starvation without a matched result.
- Dynamic-resolution floors can trade image quality for load shedding when enabled, but no setting guarantees uninterrupted 60 FPS. Any such change requires its own verified DEV comparison.
- Cloth physics (
p.ClothPhysics=1) is confirmed safe: with a realistic match ceiling of$\le 30$ active units on screen, cloth simulation consumes only$\sim 0.5\text{ ms}$ ofGameThreadtime (<3% of frame budget).
- The Dormant Leash Trap: When an Android sub-activity (e.g., Riot's
MobileFREWebViewActivitywebview login) finishes and is dismissed, WindowManager destroys the window (WIN DEATH), but SurfaceFlinger retains a dormant parent leash layer in the layer hierarchy:Surface(name=ActivityRecord{...MobileFREWebViewActivity}). A naive substring match (output.contains(tftMobileFREActivity)) permanently flags the runtime as.loginPromptActive, blinding the telemetry sampler from ever hooking back onto the activeSurfaceView[...GameActivity](BLAST)layer.- The Solution:
GameFrameTelemetry.hasActiveLoginPrompt(in:)explicitly ignoresActivityRecordleashes andSnapshotStartingWindowlayers. Only genuine, non-leash window layers trigger the login prompt state.
- The Solution:
- Damage-Driven gRPC Frame Streaming: QEMU's gRPC
streamScreenshotis damage-driven by design. When an on-screen interface is static (e.g., waiting on user credentials or during a process stall), Android marks 0 dirty rectangles and gRPC delivers 0 frames (source_fps = 0.0). This is normal power-saving guest compositor behavior, not a graphics rendering bottleneck. - Process Priority Maintenance Across PID Restarts: when the game PID changes, the runtime can reapply its configured nice-priority request. This preserves the requested scheduling preference; it is not a guarantee of Linux real-time scheduling.
- Match Forensic Analysis (
2026-09-04T17-50-10.043Z, 892 Windows / ~32 Minutes):- Overall Average FPS: 55.80 FPS (58.6% of all sample windows ran at flat 58–61 FPS).
- Planning / Shopping Phases: Consistently locked at 58.6–59.8 FPS, frame times 17.3–18.4 ms, guest CPU load ~320%.
- Combat Drops: During large late-game combat rounds (16–22+ moving champions casting spells), CPU load surged to 380%–510%, stretching frame times to 25–31 ms and pulling frame rates down into the 45–53 FPS range (1% low: 33.27 FPS).
- Definitive Memory Audit (Host vs. Guest):
- Guest Android RAM (5,120 MB): Android consumed only ~3.2 GB out of 5.1 GB. Available memory averaged 1,705 MB (minimum 1,533 MB). Zero LowMemoryKiller events occurred; Android had >1.5 GB of free headroom at all times.
- macOS Host RAM (16 GB Unified): Available host RAM was 2,938 MB average (min 2,620 MB) with 7.2 GB compressed and 1.7 GB swap.
- Historical memory finding: this 5,120-MiB match had adequate guest headroom and showed host compression/swap. It supports avoiding an unproven jump to 8 GiB, but it does not make 5,120 MiB an eternal 'exact sweet spot.' Current verified DEV uses 6144 MiB, which is the authoritative working value unless a later measured candidate changes it.
- The current 8-vCPU DEV allocation:
- Current DEV launches are verified at
-cores 8. This provides eight guest vCPUs while host scheduling remains macOS/Hypervisor-owned; it does not create two dedicated physical host cores or prove GameThread/RHIThread are unconstrained.
- Current DEV launches are verified at
- Historical Unreal combat optimizations:
p.ClothPhysics=0: Disables CPU vertex cloth simulation on 20–30 combat units, reclaiming 5–8 ms of GameThread frame budget.r.DynamicRes.OperationMode=1: Activates the Dynamic Resolution master switch with an 85% safety floor (r.DynamicRes.MinScreenPercentage=85) and 16.67 ms budget (r.DynamicRes.FrameTimeBudget=16.666666).r.pso.PrecompileThreadPoolSize=2: Restricts PSO precompile threads to 2, preventing worker threads from swamping vCPUs during combat.
- Clean Snapshot Teardown:
- In
TFTMACRuntime.swift -> stop(), issuingam force-stop com.riotgames.league.teamfighttactics300 ms beforeadb emu killcloses active Vulkan swapchains and device instances, eliminating QEMU'sUNSUPPORTED_VK_APPsnapshot save failure and enabling 2–3 second fast snapshot resumes on subsequent boots.
- In