diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f39720f..388c23c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,68 +1,46 @@ name: Bug report -description: Report a reproducible launcher, installer, or runtime problem. +description: Report a reproducible TFTMAC native-app or runtime problem. title: "[Bug]: " labels: [bug] body: - type: markdown attributes: - value: Thanks for helping improve Mactician. Remove account names and other personal data from logs before attaching them. + value: Thanks for helping improve TFTMAC. Remove account names, credentials, tokens, and other private data before attaching logs. - type: input - id: launcher-version + id: build attributes: - label: Mactician version and build - placeholder: 1.0.0 (33) + label: TFTMAC commit or build + placeholder: commit SHA or build identifier validations: required: true - type: input id: macos-version attributes: label: macOS version - placeholder: 15.6.1 validations: required: true - type: input id: mac-model attributes: label: Mac model and Apple chip - placeholder: MacBook Pro, Apple M3 Pro - validations: - required: true - - type: dropdown - id: installation-state - attributes: - label: Installation state - options: - - Fresh installation - - Updated installation - - Existing installation - validations: - required: true - - type: input - id: resolution - attributes: - label: Selected resolution - placeholder: 1920×1080 + placeholder: Mac mini, Apple M4 validations: required: true - type: input - id: resources + id: runtime attributes: - label: Selected RAM and vCPU - placeholder: 6144 MB RAM, 6 vCPU - validations: - required: true + label: Android Emulator version + placeholder: 37.1.11 - type: textarea id: expected attributes: label: Expected behavior - description: What did you expect to happen? validations: required: true - type: textarea id: actual attributes: label: Actual behavior - description: What happened instead? validations: required: true - type: textarea @@ -70,21 +48,21 @@ body: attributes: label: Reproduction steps placeholder: | - 1. Open ... - 2. Select ... - 3. Observe ... + 1. Open TFTMAC + 2. Perform the action + 3. Observe the result validations: required: true - type: textarea id: logs attributes: - label: Sanitized logs - description: Paste relevant log excerpts. Remove Riot account names, filesystem usernames, tokens, and other private data. + label: Sanitized diagnostics + description: Include only the bounded excerpt needed to show the failure. render: shell - type: checkboxes id: privacy attributes: label: Privacy confirmation options: - - label: I removed credentials, account identifiers, and raw sensitive logs from this report. + - label: I removed credentials, account identifiers, tokens, and raw sensitive data. required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6d57565..d11b757 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,27 +1,18 @@ ## Summary -Describe the problem, the change, and its scope. - -## Motivation - -Explain why this change is needed and why this scope is appropriate. +Describe the problem, the change, and its exact scope. ## Validation -- [ ] `./scripts/verify-repository.command` -- [ ] `./scripts/test-mactician.command` -- [ ] Relevant manual or integration checks are described below, or are not applicable. - -## Screenshots - -Attach before/after screenshots for UI changes, or write “Not applicable.” +- [ ] `/bin/zsh scripts/verify-tftmac.command` +- [ ] Relevant runtime/manual acceptance is described below or is not applicable. ## Contributor checklist -- [ ] I did not add game APKs, Android images, private runtime state, credentials, logs, or generated build products. -- [ ] Developer-facing output and documentation are in English. -- [ ] New machine paths are resolved from environment variables, standard locations, or `PATH`. -- [ ] Behavior changes include tests or a concrete reason why a test is impractical. -- [ ] Performance claims include reproducible evidence and avoid overstating noisy results. -- [ ] Documentation, changelog, and release notes are updated when applicable. -- [ ] Existing rollback, Repair, Reset, and fail-closed behavior is preserved or the deliberate change is explained. +- [ ] I did not add Riot APKs, Android images, credentials, private runtime state, tokens, logs, or generated build products. +- [ ] The stock Google Android Emulator remains the normal runtime authority. +- [ ] The change does not add a private game feed, APK repackaging, re-signing, or Riot binary patching. +- [ ] Behavior changes include tests or a concrete reason a test is impractical. +- [ ] Performance claims include reproducible evidence. +- [ ] Documentation is updated when behavior or commands change. +- [ ] Rollback and fail-closed behavior is preserved or the deliberate change is explained. diff --git a/.github/repository-metadata.yml b/.github/repository-metadata.yml index e086a57..636a23d 100644 --- a/.github/repository-metadata.yml +++ b/.github/repository-metadata.yml @@ -1,15 +1,12 @@ -description: A native, open-source macOS launcher for playing TFT PBE on Apple Silicon. -homepage: https://sergeinaumov.dev/mactician -social_preview: branding/generated/mactician-social-preview.png +description: Native macOS control and presentation app for the official Android Teamfight Tactics client on Apple Silicon. topics: - - mactician + - tftmac - macos - swift - - swiftui + - appkit + - metal - apple-silicon - - tft - - tft-pbe + - teamfight-tactics - android-emulator - - vulkan - - moltenvk - - open-source + - google-play + - emulator-controller diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e710ae6..d741607 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [master] pull_request: permissions: @@ -10,25 +10,27 @@ permissions: jobs: validate: - name: Validate and test + name: Validate TFTMAC runs-on: macos-latest - timeout-minutes: 30 + timeout-minutes: 45 steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '24' - name: Install validation tools run: | command -v jq >/dev/null || brew install jq command -v rg >/dev/null || brew install ripgrep - - name: Validate repository policy and metadata - run: ./scripts/verify-repository.command - - - name: Run launcher unit tests and production typecheck - run: ./scripts/test-mactician.command + - name: Validate TFTMAC source, unsigned Release build, and native tests + run: /bin/zsh scripts/verify-tftmac.command - - name: Reject whitespace and generated changes + - name: Reject whitespace and generated drift run: | git diff --check test -z "$(git status --short)" diff --git a/CHANGELOG.md b/CHANGELOG.md index edeecd1..3382078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,62 +1,38 @@ # Changelog -The current application metadata is version 1.0.4, build 40. - ## Unreleased -No changes yet. - -## 1.0.4 — 2026-08-14 - -### Fixed - -- Restore four asynchronous OpenGL PSO compiler services when a TFT update - disables them in its inherited Android device profile, avoiding first-use - shader compilation stalls on the gameplay render path. - -## 1.0.3 — 2026-08-14 - -### Changed - -- Check the signed TFT PBE feed when the launcher becomes ready. -- Show **Update game** in place of **Play** only when a newer verified game - version is available. -- Prevent starting a known-outdated game build until its update completes. - -## 1.0.2 — 2026-08-13 - -### Fixed - -- Check for Mactician updates on every launch instead of waiting only for the - daily Sparkle schedule. -- Show an explicit localized result after a game update check, including the - installed TFT PBE version when no newer hosted build is available. -- Record completed game update checks in the launcher log. - -## 1.0.1 — 2026-08-13 - -### Added - -- Added a separately signed TFT PBE update channel hosted on - `sergeinaumov.dev`. -- Added in-place split APK updates that preserve Riot sign-in and local game - data. - -## 1.0.0 — 2026-08-10 - -### Added +### Documentation -- Initial public version of Mactician. +- Reconciled current Build 8 runtime, automatic-logging, and graphics-causality + status across the human-readable project record. +- Recorded the latest 42m27s automatic graphics capture as performance evidence + while retaining internal attribution as unknown. +- Archived obsolete launch/profile/source-build entrypoints under + `docs/history/2026-08-31-pre-build8/` and replaced them with current pointers. ### Changed -- Restyled the active game Dock icon as a distinct Mactician play variant and - replaced the Android Emulator title with `Mactician: TFT PBE`. -- Reduced Trial benchmark preparation from roughly 20 seconds to 1–3 seconds - by overlapping one shop decision with combat and batching reward, XP, item, - and replay actions. -- Preserved valid measurements across same-emulator Trial retries, added a - bounded same-combat capture retry, and repaired early-exit cleanup after a - launcher crash. -- Updated Performance Max with the confirmed 67% effects/LOD profile and a - 16 KiB ASG write step; repeated Trial 1-8 proxies remained above 30 FPS. +- Split repository/CI verification from the local-only installed-runtime and + signing audit; CI no longer depends on `/Applications`, an external runtime, + a private signing identity, credentials, or captures. +- Updated GitHub checkout to `actions/checkout@v7.0.1` while retaining Node 24. +- Reconciled machine-readable runtime, retained-evidence, engineering-map, and + performance-lab authority around stock Build 8 and the planned isolated + causal logger. +- Separated historical Build 8 signing acceptance from the current-host + `CSSMERR_TP_NOT_TRUSTED` audit and its missing login-keychain identity. +- Established TFTMAC as the sole product and repository identity. +- Replaced legacy validation with the native TFTMAC build/test verifier. +- Preserved the proven native AppKit/Metal Gate 1 implementation and frozen installed EmulatorController protocol. +- Removed obsolete launcher, hosted update/feed, helper-host, and branding layers. +- Moved runtime authority to the stock Google Android Emulator and official Google Play TFT lifecycle. +- Began relational migration of retained performance evidence to TFTMAC-owned identifiers. +- Retired source-built emulator work from the normal product path. + +### Current target + +- Native macOS application bundle: `com.flashls1.tftmac`. +- Stock Android Emulator 37.1.11. +- Official Google Play package `com.riotgames.league.teamfighttactics`. +- 1920x1080 / 60 Hz target on Apple Silicon. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f465de..41c4732 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,48 +1,26 @@ # Contributing -Contributions are welcome when they are narrowly scoped, testable, and preserve -the launcher's recovery guarantees. +Keep TFTMAC changes narrow, testable, and aligned with the current stock-runtime architecture. ## Development setup -Use an Apple Silicon Mac with macOS 12 or later, Xcode Command Line Tools, zsh, -`jq`, Node.js for the optional login helper, and the local private APK build -inputs described in [docs/building.md](docs/building.md). Runtime and game data -must remain outside Git. +Use an Apple Silicon Mac with macOS 15 or later, Xcode 26.6, zsh, Node.js 24, `jq`, and `ripgrep`. Runtime and game data must remain outside Git. -Run the fast validation before opening a pull request: +Run before opening a pull request: ```sh -./scripts/verify-repository.command -./scripts/test-mactician.command +/bin/zsh scripts/verify-tftmac.command ``` -The provisioning integration test downloads large pinned Android archives and -is intentionally local/manual: +## Rules -```sh -./scripts/integration-test-mactician.command -``` +- Preserve `com.flashls1.tftmac` as the application identity. +- Preserve the stock Google Android Emulator as runtime authority unless a separately approved measured blocker requires a different architecture. +- Do not add Riot APKs, credentials, Android userdata, tokens, runtime disks, or private session data to the repository. +- Do not add hosted game feeds, APK repacking, re-signing, or binary patching. +- Keep generated build output out of Git. +- Update tests and documentation when behavior changes. +- Keep performance claims tied to reproducible captures and explicit KEEP/REJECT evidence. +- Preserve rollback and fail-closed behavior around runtime mutation. -## Pull requests - -- Keep changes small and explain their motivation and failure mode. -- Update documentation whenever commands, manifests, behavior, or recovery - steps change. -- Preserve transactional rollback, checksum verification, and fail-closed - behavior around unknown UI states and unsupported game versions. -- Do not weaken update signing, bundle identity, appcast trust, or manifest - validation. -- Add benchmark claims only with reproducible, same-scene evidence and recorded - hashes. Label single runs provisional. -- Keep shell scripts compatible with zsh, derive the project root from - `${0:A:h}`, quote paths, validate required tools, and emit English output. -- Keep the Mactician interface localized in English and Russian, update both - `.strings` files for user-visible changes, and never translate the brand name. - Game-language support is independent. -- Do not commit secrets, credentials, APKs, runtime state, AVD userdata, logs, - crash dumps, build output, or developer-specific absolute paths. - -Use the pull request template and include screenshots for UI changes. A change -that affects install, Repair, Reset, launch, stop, or update behavior must state -how rollback and recovery were verified. +UI work should include appropriate validation evidence. Runtime work should state the exact owning boundary and failure mode it changes. diff --git a/Generated/EmulatorController/emulator_controller.grpc.swift b/Generated/EmulatorController/emulator_controller.grpc.swift new file mode 100644 index 0000000..f1c4f80 --- /dev/null +++ b/Generated/EmulatorController/emulator_controller.grpc.swift @@ -0,0 +1,13758 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Note that if you add/remove methods in this file you must update +/// the metrics sql as well ./android/scripts/gen-grpc-sql.py +/// +/// Please group deleted methods in a block including the date (MM/DD/YY) +/// it was removed. This enables us to easily keep metrics around after removal +/// +/// List of deleted methods +/// rpc iWasDeleted (03/12/12) +/// ... + +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: emulator_controller.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf +import SwiftProtobuf + +// MARK: - android.emulation.control.EmulatorController + +/// Namespace containing generated types for the "android.emulation.control.EmulatorController" service. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal enum Android_Emulation_Control_EmulatorController: Sendable { + /// Service descriptor for the "android.emulation.control.EmulatorController" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController") + /// Namespace for method metadata. + internal enum Method: Sendable { + /// Namespace for "streamSensor" metadata. + internal enum streamSensor: Sendable { + /// Request type for "streamSensor". + internal typealias Input = Android_Emulation_Control_SensorValue + /// Response type for "streamSensor". + internal typealias Output = Android_Emulation_Control_SensorValue + /// Descriptor for "streamSensor". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamSensor", + type: .serverStreaming + ) + } + /// Namespace for "getSensor" metadata. + internal enum getSensor: Sendable { + /// Request type for "getSensor". + internal typealias Input = Android_Emulation_Control_SensorValue + /// Response type for "getSensor". + internal typealias Output = Android_Emulation_Control_SensorValue + /// Descriptor for "getSensor". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getSensor", + type: .unary + ) + } + /// Namespace for "setSensor" metadata. + internal enum setSensor: Sendable { + /// Request type for "setSensor". + internal typealias Input = Android_Emulation_Control_SensorValue + /// Response type for "setSensor". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setSensor". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setSensor", + type: .unary + ) + } + /// Namespace for "setPhysicalModel" metadata. + internal enum setPhysicalModel: Sendable { + /// Request type for "setPhysicalModel". + internal typealias Input = Android_Emulation_Control_PhysicalModelValue + /// Response type for "setPhysicalModel". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setPhysicalModel". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setPhysicalModel", + type: .unary + ) + } + /// Namespace for "getPhysicalModel" metadata. + internal enum getPhysicalModel: Sendable { + /// Request type for "getPhysicalModel". + internal typealias Input = Android_Emulation_Control_PhysicalModelValue + /// Response type for "getPhysicalModel". + internal typealias Output = Android_Emulation_Control_PhysicalModelValue + /// Descriptor for "getPhysicalModel". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getPhysicalModel", + type: .unary + ) + } + /// Namespace for "streamPhysicalModel" metadata. + internal enum streamPhysicalModel: Sendable { + /// Request type for "streamPhysicalModel". + internal typealias Input = Android_Emulation_Control_PhysicalModelValue + /// Response type for "streamPhysicalModel". + internal typealias Output = Android_Emulation_Control_PhysicalModelValue + /// Descriptor for "streamPhysicalModel". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamPhysicalModel", + type: .serverStreaming + ) + } + /// Namespace for "setClipboard" metadata. + internal enum setClipboard: Sendable { + /// Request type for "setClipboard". + internal typealias Input = Android_Emulation_Control_ClipData + /// Response type for "setClipboard". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setClipboard". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setClipboard", + type: .unary + ) + } + /// Namespace for "getClipboard" metadata. + internal enum getClipboard: Sendable { + /// Request type for "getClipboard". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getClipboard". + internal typealias Output = Android_Emulation_Control_ClipData + /// Descriptor for "getClipboard". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getClipboard", + type: .unary + ) + } + /// Namespace for "streamClipboard" metadata. + internal enum streamClipboard: Sendable { + /// Request type for "streamClipboard". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "streamClipboard". + internal typealias Output = Android_Emulation_Control_ClipData + /// Descriptor for "streamClipboard". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamClipboard", + type: .serverStreaming + ) + } + /// Namespace for "setBattery" metadata. + internal enum setBattery: Sendable { + /// Request type for "setBattery". + internal typealias Input = Android_Emulation_Control_BatteryState + /// Response type for "setBattery". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setBattery". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setBattery", + type: .unary + ) + } + /// Namespace for "getBattery" metadata. + internal enum getBattery: Sendable { + /// Request type for "getBattery". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getBattery". + internal typealias Output = Android_Emulation_Control_BatteryState + /// Descriptor for "getBattery". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getBattery", + type: .unary + ) + } + /// Namespace for "setGps" metadata. + internal enum setGps: Sendable { + /// Request type for "setGps". + internal typealias Input = Android_Emulation_Control_GpsState + /// Response type for "setGps". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setGps". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setGps", + type: .unary + ) + } + /// Namespace for "getGps" metadata. + internal enum getGps: Sendable { + /// Request type for "getGps". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getGps". + internal typealias Output = Android_Emulation_Control_GpsState + /// Descriptor for "getGps". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getGps", + type: .unary + ) + } + /// Namespace for "sendFingerprint" metadata. + internal enum sendFingerprint: Sendable { + /// Request type for "sendFingerprint". + internal typealias Input = Android_Emulation_Control_Fingerprint + /// Response type for "sendFingerprint". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "sendFingerprint". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "sendFingerprint", + type: .unary + ) + } + /// Namespace for "sendKey" metadata. + internal enum sendKey: Sendable { + /// Request type for "sendKey". + internal typealias Input = Android_Emulation_Control_KeyboardEvent + /// Response type for "sendKey". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "sendKey". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "sendKey", + type: .unary + ) + } + /// Namespace for "sendTouch" metadata. + internal enum sendTouch: Sendable { + /// Request type for "sendTouch". + internal typealias Input = Android_Emulation_Control_TouchEvent + /// Response type for "sendTouch". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "sendTouch". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "sendTouch", + type: .unary + ) + } + /// Namespace for "sendMouse" metadata. + internal enum sendMouse: Sendable { + /// Request type for "sendMouse". + internal typealias Input = Android_Emulation_Control_MouseEvent + /// Response type for "sendMouse". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "sendMouse". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "sendMouse", + type: .unary + ) + } + /// Namespace for "injectWheel" metadata. + internal enum injectWheel: Sendable { + /// Request type for "injectWheel". + internal typealias Input = Android_Emulation_Control_WheelEvent + /// Response type for "injectWheel". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "injectWheel". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "injectWheel", + type: .clientStreaming + ) + } + /// Namespace for "streamInputEvent" metadata. + internal enum streamInputEvent: Sendable { + /// Request type for "streamInputEvent". + internal typealias Input = Android_Emulation_Control_InputEvent + /// Response type for "streamInputEvent". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "streamInputEvent". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamInputEvent", + type: .clientStreaming + ) + } + /// Namespace for "sendPhone" metadata. + internal enum sendPhone: Sendable { + /// Request type for "sendPhone". + internal typealias Input = Android_Emulation_Control_PhoneCall + /// Response type for "sendPhone". + internal typealias Output = Android_Emulation_Control_PhoneResponse + /// Descriptor for "sendPhone". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "sendPhone", + type: .unary + ) + } + /// Namespace for "sendSms" metadata. + internal enum sendSms: Sendable { + /// Request type for "sendSms". + internal typealias Input = Android_Emulation_Control_SmsMessage + /// Response type for "sendSms". + internal typealias Output = Android_Emulation_Control_PhoneResponse + /// Descriptor for "sendSms". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "sendSms", + type: .unary + ) + } + /// Namespace for "setPhoneNumber" metadata. + internal enum setPhoneNumber: Sendable { + /// Request type for "setPhoneNumber". + internal typealias Input = Android_Emulation_Control_PhoneNumber + /// Response type for "setPhoneNumber". + internal typealias Output = Android_Emulation_Control_PhoneResponse + /// Descriptor for "setPhoneNumber". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setPhoneNumber", + type: .unary + ) + } + /// Namespace for "getStatus" metadata. + internal enum getStatus: Sendable { + /// Request type for "getStatus". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getStatus". + internal typealias Output = Android_Emulation_Control_EmulatorStatus + /// Descriptor for "getStatus". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getStatus", + type: .unary + ) + } + /// Namespace for "getScreenshot" metadata. + internal enum getScreenshot: Sendable { + /// Request type for "getScreenshot". + internal typealias Input = Android_Emulation_Control_ImageFormat + /// Response type for "getScreenshot". + internal typealias Output = Android_Emulation_Control_Image + /// Descriptor for "getScreenshot". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getScreenshot", + type: .unary + ) + } + /// Namespace for "streamScreenshot" metadata. + internal enum streamScreenshot: Sendable { + /// Request type for "streamScreenshot". + internal typealias Input = Android_Emulation_Control_ImageFormat + /// Response type for "streamScreenshot". + internal typealias Output = Android_Emulation_Control_Image + /// Descriptor for "streamScreenshot". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamScreenshot", + type: .serverStreaming + ) + } + /// Namespace for "streamAudio" metadata. + internal enum streamAudio: Sendable { + /// Request type for "streamAudio". + internal typealias Input = Android_Emulation_Control_AudioFormat + /// Response type for "streamAudio". + internal typealias Output = Android_Emulation_Control_AudioPacket + /// Descriptor for "streamAudio". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamAudio", + type: .serverStreaming + ) + } + /// Namespace for "injectAudio" metadata. + internal enum injectAudio: Sendable { + /// Request type for "injectAudio". + internal typealias Input = Android_Emulation_Control_AudioPacket + /// Response type for "injectAudio". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "injectAudio". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "injectAudio", + type: .clientStreaming + ) + } + /// Namespace for "getMicrophoneState" metadata. + internal enum getMicrophoneState: Sendable { + /// Request type for "getMicrophoneState". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getMicrophoneState". + internal typealias Output = Android_Emulation_Control_MicrophoneState + /// Descriptor for "getMicrophoneState". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getMicrophoneState", + type: .unary + ) + } + /// Namespace for "setMicrophoneState" metadata. + internal enum setMicrophoneState: Sendable { + /// Request type for "setMicrophoneState". + internal typealias Input = Android_Emulation_Control_MicrophoneState + /// Response type for "setMicrophoneState". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setMicrophoneState". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setMicrophoneState", + type: .unary + ) + } + /// Namespace for "getLogcat" metadata. + internal enum getLogcat: Sendable { + /// Request type for "getLogcat". + internal typealias Input = Android_Emulation_Control_LogMessage + /// Response type for "getLogcat". + internal typealias Output = Android_Emulation_Control_LogMessage + /// Descriptor for "getLogcat". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getLogcat", + type: .unary + ) + } + /// Namespace for "streamLogcat" metadata. + internal enum streamLogcat: Sendable { + /// Request type for "streamLogcat". + internal typealias Input = Android_Emulation_Control_LogMessage + /// Response type for "streamLogcat". + internal typealias Output = Android_Emulation_Control_LogMessage + /// Descriptor for "streamLogcat". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamLogcat", + type: .serverStreaming + ) + } + /// Namespace for "setVmState" metadata. + internal enum setVmState: Sendable { + /// Request type for "setVmState". + internal typealias Input = Android_Emulation_Control_VmRunState + /// Response type for "setVmState". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setVmState". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setVmState", + type: .unary + ) + } + /// Namespace for "getVmState" metadata. + internal enum getVmState: Sendable { + /// Request type for "getVmState". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getVmState". + internal typealias Output = Android_Emulation_Control_VmRunState + /// Descriptor for "getVmState". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getVmState", + type: .unary + ) + } + /// Namespace for "setDisplayConfigurations" metadata. + internal enum setDisplayConfigurations: Sendable { + /// Request type for "setDisplayConfigurations". + internal typealias Input = Android_Emulation_Control_DisplayConfigurations + /// Response type for "setDisplayConfigurations". + internal typealias Output = Android_Emulation_Control_DisplayConfigurations + /// Descriptor for "setDisplayConfigurations". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setDisplayConfigurations", + type: .unary + ) + } + /// Namespace for "getDisplayConfigurations" metadata. + internal enum getDisplayConfigurations: Sendable { + /// Request type for "getDisplayConfigurations". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getDisplayConfigurations". + internal typealias Output = Android_Emulation_Control_DisplayConfigurations + /// Descriptor for "getDisplayConfigurations". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getDisplayConfigurations", + type: .unary + ) + } + /// Namespace for "streamNotification" metadata. + internal enum streamNotification: Sendable { + /// Request type for "streamNotification". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "streamNotification". + internal typealias Output = Android_Emulation_Control_Notification + /// Descriptor for "streamNotification". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "streamNotification", + type: .serverStreaming + ) + } + /// Namespace for "rotateVirtualSceneCamera" metadata. + internal enum rotateVirtualSceneCamera: Sendable { + /// Request type for "rotateVirtualSceneCamera". + internal typealias Input = Android_Emulation_Control_RotationRadian + /// Response type for "rotateVirtualSceneCamera". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "rotateVirtualSceneCamera". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "rotateVirtualSceneCamera", + type: .unary + ) + } + /// Namespace for "setVirtualSceneCameraVelocity" metadata. + internal enum setVirtualSceneCameraVelocity: Sendable { + /// Request type for "setVirtualSceneCameraVelocity". + internal typealias Input = Android_Emulation_Control_Velocity + /// Response type for "setVirtualSceneCameraVelocity". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setVirtualSceneCameraVelocity". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setVirtualSceneCameraVelocity", + type: .unary + ) + } + /// Namespace for "setPosture" metadata. + internal enum setPosture: Sendable { + /// Request type for "setPosture". + internal typealias Input = Android_Emulation_Control_Posture + /// Response type for "setPosture". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setPosture". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setPosture", + type: .unary + ) + } + /// Namespace for "getBrightness" metadata. + internal enum getBrightness: Sendable { + /// Request type for "getBrightness". + internal typealias Input = Android_Emulation_Control_BrightnessValue + /// Response type for "getBrightness". + internal typealias Output = Android_Emulation_Control_BrightnessValue + /// Descriptor for "getBrightness". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getBrightness", + type: .unary + ) + } + /// Namespace for "setBrightness" metadata. + internal enum setBrightness: Sendable { + /// Request type for "setBrightness". + internal typealias Input = Android_Emulation_Control_BrightnessValue + /// Response type for "setBrightness". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setBrightness". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setBrightness", + type: .unary + ) + } + /// Namespace for "getDisplayMode" metadata. + internal enum getDisplayMode: Sendable { + /// Request type for "getDisplayMode". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getDisplayMode". + internal typealias Output = Android_Emulation_Control_DisplayMode + /// Descriptor for "getDisplayMode". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getDisplayMode", + type: .unary + ) + } + /// Namespace for "setDisplayMode" metadata. + internal enum setDisplayMode: Sendable { + /// Request type for "setDisplayMode". + internal typealias Input = Android_Emulation_Control_DisplayMode + /// Response type for "setDisplayMode". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setDisplayMode". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setDisplayMode", + type: .unary + ) + } + /// Namespace for "setXrOptions" metadata. + internal enum setXrOptions: Sendable { + /// Request type for "setXrOptions". + internal typealias Input = Android_Emulation_Control_XrOptions + /// Response type for "setXrOptions". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setXrOptions". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setXrOptions", + type: .unary + ) + } + /// Namespace for "getXrOptions" metadata. + internal enum getXrOptions: Sendable { + /// Request type for "getXrOptions". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getXrOptions". + internal typealias Output = Android_Emulation_Control_XrOptions + /// Descriptor for "getXrOptions". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getXrOptions", + type: .unary + ) + } + /// Namespace for "setEnvironment" metadata. + internal enum setEnvironment: Sendable { + /// Request type for "setEnvironment". + internal typealias Input = Android_Emulation_Control_Environment + /// Response type for "setEnvironment". + internal typealias Output = SwiftProtobuf.Google_Protobuf_Empty + /// Descriptor for "setEnvironment". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "setEnvironment", + type: .unary + ) + } + /// Namespace for "getEnvironment" metadata. + internal enum getEnvironment: Sendable { + /// Request type for "getEnvironment". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getEnvironment". + internal typealias Output = Android_Emulation_Control_Environment + /// Descriptor for "getEnvironment". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getEnvironment", + type: .unary + ) + } + /// Namespace for "getHostCameras" metadata. + internal enum getHostCameras: Sendable { + /// Request type for "getHostCameras". + internal typealias Input = SwiftProtobuf.Google_Protobuf_Empty + /// Response type for "getHostCameras". + internal typealias Output = Android_Emulation_Control_CameraList + /// Descriptor for "getHostCameras". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController"), + method: "getHostCameras", + type: .unary + ) + } + /// Descriptors for all methods in the "android.emulation.control.EmulatorController" service. + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + streamSensor.descriptor, + getSensor.descriptor, + setSensor.descriptor, + setPhysicalModel.descriptor, + getPhysicalModel.descriptor, + streamPhysicalModel.descriptor, + setClipboard.descriptor, + getClipboard.descriptor, + streamClipboard.descriptor, + setBattery.descriptor, + getBattery.descriptor, + setGps.descriptor, + getGps.descriptor, + sendFingerprint.descriptor, + sendKey.descriptor, + sendTouch.descriptor, + sendMouse.descriptor, + injectWheel.descriptor, + streamInputEvent.descriptor, + sendPhone.descriptor, + sendSms.descriptor, + setPhoneNumber.descriptor, + getStatus.descriptor, + getScreenshot.descriptor, + streamScreenshot.descriptor, + streamAudio.descriptor, + injectAudio.descriptor, + getMicrophoneState.descriptor, + setMicrophoneState.descriptor, + getLogcat.descriptor, + streamLogcat.descriptor, + setVmState.descriptor, + getVmState.descriptor, + setDisplayConfigurations.descriptor, + getDisplayConfigurations.descriptor, + streamNotification.descriptor, + rotateVirtualSceneCamera.descriptor, + setVirtualSceneCameraVelocity.descriptor, + setPosture.descriptor, + getBrightness.descriptor, + setBrightness.descriptor, + getDisplayMode.descriptor, + setDisplayMode.descriptor, + setXrOptions.descriptor, + getXrOptions.descriptor, + setEnvironment.descriptor, + getEnvironment.descriptor, + getHostCameras.descriptor + ] + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "android.emulation.control.EmulatorController" service. + internal static let android_emulation_control_EmulatorController = GRPCCore.ServiceDescriptor(fullyQualifiedService: "android.emulation.control.EmulatorController") +} + +// MARK: android.emulation.control.EmulatorController (server) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController { + /// Streaming variant of the service protocol for the "android.emulation.control.EmulatorController" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > An EmulatorController service lets you control the emulator. + /// > Note that this is currently an experimental feature, and that the + /// > service definition might change without notice. Use at your own risk! + /// > + /// > We use the following rough conventions: + /// > + /// > streamXXX --> streams values XXX (usually for emulator lifetime). Values + /// > are updated as soon as they become available. + /// > getXXX --> gets a single value XXX + /// > setXXX --> sets a single value XXX, does not returning state, these + /// > usually have an observable lasting side effect. + /// > sendXXX --> send a single event XXX, possibly returning state information. + /// > android usually responds to these events. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_SensorValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_SensorValue` messages. + func streamSensor( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_SensorValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_SensorValue` messages. + func getSensor( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_SensorValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setSensor( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_PhysicalModelValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setPhysicalModel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_PhysicalModelValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_PhysicalModelValue` messages. + func getPhysicalModel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_PhysicalModelValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_PhysicalModelValue` messages. + func streamPhysicalModel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_ClipData` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setClipboard( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_ClipData` messages. + func getClipboard( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_ClipData` messages. + func streamClipboard( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_BatteryState` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setBattery( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_BatteryState` messages. + func getBattery( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_GpsState` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setGps( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_GpsState` messages. + func getGps( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_Fingerprint` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func sendFingerprint( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_KeyboardEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func sendKey( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_TouchEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func sendTouch( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_MouseEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func sendMouse( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_WheelEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func injectWheel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_InputEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func streamInputEvent( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_PhoneCall` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_PhoneResponse` messages. + func sendPhone( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_SmsMessage` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_PhoneResponse` messages. + func sendSms( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_PhoneNumber` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_PhoneResponse` messages. + func setPhoneNumber( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_EmulatorStatus` messages. + func getStatus( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_ImageFormat` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_Image` messages. + func getScreenshot( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_ImageFormat` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_Image` messages. + func streamScreenshot( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_AudioFormat` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_AudioPacket` messages. + func streamAudio( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_AudioPacket` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func injectAudio( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_MicrophoneState` messages. + func getMicrophoneState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_MicrophoneState` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setMicrophoneState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_LogMessage` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_LogMessage` messages. + func getLogcat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_LogMessage` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_LogMessage` messages. + func streamLogcat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_VmRunState` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setVmState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_VmRunState` messages. + func getVmState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_DisplayConfigurations` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_DisplayConfigurations` messages. + func setDisplayConfigurations( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_DisplayConfigurations` messages. + func getDisplayConfigurations( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_Notification` messages. + func streamNotification( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_RotationRadian` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func rotateVirtualSceneCamera( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_Velocity` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setVirtualSceneCameraVelocity( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_Posture` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setPosture( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_BrightnessValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_BrightnessValue` messages. + func getBrightness( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_BrightnessValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setBrightness( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_DisplayMode` messages. + func getDisplayMode( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_DisplayMode` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setDisplayMode( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_XrOptions` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setXrOptions( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_XrOptions` messages. + func getXrOptions( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_Environment` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `SwiftProtobuf.Google_Protobuf_Empty` messages. + func setEnvironment( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_Environment` messages. + func getEnvironment( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - request: A streaming request of `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_CameraList` messages. + func getHostCameras( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "android.emulation.control.EmulatorController" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > An EmulatorController service lets you control the emulator. + /// > Note that this is currently an experimental feature, and that the + /// > service definition might change without notice. Use at your own risk! + /// > + /// > We use the following rough conventions: + /// > + /// > streamXXX --> streams values XXX (usually for emulator lifetime). Values + /// > are updated as soon as they become available. + /// > getXXX --> gets a single value XXX + /// > setXXX --> sets a single value XXX, does not returning state, these + /// > usually have an observable lasting side effect. + /// > sendXXX --> send a single event XXX, possibly returning state information. + /// > android usually responds to these events. + internal protocol ServiceProtocol: Android_Emulation_Control_EmulatorController.StreamingServiceProtocol { + /// Handle the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_SensorValue` messages. + func streamSensor( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_SensorValue` message. + func getSensor( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setSensor( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setPhysicalModel( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_PhysicalModelValue` message. + func getPhysicalModel( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_PhysicalModelValue` messages. + func streamPhysicalModel( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ClipData` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setClipboard( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_ClipData` message. + func getClipboard( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_ClipData` messages. + func streamClipboard( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BatteryState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setBattery( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_BatteryState` message. + func getBattery( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_GpsState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setGps( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_GpsState` message. + func getGps( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Fingerprint` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func sendFingerprint( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_KeyboardEvent` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func sendKey( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_TouchEvent` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func sendTouch( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MouseEvent` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func sendMouse( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_WheelEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func injectWheel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_InputEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func streamInputEvent( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneCall` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_PhoneResponse` message. + func sendPhone( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SmsMessage` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_PhoneResponse` message. + func sendSms( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneNumber` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_PhoneResponse` message. + func setPhoneNumber( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_EmulatorStatus` message. + func getStatus( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_Image` message. + func getScreenshot( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_Image` messages. + func streamScreenshot( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_AudioFormat` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_AudioPacket` messages. + func streamAudio( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - request: A streaming request of `Android_Emulation_Control_AudioPacket` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func injectAudio( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_MicrophoneState` message. + func getMicrophoneState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MicrophoneState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setMicrophoneState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_LogMessage` message. + func getLogcat( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_LogMessage` messages. + func streamLogcat( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_VmRunState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setVmState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_VmRunState` message. + func getVmState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayConfigurations` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_DisplayConfigurations` message. + func setDisplayConfigurations( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_DisplayConfigurations` message. + func getDisplayConfigurations( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Android_Emulation_Control_Notification` messages. + func streamNotification( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_RotationRadian` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func rotateVirtualSceneCamera( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Velocity` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setVirtualSceneCameraVelocity( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Posture` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setPosture( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_BrightnessValue` message. + func getBrightness( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setBrightness( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_DisplayMode` message. + func getDisplayMode( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayMode` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setDisplayMode( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_XrOptions` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setXrOptions( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_XrOptions` message. + func getXrOptions( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Environment` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + func setEnvironment( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_Environment` message. + func getEnvironment( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Android_Emulation_Control_CameraList` message. + func getHostCameras( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } + + /// Simple service protocol for the "android.emulation.control.EmulatorController" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > An EmulatorController service lets you control the emulator. + /// > Note that this is currently an experimental feature, and that the + /// > service definition might change without notice. Use at your own risk! + /// > + /// > We use the following rough conventions: + /// > + /// > streamXXX --> streams values XXX (usually for emulator lifetime). Values + /// > are updated as soon as they become available. + /// > getXXX --> gets a single value XXX + /// > setXXX --> sets a single value XXX, does not returning state, these + /// > usually have an observable lasting side effect. + /// > sendXXX --> send a single event XXX, possibly returning state information. + /// > android usually responds to these events. + internal protocol SimpleServiceProtocol: Android_Emulation_Control_EmulatorController.ServiceProtocol { + /// Handle the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_SensorValue` message. + /// - response: A response stream of `Android_Emulation_Control_SensorValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamSensor( + request: Android_Emulation_Control_SensorValue, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_SensorValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_SensorValue` to respond with. + func getSensor( + request: Android_Emulation_Control_SensorValue, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_SensorValue + + /// Handle the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_SensorValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setSensor( + request: Android_Emulation_Control_SensorValue, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_PhysicalModelValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setPhysicalModel( + request: Android_Emulation_Control_PhysicalModelValue, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_PhysicalModelValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_PhysicalModelValue` to respond with. + func getPhysicalModel( + request: Android_Emulation_Control_PhysicalModelValue, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_PhysicalModelValue + + /// Handle the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_PhysicalModelValue` message. + /// - response: A response stream of `Android_Emulation_Control_PhysicalModelValue` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamPhysicalModel( + request: Android_Emulation_Control_PhysicalModelValue, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_ClipData` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setClipboard( + request: Android_Emulation_Control_ClipData, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_ClipData` to respond with. + func getClipboard( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_ClipData + + /// Handle the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - response: A response stream of `Android_Emulation_Control_ClipData` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamClipboard( + request: SwiftProtobuf.Google_Protobuf_Empty, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_BatteryState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setBattery( + request: Android_Emulation_Control_BatteryState, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_BatteryState` to respond with. + func getBattery( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_BatteryState + + /// Handle the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_GpsState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setGps( + request: Android_Emulation_Control_GpsState, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_GpsState` to respond with. + func getGps( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_GpsState + + /// Handle the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_Fingerprint` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func sendFingerprint( + request: Android_Emulation_Control_Fingerprint, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_KeyboardEvent` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func sendKey( + request: Android_Emulation_Control_KeyboardEvent, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_TouchEvent` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func sendTouch( + request: Android_Emulation_Control_TouchEvent, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_MouseEvent` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func sendMouse( + request: Android_Emulation_Control_MouseEvent, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A stream of `Android_Emulation_Control_WheelEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func injectWheel( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - request: A stream of `Android_Emulation_Control_InputEvent` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func streamInputEvent( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_PhoneCall` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_PhoneResponse` to respond with. + func sendPhone( + request: Android_Emulation_Control_PhoneCall, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_PhoneResponse + + /// Handle the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_SmsMessage` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_PhoneResponse` to respond with. + func sendSms( + request: Android_Emulation_Control_SmsMessage, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_PhoneResponse + + /// Handle the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_PhoneNumber` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_PhoneResponse` to respond with. + func setPhoneNumber( + request: Android_Emulation_Control_PhoneNumber, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_PhoneResponse + + /// Handle the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_EmulatorStatus` to respond with. + func getStatus( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_EmulatorStatus + + /// Handle the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_ImageFormat` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_Image` to respond with. + func getScreenshot( + request: Android_Emulation_Control_ImageFormat, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_Image + + /// Handle the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_ImageFormat` message. + /// - response: A response stream of `Android_Emulation_Control_Image` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamScreenshot( + request: Android_Emulation_Control_ImageFormat, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_AudioFormat` message. + /// - response: A response stream of `Android_Emulation_Control_AudioPacket` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamAudio( + request: Android_Emulation_Control_AudioFormat, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - request: A stream of `Android_Emulation_Control_AudioPacket` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func injectAudio( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_MicrophoneState` to respond with. + func getMicrophoneState( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_MicrophoneState + + /// Handle the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_MicrophoneState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setMicrophoneState( + request: Android_Emulation_Control_MicrophoneState, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_LogMessage` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_LogMessage` to respond with. + func getLogcat( + request: Android_Emulation_Control_LogMessage, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_LogMessage + + /// Handle the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_LogMessage` message. + /// - response: A response stream of `Android_Emulation_Control_LogMessage` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamLogcat( + request: Android_Emulation_Control_LogMessage, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_VmRunState` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setVmState( + request: Android_Emulation_Control_VmRunState, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_VmRunState` to respond with. + func getVmState( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_VmRunState + + /// Handle the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_DisplayConfigurations` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_DisplayConfigurations` to respond with. + func setDisplayConfigurations( + request: Android_Emulation_Control_DisplayConfigurations, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_DisplayConfigurations + + /// Handle the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_DisplayConfigurations` to respond with. + func getDisplayConfigurations( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_DisplayConfigurations + + /// Handle the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - response: A response stream of `Android_Emulation_Control_Notification` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func streamNotification( + request: SwiftProtobuf.Google_Protobuf_Empty, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_RotationRadian` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func rotateVirtualSceneCamera( + request: Android_Emulation_Control_RotationRadian, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_Velocity` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setVirtualSceneCameraVelocity( + request: Android_Emulation_Control_Velocity, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_Posture` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setPosture( + request: Android_Emulation_Control_Posture, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_BrightnessValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_BrightnessValue` to respond with. + func getBrightness( + request: Android_Emulation_Control_BrightnessValue, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_BrightnessValue + + /// Handle the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_BrightnessValue` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setBrightness( + request: Android_Emulation_Control_BrightnessValue, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_DisplayMode` to respond with. + func getDisplayMode( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_DisplayMode + + /// Handle the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_DisplayMode` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setDisplayMode( + request: Android_Emulation_Control_DisplayMode, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_XrOptions` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setXrOptions( + request: Android_Emulation_Control_XrOptions, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_XrOptions` to respond with. + func getXrOptions( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_XrOptions + + /// Handle the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - request: A `Android_Emulation_Control_Environment` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `SwiftProtobuf.Google_Protobuf_Empty` to respond with. + func setEnvironment( + request: Android_Emulation_Control_Environment, + context: GRPCCore.ServerContext + ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + + /// Handle the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_Environment` to respond with. + func getEnvironment( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_Environment + + /// Handle the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - request: A `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Android_Emulation_Control_CameraList` to respond with. + func getHostCameras( + request: SwiftProtobuf.Google_Protobuf_Empty, + context: GRPCCore.ServerContext + ) async throws -> Android_Emulation_Control_CameraList + } +} + +// Default implementation of 'registerMethods(with:)'. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamSensor.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamSensor( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getSensor.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getSensor( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setSensor.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setSensor( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setPhysicalModel.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setPhysicalModel( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getPhysicalModel.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getPhysicalModel( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamPhysicalModel.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamPhysicalModel( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setClipboard.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setClipboard( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getClipboard.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getClipboard( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamClipboard.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamClipboard( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setBattery.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setBattery( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getBattery.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getBattery( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setGps.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setGps( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getGps.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getGps( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.sendFingerprint.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sendFingerprint( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.sendKey.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sendKey( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.sendTouch.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sendTouch( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.sendMouse.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sendMouse( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.injectWheel.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.injectWheel( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamInputEvent.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamInputEvent( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.sendPhone.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sendPhone( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.sendSms.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sendSms( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setPhoneNumber.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setPhoneNumber( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getStatus.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getStatus( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getScreenshot.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getScreenshot( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamScreenshot.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamScreenshot( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamAudio.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamAudio( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.injectAudio.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.injectAudio( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getMicrophoneState.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getMicrophoneState( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setMicrophoneState.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setMicrophoneState( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getLogcat.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getLogcat( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamLogcat.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamLogcat( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setVmState.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setVmState( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getVmState.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getVmState( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setDisplayConfigurations.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setDisplayConfigurations( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getDisplayConfigurations.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getDisplayConfigurations( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.streamNotification.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.streamNotification( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.rotateVirtualSceneCamera.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.rotateVirtualSceneCamera( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setVirtualSceneCameraVelocity.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setVirtualSceneCameraVelocity( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setPosture.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setPosture( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getBrightness.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getBrightness( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setBrightness.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setBrightness( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getDisplayMode.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getDisplayMode( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setDisplayMode.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setDisplayMode( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setXrOptions.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setXrOptions( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getXrOptions.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getXrOptions( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.setEnvironment.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.setEnvironment( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getEnvironment.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getEnvironment( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Android_Emulation_Control_EmulatorController.Method.getHostCameras.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.getHostCameras( + request: request, + context: context + ) + } + ) + } +} + +// Default implementation of streaming methods from 'StreamingServiceProtocol'. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController.ServiceProtocol { + internal func streamSensor( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamSensor( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func getSensor( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getSensor( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setSensor( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setSensor( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setPhysicalModel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setPhysicalModel( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getPhysicalModel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getPhysicalModel( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func streamPhysicalModel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamPhysicalModel( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func setClipboard( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setClipboard( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getClipboard( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getClipboard( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func streamClipboard( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamClipboard( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func setBattery( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setBattery( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getBattery( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getBattery( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setGps( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setGps( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getGps( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getGps( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func sendFingerprint( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sendFingerprint( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func sendKey( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sendKey( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func sendTouch( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sendTouch( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func sendMouse( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sendMouse( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func injectWheel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.injectWheel( + request: request, + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func streamInputEvent( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamInputEvent( + request: request, + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func sendPhone( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sendPhone( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func sendSms( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sendSms( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setPhoneNumber( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setPhoneNumber( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getStatus( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getStatus( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getScreenshot( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getScreenshot( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func streamScreenshot( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamScreenshot( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func streamAudio( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamAudio( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func injectAudio( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.injectAudio( + request: request, + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getMicrophoneState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getMicrophoneState( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setMicrophoneState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setMicrophoneState( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getLogcat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getLogcat( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func streamLogcat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamLogcat( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func setVmState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setVmState( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getVmState( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getVmState( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setDisplayConfigurations( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setDisplayConfigurations( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getDisplayConfigurations( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getDisplayConfigurations( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func streamNotification( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.streamNotification( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func rotateVirtualSceneCamera( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.rotateVirtualSceneCamera( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setVirtualSceneCameraVelocity( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setVirtualSceneCameraVelocity( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setPosture( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setPosture( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getBrightness( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getBrightness( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setBrightness( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setBrightness( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getDisplayMode( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getDisplayMode( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setDisplayMode( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setDisplayMode( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setXrOptions( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setXrOptions( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getXrOptions( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getXrOptions( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func setEnvironment( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.setEnvironment( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getEnvironment( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getEnvironment( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func getHostCameras( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.getHostCameras( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } +} + +// Default implementation of methods from 'ServiceProtocol'. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController.SimpleServiceProtocol { + internal func streamSensor( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamSensor( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func getSensor( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getSensor( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setSensor( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setSensor( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setPhysicalModel( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setPhysicalModel( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getPhysicalModel( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getPhysicalModel( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func streamPhysicalModel( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamPhysicalModel( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func setClipboard( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setClipboard( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getClipboard( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getClipboard( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func streamClipboard( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamClipboard( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func setBattery( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setBattery( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getBattery( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getBattery( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setGps( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setGps( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getGps( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getGps( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func sendFingerprint( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sendFingerprint( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func sendKey( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sendKey( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func sendTouch( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sendTouch( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func sendMouse( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sendMouse( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func injectWheel( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.injectWheel( + request: request.messages, + context: context + ), + metadata: [:] + ) + } + + internal func streamInputEvent( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.streamInputEvent( + request: request.messages, + context: context + ), + metadata: [:] + ) + } + + internal func sendPhone( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sendPhone( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func sendSms( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sendSms( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setPhoneNumber( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setPhoneNumber( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getStatus( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getStatus( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getScreenshot( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getScreenshot( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func streamScreenshot( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamScreenshot( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func streamAudio( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamAudio( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func injectAudio( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.injectAudio( + request: request.messages, + context: context + ), + metadata: [:] + ) + } + + internal func getMicrophoneState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getMicrophoneState( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setMicrophoneState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setMicrophoneState( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getLogcat( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getLogcat( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func streamLogcat( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamLogcat( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func setVmState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setVmState( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getVmState( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getVmState( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setDisplayConfigurations( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setDisplayConfigurations( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getDisplayConfigurations( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getDisplayConfigurations( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func streamNotification( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.streamNotification( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func rotateVirtualSceneCamera( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.rotateVirtualSceneCamera( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setVirtualSceneCameraVelocity( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setVirtualSceneCameraVelocity( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setPosture( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setPosture( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getBrightness( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getBrightness( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setBrightness( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setBrightness( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getDisplayMode( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getDisplayMode( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setDisplayMode( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setDisplayMode( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setXrOptions( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setXrOptions( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getXrOptions( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getXrOptions( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func setEnvironment( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.setEnvironment( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getEnvironment( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getEnvironment( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func getHostCameras( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getHostCameras( + request: request.message, + context: context + ), + metadata: [:] + ) + } +} + +// MARK: android.emulation.control.EmulatorController (client) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController { + /// Generated client protocol for the "android.emulation.control.EmulatorController" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > An EmulatorController service lets you control the emulator. + /// > Note that this is currently an experimental feature, and that the + /// > service definition might change without notice. Use at your own risk! + /// > + /// > We use the following rough conventions: + /// > + /// > streamXXX --> streams values XXX (usually for emulator lifetime). Values + /// > are updated as soon as they become available. + /// > getXXX --> gets a single value XXX + /// > setXXX --> sets a single value XXX, does not returning state, these + /// > usually have an observable lasting side effect. + /// > sendXXX --> send a single event XXX, possibly returning state information. + /// > android usually responds to these events. + internal protocol ClientProtocol: Sendable { + /// Call the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_SensorValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_SensorValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamSensor( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_SensorValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_SensorValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getSensor( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_SensorValue` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setSensor( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setPhysicalModel( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getPhysicalModel( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamPhysicalModel( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ClipData` message. + /// - serializer: A serializer for `Android_Emulation_Control_ClipData` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setClipboard( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_ClipData` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getClipboard( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_ClipData` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamClipboard( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BatteryState` message. + /// - serializer: A serializer for `Android_Emulation_Control_BatteryState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setBattery( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_BatteryState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getBattery( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_GpsState` message. + /// - serializer: A serializer for `Android_Emulation_Control_GpsState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setGps( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_GpsState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getGps( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Fingerprint` message. + /// - serializer: A serializer for `Android_Emulation_Control_Fingerprint` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendFingerprint( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_KeyboardEvent` message. + /// - serializer: A serializer for `Android_Emulation_Control_KeyboardEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendKey( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_TouchEvent` message. + /// - serializer: A serializer for `Android_Emulation_Control_TouchEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendTouch( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MouseEvent` message. + /// - serializer: A serializer for `Android_Emulation_Control_MouseEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendMouse( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_WheelEvent` messages. + /// - serializer: A serializer for `Android_Emulation_Control_WheelEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func injectWheel( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_InputEvent` messages. + /// - serializer: A serializer for `Android_Emulation_Control_InputEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamInputEvent( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneCall` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhoneCall` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhoneResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendPhone( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SmsMessage` message. + /// - serializer: A serializer for `Android_Emulation_Control_SmsMessage` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhoneResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendSms( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneNumber` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhoneNumber` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhoneResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setPhoneNumber( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_EmulatorStatus` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getStatus( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - serializer: A serializer for `Android_Emulation_Control_ImageFormat` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Image` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getScreenshot( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - serializer: A serializer for `Android_Emulation_Control_ImageFormat` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Image` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamScreenshot( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_AudioFormat` message. + /// - serializer: A serializer for `Android_Emulation_Control_AudioFormat` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_AudioPacket` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamAudio( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_AudioPacket` messages. + /// - serializer: A serializer for `Android_Emulation_Control_AudioPacket` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func injectAudio( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_MicrophoneState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getMicrophoneState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MicrophoneState` message. + /// - serializer: A serializer for `Android_Emulation_Control_MicrophoneState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setMicrophoneState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - serializer: A serializer for `Android_Emulation_Control_LogMessage` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_LogMessage` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getLogcat( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - serializer: A serializer for `Android_Emulation_Control_LogMessage` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_LogMessage` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamLogcat( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_VmRunState` message. + /// - serializer: A serializer for `Android_Emulation_Control_VmRunState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setVmState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_VmRunState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getVmState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayConfigurations` message. + /// - serializer: A serializer for `Android_Emulation_Control_DisplayConfigurations` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_DisplayConfigurations` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setDisplayConfigurations( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_DisplayConfigurations` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getDisplayConfigurations( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Notification` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func streamNotification( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_RotationRadian` message. + /// - serializer: A serializer for `Android_Emulation_Control_RotationRadian` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func rotateVirtualSceneCamera( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Velocity` message. + /// - serializer: A serializer for `Android_Emulation_Control_Velocity` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setVirtualSceneCameraVelocity( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Posture` message. + /// - serializer: A serializer for `Android_Emulation_Control_Posture` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setPosture( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_BrightnessValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_BrightnessValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getBrightness( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_BrightnessValue` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setBrightness( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_DisplayMode` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getDisplayMode( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayMode` message. + /// - serializer: A serializer for `Android_Emulation_Control_DisplayMode` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setDisplayMode( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_XrOptions` message. + /// - serializer: A serializer for `Android_Emulation_Control_XrOptions` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setXrOptions( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_XrOptions` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getXrOptions( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Environment` message. + /// - serializer: A serializer for `Android_Emulation_Control_Environment` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func setEnvironment( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Environment` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getEnvironment( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_CameraList` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getHostCameras( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "android.emulation.control.EmulatorController" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > An EmulatorController service lets you control the emulator. + /// > Note that this is currently an experimental feature, and that the + /// > service definition might change without notice. Use at your own risk! + /// > + /// > We use the following rough conventions: + /// > + /// > streamXXX --> streams values XXX (usually for emulator lifetime). Values + /// > are updated as soon as they become available. + /// > getXXX --> gets a single value XXX + /// > setXXX --> sets a single value XXX, does not returning state, these + /// > usually have an observable lasting side effect. + /// > sendXXX --> send a single event XXX, possibly returning state information. + /// > android usually responds to these events. + internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_SensorValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_SensorValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamSensor( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamSensor.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_SensorValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_SensorValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getSensor( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getSensor.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_SensorValue` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setSensor( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setSensor.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPhysicalModel( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setPhysicalModel.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getPhysicalModel( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getPhysicalModel.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhysicalModelValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamPhysicalModel( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamPhysicalModel.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ClipData` message. + /// - serializer: A serializer for `Android_Emulation_Control_ClipData` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setClipboard( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setClipboard.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_ClipData` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getClipboard( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getClipboard.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_ClipData` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamClipboard( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamClipboard.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BatteryState` message. + /// - serializer: A serializer for `Android_Emulation_Control_BatteryState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setBattery( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setBattery.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_BatteryState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getBattery( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getBattery.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_GpsState` message. + /// - serializer: A serializer for `Android_Emulation_Control_GpsState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setGps( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setGps.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_GpsState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getGps( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getGps.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Fingerprint` message. + /// - serializer: A serializer for `Android_Emulation_Control_Fingerprint` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendFingerprint( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.sendFingerprint.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_KeyboardEvent` message. + /// - serializer: A serializer for `Android_Emulation_Control_KeyboardEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendKey( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.sendKey.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_TouchEvent` message. + /// - serializer: A serializer for `Android_Emulation_Control_TouchEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendTouch( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.sendTouch.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MouseEvent` message. + /// - serializer: A serializer for `Android_Emulation_Control_MouseEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendMouse( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.sendMouse.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_WheelEvent` messages. + /// - serializer: A serializer for `Android_Emulation_Control_WheelEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func injectWheel( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.injectWheel.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_InputEvent` messages. + /// - serializer: A serializer for `Android_Emulation_Control_InputEvent` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamInputEvent( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamInputEvent.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneCall` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhoneCall` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhoneResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendPhone( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.sendPhone.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SmsMessage` message. + /// - serializer: A serializer for `Android_Emulation_Control_SmsMessage` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhoneResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendSms( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.sendSms.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneNumber` message. + /// - serializer: A serializer for `Android_Emulation_Control_PhoneNumber` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_PhoneResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPhoneNumber( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setPhoneNumber.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_EmulatorStatus` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getStatus( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getStatus.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - serializer: A serializer for `Android_Emulation_Control_ImageFormat` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Image` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getScreenshot( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getScreenshot.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - serializer: A serializer for `Android_Emulation_Control_ImageFormat` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Image` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamScreenshot( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamScreenshot.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_AudioFormat` message. + /// - serializer: A serializer for `Android_Emulation_Control_AudioFormat` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_AudioPacket` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamAudio( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamAudio.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_AudioPacket` messages. + /// - serializer: A serializer for `Android_Emulation_Control_AudioPacket` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func injectAudio( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.injectAudio.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_MicrophoneState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getMicrophoneState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getMicrophoneState.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MicrophoneState` message. + /// - serializer: A serializer for `Android_Emulation_Control_MicrophoneState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setMicrophoneState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setMicrophoneState.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - serializer: A serializer for `Android_Emulation_Control_LogMessage` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_LogMessage` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getLogcat( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getLogcat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - serializer: A serializer for `Android_Emulation_Control_LogMessage` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_LogMessage` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamLogcat( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamLogcat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_VmRunState` message. + /// - serializer: A serializer for `Android_Emulation_Control_VmRunState` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setVmState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setVmState.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_VmRunState` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getVmState( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getVmState.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayConfigurations` message. + /// - serializer: A serializer for `Android_Emulation_Control_DisplayConfigurations` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_DisplayConfigurations` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setDisplayConfigurations( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setDisplayConfigurations.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_DisplayConfigurations` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getDisplayConfigurations( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getDisplayConfigurations.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Notification` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamNotification( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.streamNotification.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_RotationRadian` message. + /// - serializer: A serializer for `Android_Emulation_Control_RotationRadian` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func rotateVirtualSceneCamera( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.rotateVirtualSceneCamera.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Velocity` message. + /// - serializer: A serializer for `Android_Emulation_Control_Velocity` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setVirtualSceneCameraVelocity( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setVirtualSceneCameraVelocity.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Posture` message. + /// - serializer: A serializer for `Android_Emulation_Control_Posture` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPosture( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setPosture.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_BrightnessValue` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_BrightnessValue` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getBrightness( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getBrightness.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - serializer: A serializer for `Android_Emulation_Control_BrightnessValue` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setBrightness( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setBrightness.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_DisplayMode` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getDisplayMode( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getDisplayMode.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayMode` message. + /// - serializer: A serializer for `Android_Emulation_Control_DisplayMode` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setDisplayMode( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setDisplayMode.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_XrOptions` message. + /// - serializer: A serializer for `Android_Emulation_Control_XrOptions` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setXrOptions( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setXrOptions.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_XrOptions` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getXrOptions( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getXrOptions.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Environment` message. + /// - serializer: A serializer for `Android_Emulation_Control_Environment` messages. + /// - deserializer: A deserializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setEnvironment( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.setEnvironment.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_Environment` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getEnvironment( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getEnvironment.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - serializer: A serializer for `SwiftProtobuf.Google_Protobuf_Empty` messages. + /// - deserializer: A deserializer for `Android_Emulation_Control_CameraList` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getHostCameras( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Android_Emulation_Control_EmulatorController.Method.getHostCameras.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController.ClientProtocol { + /// Call the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamSensor( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamSensor( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getSensor( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getSensor( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SensorValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setSensor( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setSensor( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPhysicalModel( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setPhysicalModel( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getPhysicalModel( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getPhysicalModel( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhysicalModelValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamPhysicalModel( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamPhysicalModel( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ClipData` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setClipboard( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setClipboard( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getClipboard( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getClipboard( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamClipboard( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamClipboard( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BatteryState` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setBattery( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setBattery( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getBattery( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getBattery( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_GpsState` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setGps( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setGps( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getGps( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getGps( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Fingerprint` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendFingerprint( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendFingerprint( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_KeyboardEvent` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendKey( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendKey( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_TouchEvent` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendTouch( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendTouch( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MouseEvent` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendMouse( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendMouse( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_WheelEvent` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func injectWheel( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.injectWheel( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_InputEvent` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamInputEvent( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.streamInputEvent( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneCall` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendPhone( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendPhone( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_SmsMessage` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendSms( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendSms( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_PhoneNumber` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPhoneNumber( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setPhoneNumber( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getStatus( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getStatus( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getScreenshot( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getScreenshot( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_ImageFormat` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamScreenshot( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamScreenshot( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_AudioFormat` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamAudio( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamAudio( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Android_Emulation_Control_AudioPacket` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func injectAudio( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.injectAudio( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getMicrophoneState( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getMicrophoneState( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_MicrophoneState` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setMicrophoneState( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setMicrophoneState( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getLogcat( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getLogcat( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_LogMessage` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamLogcat( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamLogcat( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_VmRunState` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setVmState( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setVmState( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getVmState( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getVmState( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayConfigurations` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setDisplayConfigurations( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setDisplayConfigurations( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getDisplayConfigurations( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getDisplayConfigurations( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamNotification( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.streamNotification( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_RotationRadian` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func rotateVirtualSceneCamera( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.rotateVirtualSceneCamera( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Velocity` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setVirtualSceneCameraVelocity( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setVirtualSceneCameraVelocity( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Posture` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPosture( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setPosture( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getBrightness( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getBrightness( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_BrightnessValue` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setBrightness( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setBrightness( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getDisplayMode( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getDisplayMode( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_DisplayMode` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setDisplayMode( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setDisplayMode( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_XrOptions` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setXrOptions( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setXrOptions( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getXrOptions( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getXrOptions( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - request: A request containing a single `Android_Emulation_Control_Environment` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setEnvironment( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.setEnvironment( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getEnvironment( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getEnvironment( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - request: A request containing a single `SwiftProtobuf.Google_Protobuf_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getHostCameras( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getHostCameras( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Android_Emulation_Control_EmulatorController.ClientProtocol { + /// Call the "streamSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the sensor data + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamSensor( + _ message: Android_Emulation_Control_SensorValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamSensor( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified sensor. + /// > + /// > The following gRPC error codes can be returned: + /// > - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + /// > + /// > The `status` field in the `SensorValue` reply indicates the operational state: + /// > - `OK` (0): Sensor data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + /// > - `DISABLED` (3): The sensor is disabled. + /// > - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getSensor( + _ message: Android_Emulation_Control_SensorValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getSensor( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setSensor" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the override value for a specified sensor. This operation is asynchronous + /// > and executed on the emulator's main looper. An immediate subsequent `getSensor` + /// > call might not reflect the newly set value. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// > No specific gRPC error codes are returned by this method itself, but underlying + /// > operations might log warnings if the sensor agent is unavailable. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setSensor( + _ message: Android_Emulation_Control_SensorValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setSensor( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > set/get/stream the physical model, this is likely the one you are + /// > looking for when you wish to modify the device state. + /// > + /// > This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + /// > call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + /// > physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPhysicalModel( + _ message: Android_Emulation_Control_PhysicalModelValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setPhysicalModel( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current value of a specified physical model parameter. + /// > + /// > The `status` field in the `PhysicalModelValue` reply indicates the operational state: + /// > - `OK` (0): Physical model data retrieved successfully. + /// > - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + /// > - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getPhysicalModel( + _ message: Android_Emulation_Control_PhysicalModelValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getPhysicalModel( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamPhysicalModel" method. + /// + /// > Source IDL Documentation: + /// > + /// > This RPC is not implemented in EmulatorController and will return an unimplemented error. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamPhysicalModel( + _ message: Android_Emulation_Control_PhysicalModelValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamPhysicalModel( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically sets the current primary clipboard data. This operation is asynchronous + /// > and executed on the emulator's main looper. It triggers a `ClipboardEvent` + /// > to all listeners (except the originating channel) indicating the new content. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setClipboard( + _ message: Android_Emulation_Control_ClipData, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setClipboard( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current primary clipboard data. This is a synchronous operation. + /// > + /// > This method returns `OK` (code 0) and the current `ClipData` upon success. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getClipboard( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getClipboard( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamClipboard" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams real-time updates of the clipboard content. Upon subscription, + /// > it immediately sends the current clipboard state. Subsequent updates are + /// > streamed as new content becomes available from the guest or is set via `setClipboard` + /// > from a different client. Events originating from the same client that initiated + /// > the stream are filtered out to prevent echoing. + /// > + /// > It is possible to miss very rapid clipboard updates. The stream will block + /// > awaiting new events after the initial state is sent. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamClipboard( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamClipboard( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's battery state to the provided values. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setBattery( + _ message: Android_Emulation_Control_BatteryState, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setBattery( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getBattery" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current battery state from the emulator. This is a + /// > synchronous operation that waits for completion. + /// > + /// > This method returns `OK` (code 0) and populates the `BatteryState` reply + /// > with the current information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getBattery( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getBattery( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state of the GPS in the emulator. This operation is asynchronous + /// > and executed on the main looper. It updates the emulator's GPS location + /// > (latitude, longitude, altitude, speed, bearing, and satellites) and sets + /// > the timestamp. + /// > + /// > Note: Setting the GPS position will not be immediately reflected in the user + /// > interface. Android typically samples GPS at 1 Hz. + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setGps( + _ message: Android_Emulation_Control_GpsState, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setGps( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getGps" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the latest GPS state as reported by the emulator. This includes data + /// > delivered by previous `setGps` calls or from the location UI if active. + /// > This is a synchronous operation that waits for completion. + /// > + /// > Note: The returned GPS state is not necessarily the exact coordinate + /// > visible at the time due to Android's typical 1 Hz GPS sample frequency. + /// > + /// > This method returns `OK` (code 0) and populates the `GpsState` reply + /// > with the retrieved information upon success. No explicit gRPC error codes + /// > are returned by this method. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getGps( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getGps( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendFingerprint" method. + /// + /// > Source IDL Documentation: + /// > + /// > Simulates a touch event on the fingerprint sensor. This operation is + /// > executed asynchronously on the main looper. + /// > + /// > The `isTouching` field indicates whether the fingerprint sensor is being + /// > touched, and `touchId` specifies the identifier of the registered + /// > fingerprint. The `setTouch` agent function is used to apply these values. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling + /// > of the operation. No explicit gRPC error codes are returned by this method. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendFingerprint( + _ message: Android_Emulation_Control_Fingerprint, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendFingerprint( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendKey" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a keyboard event to the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + /// > The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + /// > - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + /// > - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + /// > - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + /// > - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendKey( + _ message: Android_Emulation_Control_KeyboardEvent, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendKey( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendTouch" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + /// > Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + /// > Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendTouch( + _ message: Android_Emulation_Control_TouchEvent, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendTouch( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendMouse" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + /// > + /// > The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + /// > In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + /// > The event is sent to the emulator's `user_event_agent`. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendMouse( + _ message: Android_Emulation_Control_MouseEvent, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendMouse( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "injectWheel" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + /// > + /// > Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + /// > If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + /// > Otherwise, it's sent as a standard mouse wheel event. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func injectWheel( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.injectWheel( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamInputEvent" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + /// > This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + /// > + /// > Supported input types: + /// > - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + /// > - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + /// > - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + /// > - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + /// > - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + /// > - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + /// > - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + /// > - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + /// > - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + /// > - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + /// > - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + /// > - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + /// > + /// > All underlying input sending operations for XR events are executed asynchronously on the main looper. + /// > + /// > Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + /// > The stream reactor automatically deletes itself upon completion. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamInputEvent( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.streamInputEvent( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendPhone" method. + /// + /// > Source IDL Documentation: + /// > + /// > Initiates or manipulates a phone call in the emulator. This is a synchronous + /// > operation executed on the main looper. + /// > + /// > The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + /// > and the target phone `number`. The `telephonyCmd` agent function handles the + /// > actual call action. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The operation was successful. + /// > - `BadOperation` (1): The provided `operation` enum is out of range. + /// > - `BadNumber` (2): The provided `number` is malformed. + /// > - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// > - `RadioOff` (5): The emulator's radio is turned off. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendPhone( + _ message: Android_Emulation_Control_PhoneCall, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendPhone( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "sendSms" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends an SMS message to the emulator. This is a synchronous operation. + /// > + /// > The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + /// > The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + /// > (Protocol Data Units) and delivered via the modem agent. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The SMS message was successfully delivered. + /// > - `BadNumber` (2): The `srcAddress` is malformed. + /// > - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + /// > + /// > Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sendSms( + _ message: Android_Emulation_Control_SmsMessage, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendSms( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPhoneNumber" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the emulator's phone number. This is a synchronous operation. + /// > + /// > The `PhoneNumber` message contains the new `number` to be set. The modem agent's + /// > `amodem_update_phone_number` function is used for this update. + /// > + /// > Returns a `PhoneResponse` indicating the outcome: + /// > - `OK` (0): The phone number was successfully updated. + /// > - `BadNumber` (2): The provided `number` is invalid for the modem. + /// > - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPhoneNumber( + _ message: Android_Emulation_Control_PhoneNumber, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setPhoneNumber( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getStatus" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current status of the emulator. This includes comprehensive + /// > information about the virtual machine's configuration and the guest operating system's state. + /// > + /// > The `EmulatorStatus` reply contains: + /// > - `version`: The emulator version string. + /// > - `uptime`: The time the emulator has been active in milliseconds. + /// > - `booted`: A boolean indicating if the device has completed booting. + /// > - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + /// > - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + /// > - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + /// > - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + /// > - "multidisplay": "available" or "unavailable" based on display pipe readiness. + /// > - "androidVersion": The Android version of the guest OS. + /// > - "hypervisorVersion": The hypervisor version used by the guest. + /// > + /// > This method returns `OK` (code 0) upon successful retrieval of the emulator status. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getStatus( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getStatus( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves a single screenshot in the desired format. + /// > + /// > The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + /// > the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + /// > The returned image will never exceed the device's actual display resolution, but can be smaller. + /// > + /// > The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + /// > For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + /// > The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + /// > + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + /// > is too small, a `FAILED_PRECONDITION` error will be returned. + /// > + /// > This method returns: + /// > - `OK` (code 0) and an `Image` object upon success. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + /// > - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + /// > + /// > This method will return an image with width 0 and height 0 if the display is not visible. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getScreenshot( + _ message: Android_Emulation_Control_ImageFormat, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getScreenshot( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamScreenshot" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + /// > + /// > A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + /// > Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + /// > Subsequent frames are delivered when new content is available or a sensor event triggers an update. + /// > If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + /// > + /// > `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + /// > The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + /// > provided shared memory handle is valid and mapped, pixel data will be written directly to it. + /// > The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + /// > + /// > Performance considerations: + /// > - Streaming can produce a significant amount of data. + /// > - `PNG` format is CPU-intensive due to encoding overhead. + /// > - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + /// > + /// > This method returns: + /// > - A server-side streaming reactor. + /// > - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + /// > - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamScreenshot( + _ message: Android_Emulation_Control_ImageFormat, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamScreenshot( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + /// > A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + /// > If `samplingRate` is 0, it defaults to 44100 Hz. + /// > The stream may block indefinitely if the emulator ceases to produce audio. + /// > Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + /// > + /// > This method returns `OK` (code 0) upon successful streaming initiation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamAudio( + _ message: Android_Emulation_Control_AudioFormat, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamAudio( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "injectAudio" method. + /// + /// > Source IDL Documentation: + /// > + /// > Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + /// > Audio packets are processed at a rate determined by the emulator's request for frames. + /// > An internal buffer can hold approximately 300ms of audio. + /// > + /// > Notes: + /// > - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + /// > - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + /// > - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + /// > + /// > Returns the following gRPC error codes: + /// > - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + /// > - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + /// > + /// > This method returns `OK` (code 0) upon successful completion of the stream. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func injectAudio( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.injectAudio( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current settings for the microphone + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getMicrophoneState( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getMicrophoneState( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setMicrophoneState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the state for the microphone + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setMicrophoneState( + _ message: Android_Emulation_Control_MicrophoneState, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setMicrophoneState( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Deprecated, please use the streamLogcat method instead. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getLogcat( + _ message: Android_Emulation_Control_LogMessage, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getLogcat( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamLogcat" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + /// > The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + /// > Log lines are processed individually as they arrive. + /// > + /// > The `LogMessage.sort` field determines the output format: + /// > - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + /// > and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + /// > available for Android API Level 23 (Marshmallow) and later. + /// > - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + /// > + /// > The stream continues as long as the underlying `logcat` process is running and the client is connected. + /// > The stream will naturally terminate if the `logcat` process stops or the connection is lost. + /// > No explicit gRPC error codes are returned by this method during active streaming. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamLogcat( + _ message: Android_Emulation_Control_LogMessage, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamLogcat( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Transitions the virtual machine to the desired state. This operation is scheduled + /// > asynchronously. + /// > + /// > Transition commands: + /// > - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + /// > - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + /// > - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + /// > - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + /// > - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + /// > This can cause data corruption and must be used with caution. + /// > + /// > Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + /// > transition but are not observed as states in `getVmState`. Purely observable states + /// > (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + /// > + /// > Returns once the operation has been scheduled. + /// > Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setVmState( + _ message: Android_Emulation_Control_VmRunState, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setVmState( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getVmState" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current execution state of the virtual machine. + /// > + /// > The returned `VmRunState` represents the current condition of the VM: + /// > - `RUNNING`: The virtual machine is actively executing. + /// > - `PAUSED`: Execution is suspended (CPUs are halted). + /// > - `SHUTDOWN`: The guest operating system has initiated a shutdown. + /// > - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + /// > - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + /// > - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + /// > - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + /// > + /// > This is a synchronous query operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getVmState( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getVmState( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Atomically changes the current multi-display configuration. This operation applies + /// > the provided `DisplayConfigurations`, with special handling for secondary displays. + /// > Display ID 0 (the primary display) cannot be modified via this RPC. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > Input Validation: + /// > - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + /// > - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + /// > is outside valid ranges as determined by `multiDisplayParamValidate`. + /// > - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + /// > `[1, userConfigurable]`. + /// > + /// > Atomic Update and Rollback: + /// > The system attempts to apply each display configuration. If any update fails + /// > (e.g., after multiple retries for transient pipe errors), a rollback mechanism + /// > is initiated: successfully updated displays are reverted to their previous state, + /// > and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + /// > + /// > Deletion of Unrequested Displays: + /// > Any displays that were active before this call but are not present in the new + /// > `request.displays` (and are not display ID 0) will be deleted. + /// > + /// > Notifications: + /// > Upon successful completion, `notifyDisplayChanges()` is called to inform + /// > third-party subscribers. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + /// > - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + /// > - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setDisplayConfigurations( + _ message: Android_Emulation_Control_DisplayConfigurations, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setDisplayConfigurations( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getDisplayConfigurations" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The `android::featurecontrol::MultiDisplay` feature must be enabled. + /// > + /// > The `DisplayConfigurations` reply contains: + /// > - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + /// > the width, height, DPI, flags, and ID of an active display. + /// > For Pixel Fold devices, only the main display configuration might be relevant. + /// > - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + /// > - `maxDisplays`: The total maximum number of displays the emulator supports. + /// > + /// > This method returns: + /// > - `OK` (code 0) and the current `DisplayConfigurations` upon success. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getDisplayConfigurations( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getDisplayConfigurations( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "streamNotification" method. + /// + /// > Source IDL Documentation: + /// > + /// > Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + /// > Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + /// > and XR options are immediately sent. The stream then continuously delivers new notifications + /// > when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + /// > + /// > Notifications include: + /// > - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + /// > - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + /// > - `Posture`: Reports changes in the device's foldable posture. + /// > - `BootCompletedNotification`: Indicates when the emulator has finished booting. + /// > - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + /// > - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + /// > - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + /// > - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + /// > - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + /// > + /// > This method returns a server-side streaming reactor. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func streamNotification( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.streamNotification( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "rotateVirtualSceneCamera" method. + /// + /// > Source IDL Documentation: + /// > + /// > Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func rotateVirtualSceneCamera( + _ message: Android_Emulation_Control_RotationRadian, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.rotateVirtualSceneCamera( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setVirtualSceneCameraVelocity" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Velocity` message specifies components in meters per second along the x, y, and z axes. + /// > The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + /// > The transition to these target velocity values may be smoothed over time by the implementation. + /// > This operation only succeeds if the virtual scene camera is actively connected. + /// > + /// > This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setVirtualSceneCameraVelocity( + _ message: Android_Emulation_Control_Velocity, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setVirtualSceneCameraVelocity( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setPosture" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the foldable posture of the device. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `Posture` message contains a `PostureValue` enum, defining the desired + /// > physical configuration of the foldable device. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setPosture( + _ message: Android_Emulation_Control_Posture, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setPosture( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + /// > Internal mapping converts these to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getBrightness( + _ message: Android_Emulation_Control_BrightnessValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getBrightness( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setBrightness" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the backlight brightness for a specified light type. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + /// > The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + /// > Internal mapping converts `LightType` to string names like "lcd_backlight". + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + /// > - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setBrightness( + _ message: Android_Emulation_Control_BrightnessValue, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setBrightness( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > + /// > The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + /// > derived from the `getResizableActiveConfigId()`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the display mode. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getDisplayMode( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getDisplayMode( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setDisplayMode" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > Preconditions: + /// > - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + /// > - The emulator's `changeResizableDisplay` agent function must be available. + /// > + /// > The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + /// > - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setDisplayMode( + _ message: Android_Emulation_Control_DisplayMode, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setDisplayMode( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Changes the XR-related settings of the emulator. This operation is asynchronous + /// > and executed on the main looper. + /// > + /// > The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + /// > and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + /// > Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + /// > - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setXrOptions( + _ message: Android_Emulation_Control_XrOptions, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setXrOptions( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getXrOptions" method. + /// + /// > Source IDL Documentation: + /// > + /// > Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + /// > + /// > Preconditions: + /// > - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + /// > (Note: Current implementation of `isXrGuestOs()` always returns true). + /// > + /// > The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + /// > + /// > Returns the following gRPC error codes: + /// > - `OK` (code 0): Upon successful retrieval of the XR options. + /// > - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getXrOptions( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getXrOptions( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "setEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sets the environment background for AI glasses. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func setEnvironment( + _ message: Android_Emulation_Control_Environment, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.setEnvironment( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getEnvironment" method. + /// + /// > Source IDL Documentation: + /// > + /// > Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + /// > will be set to default values. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getEnvironment( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getEnvironment( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "getHostCameras" method. + /// + /// > Source IDL Documentation: + /// > + /// > Returns the list of video cameras connected to the host machine. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getHostCameras( + _ message: SwiftProtobuf.Google_Protobuf_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getHostCameras( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Generated/EmulatorController/emulator_controller.pb.swift b/Generated/EmulatorController/emulator_controller.pb.swift new file mode 100644 index 0000000..f9f928b --- /dev/null +++ b/Generated/EmulatorController/emulator_controller.pb.swift @@ -0,0 +1,6218 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: emulator_controller.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Note that if you add/remove methods in this file you must update +/// the metrics sql as well ./android/scripts/gen-grpc-sql.py +/// +/// Please group deleted methods in a block including the date (MM/DD/YY) +/// it was removed. This enables us to easily keep metrics around after removal +/// +/// List of deleted methods +/// rpc iWasDeleted (03/12/12) +/// ... + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate nonisolated struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// in line with android/emulation/resizable_display_config.h +nonisolated enum Android_Emulation_Control_DisplayModeValue: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case phone // = 0 + case foldable // = 1 + case tablet // = 2 + case desktop // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .phone + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .phone + case 1: self = .foldable + case 2: self = .tablet + case 3: self = .desktop + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .phone: return 0 + case .foldable: return 1 + case .tablet: return 2 + case .desktop: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_DisplayModeValue] = [ + .phone, + .foldable, + .tablet, + .desktop, + ] + +} + +/// Represents the execution state of the virtual machine, used for both querying the +/// current status and requesting state transitions. +nonisolated struct Android_Emulation_Control_VmRunState: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var state: Android_Emulation_Control_VmRunState.RunState = .unknown + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum RunState: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Purely observable state. The virtual machine is in an unmapped or transient state. + /// Cannot be set directly. + case unknown // = 0 + + /// Active execution state. Setting this state resumes the virtual machine. + case running // = 1 + + /// Purely observable state. The virtual machine is paused while restoring its state + /// (e.g., loading a snapshot). Cannot be set directly. + case restoreVm // = 2 + + /// Suspended execution state. Setting this state pauses the virtual machine and halts + /// CPU cycles. + case paused // = 3 + + /// Purely observable state. The virtual machine is paused while saving its state + /// (e.g., taking a snapshot). Cannot be set directly. + case saveVm // = 4 + + /// Graceful shutdown state. Setting this state initiates a clean shutdown of the + /// virtual machine. + case shutdown // = 5 + + /// Write-only command. Immediately terminates the emulator process without cleanup. + /// Can cause data corruption and must be used with caution. + case terminate // = 7 + + /// Write-only command. Triggers a warm reset of the virtual machine. + case reset // = 9 + + /// Purely observable state. The virtual machine has stopped execution due to a guest + /// panic, internal error, or I/O failure. Cannot be set directly. + case internalError // = 10 + + /// Write-only command. Triggers a warm reset of the virtual machine (equivalent to `RESET`). + case restart // = 11 + + /// Write-only command. Starts or resumes execution of the virtual machine (equivalent + /// to `RUNNING`). + case start // = 12 + + /// Write-only command. Stops execution of the virtual machine (equivalent to `PAUSED`). + case stop // = 13 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .running + case 2: self = .restoreVm + case 3: self = .paused + case 4: self = .saveVm + case 5: self = .shutdown + case 7: self = .terminate + case 9: self = .reset + case 10: self = .internalError + case 11: self = .restart + case 12: self = .start + case 13: self = .stop + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .running: return 1 + case .restoreVm: return 2 + case .paused: return 3 + case .saveVm: return 4 + case .shutdown: return 5 + case .terminate: return 7 + case .reset: return 9 + case .internalError: return 10 + case .restart: return 11 + case .start: return 12 + case .stop: return 13 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_VmRunState.RunState] = [ + .unknown, + .running, + .restoreVm, + .paused, + .saveVm, + .shutdown, + .terminate, + .reset, + .internalError, + .restart, + .start, + .stop, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_ParameterValue: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var data: [Float] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_PhysicalModelValue: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var target: Android_Emulation_Control_PhysicalModelValue.PhysicalType = .position + + /// [Output Only] + var status: Android_Emulation_Control_PhysicalModelValue.State = .ok + + /// Value interpretation depends on sensor. + var value: Android_Emulation_Control_ParameterValue { + get {_value ?? Android_Emulation_Control_ParameterValue()} + set {_value = newValue} + } + /// Returns true if `value` has been explicitly set. + var hasValue: Bool {self._value != nil} + /// Clears the value of `value`. Subsequent reads from it will return its default value. + mutating func clearValue() {self._value = nil} + + /// [Input Only] How to transition to the target value. + var interpolation: Android_Emulation_Control_PhysicalModelValue.Interpolation = .smooth + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum State: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case ok // = 0 + + /// qemud service is not available/initiated. + case noService // = -3 + + /// Sensor is disabled. + case disabled // = -2 + + /// Unknown sensor (should not happen) + case unknown // = -1 + case UNRECOGNIZED(Int) + + init() { + self = .ok + } + + init?(rawValue: Int) { + switch rawValue { + case -3: self = .noService + case -2: self = .disabled + case -1: self = .unknown + case 0: self = .ok + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .noService: return -3 + case .disabled: return -2 + case .unknown: return -1 + case .ok: return 0 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_PhysicalModelValue.State] = [ + .ok, + .noService, + .disabled, + .unknown, + ] + + } + + /// Details on the sensors documentation can be found here: + /// https://developer.android.com/reference/android/hardware/Sensor.html#TYPE_ + /// The types must follow the order defined in + /// "external/qemu/android/hw-sensors.h" + nonisolated enum PhysicalType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case position // = 0 + + /// All values are angles in degrees. + /// values = [x,y,z] + case rotation // = 1 + case magneticField // = 2 + + /// Temperature in °C + case temperature // = 3 + + /// Proximity sensor distance measured in centimeters + case proximity // = 4 + + /// Ambient light level in SI lux units + case light // = 5 + + /// Atmospheric pressure in hPa (millibar) + case pressure // = 6 + + /// Relative ambient air humidity in percent + case humidity // = 7 + case velocity // = 8 + case ambientMotion // = 9 + + /// Describing a hinge angle sensor in degrees. + case hingeAngle0 // = 10 + case hingeAngle1 // = 11 + case hingeAngle2 // = 12 + case rollable0 // = 13 + case rollable1 // = 14 + case rollable2 // = 15 + + /// Describing the device posture; the value should be an enum defined + /// in Posture::PostureValue. + case posture // = 16 + + /// Heart rate in bpm + case heartRate // = 17 + + /// Ambient RGBC light intensity. Values are in order (Red, Green, Blue, + /// Clear). + case rgbcLight // = 18 + + /// Wrist tilt gesture (1 = gaze, 0 = ungaze) + case wristTilt // = 19 + case UNRECOGNIZED(Int) + + init() { + self = .position + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .position + case 1: self = .rotation + case 2: self = .magneticField + case 3: self = .temperature + case 4: self = .proximity + case 5: self = .light + case 6: self = .pressure + case 7: self = .humidity + case 8: self = .velocity + case 9: self = .ambientMotion + case 10: self = .hingeAngle0 + case 11: self = .hingeAngle1 + case 12: self = .hingeAngle2 + case 13: self = .rollable0 + case 14: self = .rollable1 + case 15: self = .rollable2 + case 16: self = .posture + case 17: self = .heartRate + case 18: self = .rgbcLight + case 19: self = .wristTilt + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .position: return 0 + case .rotation: return 1 + case .magneticField: return 2 + case .temperature: return 3 + case .proximity: return 4 + case .light: return 5 + case .pressure: return 6 + case .humidity: return 7 + case .velocity: return 8 + case .ambientMotion: return 9 + case .hingeAngle0: return 10 + case .hingeAngle1: return 11 + case .hingeAngle2: return 12 + case .rollable0: return 13 + case .rollable1: return 14 + case .rollable2: return 15 + case .posture: return 16 + case .heartRate: return 17 + case .rgbcLight: return 18 + case .wristTilt: return 19 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_PhysicalModelValue.PhysicalType] = [ + .position, + .rotation, + .magneticField, + .temperature, + .proximity, + .light, + .pressure, + .humidity, + .velocity, + .ambientMotion, + .hingeAngle0, + .hingeAngle1, + .hingeAngle2, + .rollable0, + .rollable1, + .rollable2, + .posture, + .heartRate, + .rgbcLight, + .wristTilt, + ] + + } + + nonisolated enum Interpolation: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case smooth // = 0 + case step // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .smooth + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .smooth + case 1: self = .step + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .smooth: return 0 + case .step: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_PhysicalModelValue.Interpolation] = [ + .smooth, + .step, + ] + + } + + init() {} + + fileprivate var _value: Android_Emulation_Control_ParameterValue? = nil +} + +/// A single sensor value. +nonisolated struct Android_Emulation_Control_SensorValue: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Type of sensor + var target: Android_Emulation_Control_SensorValue.SensorType = .acceleration + + /// [Output Only] + var status: Android_Emulation_Control_SensorValue.State = .ok + + /// Value interpretation depends on sensor enum. + var value: Android_Emulation_Control_ParameterValue { + get {_value ?? Android_Emulation_Control_ParameterValue()} + set {_value = newValue} + } + /// Returns true if `value` has been explicitly set. + var hasValue: Bool {self._value != nil} + /// Clears the value of `value`. Subsequent reads from it will return its default value. + mutating func clearValue() {self._value = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum State: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case ok // = 0 + + /// qemud service is not available/initiated. + case noService // = -3 + + /// Sensor is disabled. + case disabled // = -2 + + /// Unknown sensor (should not happen) + case unknown // = -1 + case UNRECOGNIZED(Int) + + init() { + self = .ok + } + + init?(rawValue: Int) { + switch rawValue { + case -3: self = .noService + case -2: self = .disabled + case -1: self = .unknown + case 0: self = .ok + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .noService: return -3 + case .disabled: return -2 + case .unknown: return -1 + case .ok: return 0 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_SensorValue.State] = [ + .ok, + .noService, + .disabled, + .unknown, + ] + + } + + /// These are the various sensors that can be available in an emulated + /// devices. + nonisolated enum SensorType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Measures the acceleration force in m/s2 that is applied to a device + /// on all three physical axes (x, y, and z), including the force of + /// gravity. + case acceleration // = 0 + + /// Measures a device's rate of rotation in rad/s around each of the + /// three physical axes (x, y, and z). + case gyroscope // = 1 + + /// Measures the ambient geomagnetic field for all three physical axes + /// (x, y, z) in μT. + case magneticField // = 2 + + /// Measures degrees of rotation that a device makes around all three + /// physical axes (x, y, z) + case orientation // = 3 + + /// Measures the temperature of the device in degrees Celsius (°C). + case temperature // = 4 + + /// Measures the proximity of an object in cm relative to the view screen + /// of a device. This sensor is typically used to determine whether a + /// handset is being held up to a person's ear. + case proximity // = 5 + + /// Measures the ambient light level (illumination) in lx. + case light // = 6 + + /// Measures the ambient air pressure in hPa or mbar. + case pressure // = 7 + + /// Measures the relative ambient humidity in percent (%). + case humidity // = 8 + case magneticFieldUncalibrated // = 9 + case gyroscopeUncalibrated // = 10 + + /// Measures the heart rate in bpm. + case heartRate // = 14 + + /// Measures the ambient RGBC light intensity. + /// Values are in order (Red, Green, Blue, Clear). + case rgbcLight // = 15 + + /// WIRST_TILT (16) is skipped; clients should use get/setPhysicalModel() + /// instead. + /// Measures acceleration force and provides bias data. + case accelerationUncalibrated // = 17 + + /// A sensor of this type measures the direction in which the device is + /// pointing relative to true north in degrees. + case heading // = 18 + case UNRECOGNIZED(Int) + + init() { + self = .acceleration + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .acceleration + case 1: self = .gyroscope + case 2: self = .magneticField + case 3: self = .orientation + case 4: self = .temperature + case 5: self = .proximity + case 6: self = .light + case 7: self = .pressure + case 8: self = .humidity + case 9: self = .magneticFieldUncalibrated + case 10: self = .gyroscopeUncalibrated + case 14: self = .heartRate + case 15: self = .rgbcLight + case 17: self = .accelerationUncalibrated + case 18: self = .heading + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .acceleration: return 0 + case .gyroscope: return 1 + case .magneticField: return 2 + case .orientation: return 3 + case .temperature: return 4 + case .proximity: return 5 + case .light: return 6 + case .pressure: return 7 + case .humidity: return 8 + case .magneticFieldUncalibrated: return 9 + case .gyroscopeUncalibrated: return 10 + case .heartRate: return 14 + case .rgbcLight: return 15 + case .accelerationUncalibrated: return 17 + case .heading: return 18 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_SensorValue.SensorType] = [ + .acceleration, + .gyroscope, + .magneticField, + .orientation, + .temperature, + .proximity, + .light, + .pressure, + .humidity, + .magneticFieldUncalibrated, + .gyroscopeUncalibrated, + .heartRate, + .rgbcLight, + .accelerationUncalibrated, + .heading, + ] + + } + + init() {} + + fileprivate var _value: Android_Emulation_Control_ParameterValue? = nil +} + +/// A single backlight brightness value. +nonisolated struct Android_Emulation_Control_BrightnessValue: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Type of light + var target: Android_Emulation_Control_BrightnessValue.LightType = .lcd + + /// Light intensity, ranges from 0-255. + var value: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum LightType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Display backlight. This will affect all displays. + case lcd // = 0 + case keyboard // = 1 + case button // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .lcd + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .lcd + case 1: self = .keyboard + case 2: self = .button + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .lcd: return 0 + case .keyboard: return 1 + case .button: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_BrightnessValue.LightType] = [ + .lcd, + .keyboard, + .button, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_DisplayMode: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var value: Android_Emulation_Control_DisplayModeValue = .phone + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_XrOptions: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The currently active artificial surrounding environment (a.k.a. + /// passthrough environment). + var environment: Android_Emulation_Control_XrOptions.Environment = .livingRoomDay + + /// A value of 0.0 means that the real or artificial surrounding environment + /// (a.k.a. passthrough environment) is not visible. A value of 1.0 means + /// that the passthrough environment is fully visible. Any value outside of + /// the range [0.0-1.0] is ignored and leaves the state of passthrough + /// unchanged. For simplicity of the implementation, this number may be + /// rounded to an integer before applying. + var passthroughCoefficient: Float = 0 + + var dimmingValue: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum Environment: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case livingRoomDay // = 0 + + /// More environments may be added later. + case livingRoomNight // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .livingRoomDay + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .livingRoomDay + case 1: self = .livingRoomNight + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .livingRoomDay: return 0 + case .livingRoomNight: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_XrOptions.Environment] = [ + .livingRoomDay, + .livingRoomNight, + ] + + } + + init() {} +} + +/// The state of one LED indicator of AI glasses. +nonisolated struct Android_Emulation_Control_LedIndicator: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The indicator's identifier. + var id: UInt32 { + get {_id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// Determines whether the indicator is inside or outside of the glasses frames. + var facing: Android_Emulation_Control_LedIndicator.Facing { + get {_facing ?? .inside} + set {_facing = newValue} + } + /// Returns true if `facing` has been explicitly set. + var hasFacing: Bool {self._facing != nil} + /// Clears the value of `facing`. Subsequent reads from it will return its default value. + mutating func clearFacing() {self._facing = nil} + + /// Determines whether the indicator is on or off. + var state: Android_Emulation_Control_LedIndicator.State { + get {_state ?? .off} + set {_state = newValue} + } + /// Returns true if `state` has been explicitly set. + var hasState: Bool {self._state != nil} + /// Clears the value of `state`. Subsequent reads from it will return its default value. + mutating func clearState() {self._state = nil} + + /// Color in RGB format. + var color: UInt32 { + get {_color ?? 0} + set {_color = newValue} + } + /// Returns true if `color` has been explicitly set. + var hasColor: Bool {self._color != nil} + /// Clears the value of `color`. Subsequent reads from it will return its default value. + mutating func clearColor() {self._color = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum Facing: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case inside // = 0 + case outside // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .inside + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .inside + case 1: self = .outside + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .inside: return 0 + case .outside: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_LedIndicator.Facing] = [ + .inside, + .outside, + ] + + } + + nonisolated enum State: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case off // = 0 + case on // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .off + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .off + case 1: self = .on + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .off: return 0 + case .on: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_LedIndicator.State] = [ + .off, + .on, + ] + + } + + init() {} + + fileprivate var _id: UInt32? = nil + fileprivate var _facing: Android_Emulation_Control_LedIndicator.Facing? = nil + fileprivate var _state: Android_Emulation_Control_LedIndicator.State? = nil + fileprivate var _color: UInt32? = nil +} + +nonisolated struct Android_Emulation_Control_LogMessage: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// [Output Only] The contents of the log output. + var contents: String = String() + + /// The starting byte position of the output that was returned. This + /// should match the start parameter sent with the request. If the serial + /// console output exceeds the size of the buffer, older output will be + /// overwritten by newer content and the start values will be mismatched. + /// + /// NOTE: This field was marked as deprecated in the .proto file. + var start: Int64 = 0 + + ///[Output Only] The position of the next byte of content from the serial + /// console output. Use this value in the next request as the start + /// parameter. + /// + /// NOTE: This field was marked as deprecated in the .proto file. + var next: Int64 = 0 + + /// Set the sort of response you are interested it in. + /// It the type is "Parsed" the entries field will contain the parsed + /// results. otherwise the contents field will be set. + var sort: Android_Emulation_Control_LogMessage.LogType = .text + + /// [Output Only] The parsed logcat entries so far. Only set if sort is + /// set to Parsed + var entries: [Android_Emulation_Control_LogcatEntry] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum LogType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case text // = 0 + case parsed // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .text + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .text + case 1: self = .parsed + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .text: return 0 + case .parsed: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_LogMessage.LogType] = [ + .text, + .parsed, + ] + + } + + init() {} +} + +/// A parsed logcat entry. +nonisolated struct Android_Emulation_Control_LogcatEntry: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// A Unix timestamps in milliseconds (The number of milliseconds that + /// have elapsed since January 1, 1970 (midnight UTC/GMT), not counting + /// leap seconds) + var timestamp: UInt64 = 0 + + /// Process id. + var pid: UInt32 = 0 + + /// Thread id. + var tid: UInt32 = 0 + + var level: Android_Emulation_Control_LogcatEntry.LogLevel = .unknown + + var tag: String = String() + + var msg: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The possible log levels. + nonisolated enum LogLevel: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case unknown // = 0 + case `default` // = 1 + case verbose // = 2 + case debug // = 3 + case info // = 4 + case warn // = 5 + case err // = 6 + case fatal // = 7 + case silent // = 8 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .default + case 2: self = .verbose + case 3: self = .debug + case 4: self = .info + case 5: self = .warn + case 6: self = .err + case 7: self = .fatal + case 8: self = .silent + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .default: return 1 + case .verbose: return 2 + case .debug: return 3 + case .info: return 4 + case .warn: return 5 + case .err: return 6 + case .fatal: return 7 + case .silent: return 8 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_LogcatEntry.LogLevel] = [ + .unknown, + .default, + .verbose, + .debug, + .info, + .warn, + .err, + .fatal, + .silent, + ] + + } + + init() {} +} + +/// Information about the hypervisor that is currently in use. +nonisolated struct Android_Emulation_Control_VmConfiguration: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var hypervisorType: Android_Emulation_Control_VmConfiguration.VmHypervisorType = .unknown + + var numberOfCpuCores: Int32 = 0 + + var ramSizeBytes: Int64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum VmHypervisorType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// An unknown hypervisor + case unknown // = 0 + + /// No hypervisor is in use. This usually means that the guest is + /// running on a different CPU than the host, or you are using a + /// platform where no hypervisor is available. + case none // = 1 + + /// The Kernel based Virtual Machine + /// (https://www.linux-kvm.org/page/Main_Page) + case kvm // = 2 + + /// Intel® Hardware Accelerated Execution Manager (Intel® HAXM) + /// + /// NOTE: This enum value was marked as deprecated in the .proto file + case haxm // = 3 + + /// Hypervisor Framework. + /// https://developer.apple.com/documentation/hypervisor + case hvf // = 4 + + /// Window Hypervisor Platform + /// https://docs.microsoft.com/en-us/virtualization/api/ + case whpx // = 5 + case aehd // = 6 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .none + case 2: self = .kvm + case 3: self = .haxm + case 4: self = .hvf + case 5: self = .whpx + case 6: self = .aehd + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .none: return 1 + case .kvm: return 2 + case .haxm: return 3 + case .hvf: return 4 + case .whpx: return 5 + case .aehd: return 6 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_VmConfiguration.VmHypervisorType] = [ + .unknown, + .none, + .kvm, + .haxm, + .hvf, + .whpx, + .aehd, + ] + + } + + init() {} +} + +/// Representation of a clipped data object on the clipboard. +nonisolated struct Android_Emulation_Control_ClipData: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// UTF-8 Encoded text. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The Touch interface represents a single contact point on a +/// touch-sensitive device. The contact point is commonly a finger or stylus +/// and the device may be a touchscreen or trackpad. +nonisolated struct Android_Emulation_Control_Touch: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The horizontal coordinate. This is the physical location on the + /// screen For example 0 indicates the leftmost coordinate. + var x: Int32 = 0 + + /// The vertical coordinate. This is the physical location on the screen + /// For example 0 indicates the top left coordinate. + var y: Int32 = 0 + + /// The identifier is an arbitrary non-negative integer that is used to + /// identify and track each tool independently when multiple tools are + /// active. For example, when multiple fingers are touching the device, + /// each finger should be assigned a distinct tracking id that is used as + /// long as the finger remains in contact. Tracking ids may be reused + /// when their associated tools move out of range. + /// + /// The emulator currently supports up to 10 concurrent touch events. The + /// identifier can be any uninque value and will be mapped to the next + /// available internal identifier. + var identifier: Int32 = 0 + + /// Reports the physical pressure applied to the tip of the tool or the + /// signal strength of the touch contact. + /// + /// The values reported must be non-zero when the tool is touching the + /// device and zero otherwise to indicate that the touch event is + /// completed. + /// + /// Make sure to deliver a pressure of 0 for the given identifier when + /// the touch event is completed, otherwise the touch identifier will not + /// be unregistered! + var pressure: Int32 = 0 + + /// Optionally reports the cross-sectional area of the touch contact, or + /// the length of the longer dimension of the touch contact. + var touchMajor: Int32 = 0 + + /// Optionally reports the length of the shorter dimension of the touch + /// contact. This axis will be ignored if touch_major is reporting an + /// area measurement greater than 0. + var touchMinor: Int32 = 0 + + var expiration: Android_Emulation_Control_Touch.EventExpiration = .unspecified + + /// The orientation of the contact, if any. + var orientation: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum EventExpiration: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// The system will use the default time of 120s to track + /// the touch event with the given identifier. If no update happens + /// within this timeframe the identifier is considered expired + /// and can be made available for re-use. This means that a touch event + /// with pressure 0 for this identifier will be send to the emulator. + case unspecified // = 0 + + /// Never expire the given slot. You must *ALWAYS* close the identifier + /// by sending a touch event with 0 pressure. + case neverExpire // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .unspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .neverExpire + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unspecified: return 0 + case .neverExpire: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_Touch.EventExpiration] = [ + .unspecified, + .neverExpire, + ] + + } + + init() {} +} + +/// A Pen is similar to a touch, with the addition +/// of button and rubber information. +nonisolated struct Android_Emulation_Control_Pen: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var location: Android_Emulation_Control_Touch { + get {_location ?? Android_Emulation_Control_Touch()} + set {_location = newValue} + } + /// Returns true if `location` has been explicitly set. + var hasLocation: Bool {self._location != nil} + /// Clears the value of `location`. Subsequent reads from it will return its default value. + mutating func clearLocation() {self._location = nil} + + /// True if the button is pressed or not + var buttonPressed: Bool = false + + /// True if it is a rubber pointer. + var rubberPointer: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _location: Android_Emulation_Control_Touch? = nil +} + +/// A TouchEvent contains a list of Touch objects that are in contact with +/// the touch surface. +/// +/// Touch events are delivered in sequence as specified in the touchList. +/// +/// TouchEvents are delivered to the emulated devices using ["Protocol +/// B"](https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt) +nonisolated struct Android_Emulation_Control_TouchEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The list of Touch objects, note that these do not need to be unique + var touches: [Android_Emulation_Control_Touch] = [] + + /// The display device where the touch event occurred. + /// Omitting or using the value 0 indicates the main display. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A TouchpadEvent contains a list of Touch objects that are in contact with +/// the touchpad surface. +/// +/// Touchpad events are delivered in sequence as specified in the touchList. +/// +/// TouchpadEvents are delivered to the emulated devices using ["Protocol +/// B"](https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt) +nonisolated struct Android_Emulation_Control_TouchpadEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The list of Touch objects, note that these do not need to be unique + var touches: [Android_Emulation_Control_Touch] = [] + + /// The touchpad device where the touch event occurred. + /// Omitting or using the value 0 indicates the main touchpad. + var touchpad: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_PenEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The list of Pen objects, note that these do not need to be unique + var events: [Android_Emulation_Control_Pen] = [] + + /// The display device where the pen event occurred. + /// Omitting or using the value 0 indicates the main display. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The MouseEvent interface represents events that occur due to the user +/// interacting with a pointing device (such as a mouse). +nonisolated struct Android_Emulation_Control_MouseEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The horizontal coordinate. This is the physical location on the + /// screen, where 0 indicates the leftmost coordinate. + var x: Int32 = 0 + + /// The vertical coordinate. This is the physical location on the screen, + /// where 0 indicates the topmost coordinate. + var y: Int32 = 0 + + /// Indicates which buttons are pressed. + /// 0: No button was pressed + /// 1: Primary button (left) + /// 2: Secondary button (right) + var buttons: Int32 = 0 + + /// The display device where the mouse event occurred. + /// Omitting or using the value 0 indicates the main display. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_WheelEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The value indicating how much the mouse wheel is rotated. Scaled so that + /// 120 equals to 1 wheel click. (120 is chosen as a multiplier often used to + /// represent wheel movements less than 1 wheel click. e.g. + /// https://doc.qt.io/qt-5/qwheelevent.html#angleDelta) Positive delta value + /// is assigned to dx when the top of wheel is moved to left. Similarly + /// positive delta value is assigned to dy when the top of wheel is moved + /// away from the user. + var dx: Int32 = 0 + + var dy: Int32 = 0 + + /// The display device where the mouse event occurred. + /// Omitting or using the value 0 indicates the main display. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// KeyboardEvent objects describe a user interaction with the keyboard; each +/// event describes a single interaction between the user and a key (or +/// combination of a key with modifier keys) on the keyboard. +/// This follows the pattern as set by +/// (javascript)[https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent] +/// +/// Note: that only keyCode, key, or text can be set and that the semantics +/// will slightly vary. +nonisolated struct Android_Emulation_Control_KeyboardEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Type of keycode contained in the keyCode field. + var codeType: Android_Emulation_Control_KeyboardEvent.KeyCodeType = .usb + + /// The type of keyboard event that should be sent to the emulator + var eventType: Android_Emulation_Control_KeyboardEvent.KeyEventType = .keydown + + /// This property represents a physical key on the keyboard (as opposed + /// to the character generated by pressing the key). In other words, this + /// property is a value which isn't altered by keyboard layout or the + /// state of the modifier keys. This value will be interpreted by the + /// emulator depending on the KeyCodeType. The incoming key code will be + /// translated to an evdev code type and send to the emulator. + /// The values in key and text will be ignored. + var keyCode: Int32 = 0 + + /// The value of the key pressed by the user, taking into consideration + /// the state of modifier keys such as Shift as well as the keyboard + /// locale and layout. This follows the w3c standard used in browsers. + /// You can find an accurate description of valid values + /// [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) + /// + /// Note that some keys can result in multiple evdev events that are + /// delivered to the emulator. for example the Key "A" will result in a + /// sequence: + /// ["Shift", "a"] -> [0x2a, 0x1e] whereas "a" results in ["a"] -> [0x1e]. + /// + /// Not all documented keys are understood by android, and only printable + /// ASCII [32-127) characters are properly translated. + /// + /// Keep in mind that there are a set of key values that result in android + /// specific behavior + /// [see](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#Phone_keys): + /// + /// - "AppSwitch": Behaves as the "Overview" button in android. + /// - "GoBack": The Back button. + /// - "GoHome": The Home button, which takes the user to the phone's main + /// screen (usually an application launcher). + /// - "Power": The Power button. + var key: String = String() + + /// Series of utf8 encoded characters to send to the emulator. An attempt + /// will be made to translate every character will an EvDev event type and + /// send to the emulator as a keypress event. The values in keyCode, + /// eventType, codeType and key will be ignored. + /// + /// Note that most printable ASCII characters (range [32-127) can be send + /// individually with the "key" param. Do not expect arbitrary UTF symbols to + /// arrive in the emulator (most will be ignored). + /// + /// Note that it is possible to overrun the keyboard buffer by slamming this + /// endpoint with large quantities of text (>1kb). The clipboard api is + /// better suited for transferring large quantities of text. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Code types that the emulator can receive. Note that the emulator + /// will do its best to translate the code to an evdev value that + /// will be send to the emulator. This translation is based on + /// the chromium translation tables. See + /// (this)[https://android.googlesource.com/platform/external/qemu/+/refs/heads/emu-master-dev/android/android-grpc/android/emulation/control/keyboard/keycode_converter_data.inc] + /// for details on the translation. + nonisolated enum KeyCodeType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case usb // = 0 + case evdev // = 1 + case xkb // = 2 + case win // = 3 + case mac // = 4 + case UNRECOGNIZED(Int) + + init() { + self = .usb + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .usb + case 1: self = .evdev + case 2: self = .xkb + case 3: self = .win + case 4: self = .mac + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .usb: return 0 + case .evdev: return 1 + case .xkb: return 2 + case .win: return 3 + case .mac: return 4 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_KeyboardEvent.KeyCodeType] = [ + .usb, + .evdev, + .xkb, + .win, + .mac, + ] + + } + + nonisolated enum KeyEventType: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Indicates that this keyevent should be send to the emulator + /// as a key down event. Meaning that the key event will be + /// translated to an EvDev event type and bit 11 (0x400) will be + /// set before it is sent to the emulator. + case keydown // = 0 + + /// Indicates that the keyevent should be send to the emulator + /// as a key up event. Meaning that the key event will be + /// translated to an EvDev event type and + /// sent to the emulator. + case keyup // = 1 + + /// Indicates that the keyevent will be send to the emulator + /// as e key down event and immediately followed by a keyup event. + case keypress // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .keydown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .keydown + case 1: self = .keyup + case 2: self = .keypress + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .keydown: return 0 + case .keyup: return 1 + case .keypress: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_KeyboardEvent.KeyEventType] = [ + .keydown, + .keyup, + .keypress, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_XrCommand: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var action: Android_Emulation_Control_XrCommand.Action = .recenter + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum Action: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Recenter the viewport position and rotation. + case recenter // = 0 + case UNRECOGNIZED(Int) + + init() { + self = .recenter + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .recenter + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .recenter: return 0 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_XrCommand.Action] = [ + .recenter, + ] + + } + + init() {} +} + +/// An input event that can be delivered to the emulator. +nonisolated struct Android_Emulation_Control_InputEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var type: Android_Emulation_Control_InputEvent.OneOf_Type? = nil + + var keyEvent: Android_Emulation_Control_KeyboardEvent { + get { + if case .keyEvent(let v)? = type {return v} + return Android_Emulation_Control_KeyboardEvent() + } + set {type = .keyEvent(newValue)} + } + + var touchEvent: Android_Emulation_Control_TouchEvent { + get { + if case .touchEvent(let v)? = type {return v} + return Android_Emulation_Control_TouchEvent() + } + set {type = .touchEvent(newValue)} + } + + var mouseEvent: Android_Emulation_Control_MouseEvent { + get { + if case .mouseEvent(let v)? = type {return v} + return Android_Emulation_Control_MouseEvent() + } + set {type = .mouseEvent(newValue)} + } + + var androidEvent: Android_Emulation_Control_AndroidEvent { + get { + if case .androidEvent(let v)? = type {return v} + return Android_Emulation_Control_AndroidEvent() + } + set {type = .androidEvent(newValue)} + } + + var penEvent: Android_Emulation_Control_PenEvent { + get { + if case .penEvent(let v)? = type {return v} + return Android_Emulation_Control_PenEvent() + } + set {type = .penEvent(newValue)} + } + + var wheelEvent: Android_Emulation_Control_WheelEvent { + get { + if case .wheelEvent(let v)? = type {return v} + return Android_Emulation_Control_WheelEvent() + } + set {type = .wheelEvent(newValue)} + } + + var xrHandEvent: Android_Emulation_Control_MouseEvent { + get { + if case .xrHandEvent(let v)? = type {return v} + return Android_Emulation_Control_MouseEvent() + } + set {type = .xrHandEvent(newValue)} + } + + var xrEyeEvent: Android_Emulation_Control_MouseEvent { + get { + if case .xrEyeEvent(let v)? = type {return v} + return Android_Emulation_Control_MouseEvent() + } + set {type = .xrEyeEvent(newValue)} + } + + var xrCommand: Android_Emulation_Control_XrCommand { + get { + if case .xrCommand(let v)? = type {return v} + return Android_Emulation_Control_XrCommand() + } + set {type = .xrCommand(newValue)} + } + + var xrHeadRotationEvent: Android_Emulation_Control_RotationRadian { + get { + if case .xrHeadRotationEvent(let v)? = type {return v} + return Android_Emulation_Control_RotationRadian() + } + set {type = .xrHeadRotationEvent(newValue)} + } + + var xrHeadMovementEvent: Android_Emulation_Control_Translation { + get { + if case .xrHeadMovementEvent(let v)? = type {return v} + return Android_Emulation_Control_Translation() + } + set {type = .xrHeadMovementEvent(newValue)} + } + + var xrHeadAngularVelocityEvent: Android_Emulation_Control_AngularVelocity { + get { + if case .xrHeadAngularVelocityEvent(let v)? = type {return v} + return Android_Emulation_Control_AngularVelocity() + } + set {type = .xrHeadAngularVelocityEvent(newValue)} + } + + var xrHeadVelocityEvent: Android_Emulation_Control_Velocity { + get { + if case .xrHeadVelocityEvent(let v)? = type {return v} + return Android_Emulation_Control_Velocity() + } + set {type = .xrHeadVelocityEvent(newValue)} + } + + var touchpadEvent: Android_Emulation_Control_TouchpadEvent { + get { + if case .touchpadEvent(let v)? = type {return v} + return Android_Emulation_Control_TouchpadEvent() + } + set {type = .touchpadEvent(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum OneOf_Type: Equatable, Sendable { + case keyEvent(Android_Emulation_Control_KeyboardEvent) + case touchEvent(Android_Emulation_Control_TouchEvent) + case mouseEvent(Android_Emulation_Control_MouseEvent) + case androidEvent(Android_Emulation_Control_AndroidEvent) + case penEvent(Android_Emulation_Control_PenEvent) + case wheelEvent(Android_Emulation_Control_WheelEvent) + case xrHandEvent(Android_Emulation_Control_MouseEvent) + case xrEyeEvent(Android_Emulation_Control_MouseEvent) + case xrCommand(Android_Emulation_Control_XrCommand) + case xrHeadRotationEvent(Android_Emulation_Control_RotationRadian) + case xrHeadMovementEvent(Android_Emulation_Control_Translation) + case xrHeadAngularVelocityEvent(Android_Emulation_Control_AngularVelocity) + case xrHeadVelocityEvent(Android_Emulation_Control_Velocity) + case touchpadEvent(Android_Emulation_Control_TouchpadEvent) + + } + + init() {} +} + +/// The android input event system is a framework for handling input from a +/// variety of devices by generating events that describe changes in the +/// state of the devices and forwarding them to user space applications. +/// +/// An AndroidEvents will be delivered directly to the kernel as is. +nonisolated struct Android_Emulation_Control_AndroidEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The type of the event. The types of the event are specified + /// by the android kernel. Some examples are: + /// EV_SYN, EV_KEY, EV_SW, etc.. + /// The exact definitions can be found in the input.h header file. + var type: Int32 = 0 + + /// The actual code to be send to the kernel. The actual meaning + /// of the code depends on the type definition. + var code: Int32 = 0 + + /// The actual value of the event. + var value: Int32 = 0 + + /// The display id associated with this input event. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_Fingerprint: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// True when the fingprint is touched. + var isTouching: Bool = false + + /// The identifier of the registered fingerprint. + var touchID: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_GpsState: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Setting this to false will disable auto updating from the LocationUI, + /// otherwise the location UI will override the location at a frequency of + /// 1hz. + /// + /// - This is unused if the emulator is launched with -no-window, or when he + /// location ui is disabled. + /// - This will BREAK the location ui experience if it is set to false. For + /// example routing will no longer function. + var passiveUpdate: Bool = false + + /// The latitude, in degrees. + var latitude: Double = 0 + + /// The longitude, in degrees. + var longitude: Double = 0 + + /// The speed if it is available, in meters/second over ground + var speed: Double = 0 + + /// gets the horizontal direction of travel of this device, and is not + /// related to the device orientation. It is guaranteed to be in the + /// range [0.0, 360.0] if the device has a bearing. 0=North, 90=East, + /// 180=South, etc.. + var bearing: Double = 0 + + /// The altitude if available, in meters above the WGS 84 reference + /// ellipsoid. + var altitude: Double = 0 + + /// The number of satellites used to derive the fix + var satellites: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_BatteryState: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var hasBattery_p: Bool = false + + var isPresent: Bool = false + + var charger: Android_Emulation_Control_BatteryState.BatteryCharger = .none + + var chargeLevel: Int32 = 0 + + var health: Android_Emulation_Control_BatteryState.BatteryHealth = .good + + var status: Android_Emulation_Control_BatteryState.BatteryStatus = .unknown + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum BatteryStatus: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case unknown // = 0 + case charging // = 1 + case discharging // = 2 + case notCharging // = 3 + case full // = 4 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .charging + case 2: self = .discharging + case 3: self = .notCharging + case 4: self = .full + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .charging: return 1 + case .discharging: return 2 + case .notCharging: return 3 + case .full: return 4 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_BatteryState.BatteryStatus] = [ + .unknown, + .charging, + .discharging, + .notCharging, + .full, + ] + + } + + nonisolated enum BatteryCharger: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case none // = 0 + case ac // = 1 + case usb // = 2 + case wireless // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .none + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .none + case 1: self = .ac + case 2: self = .usb + case 3: self = .wireless + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .none: return 0 + case .ac: return 1 + case .usb: return 2 + case .wireless: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_BatteryState.BatteryCharger] = [ + .none, + .ac, + .usb, + .wireless, + ] + + } + + nonisolated enum BatteryHealth: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case good // = 0 + case failed // = 1 + case dead // = 2 + case overvoltage // = 3 + case overheated // = 4 + case UNRECOGNIZED(Int) + + init() { + self = .good + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .good + case 1: self = .failed + case 2: self = .dead + case 3: self = .overvoltage + case 4: self = .overheated + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .good: return 0 + case .failed: return 1 + case .dead: return 2 + case .overvoltage: return 3 + case .overheated: return 4 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_BatteryState.BatteryHealth] = [ + .good, + .failed, + .dead, + .overvoltage, + .overheated, + ] + + } + + init() {} +} + +/// An ImageTransport allows for specifying a side channel for +/// delivering image frames versus using the standard bytes array that is +/// returned with the gRPC request. +nonisolated struct Android_Emulation_Control_ImageTransport: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The desired transport channel used for delivering image frames. Only + /// relevant when streaming screenshots. + var channel: Android_Emulation_Control_ImageTransport.TransportChannel = .unspecified + + /// Handle used for writing image frames if transport is mmap. The client + /// sets and owns this handle. It can be either a shm region, or a mmap. A + /// mmap should be a url that starts with `file:///` Note: the mmap can + /// result in tearing. + var handle: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum TransportChannel: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Return full frames over the gRPC transport + case unspecified // = 0 + + /// Write images to the a file/shared memory handle. + case mmap // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .unspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .mmap + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unspecified: return 0 + case .mmap: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_ImageTransport.TransportChannel] = [ + .unspecified, + .mmap, + ] + + } + + init() {} +} + +/// The aspect ratio (width/height) will be different from the one +/// where the device is unfolded. +nonisolated struct Android_Emulation_Control_FoldedDisplay: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var width: UInt32 = 0 + + var height: UInt32 = 0 + + /// It is possible for the screen to be folded in different ways depending + /// on which surface is shown to the user. So xOffset and yOffset indicate + /// the top left corner of the folded screen within the original unfolded + /// screen. + var xOffset: UInt32 = 0 + + var yOffset: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_ImageFormat: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The (desired) format of the resulting bytes. + var format: Android_Emulation_Control_ImageFormat.ImgFormat = .png + + /// [Output Only] The rotation of the image. The image will be rotated + /// based upon the coarse grained orientation of the device. + var rotation: Android_Emulation_Control_Rotation { + get {_rotation ?? Android_Emulation_Control_Rotation()} + set {_rotation = newValue} + } + /// Returns true if `rotation` has been explicitly set. + var hasRotation: Bool {self._rotation != nil} + /// Clears the value of `rotation`. Subsequent reads from it will return its default value. + mutating func clearRotation() {self._rotation = nil} + + /// The (desired) width of the image. When passed as input + /// the image will be scaled to match the given + /// width, while maintaining the aspect ratio of the device. + /// The returned image will never exceed the given width, but can be less. + /// Omitting this value (or passing in 0) will result in no scaling, + /// and the width of the actual device will be used. + var width: UInt32 = 0 + + /// The (desired) height of the image. When passed as input + /// the image will be scaled to match the given + /// height, while maintaining the aspect ratio of the device. + /// The returned image will never exceed the given height, but can be less. + /// Omitting this value (or passing in 0) will result in no scaling, + /// and the height of the actual device will be used. + var height: UInt32 = 0 + + /// The (desired) display id of the device. Setting this to 0 (or omitting) + /// indicates the main display. + var display: UInt32 = 0 + + /// Set this if you wish to use a different transport channel to deliver + /// image frames. + var transport: Android_Emulation_Control_ImageTransport { + get {_transport ?? Android_Emulation_Control_ImageTransport()} + set {_transport = newValue} + } + /// Returns true if `transport` has been explicitly set. + var hasTransport: Bool {self._transport != nil} + /// Clears the value of `transport`. Subsequent reads from it will return its default value. + mutating func clearTransport() {self._transport = nil} + + /// [Output Only] Display configuration when screen is folded. The value is + /// the original configuration before scaling. + var foldedDisplay: Android_Emulation_Control_FoldedDisplay { + get {_foldedDisplay ?? Android_Emulation_Control_FoldedDisplay()} + set {_foldedDisplay = newValue} + } + /// Returns true if `foldedDisplay` has been explicitly set. + var hasFoldedDisplay: Bool {self._foldedDisplay != nil} + /// Clears the value of `foldedDisplay`. Subsequent reads from it will return its default value. + mutating func clearFoldedDisplay() {self._foldedDisplay = nil} + + /// [Output Only] Display mode when AVD is resizable. + var displayMode: Android_Emulation_Control_DisplayModeValue = .phone + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum ImgFormat: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Portable Network Graphics format + /// (https://en.wikipedia.org/wiki/Portable_Network_Graphics) + case png // = 0 + + /// Three-channel RGB color model supplemented with a fourth alpha + /// channel. https://en.wikipedia.org/wiki/RGBA_color_model + /// Each pixel consists of 4 bytes. + case rgba8888 // = 1 + + /// Three-channel RGB color model, each pixel consists of 3 bytes + case rgb888 // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .png + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .png + case 1: self = .rgba8888 + case 2: self = .rgb888 + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .png: return 0 + case .rgba8888: return 1 + case .rgb888: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_ImageFormat.ImgFormat] = [ + .png, + .rgba8888, + .rgb888, + ] + + } + + init() {} + + fileprivate var _rotation: Android_Emulation_Control_Rotation? = nil + fileprivate var _transport: Android_Emulation_Control_ImageTransport? = nil + fileprivate var _foldedDisplay: Android_Emulation_Control_FoldedDisplay? = nil +} + +nonisolated struct Android_Emulation_Control_Image: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var format: Android_Emulation_Control_ImageFormat { + get {_storage._format ?? Android_Emulation_Control_ImageFormat()} + set {_uniqueStorage()._format = newValue} + } + /// Returns true if `format` has been explicitly set. + var hasFormat: Bool {_storage._format != nil} + /// Clears the value of `format`. Subsequent reads from it will return its default value. + mutating func clearFormat() {_uniqueStorage()._format = nil} + + /// width is contained in format. + /// + /// NOTE: This field was marked as deprecated in the .proto file. + var width: UInt32 { + get {_storage._width} + set {_uniqueStorage()._width = newValue} + } + + /// height is contained in format. + /// + /// NOTE: This field was marked as deprecated in the .proto file. + var height: UInt32 { + get {_storage._height} + set {_uniqueStorage()._height = newValue} + } + + /// The organization of the pixels in the image buffer is from left to + /// right and bottom up. This will be empty if an alternative image transport + /// is requested in the image format. In that case the side channel should + /// be used to obtain the image data. + var image: Data { + get {_storage._image} + set {_uniqueStorage()._image = newValue} + } + + /// [Output Only] Monotonically increasing sequence number in a stream of + /// screenshots. The first screenshot will have a sequence of 0. A single + /// screenshot will always have a sequence number of 0. The sequence is not + /// necessarily contiguous, and can be used to detect how many frames were + /// dropped. An example sequence could be: [0, 3, 5, 7, 9, 11]. + var seq: UInt32 { + get {_storage._seq} + set {_uniqueStorage()._seq = newValue} + } + + /// [Output Only] Unix timestamp in microseconds when the emulator estimates + /// the frame was generated. The timestamp is before the actual frame is + /// copied and transformed. This can be used to calculate variance between + /// frame production time, and frame depiction time. + var timestampUs: UInt64 { + get {_storage._timestampUs} + set {_uniqueStorage()._timestampUs = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +nonisolated struct Android_Emulation_Control_Rotation: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The rotation of the device, derived from the sensor state + /// of the emulator. The derivation reflects how android observes + /// the rotation state. + var rotation: Android_Emulation_Control_Rotation.SkinRotation = .portrait + + /// Specifies the angle of rotation, in degrees [-180, 180] + var xAxis: Double = 0 + + var yAxis: Double = 0 + + var zAxis: Double = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum SkinRotation: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// 0 degrees + case portrait // = 0 + + /// 90 degrees + case landscape // = 1 + + /// -180 degrees + case reversePortrait // = 2 + + /// -90 degrees + case reverseLandscape // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .portrait + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .portrait + case 1: self = .landscape + case 2: self = .reversePortrait + case 3: self = .reverseLandscape + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .portrait: return 0 + case .landscape: return 1 + case .reversePortrait: return 2 + case .reverseLandscape: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_Rotation.SkinRotation] = [ + .portrait, + .landscape, + .reversePortrait, + .reverseLandscape, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_PhoneCall: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var operation: Android_Emulation_Control_PhoneCall.Operation = .initCall + + var number: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum Operation: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case initCall // = 0 + case acceptCall // = 1 + case rejectCallExplicit // = 2 + case rejectCallBusy // = 3 + case disconnectCall // = 4 + case placeCallOnHold // = 5 + case takeCallOffHold // = 6 + case UNRECOGNIZED(Int) + + init() { + self = .initCall + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .initCall + case 1: self = .acceptCall + case 2: self = .rejectCallExplicit + case 3: self = .rejectCallBusy + case 4: self = .disconnectCall + case 5: self = .placeCallOnHold + case 6: self = .takeCallOffHold + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .initCall: return 0 + case .acceptCall: return 1 + case .rejectCallExplicit: return 2 + case .rejectCallBusy: return 3 + case .disconnectCall: return 4 + case .placeCallOnHold: return 5 + case .takeCallOffHold: return 6 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_PhoneCall.Operation] = [ + .initCall, + .acceptCall, + .rejectCallExplicit, + .rejectCallBusy, + .disconnectCall, + .placeCallOnHold, + .takeCallOffHold, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_PhoneResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var response: Android_Emulation_Control_PhoneResponse.Response = .ok + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum Response: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case ok // = 0 + + /// Enum out of range + case badOperation // = 1 + + /// Mal-formed telephone number + case badNumber // = 2 + + /// E.g., disconnect when no call is in progress + case invalidAction // = 3 + + /// Internal error + case actionFailed // = 4 + + /// Radio power off + case radioOff // = 5 + case UNRECOGNIZED(Int) + + init() { + self = .ok + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .badOperation + case 2: self = .badNumber + case 3: self = .invalidAction + case 4: self = .actionFailed + case 5: self = .radioOff + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .ok: return 0 + case .badOperation: return 1 + case .badNumber: return 2 + case .invalidAction: return 3 + case .actionFailed: return 4 + case .radioOff: return 5 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_PhoneResponse.Response] = [ + .ok, + .badOperation, + .badNumber, + .invalidAction, + .actionFailed, + .radioOff, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_Entry: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var key: String = String() + + var value: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_EntryList: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var entry: [Android_Emulation_Control_Entry] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_EmulatorStatus: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The emulator version string. + var version: String = String() + + /// The time the emulator has been active in .ms + var uptime: UInt64 = 0 + + /// True if the device has completed booting. + /// For P and later this information will accurate, + /// for older images we rely on adb. + var booted: Bool = false + + /// The current vm configuration + var vmConfig: Android_Emulation_Control_VmConfiguration { + get {_vmConfig ?? Android_Emulation_Control_VmConfiguration()} + set {_vmConfig = newValue} + } + /// Returns true if `vmConfig` has been explicitly set. + var hasVmConfig: Bool {self._vmConfig != nil} + /// Clears the value of `vmConfig`. Subsequent reads from it will return its default value. + mutating func clearVmConfig() {self._vmConfig = nil} + + /// Use platformConfig instead + /// + /// NOTE: This field was marked as deprecated in the .proto file. + var hardwareConfig: Android_Emulation_Control_EntryList { + get {_hardwareConfig ?? Android_Emulation_Control_EntryList()} + set {_hardwareConfig = newValue} + } + /// Returns true if `hardwareConfig` has been explicitly set. + var hasHardwareConfig: Bool {self._hardwareConfig != nil} + /// Clears the value of `hardwareConfig`. Subsequent reads from it will return its default value. + mutating func clearHardwareConfig() {self._hardwareConfig = nil} + + /// Some guests will produce a heart beat, that can be used to + /// detect if the guest is active. + /// This is a monotonically increasing number that gets incremented + /// around once a second. + var heartbeat: UInt64 = 0 + + /// The configuration of services in the guest, this map + /// contains key value pairs that are specific to the image + /// used by the guest. + var guestConfig: Dictionary = [:] + + /// Configuration of the emulator hardware + var platformConfig: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _vmConfig: Android_Emulation_Control_VmConfiguration? = nil + fileprivate var _hardwareConfig: Android_Emulation_Control_EntryList? = nil +} + +nonisolated struct Android_Emulation_Control_AudioFormat: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Sampling rate to use, defaulting to 44100 if this is not set. + /// Note, that android devices typically will not use a sampling + /// rate higher than 48kHz. See + /// https://developer.android.com/ndk/guides/audio. + var samplingRate: UInt64 = 0 + + var channels: Android_Emulation_Control_AudioFormat.Channels = .mono + + var format: Android_Emulation_Control_AudioFormat.SampleFormat = .audFmtU8 + + /// [Input Only] + /// The mode used when delivering audio packets. + var mode: Android_Emulation_Control_AudioFormat.DeliveryMode = .modeUnspecified + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum SampleFormat: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// Unsigned 8 bit + case audFmtU8 // = 0 + + /// Signed 16 bit (little endian) + case audFmtS16 // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .audFmtU8 + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .audFmtU8 + case 1: self = .audFmtS16 + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .audFmtU8: return 0 + case .audFmtS16: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_AudioFormat.SampleFormat] = [ + .audFmtU8, + .audFmtS16, + ] + + } + + nonisolated enum Channels: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case mono // = 0 + case stereo // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .mono + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .mono + case 1: self = .stereo + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .mono: return 0 + case .stereo: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_AudioFormat.Channels] = [ + .mono, + .stereo, + ] + + } + + nonisolated enum DeliveryMode: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + + /// The audio queue will block and wait until the emulator requests + /// packets. The client does not have to throttle and can push packets at + /// will. This can result in the client falling behind. + case modeUnspecified // = 0 + + /// Audio packets will be delivered in real time (when possible). The + /// audio queue will be overwritten with incoming data if data is made + /// available. This means the client needs to control timing properly, or + /// packets will get overwritten. + case modeRealTime // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .modeUnspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .modeUnspecified + case 1: self = .modeRealTime + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .modeUnspecified: return 0 + case .modeRealTime: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_AudioFormat.DeliveryMode] = [ + .modeUnspecified, + .modeRealTime, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_AudioPacket: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var format: Android_Emulation_Control_AudioFormat { + get {_format ?? Android_Emulation_Control_AudioFormat()} + set {_format = newValue} + } + /// Returns true if `format` has been explicitly set. + var hasFormat: Bool {self._format != nil} + /// Clears the value of `format`. Subsequent reads from it will return its default value. + mutating func clearFormat() {self._format = nil} + + /// Unix epoch in us when this frame was captured. + var timestamp: UInt64 = 0 + + /// Contains a sample in the given audio format. + var audio: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _format: Android_Emulation_Control_AudioFormat? = nil +} + +nonisolated struct Android_Emulation_Control_MicrophoneState: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Whether or not host microphone access is enabled + var realAudioEnabled: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_SmsMessage: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The source address where this message came from. + /// + /// The address should be a valid GSM-formatted address as specified by + /// 3GPP 23.040 Sec 9.1.2.5. + /// + /// For example: +3106225412 or (650) 555-1221 + var srcAddress: String = String() + + /// A utf8 encoded text message that should be delivered. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A DisplayConfiguration describes a primary or secondary +/// display available to the emulator. The screen aspect ratio +/// cannot be longer (or wider) than 21:9 (or 9:21). Screen sizes +/// larger than 4k will be rejected. +/// +/// Common configurations (w x h) are: +/// - 480p (480x720) 142 dpi +/// - 720p (720x1280) 213 dpi +/// - 1080p (1080x1920) 320 dpi +/// - 4K (2160x3840) 320 dpi +/// - 4K (2160x3840) 640 dpi (upscaled) +/// +/// The behavior of the virtual display depends on the flags that are provided to +/// this method. By default, virtual displays are created to be private, +/// non-presentation and unsecure. +nonisolated struct Android_Emulation_Control_DisplayConfiguration: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The width of the display, restricted to: + /// 320 * (dpi / 160) <= width + var width: UInt32 = 0 + + /// The heigh of the display, restricted to: + /// * 320 * (dpi / 160) <= height + var height: UInt32 = 0 + + /// The pixel density (dpi). + /// See https://developer.android.com/training/multiscreen/screendensities + /// for details. This value should be in the range [120, ..., 640] + var dpi: UInt32 = 0 + + /// A combination of virtual display flags. These flags can be constructed + /// by combining the DisplayFlags enum described above. + /// + /// The behavior of the virtual display depends on the flags. By default + /// virtual displays are created to be private, non-presentation and + /// unsecure. + var flags: UInt32 = 0 + + /// The id of the display. + /// The primary (default) display has the display ID of 0. + /// A secondary display has a display ID not 0. + /// + /// A display with the id in the range [1, userConfigurable] + /// can be modified. See DisplayConfigurations below for details. + /// + /// The id can be used to get or stream a screenshot. + var display: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// These are the set of known android flags and their respective values. + /// you can combine the int values to (de)construct the flags field below. + nonisolated enum DisplayFlags: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case unspecified // = 0 + + /// When this flag is set, the virtual display is public. + /// A public virtual display behaves just like most any other display + /// that is connected to the system such as an external or wireless + /// display. Applications can open windows on the display and the system + /// may mirror the contents of other displays onto it. see: + /// https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC + case virtualDisplayFlagPublic // = 1 + + /// When this flag is set, the virtual display is registered as a + /// presentation display in the presentation display category. + /// Applications may automatically project their content to presentation + /// displays to provide richer second screen experiences. + /// https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION + case virtualDisplayFlagPresentation // = 2 + + /// When this flag is set, the virtual display is considered secure as + /// defined by the Display#FLAG_SECURE display flag. The caller promises + /// to take reasonable measures, such as over-the-air encryption, to + /// prevent the contents of the display from being intercepted or + /// recorded on a persistent medium. + /// see: + /// https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_SECURE + case virtualDisplayFlagSecure // = 4 + + /// This flag is used in conjunction with VIRTUAL_DISPLAY_FLAG_PUBLIC. + /// Ordinarily public virtual displays will automatically mirror the + /// content of the default display if they have no windows of their own. + /// When this flag is specified, the virtual display will only ever show + /// its own content and will be blanked instead if it has no windows. See + /// https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + case virtualDisplayFlagOwnContentOnly // = 8 + + /// Allows content to be mirrored on private displays when no content is + /// being shown. + /// This flag is mutually exclusive with + /// VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY. If both flags are specified + /// then the own-content only behavior will be applied. + /// see: + /// https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + case virtualDisplayFlagAutoMirror // = 16 + case UNRECOGNIZED(Int) + + init() { + self = .unspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .virtualDisplayFlagPublic + case 2: self = .virtualDisplayFlagPresentation + case 4: self = .virtualDisplayFlagSecure + case 8: self = .virtualDisplayFlagOwnContentOnly + case 16: self = .virtualDisplayFlagAutoMirror + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unspecified: return 0 + case .virtualDisplayFlagPublic: return 1 + case .virtualDisplayFlagPresentation: return 2 + case .virtualDisplayFlagSecure: return 4 + case .virtualDisplayFlagOwnContentOnly: return 8 + case .virtualDisplayFlagAutoMirror: return 16 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_DisplayConfiguration.DisplayFlags] = [ + .unspecified, + .virtualDisplayFlagPublic, + .virtualDisplayFlagPresentation, + .virtualDisplayFlagSecure, + .virtualDisplayFlagOwnContentOnly, + .virtualDisplayFlagAutoMirror, + ] + + } + + init() {} +} + +/// Provides information about all the displays that can be attached +/// to the emulator. The emulator will always have at least one display. +/// +/// The emulator usually has the following display configurations: +/// 0: The default display. +/// 1 - 3: User configurable displays. These can be added/removed. +/// For example the standalone emulator allows you to modify these +/// in the extended controls. +/// 6 - 11: Fixed external displays. For example Android Auto uses fixed +/// displays in this range. +nonisolated struct Android_Emulation_Control_DisplayConfigurations: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var displays: [Android_Emulation_Control_DisplayConfiguration] = [] + + /// Display configurations with id [1, userConfigurable] are + /// user configurable, that is they can be added, removed or + /// updated. + var userConfigurable: UInt32 = 0 + + /// The maximum number of attached displays this emulator supports. + /// This is the total number of displays that can be attached to + /// the emulator. + /// + /// Note: A display with an id that is larger than userConfigurable cannot + /// be modified. + var maxDisplays: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_Notification: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Detailed notification information. + var type: Android_Emulation_Control_Notification.OneOf_Type? = nil + + var cameraNotification: Android_Emulation_Control_CameraNotification { + get { + if case .cameraNotification(let v)? = type {return v} + return Android_Emulation_Control_CameraNotification() + } + set {type = .cameraNotification(newValue)} + } + + var displayConfigurationsChangedNotification: Android_Emulation_Control_DisplayConfigurationsChangedNotification { + get { + if case .displayConfigurationsChangedNotification(let v)? = type {return v} + return Android_Emulation_Control_DisplayConfigurationsChangedNotification() + } + set {type = .displayConfigurationsChangedNotification(newValue)} + } + + var posture: Android_Emulation_Control_Posture { + get { + if case .posture(let v)? = type {return v} + return Android_Emulation_Control_Posture() + } + set {type = .posture(newValue)} + } + + var booted: Android_Emulation_Control_BootCompletedNotification { + get { + if case .booted(let v)? = type {return v} + return Android_Emulation_Control_BootCompletedNotification() + } + set {type = .booted(newValue)} + } + + var brightness: Android_Emulation_Control_BrightnessValue { + get { + if case .brightness(let v)? = type {return v} + return Android_Emulation_Control_BrightnessValue() + } + set {type = .brightness(newValue)} + } + + /// This notification is sent when a TextView receives or loses focus. + /// It is also sent immediately in response to the streamNotification + /// call. + var textViewFocus: Android_Emulation_Control_TextViewFocus { + get { + if case .textViewFocus(let v)? = type {return v} + return Android_Emulation_Control_TextViewFocus() + } + set {type = .textViewFocus(newValue)} + } + + /// This notification is sent when XrOptions change. + /// It is also sent immediately in response to the streamNotification + /// call. + var xrOptions: Android_Emulation_Control_XrOptions { + get { + if case .xrOptions(let v)? = type {return v} + return Android_Emulation_Control_XrOptions() + } + set {type = .xrOptions(newValue)} + } + + var microphoneState: Android_Emulation_Control_MicrophoneState { + get { + if case .microphoneState(let v)? = type {return v} + return Android_Emulation_Control_MicrophoneState() + } + set {type = .microphoneState(newValue)} + } + + /// This notification is sent when AI glasses LED light status change. + var ledIndicator: Android_Emulation_Control_LedIndicator { + get { + if case .ledIndicator(let v)? = type {return v} + return Android_Emulation_Control_LedIndicator() + } + set {type = .ledIndicator(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Detailed notification information. + nonisolated enum OneOf_Type: Equatable, Sendable { + case cameraNotification(Android_Emulation_Control_CameraNotification) + case displayConfigurationsChangedNotification(Android_Emulation_Control_DisplayConfigurationsChangedNotification) + case posture(Android_Emulation_Control_Posture) + case booted(Android_Emulation_Control_BootCompletedNotification) + case brightness(Android_Emulation_Control_BrightnessValue) + /// This notification is sent when a TextView receives or loses focus. + /// It is also sent immediately in response to the streamNotification + /// call. + case textViewFocus(Android_Emulation_Control_TextViewFocus) + /// This notification is sent when XrOptions change. + /// It is also sent immediately in response to the streamNotification + /// call. + case xrOptions(Android_Emulation_Control_XrOptions) + case microphoneState(Android_Emulation_Control_MicrophoneState) + /// This notification is sent when AI glasses LED light status change. + case ledIndicator(Android_Emulation_Control_LedIndicator) + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_BootCompletedNotification: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The time in milliseconds it took for the boot to complete. + /// Note that this value can be 0 when you are loading from a snapshot. + var time: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Fired when the virtual scene camera is activated or deactivated and also in +/// response to the streamNotification call. +nonisolated struct Android_Emulation_Control_CameraNotification: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Indicates whether the camera app was activated or deactivated. + var active: Bool = false + + /// The display the camera app is associated with. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_TextViewFocus: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Indicates whether a text view currently has focus. + var textViewHasFocus: Bool = false + + /// If a text view has focus, the display where the text view is located. + /// Otherwise, unset. + var display: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Fired when an update to a display event has been fired through the extended +/// ui. This does not fire events when the display is changed through the console +/// or the gRPC endpoint. +nonisolated struct Android_Emulation_Control_DisplayConfigurationsChangedNotification: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var displayConfigurations: Android_Emulation_Control_DisplayConfigurations { + get {_displayConfigurations ?? Android_Emulation_Control_DisplayConfigurations()} + set {_displayConfigurations = newValue} + } + /// Returns true if `displayConfigurations` has been explicitly set. + var hasDisplayConfigurations: Bool {self._displayConfigurations != nil} + /// Clears the value of `displayConfigurations`. Subsequent reads from it will return its default value. + mutating func clearDisplayConfigurations() {self._displayConfigurations = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _displayConfigurations: Android_Emulation_Control_DisplayConfigurations? = nil +} + +nonisolated struct Android_Emulation_Control_RotationRadian: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Components of the rotation vector in radians. + /// Rotation angles are relative to the current orientation. + var x: Float = 0 + + /// Angle of rotation around the y axis in right-handed direction. + var y: Float = 0 + + /// Angle of rotation around the z axis in right-handed direction. + var z: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_Translation: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Components of the translation vector in meters. + var deltaX: Float = 0 + + var deltaY: Float = 0 + + var deltaZ: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_AngularVelocity: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Components of the target angular velocity vector in radians per second. + /// Transition to these values is implementation dependent, and may be + /// smoothed over time. + var omegaX: Float = 0 + + var omegaY: Float = 0 + + var omegaZ: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_Velocity: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Components of the target velocity vector in meters per second. + /// Transition to these values is implementation dependent, and may be + /// smoothed over time. + var x: Float = 0 + + var y: Float = 0 + + var z: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Must follow the definition in "external/qemu/android/hw-sensors.h" +nonisolated struct Android_Emulation_Control_Posture: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var value: Android_Emulation_Control_Posture.PostureValue = .postureUnknown + + var unknownFields = SwiftProtobuf.UnknownStorage() + + nonisolated enum PostureValue: SwiftProtobuf.Enum, Swift.CaseIterable { + typealias RawValue = Int + case postureUnknown // = 0 + case postureClosed // = 1 + case postureHalfOpened // = 2 + case postureOpened // = 3 + case postureFlipped // = 4 + case postureTent // = 5 + case postureMax // = 6 + case UNRECOGNIZED(Int) + + init() { + self = .postureUnknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .postureUnknown + case 1: self = .postureClosed + case 2: self = .postureHalfOpened + case 3: self = .postureOpened + case 4: self = .postureFlipped + case 5: self = .postureTent + case 6: self = .postureMax + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .postureUnknown: return 0 + case .postureClosed: return 1 + case .postureHalfOpened: return 2 + case .postureOpened: return 3 + case .postureFlipped: return 4 + case .postureTent: return 5 + case .postureMax: return 6 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Android_Emulation_Control_Posture.PostureValue] = [ + .postureUnknown, + .postureClosed, + .postureHalfOpened, + .postureOpened, + .postureFlipped, + .postureTent, + .postureMax, + ] + + } + + init() {} +} + +nonisolated struct Android_Emulation_Control_PhoneNumber: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// The phone number should be a valid GSM-formatted number as specified by + /// 3GPP 23.040 Sec 9.1.2.5. + /// + /// For example: +3106225412 or (650) 555-1221 + var number: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Specifies environment background for AI glasses. +nonisolated struct Android_Emulation_Control_Environment: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Key/value pairs corresponing to the contents of the environment.ini file. + /// An empty map means no environment. + var environment: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_Camera: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Human-readable name for use with UI + var displayName: String = String() + + /// Os Specific identifier + var id: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +nonisolated struct Android_Emulation_Control_CameraList: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var cameras: [Android_Emulation_Control_Camera] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate nonisolated let _protobuf_package = "android.emulation.control" + +nonisolated extension Android_Emulation_Control_DisplayModeValue: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0PHONE\0\u{1}FOLDABLE\0\u{1}TABLET\0\u{1}DESKTOP\0") +} + +nonisolated extension Android_Emulation_Control_VmRunState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".VmRunState" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}state\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.state) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.state != .unknown { + try visitor.visitSingularEnumField(value: self.state, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_VmRunState, rhs: Android_Emulation_Control_VmRunState) -> Bool { + if lhs.state != rhs.state {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_VmRunState.RunState: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}RUNNING\0\u{1}RESTORE_VM\0\u{1}PAUSED\0\u{1}SAVE_VM\0\u{1}SHUTDOWN\0\u{2}\u{2}TERMINATE\0\u{2}\u{2}RESET\0\u{1}INTERNAL_ERROR\0\u{1}RESTART\0\u{1}START\0\u{1}STOP\0") +} + +nonisolated extension Android_Emulation_Control_ParameterValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ParameterValue" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}data\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedFloatField(value: &self.data) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.data.isEmpty { + try visitor.visitPackedFloatField(value: self.data, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_ParameterValue, rhs: Android_Emulation_Control_ParameterValue) -> Bool { + if lhs.data != rhs.data {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_PhysicalModelValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PhysicalModelValue" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}target\0\u{1}status\0\u{1}value\0\u{1}interpolation\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.target) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.status) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._value) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.interpolation) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.target != .position { + try visitor.visitSingularEnumField(value: self.target, fieldNumber: 1) + } + if self.status != .ok { + try visitor.visitSingularEnumField(value: self.status, fieldNumber: 2) + } + try { if let v = self._value { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if self.interpolation != .smooth { + try visitor.visitSingularEnumField(value: self.interpolation, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_PhysicalModelValue, rhs: Android_Emulation_Control_PhysicalModelValue) -> Bool { + if lhs.target != rhs.target {return false} + if lhs.status != rhs.status {return false} + if lhs._value != rhs._value {return false} + if lhs.interpolation != rhs.interpolation {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_PhysicalModelValue.State: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}}\u{7f}\u{7f}\u{7f}\u{7f}\u{3}NO_SERVICE\0\u{1}DISABLED\0\u{1}UNKNOWN\0\u{1}OK\0") +} + +nonisolated extension Android_Emulation_Control_PhysicalModelValue.PhysicalType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0POSITION\0\u{1}ROTATION\0\u{1}MAGNETIC_FIELD\0\u{1}TEMPERATURE\0\u{1}PROXIMITY\0\u{1}LIGHT\0\u{1}PRESSURE\0\u{1}HUMIDITY\0\u{1}VELOCITY\0\u{1}AMBIENT_MOTION\0\u{1}HINGE_ANGLE0\0\u{1}HINGE_ANGLE1\0\u{1}HINGE_ANGLE2\0\u{1}ROLLABLE0\0\u{1}ROLLABLE1\0\u{1}ROLLABLE2\0\u{1}POSTURE\0\u{1}HEART_RATE\0\u{1}RGBC_LIGHT\0\u{1}WRIST_TILT\0") +} + +nonisolated extension Android_Emulation_Control_PhysicalModelValue.Interpolation: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0SMOOTH\0\u{1}STEP\0") +} + +nonisolated extension Android_Emulation_Control_SensorValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SensorValue" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}target\0\u{1}status\0\u{1}value\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.target) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.status) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.target != .acceleration { + try visitor.visitSingularEnumField(value: self.target, fieldNumber: 1) + } + if self.status != .ok { + try visitor.visitSingularEnumField(value: self.status, fieldNumber: 2) + } + try { if let v = self._value { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_SensorValue, rhs: Android_Emulation_Control_SensorValue) -> Bool { + if lhs.target != rhs.target {return false} + if lhs.status != rhs.status {return false} + if lhs._value != rhs._value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_SensorValue.State: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}}\u{7f}\u{7f}\u{7f}\u{7f}\u{3}NO_SERVICE\0\u{1}DISABLED\0\u{1}UNKNOWN\0\u{1}OK\0") +} + +nonisolated extension Android_Emulation_Control_SensorValue.SensorType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ACCELERATION\0\u{1}GYROSCOPE\0\u{1}MAGNETIC_FIELD\0\u{1}ORIENTATION\0\u{1}TEMPERATURE\0\u{1}PROXIMITY\0\u{1}LIGHT\0\u{1}PRESSURE\0\u{1}HUMIDITY\0\u{1}MAGNETIC_FIELD_UNCALIBRATED\0\u{1}GYROSCOPE_UNCALIBRATED\0\u{2}\u{4}HEART_RATE\0\u{1}RGBC_LIGHT\0\u{2}\u{2}ACCELERATION_UNCALIBRATED\0\u{1}HEADING\0") +} + +nonisolated extension Android_Emulation_Control_BrightnessValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".BrightnessValue" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}target\0\u{1}value\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.target) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.target != .lcd { + try visitor.visitSingularEnumField(value: self.target, fieldNumber: 1) + } + if self.value != 0 { + try visitor.visitSingularUInt32Field(value: self.value, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_BrightnessValue, rhs: Android_Emulation_Control_BrightnessValue) -> Bool { + if lhs.target != rhs.target {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_BrightnessValue.LightType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0LCD\0\u{1}KEYBOARD\0\u{1}BUTTON\0") +} + +nonisolated extension Android_Emulation_Control_DisplayMode: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DisplayMode" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}value\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.value != .phone { + try visitor.visitSingularEnumField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_DisplayMode, rhs: Android_Emulation_Control_DisplayMode) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_XrOptions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XrOptions" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}environment\0\u{3}passthrough_coefficient\0\u{4}\u{2}dimming_value\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.environment) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.passthroughCoefficient) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self.dimmingValue) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.environment != .livingRoomDay { + try visitor.visitSingularEnumField(value: self.environment, fieldNumber: 1) + } + if self.passthroughCoefficient.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.passthroughCoefficient, fieldNumber: 2) + } + if self.dimmingValue.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.dimmingValue, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_XrOptions, rhs: Android_Emulation_Control_XrOptions) -> Bool { + if lhs.environment != rhs.environment {return false} + if lhs.passthroughCoefficient != rhs.passthroughCoefficient {return false} + if lhs.dimmingValue != rhs.dimmingValue {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_XrOptions.Environment: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0LIVING_ROOM_DAY\0\u{1}LIVING_ROOM_NIGHT\0") +} + +nonisolated extension Android_Emulation_Control_LedIndicator: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LedIndicator" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}facing\0\u{1}state\0\u{1}color\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._id) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self._facing) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self._state) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self._color) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._id { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._facing { + try visitor.visitSingularEnumField(value: v, fieldNumber: 2) + } }() + try { if let v = self._state { + try visitor.visitSingularEnumField(value: v, fieldNumber: 3) + } }() + try { if let v = self._color { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_LedIndicator, rhs: Android_Emulation_Control_LedIndicator) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._facing != rhs._facing {return false} + if lhs._state != rhs._state {return false} + if lhs._color != rhs._color {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_LedIndicator.Facing: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0INSIDE\0\u{1}OUTSIDE\0") +} + +nonisolated extension Android_Emulation_Control_LedIndicator.State: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OFF\0\u{1}ON\0") +} + +nonisolated extension Android_Emulation_Control_LogMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LogMessage" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}contents\0\u{1}start\0\u{1}next\0\u{1}sort\0\u{1}entries\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.contents) }() + case 2: try { try decoder.decodeSingularInt64Field(value: &self.start) }() + case 3: try { try decoder.decodeSingularInt64Field(value: &self.next) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.sort) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.entries) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.contents.isEmpty { + try visitor.visitSingularStringField(value: self.contents, fieldNumber: 1) + } + if self.start != 0 { + try visitor.visitSingularInt64Field(value: self.start, fieldNumber: 2) + } + if self.next != 0 { + try visitor.visitSingularInt64Field(value: self.next, fieldNumber: 3) + } + if self.sort != .text { + try visitor.visitSingularEnumField(value: self.sort, fieldNumber: 4) + } + if !self.entries.isEmpty { + try visitor.visitRepeatedMessageField(value: self.entries, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_LogMessage, rhs: Android_Emulation_Control_LogMessage) -> Bool { + if lhs.contents != rhs.contents {return false} + if lhs.start != rhs.start {return false} + if lhs.next != rhs.next {return false} + if lhs.sort != rhs.sort {return false} + if lhs.entries != rhs.entries {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_LogMessage.LogType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0Text\0\u{1}Parsed\0") +} + +nonisolated extension Android_Emulation_Control_LogcatEntry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LogcatEntry" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}timestamp\0\u{1}pid\0\u{1}tid\0\u{1}level\0\u{1}tag\0\u{1}msg\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.timestamp) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.pid) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.tid) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.level) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.tag) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.msg) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.timestamp != 0 { + try visitor.visitSingularUInt64Field(value: self.timestamp, fieldNumber: 1) + } + if self.pid != 0 { + try visitor.visitSingularUInt32Field(value: self.pid, fieldNumber: 2) + } + if self.tid != 0 { + try visitor.visitSingularUInt32Field(value: self.tid, fieldNumber: 3) + } + if self.level != .unknown { + try visitor.visitSingularEnumField(value: self.level, fieldNumber: 4) + } + if !self.tag.isEmpty { + try visitor.visitSingularStringField(value: self.tag, fieldNumber: 5) + } + if !self.msg.isEmpty { + try visitor.visitSingularStringField(value: self.msg, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_LogcatEntry, rhs: Android_Emulation_Control_LogcatEntry) -> Bool { + if lhs.timestamp != rhs.timestamp {return false} + if lhs.pid != rhs.pid {return false} + if lhs.tid != rhs.tid {return false} + if lhs.level != rhs.level {return false} + if lhs.tag != rhs.tag {return false} + if lhs.msg != rhs.msg {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_LogcatEntry.LogLevel: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}DEFAULT\0\u{1}VERBOSE\0\u{1}DEBUG\0\u{1}INFO\0\u{1}WARN\0\u{1}ERR\0\u{1}FATAL\0\u{1}SILENT\0") +} + +nonisolated extension Android_Emulation_Control_VmConfiguration: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".VmConfiguration" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}hypervisorType\0\u{1}numberOfCpuCores\0\u{1}ramSizeBytes\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.hypervisorType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.numberOfCpuCores) }() + case 3: try { try decoder.decodeSingularInt64Field(value: &self.ramSizeBytes) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.hypervisorType != .unknown { + try visitor.visitSingularEnumField(value: self.hypervisorType, fieldNumber: 1) + } + if self.numberOfCpuCores != 0 { + try visitor.visitSingularInt32Field(value: self.numberOfCpuCores, fieldNumber: 2) + } + if self.ramSizeBytes != 0 { + try visitor.visitSingularInt64Field(value: self.ramSizeBytes, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_VmConfiguration, rhs: Android_Emulation_Control_VmConfiguration) -> Bool { + if lhs.hypervisorType != rhs.hypervisorType {return false} + if lhs.numberOfCpuCores != rhs.numberOfCpuCores {return false} + if lhs.ramSizeBytes != rhs.ramSizeBytes {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_VmConfiguration.VmHypervisorType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}NONE\0\u{1}KVM\0\u{1}HAXM\0\u{1}HVF\0\u{1}WHPX\0\u{1}AEHD\0") +} + +nonisolated extension Android_Emulation_Control_ClipData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClipData" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}text\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_ClipData, rhs: Android_Emulation_Control_ClipData) -> Bool { + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Touch: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Touch" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}x\0\u{1}y\0\u{1}identifier\0\u{1}pressure\0\u{3}touch_major\0\u{3}touch_minor\0\u{1}expiration\0\u{1}orientation\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.x) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.y) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.identifier) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.pressure) }() + case 5: try { try decoder.decodeSingularInt32Field(value: &self.touchMajor) }() + case 6: try { try decoder.decodeSingularInt32Field(value: &self.touchMinor) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.expiration) }() + case 8: try { try decoder.decodeSingularInt32Field(value: &self.orientation) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.x != 0 { + try visitor.visitSingularInt32Field(value: self.x, fieldNumber: 1) + } + if self.y != 0 { + try visitor.visitSingularInt32Field(value: self.y, fieldNumber: 2) + } + if self.identifier != 0 { + try visitor.visitSingularInt32Field(value: self.identifier, fieldNumber: 3) + } + if self.pressure != 0 { + try visitor.visitSingularInt32Field(value: self.pressure, fieldNumber: 4) + } + if self.touchMajor != 0 { + try visitor.visitSingularInt32Field(value: self.touchMajor, fieldNumber: 5) + } + if self.touchMinor != 0 { + try visitor.visitSingularInt32Field(value: self.touchMinor, fieldNumber: 6) + } + if self.expiration != .unspecified { + try visitor.visitSingularEnumField(value: self.expiration, fieldNumber: 7) + } + if self.orientation != 0 { + try visitor.visitSingularInt32Field(value: self.orientation, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Touch, rhs: Android_Emulation_Control_Touch) -> Bool { + if lhs.x != rhs.x {return false} + if lhs.y != rhs.y {return false} + if lhs.identifier != rhs.identifier {return false} + if lhs.pressure != rhs.pressure {return false} + if lhs.touchMajor != rhs.touchMajor {return false} + if lhs.touchMinor != rhs.touchMinor {return false} + if lhs.expiration != rhs.expiration {return false} + if lhs.orientation != rhs.orientation {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Touch.EventExpiration: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0EVENT_EXPIRATION_UNSPECIFIED\0\u{1}NEVER_EXPIRE\0") +} + +nonisolated extension Android_Emulation_Control_Pen: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Pen" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}location\0\u{3}button_pressed\0\u{3}rubber_pointer\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._location) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.buttonPressed) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.rubberPointer) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._location { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.buttonPressed != false { + try visitor.visitSingularBoolField(value: self.buttonPressed, fieldNumber: 2) + } + if self.rubberPointer != false { + try visitor.visitSingularBoolField(value: self.rubberPointer, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Pen, rhs: Android_Emulation_Control_Pen) -> Bool { + if lhs._location != rhs._location {return false} + if lhs.buttonPressed != rhs.buttonPressed {return false} + if lhs.rubberPointer != rhs.rubberPointer {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_TouchEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TouchEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}touches\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.touches) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.touches.isEmpty { + try visitor.visitRepeatedMessageField(value: self.touches, fieldNumber: 1) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_TouchEvent, rhs: Android_Emulation_Control_TouchEvent) -> Bool { + if lhs.touches != rhs.touches {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_TouchpadEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TouchpadEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}touches\0\u{1}touchpad\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.touches) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.touchpad) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.touches.isEmpty { + try visitor.visitRepeatedMessageField(value: self.touches, fieldNumber: 1) + } + if self.touchpad != 0 { + try visitor.visitSingularInt32Field(value: self.touchpad, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_TouchpadEvent, rhs: Android_Emulation_Control_TouchpadEvent) -> Bool { + if lhs.touches != rhs.touches {return false} + if lhs.touchpad != rhs.touchpad {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_PenEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PenEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}events\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.events) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.events.isEmpty { + try visitor.visitRepeatedMessageField(value: self.events, fieldNumber: 1) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_PenEvent, rhs: Android_Emulation_Control_PenEvent) -> Bool { + if lhs.events != rhs.events {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_MouseEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MouseEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}x\0\u{1}y\0\u{1}buttons\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.x) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.y) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.buttons) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.x != 0 { + try visitor.visitSingularInt32Field(value: self.x, fieldNumber: 1) + } + if self.y != 0 { + try visitor.visitSingularInt32Field(value: self.y, fieldNumber: 2) + } + if self.buttons != 0 { + try visitor.visitSingularInt32Field(value: self.buttons, fieldNumber: 3) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_MouseEvent, rhs: Android_Emulation_Control_MouseEvent) -> Bool { + if lhs.x != rhs.x {return false} + if lhs.y != rhs.y {return false} + if lhs.buttons != rhs.buttons {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_WheelEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".WheelEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}dx\0\u{1}dy\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.dx) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.dy) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.dx != 0 { + try visitor.visitSingularInt32Field(value: self.dx, fieldNumber: 1) + } + if self.dy != 0 { + try visitor.visitSingularInt32Field(value: self.dy, fieldNumber: 2) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_WheelEvent, rhs: Android_Emulation_Control_WheelEvent) -> Bool { + if lhs.dx != rhs.dx {return false} + if lhs.dy != rhs.dy {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_KeyboardEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".KeyboardEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}codeType\0\u{1}eventType\0\u{1}keyCode\0\u{1}key\0\u{1}text\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.codeType) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.eventType) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.keyCode) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.key) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.codeType != .usb { + try visitor.visitSingularEnumField(value: self.codeType, fieldNumber: 1) + } + if self.eventType != .keydown { + try visitor.visitSingularEnumField(value: self.eventType, fieldNumber: 2) + } + if self.keyCode != 0 { + try visitor.visitSingularInt32Field(value: self.keyCode, fieldNumber: 3) + } + if !self.key.isEmpty { + try visitor.visitSingularStringField(value: self.key, fieldNumber: 4) + } + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_KeyboardEvent, rhs: Android_Emulation_Control_KeyboardEvent) -> Bool { + if lhs.codeType != rhs.codeType {return false} + if lhs.eventType != rhs.eventType {return false} + if lhs.keyCode != rhs.keyCode {return false} + if lhs.key != rhs.key {return false} + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_KeyboardEvent.KeyCodeType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0Usb\0\u{1}Evdev\0\u{1}XKB\0\u{1}Win\0\u{1}Mac\0") +} + +nonisolated extension Android_Emulation_Control_KeyboardEvent.KeyEventType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0keydown\0\u{1}keyup\0\u{1}keypress\0") +} + +nonisolated extension Android_Emulation_Control_XrCommand: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XrCommand" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}action\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.action) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.action != .recenter { + try visitor.visitSingularEnumField(value: self.action, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_XrCommand, rhs: Android_Emulation_Control_XrCommand) -> Bool { + if lhs.action != rhs.action {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_XrCommand.Action: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0RECENTER\0") +} + +nonisolated extension Android_Emulation_Control_InputEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".InputEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_event\0\u{3}touch_event\0\u{3}mouse_event\0\u{3}android_event\0\u{3}pen_event\0\u{3}wheel_event\0\u{3}xr_hand_event\0\u{3}xr_eye_event\0\u{3}xr_command\0\u{3}xr_head_rotation_event\0\u{3}xr_head_movement_event\0\u{3}xr_head_angular_velocity_event\0\u{3}xr_head_velocity_event\0\u{3}touchpad_event\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Android_Emulation_Control_KeyboardEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .keyEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .keyEvent(v) + } + }() + case 2: try { + var v: Android_Emulation_Control_TouchEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .touchEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .touchEvent(v) + } + }() + case 3: try { + var v: Android_Emulation_Control_MouseEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .mouseEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .mouseEvent(v) + } + }() + case 4: try { + var v: Android_Emulation_Control_AndroidEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .androidEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .androidEvent(v) + } + }() + case 5: try { + var v: Android_Emulation_Control_PenEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .penEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .penEvent(v) + } + }() + case 6: try { + var v: Android_Emulation_Control_WheelEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .wheelEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .wheelEvent(v) + } + }() + case 7: try { + var v: Android_Emulation_Control_MouseEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrHandEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrHandEvent(v) + } + }() + case 8: try { + var v: Android_Emulation_Control_MouseEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrEyeEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrEyeEvent(v) + } + }() + case 9: try { + var v: Android_Emulation_Control_XrCommand? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrCommand(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrCommand(v) + } + }() + case 10: try { + var v: Android_Emulation_Control_RotationRadian? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrHeadRotationEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrHeadRotationEvent(v) + } + }() + case 11: try { + var v: Android_Emulation_Control_Translation? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrHeadMovementEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrHeadMovementEvent(v) + } + }() + case 12: try { + var v: Android_Emulation_Control_AngularVelocity? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrHeadAngularVelocityEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrHeadAngularVelocityEvent(v) + } + }() + case 13: try { + var v: Android_Emulation_Control_Velocity? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrHeadVelocityEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrHeadVelocityEvent(v) + } + }() + case 14: try { + var v: Android_Emulation_Control_TouchpadEvent? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .touchpadEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .touchpadEvent(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.type { + case .keyEvent?: try { + guard case .keyEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .touchEvent?: try { + guard case .touchEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .mouseEvent?: try { + guard case .mouseEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .androidEvent?: try { + guard case .androidEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .penEvent?: try { + guard case .penEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .wheelEvent?: try { + guard case .wheelEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .xrHandEvent?: try { + guard case .xrHandEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case .xrEyeEvent?: try { + guard case .xrEyeEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() + case .xrCommand?: try { + guard case .xrCommand(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() + case .xrHeadRotationEvent?: try { + guard case .xrHeadRotationEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case .xrHeadMovementEvent?: try { + guard case .xrHeadMovementEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .xrHeadAngularVelocityEvent?: try { + guard case .xrHeadAngularVelocityEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .xrHeadVelocityEvent?: try { + guard case .xrHeadVelocityEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case .touchpadEvent?: try { + guard case .touchpadEvent(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_InputEvent, rhs: Android_Emulation_Control_InputEvent) -> Bool { + if lhs.type != rhs.type {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_AndroidEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AndroidEvent" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{1}code\0\u{1}value\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.type) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.code) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.value) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.type != 0 { + try visitor.visitSingularInt32Field(value: self.type, fieldNumber: 1) + } + if self.code != 0 { + try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 2) + } + if self.value != 0 { + try visitor.visitSingularInt32Field(value: self.value, fieldNumber: 3) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_AndroidEvent, rhs: Android_Emulation_Control_AndroidEvent) -> Bool { + if lhs.type != rhs.type {return false} + if lhs.code != rhs.code {return false} + if lhs.value != rhs.value {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Fingerprint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Fingerprint" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}isTouching\0\u{1}touchId\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.isTouching) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.touchID) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.isTouching != false { + try visitor.visitSingularBoolField(value: self.isTouching, fieldNumber: 1) + } + if self.touchID != 0 { + try visitor.visitSingularInt32Field(value: self.touchID, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Fingerprint, rhs: Android_Emulation_Control_Fingerprint) -> Bool { + if lhs.isTouching != rhs.isTouching {return false} + if lhs.touchID != rhs.touchID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_GpsState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".GpsState" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}passiveUpdate\0\u{1}latitude\0\u{1}longitude\0\u{1}speed\0\u{1}bearing\0\u{1}altitude\0\u{1}satellites\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.passiveUpdate) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.latitude) }() + case 3: try { try decoder.decodeSingularDoubleField(value: &self.longitude) }() + case 4: try { try decoder.decodeSingularDoubleField(value: &self.speed) }() + case 5: try { try decoder.decodeSingularDoubleField(value: &self.bearing) }() + case 6: try { try decoder.decodeSingularDoubleField(value: &self.altitude) }() + case 7: try { try decoder.decodeSingularInt32Field(value: &self.satellites) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.passiveUpdate != false { + try visitor.visitSingularBoolField(value: self.passiveUpdate, fieldNumber: 1) + } + if self.latitude.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.latitude, fieldNumber: 2) + } + if self.longitude.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.longitude, fieldNumber: 3) + } + if self.speed.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.speed, fieldNumber: 4) + } + if self.bearing.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.bearing, fieldNumber: 5) + } + if self.altitude.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.altitude, fieldNumber: 6) + } + if self.satellites != 0 { + try visitor.visitSingularInt32Field(value: self.satellites, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_GpsState, rhs: Android_Emulation_Control_GpsState) -> Bool { + if lhs.passiveUpdate != rhs.passiveUpdate {return false} + if lhs.latitude != rhs.latitude {return false} + if lhs.longitude != rhs.longitude {return false} + if lhs.speed != rhs.speed {return false} + if lhs.bearing != rhs.bearing {return false} + if lhs.altitude != rhs.altitude {return false} + if lhs.satellites != rhs.satellites {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_BatteryState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".BatteryState" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}hasBattery\0\u{1}isPresent\0\u{1}charger\0\u{1}chargeLevel\0\u{1}health\0\u{1}status\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.hasBattery_p) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.isPresent) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.charger) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.chargeLevel) }() + case 5: try { try decoder.decodeSingularEnumField(value: &self.health) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.status) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.hasBattery_p != false { + try visitor.visitSingularBoolField(value: self.hasBattery_p, fieldNumber: 1) + } + if self.isPresent != false { + try visitor.visitSingularBoolField(value: self.isPresent, fieldNumber: 2) + } + if self.charger != .none { + try visitor.visitSingularEnumField(value: self.charger, fieldNumber: 3) + } + if self.chargeLevel != 0 { + try visitor.visitSingularInt32Field(value: self.chargeLevel, fieldNumber: 4) + } + if self.health != .good { + try visitor.visitSingularEnumField(value: self.health, fieldNumber: 5) + } + if self.status != .unknown { + try visitor.visitSingularEnumField(value: self.status, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_BatteryState, rhs: Android_Emulation_Control_BatteryState) -> Bool { + if lhs.hasBattery_p != rhs.hasBattery_p {return false} + if lhs.isPresent != rhs.isPresent {return false} + if lhs.charger != rhs.charger {return false} + if lhs.chargeLevel != rhs.chargeLevel {return false} + if lhs.health != rhs.health {return false} + if lhs.status != rhs.status {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_BatteryState.BatteryStatus: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}CHARGING\0\u{1}DISCHARGING\0\u{1}NOT_CHARGING\0\u{1}FULL\0") +} + +nonisolated extension Android_Emulation_Control_BatteryState.BatteryCharger: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}AC\0\u{1}USB\0\u{1}WIRELESS\0") +} + +nonisolated extension Android_Emulation_Control_BatteryState.BatteryHealth: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0GOOD\0\u{1}FAILED\0\u{1}DEAD\0\u{1}OVERVOLTAGE\0\u{1}OVERHEATED\0") +} + +nonisolated extension Android_Emulation_Control_ImageTransport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ImageTransport" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}channel\0\u{1}handle\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.channel) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.handle) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.channel != .unspecified { + try visitor.visitSingularEnumField(value: self.channel, fieldNumber: 1) + } + if !self.handle.isEmpty { + try visitor.visitSingularStringField(value: self.handle, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_ImageTransport, rhs: Android_Emulation_Control_ImageTransport) -> Bool { + if lhs.channel != rhs.channel {return false} + if lhs.handle != rhs.handle {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_ImageTransport.TransportChannel: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0TRANSPORT_CHANNEL_UNSPECIFIED\0\u{1}MMAP\0") +} + +nonisolated extension Android_Emulation_Control_FoldedDisplay: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".FoldedDisplay" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}width\0\u{1}height\0\u{1}xOffset\0\u{1}yOffset\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.width) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.height) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.xOffset) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.yOffset) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.width != 0 { + try visitor.visitSingularUInt32Field(value: self.width, fieldNumber: 1) + } + if self.height != 0 { + try visitor.visitSingularUInt32Field(value: self.height, fieldNumber: 2) + } + if self.xOffset != 0 { + try visitor.visitSingularUInt32Field(value: self.xOffset, fieldNumber: 3) + } + if self.yOffset != 0 { + try visitor.visitSingularUInt32Field(value: self.yOffset, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_FoldedDisplay, rhs: Android_Emulation_Control_FoldedDisplay) -> Bool { + if lhs.width != rhs.width {return false} + if lhs.height != rhs.height {return false} + if lhs.xOffset != rhs.xOffset {return false} + if lhs.yOffset != rhs.yOffset {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_ImageFormat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ImageFormat" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}format\0\u{1}rotation\0\u{1}width\0\u{1}height\0\u{1}display\0\u{1}transport\0\u{1}foldedDisplay\0\u{1}displayMode\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.format) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._rotation) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.width) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.height) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.display) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._transport) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._foldedDisplay) }() + case 8: try { try decoder.decodeSingularEnumField(value: &self.displayMode) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.format != .png { + try visitor.visitSingularEnumField(value: self.format, fieldNumber: 1) + } + try { if let v = self._rotation { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if self.width != 0 { + try visitor.visitSingularUInt32Field(value: self.width, fieldNumber: 3) + } + if self.height != 0 { + try visitor.visitSingularUInt32Field(value: self.height, fieldNumber: 4) + } + if self.display != 0 { + try visitor.visitSingularUInt32Field(value: self.display, fieldNumber: 5) + } + try { if let v = self._transport { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + try { if let v = self._foldedDisplay { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + if self.displayMode != .phone { + try visitor.visitSingularEnumField(value: self.displayMode, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_ImageFormat, rhs: Android_Emulation_Control_ImageFormat) -> Bool { + if lhs.format != rhs.format {return false} + if lhs._rotation != rhs._rotation {return false} + if lhs.width != rhs.width {return false} + if lhs.height != rhs.height {return false} + if lhs.display != rhs.display {return false} + if lhs._transport != rhs._transport {return false} + if lhs._foldedDisplay != rhs._foldedDisplay {return false} + if lhs.displayMode != rhs.displayMode {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_ImageFormat.ImgFormat: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0PNG\0\u{1}RGBA8888\0\u{1}RGB888\0") +} + +nonisolated extension Android_Emulation_Control_Image: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Image" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}format\0\u{1}width\0\u{1}height\0\u{1}image\0\u{1}seq\0\u{1}timestampUs\0") + + fileprivate class _StorageClass { + var _format: Android_Emulation_Control_ImageFormat? = nil + var _width: UInt32 = 0 + var _height: UInt32 = 0 + var _image: Data = Data() + var _seq: UInt32 = 0 + var _timestampUs: UInt64 = 0 + + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _format = source._format + _width = source._width + _height = source._height + _image = source._image + _seq = source._seq + _timestampUs = source._timestampUs + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._format) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &_storage._width) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &_storage._height) }() + case 4: try { try decoder.decodeSingularBytesField(value: &_storage._image) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &_storage._seq) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &_storage._timestampUs) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._format { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if _storage._width != 0 { + try visitor.visitSingularUInt32Field(value: _storage._width, fieldNumber: 2) + } + if _storage._height != 0 { + try visitor.visitSingularUInt32Field(value: _storage._height, fieldNumber: 3) + } + if !_storage._image.isEmpty { + try visitor.visitSingularBytesField(value: _storage._image, fieldNumber: 4) + } + if _storage._seq != 0 { + try visitor.visitSingularUInt32Field(value: _storage._seq, fieldNumber: 5) + } + if _storage._timestampUs != 0 { + try visitor.visitSingularUInt64Field(value: _storage._timestampUs, fieldNumber: 6) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Image, rhs: Android_Emulation_Control_Image) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._format != rhs_storage._format {return false} + if _storage._width != rhs_storage._width {return false} + if _storage._height != rhs_storage._height {return false} + if _storage._image != rhs_storage._image {return false} + if _storage._seq != rhs_storage._seq {return false} + if _storage._timestampUs != rhs_storage._timestampUs {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Rotation: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Rotation" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}rotation\0\u{1}xAxis\0\u{1}yAxis\0\u{1}zAxis\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.rotation) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.xAxis) }() + case 3: try { try decoder.decodeSingularDoubleField(value: &self.yAxis) }() + case 4: try { try decoder.decodeSingularDoubleField(value: &self.zAxis) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.rotation != .portrait { + try visitor.visitSingularEnumField(value: self.rotation, fieldNumber: 1) + } + if self.xAxis.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.xAxis, fieldNumber: 2) + } + if self.yAxis.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.yAxis, fieldNumber: 3) + } + if self.zAxis.bitPattern != 0 { + try visitor.visitSingularDoubleField(value: self.zAxis, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Rotation, rhs: Android_Emulation_Control_Rotation) -> Bool { + if lhs.rotation != rhs.rotation {return false} + if lhs.xAxis != rhs.xAxis {return false} + if lhs.yAxis != rhs.yAxis {return false} + if lhs.zAxis != rhs.zAxis {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Rotation.SkinRotation: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0PORTRAIT\0\u{1}LANDSCAPE\0\u{1}REVERSE_PORTRAIT\0\u{1}REVERSE_LANDSCAPE\0") +} + +nonisolated extension Android_Emulation_Control_PhoneCall: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PhoneCall" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}operation\0\u{1}number\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.operation) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.number) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.operation != .initCall { + try visitor.visitSingularEnumField(value: self.operation, fieldNumber: 1) + } + if !self.number.isEmpty { + try visitor.visitSingularStringField(value: self.number, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_PhoneCall, rhs: Android_Emulation_Control_PhoneCall) -> Bool { + if lhs.operation != rhs.operation {return false} + if lhs.number != rhs.number {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_PhoneCall.Operation: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0InitCall\0\u{1}AcceptCall\0\u{1}RejectCallExplicit\0\u{1}RejectCallBusy\0\u{1}DisconnectCall\0\u{1}PlaceCallOnHold\0\u{1}TakeCallOffHold\0") +} + +nonisolated extension Android_Emulation_Control_PhoneResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PhoneResponse" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}response\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.response) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.response != .ok { + try visitor.visitSingularEnumField(value: self.response, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_PhoneResponse, rhs: Android_Emulation_Control_PhoneResponse) -> Bool { + if lhs.response != rhs.response {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_PhoneResponse.Response: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}BadOperation\0\u{1}BadNumber\0\u{1}InvalidAction\0\u{1}ActionFailed\0\u{1}RadioOff\0") +} + +nonisolated extension Android_Emulation_Control_Entry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Entry" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}key\0\u{1}value\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.key) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.key.isEmpty { + try visitor.visitSingularStringField(value: self.key, fieldNumber: 1) + } + if !self.value.isEmpty { + try visitor.visitSingularStringField(value: self.value, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Entry, rhs: Android_Emulation_Control_Entry) -> Bool { + if lhs.key != rhs.key {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_EntryList: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EntryList" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}entry\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.entry) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.entry.isEmpty { + try visitor.visitRepeatedMessageField(value: self.entry, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_EntryList, rhs: Android_Emulation_Control_EntryList) -> Bool { + if lhs.entry != rhs.entry {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_EmulatorStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EmulatorStatus" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{1}uptime\0\u{1}booted\0\u{1}vmConfig\0\u{1}hardwareConfig\0\u{1}heartbeat\0\u{1}guestConfig\0\u{1}platformConfig\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.version) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.uptime) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.booted) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._vmConfig) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._hardwareConfig) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &self.heartbeat) }() + case 7: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.guestConfig) }() + case 8: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.platformConfig) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.version.isEmpty { + try visitor.visitSingularStringField(value: self.version, fieldNumber: 1) + } + if self.uptime != 0 { + try visitor.visitSingularUInt64Field(value: self.uptime, fieldNumber: 2) + } + if self.booted != false { + try visitor.visitSingularBoolField(value: self.booted, fieldNumber: 3) + } + try { if let v = self._vmConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._hardwareConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + if self.heartbeat != 0 { + try visitor.visitSingularUInt64Field(value: self.heartbeat, fieldNumber: 6) + } + if !self.guestConfig.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.guestConfig, fieldNumber: 7) + } + if !self.platformConfig.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.platformConfig, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_EmulatorStatus, rhs: Android_Emulation_Control_EmulatorStatus) -> Bool { + if lhs.version != rhs.version {return false} + if lhs.uptime != rhs.uptime {return false} + if lhs.booted != rhs.booted {return false} + if lhs._vmConfig != rhs._vmConfig {return false} + if lhs._hardwareConfig != rhs._hardwareConfig {return false} + if lhs.heartbeat != rhs.heartbeat {return false} + if lhs.guestConfig != rhs.guestConfig {return false} + if lhs.platformConfig != rhs.platformConfig {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_AudioFormat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AudioFormat" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}samplingRate\0\u{1}channels\0\u{1}format\0\u{1}mode\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.samplingRate) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.channels) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.format) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.mode) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.samplingRate != 0 { + try visitor.visitSingularUInt64Field(value: self.samplingRate, fieldNumber: 1) + } + if self.channels != .mono { + try visitor.visitSingularEnumField(value: self.channels, fieldNumber: 2) + } + if self.format != .audFmtU8 { + try visitor.visitSingularEnumField(value: self.format, fieldNumber: 3) + } + if self.mode != .modeUnspecified { + try visitor.visitSingularEnumField(value: self.mode, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_AudioFormat, rhs: Android_Emulation_Control_AudioFormat) -> Bool { + if lhs.samplingRate != rhs.samplingRate {return false} + if lhs.channels != rhs.channels {return false} + if lhs.format != rhs.format {return false} + if lhs.mode != rhs.mode {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_AudioFormat.SampleFormat: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0AUD_FMT_U8\0\u{1}AUD_FMT_S16\0") +} + +nonisolated extension Android_Emulation_Control_AudioFormat.Channels: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0Mono\0\u{1}Stereo\0") +} + +nonisolated extension Android_Emulation_Control_AudioFormat.DeliveryMode: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0MODE_UNSPECIFIED\0\u{1}MODE_REAL_TIME\0") +} + +nonisolated extension Android_Emulation_Control_AudioPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AudioPacket" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}format\0\u{1}timestamp\0\u{1}audio\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._format) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.timestamp) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.audio) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._format { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.timestamp != 0 { + try visitor.visitSingularUInt64Field(value: self.timestamp, fieldNumber: 2) + } + if !self.audio.isEmpty { + try visitor.visitSingularBytesField(value: self.audio, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_AudioPacket, rhs: Android_Emulation_Control_AudioPacket) -> Bool { + if lhs._format != rhs._format {return false} + if lhs.timestamp != rhs.timestamp {return false} + if lhs.audio != rhs.audio {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_MicrophoneState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MicrophoneState" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}realAudioEnabled\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.realAudioEnabled) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.realAudioEnabled != false { + try visitor.visitSingularBoolField(value: self.realAudioEnabled, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_MicrophoneState, rhs: Android_Emulation_Control_MicrophoneState) -> Bool { + if lhs.realAudioEnabled != rhs.realAudioEnabled {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_SmsMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SmsMessage" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}srcAddress\0\u{1}text\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.srcAddress) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.srcAddress.isEmpty { + try visitor.visitSingularStringField(value: self.srcAddress, fieldNumber: 1) + } + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_SmsMessage, rhs: Android_Emulation_Control_SmsMessage) -> Bool { + if lhs.srcAddress != rhs.srcAddress {return false} + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_DisplayConfiguration: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DisplayConfiguration" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}width\0\u{1}height\0\u{1}dpi\0\u{1}flags\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.width) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.height) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.dpi) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.flags) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.width != 0 { + try visitor.visitSingularUInt32Field(value: self.width, fieldNumber: 1) + } + if self.height != 0 { + try visitor.visitSingularUInt32Field(value: self.height, fieldNumber: 2) + } + if self.dpi != 0 { + try visitor.visitSingularUInt32Field(value: self.dpi, fieldNumber: 3) + } + if self.flags != 0 { + try visitor.visitSingularUInt32Field(value: self.flags, fieldNumber: 4) + } + if self.display != 0 { + try visitor.visitSingularUInt32Field(value: self.display, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_DisplayConfiguration, rhs: Android_Emulation_Control_DisplayConfiguration) -> Bool { + if lhs.width != rhs.width {return false} + if lhs.height != rhs.height {return false} + if lhs.dpi != rhs.dpi {return false} + if lhs.flags != rhs.flags {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_DisplayConfiguration.DisplayFlags: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DISPLAYFLAGS_UNSPECIFIED\0\u{1}VIRTUAL_DISPLAY_FLAG_PUBLIC\0\u{1}VIRTUAL_DISPLAY_FLAG_PRESENTATION\0\u{2}\u{2}VIRTUAL_DISPLAY_FLAG_SECURE\0\u{2}\u{4}VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY\0\u{2}\u{8}VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR\0") +} + +nonisolated extension Android_Emulation_Control_DisplayConfigurations: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DisplayConfigurations" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}displays\0\u{1}userConfigurable\0\u{1}maxDisplays\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.displays) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.userConfigurable) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.maxDisplays) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.displays.isEmpty { + try visitor.visitRepeatedMessageField(value: self.displays, fieldNumber: 1) + } + if self.userConfigurable != 0 { + try visitor.visitSingularUInt32Field(value: self.userConfigurable, fieldNumber: 2) + } + if self.maxDisplays != 0 { + try visitor.visitSingularUInt32Field(value: self.maxDisplays, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_DisplayConfigurations, rhs: Android_Emulation_Control_DisplayConfigurations) -> Bool { + if lhs.displays != rhs.displays {return false} + if lhs.userConfigurable != rhs.userConfigurable {return false} + if lhs.maxDisplays != rhs.maxDisplays {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Notification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Notification" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\u{2}cameraNotification\0\u{1}displayConfigurationsChangedNotification\0\u{1}posture\0\u{1}booted\0\u{1}brightness\0\u{1}textViewFocus\0\u{1}xrOptions\0\u{1}microphoneState\0\u{1}ledIndicator\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 2: try { + var v: Android_Emulation_Control_CameraNotification? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .cameraNotification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .cameraNotification(v) + } + }() + case 3: try { + var v: Android_Emulation_Control_DisplayConfigurationsChangedNotification? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .displayConfigurationsChangedNotification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .displayConfigurationsChangedNotification(v) + } + }() + case 4: try { + var v: Android_Emulation_Control_Posture? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .posture(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .posture(v) + } + }() + case 5: try { + var v: Android_Emulation_Control_BootCompletedNotification? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .booted(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .booted(v) + } + }() + case 6: try { + var v: Android_Emulation_Control_BrightnessValue? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .brightness(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .brightness(v) + } + }() + case 7: try { + var v: Android_Emulation_Control_TextViewFocus? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .textViewFocus(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .textViewFocus(v) + } + }() + case 8: try { + var v: Android_Emulation_Control_XrOptions? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .xrOptions(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .xrOptions(v) + } + }() + case 9: try { + var v: Android_Emulation_Control_MicrophoneState? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .microphoneState(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .microphoneState(v) + } + }() + case 10: try { + var v: Android_Emulation_Control_LedIndicator? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .ledIndicator(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .ledIndicator(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.type { + case .cameraNotification?: try { + guard case .cameraNotification(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .displayConfigurationsChangedNotification?: try { + guard case .displayConfigurationsChangedNotification(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .posture?: try { + guard case .posture(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .booted?: try { + guard case .booted(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .brightness?: try { + guard case .brightness(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .textViewFocus?: try { + guard case .textViewFocus(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case .xrOptions?: try { + guard case .xrOptions(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() + case .microphoneState?: try { + guard case .microphoneState(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() + case .ledIndicator?: try { + guard case .ledIndicator(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Notification, rhs: Android_Emulation_Control_Notification) -> Bool { + if lhs.type != rhs.type {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_BootCompletedNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".BootCompletedNotification" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}time\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.time) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.time != 0 { + try visitor.visitSingularInt32Field(value: self.time, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_BootCompletedNotification, rhs: Android_Emulation_Control_BootCompletedNotification) -> Bool { + if lhs.time != rhs.time {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_CameraNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CameraNotification" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}active\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.active) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.active != false { + try visitor.visitSingularBoolField(value: self.active, fieldNumber: 1) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_CameraNotification, rhs: Android_Emulation_Control_CameraNotification) -> Bool { + if lhs.active != rhs.active {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_TextViewFocus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TextViewFocus" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}textViewHasFocus\0\u{1}display\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.textViewHasFocus) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.display) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.textViewHasFocus != false { + try visitor.visitSingularBoolField(value: self.textViewHasFocus, fieldNumber: 1) + } + if self.display != 0 { + try visitor.visitSingularInt32Field(value: self.display, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_TextViewFocus, rhs: Android_Emulation_Control_TextViewFocus) -> Bool { + if lhs.textViewHasFocus != rhs.textViewHasFocus {return false} + if lhs.display != rhs.display {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_DisplayConfigurationsChangedNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DisplayConfigurationsChangedNotification" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}displayConfigurations\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._displayConfigurations) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._displayConfigurations { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_DisplayConfigurationsChangedNotification, rhs: Android_Emulation_Control_DisplayConfigurationsChangedNotification) -> Bool { + if lhs._displayConfigurations != rhs._displayConfigurations {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_RotationRadian: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RotationRadian" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}x\0\u{1}y\0\u{1}z\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularFloatField(value: &self.x) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.y) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.z) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.x.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.x, fieldNumber: 1) + } + if self.y.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.y, fieldNumber: 2) + } + if self.z.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.z, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_RotationRadian, rhs: Android_Emulation_Control_RotationRadian) -> Bool { + if lhs.x != rhs.x {return false} + if lhs.y != rhs.y {return false} + if lhs.z != rhs.z {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Translation: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Translation" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}delta_x\0\u{3}delta_y\0\u{3}delta_z\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularFloatField(value: &self.deltaX) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.deltaY) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.deltaZ) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.deltaX.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.deltaX, fieldNumber: 1) + } + if self.deltaY.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.deltaY, fieldNumber: 2) + } + if self.deltaZ.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.deltaZ, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Translation, rhs: Android_Emulation_Control_Translation) -> Bool { + if lhs.deltaX != rhs.deltaX {return false} + if lhs.deltaY != rhs.deltaY {return false} + if lhs.deltaZ != rhs.deltaZ {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_AngularVelocity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AngularVelocity" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}omega_x\0\u{3}omega_y\0\u{3}omega_z\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularFloatField(value: &self.omegaX) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.omegaY) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.omegaZ) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.omegaX.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.omegaX, fieldNumber: 1) + } + if self.omegaY.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.omegaY, fieldNumber: 2) + } + if self.omegaZ.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.omegaZ, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_AngularVelocity, rhs: Android_Emulation_Control_AngularVelocity) -> Bool { + if lhs.omegaX != rhs.omegaX {return false} + if lhs.omegaY != rhs.omegaY {return false} + if lhs.omegaZ != rhs.omegaZ {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Velocity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Velocity" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}x\0\u{1}y\0\u{1}z\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularFloatField(value: &self.x) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.y) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.z) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.x.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.x, fieldNumber: 1) + } + if self.y.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.y, fieldNumber: 2) + } + if self.z.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.z, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Velocity, rhs: Android_Emulation_Control_Velocity) -> Bool { + if lhs.x != rhs.x {return false} + if lhs.y != rhs.y {return false} + if lhs.z != rhs.z {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Posture: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Posture" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\u{3}value\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 3: try { try decoder.decodeSingularEnumField(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.value != .postureUnknown { + try visitor.visitSingularEnumField(value: self.value, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Posture, rhs: Android_Emulation_Control_Posture) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Posture.PostureValue: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0POSTURE_UNKNOWN\0\u{1}POSTURE_CLOSED\0\u{1}POSTURE_HALF_OPENED\0\u{1}POSTURE_OPENED\0\u{1}POSTURE_FLIPPED\0\u{1}POSTURE_TENT\0\u{1}POSTURE_MAX\0") +} + +nonisolated extension Android_Emulation_Control_PhoneNumber: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PhoneNumber" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}number\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.number) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.number.isEmpty { + try visitor.visitSingularStringField(value: self.number, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_PhoneNumber, rhs: Android_Emulation_Control_PhoneNumber) -> Bool { + if lhs.number != rhs.number {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Environment: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Environment" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}environment\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.environment) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.environment.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.environment, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Environment, rhs: Android_Emulation_Control_Environment) -> Bool { + if lhs.environment != rhs.environment {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_Camera: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Camera" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}display_name\0\u{1}id\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.displayName) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.id) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.displayName.isEmpty { + try visitor.visitSingularStringField(value: self.displayName, fieldNumber: 1) + } + if !self.id.isEmpty { + try visitor.visitSingularStringField(value: self.id, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_Camera, rhs: Android_Emulation_Control_Camera) -> Bool { + if lhs.displayName != rhs.displayName {return false} + if lhs.id != rhs.id {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +nonisolated extension Android_Emulation_Control_CameraList: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CameraList" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}cameras\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.cameras) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.cameras.isEmpty { + try visitor.visitRepeatedMessageField(value: self.cameras, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Android_Emulation_Control_CameraList, rhs: Android_Emulation_Control_CameraList) -> Bool { + if lhs.cameras != rhs.cameras {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/NOTICE.md b/NOTICE.md index ca2e6f9..c5e22cd 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,47 +1,17 @@ # Notices and attribution -The source code and documentation authored for Mactician are available -under the repository's [MIT License](LICENSE). Third-party software, services, -names, and artwork retain their own terms; they are not relicensed by this -repository. - -## Sparkle - -The launcher integrates [Sparkle 2](https://github.com/sparkle-project/Sparkle) -version 2.9.4 for signed updates. The build downloads Sparkle's upstream binary -release and verifies its pinned SHA-256 before use. See the -[Sparkle license](https://github.com/sparkle-project/Sparkle/blob/2.x/LICENSE) -for its terms. Release builds also include the complete upstream license at -`Mactician.app/Contents/Resources/ThirdPartyLicenses/Sparkle-LICENSE.txt`. - -## Android components and graphics stack - -The installer downloads pinned Apple Silicon builds of Android Platform Tools, -Android Emulator, and the Google APIs Android 36 system image from Google's -Android repository. These packages are not stored in this repository. Their -archives carry upstream license and notice material; review that material after -download and the [Android SDK terms](https://developer.android.com/studio/terms) -before redistribution. - -The Android Emulator distribution used by this project contains or interacts -with upstream components including -[ANGLE](https://chromium.googlesource.com/angle/angle/), -[gfxstream](https://android.googlesource.com/platform/hardware/google/gfxstream/), -and [MoltenVK](https://github.com/KhronosGroup/MoltenVK). Exact component -notices are supplied by the pinned Emulator package; no single license is -asserted here for the combined distribution. +TFTMAC source and documentation are provided under the repository's [MIT License](LICENSE). Copyright and license notices applicable to retained or derived source remain in force. Third-party software, services, names, and artwork retain their own terms and are not relicensed by this repository. + +## Android and graphics components + +TFTMAC uses Google's stock Android Emulator and related Android platform components. These packages are installed outside the repository and carry their own upstream license and notice material. Review the applicable Android SDK terms before redistribution. + +The Android graphics path may include upstream components such as ANGLE, gfxstream, and MoltenVK. Their applicable notices are supplied by their upstream distributions. ## Riot Games and Teamfight Tactics -Teamfight Tactics, TFT, Riot Games, and related names and artwork belong to -their respective owners. The launcher artwork in -`launcher/Resources/MacticianHero.png`, icons, and any game packages -provided locally for a build are not covered by this repository's MIT grant. -This independent community project is not endorsed by or affiliated with Riot -Games. +Teamfight Tactics, TFT, Riot Games, and related names and artwork belong to their respective owners. TFTMAC does not distribute Riot application binaries and is not endorsed by or affiliated with Riot Games. ## Apple platforms -Apple, macOS, Apple Silicon, Metal, and related platform names are trademarks -of Apple Inc. Building and distributing a signed app requires the applicable -Apple developer tools and agreements. +Apple, macOS, Apple Silicon, Metal, and related platform names are trademarks of Apple Inc. Building and distributing a signed macOS application requires the applicable Apple developer tools and agreements. diff --git a/Probes/EmbeddedControlProbe/EmbeddedControlProbe.swift b/Probes/EmbeddedControlProbe/EmbeddedControlProbe.swift new file mode 100644 index 0000000..d4f72c5 --- /dev/null +++ b/Probes/EmbeddedControlProbe/EmbeddedControlProbe.swift @@ -0,0 +1,178 @@ +import Foundation +import GRPCCore +import GRPCNIOTransportHTTP2 +import GRPCProtobuf +import SwiftProtobuf + +// HISTORICAL DIAGNOSTIC ONLY. +// This probe starts the retired direct-control runtime and is intentionally not +// part of release verification. Native live acceptance is owned by TFTMAC.app. + +struct ProbeError: LocalizedError { + let message: String + init(_ message: String) { self.message = message } + var errorDescription: String? { message } +} + +struct ControlStart: Decodable { + let sessionId: String + let captureDir: String + let samplerPid: Int32 + let emulatorPid: Int32 + let sdkRoot: String + let avdHome: String? + let embeddedControl: Bool? +} + +struct DiscoveryRecord { + let path: String + let port: Int + let token: String +} + +@main +enum EmbeddedControlProbe { + static func main() async { + do { + let root = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + let node = URL(fileURLWithPath: "/opt/homebrew/bin/node") + let controlTool = root.appendingPathComponent("tools/tftmac-direct-control.mjs") + guard FileManager.default.isExecutableFile(atPath: node.path) else { throw ProbeError("Node.js is unavailable") } + guard FileManager.default.fileExists(atPath: controlTool.path) else { throw ProbeError("TFTMAC control tool is missing") } + + let startText = try run(node, [controlTool.path, "start-native-controller-probe"]) + let start = try JSONDecoder().decode(ControlStart.self, from: Data(startText.utf8)) + defer { _ = try? run(node, [controlTool.path, "stop"]) } + + guard start.embeddedControl == true else { throw ProbeError("Hidden embedded-control launch was not selected") } + let discovery = try await waitForDiscovery(pid: start.emulatorPid, avdHome: start.avdHome, captureDir: start.captureDir) + + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(address: "127.0.0.1", port: discovery.port), + transportSecurity: .plaintext + ) + let status: Android_Emulation_Control_EmulatorStatus = try await withGRPCClient(transport: transport) { grpc in + let client = Android_Emulation_Control_EmulatorController.Client(wrapping: grpc) + let request = GRPCCore.ClientRequest( + message: SwiftProtobuf.Google_Protobuf_Empty(), + metadata: ["authorization": "Bearer \(discovery.token)"] + ) + return try await client.getStatus( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer() + ) + } + + guard status.booted else { throw ProbeError("Authenticated getStatus reports Android not booted") } + guard status.vmConfig.numberOfCpuCores == 6 else { throw ProbeError("Unexpected CPU count: \(status.vmConfig.numberOfCpuCores)") } + let ramMiB = status.vmConfig.ramSizeBytes / 1024 / 1024 + guard ramMiB >= 5000 && ramMiB <= 5300 else { throw ProbeError("Unexpected guest RAM: \(ramMiB) MiB") } + guard status.version.contains("37.1.11") else { throw ProbeError("Unexpected emulator version: \(status.version)") } + + let evidence: [String: Any] = [ + "schema": 1, + "gate": 2, + "result": "PASS", + "hidden": true, + "authenticatedGetStatus": true, + "emulatorPid": start.emulatorPid, + "avd": "TFT_Ultra_Tablet", + "emulatorVersion": status.version, + "booted": status.booted, + "cpuCores": status.vmConfig.numberOfCpuCores, + "ramMiB": ramMiB, + "grpcPort": discovery.port, + "discoveryRecord": discovery.path, + "sessionId": start.sessionId, + "captureDir": start.captureDir + ] + try writeJSON(evidence, to: root.appendingPathComponent("ssot/native-app-probe-results.json")) + + let authority: [String: Any] = [ + "schema": 1, + "authority": "INSTALLED_EMULATOR_CONTROLLER", + "emulatorVersion": status.version, + "avd": "TFT_Ultra_Tablet", + "authenticated": true, + "loopbackOnly": true, + "discoveryRecord": discovery.path, + "tokenPersisted": false + ] + try writeJSON(authority, to: root.appendingPathComponent("ssot/emulator-controller-authority.json")) + print("TFTMAC Gate 2: PASS") + } catch { + fputs("TFTMAC Gate 2: FAIL: \(error.localizedDescription)\n", stderr) + exit(EXIT_FAILURE) + } + } + + static func waitForDiscovery(pid: Int32, avdHome: String?, captureDir: String) async throws -> DiscoveryRecord { + var home = FileManager.default.homeDirectoryForCurrentUser + if let marker = captureDir.range(of: "/Library/Application Support/TFTMAC/") { + home = URL(fileURLWithPath: String(captureDir[.. 0 else { continue } + guard let token = values["grpc.token"], !token.isEmpty else { throw ProbeError("Emulator registration has no grpc.token at \(candidate.path)") } + return DiscoveryRecord(path: candidate.path, port: port, token: token) + } + try await Task.sleep(for: .milliseconds(200)) + } + throw ProbeError("No emulator registration record appeared for PID \(pid)") + } + + static func parseINI(_ text: String) -> [String: String] { + var result: [String: String] = [:] + for line in text.split(whereSeparator: \.isNewline) { + let pair = line.split(separator: "=", maxSplits: 1).map(String.init) + if pair.count == 2 { result[pair[0].trimmingCharacters(in: .whitespaces)] = pair[1].trimmingCharacters(in: .whitespaces) } + } + return result + } + + static func writeJSON(_ object: [String: Any], to url: URL) throws { + let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]) + try data.write(to: url, options: .atomic) + } + + @discardableResult + static func run(_ executable: URL, _ arguments: [String]) throws -> String { + let process = Process() + process.executableURL = executable + process.arguments = arguments + let out = Pipe() + let err = Pipe() + process.standardOutput = out + process.standardError = err + try process.run() + let stdout = out.fileHandleForReading.readDataToEndOfFile() + let stderr = err.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + let outText = String(data: stdout, encoding: .utf8) ?? "" + let errText = String(data: stderr, encoding: .utf8) ?? "" + guard process.terminationStatus == 0 else { throw ProbeError(errText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? outText : errText) } + return outText + } +} diff --git a/README.md b/README.md index 7c3e2ab..ab08390 100644 --- a/README.md +++ b/README.md @@ -1,269 +1,117 @@ -# Mactician - -**A native, open-source TFT PBE launcher for Apple Silicon.** - -Mactician prepares and manages everything needed to play TFT PBE on a modern -Mac—without Android Studio, Terminal commands, or manual configuration. - -Created by [Sergei Naumov](https://sergeinaumov.dev/writing), a backend and -security platform engineer. Read my [technical writing](https://sergeinaumov.dev/writing/how-i-built-mactician) about this project -or connect with me on [LinkedIn](https://www.linkedin.com/in/sergei-naumov-dev/). +# TFTMAC -[Download Mactician](https://github.com/tweet9ra/mactician/releases/latest) · -[Documentation](#documentation) · -[Technical case study](https://sergeinaumov.dev/writing/how-i-built-mactician) +TFTMAC is a native macOS application for running the official Android Teamfight Tactics client on Apple Silicon through Google's stock Android Emulator. -Built for two tacticians. Shared with everyone. +## Current architecture -![Mactician social preview](branding/generated/mactician-social-preview.png) +```text +TFTMAC.app + -> AppKit window + -> Metal presentation layer + -> authenticated local EmulatorController + -> stock Google Android Emulator + -> official Google Play ARM64 guest + -> official Google Play TFT package + -> Riot authentication and content lifecycle +``` -## Project status +The product does not bundle, mirror, patch, re-sign, or privately update Riot binaries. Google Play is the installation/update authority for the Android application, and Riot's application owns its own content initialization. -- Version: **1.0.4** (build 40) -- Host architecture: **Apple Silicon (`arm64`)** -- Minimum deployment target: **macOS 12.0**, enforced by the build target and - runtime preflight -- Status: **experimental, best effort**; there is no support or compatibility - SLA -- Compatibility is pinned to TFT PBE `18.1-5212127`, Android Emulator 37.1.11, - and Android 36. A game or emulator update can require a new Mactician release. +## Runtime authority -## Preview +The working runtime is stored outside the repository under: -![Mactician running TFT PBE on macOS](docs/images/mactician-running-tft-pbe.webp) +```text +/Volumes/MAC MINI M4/TFTMAC/Runtime +``` -Mactician keeps launcher controls and the running TFT PBE window side by side. -The native SwiftUI interface is localized in English and Russian; game language -is configured independently. +The current stock emulator authority is Android Emulator 37.1.11. The exact installed EmulatorController protocol is vendored at `Vendor/AndroidEmulator/emulator_controller.proto` with provenance in `Vendor/AndroidEmulator/SOURCE.json`. -## Features +The source-built emulator laboratory is not part of the normal product path. +`flashls1/tftmac-runtime@c8aa26e` is eligible only as an isolated, +non-comparable diagnostic source runtime for the planned causal logger. -- Installs and verifies pinned Android Platform Tools, Emulator, and system - image archives. -- Verifies every downloaded component and bundled game split with SHA-256. -- Creates, provisions, starts, stops, repairs, and resets a dedicated AVD. -- Offers resolution, UI scale, Android RAM, vCPU, and game-language controls. -- Preserves the local Android runtime, Riot sign-in, game data, and launcher - preferences across full application updates. -- Repairs incomplete installs and a known zero-byte streaming-install cache - without clearing unrelated app data. -- Provides game hotkeys for shop, reroll, XP, item/trait and player/damage tabs, - plus the macOS window-fill shortcut. -- Uses a Sparkle appcast with Ed25519 archive verification for updates; the - current v1 release is ad-hoc signed and the workflow also supports future - Developer ID signing and notarization. -- Sends one unlinkable first-session event, offers separately consented extended - diagnostics, and can display validated operator messages. See - [Telemetry and privacy](docs/telemetry.md). +## Current handoff authority -## Requirements +- [`facts.md`](facts.md) — locked facts, current observations, verified results, and explicit unknowns. +- [`project.md`](project.md) — complete project history, architecture pivots, current Build 8 logger state, and continuity for a new chat. +- [`dev.md`](dev.md) — code ownership, experiment ledger, SQL contracts, hypotheses, and the next development gates. -### To run a release +Historical plans and benchmark records remain useful evidence, but they do not override these current boundaries or the machine-readable files under `ssot/`. The dated archive under `docs/history/2026-08-31-pre-build8/` is not current execution authority. -- An Apple Silicon Mac with macOS 12.0 or later. -- At least 16 GB of system memory; the launcher validates this preflight. -- At least 25 GiB of free disk space for downloads, extraction, the AVD, TFT - assets, and update headroom. -- Internet access to Google's Android repository and TFT services. -- Hypervisor Framework support, available on supported Apple Silicon Macs. -- Acceptance of the linked Android SDK terms during installation. -- Accessibility permission only if the built-in game hotkeys are used. The - emulator itself does not need this permission. +## Native build -### To build from source +Requirements: -Xcode Command Line Tools, zsh, `jq`, and four exact unmodified TFT PBE APK -splits matching the release manifest are required. Node.js is needed only for -the optional Keychain-backed login helper. Developer ID credentials, a -notarytool Keychain profile, and a Sparkle Ed25519 key are release-only -requirements. +- Apple Silicon Mac +- macOS 15 or later +- Xcode 26.6 +- Node.js 24 for repository tooling +- `jq`, `ripgrep`, and zsh -## Download and installation +Build: -When a public build is available, download the latest DMG from the repository's -[GitHub Releases page](https://github.com/tweet9ra/mactician/releases/latest). -Verify the version, build number, -and the SHA-256 published with that release before opening it. +```sh +/bin/zsh scripts/build-native-app.command +``` -1. Open the DMG and drag **Mactician** to **Applications**. -2. Open it. Version 1.0.4 is ad-hoc signed and not Apple-notarized, so macOS may - block the first launch. In **System Settings → Privacy & Security**, confirm - **Open Anyway**, then approve the second launch prompt. -3. Review and accept the Android SDK terms, then choose **Install**. About - 2.3 GB is downloaded before extraction and AVD provisioning. -4. Enter Riot credentials manually inside the official TFT client. +Test: -Mactician-managed data stays in -`$HOME/Library/Application Support/Mactician`. +```sh +/bin/zsh scripts/test-native-app.command +``` -**Repair Installation** re-verifies components, refreshes Mactician-owned -runtime scripts, and reprovisions missing pieces while preserving the AVD and -Riot/game state. **Reset** deletes the complete launcher-managed data directory, -including the AVD, sign-in state, and game data, after confirmation. - -## Build from source - -Keep the four pinned APK files outside Git and point the build at their -directory. Their names and hashes are recorded in -[`launcher/Resources/release-manifest.json`](launcher/Resources/release-manifest.json). - -### Unit tests and typecheck +Repository/CI validation (no installed app, private runtime, credentials, or +signing identity required): ```sh -./scripts/verify-repository.command -./scripts/test-mactician.command +/bin/zsh scripts/verify-tftmac.command ``` -The test script compiles unit tests for the current host architecture, checks C -syntax, validates release safeguards, runs the tests, and typechecks the full -Apple Silicon production source set. - -### Local ad-hoc build +Local installed-app/runtime/signing validation: ```sh -PROJECT_DIR="$PWD" -TFT_GAME_APK_DIR="$PROJECT_DIR/private/tft-pbe-apks" \ - ./scripts/build-mactician.command +/bin/zsh scripts/verify-installed-runtime.command ``` -This produces `dist/Mactician.app` and -`dist/Mactician-1.0.4.dmg`, signed ad hoc for local validation. +The latter currently reports the known missing local signing identity and +`CSSMERR_TP_NOT_TRUSTED`; it is intentionally not a CI dependency. -### Provisioning integration test +The native application bundle identifier is `com.flashls1.tftmac`. -After a local build: +## Runtime and package rules -```sh -./scripts/integration-test-mactician.command -``` +TFTMAC preserves the known-good stock SDK and AVD. Runtime state, Google credentials, Riot credentials, Android userdata, APK bytes, tokens, and private session data are never committed to Git. -This test downloads the large pinned Android archives and provisions temporary -data, so it is not run on every pull request. +The supported package is: -### Signed and notarized release - -```sh -PROJECT_DIR="$PWD" -: "${MACTICIAN_CODESIGN_IDENTITY:?Set MACTICIAN_CODESIGN_IDENTITY in the environment}" -: "${MACTICIAN_NOTARY_PROFILE:?Set MACTICIAN_NOTARY_PROFILE in the environment}" -TFT_GAME_APK_DIR="$PROJECT_DIR/private/tft-pbe-apks" \ - ./scripts/build-mactician.command +```text +com.riotgames.league.teamfighttactics ``` -Only environment-variable names belong in documentation or automation; never -commit identity secrets, passwords, private update keys, or notarization -credentials. See [Building](docs/building.md) and -[Releasing](docs/releasing.md) for the verified workflow. +Expected installer authority: -Developer ID signing and notarization are supported by the pipeline but are not -used for the initial ad-hoc v1 release. +```text +com.android.vending +``` -## How it works +If Google Play or Riot requires authentication, MFA, consent, or CAPTCHA, TFTMAC surfaces the official UI for the user to complete that step. -The game keeps its GLES interface while ANGLE and the emulator translate it to -Apple's graphics stack: +## Performance and diagnostics -```mermaid -flowchart LR - A["TFT GLES"] --> B["Android ANGLE"] - B --> C["Vulkan"] - C --> D["Android Emulator gfxstream / MoltenVK"] - D --> E["Apple Metal"] -``` +Performance work is evidence-driven. Build 8 automatically logs the TFT process/layer lifetime and has been live-verified. It proves exact gameplay cadence and degradation, but it does not yet name an internal graphics root. Source-level causal instrumentation is planned in an isolated diagnostic runtime, never by silently replacing the stock playable runtime. -The application owns orchestration and local state; Google's emulator owns the -host/guest boundary: +The current target is 1920x1080 at 60 Hz. High graphics at a 60 FPS cap with Riot Performance Mode OFF is the accepted playable baseline. Ultra High and Riot Performance Mode Beta were rejected on the target M4 host because of severe lag and unacceptable combat tails. -```mermaid -flowchart LR - A["SwiftUI launcher"] --> B["Installer and state machine"] - B --> C["Local runtime"] - C --> D["Emulator host"] - D --> E["Android guest"] -``` +## Project boundaries -## Performance research - -Only reproducible or explicitly qualified results are treated as conclusions: - -- In an exact stage-1-1 battle A/B, ASG measured **40.1 FPS / 34.85 ms p95** - versus **29.6 FPS / 49.75 ms p95** on the old pipe transport. -- The selected GPU-scene/RHI/MoltenVK stack measured **36.0–36.8 FPS** in the - later stage-1-5 scene. -- In a controlled stage-1-5 comparison, increasing source pixels from 1600×900 - to 2560×1440 by **2.56×** measured **30.5 versus 31.3 FPS**, showing no - material loss in that CPU/RHI-bound scene. This does not generalize to every - scene. -- MoltenVK-128 produced one promising **40.20 / 34.50 / 32.40 FPS** run at - stages 1-2/1-5/1-8, but cold confirmation fell to - **39.5 / 31.6 / 23.3 FPS**. It failed the reproducibility threshold and remains - experimental. - -See [Benchmarks](docs/benchmarks.md), -[Reproducibility](docs/reproducibility.md), and the -[Research log](docs/research-log.md) for methodology, rejected experiments, and -limitations. The focused -[native GLES and graphics-transport experiment](docs/native-gles-transport-experiment.md) -documents the shortened-path prototype, its ES 3.2 blockers, and the measured -current-path alternatives. - -## Privacy and security - -Runtime data, the AVD, downloads, and launcher logs remain local to the Mac. -After the first successfully completed game session, the launcher sends one -basic event containing a fresh event UUID, launcher version/build, calendar day, -and a rounded duration range. It contains no installation/device identifier, -exact duration or time, settings, language, or Mac characteristics. The metric -is reported as **Approximate activated installations**, not as users or people. - -With explicit opt-in only, every completed session also sends a separate -diagnostic event with exact duration, applied graphics/resource settings, Mac -model identifier, macOS version, total memory, and logical CPU count. Every -session has an independent event UUID and no installation ID. Turning the -setting off immediately deletes its local retry queue. Neither level contains a -Mac name, serial number, MAC address, Apple/Riot identity, logs, game state, or -AVD data. See [Telemetry and privacy](docs/telemetry.md) for the complete fields, -limitations, consent behavior, and retention periods. - -The same HTTPS API can return a title, text, and optional PNG/JPEG for a popup -when the launcher starts or a game closes. Responses, redirects, image origin, -encoded size, MIME type, dimensions, and pixel count are checked before remote -content is shown. - -Accessibility permission is used only for launcher hotkeys. A loopback-only -WebView DevTools connection performs a narrowly scoped login-field repaint -repair without reading or changing form values. Full raw game logs can contain -authentication-like data, so diagnostics must be filtered and sanitized before -sharing. Report vulnerabilities through the private process in -[SECURITY.md](SECURITY.md). - -## Documentation - -- [Architecture](docs/architecture.md) -- [Building](docs/building.md) -- [Releasing](docs/releasing.md) -- [Troubleshooting](docs/troubleshooting.md) -- [Benchmarks](docs/benchmarks.md) -- [Engineering case study](https://sergeinaumov.dev/writing/how-i-built-mactician) -- [Research log](docs/research-log.md) -- [Native GLES and graphics-transport experiment](docs/native-gles-transport-experiment.md) -- [Reproducibility](docs/reproducibility.md) -- [Launch profiles](docs/launch-profiles.md) -- [Telemetry and privacy](docs/telemetry.md) - -## Support - -See [SUPPORT.md](SUPPORT.md). - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md). +- One active product: TFTMAC. +- One authoritative repository: `flashls1/TFTMAC`. +- No legacy launcher, hosted game feed, private update service, or donor branding belongs in the shipping tree. +- No source-built emulator checkout is required for normal build, launch, test, repair, or release. ## License and attribution -Project code and documentation are available under the [MIT License](LICENSE). -Third-party notices and asset boundaries are documented in -[NOTICE.md](NOTICE.md). +Repository source is provided under [LICENSE](LICENSE). Third-party software and platform components retain their own licenses and terms; see [NOTICE.md](NOTICE.md). -Mactician is an independent community project and is not an official -Riot Games product. +Teamfight Tactics, TFT, Riot Games, Google, Android, Apple, macOS, Metal, and related names belong to their respective owners. TFTMAC is an independent project and is not endorsed by Riot Games, Google, or Apple. diff --git a/launcher/Resources/EmulatorHost-Info.plist b/RuntimeHost/Info.plist similarity index 71% rename from launcher/Resources/EmulatorHost-Info.plist rename to RuntimeHost/Info.plist index 99adaec..cf0c495 100644 --- a/launcher/Resources/EmulatorHost-Info.plist +++ b/RuntimeHost/Info.plist @@ -3,27 +3,27 @@ CFBundleDisplayName - Mactician Game Host + TFTMAC Emulator Host CFBundleExecutable - MacticianGameHost - CFBundleIconFile - EmulatorIcon.icns + TFTMACEmulatorHost CFBundleIdentifier - dev.sergeinaumov.mactician.game-host + com.flashls1.tftmac.emulator-host CFBundleInfoDictionaryVersion 6.0 CFBundleName - Mactician Game Host + TFTMAC Emulator Host CFBundlePackageType APPL CFBundleShortVersionString - 1.0.4 + 1.0.0 CFBundleVersion - 40 + 1 GCSupportsGameMode LSApplicationCategoryType public.app-category.games + LSUIElement + LSSupportsGameMode NSHighResolutionCapable diff --git a/RuntimeHost/main.c b/RuntimeHost/main.c new file mode 100644 index 0000000..453c8f3 --- /dev/null +++ b/RuntimeHost/main.c @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include +#include + +static const char *qos_name(qos_class_t qos_class) { + switch (qos_class) { + case QOS_CLASS_USER_INTERACTIVE: return "user_interactive"; + case QOS_CLASS_USER_INITIATED: return "user_initiated"; + case QOS_CLASS_DEFAULT: return "default"; + case QOS_CLASS_UTILITY: return "utility"; + case QOS_CLASS_BACKGROUND: return "background"; + default: return "unspecified"; + } +} + +int main(int argc, char *argv[]) { + const char *stdout_path = getenv("TFT_HOST_STDOUT"); + const char *stderr_path = getenv("TFT_HOST_STDERR"); + if (stdout_path != NULL && stdout_path[0] != '\0') { + (void)freopen(stdout_path, "a", stdout); + } + if (stderr_path != NULL && stderr_path[0] != '\0') { + (void)freopen(stderr_path, "a", stderr); + } + + const char *qos_request = getenv("TFT_HOST_LATENCY_QOS"); + const int latency_qos_requested = qos_request != NULL && strcmp(qos_request, "user_interactive") == 0; + const int qos_result = latency_qos_requested + ? pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0) + : 0; + qos_class_t effective_qos = QOS_CLASS_UNSPECIFIED; + int relative_priority = 0; + const int qos_read_result = pthread_get_qos_class_np( + pthread_self(), + &effective_qos, + &relative_priority + ); + if (qos_read_result != 0) { + effective_qos = QOS_CLASS_UNSPECIFIED; + relative_priority = 0; + } + dprintf(STDOUT_FILENO, "TFTMAC_HOST_QOS_REQUESTED=%s\n", + latency_qos_requested ? "user_interactive" : "default"); + dprintf(STDOUT_FILENO, "TFTMAC_HOST_QOS_SET_RESULT=%d\n", qos_result); + dprintf(STDOUT_FILENO, "TFTMAC_HOST_QOS_EFFECTIVE=%s\n", qos_name(effective_qos)); + dprintf(STDOUT_FILENO, "TFTMAC_HOST_QOS_RELATIVE_PRIORITY=%d\n", relative_priority); + + const char *emulator = getenv("TFT_EMULATOR"); + if (emulator == NULL || emulator[0] == '\0' || access(emulator, X_OK) != 0) { + fputs("TFTMAC Emulator Host could not find the Android Emulator executable.\n", stderr); + return EXIT_FAILURE; + } + + argv[0] = (char *)emulator; + execv(emulator, argv); + fprintf(stderr, "TFTMAC Emulator Host could not start Android Emulator: %s\n", strerror(errno)); + return EXIT_FAILURE; +} diff --git a/SECURITY.md b/SECURITY.md index 12ea8c8..f5bf7e9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,27 +1,11 @@ # Security policy -## Supported versions +Security fixes are handled on a best-effort basis for the current TFTMAC codebase. -Security fixes are provided on a best-effort basis for the latest published -Mactician release. Older releases may be asked to upgrade before a report is -investigated. +## Reporting -## Reporting a vulnerability +Use GitHub Private Vulnerability Reporting or a private Security Advisory for the repository. Do not disclose an unpatched vulnerability in a public issue. -Use GitHub Private Vulnerability Reporting or open a private Security Advisory -for this repository. Do not disclose an unpatched vulnerability in a normal -public issue. If private reporting is not available, email - with `Mactician security` in the subject. +Include the affected commit/version, macOS version and architecture, impact, reproduction steps, and the smallest proof needed to demonstrate the issue. Explain whether the issue affects the native application, stock emulator control, local runtime state, package-authority checks, or diagnostics. -Include the affected version, macOS version and architecture, impact, -reproduction steps, and the smallest proof needed to demonstrate the issue. -Explain whether the issue affects the launcher, installer, local runtime, -Sparkle update chain, or diagnostic tooling. - -Sanitize logs before attaching them: keep only the lines needed to show the -failure and remove usernames, home paths, tokens, cookies, credentials, device -identifiers, Riot session data, and unrelated process output. Never attach a -full AVD, raw game log, Keychain export, crash-memory dump, or private key. - -Receipt and remediation times depend on maintainer availability and issue -complexity; this policy does not promise a fixed response SLA. +Sanitize logs before attaching them. Remove usernames, home paths, tokens, cookies, credentials, device identifiers, Google/Riot session data, and unrelated process output. Never attach an AVD image, raw game log, Keychain export, crash-memory dump, or private key. diff --git a/SUPPORT.md b/SUPPORT.md index 23b9fac..83786fc 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,24 +1,17 @@ # Support -Mactician is maintained on a best-effort basis without a service-level -agreement. Support focuses on the latest Mactician release. TFT PBE and Android -Emulator updates can temporarily break compatibility with a pinned release. +TFTMAC is maintained on a best-effort basis for the current Apple Silicon build and stock Android runtime. -Use [GitHub Issues](https://github.com/tweet9ra/mactician/issues) for reproducible -Mactician bugs and feature requests. Personal -messages are not a support channel, and feature requests are not guaranteed to -be implemented. +For a useful bug report include: -A useful bug report includes: - -- launcher version and build number; -- macOS version, Mac model, and Apple chip; -- whether this was a fresh install, update, or existing installation; -- selected resolution, UI scale, RAM, vCPU, and game language; +- TFTMAC commit/version; +- macOS version and Mac model/chip; +- whether the stock emulator and AVD already existed; +- package version and installer authority when relevant; - expected and actual behavior; - minimal reproduction steps; -- a small, sanitized diagnostic excerpt. +- a small sanitized diagnostic excerpt. + +Do not publish credentials, tokens, Google or Riot account identifiers, private Android data, full AVD images, unfiltered game logs, or crash-memory dumps. -Do not publish credentials, tokens, private Android data, complete game logs, -full AVD images, or unfiltered crash memory. For security-sensitive reports, -follow [SECURITY.md](SECURITY.md) instead of opening a public issue. +Security-sensitive reports should follow [SECURITY.md](SECURITY.md). diff --git a/TFTMAC.md b/TFTMAC.md index f3d62bd..92762ee 100644 --- a/TFTMAC.md +++ b/TFTMAC.md @@ -1,54 +1,25 @@ # TFTMAC -TFTMAC runs the official live Teamfight Tactics Android client on Apple-silicon Macs using a local high-end Android tablet runtime. Riot's game binary is not modified, mirrored, or committed to Git. - -## Current working profile - -- Runtime: Google Play ARM64 Android 16 / API 36 -- Game package: `com.riotgames.league.teamfighttactics` -- Render target: **1920×1080** -- Density: **280 DPI** (617dp tablet-class short side) -- Guest resources: **8 vCPU / 8 GB RAM** -- GPU: host accelerated -- OpenGL ES: **3.2** (`196610`) -- EGL: **ANGLE** -- Vulkan: **1.3**, backed by Apple M4 through the Android emulator graphics stack -- Fallback: Compatibility 1080p without the enhanced ES 3.2 boot flags - -The installed application is `/Applications/TFTMAC.app`. Its persistent Android runtime is stored under `~/Library/Application Support/TFTMAC`. - -## Use - -Open **TFTMAC** from Applications. The launcher starts the high-end tablet runtime and opens live TFT automatically. - -- **Enhanced 1080p** is the default renderer profile. -- **Compatibility 1080p** is the fallback if a future Android/Riot update rejects the enhanced renderer flags. -- **Google Play / Update** opens Riot's official TFT listing inside the Android runtime. -- **Stop Android** cleanly shuts down the virtual tablet. - -Google credentials are entered only inside Google Play. Riot credentials are entered only inside TFT. - -## Trust boundary - -TFTMAC keeps the acquisition and game runtime local: - -1. Google Play installs Riot's official package. -2. The acquisition helper verifies Google Play installer authority and records SHA-256 evidence locally. -3. Riot APKs, Android userdata, credentials, and AVD disks remain under ignored/protected local paths and are never committed. -4. The Mac launcher controls emulator lifecycle, display geometry, resource allocation, and graphics capability only. - -## Development gates - -```text -node tools/clara-task.mjs test -node tools/clara-task.mjs build -node tools/clara-task.mjs acceptance -``` - -`acceptance` installs the runtime under Application Support, installs `TFTMAC.app` into Applications, launches it, and requires live TFT to start with the enhanced 1080p contract before succeeding. - -## Unreal transition - -The original Mactician PBE implementation is intentionally preserved as donor/reference code. When Riot's live Android client exposes Unreal runtime evidence, TFTMAC can activate a separately validated `UnrealEnhancedAdapter` using the donor ANGLE/MoltenVK/DeviceProfiles research. Until then, the working native live path remains isolated and is never replaced by an unproven Unreal overlay. - -See `docs/TFTMAC_GRAPHICS_ARCHITECTURE.md` for the graphics and fallback design. +TFTMAC is a native macOS application that launches and presents the official +Google Play Teamfight Tactics Android client through Google's stock Android +Emulator on Apple silicon. + +## Current status + +- Normal-play authority: installed TFTMAC 2.3.0 build 8 using stock Android + Emulator 37.1.11, High graphics, 60 FPS, and Riot Performance Mode OFF. Its + hashes match the historical release receipt; current-host signing trust is + separately blocked by the absent local identity. +- Automatic graphics logging is live-verified for the TFT process/layer + lifetime. It does not require match markers or a combat classifier. +- The current automatic evidence proves gameplay frame degradation but cannot + name an internal graphics owner. Source-level causal instrumentation is + planned; it is not implemented or claimed as complete. +- The final Mac presenter is retained only as a hidden correctness receipt. It + is not a graphics optimization target or a root-cause candidate. + +Read [README.md](README.md) first, then [facts.md](facts.md), +[benchmark.md](benchmark.md), [dev.md](dev.md), and [project.md](project.md). +The prior document is preserved at +`docs/history/2026-08-31-pre-build8/TFTMAC.md`; it is historical and must not +override those authorities. diff --git a/TFTMAC.xcodeproj/project.pbxproj b/TFTMAC.xcodeproj/project.pbxproj new file mode 100644 index 0000000..667a8b3 --- /dev/null +++ b/TFTMAC.xcodeproj/project.pbxproj @@ -0,0 +1,255 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 100000000000000000000001 /* TFTMACApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000001 /* TFTMACApplication.swift */; }; + 100000000000000000000002 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000002 /* AppCoordinator.swift */; }; + 100000000000000000000003 /* MainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000003 /* MainWindowController.swift */; }; + 100000000000000000000004 /* EmbeddedEmulatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000004 /* EmbeddedEmulatorView.swift */; }; + 100000000000000000000005 /* ViewportMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000005 /* ViewportMapper.swift */; }; + 100000000000000000000006 /* EmbeddedControlProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000006 /* EmbeddedControlProbe.swift */; }; + 100000000000000000000007 /* TFTMACGate1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000007 /* TFTMACGate1Tests.swift */; }; + 100000000000000000000008 /* ViewportMapper.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000005 /* ViewportMapper.swift */; }; + 100000000000000000000011 /* GRPCCore in Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000001 /* GRPCCore */; }; + 100000000000000000000012 /* GRPCNIOTransportHTTP2 in Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000002 /* GRPCNIOTransportHTTP2 */; }; + 100000000000000000000013 /* GRPCProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000003 /* GRPCProtobuf */; }; + 100000000000000000000014 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000004 /* SwiftProtobuf */; }; + 100000000000000000000015 /* emulator_controller.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000009 /* emulator_controller.pb.swift */; }; + 100000000000000000000016 /* emulator_controller.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000010 /* emulator_controller.grpc.swift */; }; + 100000000000000000000017 /* emulator_controller.pb.swift in Probe */ = {isa = PBXBuildFile; fileRef = 200000000000000000000009 /* emulator_controller.pb.swift */; }; + 100000000000000000000018 /* emulator_controller.grpc.swift in Probe */ = {isa = PBXBuildFile; fileRef = 200000000000000000000010 /* emulator_controller.grpc.swift */; }; + 100000000000000000000019 /* GRPCCore in Probe Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000001 /* GRPCCore */; }; + 10000000000000000000001A /* GRPCNIOTransportHTTP2 in Probe Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000002 /* GRPCNIOTransportHTTP2 */; }; + 10000000000000000000001B /* GRPCProtobuf in Probe Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000003 /* GRPCProtobuf */; }; + 10000000000000000000001C /* SwiftProtobuf in Probe Frameworks */ = {isa = PBXBuildFile; productRef = D00000000000000000000004 /* SwiftProtobuf */; }; + 100000000000000000000020 /* FrameContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000014 /* FrameContract.swift */; }; + 100000000000000000000021 /* TFTMACRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000015 /* TFTMACRuntime.swift */; }; + 100000000000000000000022 /* FrameContract.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000014 /* FrameContract.swift */; }; + 100000000000000000000023 /* AVDTransactionGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000016 /* AVDTransactionGuard.swift */; }; + 100000000000000000000024 /* AVDTransactionGuard.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000016 /* AVDTransactionGuard.swift */; }; + 100000000000000000000025 /* RuntimeProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000017 /* RuntimeProfile.swift */; }; + 100000000000000000000026 /* RuntimeProfile.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000017 /* RuntimeProfile.swift */; }; + 100000000000000000000027 /* RuntimeSettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000018 /* RuntimeSettingsWindowController.swift */; }; + 100000000000000000000028 /* RuntimeLease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000019 /* RuntimeLease.swift */; }; + 100000000000000000000029 /* RuntimeLease.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000019 /* RuntimeLease.swift */; }; + 10000000000000000000002A /* TelemetrySignalClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001A /* TelemetrySignalClassifier.swift */; }; + 10000000000000000000002B /* TelemetrySignalClassifier.swift in Tests */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001A /* TelemetrySignalClassifier.swift */; }; + 10000000000000000000002C /* GameFrameTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001B /* GameFrameTelemetry.swift */; }; + 10000000000000000000002D /* GameFrameTelemetry.swift in Tests */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001B /* GameFrameTelemetry.swift */; }; + 10000000000000000000002E /* GameFrameTelemetryTests.swift in Tests */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001C /* GameFrameTelemetryTests.swift */; }; + 10000000000000000000002F /* TouchInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001D /* TouchInput.swift */; }; + 100000000000000000000030 /* TouchInput.swift in Tests */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001D /* TouchInput.swift */; }; + 100000000000000000000031 /* CombatBenchmarkAnalysis.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001E /* CombatBenchmarkAnalysis.swift */; }; + 100000000000000000000032 /* CombatBenchmarkAnalysis.swift in Tests */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001E /* CombatBenchmarkAnalysis.swift */; }; + 100000000000000000000033 /* CombatBenchmarkStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001F /* CombatBenchmarkStore.swift */; }; + 100000000000000000000034 /* CombatBenchmarkAnalysisTests.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000020 /* CombatBenchmarkAnalysisTests.swift */; }; + 100000000000000000000035 /* GraphicsStackReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000021 /* GraphicsStackReceipt.swift */; }; + 100000000000000000000036 /* GraphicsStackReceipt.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000021 /* GraphicsStackReceipt.swift */; }; + 100000000000000000000037 /* GraphicsStackReceiptTests.swift in Tests */ = {isa = PBXBuildFile; fileRef = 200000000000000000000022 /* GraphicsStackReceiptTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 200000000000000000000001 /* TFTMACApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFTMACApplication.swift; sourceTree = ""; }; + 200000000000000000000002 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 200000000000000000000003 /* MainWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindowController.swift; sourceTree = ""; }; + 200000000000000000000004 /* EmbeddedEmulatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedEmulatorView.swift; sourceTree = ""; }; + 200000000000000000000005 /* ViewportMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewportMapper.swift; sourceTree = ""; }; + 200000000000000000000006 /* EmbeddedControlProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedControlProbe.swift; sourceTree = ""; }; + 200000000000000000000007 /* TFTMACGate1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFTMACGate1Tests.swift; sourceTree = ""; }; + 200000000000000000000008 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 200000000000000000000009 /* emulator_controller.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = emulator_controller.pb.swift; sourceTree = ""; }; + 200000000000000000000010 /* emulator_controller.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = emulator_controller.grpc.swift; sourceTree = ""; }; + 200000000000000000000011 /* TFTMAC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TFTMAC.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 200000000000000000000012 /* EmbeddedControlProbe */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = EmbeddedControlProbe; sourceTree = BUILT_PRODUCTS_DIR; }; + 200000000000000000000013 /* TFTMACTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TFTMACTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 200000000000000000000014 /* FrameContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameContract.swift; sourceTree = ""; }; + 200000000000000000000015 /* TFTMACRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFTMACRuntime.swift; sourceTree = ""; }; + 200000000000000000000016 /* AVDTransactionGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVDTransactionGuard.swift; sourceTree = ""; }; + 200000000000000000000017 /* RuntimeProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeProfile.swift; sourceTree = ""; }; + 200000000000000000000018 /* RuntimeSettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSettingsWindowController.swift; sourceTree = ""; }; + 200000000000000000000019 /* RuntimeLease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeLease.swift; sourceTree = ""; }; + 20000000000000000000001A /* TelemetrySignalClassifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetrySignalClassifier.swift; sourceTree = ""; }; + 20000000000000000000001B /* GameFrameTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameFrameTelemetry.swift; sourceTree = ""; }; + 20000000000000000000001C /* GameFrameTelemetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameFrameTelemetryTests.swift; sourceTree = ""; }; + 20000000000000000000001D /* TouchInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchInput.swift; sourceTree = ""; }; + 20000000000000000000001E /* CombatBenchmarkAnalysis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombatBenchmarkAnalysis.swift; sourceTree = ""; }; + 20000000000000000000001F /* CombatBenchmarkStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombatBenchmarkStore.swift; sourceTree = ""; }; + 200000000000000000000020 /* CombatBenchmarkAnalysisTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombatBenchmarkAnalysisTests.swift; sourceTree = ""; }; + 200000000000000000000021 /* GraphicsStackReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicsStackReceipt.swift; sourceTree = ""; }; + 200000000000000000000022 /* GraphicsStackReceiptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicsStackReceiptTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 300000000000000000000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 100000000000000000000011 /* GRPCCore in Frameworks */, + 100000000000000000000012 /* GRPCNIOTransportHTTP2 in Frameworks */, + 100000000000000000000013 /* GRPCProtobuf in Frameworks */, + 100000000000000000000014 /* SwiftProtobuf in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 300000000000000000000002 /* Frameworks */ = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (100000000000000000000019, 10000000000000000000001A, 10000000000000000000001B, 10000000000000000000001C,); runOnlyForDeploymentPostprocessing = 0; }; + 300000000000000000000003 /* Frameworks */ = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 400000000000000000000001 = { + isa = PBXGroup; + children = ( + 400000000000000000000002 /* TFTMAC */, + 40000000000000000000000A /* Generated */, + 400000000000000000000005 /* Probes */, + 400000000000000000000007 /* Tests */, + 400000000000000000000009 /* Products */, + ); + sourceTree = ""; + }; + 400000000000000000000002 /* TFTMAC */ = { + isa = PBXGroup; + children = ( + 400000000000000000000003 /* App */, + 400000000000000000000004 /* Presentation */, + 40000000000000000000000C /* Runtime */, + 200000000000000000000008 /* Info.plist */, + ); + path = TFTMAC; + sourceTree = ""; + }; + 400000000000000000000003 /* App */ = { + isa = PBXGroup; + children = ( + 200000000000000000000001 /* TFTMACApplication.swift */, + 200000000000000000000002 /* AppCoordinator.swift */, + 200000000000000000000003 /* MainWindowController.swift */, + 200000000000000000000018 /* RuntimeSettingsWindowController.swift */, + ); + path = App; + sourceTree = ""; + }; + 400000000000000000000004 /* Presentation */ = { + isa = PBXGroup; + children = ( + 200000000000000000000004 /* EmbeddedEmulatorView.swift */, + 200000000000000000000005 /* ViewportMapper.swift */, + 200000000000000000000014 /* FrameContract.swift */, + ); + path = Presentation; + sourceTree = ""; + }; + 40000000000000000000000C /* Runtime */ = {isa = PBXGroup; children = (200000000000000000000017 /* RuntimeProfile.swift */, 200000000000000000000019 /* RuntimeLease.swift */, 20000000000000000000001A /* TelemetrySignalClassifier.swift */, 20000000000000000000001B /* GameFrameTelemetry.swift */, 200000000000000000000021 /* GraphicsStackReceipt.swift */, 20000000000000000000001E /* CombatBenchmarkAnalysis.swift */, 20000000000000000000001F /* CombatBenchmarkStore.swift */, 20000000000000000000001D /* TouchInput.swift */, 200000000000000000000015 /* TFTMACRuntime.swift */, 200000000000000000000016 /* AVDTransactionGuard.swift */,); path = Runtime; sourceTree = ""; }; + 40000000000000000000000A /* Generated */ = {isa = PBXGroup; children = (40000000000000000000000B /* EmulatorController */,); path = Generated; sourceTree = ""; }; + 40000000000000000000000B /* EmulatorController */ = {isa = PBXGroup; children = (200000000000000000000009 /* emulator_controller.pb.swift */, 200000000000000000000010 /* emulator_controller.grpc.swift */,); path = EmulatorController; sourceTree = ""; }; + 400000000000000000000005 /* Probes */ = {isa = PBXGroup; children = (400000000000000000000006 /* EmbeddedControlProbe */,); path = Probes; sourceTree = ""; }; + 400000000000000000000006 /* EmbeddedControlProbe */ = {isa = PBXGroup; children = (200000000000000000000006 /* EmbeddedControlProbe.swift */,); path = EmbeddedControlProbe; sourceTree = ""; }; + 400000000000000000000007 /* Tests */ = {isa = PBXGroup; children = (400000000000000000000008 /* TFTMACTests */,); path = Tests; sourceTree = ""; }; + 400000000000000000000008 /* TFTMACTests */ = {isa = PBXGroup; children = (200000000000000000000007 /* TFTMACGate1Tests.swift */, 20000000000000000000001C /* GameFrameTelemetryTests.swift */, 200000000000000000000020 /* CombatBenchmarkAnalysisTests.swift */, 200000000000000000000022 /* GraphicsStackReceiptTests.swift */,); path = TFTMACTests; sourceTree = ""; }; + 400000000000000000000009 /* Products */ = {isa = PBXGroup; children = (200000000000000000000011 /* TFTMAC.app */, 200000000000000000000012 /* EmbeddedControlProbe */, 200000000000000000000013 /* TFTMACTests.xctest */,); name = Products; sourceTree = ""; }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 500000000000000000000001 /* TFTMAC */ = { + isa = PBXNativeTarget; + buildConfigurationList = 900000000000000000000002 /* Build configuration list for PBXNativeTarget "TFTMAC" */; + buildPhases = (700000000000000000000001 /* Sources */, 300000000000000000000001 /* Frameworks */, 800000000000000000000001 /* Resources */); + buildRules = (); + dependencies = (); + name = TFTMAC; + packageProductDependencies = (D00000000000000000000001 /* GRPCCore */, D00000000000000000000002 /* GRPCNIOTransportHTTP2 */, D00000000000000000000003 /* GRPCProtobuf */, D00000000000000000000004 /* SwiftProtobuf */); + productName = TFTMAC; + productReference = 200000000000000000000011 /* TFTMAC.app */; + productType = "com.apple.product-type.application"; + }; + 500000000000000000000002 /* EmbeddedControlProbe */ = { + isa = PBXNativeTarget; + buildConfigurationList = 900000000000000000000003 /* Build configuration list for PBXNativeTarget "EmbeddedControlProbe" */; + buildPhases = (700000000000000000000002 /* Sources */, 300000000000000000000002 /* Frameworks */); + buildRules = (); + dependencies = (); + name = EmbeddedControlProbe; + packageProductDependencies = (D00000000000000000000001 /* GRPCCore */, D00000000000000000000002 /* GRPCNIOTransportHTTP2 */, D00000000000000000000003 /* GRPCProtobuf */, D00000000000000000000004 /* SwiftProtobuf */); + productName = EmbeddedControlProbe; + productReference = 200000000000000000000012 /* EmbeddedControlProbe */; + productType = "com.apple.product-type.tool"; + }; + 500000000000000000000003 /* TFTMACTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 900000000000000000000004 /* Build configuration list for PBXNativeTarget "TFTMACTests" */; + buildPhases = (700000000000000000000003 /* Sources */, 300000000000000000000003 /* Frameworks */); + buildRules = (); + dependencies = (); + name = TFTMACTests; + productName = TFTMACTests; + productReference = 200000000000000000000013 /* TFTMACTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 600000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = {BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 2600;}; + buildConfigurationList = 900000000000000000000001 /* Build configuration list for PBXProject "TFTMAC" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = (en, Base); + mainGroup = 400000000000000000000001; + packageReferences = (A00000000000000000000001 /* XCRemoteSwiftPackageReference "grpc-swift-2" */, A00000000000000000000002 /* XCRemoteSwiftPackageReference "grpc-swift-nio-transport" */, A00000000000000000000003 /* XCRemoteSwiftPackageReference "grpc-swift-protobuf" */, A00000000000000000000004 /* XCRemoteSwiftPackageReference "swift-protobuf" */); + productRefGroup = 400000000000000000000009 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = (500000000000000000000001 /* TFTMAC */, 500000000000000000000002 /* EmbeddedControlProbe */, 500000000000000000000003 /* TFTMACTests */); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 800000000000000000000001 /* Resources */ = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 700000000000000000000001 /* Sources */ = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (100000000000000000000001, 100000000000000000000002, 100000000000000000000003, 100000000000000000000027, 100000000000000000000004, 100000000000000000000005, 100000000000000000000020, 100000000000000000000021, 100000000000000000000023, 100000000000000000000025, 100000000000000000000028, 10000000000000000000002A, 10000000000000000000002C, 100000000000000000000035, 10000000000000000000002F, 100000000000000000000031, 100000000000000000000033, 100000000000000000000015, 100000000000000000000016,); runOnlyForDeploymentPostprocessing = 0; }; + 700000000000000000000002 /* Sources */ = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (100000000000000000000006, 100000000000000000000017, 100000000000000000000018,); runOnlyForDeploymentPostprocessing = 0; }; + 700000000000000000000003 /* Sources */ = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (100000000000000000000007, 10000000000000000000001C, 100000000000000000000008, 100000000000000000000022, 100000000000000000000024, 100000000000000000000026, 100000000000000000000029, 10000000000000000000002B, 10000000000000000000002D, 10000000000000000000002E, 100000000000000000000036, 100000000000000000000030, 100000000000000000000032, 100000000000000000000034, 100000000000000000000037,); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + B00000000000000000000001 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {CLANG_ENABLE_MODULES = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; SWIFT_VERSION = 6.0;}; name = Debug; }; + B00000000000000000000002 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {CLANG_ENABLE_MODULES = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 6.0;}; name = Release; }; + B00000000000000000000011 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {ARCHS = arm64; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 8; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = TFTMAC/Info.plist; MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = "-lsqlite3"; PRODUCT_BUNDLE_IDENTIFIER = com.flashls1.tftmac; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0;}; name = Debug; }; + B00000000000000000000012 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {ARCHS = arm64; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 8; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = TFTMAC/Info.plist; MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = "-lsqlite3"; PRODUCT_BUNDLE_IDENTIFIER = com.flashls1.tftmac; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0;}; name = Release; }; + B00000000000000000000021 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {ARCHS = arm64; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0;}; name = Debug; }; + B00000000000000000000022 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {ARCHS = arm64; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0;}; name = Release; }; + B00000000000000000000031 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {ARCHS = arm64; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = com.flashls1.tftmac.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0;}; name = Debug; }; + B00000000000000000000032 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {ARCHS = arm64; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = com.flashls1.tftmac.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0;}; name = Release; }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 900000000000000000000001 /* Build configuration list for PBXProject "TFTMAC" */ = {isa = XCConfigurationList; buildConfigurations = (B00000000000000000000001, B00000000000000000000002,); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 900000000000000000000002 /* Build configuration list for PBXNativeTarget "TFTMAC" */ = {isa = XCConfigurationList; buildConfigurations = (B00000000000000000000011, B00000000000000000000012,); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 900000000000000000000003 /* Build configuration list for PBXNativeTarget "EmbeddedControlProbe" */ = {isa = XCConfigurationList; buildConfigurations = (B00000000000000000000021, B00000000000000000000022,); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 900000000000000000000004 /* Build configuration list for PBXNativeTarget "TFTMACTests" */ = {isa = XCConfigurationList; buildConfigurations = (B00000000000000000000031, B00000000000000000000032,); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + A00000000000000000000001 /* XCRemoteSwiftPackageReference "grpc-swift-2" */ = {isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/grpc/grpc-swift-2.git"; requirement = {kind = exactVersion; version = 2.4.2;}; }; + A00000000000000000000002 /* XCRemoteSwiftPackageReference "grpc-swift-nio-transport" */ = {isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/grpc/grpc-swift-nio-transport.git"; requirement = {kind = exactVersion; version = 2.9.1;}; }; + A00000000000000000000003 /* XCRemoteSwiftPackageReference "grpc-swift-protobuf" */ = {isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/grpc/grpc-swift-protobuf.git"; requirement = {kind = exactVersion; version = 2.4.1;}; }; + A00000000000000000000004 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; requirement = {kind = exactVersion; version = 1.38.1;}; }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D00000000000000000000001 /* GRPCCore */ = {isa = XCSwiftPackageProductDependency; package = A00000000000000000000001 /* XCRemoteSwiftPackageReference "grpc-swift-2" */; productName = GRPCCore; }; + D00000000000000000000002 /* GRPCNIOTransportHTTP2 */ = {isa = XCSwiftPackageProductDependency; package = A00000000000000000000002 /* XCRemoteSwiftPackageReference "grpc-swift-nio-transport" */; productName = GRPCNIOTransportHTTP2; }; + D00000000000000000000003 /* GRPCProtobuf */ = {isa = XCSwiftPackageProductDependency; package = A00000000000000000000003 /* XCRemoteSwiftPackageReference "grpc-swift-protobuf" */; productName = GRPCProtobuf; }; + D00000000000000000000004 /* SwiftProtobuf */ = {isa = XCSwiftPackageProductDependency; package = A00000000000000000000004 /* XCRemoteSwiftPackageReference "swift-protobuf" */; productName = SwiftProtobuf; }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 600000000000000000000001 /* Project object */; +} diff --git a/TFTMAC.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TFTMAC.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4eca42f --- /dev/null +++ b/TFTMAC.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,204 @@ +{ + "originHash" : "9afaf1a1d30e374e2a7b5940d1cc3b2b2293103c55b34e9bc2cf060b4ab63c18", + "pins" : [ + { + "identity" : "grpc-swift-2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-2.git", + "state" : { + "revision" : "28cdd63ef88583ddc67d7bb179eab46fab465ce9", + "version" : "2.4.2" + } + }, + { + "identity" : "grpc-swift-nio-transport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-nio-transport.git", + "state" : { + "revision" : "c46f77c07c3ae4fce4734e7af03fcb35ab544428", + "version" : "2.9.1" + } + }, + { + "identity" : "grpc-swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-protobuf.git", + "state" : { + "revision" : "176c5a434fd76f6f479848d1a8f7d44967534168", + "version" : "2.4.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", + "version" : "1.7.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c", + "version" : "1.1.5" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "449dbbecd0f31e82b510ada227ca152caa8b5e98", + "version" : "1.19.4" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "47d3869a7291f085c1fb9fb1e6d3b97a793f45c6", + "version" : "4.5.1" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "933538faa42c432d385f02e07df0ace7c5ecfc47", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "db774a277f60063a32d854f2980299caf06da041", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "3ffafb9722d5d918c614feb496c8789a3b59d222", + "version" : "1.15.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "0b18836bd8b0162e7e17a995a3fbee20ed8f3b2b", + "version" : "2.101.3" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "88a51340f59cf181ebde888bd1b749296b3ec029", + "version" : "1.34.3" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "45bdf670248be5f16ec0340e125dca285536f0fb", + "version" : "1.45.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "d930168b86f46ca51a4bc09c5ca45c1833db8067", + "version" : "2.37.2" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "67787bb645a5e67d2edcdfbe48a216cc549222d5", + "version" : "1.28.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8", + "version" : "1.38.1" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "7f9326b0326ff86e3646295ea6e891f68c471c5e", + "version" : "2.12.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", + "version" : "1.8.1" + } + } + ], + "version" : 3 +} diff --git a/TFTMAC.xcodeproj/xcshareddata/xcschemes/TFTMAC.xcscheme b/TFTMAC.xcodeproj/xcshareddata/xcschemes/TFTMAC.xcscheme new file mode 100644 index 0000000..8f45553 --- /dev/null +++ b/TFTMAC.xcodeproj/xcshareddata/xcschemes/TFTMAC.xcscheme @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TFTMACAPP.md b/TFTMACAPP.md index 7851eb3..d077297 100644 --- a/TFTMACAPP.md +++ b/TFTMACAPP.md @@ -1,2048 +1,15 @@ -# TFTMACAPP.md — Native macOS TFTMAC Full Implementation Plan +# TFTMAC Native App Status -**Status:** AUTHORITATIVE NATIVE-APP IMPLEMENTATION PLAN -**Project:** TFTMAC -**Target:** Apple Silicon / Mac mini M4 -**Product:** One native macOS application containing the complete Android/TFT user experience -**Primary workload:** Current official Google Play Teamfight Tactics client -**Presentation target:** Full 1920×1080 Android framebuffer, native Mac window and native macOS fullscreen -**Performance target:** Stable 60-Hz-class presentation with truthful FPS/frame telemetry -**Architecture rule:** Preserve the proven stock Android Emulator graphics/runtime path; replace only the user-facing wrapper/presentation/control layer unless measurement proves a deeper runtime blocker. +The native AppKit/Metal application is implemented and Build 8 is the current +normal-play authority. The application launches its packaged emulator host in +the logged-in macOS session, keeps the emulator window hidden, presents the +official Android client fullscreen, and uses authenticated local +EmulatorController control. ---- +Current work is advanced graphics diagnosis, not a replacement of the native +app or a direct Node/Clara emulator launch. Build 8 logging is automatic and +verified; internal source-level causal attribution remains planned. -## 0. Executive implementation directive - -Build **one real native Mac application, `TFTMAC.app`**, that launches the proven Android runtime invisibly, embeds the full Android display directly inside its own AppKit window, accepts normal Mac mouse/keyboard input, provides all normal emulator/device controls natively, manages Google Play/TFT lifecycle, exposes approved runtime settings, displays and records truthful FPS, keeps the continuous raw logger alive from before emulator launch through stop, seals raw data immediately at shutdown, and can optionally connect to legitimate Riot League Voice through the official League Client if Riot permits concurrent authentication. - -This file supersedes the old production direction based on: - -- a visible QEMU/Qt Android Emulator window; -- window repositioning, cropping, overlays, or Accessibility automation; -- `-scale` tricks to make the emulator fit the Mac screen; -- source-built AEMU as the normal critical path; -- API 37 as the required production guest; -- custom ANGLE as normal production work; -- scrcpy, video encoding, screenshots, or software streaming as the gameplay display path; -- Mactician's hosted TFT feed/updater/orchestration; -- third-party Riot APK distribution; -- per-match telemetry as the primary performance model. - -The current working stock runtime is the control. The native app must first reproduce it unchanged, then prove that embedding does not materially degrade it. - ---- - -# 1. Product definition - -The shipping user experience is: - -```text -Finder / Dock - -> TFTMAC.app - -> one normal native macOS window - -> full Android/TFT display inside the window - -> native toolbar / menus / optional Device Controls inspector - -> native Settings - -> live FPS overlay - -> official Google Play / Riot sign-in surfaces inside the embedded Android display - -> native macOS fullscreen -``` - -The user must never need to interact with a separate Android Emulator window in normal operation. - -The final product is not a skin around QEMU. QEMU remains the VM process, but TFTMAC owns the presentation, lifecycle, controls, telemetry, state, recovery, and native Mac UX. - ---- - -# 2. Current known-good runtime authority - -The native app begins from the runtime that has already completed real official TFT games on this machine. - -## 2.1 Host - -```text -Machine: Mac mini Mac16,10 -Chip: Apple M4 -Architecture: arm64 -Unified memory: 16 GB -macOS: 26.6.2 -Build: 25G83 -Authoritative Xcode: /Users/flash/Downloads/Xcode.app -Xcode: 26.6 / 17F113 -``` - -Do not allow a stale `/Applications/Xcode.app` selection to control production builds. Native build scripts set: - -```text -DEVELOPER_DIR=/Users/flash/Downloads/Xcode.app/Contents/Developer -``` - -unless machine-state discovery proves that this exact installation has intentionally changed. - -## 2.2 Android runtime - -```text -Bulk runtime root: /Volumes/MAC MINI M4/TFTMAC/Runtime -Android Emulator: 37.1.11.0 -Emulator build: 15917651 -ADB protocol: 1.0.41 -AVD: TFT_Ultra_Tablet -Guest family: official Google Play ARM64 API 36 -Guest display: 1920x1080 -Density: 320 dpi -Refresh target: 60 Hz -CPU baseline: 6 cores -Guest RAM baseline: 5120 MB / 5.0 GB -GPU mode: host -Audio backend: CoreAudio -Graphics transport: virtio-gpu-asg / gfxstream -ASG write buffer: 1 MiB -ASG write step: 16 KiB -ASG data ring: 32 KiB -ASG draw flush baseline: 800 -``` - -Current graphics execution path: - -```text -TFT / Unreal GameActivity - -> ANGLE - -> guest Vulkan/ranchu - -> virtio-gpu-asg / gfxstream - -> host Vulkan - -> MoltenVK - -> Metal - -> Apple M4 -``` - -Current compatibility adapter required by the proven build: - -```text -ANGLE_FEATURE_OVERRIDES_ENABLED= - exposeNonConformantExtensionsAndVersions:exposeES32ForTesting -``` - -This is recorded truthfully as a compatibility adapter. The native-app project does **not** reopen source-AEMU/custom-ANGLE work merely to remove it. A future conformance project may replace it only after the native product is stable and measurement proves that work is necessary. - -## 2.3 Current official TFT package - -Observed working package: - -```text -applicationId/package: com.riotgames.league.teamfighttactics -versionName observed: 18.1-5392842 -versionCode observed: 8392842 -installer observed: com.android.vending -launch activity: com.epicgames.unreal.GameActivity -``` - -The observed version is not permanently pinned. Google Play remains package/update authority. Every launch re-queries package state. - -The signing certificate digest has not yet been promoted as verified project authority. Native-app implementation must capture and record the Google-delivered signer digest before release rather than inventing or assuming it. - -## 2.4 Current performance decisions - -```text -Guest RAM 5.0 GB: KEEP as current development baseline -Guest RAM 4.0 GB: not the default; do not force a further RAM cut -CPU: 6 cores baseline -TFT graphics: Medium baseline -TFT FPS cap: 60 baseline -TFT Performance Mode (Beta): OFF baseline -Ultra High graphics: REJECTED on current stack for usability; user observed unplayable lag -ASG draw flush 800: baseline -ASG draw flush 400: experimental until controlled evidence promotes it -``` - -Do not silently default the app to Ultra High. Medium/60/OFF is the current playable in-game reference until better measured evidence exists. - ---- - -# 3. Non-negotiable architecture rules - -1. **Stock Google Android Emulator remains the normal runtime.** No mandatory source-built AEMU tree. -2. **Mactician remains untouched as donor/control evidence.** Do not make the shipping app depend on Mactician servers, feed, updater, or orchestration. -3. **Official Google Play/Riot package only.** Do not mirror, repack, patch, or re-sign Riot binaries. -4. **The Android source framebuffer stays 1920×1080.** Native window resizing changes presentation size, not guest resolution. -5. **No visible Qt/QEMU UI in production.** The emulator runs hidden. -6. **No scrcpy/video encode path.** Use EmulatorController raw frame transport and Metal. -7. **No fake fullscreen.** Use ordinary AppKit fullscreen/Spaces. -8. **No Accessibility permission for normal operation.** Input goes through EmulatorController/ADB fallbacks. -9. **Logger starts before emulator.** TFT does not launch if the critical logger gate cannot initialize. -10. **Continuous run is the performance source of truth.** Matches and setting changes are timestamped annotations. -11. **Raw telemetry is sealed before SQL/post-processing.** Analysis can fail without losing the run. -12. **One-variable experimentation.** No combinatorial tuning runs unless evidence requires them. -13. **No source-level zero-copy presentation bridge unless the simple MMAP/Metal path fails the measured performance gate.** -14. **League Voice cannot bypass Riot authentication or entitlement.** It is a separate conditional module and cannot block the core app release. - ---- - -# 4. Production build system - -The current ad-hoc `swiftc` wrapper build is not sufficient for the final embedded app. - -Create a normal Xcode project: - -```text -TFTMAC.xcodeproj -Product: TFTMAC.app -Architecture: arm64 -Minimum macOS: 15.0 -Swift language mode: Swift 6.x -Primary UI: AppKit -Optional hosted UI: SwiftUI where it reduces code, not for window control -Renderer: Metal / MetalKit -Distribution: Developer ID direct distribution -``` - -Why macOS 15.0: the maintained gRPC Swift 2 ecosystem is the intended client stack and currently targets modern macOS. The actual target machine is much newer, so lowering deployment compatibility is not worth complicating the embedded-control build. - -## 4.1 Dependencies - -Use maintained Swift packages only: - -```text -gRPC Swift 2.x -gRPC Swift NIO transport 2.x -gRPC Swift Protobuf 2.x -SwiftProtobuf 1.x -``` - -Implementation procedure: - -1. Resolve versions compatible with Xcode 26.6 and the vendored emulator proto. -2. Pin exact versions and checksums in `Package.resolved`. -3. Never ship floating `main`/branch dependencies. -4. Commit `Package.resolved`. -5. Treat dependency update as an explicit maintenance change, not an automatic build-time upgrade. - -## 4.2 Emulator protocol source - -Vendor the exact compatible EmulatorController protocol source: - -```text -Vendor/AndroidEmulator/emulator_controller.proto -Vendor/AndroidEmulator/SOURCE.json -Generated/EmulatorController/*.swift -``` - -`SOURCE.json` records: - -```text -installed emulator version/build -upstream source URL/revision -proto SHA-256 -generator versions -generated-source SHA-256 -``` - -Generate the Swift protobuf/gRPC client once through a reproducible script and commit the generated code. Normal end-user builds must not depend on downloading Android Emulator source or running an uncontrolled code generator. - -Required RPC capability set to prove against Emulator 37.1.11: - -```text -status / getStatus -raw screenshot/frame streaming -single screenshot -keyboard input -mouse/touch input -wheel input if supported -VM state / restart / shutdown -microphone state where exposed -optional notifications/device-control RPCs needed by Advanced Controls -``` - -Exact RPC names follow the vendored proto, not remembered examples. - ---- - -# 5. Repository structure - -Keep the app modular without creating a forest of frameworks. - -Use one production app target plus tests, one small developer probe target, and generated protocol code. - -```text -TFTMAC.xcodeproj -TFTMAC/ - App/ - TFTMACApplication.swift - AppCoordinator.swift - AppState.swift - MenuBuilder.swift - MainWindowController.swift - Runtime/ - RuntimeLocation.swift - RuntimeProfile.swift - RuntimeSettingDefinition.swift - RuntimeProfileStore.swift - RuntimeController.swift - EmulatorProcessController.swift - AndroidBootVerifier.swift - Emulator/ - EmulatorDiscoveryService.swift - EmulatorControllerClient.swift - EmulatorConnectionState.swift - FrameStreamSession.swift - Presentation/ - EmbeddedEmulatorView.swift - MetalFramePresenter.swift - FrameRing.swift - ViewportMapper.swift - FPSOverlayView.swift - Input/ - AndroidInputRouter.swift - KeyboardMapper.swift - PointerMapper.swift - DeviceControls/ - DeviceActionService.swift - DeviceControlsInspector.swift - AdvancedControlsController.swift - Package/ - PackageStateManager.swift - TFTPackageState.swift - PlayStoreCoordinator.swift - Telemetry/ - CaptureManager.swift - CaptureSeal.swift - FPSAccumulator.swift - HostSampler.swift - AndroidSampler.swift - SurfaceFlingerSampler.swift - TelemetryNormalizer.swift - Voice/ - LeagueVoiceProvider.swift - LeagueClientVoiceProvider.swift - LeagueVoiceState.swift - Resources/ -Generated/ - EmulatorController/ -Vendor/ - AndroidEmulator/ -Probes/ - EmbeddedControlProbe/ -Tests/ - TFTMACTests/ - TFTMACUITests/ -scripts/ - generate-emulator-proto.command - build-native-app.command - test-native-app.command - notarize-native-app.command -ssot/ - native-app-probe-results.json - native-app-release-checklist.md -``` - -Do not recreate a monolithic `TFTMACWindowCoordinator` that owns every responsibility. - -Existing Node tools remain diagnostic/control oracles during migration, but the final user-facing runtime must not require Node for display, input, normal lifecycle, settings, or telemetry capture. - ---- - -# 6. Native application state machine - -The app state must describe what the user can actually do. - -```text -BOOTSTRAPPING - -> EXTERNAL_RUNTIME_MISSING - -> RUNTIME_NEEDS_REPAIR - -> READY_TO_START - -READY_TO_START - -> STARTING_CAPTURE - -> STARTING_EMULATOR - -> ANDROID_BOOTING - -> ANDROID_LOCKED_OR_SETUP_REQUIRED - -> GOOGLE_PLAY_SIGNIN_REQUIRED - -> TFT_INSTALL_REQUIRED - -> TFT_UPDATE_REQUIRED - -> TFT_PATCHING_OR_INITIALIZING - -> READY_TO_PLAY - -READY_TO_PLAY - -> LAUNCHING_TFT - -> PLAYING - -PLAYING - -> RESTARTING_TFT - -> APPLYING_ANDROID_PROFILE - -> STOPPING - -> LOGGER_FAULT - -STOPPING - -> SEALING_CAPTURE - -> READY_TO_START -``` - -Error states expose one specific recovery action. Do not terminate with a generic failure when the package is missing, Play sign-in is needed, Android is locked, or Riot is patching. - ---- - -# 7. Runtime root and storage behavior - -Bulk runtime authority remains: - -```text -/Volumes/MAC MINI M4/TFTMAC/Runtime -``` - -Small native state remains: - -```text -~/Library/Application Support/TFTMAC/ - Profiles/ - State/ - Captures/ - Diagnostics/ - Logs/ - Rollback/ -``` - -Large capture traces may be stored on the external volume with an index under Application Support. - -Rules: - -- if `/Volumes/MAC MINI M4` is not mounted, do not start the emulator; -- never silently build a second SDK/AVD on the internal disk; -- verify the runtime root and AVD before launch; -- preserve the existing runtime instead of reinstalling it on every app build; -- first-run bootstrap only installs/repairs missing official components; -- Android SDK licenses are accepted through the normal Google tooling/user agreement path; -- Riot APKs and private AVD userdata never enter Git or the app bundle. - ---- - -# 8. Hidden Android Emulator launch - -Normal production launch is the current proven runtime plus embedded-control flags. - -Required additional flags/direction: - -```text --qt-hide-window --grpc-use-token --idle-grpc-timeout 300 -``` - -Use isolated ADB: - -```text -ANDROID_ADB_SERVER_PORT=5040 -``` - -Preserve the current renderer/audio/CPU/RAM/display/compatibility arguments from the active RuntimeProfile. - -Do not rely on: - -```text --scale -emulator-user.ini window placement -Finder desktop dimensions -Accessibility window APIs -off-screen QEMU positioning -cover windows / fake chrome -``` - -The app tracks the emulator process directly by PID and owns its lifecycle. - -## 8.1 gRPC discovery and authentication - -On each launch: - -1. Locate the registration/discovery record belonging to the new emulator process. -2. Read the actual local gRPC endpoint. -3. Read the token into memory only. -4. Determine the exact authentication-header form required by the installed emulator. -5. Call status/getStatus. -6. Verify the endpoint belongs to the expected emulator PID/AVD. -7. Store neither token nor auth header in logs, SQL, crash metadata, or profiles. -8. Discard credentials when the emulator terminates. - -A hidden emulator with no authenticated controller connection is a hard architecture failure. Do not continue building product UI around it. - ---- - -# 9. Android boot and Google Play preflight - -After process start, native TFTMAC verifies: - -```text -ADB serial present -boot_completed=1 -user 0 unlocked or actionable lock/setup state shown -1920x1080 display -320 dpi -expected CPU count -expected guest memory range -Google Play Store package present -Google Play Services present -network active -host/guest clock reasonably synchronized -automatic time enabled -automatic timezone enabled -``` - -Date/time is a first-class Play preflight because Play installation/auth failures can be caused by clock drift. - -Before opening Play installation/sign-in: - -- enable Android automatic time/time-zone settings when permitted; -- compare host UTC with guest UTC; -- if the guest is materially wrong, repair through the normal emulator/Android time path or restart the guest; -- do not proceed with Play account flow while the clock is obviously wrong. - -Do not automate Google credentials, CAPTCHA, consent, MFA, or Riot credentials. - ---- - -# 10. Official TFT package lifecycle - -`PackageStateManager` is a required native service. - -States: - -```text -MISSING -PLAY_AVAILABLE -INSTALLING -INSTALLED_UNKNOWN_VERSION -INSTALLED_VERIFIED -UPDATE_AVAILABLE -PATCHING_OR_INITIALIZING -READY_FOR_LAUNCH -PACKAGE_DAMAGED_OR_INCOMPLETE -``` - -Every package observation captures: - -```text -package/applicationId -versionName -versionCode -installer package -base APK path -split APK paths -first install time -last update time -launch activity -base/split SHA-256 when readable -signing certificate digest(s) -``` - -Verification rules: - -- package name must be `com.riotgames.league.teamfighttactics`; -- installer must be `com.android.vending` for the production authority path; -- versionName/versionCode are observed from the installed package, not hardcoded forever; -- signing digest is extracted from the actual Google-delivered APK set and stored as package evidence; -- on every update, re-record signer and hashes; -- a signer mismatch is a hard package-integrity warning requiring review; -- never re-sign or modify the package. - -If TFT is missing or out of date, TFTMAC opens the official Google Play surface **inside the embedded Android display**. The user signs into Play normally there. - -Once TFT is installed, TFTMAC launches the official activities and allows Riot's own patch/init flow to complete. Riot login remains inside the official game surface. - ---- - -# 11. Full embedded display architecture - -The user-facing Android display is a native `MTKView` or equivalent Metal-backed AppKit view. - -```text -Android Emulator hidden Qt UI - -> EmulatorController raw frame stream - -> MMAP/shared-memory source - -> bounded host copy - -> 2/3-frame Metal texture ring - -> MTKView - -> TFTMAC NSWindow -``` - -Initial stream request: - -```text -display: 0 -width: native / 0 where proto means native -height: native / 0 where proto means native -format: raw RGBA8888 or exact native raw format exposed by proto -transport: MMAP/shared memory -``` - -The app verifies the received source is 1920×1080 landscape. - -Do not use PNG, H.264, VP8, scrcpy, periodic screenshots, or Android software encoding as the primary display. - -## 11.1 Why the first implementation uses one copy - -A 1920×1080 RGBA frame is about 8.3 MB. Shared-memory producers can overwrite their region while a client is rendering. The safe first implementation copies each new complete frame into a bounded Metal presentation ring. - -This gives deterministic ownership and prevents half-written frames/tearing. - -Only if measured embedded-vs-direct A/B proves this single copy materially harms performance may the project activate a source-level IOSurface/zero-copy adapter. Do not build that preemptively. - -## 11.2 Metal presenter - -`MetalFramePresenter` must: - -- detect each new source sequence number; -- copy/swap into an available presentation buffer without blocking the frame-receive loop; -- use a bounded 2- or 3-buffer ring; -- present only complete new frames; -- retain source sequence/timestamp metadata with the presented frame; -- record receive, submit, and present/completion monotonic timestamps; -- avoid unnecessary color conversions; -- preserve normal sRGB-looking output unless measured source metadata requires another color space; -- never perform heavy telemetry/SQL work on the render thread. - ---- - -# 12. Full-size window, fit, 100%, and fullscreen behavior - -The source Android display always stays 1920×1080. - -Main `NSWindow` style: - -```text -.titled -.closable -.miniaturizable -.resizable -.fullSizeContentView -``` - -Required behavior: - -- real macOS red/yellow/green traffic lights; -- title `TFTMAC`; -- normal Dock identity; -- normal menu-bar identity; -- native unified/compact toolbar; -- `.fullScreenPrimary` collection behavior; -- green traffic-light fullscreen works; -- `View > Enter Full Screen` works; -- `Control-Command-F` works; -- no fake fullscreen or menu-bar/Dock hiding code. - -Display modes: - -```text -FIT - Aspect-fit the entire 1920x1080 source into available content. - Never crop or stretch. - -100% - One source pixel equals one view pixel. - If the window is smaller than 1920x1080, use a native scroll container rather than scaling/cropping silently. - -FULLSCREEN - Keep source at 1920x1080. - Aspect-fit into the native fullscreen content area. - On a 16:9 display, fill the complete content area without Qt/QEMU chrome. -``` - -Letterboxing is allowed only when the Mac content region is not 16:9. - ---- - -# 13. Native input system - -Normal gameplay input must not depend on fixed Android screen coordinates. - -Input mapping: - -```text -Mac event point - -> EmbeddedEmulatorView local point - -> actual displayed-image rectangle - -> normalized source coordinates - -> Android 1920x1080 coordinates - -> EmulatorController input RPC -``` - -Required input: - -```text -left click / touch down-up -drag / touch move -mouse movement where useful -wheel/scroll -keyboard keys -text entry -Back -Home -Overview -``` - -Rules: - -- ignore letterbox regions for Android touch; -- preserve Mac command shortcuts (`Command-*`) for TFTMAC menus; -- send normal unhandled gameplay keys to Android; -- keep input ordering deterministic; -- do not block input while telemetry writes; -- gRPC is primary for pointer/key transport where supported; -- isolated ADB is allowed as a service-control fallback, not as brittle coordinate automation. - ---- - -# 14. Native toolbar, menus, and Device Controls inspector - -The game gets the full content width by default. All emulator functionality remains available through native controls. - -Primary toolbar: - -```text -Back -Home -Overview -Device Controls toggle -Volume/Mute -Screenshot -Restart TFT -FPS indicator -League Voice -``` - -The Device Controls inspector opens on the right when requested and may contain: - -```text -Power / Wake -Back -Home -Overview -Rotate Left -Rotate Right -Volume Up -Volume Down -Mute -Screenshot -Fit -100% -Restart TFT -Restart Android -Pause/Resume VM where supported -Advanced Emulator Controls -``` - -When the inspector is visible, the Android view aspect-fits into the remaining content. It is never stretched. - -Application menus: - -```text -TFTMAC - About TFTMAC - Settings... - Quit TFTMAC - -File - Screenshot - Open Capture Folder - -View - Fit - 100% - Enter Full Screen - Show FPS Overlay - Show Device Controls - -Device - Power / Wake - Back - Home - Overview - Volume Up - Volume Down - Mute - Rotate Left - Rotate Right - Restart TFT - Restart Android - Advanced Emulator Controls... - -Voice - League Voice - Disconnect Voice - -Help -``` - -Every visible control receives an explicit release test. No button may silently do nothing. - ---- - -# 15. Advanced emulator controls - -Do not recreate Google's Qt Extended Controls UI pixel-for-pixel. Recreate the **functional capabilities** the native app may need. - -`AdvancedControlsController` capability-discovers the current EmulatorController and exposes supported controls such as: - -```text -location/GPS -battery state -cellular/network simulation -microphone state -clipboard -camera/sensor status where exposed -VM pause/resume/restart -additional display/device state available in the exact installed proto -``` - -For each action: - -1. Prefer the supported EmulatorController RPC. -2. Use a deterministic ADB fallback only if it is safer and already proven. -3. Hide/disable capabilities not exposed by the installed emulator rather than presenting dead controls. -4. Never require the hidden Qt toolbar. - ---- - -# 16. Runtime profile system - -Runtime tuning is a product feature, not a source-edit workflow. - -Persistent profiles live in: - -```text -~/Library/Application Support/TFTMAC/Profiles/ -``` - -Each `RuntimeProfile` is canonical JSON with a deterministic SHA-256 config hash. - -Minimum fields: - -```text -profileVersion -name -cpuCores -ramMB -guestWidth -guestHeight -densityDpi -refreshHz -gpuMode -audioBackend -graphicsTransport -asgWriteBuffer -asgWriteStep -asgDataRing -asgDrawFlushInterval -angleCompatibilityFeatures -configHash -``` - -Each session writes an immutable copy as `runtime-profile.json`. - -## 16.1 Declarative settings registry - -Each `RuntimeSettingDefinition` contains: - -```text -id -label -group -value type -allowed values/range -default -unit -apply mode -restart requirement -runtime mapping -validation rule -safety class -log key -help text -``` - -Apply modes: - -```text -LIVE -RESTART_TFT -RESTART_ANDROID -NEXT_SESSION -``` - -Safety badges: - -```text -BASELINE -VALIDATED -EXPERIMENTAL -UNSUPPORTED -``` - -## 16.2 CPU cores - -Expose: - -```text -2, 3, 4, 5, 6, 7, 8 -``` - -Default: `6` -Apply: `RESTART_ANDROID` - -Pre-release validation tests the CPU dimension one value at a time against the baseline profile. Do not test the Cartesian product of every CPU/RAM combination. - -## 16.3 Guest RAM - -Expose exactly: - -```text -4.0 -4.5 -5.0 -5.5 -6.0 -6.5 -7.0 -7.5 -8.0 GB -``` - -Default: `5.0 GB / 5120 MB` -Apply: `RESTART_ANDROID` - -The UI is simple: dropdown/stepper in 0.5-GB increments. - -Pre-release validation smoke-tests each value independently against the baseline CPU/graphics configuration. This proves launch compatibility without requiring nine full TFT matches. - -5.0 GB remains the performance baseline until evidence changes it. - -## 16.4 ASG draw flush interval - -Expose: - -```text -800 — BASELINE -400 — EXPERIMENTAL until promoted -``` - -Apply: `RESTART_ANDROID` - -Do not expose arbitrary ASG values in v1. - -## 16.5 Locked baseline values - -Show but do not make editable in the first release: - -```text -1920x1080 -320 dpi -60 Hz target -host GPU -virtio-gpu-asg -1 MiB ASG write buffer -16 KiB ASG write step -32 KiB ASG ring -CoreAudio -ANGLE compatibility feature set -MoltenVK configuration -``` - -New knobs are added only when measurement names a causal variable. - -## 16.6 Apply & Restart workflow - -When a restart-required value changes: - -1. Save the pending profile. -2. Mark `Restart required`. -3. User selects `Apply & Restart Android`. -4. Record `CONFIG_CHANGE_REQUESTED`. -5. Seal the current raw capture using the bounded shutdown protocol. -6. Stop Android cleanly. -7. Persist new profile and config hash. -8. Start a new logger/capture **before** the new emulator process. -9. Start hidden emulator with the new profile. -10. Verify requested vs observed CPU/RAM/config. -11. Record `CONFIG_CHANGE_APPLIED`. -12. Relaunch TFT when Android/package state is ready. - -One runtime configuration never silently mixes into another capture. - ---- - -# 17. TFT in-game graphics settings - -Known user-facing values: - -```text -Graphics: Low / Medium / High / Ultra High -FPS cap: 30 / 60 / None -Performance Mode (Beta): On / Off -``` - -Current baseline: - -```text -Medium / 60 / OFF -``` - -Current Ultra High verdict: - -```text -REJECTED for current usability because it produced unplayable lag. -``` - -Do not automate these with blind screen-coordinate taps. - -Implementation sequence: - -1. Inspect logcat and Riot/Unreal runtime output for deterministic setting-change events. -2. Inspect any stable local configuration/preferences interface legally and non-invasively accessible from the Play guest. -3. If a deterministic interface exists, expose the setting through TFTMAC and timestamp every apply automatically. -4. If no stable write interface exists but logcat exposes changes, observe and timestamp them automatically. -5. If neither exists, provide an explicit native `Game Settings Observation` control that records the user's declared current values without pretending TFTMAC applied them. - -Manual in-game setting changes may never be assigned a fabricated timestamp or value. - ---- - -# 18. FPS and presentation telemetry — required product feature - -`gfxinfo` is not the primary FPS source because TFT renders through a native Unreal/Vulkan SurfaceView path. - -The embedded frame stream supplies always-on presentation metrics. SurfaceFlinger/Perfetto calibrates and deepens them. - -Track separately: - -```text -emulator/source frame rate -frame stream delivery rate -Metal presented FPS -sequence gaps / stream drops -source frame intervals -source-to-receive latency -receive-to-submit latency -submit-to-present latency -source-to-present latency -Mac display refresh rate -``` - -## 18.1 User-visible FPS - -Default HUD: - -```text -FPS 59.8 -``` - -Default user number = distinct **Metal presented FPS**, because this is what the user actually sees in TFTMAC. - -Expanded diagnostic HUD: - -```text -FPS 59.8 -SRC 60.0 -DROP 0.0% -P95 17.4 ms -``` - -The overlay: - -- is native AppKit/Metal overlay content; -- is not rendered inside Android; -- does not alter guest resolution; -- does not intercept normal gameplay outside its own small bounds; -- is toggleable from `View > Show FPS Overlay`; -- persists the user's preference. - -## 18.2 FPS accumulator - -Every source frame captures: - -```text -source sequence -source timestampUs -host receive monotonic time -Metal submit monotonic time -Metal present/completion monotonic time when available -``` - -Maintain rolling windows: - -```text -1 second -5 seconds -30 seconds -``` - -Do not write one database row per rendered frame during gameplay. - -## 18.3 Raw `fps.jsonl` - -Append one compact record per second: - -```text -utc -host_mono_ns -source_fps_1s -source_fps_5s -presented_fps_1s -presented_fps_5s -source_frame_count -delivered_frame_count -presented_frame_count -stream_dropped_frame_count -stream_dropped_percent -source_frame_interval_mean_ms -source_frame_interval_p95_ms -source_to_present_mean_ms -source_to_present_p95_ms -display_refresh_hz -emulator_seq_first -emulator_seq_last -runtime_config_hash -``` - -## 18.4 Calibration gate - -Before calling emulator stream timestamps the authoritative **game/display source FPS**: - -1. Capture a stable TFT animation interval. -2. Record gRPC source seq/timestamps. -3. Simultaneously capture bounded SurfaceFlinger/FrameTimeline Perfetto evidence. -4. Compare frame count/timing. -5. Repeat during known heavy/stutter gameplay. - -If the streams track closely, source FPS is promoted. If not, SurfaceFlinger remains source/display-production authority and the gRPC source number is labeled transport/source-stream FPS. - -Do not lie with labels. - ---- - -# 19. Continuous raw telemetry architecture - -One emulator/logger start-to-stop interval is one authoritative continuous performance run. - -Matches, wins, placements, TFT restarts, quality reports, graphics changes, FPS-cap changes, Performance Mode changes, and traces are annotations inside that continuous timeline. - -No match-start marker is required for the run to be useful. - -Required capture root: - -```text -~/Library/Application Support/TFTMAC/Captures// -``` - -Required or attempted raw artifacts: - -```text -session.json -runtime-profile.json -runtime-state.json -package-state.json -renderer-state.json -clock-sync.jsonl -host-events.jsonl -markers.jsonl -fps.jsonl -embed-present.jsonl -host-process.jsonl -host-memory.jsonl -surfaceflinger/counters.jsonl -logcat.raw.txt -logcat.filtered.txt -emulator.stdout.log -emulator.stderr.log -perfetto/ # bounded traces when requested/needed -capture.seal.json -manifest.sha256 # post-seal integrity pass -``` - -Critical always-on streams: - -```text -FPS/presentation -host process/CPU/RSS -host memory/compression/swap/pageouts -Android/logcat -SurfaceFlinger miss counters -clock sync -runtime events -``` - -The native app owns enough telemetry directly that a played session cannot become totally valueless because a separate analysis command failed. - ---- - -# 20. Logger gate and fault behavior - -Before TFT launch, `CaptureManager` must prove: - -```text -session created -runtime profile frozen -host sampler advancing -host memory sampler advancing -frame/FPS telemetry ready once display stream starts -logcat collector available once Android is ready -capture directory writable -``` - -If the critical gate fails before gameplay, do not launch TFT. - -During gameplay: - -- restart a failed non-render sampler automatically once; -- record `LOGGER_STREAM_RESTARTED`; -- keep native FPS/presentation recording independent of logcat/process samplers; -- surface `LOGGER DEGRADED` if a critical stream cannot recover; -- never destroy the game merely because post-processing is unavailable. - ---- - -# 21. Bounded raw-first stop/seal protocol - -Shutdown must be fast and deterministic. It must never recursively inventory the Android SDK/AVD before protecting gameplay data. - -When Stop/Quit/Apply-Restart is requested: - -1. Record `STOP_REQUESTED` and final monotonic timestamp. -2. Stop accepting new experiment annotations. -3. Stop/flush critical telemetry producers. -4. `fsync`/close raw files where practical. -5. Write `session.json` final raw state. -6. Atomically write `capture.seal.json` containing: - - session id; - - start/end timestamps; - - exact runtime config hash; - - package identity; - - immutable raw file list; - - final byte counts; - - capture state `RAW_SEALED`; - - integrity state `PENDING`. -7. Once `capture.seal.json` exists, the raw run is protected and may not be rewritten. -8. Stop/terminate the emulator if the requested action requires it. -9. Compute SHA-256 integrity manifest **after** raw seal at background priority. -10. Normalize/ingest SQLite **after** raw seal. -11. Storage/BOM inventory, compression, long analysis, and report generation are post-processing only. - -A full SDK/AVD recursive size walk is explicitly forbidden on the critical stop path. - -If SHA/SQL/post-processing fails, raw capture remains `RAW_SEALED` and the app writes a secondary error artifact. - -If the app or Mac dies before normal seal, next launch recovers the abandoned `CAPTURING` directory as `RECOVERED_PARTIAL` without modifying existing raw bytes. - -For macOS application termination, use `applicationShouldTerminate` / terminate-later semantics so the app gets a short bounded seal opportunity rather than exiting mid-write. - ---- - -# 22. SQL performance laboratory integration - -Use SQLite as post-session normalized evidence, not as the high-rate capture transport. - -Add/maintain a time-series FPS table logically equivalent to: - -```sql -fps_samples( - session_id, - observed_utc, - host_mono_ns, - source_fps_1s, - source_fps_5s, - presented_fps_1s, - presented_fps_5s, - stream_dropped_frames, - stream_dropped_percent, - source_interval_mean_ms, - source_interval_p95_ms, - source_to_present_mean_ms, - source_to_present_p95_ms, - display_refresh_hz, - config_hash -) -``` - -Index: - -```text -(session_id, host_mono_ns) -``` - -Post-session rollups include: - -```text -source_fps_mean -source_fps_p5 -source_fps_p50 -source_fps_p95 -source_fps_min -presented_fps_mean -presented_fps_p5 -presented_fps_min -percent_time_below_60 -percent_time_below_50 -percent_time_below_30 -stream_drop_percent -source_to_present_p95_ms -longest_sub_30fps_interval_seconds -CPU mean/p95/max -RSS mean/p95/max -host available/compressed/swap -pageout delta/rate -SurfaceFlinger/HWC/GPU miss deltas -ANR/fatal/OOM/restart counts -``` - -Schema migrations are idempotent and tested against the current live performance DB. A normalization/migration failure must never invalidate raw capture sealing. - ---- - -# 23. Deep graphics diagnostics - -Always-on FPS tells us **that** a frame problem exists. It does not always tell us **why**. - -Keep bounded deep diagnostics separate: - -```text -SurfaceFlinger frame/frametimeline/layers -GPU memory -Perfetto process/system stats where needed -host CPU/memory -ASG/gfxstream instrumentation when needed -MoltenVK instrumentation when needed -``` - -Native trace actions remain developer diagnostics, not something every player session must run at maximum intensity. - -No source-built AEMU or custom renderer work is activated until these measurements identify that layer as the first causal blocker. - ---- - -# 24. Audio and microphone - -Core TFT audio remains the current proven explicit backend: - -```text -CoreAudio -``` - -Native app launch must preserve it and record the observed backend. - -Acceptance: - -```text -TFT audible through selected Mac output -no recurring emulator PCM I/O error -no recurring mixer underrun problem -audio survives native display embedding -audio survives fullscreen -audio survives TFT restart -``` - -Android volume controls alter Android audio state; native Mac system volume remains user-controlled by macOS. - -Microphone passthrough is implemented only where required by an Android feature or diagnostics. League Voice uses native/official League Client audio rather than routing voice through the Android guest. - ---- - -# 25. League Voice integration — separate conditional module - -Include a native `League Voice` toolbar/menu location from the beginning, but do not allow it to delay the core TFTMAC app. - -## 25.1 Allowed architecture - -Preferred path: - -```text -TFTMAC League Voice button - -> detect official Riot/League Client on Mac - -> user signs in through Riot's own UI - -> discover local League Client API endpoint - -> keep local credentials only in memory - -> dynamically verify current premade-voice capability - -> ask official League Client/plugin to join the account's legitimate party voice session - -> Riot/Vivox auth/media remains owned by Riot's client -``` - -Never: - -- fake Riot/Vivox tokens; -- hardcode private voice endpoints as permanent authority; -- persist LCU credentials; -- capture League passwords; -- bypass party entitlement; -- work around a Riot same-account concurrent-session restriction. - -## 25.2 Provider isolation - -Define: - -```swift -protocol LeagueVoiceProvider { - func probe() async -> LeagueVoiceCapability - func connect() async throws - func disconnect() async - func setSelfMuted(_ muted: Bool) async throws - func setParticipantVolume(id: String, value: Double) async throws -} -``` - -The rest of TFTMAC depends only on this interface. - -## 25.3 Mandatory feasibility gate - -Prove on the target Mac: - -```text -official League Client available -user authenticates only in Riot UI -local client API discoverable -premade voice endpoints currently exist -same user can remain authenticated in League Client while Android TFT is active -joining voice does not log TFT out -TFT activity does not tear down voice -Mac mic/output work -self mute works -participant mute/volume works -son/other party member on PC can hear user and user can hear them -``` - -The decisive gate is same-account concurrency. - -If Riot rejects concurrent session use: - -```text -League Voice = UNAVAILABLE — RIOT SESSION LIMIT -``` - -Ship TFTMAC without an authentication bypass. - ---- - -# 26. Security, privacy, and credential handling - -TFTMAC may handle local ephemeral control credentials but must not become a credential store. - -Rules: - -```text -Google credentials: only inside official Google Play Android UI -Riot credentials: only inside official Riot/TFT/League Client UI -emulator gRPC token: memory only, never logged -League Client local API token: memory only, never logged -Riot/Vivox voice token: never persist/log -Riot APKs: never commit/bundle/redistribute -AVD userdata: never commit -capture logs: redact obvious auth headers/tokens before filtered diagnostics -``` - -Bind emulator control to local loopback only. - -The native app should use Hardened Runtime and least required entitlements. Do not enable the App Sandbox if it prevents required child-process, external-volume, local gRPC, ADB, or runtime behavior; this is a direct Developer ID application, not a Mac App Store-first product. - ---- - -# 27. Repair and rollback model - -Keep repair simple and targeted. - -User-facing repair actions: - -```text -Repair Runtime -Restart Display Connection -Restart TFT -Restart Android -Open Google Play -Reset Runtime Profile to Baseline -Open Diagnostics -``` - -Runtime repair verifies: - -```text -external root mounted -emulator binary/version present -platform-tools present -AVD exists -Google Play image metadata matches expected family -AVD config is readable -TFT package state -``` - -Do not delete/recreate the AVD automatically because one probe failed. - -Profile rollback: - -- keep `Baseline` immutable; -- keep last-known-good profile; -- if a new restart configuration fails boot/readiness, automatically revert the pending profile and offer `Restart with Last Known Good`; -- unsupported values never silently clamp to a different value. - -Application rollback: - -- retain the last notarized release artifact; -- keep the current direct-window control build available for engineering A/B until native embedding passes release gates; -- the direct-window control is not a second shipping UX after native acceptance. - ---- - -# 28. Native build/sign/notarize/release - -Production output: - -```text -dist/TFTMAC.app -dist/TFTMAC.dmg -``` - -Build script: - -```text -scripts/build-native-app.command -``` - -Requirements: - -- use authoritative Xcode path; -- build arm64 Release; -- preserve/discover the existing TFTMAC bundle identifier rather than inventing a second app identity; -- set semantic app version/build number; -- sign all app executables/frameworks with Developer ID Application; -- Hardened Runtime enabled; -- notarize with Apple notary service; -- staple notarization ticket; -- validate with `codesign`, `spctl`, and launch smoke test; -- install test copy to `/Applications/TFTMAC.app`; -- verify Dock/Finder identity is TFTMAC. - -Do not bundle Google Play system images or Riot game binaries into the DMG. The app uses/bootstraps the user's licensed official Android runtime installation on the external runtime root. - ---- - -# 29. Test strategy - -Avoid a combinatorial test explosion. Test each independent variable against the known-good baseline and reserve full real matches for release-critical paths. - -## 29.1 Unit tests - -Required deterministic tests: - -```text -runtime profile canonicalization/hash -setting allowed values -CPU/RAM conversion -viewport/letterbox coordinate mapping -1920x1080 touch mapping -FPS rolling-window math -sequence-gap detection -latency math -state-machine transitions -capture seal atomicity -abandoned-capture recovery -package metadata parsing -redaction -gRPC discovery parsing -``` - -## 29.2 Integration tests - -```text -hidden emulator boot -controller authentication -status/getStatus -1920x1080 raw stream -10-minute animated frame stream -Metal presentation -mouse/touch round trip -keyboard round trip -Back/Home/Overview -Power/Wake -rotate both directions -volume/mute -screenshot -TFT restart -Android restart -CoreAudio -logger start-before-emulator -raw seal -post-seal SQL -``` - -## 29.3 Runtime settings smoke matrix - -Do not test all combinations. - -RAM dimension: - -```text -4.0 / 4.5 / 5.0 / 5.5 / 6.0 / 6.5 / 7.0 / 7.5 / 8.0 GB -``` - -For each, hold CPU=6 and all other baseline values. Verify Android boots and TFT reaches a stable ready/lobby state without OOM/ANR. - -CPU dimension: - -```text -2 / 3 / 4 / 5 / 6 / 7 / 8 -``` - -For each, hold RAM=5.0 GB and all other baseline values. Verify boot/readiness. - -ASG dimension: - -```text -800 / 400 -``` - -Hold CPU=6, RAM=5.0 GB, Medium/60/OFF. - -Full-match performance validation is required only for the baseline and any candidate being promoted, not every valid dropdown choice. - -## 29.4 Real user acceptance - -At least one complete official TFT match must be played entirely through `TFTMAC.app` with: - -```text -embedded display only -native input -CoreAudio -logger healthy -FPS visible/logging -no visible Qt/QEMU -native fullscreen tested -normal window mode tested -restart controls tested outside active match -raw capture sealed correctly -``` - ---- - -# 30. Performance acceptance contract - -The native embed layer must not become the reason TFT is slower. - -For a stable gameplay measurement: - -```text -warm-up exclusion: first 120 seconds -measurement: next 600 continuous seconds -Average presented FPS >= 58.0 -P95 frame interval <= 20.0 ms -P99 frame interval <= 33.334 ms -janky frame = interval > 33.334 ms -janky frames <= 1.0% -severe stall = interval > 100 ms -severe stalls <= 3 in 600 seconds -no renderer crash -no Vulkan device loss -no recurring graphics error storm -``` - -Embedded-vs-direct control regression gate under equivalent runtime/workload: - -```text -Average/source FPS regression <= 1.0 FPS -P99 frame interval regression <= 3% -jank regression <= 0.25 percentage points -median synthetic input-to-present <= direct control + 5 ms -P95 synthetic input-to-present <= direct control + 8.334 ms -absolute median input-to-present <= 50 ms -absolute P95 input-to-present <= 83.334 ms -``` - -If the simple MMAP/Metal presentation cannot pass this gate, investigate the embed transport first. Do not randomly tune guest RAM, renderer, and graphics settings simultaneously. - ---- - -# 31. Strict implementation gates - -## Gate 0 — preserve the control - -Deliverables: - -```text -record exact current runtime config -retain current direct-window control build for engineering A/B -preserve current telemetry/performance evidence -mark old window-hack production path superseded -``` - -Exit: known-good control remains launchable and recoverable. - -## Gate 1 — Xcode/native project and protocol lock - -Implement: - -```text -TFTMAC.xcodeproj -SwiftPM locks -vendored compatible emulator proto -generated Swift client -basic native NSWindow -unit-test target -EmbeddedControlProbe target -``` - -Exit: clean Release build on authoritative Xcode. - -## Gate 2 — hidden emulator controller probe - -Probe only; no product polish. - -```text -start logger -launch current 5-GB/6-core emulator hidden -locate/authenticate gRPC -get status -verify AVD/runtime -clean shutdown -``` - -Exit: no visible Qt/QEMU and authenticated controller works reliably. - -Stop if this fails. - -## Gate 3 — full 1920×1080 raw display - -```text -MMAP raw stream -bounded copy -Metal ring -MTKView -complete 1920x1080 image -resize/aspect-fit -10-minute TFT foreground animation -``` - -Exit: no recurring corruption, tearing, gRPC disconnect, or renderer change. - -## Gate 4 — native input - -```text -mouse/touch -keyboard -wheel -Back/Home/Overview -``` - -Exit: Android/TFT can be operated without a visible emulator UI. - -## Gate 5 — FPS truth/calibration - -```text -source seq/timestamps -Metal present counters -fps.jsonl -native HUD -SurfaceFlinger/Perfetto calibration -``` - -Exit: displayed FPS label is truthful and raw FPS data advances once/sec. - -## Gate 6 — full native Mac UX - -```text -real traffic lights -native toolbar -native menus -normal resize -FIT/100% -native fullscreen -Device Controls inspector -all side/device controls -advanced controls capability sheet -``` - -Exit: every visible control has a deterministic PASS test. - -## Gate 7 — Play/TFT lifecycle - -```text -clock/time preflight -Play sign-in-needed state -official install/update flow -package applicationId/versionCode/versionName -installer verification -signer digest capture -Riot patch/init state -Riot login surface -TFT restart -``` - -Exit: clean machine/runtime state can get from app launch to official TFT ready without developer terminal intervention except human authentication. - -## Gate 8 — runtime profiles/settings - -```text -Baseline/Custom profiles -config hashes -CPU 2-8 -RAM 4.0-8.0 by 0.5 -ASG 800/400 -Apply & Restart -last-known-good rollback -requested-vs-observed verification -``` - -Exit: approved runtime experiments require no source edit. - -## Gate 9 — raw-first logger and SQL - -```text -continuous source-of-truth run -capture seal -post-seal hashes -FPS SQL schema -normalization -forced SQL-failure recovery test -abandoned-capture recovery -``` - -Exit: SQL/post-processing failure cannot lose or delay protection of the raw run. - -## Gate 10 — embedded-vs-direct performance A/B - -Run current baseline through both paths with the same Android/TFT configuration. - -Compare: - -```text -source FPS -presented FPS -frame intervals -stream drops -source-to-present latency -SurfaceFlinger/HWC/GPU misses -CPU/RSS -host compression/swap/pageouts -guest memory -input latency -audio -network stability -logger health -``` - -Exit: native embed passes the frozen regression gate. - -Only now is the direct-window wrapper retired as a product path. - -## Gate 11 — full real-match acceptance - -Complete a real official match entirely in the native app and seal/analyze the run. - -Exit: native TFTMAC is `PLAYABLE_NATIVE`. - -## Gate 12 — League Voice feasibility - -Run only after core app is stable. - -Exit: either `SUPPORTED` with legitimate official-client proof or `UNAVAILABLE — RIOT SESSION LIMIT/NO SUPPORTED INTERFACE` with no bypass attempt. - -## Gate 13 — signing/notarization/release - -```text -Release build -Developer ID signing -notarization -stapling -/Applications launch -release checklist -rollback artifact -``` - -Exit: production-shippable native app artifact. - ---- - -# 32. Release acceptance checklist - -Core native release is not complete until every item below is proven: - -```text -[ ] one Finder/Dock TFTMAC app identity -[ ] real red/yellow/green controls -[ ] normal resizable Mac window -[ ] native fullscreen via green button / Control-Command-F -[ ] no visible QEMU title bar -[ ] no visible Android Emulator toolbar -[ ] no Accessibility permission required -[ ] full Android 1920x1080 display embedded in TFTMAC -[ ] complete game UI visible -[ ] correct aspect ratio in windowed mode -[ ] full content fill on 16:9 fullscreen -[ ] FIT works -[ ] 100% works -[ ] mouse/touch works -[ ] keyboard works -[ ] wheel/scroll works -[ ] Back works -[ ] Home works -[ ] Overview works -[ ] Power/Wake works -[ ] Rotate Left works -[ ] Rotate Right works -[ ] Volume Up works -[ ] Volume Down works -[ ] Mute works -[ ] Screenshot works -[ ] Restart TFT works -[ ] Restart Android works -[ ] implemented Advanced Emulator Controls work -[ ] current Google Play ARM64 image verified -[ ] Android date/time preflight works -[ ] TFT applicationId verified -[ ] installer verified as Google Play -[ ] current versionName/versionCode captured -[ ] signer digest captured and retained as package evidence -[ ] official Play install/update flow works -[ ] Riot login/patch flow works -[ ] CPU 2-8 UI works -[ ] RAM 4.0-8.0 by 0.5-GB UI works -[ ] ASG 800/400 UI works -[ ] profile persistence/hash works -[ ] Apply & Restart seals old capture and starts new logger first -[ ] Medium/60/OFF baseline represented correctly -[ ] Ultra High remains rejected until new evidence promotes it -[ ] live FPS visible -[ ] fps.jsonl advances once/sec -[ ] stream drops measured -[ ] presentation latency measured -[ ] SurfaceFlinger counters continue -[ ] runtime config/hash attached to every run -[ ] logger starts before emulator/gameplay -[ ] Stop/Quit writes raw seal before post-processing -[ ] no SDK/AVD recursive inventory on critical stop path -[ ] SQL failure cannot invalidate a raw-sealed run -[ ] CoreAudio works through a long session -[ ] no recurring network disconnect caused by embed layer -[ ] complete official TFT match succeeds in native app -[ ] embedded presentation passes direct-control performance gate -[ ] app is Developer ID signed -[ ] app is notarized/stapled -[ ] /Applications/TFTMAC.app launch passes -``` - -League Voice has its own separate release checklist and may remain unavailable without blocking these core requirements. - ---- - -# 33. Explicit stop conditions - -Stop adding layers and diagnose the first broken boundary if any of these occurs: - -```text -hidden emulator cannot authenticate gRPC -frame stream changes guest resolution or renderer path -MMAP source repeatedly corrupts/tears after bounded-copy protection -gRPC display adds material source-FPS/input regression -native presentation requires software video encoding -raw logger becomes dependent on the render/UI thread -SQL/post-processing can delay or prevent raw sealing -Google Play package cannot be verified as official authority -normal device controls require visible Qt UI and no safe gRPC/ADB equivalent exists -runtime-setting application silently changes more than the selected variable -League Voice requires forging/replaying Riot/Vivox authentication -League Voice breaks the active Android TFT session because of Riot session rules -``` - -These are architecture faults, not reasons to add wrapper hacks. - ---- - -# 34. Zen Gate / no-overengineering rules - -The implementation remains deliberately small: - -- one native app; -- one official Google Play AVD; -- one current stock Emulator runtime; -- one embedded display path; -- one bounded frame copy before considering zero-copy; -- one data-driven settings registry; -- one continuous logger architecture; -- one official package authority; -- one optional isolated League Voice provider; -- no source AEMU build unless a measured blocker demands it; -- no custom ANGLE build unless a measured blocker demands it; -- no giant cross-product performance matrix; -- no duplicate runtime on internal storage; -- no second updater/feed/orchestration service; -- no web/Electron shell; -- no fake fullscreen/window management system. - -Every new component must either satisfy a user-visible native-app requirement or own a measured causal failure. - ---- - -# 35. Required implementation artifacts - -Implementation is expected to leave durable evidence, not just code. - -Required artifacts by completion: - -```text -TFTMACAPP.md -TFTMAC.xcodeproj -Package.resolved -Vendor/AndroidEmulator/emulator_controller.proto -Vendor/AndroidEmulator/SOURCE.json -Generated/EmulatorController/*.swift -ssot/native-app-probe-results.json -ssot/native-app-release-checklist.md -ssot/native-app-performance-ab.json -ssot/tft-package-authority.json -ssot/emulator-controller-authority.json -runtime profile schema/version -FPS raw schema/version -SQLite FPS migration -unit/integration/UI tests -dist/TFTMAC.app -dist/TFTMAC.dmg -notarization evidence -``` - ---- - -# 36. Final execution order - -A fresh implementation agent should execute this file from top to bottom with this priority: - -```text -1. Preserve current control. -2. Create normal Xcode app and lock protocol/dependencies. -3. Prove hidden emulator + authenticated controller. -4. Prove full 1920x1080 raw Metal display. -5. Prove native input. -6. Add truthful FPS and calibration. -7. Build complete native window/toolbar/device controls. -8. Integrate Play/TFT lifecycle and package verification. -9. Add runtime profiles/settings. -10. Harden continuous logging and raw-first stop/recovery. -11. Run direct-vs-embedded performance A/B. -12. Complete real native-app TFT match acceptance. -13. Run League Voice feasibility separately. -14. Sign, notarize, install, and release. -``` - -Do not ask the user to choose architecture already resolved by this file. Machine-state discovery should resolve paths, package state, component versions, runtime state, and available controller capabilities automatically. - -The only expected human pauses are official Google/Riot authentication, MFA/CAPTCHA/consent, and real gameplay/subjective quality confirmation when a release acceptance test specifically requires it. - ---- - -# 37. Definition of done - -TFTMAC native app work is done when the user can click **TFTMAC** in Finder/Dock, see one normal native Mac window, use the entire full-resolution Android/TFT experience inside it, enter normal macOS fullscreen, play an official current TFT match with mouse/keyboard and CoreAudio, operate all required emulator/device controls without a second window, change supported runtime resources from native Settings, see truthful live FPS, finish/quit with the raw run immediately protected, and reopen the app without developer-terminal cleanup. - -At that point: - -```text -PLAYABLE_NATIVE = YES -FULL_SIZE_EMBEDDED_DISPLAY = YES -NATIVE_FULLSCREEN = YES -OFFICIAL_PLAY_TFT = YES -CONTINUOUS_FPS_TELEMETRY = YES -SAFE_RAW_CAPTURE_SEAL = YES -RUNTIME_SETTINGS = YES -PRODUCTION_SIGNED/NOTARIZED = YES -LEAGUE_VOICE = SUPPORTED or TRUTHFULLY_UNAVAILABLE, never bypassed -``` - -That is the TFTMACAPP ship target. +Current implementation truth is in [facts.md](facts.md), [project.md](project.md), +and [dev.md](dev.md). The former implementation plan is retained at +`docs/history/2026-08-31-pre-build8/TFTMACAPP.md` as historical design context. diff --git a/TFTMAC_CLEAN_OWNERSHIP_PLAN.md b/TFTMAC_CLEAN_OWNERSHIP_PLAN.md index 3ac9562..cb5b4ee 100644 --- a/TFTMAC_CLEAN_OWNERSHIP_PLAN.md +++ b/TFTMAC_CLEAN_OWNERSHIP_PLAN.md @@ -1,1057 +1,11 @@ -# TFTMAC Clean Ownership, Runtime Convergence, and Storage Reclamation Plan +# Historical Clean-Ownership Plan -**Status:** AUTHORITATIVE IMPLEMENTATION PLAN -**Project authority:** `flashls1/TFTMAC` -**Target host:** Apple Silicon Mac mini M4 -**Date frozen:** 2026-08-29 -**Scope:** source identity, runtime authority, validation/CI, storage reclamation, redundant-project retirement, and safe continuation of the native application build. +**Status: historical — completed context, not current execution authority.** -**Mandatory execution preflight:** `TFTMAC_CLEAN_OWNERSHIP_PREFLIGHT.md` +The original plan is archived at +`docs/history/2026-08-31-pre-build8/TFTMAC_CLEAN_OWNERSHIP_PLAN.md`. -> Implementation agents must read the preflight before repository discovery. The preflight contains the exact already-discovered files, symbols, hashes, branch state, storage paths, validation coupling, migration hazards, and KEEP/DELETE/REWRITE/MIGRATE classifications. Do not repeat discovery already recorded there unless its referenced source/hash has changed or the item is explicitly marked REMEASURE. - ---- - -## 0. Executive directive - -TFTMAC becomes one independent product and one independent engineering system. - -The shipping project must contain only TFTMAC-owned product identity, TFTMAC runtime controls, official Google Android Emulator/Google Play integration, Riot's official TFT package/update path, TFTMAC telemetry, TFTMAC native macOS UI, and the minimum source/evidence required to build and maintain those functions. - -The legacy donor product is no longer a runtime dependency, launcher, control application, update authority, branding source, test authority, CI authority, project identity, or installed dependency. Transferable facts that TFTMAC has independently reproduced may remain as TFTMAC facts under TFTMAC naming. Legacy implementation material that is not required by the working architecture is removed. - -The abandoned source-built emulator laboratory is also retired from the normal TFTMAC system. The working product uses the released Google Android Emulator and the exact installed EmulatorController protocol. The large development-source tree created by earlier experiments is reclaimed after safety gates prove that nothing in the active product depends on it. - ---- - -# 1. Concrete win condition - -The implementation is complete only when all of the following are true: - -```text -ONE_ACTIVE_PROJECT = TFTMAC -APP_IDENTITY = TFTMAC -BUNDLE_ID = com.flashls1.tftmac -LEGACY_PRODUCT_UI_REFERENCES = 0 -LEGACY_PRODUCT_SOURCE_REFERENCES = 0 in the current tracked tree -LEGACY_LAUNCHERS = 0 -LEGACY_UPDATE_FEEDS = 0 -LEGACY_RUNTIME_DEPENDENCIES = 0 -OFFICIAL_TFT_INSTALLER = com.android.vending -RIOT_PACKAGE_REPACKING = 0 -CUSTOM_RIOT_PATCHING = 0 -SOURCE_BUILT_AEMU_REQUIRED = NO -WORKING_RUNTIME_ROOT_PRESERVED = YES -WORKING_AVD_PRESERVED = YES -CURRENT_NATIVE_GATE1_WORK_PRESERVED = YES -ABANDONED_BUILD_TREE_RECLAIMED = YES -REDUNDANT_PROJECTS_RETIRED = YES after unique-content gates -NATIVE_TFTMAC_IMPLEMENTATION_CAN_CONTINUE = YES -``` - -The user must be able to continue building and playing through TFTMAC after cleanup without reinstalling or reconstructing the known-good runtime. - ---- - -# 2. Preflight findings - -## 2.1 Current repository authority - -Observed current authority: - -```text -repository: flashls1/TFTMAC -default branch: master -master SHA: fdfb325100db508f795ed5b83cea48887a88cb71 -local/remote: aligned and clean -``` - -The current native implementation change is: - -```text -change: 14d01e7a-b64d-4b7f-b856-e3366885b381 -branch: clara/implement-tftmac-native-mac-app-from-tft-14d01e7a -state: active dirty worktree -``` - -That change contains the new native Xcode/AppKit/Metal work and must be preserved before any destructive cleanup. - -Already proven in that change: - -- normal `TFTMAC.xcodeproj` exists; -- arm64 Release build passes under Xcode 26.6; -- native unit tests pass; -- Swift 6 concurrency issue found by the compiler was repaired; -- gRPC/SwiftPM dependency graph resolves successfully; -- the installed Android Emulator 37.1.11 controller protocol was frozen directly from the working runtime; -- frozen `emulator_controller.proto` SHA-256 is `1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f`; -- the stock runtime remains the control while the native presentation layer is built. - -## 2.2 Legacy identity contamination is broad, not cosmetic - -Repository search found hundreds of current-tree references tied to the old donor product. They exist in multiple classes: - -```text -README / contribution / support documentation -launcher source and resources -bundle identifiers -application metadata -old icons and social artwork -build scripts -release/update scripts -CI validation -integration tests -runtime-control commands -process detection -profile/config identifiers -performance SQL primary/foreign keys -historical engineering-map rows -old experiment scripts -old updater/feed logic -old keychain/service names -old test names -old package/update terminology -``` - -Therefore this cannot be implemented as a blind text replacement. Some records are executable behavior, some are SQL identities, some are obsolete code that should be deleted, and some are historical evidence that must be compacted or migrated before removal. - -## 2.3 Current validation is still coupled to legacy tests - -The registered TFTMAC production validation still executes the old validation script. GitHub CI also invokes that legacy test path. - -This must be changed before legacy source is deleted, otherwise a correct cleanup would intentionally break the project's own validation authority. - -Final validation authority must be TFTMAC-native and must not depend on a deleted launcher tree. - -## 2.4 Abandoned source-build storage is the main reclaim target - -The earlier from-source emulator effort created bulk development data under: - -```text -/Volumes/MAC MINI M4/TFTMAC/Build -``` - -Project research recorded the development source/build tree at approximately **131 GB**. - -That path includes or historically included: - -```text -AEMU multi-repository source checkout -large emulator prebuilts -source-build objects -CMake/Ninja outputs -CTS/reference source trees -host compatibility wrappers -build logs -failed/partial build state -source synchronization metadata -``` - -The source build reached a real 9,854-step compile and later failed during that obsolete path. Those compiled/source artifacts are no longer required by the working architecture. - -The released stock emulator path is already proven and the native project now freezes its protocol from the installed runtime itself. Therefore the large source checkout has no normal product role. - -## 2.5 Exact live free-space measurement is a cleanup-time gate - -The current Clara registry advertises an external-volume disk probe, but the live allowlist rejected that probe during this preflight. The plan therefore does **not** invent a current free-space number. - -Before deletion, implementation must perform one read-only byte inventory through an approved host path and record: - -```text -filesystem free bytes before cleanup -size of each candidate directory -protected runtime size -capture size -native DerivedData/build-cache size -redundant project roots/worktrees size -filesystem free bytes after cleanup -exact reclaimed bytes -``` - -No cleanup acceptance may use an estimated reclaimed figure when an exact post-delete measurement is available. - -## 2.6 Redundant Clara project identities - -Three TFT-related project identities currently exist: - -```text -TFTMAC -> authoritative project -TFTMAC Runtime -> older source/runtime project -TFTMAC2 -> essentially empty shell project -``` - -`TFTMAC2` currently has no active changes, no operations, and only minimal foundation files. - -`TFTMAC Runtime` cannot be deleted immediately because it still contains one published but unmerged compatibility-laboratory change: - -```text -change: 7c24d6df-bb78-4a4d-9c8f-1b4028871f60 -head: 994cf8d8524b87cc03c7decb4b253fc7be2253c7 -``` - -That change must receive a unique-content review before retirement. Any still-useful fact must be reproduced/migrated into TFTMAC or explicitly classified as superseded before the old project is retired. - -## 2.7 Licensing boundary - -The current repository contains MIT-licensed material with an upstream copyright notice. - -Product independence does not authorize false authorship or removal of legally required notices from substantial retained code. - -Required rule: - -1. Audit which current files, if any, remain copied or substantially derived from the legacy open-source code. -2. Prefer deleting obsolete copied implementation rather than carrying it forward. -3. If substantial licensed portions remain, retain the required copyright and MIT permission notice in a neutral third-party notice location. -4. The old product name is not required by the MIT text and does not need to remain in the product identity. -5. If no substantial licensed portion remains after replacement/removal, update the project's primary licensing/notice structure to accurately describe the resulting TFTMAC codebase. - ---- - -# 3. Non-negotiable implementation rules - -1. **TFTMAC is the sole product identity.** -2. **No old launcher ships, starts, or remains callable from TFTMAC.** -3. **No old logo, icon, wordmark, favicon, social artwork, hero image, or fallback icon remains.** -4. **No old hosted game feed or updater remains.** -5. **No third-party Riot APK mirroring, repacking, re-signing, or patching.** -6. **Google Play is package installation/update authority.** -7. **Riot's own application performs its own patch/content initialization.** -8. **The working stock Android Emulator remains runtime authority.** -9. **No source-built AEMU checkout is needed for normal build, launch, test, repair, or release.** -10. **Do not delete the working SDK, AVD, Google account state, TFT install, or active native work during storage cleanup.** -11. **Do not delete a redundant project until unique-source and active-change checks pass.** -12. **Do not rewrite Git history as part of this cleanup.** Current tracked source, artifacts, metadata, and shipping outputs must be clean; destructive historical-object rewriting is not needed for product independence. -13. **Delete recreation paths before deleting large storage.** Old commands must not be able to silently regenerate the 100+ GB source tree. -14. **Every destructive delete requires an exact candidate path and pre-delete classification.** No broad `/Volumes/.../TFTMAC` recursive deletion. -15. **Raw evidence is retained only when it still protects a current decision.** Superseded duplicate experiments are not permanent product assets. - ---- - -# 4. Final TFTMAC architecture after convergence - -```text -TFTMAC.app - -> native AppKit window - -> Metal presentation - -> TFTMAC runtime controller - -> authenticated local EmulatorController - -> stock Google Android Emulator - -> official Google Play ARM64 guest - -> official Google Play TFT package - -> Riot official authentication/content lifecycle - -> CoreAudio - -> TFTMAC raw-first telemetry -``` - -Development source: - -```text -flashls1/TFTMAC - TFTMAC.xcodeproj - TFTMAC/ - Vendor/AndroidEmulator/ - Generated/EmulatorController/ - Probes/ - Tests/ - scripts/ - ssot/ -``` - -Normal product development does **not** require: - -```text -source-built emulator checkout -legacy launcher tree -legacy application installation -legacy update server/feed -legacy branding tree -legacy release builder -legacy hosted Riot package flow -large CTS/source research checkout -``` - ---- - -# 5. Preservation matrix - -## 5.1 Protected — never delete during initial cleanup - -```text -/Volumes/MAC MINI M4/TFTMAC/Runtime/SDK -/Volumes/MAC MINI M4/TFTMAC/Runtime/AVD -current official Google Play system image -current TFT_Ultra_Tablet userdata -Google Play account/session state inside the AVD -current official TFT package and Riot data inside the AVD -current runtime manifests required by active stock-runtime control -~/Library/Application Support/TFTMAC/Profiles -~/Library/Application Support/TFTMAC/State -current package-authority evidence -current native implementation worktree 14d01e7a... -TFTMAC.xcodeproj source -Vendor/AndroidEmulator frozen protocol -Package.resolved -current source repository and .git -``` - -## 5.2 Preserve compact evidence, then allow raw-data pruning - -Keep a compact canonical record for: - -```text -latest known-good playable baseline -latest 5 GB baseline decision -current Medium / 60 / Performance OFF decision -Ultra High rejection -current package identity / installer / signer evidence -current stock-emulator version -current frozen EmulatorController proto hash -current CoreAudio decision -current raw-seal contract -one representative valid performance capture -one representative native trace if still used by an active decision -``` - -Older duplicate raw captures, abandoned traces, repeated screenshots, rejected experiment runs, and duplicate derived SQL can be deleted only after their decision/result is represented in current TFTMAC SSOT with source hashes where useful. - -## 5.3 Disposable after verification - -Primary reclaim candidates: - -```text -/Volumes/MAC MINI M4/TFTMAC/Build - including AEMU source checkout - objects / Ninja / CMake outputs - large prebuilts downloaded only for source compilation - CTS/reference source trees used only by abandoned source-build work - phase-build logs - compatibility wrappers used only by source compilation - -obsolete native DerivedData/build caches -completed obsolete Clara worktrees whose commits already exist on master -old donor checkout directories if present -old installed donor application if present -old donor Application Support/runtime data if present -old external donor runtime directories if present -obsolete launcher dist/DMG/update output -superseded test campaigns and rejected experimental runtime copies -``` - -The entire `Build` root may be removed only after the recreation-path scan in Gate 3 proves the current native/runtime system no longer references it for required operations. - ---- - -# 6. Source ownership cleanup - -## 6.1 Delete obsolete product layers instead of renaming them - -Remove the old launcher implementation where TFTMAC no longer uses it: - -```text -legacy launcher app source -legacy launcher resources -legacy release manifest for that launcher -legacy updater/feed code -legacy Sparkle integration if TFTMAC does not use it -legacy release builder -legacy update publisher -legacy game-package publisher -legacy launcher integration tests -legacy localization files tied only to the removed launcher -legacy game-host helper app if native TFTMAC no longer requires it -legacy branding source and generated output -``` - -Do not preserve dead code merely to keep old tests green. - -## 6.2 Replace current repository docs with TFTMAC docs - -Rewrite or replace: - -```text -README.md -CONTRIBUTING.md -SUPPORT.md -SECURITY.md -NOTICE/third-party notices -docs/architecture.md -docs/building.md -docs/releasing.md -docs/reproducibility.md -docs/telemetry.md -docs/troubleshooting.md -GitHub repository metadata -pull-request template -``` - -These documents describe TFTMAC only. - -Historical lessons that still matter are rewritten as independently verified TFTMAC facts. The old product is not treated as continuing architectural authority. - -## 6.3 Remove old process/runtime controls - -Delete controls whose only purpose is to start, stop, inspect, or compare the old application. - -From TFTMAC runtime tooling remove: - -```text -legacy-app process classification -legacy-app launch command -legacy-app stop command -legacy-app runtime audit -legacy ADB-port cleanup paths that exist only for that app -legacy Application Support path discovery -legacy installed-app lookup -legacy icon fallback -legacy named compatibility-source labels -``` - -No command advertised by `tftmac-direct-control.mjs` or other TFTMAC tooling may start the retired application. - -## 6.4 Rename TFTMAC runtime identities semantically - -Do not carry old names inside data models. - -Use TFTMAC-owned identifiers, for example: - -```text -tftmac_official_baseline_v1 -tftmac_5gb_baseline_v1 -tftmac_5gb_flush400_exp_v1 -``` - -Rename code concepts such as: - -```text -DONOR_PROFILE -> BASELINE_PROFILE -startDonorControl -> startBaselineControl -prepareDonorAVD -> prepareBaselineAVD -restoreDonorAVD -> restoreBaselineAVD -``` - -Exact final names should remain short and descriptive. - -## 6.5 SQL migration, not destructive string replacement - -Performance-lab and engineering-map identifiers are relational keys. - -Implement an idempotent migration that: - -1. inserts new TFTMAC-owned config IDs; -2. remaps foreign keys in experiments/sessions/evidence; -3. preserves measured values and timestamps; -4. removes superseded old IDs after referential-integrity checks pass; -5. updates lab metadata/current-baseline keys; -6. runs `PRAGMA foreign_key_check`; -7. proves no old identifier remains in active schema/seed data. - -Do not destroy useful measurements just to rename their primary key. - -## 6.6 Repository-wide forbidden-reference gate - -The forbidden legacy product token must be supplied transiently to the validation command and must **not** be committed into the repository solely for the purpose of testing itself. - -Validation scans: - -```text -tracked file paths -tracked text content -bundle metadata -compiled app strings where practical -CI files -scripts -source code -SQL seed data -assets/resources -project metadata -``` - -Acceptance: zero current-tree/product matches. - ---- - -# 7. Official update and package authority - -TFTMAC's package lifecycle is intentionally simple: - -```text -Google Play system image - -> Google account through official Android UI - -> Play Store listing for com.riotgames.league.teamfighttactics - -> Install / Update through com.android.vending - -> launch official Riot activity - -> Riot performs its own patch/content initialization -``` - -Required verification per installed/update state: - -```text -package = com.riotgames.league.teamfighttactics -installer = com.android.vending -versionName observed -versionCode observed -signer digest observed -base/split paths observed -launch activity observed -``` - -Never reintroduce: - -```text -hosted APK feed -private updater -bundled game APK -manual Riot patch bundle -re-signing -binary patching -screen-coordinate package install automation -``` - -If Play requires user authentication, consent, MFA, or CAPTCHA, TFTMAC surfaces the official Play UI and pauses only for that human action. - ---- - -# 8. Validation and CI cutover - -## 8.1 Replace legacy validation authority first - -Before removing old test source, create TFTMAC-native validation: - -```text -scripts/verify-tftmac.command -``` - -It should run only relevant checks: - -```text -repository forbidden-reference scan -TFTMAC Xcode project parse/build -native unit tests -frozen proto SHA verification -Package.resolved presence/pin verification -TFTMAC bundle identity verification -no legacy launcher/update/feed paths -performance-lab schema self-test -engineering-map/SSOT integrity checks still relevant to current architecture -shell/script syntax for retained scripts -``` - -Do not make normal CI boot Android or require a live game. - -## 8.2 GitHub CI - -Change CI from legacy launcher validation to: - -```text -verify repository ownership -build native TFTMAC -run native unit tests -verify generated/frozen protocol authority -run current SQL/SSOT static checks -reject dirty/generated drift -``` - -## 8.3 Clara validation - -Update the TFTMAC Clara project validation command only after the new verifier passes locally on the exact change. - -The production validator must no longer reference a deleted legacy test script. - ---- - -# 9. Safe storage reclamation procedure - -Storage cleanup is a controlled effect with evidence before and after. - -## 9.1 Freeze active work first - -Before any delete: - -1. review native change `14d01e7a...`; -2. checkpoint all current native Gate 1 source; -3. ensure `Package.resolved`, frozen proto, Xcode project, tests, and scripts are included; -4. capture current worktree SHA/status; -5. prove the working Runtime root is outside every planned deletion target. - -No cleanup starts while unique native source exists only as uncheckpointed local files. - -## 9.2 Process quiescence gate - -Before deleting build/runtime-adjacent directories: - -```text -no source-build worker alive -no repo sync alive -no CMake/Ninja source build alive -no process has an open file under the candidate build tree -no TFTMAC game/emulator process requires the candidate path -``` - -The working stock emulator may remain installed; cleanup should preferably occur with TFTMAC stopped to reduce ambiguity. - -## 9.3 Exact size inventory - -Create a machine-readable record such as: - -```text -ssot/storage-reclamation-preflight.json -``` - -For every candidate record: - -```text -path -realpath -bytes -classification = PROTECTED | KEEP_EVIDENCE | DISPOSABLE | REVIEW -reason -referenced_by_current_source = true/false -active_process_reference = true/false -planned_action -``` - -The deletion engine accepts only exact paths classified `DISPOSABLE`. - -## 9.4 Remove source-build recreation paths - -Before deleting `/Volumes/MAC MINI M4/TFTMAC/Build`: - -- remove normal commands that initialize/sync/build AEMU from source; -- remove current product docs instructing source builds; -- remove CI/validation dependence on source-build artifacts; -- remove active SSOT statements that declare the Build tree mandatory; -- retain only compact historical outcome facts if still useful; -- verify current native build and stock-runtime controls do not reference the Build root. - -Only then remove the abandoned Build tree. - -## 9.5 Tiered deletion order - -Recommended order: - -```text -Tier 1: obsolete local build caches / DerivedData -Tier 2: closed obsolete TFTMAC worktrees already merged or superseded -Tier 3: old launcher dist/update/build artifacts -Tier 4: abandoned source-build / AEMU / CTS development tree (~131 GB historical footprint) -Tier 5: obsolete donor application/runtime/data installations -Tier 6: superseded raw captures/traces after compact evidence retention -Tier 7: redundant project local roots after retirement gates -``` - -After each tier, re-measure free space and verify the working stock runtime still resolves. - -## 9.6 Runtime smoke after large deletion - -Immediately after the abandoned Build tree is removed: - -```text -external Runtime root exists -SDK emulator executable exists -ADB exists -TFT_Ultra_Tablet AVD exists -Google Play image exists -frozen proto still matches installed controller proto -native TFTMAC Release build still passes -runtime inventory succeeds -``` - -Do not wait until the very end to discover that a supposedly disposable path was still referenced. - ---- - -# 10. Capture and diagnostics retention policy - -The logger remains valuable, but unlimited raw research growth is not. - -Adopt a bounded retention policy: - -```text -KEEP: - latest successful playable baseline raw capture - latest native-app acceptance capture - current package-authority evidence - current promoted A/B evidence - current crash/failure capture for an unresolved blocker - -COMPACT THEN DELETE RAW: - superseded baseline runs - rejected experiments already summarized - duplicate screenshots - repeated diagnostic dumps - old traces not tied to an open hypothesis - abandoned setup/install captures -``` - -Compact record retains: - -```text -session id -dates -runtime config hash -result/verdict -key metrics -manifest/hash references when useful -reason raw data was removed -``` - -No automated age-based deletion is required in the first cleanup. Use decision relevance, not arbitrary days. - ---- - -# 11. Redundant-project retirement - -## 11.1 TFTMAC2 - -Preflight indicates this project is effectively an empty shell: - -```text -no active changes -no operations -no release -minimal foundation files only -``` - -Retirement procedure: - -1. one final unique-file/commit comparison; -2. confirm nothing needs migration; -3. close/remove local worktrees/root; -4. archive remote repository rather than leaving it as an active engineering target; -5. remove or mark retired in Clara so it is no longer selectable as an active TFT project. - -## 11.2 TFTMAC Runtime - -This project requires a stronger gate because it has an unmerged published change. - -Procedure: - -1. inspect published head `994cf8d8524b87cc03c7decb4b253fc7be2253c7`; -2. compare its unique files/findings with current TFTMAC master + native implementation branch; -3. for each unique item classify: - - `MIGRATE_TO_TFTMAC`, - - `ALREADY_SUPERSEDED`, or - - `REJECTED/OBSOLETE`; -4. migrate only genuinely useful current facts/code; -5. validate migrated TFTMAC source independently; -6. close the old change; -7. archive the remote project; -8. delete local project/worktree/runtime-local build artifacts; -9. remove or mark retired in Clara. - -Final Clara project catalog should present **TFTMAC as the only active TFT engineering authority**. - ---- - -# 12. Implementation sequence and gates - -## Gate 0 — preserve current native work - -Deliverables: - -```text -checkpoint active native Gate 1 work -record exact head and diff -freeze Package.resolved -freeze installed controller proto provenance -``` - -Exit: no unique active work can be lost by cleanup. - -## Gate 1 — ownership inventory - -Deliverables: - -```text -current-tree legacy-reference inventory -legacy path inventory -legal/code-origin inventory -runtime dependency map -SQL identity migration map -``` - -Exit: every current reference has one action: DELETE, REWRITE, MIGRATE, or LEGAL_NOTICE. - -## Gate 2 — new TFTMAC validation authority - -Implement `verify-tftmac.command`, update native tests, and prove it passes before removing old tests. - -Exit: TFTMAC can validate itself without the legacy launcher. - -## Gate 3 — source/product separation - -Delete obsolete launcher/updater/feed/branding code, rename runtime/profile identifiers, migrate SQL, rewrite docs/metadata, remove executable legacy controls. - -Exit: - -```text -forbidden-reference scan = 0 -native Release build = PASS -native tests = PASS -SQL integrity = PASS -``` - -## Gate 4 — stock-runtime independence proof - -Using the current working runtime: - -```text -inventory runtime -verify SDK/AVD -verify Google Play package authority -verify installed controller proto -launch/stop control path if safe -``` - -Exit: no runtime operation requires the abandoned Build tree or retired application. - -## Gate 5 — storage preflight and source-build retirement - -Produce exact storage inventory and remove all source-build recreation paths. - -Exit: `/Volumes/MAC MINI M4/TFTMAC/Build` is classified disposable in full or has explicitly listed small exceptions. - -## Gate 6 — reclaim bulk storage - -Delete approved candidates by tier and record exact reclaimed bytes. - -Exit: - -```text -large abandoned source/build tree removed -stock Runtime intact -native source intact -post-delete runtime smoke PASS -``` - -## Gate 7 — compact/prune superseded telemetry - -Retain active evidence, compact obsolete decision evidence, remove no-longer-useful raw bulk. - -Exit: captures have a bounded, explainable product role. - -## Gate 8 — retire redundant projects - -Retire empty project first. Retire old runtime project only after unique-content migration gate. - -Exit: TFTMAC is the sole active TFT project in Clara. - -## Gate 9 — resume native implementation - -Rebase/continue the native implementation on the clean ownership baseline. - -Next product gate remains: - -```text -hidden stock emulator -authenticated EmulatorController -getStatus -correct PID/AVD proof -clean shutdown -``` - -The cleanup does not replace or postpone the native-app roadmap; it makes that roadmap operate on the correct clean foundation. - ---- - -# 13. Failure handling and rollback - -## Source cleanup failure - -If native build/test fails after deleting legacy source: - -- restore only the exact deleted source files from Git; -- do not restore old installed applications or large external build trees automatically; -- identify the actual dependency and either migrate it into TFTMAC or remove the dependency. - -## SQL migration failure - -If foreign-key or semantic checks fail: - -- rollback transaction; -- keep the old database untouched; -- repair the migration map; -- never hand-edit primary/foreign IDs in a partially migrated live DB. - -## Storage cleanup failure - -If a candidate cannot be proven disposable: - -```text -classification -> REVIEW -no delete -continue with other proven-disposable candidates -``` - -If the stock runtime fails after a deletion tier: - -- stop further deletion; -- restore only the minimum missing configuration/artifact if it was actually removed; -- never regenerate the full source-build tree unless a current measured blocker specifically requires a new source-build project. - -## Redundant-project retirement failure - -If unique source is found: - -- migrate/reproduce it into TFTMAC first; -- do not delete that old project until the migration commit is validated and durable. - ---- - -# 14. Acceptance tests - -Ownership: - -```text -[ ] app name is TFTMAC -[ ] bundle id is com.flashls1.tftmac -[ ] only TFTMAC branding/assets exist -[ ] current-tree forbidden legacy product scan has zero matches -[ ] no legacy launcher executable or command exists -[ ] no legacy updater/feed exists -[ ] no legacy hosted game package path exists -[ ] CI does not reference legacy tests -[ ] Clara TFTMAC validation does not reference legacy tests -``` - -Official package/runtime: - -```text -[ ] stock Google Android Emulator is runtime authority -[ ] current SDK/AVD remain on external Runtime root -[ ] EmulatorController proto authority remains installed Emulator 37.1.11 -[ ] official TFT package is com.riotgames.league.teamfighttactics -[ ] installer is com.android.vending -[ ] version/signing evidence captured -[ ] Google Play handles app update -[ ] Riot handles Riot content initialization -[ ] no custom Riot binary patch/repack/resign path exists -``` - -Storage: - -```text -[ ] exact pre-clean storage inventory captured -[ ] active processes checked before deletion -[ ] active native work checkpointed -[ ] abandoned source-build recreation paths removed -[ ] abandoned source/build tree removed -[ ] exact reclaimed bytes captured -[ ] protected Runtime root unchanged -[ ] protected AVD unchanged -[ ] native Release build passes after cleanup -[ ] runtime inventory passes after cleanup -``` - -Project convergence: - -```text -[ ] TFTMAC2 unique-content check complete and project retired -[ ] TFTMAC Runtime unique published change reviewed -[ ] useful unique items migrated or proven superseded -[ ] old project retired only after migration gate -[ ] TFTMAC is sole active TFT project authority -``` - ---- - -# 15. Required durable cleanup evidence - -Implementation should leave small evidence artifacts: - -```text -ssot/storage-reclamation-preflight.json -ssot/storage-reclamation-result.json -ssot/ownership-migration.json -ssot/runtime-authority.json -ssot/project-retirement.json -ssot/retained-evidence-index.json -``` - -`storage-reclamation-result.json` records: - -```text -startedAt -completedAt -filesystemFreeBytesBefore -filesystemFreeBytesAfter -reclaimedBytes -removedPaths[] -protectedPathsVerified[] -postCleanupNativeBuild -postCleanupRuntimeInventory -``` - -Do not keep giant cleanup logs when a compact hash-addressed result is enough. - ---- - -# 16. Zen Gate evaluation - -Evaluated using the exact retrieved Zen Gate v2.2 scoring engine. - -## Hard gates - -```text -H1 Win Condition: PASS -H2 Source of Truth: PASS -H3 Material Ambiguity: PASS -H4 Safety/Reversibility: PASS -H5 Validation Integrity: PASS -Project-specific absolute constraints: PASS -H = 1 -``` - -Material ambiguity is resolved by fail-closed execution gates: exact storage bytes are measured before deletion; redundant projects receive unique-content checks; legal attribution is determined by retained-code audit; no uncertain path is deleted. - -## Weighted quality score - -| Dimension | Weight | Score / 5 | Contribution | -|---|---:|---:|---:| -| Win Condition Alignment | 15 | 5.0 | 15.0 | -| Source-of-Truth Integrity | 12 | 5.0 | 12.0 | -| Ambiguity Resolution | 13 | 5.0 | 13.0 | -| Simplicity / Minimality | 16 | 4.5 | 14.4 | -| Complexity Justification | 10 | 5.0 | 10.0 | -| Architecture / Boundaries | 9 | 5.0 | 9.0 | -| Failure Handling / Observability | 12 | 5.0 | 12.0 | -| Maintainability / Readability | 6 | 4.5 | 5.4 | -| Practical Shipping Slice | 4 | 5.0 | 4.0 | -| Explainability | 3 | 5.0 | 3.0 | - -```text -Q = 97.8 -``` - -Risk penalties: - -```text -O overengineering = 1 -A unresolved ambiguity = 0 -F failure exposure = 2 -I irreversibility = 2 -R = 5 -``` - -Final: - -```text -Z = H × max(0, Q - R) -Z = 1 × (97.8 - 5) -Z = 92.8 -PASS threshold = 85 -ZEN GATE RESULT = PASS -Remediation mode = NONE -``` - -Why the remaining penalty exists: bulk storage deletion and project retirement are intentionally irreversible effects. The plan contains explicit preservation, exact-path classification, unique-content, quiescence, checkpoint, and post-delete smoke gates to contain that risk. - ---- - -# 17. Definition of done - -This convergence is done when TFTMAC has one clean identity, one native build system, one stock official runtime, one official Google Play/Riot package path, one current validation system, one bounded evidence system, and one active Clara project. - -The external drive contains the **working runtime and useful TFTMAC evidence**, not a 100+ GB abandoned source-build laboratory. - -After this plan is completed, engineering proceeds directly into the native application's hidden-emulator/EmulatorController gate without bringing any retired launcher, branding, updater, project, or source-build dependency back into the system. +Current repository and runtime authority is defined by [README.md](README.md), +[facts.md](facts.md), [project.md](project.md), and the machine-readable files +under `ssot/`. Do not use historical branch hashes, dirty-worktree state, or +cleanup commands from the archived plan as current instructions. diff --git a/TFTMAC_CLEAN_OWNERSHIP_PREFLIGHT.md b/TFTMAC_CLEAN_OWNERSHIP_PREFLIGHT.md deleted file mode 100644 index dbda155..0000000 --- a/TFTMAC_CLEAN_OWNERSHIP_PREFLIGHT.md +++ /dev/null @@ -1,976 +0,0 @@ -# TFTMAC Clean Ownership — Execution Preflight Evidence Handoff - -**Purpose:** eliminate redundant rediscovery before executing `TFTMAC_CLEAN_OWNERSHIP_PLAN.md`. - -**Status:** REQUIRED IMPLEMENTATION INPUT -**Authority repository:** `flashls1/TFTMAC` -**Observed master at preflight creation:** `0bb14a566be22af560548fcf5399e41babef05d5` -**Observed date:** 2026-08-29 -**Native work to preserve:** change `14d01e7a-b64d-4b7f-b856-e3366885b381` - -> **Execution rule:** The coding agent must consume this file before doing repository discovery. Items marked **PROVEN / STABLE** are already discovered facts and should not be re-researched unless the referenced file/hash has changed. Items marked **REMEASURE** are intentionally deferred because they are time-sensitive machine state. -> -> This preflight contains exact legacy identifiers and paths solely so they can be removed correctly. It is an execution artifact. Before the final repository-wide forbidden-reference gate, either remove this file from the tracked tree or replace it with a sanitized post-migration record that contains no retired product identifiers. - ---- - -# 1. Start here — what is already known - -Do **not** begin by searching the repository generally. The following work has already been completed: - -1. Current master/source authority was reconciled. -2. The legacy product contamination was searched across the repository. -3. CI/validation coupling was identified. -4. Runtime control functions that can still invoke the retired application were identified. -5. SQL/data-model legacy identifiers were identified. -6. The abandoned source-built emulator storage root was identified. -7. Redundant TFT project identities were inspected. -8. The MIT licensing boundary was inspected. -9. A new native Xcode/AppKit/Metal TFTMAC Gate 1 implementation was built successfully. -10. Native unit tests passed. -11. The exact installed Android Emulator controller protocol was frozen from the working stock runtime. - -The implementation agent should use the maps below as the starting point and spend discovery effort only where this preflight explicitly says a gap remains. - ---- - -# 2. Current authority and branch state - -## 2.1 Repository authority — PROVEN / STABLE at this preflight - -```text -project: TFTMAC -repository: flashls1/TFTMAC -default branch: master -master at preflight creation: 0bb14a566be22af560548fcf5399e41babef05d5 -implementation plan: TFTMAC_CLEAN_OWNERSHIP_PLAN.md -``` - -The clean-ownership plan was merged by PR #4. - -## 2.2 Native implementation work — MUST PRESERVE - -```text -change_id: 14d01e7a-b64d-4b7f-b856-e3366885b381 -branch: clara/implement-tftmac-native-mac-app-from-tft-14d01e7a -base/head at last observation: 4e5e62b3410426e06f17c52d2b045a221adcdb52 -worktree: -/Volumes/MAC MINI M4/Clara/Worktrees/flashls1--tftmac/tftmac--14d01e7a-b64d-4b7f-b856-e3366885b381 -state: dirty worktree containing new Gate 1 source -``` - -**Hard rule:** checkpoint this change before any destructive storage cleanup or worktree retirement. The code listed in section 3 exists only in this active change until checkpointed/merged. - ---- - -# 3. Native Gate 1 code already written and proven - -These files are not design suggestions. They are already present in the active native change and were compiled/tested successfully. - -## 3.1 App entry point - -**File:** `TFTMAC/App/TFTMACApplication.swift` -**SHA-256:** `463b4a4f83d3f777fb2f3a26f905c2a8cf44aa56a61d6fca27e9e03a27db1de7` - -Current implementation: - -```swift -import AppKit - -@main -enum TFTMACApplication { - @MainActor private static var coordinator: AppCoordinator? - - @MainActor - static func main() { - let application = NSApplication.shared - let coordinator = AppCoordinator() - Self.coordinator = coordinator - application.delegate = coordinator - application.setActivationPolicy(.regular) - application.run() - } -} -``` - -**Important prior compiler finding:** Swift 6 rejected the retained coordinator as unsafely shared mutable state until it was isolated to `@MainActor`. Do not remove that actor isolation casually. - -## 3.2 Application coordinator - -**File:** `TFTMAC/App/AppCoordinator.swift` -**SHA-256:** `054912c1da75cb1cab40a838de2d4112559ff516a0019efeac792aca452aa89f` - -Current behavior: - -- `@MainActor` -- owns `MainWindowController` -- creates/shows the main window on launch -- activates the app -- terminates after last window closes - -## 3.3 Main window - -**File:** `TFTMAC/App/MainWindowController.swift` -**SHA-256:** `0a257cb75580f3196c3a24869ef737d343b9579bffb3693c09ff61ad9126f875` - -Current facts: - -```text -initial content size: 1280 x 720 -minimum size: 960 x 540 -title: TFTMAC -style: titled / closable / miniaturizable / resizable / fullSizeContentView -fullscreen behavior: fullScreenPrimary -content view: EmbeddedEmulatorView -``` - -## 3.4 Metal presentation shell - -**File:** `TFTMAC/Presentation/EmbeddedEmulatorView.swift` -**SHA-256:** `1ea417309119395873c36daf723ad2b6f3a32bf1b1cb8e0be3f58176e259ecf5` - -Current proven shell: - -```text -base class: MTKView -Metal device: MTLCreateSystemDefaultDevice() -pixel format: bgra8Unorm_srgb -preferred FPS: 60 -framebufferOnly: true -continuous drawing: enabled -``` - -It currently clears/presents the drawable; real emulator frame ingestion is intentionally not implemented yet. Do not mistake this for completed embedded rendering. - -## 3.5 Viewport/input mapper - -**File:** `TFTMAC/Presentation/ViewportMapper.swift` -**SHA-256:** `a5a3158be2fb5d0513dbba4298c4849553c2b06df4e429e24430bb70d336984d` - -Already implements: - -- aspect-fit rectangle calculation; -- centered letterbox handling; -- viewport point -> source coordinate conversion; -- rejection of input outside displayed game content. - -This is the intended base for 1920x1080 Android coordinate mapping. - -## 3.6 Native Gate 1 tests - -**File:** `Tests/TFTMACTests/TFTMACGate1Tests.swift` -**SHA-256:** `b5bba4165b4c3db761a2d8ddd23a1952e59c0fe956da19260fe19f71bda7e01b` - -Existing tests prove: - -```text -1920x1080 -> 1600x900 fills exact 16:9 viewport -letterbox area produces no Android touch -viewport center maps to source 960,540 -``` - -These tests are useful regression protection and should survive ownership cleanup. - -## 3.7 Build evidence — PROVEN - -Successful native Release build operation: - -```text -operation: tftmac-native-gate1-build-after-mainactor-20260829 -result: SUCCEEDED -exit_code: 0 -``` - -Successful native test operation: - -```text -operation: tftmac-native-gate1-tests-quiet-20260829 -result: SUCCEEDED -exit_code: 0 -``` - -The first test attempt hit Clara's command-output ceiling due verbose Xcode output, not a logical test failure. `scripts/test-native-app.command` was changed to quiet Xcode output and then succeeded. Preserve quiet-output behavior. - ---- - -# 4. Native Xcode/dependency facts already resolved - -## 4.1 Xcode project - -`TFTMAC.xcodeproj` exists in the active native change. - -Observed build configuration facts: - -```text -architecture: arm64 -Swift: 6.0 -macOS deployment target in generated project: 15.0 -Release optimization: -O / whole-module -``` - -Do not recreate the project from scratch unless it is proven corrupt. - -## 4.2 Swift package versions resolved successfully - -The first successful resolution/build proved this dependency set works together: - -```text -grpc-swift-2 2.4.2 -grpc-swift-nio-transport 2.9.1 -grpc-swift-protobuf 2.4.1 -SwiftProtobuf 1.38.1 -``` - -Transitive versions are frozen in: - -```text -TFTMAC.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved -``` - -Do not research substitute gRPC libraries before using this already-resolved graph. - ---- - -# 5. Installed Android Emulator controller authority already frozen - -## 5.1 Proven installed runtime - -**Installed emulator version:** - -```text -Android emulator version 37.1.11.0 -build_id 15917651 -``` - -**Installed binary:** - -```text -/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk/emulator/emulator -``` - -## 5.2 Controller proto authority - -**Installed proto:** - -```text -/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk/emulator/lib/emulator_controller.proto -``` - -**Vendored proto:** - -```text -Vendor/AndroidEmulator/emulator_controller.proto -``` - -**SHA-256 for both:** - -```text -1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f -``` - -**Provenance file:** `Vendor/AndroidEmulator/SOURCE.json` -**SHA-256:** `936aed845d7688a71c25daa4d1b1136f9f2b1766fadbca4f50bce4d20051c6f3` - -Known source authority recorded there: - -```text -emu branch: emu-master-dev -qemu commit: ae9d18d2b6261179fbd57fffec720a04f7bfb053 -aemu commit: 3c1ced8a369417db591eb7cd083af5bb2c317975 -manifest SHA-256: 28865cd8a162178ba462b296f5714b6b7b6916e0cafcddfc0c5e44aa03f8e8d3 -``` - -**Implementation rule:** generate/use the client from this frozen installed protocol. Do not fetch a random current AOSP proto and assume compatibility. - ---- - -# 6. Current legacy validation coupling — exact locations - -This is a required early cutover because deleting old source before replacing validation will deliberately break CI/Clara validation. - -## 6.1 GitHub CI - -**File:** `.github/workflows/ci.yml` - -Current steps include: - -```text -Validate repository policy and metadata -> ./scripts/verify-repository.command -Run launcher unit tests and production typecheck -> ./scripts/test-mactician.command -``` - -The old test invocation is at approximately line 29 in the current file. - -**Action:** REWRITE CI before deleting the old launcher/test tree. - -Target replacement from implementation plan: - -```text -./scripts/verify-tftmac.command -native Xcode Release build -native XCTest -frozen proto verification -Package.resolved verification -SQL/SSOT integrity checks that remain current -forbidden-reference scan -``` - -## 6.2 Clara registered production validator - -The Clara TFTMAC project validator is also currently wired to: - -```text -./scripts/test-mactician.command -``` - -**Action:** update Clara project validation only after the new TFTMAC verifier passes on the exact implementation change. - ---- - -# 7. Runtime-control legacy code — exact hotspots already found - -**Primary file:** `tools/tftmac-direct-control.mjs` - -Do not perform a blind file-wide rename. Use the classifications below. - -## 7.1 Baseline profile naming — MIGRATE/RENAME, preserve values - -Current symbol: - -```text -DONOR_PROFILE -``` - -Definition begins around line 21. - -The symbol is used throughout: - -```text -prepareDonorAVD -startDonorEmulator -donorRuntimeState -startDonorControl -window-fit functions -shutdown restore logic -``` - -Current runtime values represented by that profile are useful TFTMAC baseline facts; preserve values while moving them to TFTMAC-owned names such as: - -```text -BASELINE_PROFILE -prepareBaselineAVD -startBaselineEmulator -baselineRuntimeState -startBaselineControl -``` - -## 7.2 Legacy baseline IDs — SQL-aware migration required - -Current IDs found in code/data: - -```text -mactician_compatible_official_v0 -mactician_compatible_5gb_v1 -mactician_compatible_5gb_flush400_v1 -``` - -Known code locations include approximately: - -```text -tools/tftmac-direct-control.mjs:22 -tools/tftmac-direct-control.mjs:1406 -tools/tftmac-direct-control.mjs:1895 -tools/tftmac-direct-control.mjs:2140-2141 -tools/tftmac-direct-control.mjs:2257 -tools/tftmac-direct-control.mjs:3995 -tools/tftmac-direct-control.mjs:4295 -tools/tftmac-direct-control.mjs:4731-4732 -``` - -Target names proposed in the plan: - -```text -tftmac_official_baseline_v1 -tftmac_5gb_baseline_v1 -tftmac_5gb_flush400_exp_v1 -``` - -**Do not plain-text replace SQL keys.** See section 9. - -## 7.3 Old app launch/stop/audit functions — DELETE - -Known functions/dispatches: - -```text -launchMacticianControl() around line 4546 -stopMacticianControl() around line 4557 -macticianRuntimeAudit() later in same section -'action === launch-mactician-control' around line 4725 -'action === stop-mactician-control' around line 4726 -``` - -`tools/tftmac-v2.mjs` also currently allowlists those actions around line 1578. - -**Action:** DELETE the functions and remove them from every command allowlist/usage string. They must not remain callable under a renamed alias. - -## 7.4 Old installed-app/Application Support probes — DELETE - -Known current paths in `tools/tftmac-direct-control.mjs`: - -```text -/Applications/Mactician.app -~/Library/Application Support/Mactician/sdk/platform-tools/adb -/Volumes/MAC MINI M4/Mactician/sdk/platform-tools/adb -~/Library/Application Support/Mactician/logs -/Volumes/MAC MINI M4/Mactician -``` - -These are used by old process/audit/ADB cleanup functions. - -**Action:** remove these discovery paths entirely after confirming no current TFTMAC runtime function depends on them. - -## 7.5 Legacy icon fallback — DELETE/REPLACE - -Known current line near 4473: - -```js -const iconSource = path.join(repoRoot, 'branding', 'generated', 'Mactician.icns'); -``` - -This is a direct path from the current TFTMAC build fallback to retired branding. - -**Action:** replace with a TFTMAC-owned icon or omit icon copy until a TFTMAC asset exists. Never retain the old asset as fallback. - ---- - -# 8. Legacy product source/assets — known removal groups - -Repository search returned hundreds of legacy references. The following groups are already identified and should be processed directly, not rediscovered from scratch. - -## 8.1 Launcher implementation — DELETE unless a specific current TFTMAC dependency is proven - -```text -launcher/Sources/* -launcher/Tests/* -launcher/Resources/* -launcher/EmulatorHost/* -launcher/Info.plist -``` - -Known embedded identities include: - -```text -dev.sergeinaumov.mactician -dev.sergeinaumov.mactician.game-host -``` - -These appear in `launcher/Info.plist`, `launcher/Resources/EmulatorHost-Info.plist`, source queue labels, tests, keychain/defaults metadata, and helper targets. - -The preferred action is deletion of the obsolete launcher layer, not wholesale renaming of dead source. - -## 8.2 Legacy branding — DELETE - -Known source assets: - -```text -branding/mactician-app-icon.svg -branding/mactician-favicon.svg -branding/mactician-game-host-icon.svg -branding/mactician-mark.svg -branding/mactician-mark-dark.svg -branding/mactician-mark-light.svg -branding/mactician-mark-monochrome.svg -branding/mactician-open-graph.svg -branding/mactician-product-hero.svg -branding/mactician-small-size-test.svg -branding/mactician-social-preview.svg -branding/mactician-wordmark.svg -branding/generated/Mactician.icns -``` - -Also remove related generation scripts and generated PNG/ICO files. - -## 8.3 Legacy build/release/update scripts — DELETE - -Known obsolete scripts include: - -```text -scripts/build-mactician.command -scripts/build-mactician-icns.pl -scripts/generate-mactician-assets.command -scripts/integration-test-mactician.command -scripts/publish-mactician-update.command -scripts/test-mactician.command -``` - -Other scripts must be reviewed if they reference legacy Application Support, bundle IDs, old Game Host, hosted game updates, or legacy profile markers. - -## 8.4 Legacy docs/metadata — REWRITE - -Known files currently centered on the old product: - -```text -README.md -CHANGELOG.md -CONTRIBUTING.md -SUPPORT.md -SECURITY.md -NOTICE.md -docs/architecture.md -docs/building.md -docs/releasing.md -docs/reproducibility.md -docs/telemetry.md -docs/troubleshooting.md -.github/pull_request_template.md -.github/repository-metadata.yml -.github/ISSUE_TEMPLATE/bug_report.yml -``` - -Rewrite them for TFTMAC rather than leaving donor-era product instructions. - ---- - -# 9. SQL and evidence migration map — do not rediscover the key problem - -## 9.1 Performance lab - -**File:** `ssot/TFTMAC_PERFORMANCE_LAB.sql` - -Known current metadata/key references: - -```text -line ~527 current_playable_baseline -> mactician_compatible_official_v0 -line ~559 baseline config row -line ~560 5 GiB candidate -line ~561 flush400 candidate -line ~574 exp_ram_5gb_ab baseline/candidate references -line ~575 exp_native_frame_trace baseline reference -line ~578 baseline_config_id update -line ~585 supersession note -line ~597 exp_asg_flush400_ab references -line ~598 performance mode A/B baseline reference -line ~599 FPS cap A/B baseline reference -line ~600 graphics preset A/B baseline reference -line ~640 latest closed session label -line ~647 current graphics experiment label -``` - -**Required implementation:** transactional/idempotent relational migration. Preserve measurements, timestamps, experiment relationships and verdicts. Run `PRAGMA foreign_key_check` before commit. - -## 9.2 Engineering map - -**File:** `ssot/TFTMAC_ENGINEERING_MAP.sql` - -Known legacy-heavy areas include approximately: - -```text -~266 tftmac_shell note -~662-672 donor/version/evidence component rows -~697-749 donor documents/version stacks -~792 deployment-target decision tied to donor evidence -~888 version strategy history -~963-1000 legacy docs/runtime profile rows and experiment records -~1021-1025 donor artifact registry -~1344 current presentation candidate naming -~1358 latest closed run naming -~1364 current graphics next action naming -``` - -**Action classification:** - -- current product decisions that TFTMAC has independently reproduced -> rewrite as TFTMAC evidence; -- historical donor-only rows no longer needed for current decision -> remove/compact; -- negative experimental facts still protecting against regression -> preserve result but remove retired product identity where legally/semantically possible; -- relational IDs -> migrate carefully, not string replace. - ---- - -# 10. Current package/update authority — already decided - -Do not rediscover or redesign the package lifecycle. - -Current product authority is: - -```text -Android guest: official Google Play image -TFT package: com.riotgames.league.teamfighttactics -installer/update authority: com.android.vending -Riot content initialization/patching: official Riot application behavior -``` - -Forbidden to reintroduce: - -```text -hosted TFT APK feed -bundled game APK -third-party game update service -APK repacking -APK re-signing -binary patching -manual Riot content patch bundle -``` - -Existing runtime tooling already has package-state/Play diagnostics that should be kept where they operate on TFTMAC's stock runtime. - ---- - -# 11. Storage map already discovered - -## 11.1 Protected runtime — KEEP - -The working runtime is under: - -```text -/Volumes/MAC MINI M4/TFTMAC/Runtime -``` - -Known protected sub-authorities include: - -```text -/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk -/Volumes/MAC MINI M4/TFTMAC/Runtime/AVD -``` - -The current working AVD/product state must not be deleted during cleanup. - -Also preserve: - -```text -Google Play account/session state in AVD -official TFT package and Riot data in AVD -current runtime manifests required by stock control -current package authority evidence -Vendor/AndroidEmulator protocol snapshot -native source/worktree -``` - -## 11.2 Abandoned source-build tree — PRIMARY RECLAIM CANDIDATE - -Known path: - -```text -/Volumes/MAC MINI M4/TFTMAC/Build -``` - -Known contents/uses from earlier work: - -```text -repo launcher -AEMU multi-repo checkout -external/qemu source -prebuilts -CMake/Ninja objects -source-build compatibility wrappers -reference/CTS trees -phase1 build logs -failed worker/source-build state -``` - -Project research recorded the development tree around **131 GB** historically. - -Known source references that can recreate/use it: - -```text -TFTMAC_FULL_IMPLEMENTATION_PLAN.md ~261-262, 481-496 -ssot/phase0-remediation-inventory.md ~43,65 -ssot/phase0-source.json ~31+ -ssot/TFTMAC_ENGINEERING_MAP.sql ~314-315,444,670 -tools/tftmac-v2.mjs constants and Phase 0/Phase 1 source-build functions -``` - -**Hard sequence:** remove/disable current recreation paths first; prove stock runtime/native build independence; then delete the exact Build candidate. - -## 11.3 Native build caches — DISPOSABLE AFTER CHECKPOINT - -The active native worktree currently contains large generated `.build/native-release` and `.build/native-tests` trees from Xcode/SwiftPM. - -They are useful proof that compilation happened but are not source authority. - -After source is checkpointed and Package.resolved is retained, these generated caches are reclaimable and can be regenerated. - -Do not treat generated Xcode object files as required evidence. - -## 11.4 Exact size/free space — REMEASURE - -No exact current byte count was frozen because the live disk probe was rejected by Clara allowlist during the earlier preflight. - -At execution time, measure: - -```text -free bytes before cleanup -Build tree bytes -Runtime tree bytes -capture/diagnostic bytes -native .build/DerivedData bytes -redundant project/worktree bytes -free bytes after each tier -exact reclaimed bytes -``` - -This is a machine-state remeasurement, not repository rediscovery. - ---- - -# 12. Redundant project state already inspected - -## 12.1 TFTMAC2 — RETIREMENT CANDIDATE - -Observed project: - -```text -project_id: tftmac2 -repository: flashls1/tftmac2 -head: 30f9695b757551bad9fc788423e031e46a89f43d -active changes: 0 -operations: 0 -release: none -``` - -Contents observed as minimal foundation files only (`facts.md`, `AGENTS.md`, `README.md`, `.clara/project.json`). - -**Required remaining work:** one unique-content/commit check, then retire/archive/remove from active Clara catalog. - -Do not perform broad rediscovery. - -## 12.2 TFTMAC Runtime — UNIQUE-CONTENT REVIEW REQUIRED - -Observed project: - -```text -project_id: tftmac-runtime -repository: flashls1/tftmac-runtime -main head: a121c2681132428c0b3e04d73d748ffae1b3ffe2 -``` - -One published but unmerged managed change exists: - -```text -change_id: 7c24d6df-bb78-4a4d-9c8f-1b4028871f60 -branch: clara/phase-2-compatibility-laboratory-reevalu-7c24d6df -head: 994cf8d8524b87cc03c7decb4b253fc7be2253c7 -state: published / clean -``` - -**Required remaining work:** diff that exact head against current TFTMAC knowledge and classify only unique items: - -```text -MIGRATE_TO_TFTMAC -ALREADY_SUPERSEDED -REJECTED_OBSOLETE -``` - -Do not rerun the old compatibility laboratory just to understand the branch. - ---- - -# 13. Licensing boundary already discovered - -**Current `LICENSE`:** MIT license with copyright: - -```text -Copyright (c) 2026 Sergei Naumov -``` - -**Current `NOTICE.md`:** describes the old product and third-party components. - -The implementation agent must not assume that removing branding permits removing legally required copyright/license notices from substantial retained source. - -Efficient path: - -1. delete obsolete copied implementation wherever TFTMAC no longer needs it; -2. identify any substantial retained upstream-derived portions; -3. retain required MIT notice for those portions in neutral third-party notices; -4. rewrite/remove obsolete product-specific NOTICE text; -5. if no substantial licensed upstream portion remains, update project licensing metadata truthfully. - -This is a code-origin audit, not a branding preservation requirement. - ---- - -# 14. Known exact legacy identifier groups - -The repository currently contains the retired product token in hundreds of locations. Do not spend a new session proving that fact. - -Known categories already established: - -```text -product name in README/docs -bundle IDs -Application Support paths -keychain/defaults/logging subsystem names -launcher source/tests -helper Game Host app -build/release/update scripts -branding source/generated assets -CI and PR templates -runtime control actions -runtime process detection -profile/config IDs -performance-lab keys -engineering-map evidence rows -historical experiment scripts -old update URLs/feed logic -``` - -Final verification still must run a full forbidden-reference scan because implementation may miss an occurrence, but that scan is **validation**, not discovery. - ---- - -# 15. What to keep from current `tools/tftmac-direct-control.mjs` - -Ownership cleanup should not throw away the working TFTMAC runtime harness. - -Known useful current capabilities to retain under TFTMAC naming include: - -```text -runtime inventory/discovery -AVD preparation for stock runtime -stock emulator start/stop -Google Play account UI -Play certification/diagnostics -package-state capture -TFT launch/restart/status -CoreAudio health/probes -logger health -raw-first capture sealing -fast closed-run analysis -continuous-run analysis -SQLite normalization -SurfaceFlinger counters -native trace capability -window/presentation probes until native embedded replacement is proven -Android back/home/overview/power/volume/rotate/screenshot controls where still used -runtime process audit for TFTMAC/emulator/ADB -native Gate 1 bootstrap/build/test actions added in active native change -``` - -The cleanup target is legacy product coupling, not wholesale deletion of the proven TFTMAC harness. - ---- - -# 16. Already rejected/obsolete paths — do not restart them - -Do not restart source-built AEMU merely because its source tree exists. - -Known obsolete research path facts: - -```text -AEMU source checkout was moved to external Build root due internal-disk exhaustion -source build entered a 9,854-step Ninja compile -one observed failure was old macOS deployment-target/toolchain compatibility -later project direction explicitly chose released stock Emulator control instead -stock Emulator 37.1.11 is already working -native client freezes installed EmulatorController contract directly -``` - -Unless a new measured blocker proves a released-emulator limitation that cannot be solved otherwise, source-building AEMU is out of scope. - ---- - -# 17. Implementation order — optimized to avoid duplicated work - -The implementation agent should execute in this order: - -## A. Preserve current native source - -1. review `14d01e7a...`; -2. checkpoint its source files; -3. preserve `Package.resolved` and proto provenance; -4. exclude generated `.build` products from the checkpoint unless intentionally tracked. - -## B. Cut validation over before deleting old tests - -1. create `scripts/verify-tftmac.command`; -2. make it build/test the native Xcode target and run current static/SQL checks; -3. run it locally; -4. update GitHub CI; -5. update Clara project validation; -6. prove the new validator passes. - -## C. Remove obsolete product layers - -Use sections 7-9 and 14 as the source map. Delete old launcher/updater/branding paths; rename only active TFTMAC baseline concepts; migrate SQL relationally. - -## D. Prove runtime independence - -Use existing stock-runtime inventory/package/proto probes. Do not rebuild the runtime. Prove no required path resolves through `/Volumes/MAC MINI M4/TFTMAC/Build` or the retired installed application. - -## E. Measure and reclaim storage - -Remeasure exact machine bytes, classify candidates, delete by tier, and smoke-test after the large Build deletion. - -## F. Retire redundant projects - -TFTMAC2 first after unique-content check. TFTMAC Runtime only after exact `994cf8...` unique-content migration/supersession review. - -## G. Resume native Gate 2 - -Continue from the already-built Gate 1 Xcode project into authenticated hidden-emulator `EmulatorController` status/control. Do not recreate Gate 1. - ---- - -# 18. Mandatory implementation-agent no-rediscovery rules - -The coding agent MAY re-check a fact only when: - -```text -the referenced source hash changed; -the branch/head moved materially; -a required file no longer exists; -a runtime/storage fact is explicitly marked REMEASURE; -a validation run contradicts this preflight; -or a current compile/runtime error proves the recorded fact stale. -``` - -The coding agent MUST NOT: - -```text -repeat broad donor research; -repeat old source-build feasibility experiments; -redesign the dependency stack before using the resolved one; -rebuild the Xcode project from scratch; -redo the legacy-reference inventory as its first task; -retest whether the old launcher is useful; -rerun the old compatibility laboratory to understand its branch; -recreate the abandoned Build tree to inspect it; -reinstall the retired application for comparison; -``` - -A final repository-wide scan after implementation is required as validation, but it must be treated as a completeness gate rather than a new discovery phase. - ---- - -# 19. Remaining truthful unknowns - -These are the only material items intentionally left for execution-time resolution: - -1. **REMEASURE:** exact current bytes used by `/Volumes/MAC MINI M4/TFTMAC/Build`. -2. **REMEASURE:** exact free bytes before/after cleanup. -3. **REMEASURE:** exact bytes in captures/native build caches/redundant project roots. -4. **REVIEW:** unique content in `tftmac-runtime` head `994cf8d8524b87cc03c7decb4b253fc7be2253c7` not already represented in TFTMAC. -5. **LEGAL REVIEW:** which substantial upstream-derived source portions, if any, remain after deletion/replacement and therefore require MIT attribution. -6. **IMPLEMENTATION:** generated Swift protobuf/gRPC client sources from the frozen installed proto are not yet committed. -7. **IMPLEMENTATION:** native Gate 2 hidden authenticated EmulatorController connection/status/shutdown is not yet complete. - -Everything else above should be treated as existing engineering knowledge, not an invitation to start over. - ---- - -# 20. Preflight completion criteria - -This preflight is considered successfully consumed when the implementation agent can begin with the following statement of fact: - -```text -I know which current native files already compile and test, -which exact old source groups are obsolete, -which runtime functions need delete vs rename, -which SQL IDs require relational migration, -which validation paths must be cut over first, -which runtime/storage paths are protected, -which bulk Build path is the primary reclaim target, -which project identities are retirement candidates, -and which facts must be remeasured rather than rediscovered. -``` - -At that point implementation should begin. No second discovery phase is required. diff --git a/TFTMAC_FULL_IMPLEMENTATION_PLAN.md b/TFTMAC_FULL_IMPLEMENTATION_PLAN.md index 242b15b..29c8b06 100644 --- a/TFTMAC_FULL_IMPLEMENTATION_PLAN.md +++ b/TFTMAC_FULL_IMPLEMENTATION_PLAN.md @@ -1,1474 +1,12 @@ -# TFTMAC GPU Runtime — Full Implementation Plan +# Historical Source-Build Plan -**Version:** 2.0 revised after ZenGate v2.3 remediation -**Date:** 2026-08-26 -**Governing SSOT:** `TFTMAC_GPU_RUNTIME_SSOT.md` v2.0 -**Status:** Execution plan; implementation must not begin until this plan passes ZenGate v2.3 -**Project:** One project only — `TFTMAC` +**Status: historical — do not execute as normal-play work.** ---- +The former source-built AEMU plan is archived at +`docs/history/2026-08-31-pre-build8/TFTMAC_FULL_IMPLEMENTATION_PLAN.md`. -# 0. Concrete win condition - -Build and release one Apple-Silicon Android 3D runtime that: - -- uses the official Android 17 Google Play ARM64 guest; -- uses source-built AEMU from one frozen `emu-master-dev` manifest; -- transports required Vulkan capability through gfxstream; -- provides a genuine OpenGL ES 3.2 context through built-in Android ANGLE without nonconformant version exposure; -- renders through MoltenVK/Metal; -- runs the current official Google Play TFT client at 1920×1080; -- meets the frozen performance and latency contract; -- supports audio, microphone transport, input, diagnostics, update, and rollback; -- presents as one native macOS application with emulator chrome hidden; -- passes the frozen non-TFT generality corpus. - -No phase is complete merely because a command exits 0. Each phase has an evidence artifact and a behavioral exit gate. - ---- - -# 1. Fixed architecture - -```text -Official Android 17 Google Play ARM64 guest - | - +-------+-------+ - | | - GLES Native Vulkan - | | - Android built-in | - ANGLE/Vulkan | - +-------+-------+ - | - gfxstream - | - source-built AEMU - | - selected MoltenVK - | - Metal - | - Apple Silicon GPU - | - native TFTMAC shell -``` - -Normal production uses one Google Play AVD. - -A rootable execution guest and custom ANGLE are a **conditional repair adapter only**. They are not prebuilt or maintained unless Phase 5 proves built-in ANGLE itself is the remaining blocker. - ---- - -# 2. Phase map - -```text -PHASE 0 — Authority + preflight freeze -PHASE 1 — Build unmodified source AEMU -PHASE 2 — Build capability laboratory -PHASE 3 — Make host Vulkan capability real -PHASE 4 — Make gfxstream preserve required Vulkan capability -PHASE 5 — Prove genuine built-in-ANGLE GLES 3.2 -PHASE 6 — Current official TFT vertical slice -PHASE 7 — Performance, diagnostics, audio, mic, input -PHASE 8 — Native macOS presentation -PHASE 9 — Update, rollback, generality, release -``` - -Each phase changes only the layer that owns the current failure. - ---- - -# PHASE 0 — Authority and complete preflight freeze - -## Objective - -Resolve every drift-sensitive critical input before implementation code changes. - -At Phase 0 exit: - -```text -STACK.lock.yaml contains no unresolved critical-path field. -``` - -## 0.1 Host identity - -Require: - -```text -Apple Silicon arm64 -supported macOS for Xcode 26.6 -Xcode 26.6 / 17F113 -``` - -Record: - -```bash -uname -m -sw_vers -xcodebuild -version -xcrun --sdk macosx --show-sdk-path -system_profiler SPHardwareDataType -``` - -Write: - -```text -ssot/host-preflight.json -``` - -## 0.2 Canonical local roots - -Bulk build/runtime data is external and mandatory: - -```text -/Volumes/MAC MINI M4/TFTMAC/ -├── Build/ -└── Runtime/ - ├── SDK/ - ├── AVD/ - ├── Packages/ - ├── Probes/ - ├── Manifests/ - └── VulkanSDK/ -``` - -Small control/log state may remain internal: - -```text -~/Library/Application Support/TFTMAC/ -├── Logs/ -├── Diagnostics/ -└── Rollback/ -``` - -If `/Volumes/MAC MINI M4` is not mounted, TFTMAC fails closed and must not create Build or Runtime data on the internal disk. No source file hardcodes the current developer checkout path. - -## 0.3 Android command-line tools - -Download: - -```text -commandlinetools-mac_arm64-15859902_latest.zip -``` - -Verify: - -```text -SHA-256 -835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e -``` - -Install into TFTMAC-local SDK root. - -Do not use the user's global Android SDK as production authority. - -## 0.4 SDK packages - -Using the TFTMAC-local `sdkmanager`, install: - -```text -platform-tools -emulator -platforms;android-37.1 -build-tools;37.0.0 -system-images;android-37.0;google_apis_playstore_ps16k;arm64-v8a -``` - -Run: - -```bash -sdkmanager --list_installed -``` - -Freeze exact revisions. - -API 37 Google Play image must be revision >= 5. - -Write: - -```text -ssot/android-sdk-packages.txt -``` - -## 0.5 Production AVD - -Create: - -```bash -avdmanager create avd \ - --name TFTMAC_Live_API37 \ - --package "system-images;android-37.0;google_apis_playstore_ps16k;arm64-v8a" \ - --device pixel_tablet \ - --force -``` - -Normalize configuration: - -```text -AvdId=TFTMAC_Live_API37 -avd.ini.displayname=TFTMAC Live -hw.device.manufacturer=Google -hw.device.name=pixel_tablet -hw.initialOrientation=Landscape -hw.cpu.arch=arm64 -hw.cpu.ncore=8 -hw.ramSize=8192 -hw.vmHeapSize=768 -hw.lcd.width=1920 -hw.lcd.height=1080 -hw.lcd.density=280 -hw.gpu.enabled=yes -hw.gpu.mode=host -hw.audioInput=yes -hw.keyboard=yes -showDeviceFrame=no -skin.name=1920x1080 -disk.dataPartition.size=16G -runtime.network.speed=full -runtime.network.latency=none -PlayStore.enabled=true -fastboot.forceColdBoot=yes -fastboot.forceFastBoot=no -``` - -## 0.6 Vulkan developer SDK - -Install: - -```text -Vulkan SDK 1.4.357.0 -SHA-256: -539433589c83522e6f31b1c7b418a4167e21597a4a361ab119e1dc0760cf3865 -``` - -Require: - -```bash -vulkaninfo --summary -``` - -## 0.7 AEMU source authority - -The project uses only: - -```text -emu-master-dev -``` - -Initialize: - -```bash -mkdir -p "/Volumes/MAC MINI M4/TFTMAC/Build/aemu" -cd "/Volumes/MAC MINI M4/TFTMAC/Build/aemu" - -repo init \ - -u https://android.googlesource.com/platform/manifest \ - -b emu-master-dev - -repo sync -c -j8 -``` - -Freeze: - -```bash -repo manifest -r > "$TFTMAC_REPO/ssot/upstreams-aemu.lock.xml" -shasum -a 256 "$TFTMAC_REPO/ssot/upstreams-aemu.lock.xml" -``` - -Record exact commits: - -```bash -git -C external/qemu rev-parse HEAD -git -C hardware/google/aemu rev-parse HEAD -git -C hardware/google/gfxstream rev-parse HEAD -git -C external/angle rev-parse HEAD -git -C external/moltenvk rev-parse HEAD -``` - -Write them into `STACK.lock.yaml`. - -## 0.8 GuestAngle source-authority assertion - -Inspect the exact locked QEMU revision: - -```text -external/qemu/android/android-emu/android/userspace-boot-properties.cpp -``` - -The preflight script must prove the locked source implements materially equivalent GuestAngle behavior to: - -```text -hardware EGL -> angle -hardware Vulkan -> ranchu/guest Vulkan path -nonconformant ES version exposure not automatically enabled -``` - -Write: - -```text -ssot/guestangle-authority.json -``` - -The JSON contains: - -```text -qemu commit -source file hash -GuestAngle symbol/location -hardware EGL value -hardware Vulkan value -nonconformant exposure policy -PASS/FAIL -``` - -If FAIL, no code mutation begins. The source review updates both planning documents before execution. - -This is the only source-authority stop condition. - -## 0.9 MoltenVK reference lock - -Freeze: - -```text -MoltenVK v1.4.2 -``` - -Store reference checkout: - -```text -Build/references/MoltenVK-1.4.2 -``` - -Record exact commit. - -Do not yet replace the manifest-integrated MoltenVK. - -## 0.10 Freeze the generality corpus - -### OpenGL ES - -Pin: - -```text -Khronos VK-GL-CTS -opengl-es-cts-3.2.14.1 -Apache-2.0 -``` - -Resolve and lock full commit before implementation: - -```bash -git rev-list -n 1 opengl-es-cts-3.2.14.1 -``` - -### Vulkan CTS - -Pin: - -```text -vulkan-cts-1.4.6.1 -5c8aae22885448d70a2873e94a93b24b49505c32 -Apache-2.0 -``` - -Create `ssot/vulkan-required-cases.txt` before implementation. - -The case list must include only cases required by: - -- TFTMAC guest Vulkan probe; -- GLES 3.2 feature dependencies; -- draw; -- compute; -- image; -- synchronization; -- dynamic rendering; -- WSI/presentation used by the runtime. - -Hash and lock the case list. - -Do not remove cases later because they fail. - -### Vulkan Samples - -Pin: - -```text -Khronos Vulkan-Samples -89dd3af22d41f9244eeab6e0650460112285c0e1 -Apache-2.0 -``` - -Frozen sample names: - -```text -dynamic_rendering -compute_nbody -``` - -## 0.11 Freeze performance contract - -Copy the SSOT performance values into `STACK.lock.yaml`: - -```text -FPS minimum: 58.0 -P95 max: 20.0 ms -P99 max: 33.334 ms -jank threshold: >33.334 ms -jank max: 1.0% -severe stall: >100 ms -severe stalls max: 3 / 600 seconds - -native input median: - <= Qt control + 5 ms - <= 50 ms absolute - -native input P95: - <= Qt control + 8.334 ms - <= 83.334 ms absolute -``` - -These values cannot be changed during tuning to make a candidate pass. - -## 0.12 Generate complete Phase 0 artifacts - -Required: - -```text -ssot/STACK.lock.yaml -ssot/upstreams-aemu.lock.xml -ssot/host-preflight.json -ssot/android-sdk-packages.txt -ssot/tool-versions.txt -ssot/source-hashes.txt -ssot/guestangle-authority.json -ssot/vulkan-required-cases.txt -ssot/preflight-report.md -``` - -### Phase 0 exit gate - -PASS only if: - -```text -host = supported -all downloaded hashes = verified -Android image = exact and rev >=5 -AEMU = emu-master-dev -resolved manifest = frozen -GuestAngle locked-source semantics = PASS -MoltenVK reference = frozen -GLES CTS full commit = frozen -Vulkan CTS = frozen -Vulkan required cases = frozen and hashed -Vulkan Samples = frozen -performance contract = frozen -no critical-path null remains -``` - ---- - -# PHASE 1 — Build unmodified source AEMU - -## Objective - -Prove the locked integrated source family builds and boots before graphics modification. - -## 1.1 Build - -From locked source: - -```bash -cd "/Volumes/MAC MINI M4/TFTMAC/Build/aemu/external/qemu" -android/rebuild.sh \ - --out-dir="/Volumes/MAC MINI M4/TFTMAC/Build/aemu-out" -``` - -## 1.2 Unit tests - -```bash -cd "/Volumes/MAC MINI M4/TFTMAC/Build/aemu-out" -ctest -j8 --output-on-failure -``` - -Also: - -```bash -cd "/Volumes/MAC MINI M4/TFTMAC/Build/aemu/external/qemu" -python android/build/python/cmake.py --gfxstream -``` - -## 1.3 Boot production AVD - -Use source-built emulator: - -```bash -emulator @TFTMAC_Live_API37 \ - -port 5592 \ - -gpu host \ - -feature GuestAngle,Vulkan,GLESDynamicVersion \ - -no-snapshot \ - -no-boot-anim \ - -no-metrics -``` - -Environment: - -```text -ANDROID_ADB_SERVER_PORT=5040 -``` - -## 1.4 Verify runtime parity - -Capture: - -```text -getprop -SurfaceFlinger -display -logcat -emulator version -host renderer log -Google Play launch -``` - -### Phase 1 proof - -Artifacts: - -```text -Diagnostics/phase1-build.json -Diagnostics/phase1-boot.json -``` - -PASS: - -- source build succeeds; -- unit tests green; -- AVD boots; -- GuestAngle active; -- Google Play opens; -- no patch is yet required. - -If the source build fails, repair toolchain/build parity only. - ---- - -# PHASE 2 — Build the capability laboratory - -## Objective - -Make every graphics failure attributable without a game. - -## 2.1 Host Vulkan probe - -Native macOS executable. - -Required executable feature set: - -```text -API/device identity -geometry shader -tessellation -shader cull distance -indirect draw -descriptor indexing -timeline semaphore -synchronization2 -dynamic rendering -buffer device address -subgroups -sampler/filter behavior -``` - -Output: - -```text -Diagnostics/host-vulkan.json -``` - -## 2.2 Guest Vulkan probe - -Android ARM64 probe. - -Same schema. - -Output: - -```text -Diagnostics/guest-vulkan.json -Diagnostics/vulkan-transport-delta.json -``` - -## 2.3 Guest GLES 3.2 probe - -Package: - -```text -dev.tftmac.gpuprobe -``` - -Tests: - -```text -EGL initialization -ES3 renderable config -3.2 context creation -GL_VERSION -GL_RENDERER -GL_VENDOR -extension inventory -geometry compile/link/draw -tessellation compile/link/draw -compute -SSBO -texture buffer -cube-map array -multisample/sample shading -base vertex -fence/sync -known rendered output -``` - -Output: - -```text -Diagnostics/guest-gles32.json -Diagnostics/guest-gles32-frame.png -``` - -## 2.4 Frame/input probe - -Implements deterministic surface animation and visible state change on injected input. - -Outputs: - -```text -Diagnostics/frame-performance.json -Diagnostics/input-latency.json -``` - -## 2.5 Probe self-test - -The probe suite must intentionally detect: - -- a forced feature-disabled configuration; -- a wrong GL version; -- a corrupted expected render checksum. - -This proves the verifier can go red. - -### Phase 2 exit gate - -Every probe compiles, runs, returns structured evidence, and detects intentional negative cases. - ---- - -# PHASE 3 — Make host Vulkan capability real - -## Objective - -Provide the host Vulkan features actually required by the guest GLES 3.2 path. - -## 3.1 Baseline integrated MoltenVK - -Run host probe against the MoltenVK revision from the locked AEMU family. - -Store: - -```text -Diagnostics/moltenvk-integrated.json -``` - -## 3.2 Baseline upstream v1.4.2 - -Build: - -```bash -./fetchDependencies --macos -make macos -``` - -Run the same probe. - -Store: - -```text -Diagnostics/moltenvk-1.4.2.json -``` - -## 3.3 Select host driver base - -Algorithm: - -```text -required = host Vulkan features consumed by guest probe / GLES 3.2 dependency map - -if integrated satisfies required: - select integrated -elif v1.4.2 satisfies a strict superset and integrates cleanly: - select v1.4.2 -else: - select integrated - port only causal required improvements -``` - -Record selection in `STACK.lock.yaml`. - -## 3.4 Causal geometry/cull repair - -If a required feature such as geometry shader or shader cull distance is absent: - -1. reproduce failure in host probe; -2. inspect selected MoltenVK; -3. inspect UTM/CrossOver donor; -4. port smallest compatible implementation; -5. run direct feature test; -6. run indirect-draw interaction test; -7. reject any patch that only changes advertised feature bits. - -No other MoltenVK changes are permitted until the required host probe says they are causal. - -### Phase 3 exit gate - -All host Vulkan capabilities required by Phase 5 are executable and green. - ---- - -# PHASE 4 — Make gfxstream preserve required Vulkan capability - -## Objective - -Ensure the Android guest sees working features, not merely the host. - -## 4.1 Boot source AEMU with selected Phase 3 host Vulkan - -Features: - -```text -GuestAngle -Vulkan -GLESDynamicVersion -``` - -No nonconformant ANGLE exposure. - -## 4.2 Run guest Vulkan probe - -Generate transport delta. - -## 4.3 Repair only host-green / guest-red features - -If: - -```text -host feature = PASS -guest feature = FAIL -``` - -then gfxstream/AEMU owns the repair. - -Inspect: - -- feature filtering; -- `VkPhysicalDeviceFeatures*`; -- extension filtering; -- encoder/decoder support; -- host feature discovery; -- MoltenVK-specific host policy. - -Every patch records before/after probe state. - -Never expose a guest feature until its guest executable test passes. - -### Phase 4 exit gate - -No Vulkan capability required by the frozen GLES 3.2 dependency map is host-green / guest-red. - ---- - -# PHASE 5 — Prove genuine built-in-ANGLE GLES 3.2 - -## Objective - -Pass the exact graphics contract that current TFT requires without lying about capability. - -## 5.1 Clean production boot - -```bash -emulator @TFTMAC_Live_API37 \ - -port 5592 \ - -gpu host \ - -feature GuestAngle,Vulkan,GLESDynamicVersion \ - -no-snapshot \ - -no-metrics -``` - -Forbidden: - -```text -ANGLE_FEATURE_OVERRIDES_ENABLED=exposeNonConformantExtensionsAndVersions -``` - -## 5.2 Run guest GLES 3.2 probe - -Required: - -```text -context_requested = 3.2 -context_created = true -GL_VERSION = OpenGL ES 3.2... -ANGLE/Vulkan renderer path identified -all executable cases = PASS -known frame = PASS -``` - -## 5.3 Failure routing - -### Host Vulkan red - -Return Phase 3. - -### Host green, guest Vulkan red - -Return Phase 4. - -### Guest Vulkan required set green, built-in ANGLE still caps at 3.1 - -Now inspect the exact locked built-in ANGLE revision and requirement code. - -Only if ANGLE revision behavior itself is the blocker activate: - -```text -Custom ANGLE Adapter -``` - -## 5.4 Conditional Custom ANGLE Adapter - -This section does not execute unless 5.3 proves it necessary. - -Build host: - -```text -Ubuntu 24.04 LTS x86_64 -``` - -Build current pinned ANGLE Android arm64 artifact. - -Because TFT is non-debuggable and the Google Play guest is not a general root environment, if loose driver selection cannot be applied, create a rootable Android 17 userdebug execution image. - -Google Play AVD remains package/update authority. - -Exact Google-delivered APK splits are transferred only after package identity/signature/hash verification. - -The adapter must pass the same Phase 5 probe. - -### Phase 5 exit gate - -A real conformant GLES 3.2 context and all required executable tests pass. - ---- - -# PHASE 6 — Current official TFT vertical slice - -## Objective - -Use a real production workload after the general graphics contract is green. - -## 6.1 Official Google Play installation - -Inside `TFTMAC_Live_API37`: - -- sign into Google Play; -- install/update official TFT; -- record package metadata. - -Required manifest fields: - -```text -installer=com.android.vending -package=com.riotgames.league.teamfighttactics -versionName -versionCode -package paths -APK/split hashes -signing certificate hash -capture timestamp -``` - -## 6.2 Pre-launch gate - -TFTMAC refuses to call the workload green unless: - -```text -guest Vulkan required set PASS -guest GLES 3.2 PASS -nonconformant override absent -display 1920x1080 -runtime manifest identity current -``` - -## 6.3 Launch - -First success: - -The client no longer reports the ES 3.2 hardware-requirement error. - -## 6.4 Gameplay acceptance - -Required: - -```text -account/lobby flow reachable -current live content loads -queue succeeds -match starts -game renders -audio works -input works -20+ minute crash-free live session -``` - -### Phase 6 exit gate - -Current official TFT is genuinely playable on the final graphics architecture. - ---- - -# PHASE 7 — Performance, diagnostics, audio, microphone, input - -## Objective - -Turn functional success into measured production quality. - -## 7.1 Session diagnostics - -Every session produces: - -```text -Logs//session.json -Logs//events.jsonl -Logs//frame-times.jsonl -Logs//logcat.txt -Logs//emulator.log -Logs//summary.md -``` - -Include all locked component identities and capability hashes. - -## 7.2 Frozen TFT performance benchmark - -Measurement: - -```text -2-minute warm-up excluded -600 continuous measured seconds -60 Hz display -SurfaceFlinger timestamps primary -``` - -PASS: - -```text -Average FPS >= 58.0 -P95 <= 20.0 ms -P99 <= 33.334 ms -jank (>33.334 ms) <= 1.0% -stalls >100 ms <= 3 / 600 s -no crash -no device loss -no repeated validation-error storm -``` - -## 7.3 Candidate regression gate - -A new candidate must not regress: - -```text -Average FPS > 1 FPS -P99 > 3% -jank > 0.25 percentage points -median input-to-present > 5 ms -P95 input-to-present > 8.334 ms -``` - -and must still satisfy absolute thresholds. - -## 7.4 Resource-profile benchmark - -Run: - -```text -6 vCPU / 6 GB -8 vCPU / 8 GB -``` - -Selection: - -1. both must be measured identically; -2. any failing absolute threshold is rejected; -3. lower P99 wins; -4. if within 3%, lower host CPU wins. - -Persist winner. - -## 7.5 Renderer tuning - -Only benchmark-backed changes are allowed. - -Candidate areas: - -- MoltenVK synchronization/queue behavior; -- gfxstream batching; -- descriptor behavior; -- shader compilation; -- present pacing. - -Every change receives an A/B benchmark ID. - -## 7.6 Audio - -First use direct emulator CoreAudio. - -PASS: - -- continuous output during 20+ minute test; -- no blocking underrun condition; -- no audio crash. - -## 7.7 Microphone - -Implement: - -```text -CoreAudio input --> TFTMAC capture --> authenticated emulator controller injectAudio --> Android mic -``` - -Create a dedicated Android microphone test app. - -PASS: - -- macOS permission granted; -- selected input device identified; -- input meter responds; -- mute works; -- Android app receives audio; -- 5-minute capture/loopback has no fatal discontinuity. - -## 7.8 Input - -Use emulator controller APIs. - -The probe measures injection-to-visible-frame latency. - -This evidence becomes the baseline used by the native shell in Phase 8. - -### Phase 7 exit gate - -All frozen performance thresholds pass; audio, microphone transport, and input probes pass. - ---- - -# PHASE 8 — Native macOS presentation - -## Objective - -Hide emulator chrome without sacrificing responsiveness. - -## 8.1 Runtime control security - -Production discovers the running emulator's authenticated control endpoint from the local discovery file. - -No unauthenticated fixed gRPC port in production. - -## 8.2 Video transport - -Use emulator screenshot streaming with shared-memory/MMAP transport where available. - -Path: - -```text -emulator MMAP frame --> TFTMAC mapped memory --> Metal texture --> MTKView/native SwiftUI/AppKit container -``` - -Production does not use PNG/JPEG streaming for normal frame delivery. - -## 8.3 Native input - -Map: - -```text -mouse -> touch -drag -> touch gesture -scroll -> gesture -keyboard -> key event -Back/Home -> emulator control -``` - -## 8.4 Latency acceptance - -Compare native path to direct Qt emulator control path. - -Required: - -```text -native median <= Qt + 5 ms -native P95 <= Qt + 8.334 ms -native absolute median <= 50 ms -native absolute P95 <= 83.334 ms -``` - -If any bound fails, emulator chrome is not removed yet. - -## 8.5 Native controls - -One TFTMAC window exposes: - -```text -Play / Stop -Back -Home -Volume -Microphone -Fullscreen -Google Play / Update -Screenshot -Diagnostics -Rollback/Runtime Status -``` - -### Phase 8 exit gate - -One Dock icon; one native Mac window; Android runtime hidden; latency gate green. - ---- - -# PHASE 9 — Generality, updates, rollback, packaging, final release - -## Objective - -Prove the runtime is general and maintainable. - -## 9.1 Generality Corpus A — GLES CTS - -Source: - -```text -opengl-es-cts-3.2.14.1 -locked full commit from Phase 0 -Apache-2.0 -``` - -Run Android GLES32 official mustpass applicable cases. - -PASS: - -```text -zero Fail -zero Crash -zero Timeout -all required cases executed -NotSupported only where CTS/spec allows optional support -``` - -Archive `.qpa`. - -## 9.2 Generality Corpus B — Vulkan CTS - -Source: - -```text -vulkan-cts-1.4.6.1 -5c8aae22885448d70a2873e94a93b24b49505c32 -Apache-2.0 -``` - -Run: - -```text -ssot/vulkan-required-cases.txt -``` - -PASS: - -```text -zero Fail -zero Crash -zero Timeout -100% case-list execution -``` - -## 9.3 Generality Corpus C — Vulkan Samples - -Source: - -```text -89dd3af22d41f9244eeab6e0650460112285c0e1 -Apache-2.0 -``` - -Run: - -```text -dynamic_rendering -compute_nbody -``` - -Each: - -```text -1920x1080 -10,000 frames or 10 minutes -zero crash -zero device loss -zero validation ERROR -responsive input -startup/mid/end screenshots show non-uniform rendered content -``` - -## 9.4 Runtime identity - -Each runtime has: - -```text -build ID -AEMU resolved-manifest hash -qemu commit -gfxstream commit -MoltenVK commit -patch-series hash -Android image revision -probe-suite version -``` - -## 9.5 Two-slot rollback - -```text -Runtime/current -Runtime/previous -``` - -Promotion: - -```text -candidate --> capability probes --> generality smoke --> TFT smoke --> performance regression gate --> current -``` - -Failure leaves `current` unchanged. - -## 9.6 Game updates - -Google Play remains app authority. - -After a game update: - -1. capture new package identity; -2. rerun capability gate; -3. launch smoke; -4. if workload requirement fails, report compatibility red without corrupting runtime. - -## 9.7 macOS packaging - -Final bundle: - -```text -TFTMAC.app -``` - -Includes: - -```text -native shell -runtime launcher -probe binaries -gRPC client/protobuf -runtime manifest -licenses -``` - -Large Android image/userdata stays under `/Volumes/MAC MINI M4/TFTMAC/Runtime`; it must not fall back to the internal disk. - -## 9.8 Signing - -Development: - -```text -ad-hoc -``` - -Release: - -```text -Developer ID Application -hardened runtime -notarization -stapling -``` - -Do not re-sign Riot/Google app binaries. - -## 9.9 Reliability matrix - -PASS all: - -```text -cold boot -warm boot -clean shutdown -forced TFTMAC quit recovery -macOS reboot -runtime candidate rejection -runtime rollback -Google Play game update -30+ minute session -3 sequential sessions -``` - -### Phase 9 exit gate - -Every final requirement in the traceability matrix has a PASS artifact. - ---- - -# 3. Requirement-to-proof traceability matrix - -| Requirement | Owner | Evidence | PASS | -|---|---|---|---| -| Reproducible source/toolchain | Phase 0 | locks/hashes | No critical null; hashes match | -| Canonical AEMU source | Phase 0 | resolved manifest + GuestAngle audit | `emu-master-dev` locked; audit PASS | -| Source-built emulator | Phase 1 | build/ctest/boot logs | all green | -| Host Vulkan | Phase 3 | host probe | all required executable cases | -| gfxstream | Phase 4 | transport delta | no required host-green/guest-red | -| Real GLES 3.2 | Phase 5 | guest GLES probe | genuine 3.2 + cases + render | -| No spoof | Phase 5 | env/source audit | nonconformant override absent | -| TFT current | Phase 6 | Play manifest/gameplay | 20+ min live session | -| Performance | Phase 7 | 600s metrics | all absolute bounds | -| Regression protection | Phase 7/9 | A/B report | all regression bounds | -| Audio | Phase 7 | audio test | continuous, no blocking failure | -| Mic | Phase 7 | mic test | CoreAudio→Android verified | -| Input | Phase 7/8 | latency report | absolute + relative bounds | -| Native Mac UX | Phase 8 | UI/latency acceptance | one window, hidden emulator | -| Diagnostics | Phase 7 | session bundle | required fields present | -| Google Play updates | Phase 9 | update receipt | app authority retained | -| Rollback | Phase 9 | rollback test | current protected, previous works | -| GLES generality | Phase 9 | CTS `.qpa` | corpus A PASS | -| Vulkan generality | Phase 9 | CTS/sample logs | corpora B/C PASS | -| Release | Phase 9 | signing/notary | receipts + launch | -| Protected-data boundary | all | Git/package scan | no private binaries/data leaked | - -This matrix is authoritative. No new proof subsystem is added unless a material required behavior lacks falsifiable evidence. - ---- - -# 4. Causal failure routing - -| Failure | Owner | Route | -|---|---|---| -| source AEMU build fails | host/toolchain | repair parity; remain Phase 1 | -| host Vulkan feature red | MoltenVK | repair Phase 3 | -| host green / guest red | gfxstream/AEMU | repair Phase 4 | -| guest Vulkan green / GLES 3.1 | built-in ANGLE requirement | inspect Phase 5 | -| built-in ANGLE revision is causal blocker | conditional custom ANGLE | activate only then | -| real GLES 3.2 green / TFT red | Riot/Unreal workload | inspect exact new requirement | -| TFT works / performance red | runtime performance | Phase 7 measured tuning | -| mic red | audio bridge | Phase 7 | -| native latency red | presentation | Phase 8 | -| candidate update red | release system | reject/rollback Phase 9 | - -No layer is changed merely because another project once needed a similar fix. - ---- - -# 5. Forbidden shortcuts - -Do not: - -- use `ro.opengles.version` as acceptance; -- ship nonconformant ANGLE version exposure; -- mix `emu-main-dev` evidence into the locked `emu-master-dev` authority; -- change the performance thresholds after seeing benchmark results; -- shrink the generality corpus because cases fail; -- prebuild custom ANGLE/rootable execution infrastructure without Phase 5 evidence; -- replace MoltenVK wholesale with an older donor fork; -- modify Riot binaries; -- use third-party APK mirrors; -- commit APKs, credentials, AVD userdata, or tokens; -- hide causal errors with retries/fallbacks; -- remove the working current runtime before candidate acceptance; -- expose unauthenticated emulator control in production. - ---- - -# 6. Final Definition of Done - -Implementation is complete only when: - -```text -[PASS] Phase 0 authority/lock -[PASS] Phase 1 source AEMU -[PASS] Phase 2 capability laboratory -[PASS] Phase 3 host Vulkan -[PASS] Phase 4 gfxstream transport -[PASS] Phase 5 genuine GLES 3.2 -[PASS] Phase 6 current official TFT gameplay -[PASS] Phase 7 frozen performance contract -[PASS] Phase 7 audio/mic/input -[PASS] Phase 8 native single-window Mac UX -[PASS] Phase 9 generality corpus -[PASS] Phase 9 Google Play update -[PASS] Phase 9 rollback -[PASS] Phase 9 signed/notarized release path -[PASS] protected-data audit -``` - -No partial phase may be relabeled as completion. - ---- - -# 7. Source anchors - -Primary authority links: - -- Emulator development branch: https://android.googlesource.com/platform/external/qemu/+/emu-master-dev/android/docs/DEVELOPMENT.md -- Emulator macOS development: https://android.googlesource.com/platform/external/qemu/+/emu-master-dev/android/docs/DARWIN-DEV.md -- Manifest: https://android.googlesource.com/platform/manifest/+/refs/heads/emu-master-dev/default.xml -- gfxstream: https://android.googlesource.com/platform/hardware/google/gfxstream/ -- MoltenVK: https://github.com/KhronosGroup/MoltenVK -- UTM donor issue: https://github.com/utmapp/UTM/issues/7575 -- GLES CTS: https://github.com/KhronosGroup/VK-GL-CTS/releases -- Vulkan CTS 1.4.6.1: https://chromium.googlesource.com/external/github.com/KhronosGroup/VK-GL-CTS/+/refs/tags/vulkan-cts-1.4.6.1 -- Vulkan Samples: https://github.com/KhronosGroup/Vulkan-Samples -- Android SDK: https://developer.android.com/studio -- Vulkan SDK: https://vulkan.lunarg.com/sdk/home - ---- - -# 8. Authority rule - -This plan and `TFTMAC_GPU_RUNTIME_SSOT.md` describe the **same architecture**. - -They must be revised together if architecture changes. - -After Phase 0: - -```text -STACK.lock.yaml -``` - -owns exact machine-resolved versions. - -Capability probes own graphics truth. - -The frozen performance contract owns optimization acceptance. - -The traceability matrix owns completion evidence. +The stock Build 8 runtime is the normal-play authority. A separately isolated, +source-built `tftmac-runtime` diagnostic environment at commit `c8aa26e` is +eligible only for future causal instrumentation. It must never replace the +stock runtime or be compared directly with stock gameplay performance until +its independent correctness and parity gates pass. diff --git a/TFTMAC_GPU_RUNTIME_SSOT.md b/TFTMAC_GPU_RUNTIME_SSOT.md index 3b0840d..20ab2f8 100644 --- a/TFTMAC_GPU_RUNTIME_SSOT.md +++ b/TFTMAC_GPU_RUNTIME_SSOT.md @@ -1,1094 +1,97 @@ -# TFTMAC GPU Runtime — Single Source of Truth +# TFTMAC Runtime — Single Source of Truth -**Version:** 2.0 revised after ZenGate v2.3 remediation -**Freeze date:** 2026-08-26 -**Status:** Planning authority; implementation must not begin until the companion implementation plan passes ZenGate -**Project:** One project only — `TFTMAC` -**Primary production workload:** Current official Teamfight Tactics Android client -**Shipping render target:** 1920×1080 at 60 Hz -**North-star objective:** A general, high-performance Android 3D runtime for Apple Silicon that truthfully provides modern Vulkan capability and genuine OpenGL ES 3.2 through ANGLE, renders through Metal, and is surfaced as a native macOS application. +**Status:** CURRENT RUNTIME AUTHORITY +**Updated:** 2026-08-31 +**Project:** TFTMAC ---- +## Current production/control runtime -# 1. Product win condition - -TFTMAC is complete only when all of the following are true: - -1. A source-built Android Emulator/AEMU runtime executes on Apple Silicon. -2. The host Vulkan layer is backed by Metal through a locked MoltenVK implementation. -3. gfxstream transports the required Vulkan features into the Android guest. -4. Android's ANGLE path creates a **real GLES 3.2 EGL context** without nonconformant version spoofing. -5. The current official Google Play TFT client passes its graphics-hardware gate and is playable at 1920×1080. -6. The runtime satisfies the frozen performance acceptance contract in this SSOT. -7. Audio, microphone transport, keyboard/mouse/touch, local diagnostics, update, and rollback are operational. -8. The emulator chrome is hidden in normal use and TFTMAC provides one native macOS application window. -9. The frozen non-TFT generality corpus passes, proving the graphics runtime is not a TFT-only special case. - -The final product is not “TFT running in an emulator.” - -It is: - -> **A modern Apple-Silicon Android graphics runtime, surfaced as a native Mac application, with TFT as its first production workload.** - ---- - -# 2. Hard architecture invariants - -## 2.1 One project - -There is one repository and one product: - -```text -TFTMAC -``` - -AEMU, gfxstream, ANGLE, MoltenVK, Khronos tests, and reference implementations are upstream dependencies or donors, not separate product projects. - -## 2.2 No capability spoofing - -The following is never sufficient proof of GLES 3.2: - -```text -ro.opengles.version=196610 -``` - -TFTMAC must never ship ANGLE with: - -```text -exposeNonConformantExtensionsAndVersions -``` - -enabled as a way to make software believe unsupported functionality exists. - -A GLES 3.2 PASS requires: - -```text -EGL context request = 3.2 -context creation = success -GL_VERSION = OpenGL ES 3.2... -required executable probe cases = PASS -known render output = PASS -``` - -## 2.3 No Riot binary modification - -TFTMAC may: - -- install/update through Google Play; -- inspect package metadata; -- observe runtime graphics behavior; -- control the Android runtime around the app. - -TFTMAC may not: - -- patch Riot gameplay code; -- bypass Riot graphics checks by modifying Riot binaries; -- re-sign Riot binaries; -- use third-party APK mirrors as package authority; -- commit Riot APKs or private Android userdata to Git. - -## 2.4 Measurement owns unknowns - -A material unknown is resolved by a deterministic probe at its owning boundary. - -The normal failure path is: - -```text -host Vulkan - ↓ -gfxstream guest Vulkan - ↓ -Android ANGLE / GLES - ↓ -TFT workload -``` - -A failure is repaired at the first boundary where evidence changes from PASS to FAIL. - ---- - -# 3. Canonical source authority - -## 3.1 Android Emulator branch authority - -For this project the authoritative Android Emulator development branch is: - -```text -emu-master-dev -``` - -Reason: the official Android Emulator DEVELOPMENT and macOS development documentation identify `emu-master-dev` as the major emulator development branch and instruct developers to initialize the platform manifest from that branch. - -Official references: - -- https://android.googlesource.com/platform/external/qemu/+/emu-master-dev/android/docs/DEVELOPMENT.md -- https://android.googlesource.com/platform/external/qemu/+/emu-master-dev/android/docs/DARWIN-DEV.md -- https://android.googlesource.com/platform/manifest/+/refs/heads/emu-master-dev/default.xml - -`emu-main-dev` is not co-authoritative for TFTMAC. It may exist upstream, but this project does not mix behavior or assumptions between the two branches. - -## 3.2 Freeze rule - -Phase 0 initializes: - -```bash -repo init \ - -u https://android.googlesource.com/platform/manifest \ - -b emu-master-dev -repo sync -c -j8 -repo manifest -r > ssot/upstreams-aemu.lock.xml -``` - -From that point forward: - -```text -ssot/upstreams-aemu.lock.xml -``` - -is the source authority for the integrated AEMU family. - -Current branch names, remembered commits, or web research do not override the resolved manifest. - -## 3.3 GuestAngle authority check - -Before implementation code changes, the exact locked `external/qemu` revision must be inspected at: - -```text -android/android-emu/android/userspace-boot-properties.cpp -``` - -The preflight must prove that the locked revision: - -- supports `GuestAngle`; -- sets Android's hardware EGL implementation to ANGLE when GuestAngle is active; -- enables the Vulkan guest path required by GuestAngle; -- does not automatically enable ANGLE's nonconformant GLES-version exposure. - -The verification result is written to: - -```text -ssot/guestangle-authority.json -``` - -If the locked source no longer has materially equivalent semantics, implementation does not guess. The SSOT is revalidated against current source before coding begins. - ---- - -# 4. Frozen production architecture - -The primary production guest is **one official Google Play Android 17 ARM64 AVD**. - -```text -Official Android 17 Google Play ARM64 guest - | - +-------+-------+ - | | - GLES Native Vulkan - | | - Android built-in | - ANGLE/Vulkan | - +-------+-------+ - | - gfxstream - | - source-built AEMU - | - MoltenVK - | - Metal - | - Apple Silicon GPU - | - TFTMAC native shell -``` - -This is the normal architecture. - ---- - -# 5. Production Android guest - -## 5.1 System image - -Target packages: - -```text -platforms;android-37.1 -system-images;android-37.0;google_apis_playstore_ps16k;arm64-v8a -``` - -The exact installed package revisions are resolved by the current Android CLI during Phase 0 and frozen in `STACK.lock.yaml`. - -Requirement: - -```text -API 37 Google Play image revision >= 5 -``` - -## 5.2 AVD identity - -```text -TFTMAC_Live_API37 -``` - -Initial configuration: - -```text -Pixel Tablet profile -ARM64 -1920x1080 -280 DPI -8 vCPU -8192 MB RAM -768 MB VM heap -16 GB data partition minimum -host GPU -audio input enabled -landscape -Google Play enabled -``` - -No OEM device spoofing beyond the official Pixel Tablet AVD profile. - -## 5.3 Runtime ports - -```text -Emulator console: 5592 -ADB serial: emulator-5592 -Isolated ADB server: 5040 -``` - -Development-only gRPC may use: - -```text -8554 -``` - -Production control uses the authenticated emulator discovery endpoint and never exposes an unauthenticated fixed control service. - ---- - -# 6. Primary ANGLE strategy - -The production GLES path is: - -```text -Android built-in ANGLE -<- AEMU GuestAngle -<- guest Vulkan -<- gfxstream -<- MoltenVK -<- Metal -``` - -Normal launch features: - -```text -GuestAngle -Vulkan -GLESDynamicVersion -``` - -No custom ANGLE build is on the normal critical path. - ---- - -# 7. Conditional custom ANGLE adapter - -A custom ANGLE build is allowed only if all of the following are already proven: - -1. host Vulkan requirements are green; -2. gfxstream guest Vulkan requirements are green; -3. built-in Android 17 ANGLE still refuses or misimplements a genuine ES 3.2 context; -4. the failure is traced to the built-in ANGLE revision rather than Vulkan capability. - -Only then may the plan activate: - -```text -Custom ANGLE Adapter -``` - -Because a non-debuggable app cannot normally select a loose custom ANGLE package without root, this conditional adapter may introduce a **rootable Android 17 execution guest** while retaining the Google Play AVD as package/update authority. - -This is a conditional repair route, not a second normal architecture. - -It must not be built speculatively. - ---- - -# 8. Frozen toolchain and dependency family - -## 8.1 Apple - -```text -Xcode 26.6 -Build 17F113 -``` - -Official: -- https://developer.apple.com/download/all/ -- https://developer.apple.com/xcode/system-requirements/ - -## 8.2 Android command-line tools - -```text -commandlinetools-mac_arm64-15859902_latest.zip -SHA-256: -835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e -``` - -Official: -- https://developer.android.com/studio -- https://dl.google.com/android/repository/commandlinetools-mac_arm64-15859902_latest.zip - -## 8.3 Platform tools and control emulator - -Initial control references: - -```text -Platform-Tools 37.0.1 -Android Emulator 37.1.11 -``` - -The exact installed package revisions are frozen during preflight. - -## 8.4 Vulkan SDK - -```text -Vulkan SDK 1.4.357.0 -macOS SHA-256: -539433589c83522e6f31b1c7b418a4167e21597a4a361ab119e1dc0760cf3865 -``` - -Official: -- https://vulkan.lunarg.com/sdk/home - -The SDK is the developer/validation toolchain, not the shipping driver once TFTMAC uses its selected MoltenVK build. - -## 8.5 MoltenVK - -Reference baseline: - -```text -MoltenVK v1.4.2 -``` - -Official: -- https://github.com/KhronosGroup/MoltenVK -- https://github.com/KhronosGroup/MoltenVK/releases - -TFTMAC first tests the MoltenVK revision resolved by the locked AEMU manifest, then compares it with upstream v1.4.2. - -The better required feature set wins only if integration remains clean. - -## 8.6 UTM/CrossOver donor - -UTM/CrossOver MoltenVK geometry/cull work is a donor/reference only. - -Official/reference: -- https://github.com/utmapp/UTM/issues/7575 -- https://github.com/utmapp/UTM/releases -- https://github.com/utmapp/MoltenVK/tree/crossovers/v25.1.0 - -Never replace the selected MoltenVK base wholesale merely because UTM already contains a feature. - -Port only the smallest causal patch after probe evidence. - ---- - -# 9. Persistent filesystem authority - -Bulk storage authority is the external M4 volume: - -```text -/Volumes/MAC MINI M4/TFTMAC/ -├── Build/ -└── Runtime/ - ├── SDK/ - ├── AVD/ - ├── Packages/ - ├── Probes/ - ├── Manifests/ - └── VulkanSDK/ -``` - -Small control/log state may remain internal: - -```text -~/Library/Application Support/TFTMAC/ -├── Logs/ -├── Diagnostics/ -└── Rollback/ -``` - -The external volume is mandatory for default Build and Runtime roots. If `/Volumes/MAC MINI M4` is unavailable, TFTMAC fails closed and must not create bulk Build or Runtime data on the internal disk. - -Repository stores only code, lock files, patches, probes, manifests that contain no protected data, documentation, and tests. - -The repository must not contain: - -```text -Riot APKs -Google credentials -Riot credentials -AVD userdata -session tokens -OAuth tokens -private runtime disks -``` - ---- - -# 10. Capability laboratory - -The capability laboratory is permanent product infrastructure. - -## 10.1 Host Vulkan probe - -Produces: - -```text -Diagnostics/host-vulkan.json -``` - -Checks and executes representative workloads for: - -- API version; -- device/driver; -- geometry shaders; -- tessellation; -- shader cull distance; -- indirect draw; -- descriptor indexing; -- timeline semaphores; -- synchronization2; -- dynamic rendering; -- buffer device address; -- subgroups; -- sampler/filter features. - -Enumeration alone is not proof. - -## 10.2 Guest Vulkan probe - -Produces: +TFTMAC uses the released Google Android Emulator already proven on the target Apple Silicon host. Source-built AEMU is retired from the normal product path and is not required for build, launch, test, repair, or release. ```text -Diagnostics/guest-vulkan.json -Diagnostics/vulkan-transport-delta.json +Runtime root: /Volumes/MAC MINI M4/TFTMAC/Runtime +Android Emulator: 37.1.11.0 (build 15917651) +ADB server: donor port 5038 +Emulator console: 5582 +AVD: TFT_Ultra_Tablet +Guest image: official Google Play ARM64 image +Target display: 1920x1080 / 60 Hz +Package: com.riotgames.league.teamfighttactics +Installer/update authority: com.android.vending ``` -The delta report is the semantic owner for determining whether a missing capability belongs to MoltenVK or gfxstream/AEMU. +The normal-play control is `tftmac_5gb_native_v1` at 6 vCPU / 5120 MB, High +graphics, 60 FPS, Performance OFF. The latest Build 8 capture observed +`combat_latency_a` layered over those values; that is an observed active preset, +not a performance promotion. Ultra High and Riot Performance Mode Beta remain +rejected on the target M4 host because of unacceptable tails and playability. -## 10.3 Guest GLES 3.2 probe +The launcher boundary is frozen: TFTMAC starts its packaged `TFTMAC Emulator Host.app` through `/usr/bin/open -n -W --env ... --args ...` in the logged-in user session. It does not directly spawn QEMU from a Node/service context and does not inject `ADB_VENDOR_KEYS`. The previous `5040/5592` direct-service identity is retained only in historical evidence as the ADB-authorization regression. -Package: +## EmulatorController authority -```text -dev.tftmac.gpuprobe -``` - -Must execute: - -- EGL initialization; -- ES3 renderable config; -- 3.2 context creation; -- GL_VERSION / GL_VENDOR / GL_RENDERER; -- extension inventory; -- geometry shader; -- tessellation; -- compute; -- SSBO; -- texture buffer; -- cube-map array; -- multisample/sample shading; -- base-vertex path; -- synchronization; -- deterministic known-image output. - -## 10.4 Frame and input probe - -Produces: - -```text -Diagnostics/frame-performance.json -Diagnostics/input-latency.json -``` - -Sources: - -- SurfaceFlinger timestats; -- gfxinfo framestats where applicable; -- host monotonic timestamps; -- Android display refresh; -- native-shell input injection timestamps; -- first observable changed frame. - ---- - -# 11. Frozen performance acceptance contract - -This contract is fixed **before optimization** and must not be changed merely because a candidate misses it. - -## 11.1 Display budget - -Shipping refresh: - -```text -60 Hz -``` - -One frame budget: - -```text -16.667 ms -``` - -Two-frame budget: - -```text -33.334 ms -``` - -## 11.2 Gameplay measurement window - -For TFT performance acceptance: - -1. Enter a live rendered match. -2. Exclude the first **120 seconds** as shader/content warm-up. -3. Capture the next **600 continuous seconds**. -4. Use SurfaceFlinger-based presentation timestamps as the primary FPS/frame-time source. -5. If a metric is unavailable for a specific surface, record the fallback source explicitly rather than silently changing methodology. - -## 11.3 Absolute graphics thresholds - -Over the 600-second measurement window: +The native application uses the exact controller protocol shipped with the installed stock emulator. ```text -Average presented FPS >= 58.0 -P95 frame interval <= 20.0 ms -P99 frame interval <= 33.334 ms -Janky frame = interval > 33.334 ms -Janky frames <= 1.0% -Severe stall = interval > 100 ms -Severe stalls <= 3 in 600 seconds -No renderer crash -No Vulkan device loss -No repeated validation-error storm +Installed: /Volumes/MAC MINI M4/TFTMAC/Runtime/SDK/emulator/lib/emulator_controller.proto +Vendored: Vendor/AndroidEmulator/emulator_controller.proto +SHA-256: 1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f ``` -58 FPS is the minimum 96.7% delivery rate for a 60 Hz target. +The installed and vendored bytes were reverified equal during Clean Ownership Gate 4. Protocol drift fails closed until intentionally refreshed from the installed runtime. -P99 is capped at two frame budgets. - -## 11.4 Candidate regression thresholds - -A candidate runtime is not promoted if, compared with the currently accepted runtime under the same workload: +## Product architecture ```text -Average FPS decreases by > 1.0 FPS -or -P99 frame interval worsens by > 3% -or -jank percentage worsens by > 0.25 percentage points -or -median synthetic input-to-present latency worsens by > 5 ms -or -P95 synthetic input-to-present latency worsens by > 8.334 ms +TFTMAC.app + -> AppKit window + -> Metal presentation + -> authenticated local EmulatorController + -> stock Google Android Emulator + -> official Google Play ARM64 guest + -> official Google Play TFT package + -> Riot official authentication/content lifecycle ``` -A candidate must satisfy both absolute and regression thresholds. +Google Play owns package installation and updates. Riot owns its own application authentication and content initialization. TFTMAC does not mirror, bundle, patch, re-sign, or privately update Riot binaries. -## 11.5 Native-wrapper input thresholds +## Graphics/control evidence -The native MMAP/Metal presentation path is compared against the direct emulator Qt control path using the deterministic input probe. +The known-good stock control uses host GPU acceleration through gfxstream, +host Vulkan, MoltenVK and Metal. The latest TFT receipt identifies direct Unreal +Vulkan; ANGLE may be present for another guest path but is not assumed to render +the game. The native Mac presenter is hidden correctness context only. -Requirements: +Build 8 automatic logging is live-verified and captures the TFT process/layer +lifetime without match markers. It can prove exact SurfaceFlinger degradation, +but it cannot yet name an internal graphics owner. Source-level instrumentation +is planned only in isolated `tftmac-runtime` commit `c8aa26e`, never as a +normal-play replacement or stock-performance comparison. -```text -Median native input-to-present <= Qt control + 5 ms -P95 native input-to-present <= Qt control + 8.334 ms -Absolute median input-to-present <= 50 ms -Absolute P95 input-to-present <= 83.334 ms -``` +Performance changes remain one-variable, reversible A/B experiments with explicit KEEP/REJECT evidence. Raw capture remains append-only during measurement and is normalized after capture. -The native shell does not ship if it fails these bounds. +## Storage authority -## 11.6 Resource-profile selection +Protected runtime state remains under `/Volumes/MAC MINI M4/TFTMAC/Runtime` and must not be deleted by build/storage cleanup. AVD userdata, Google/Riot credentials, tokens, APK bytes and private runtime disks are not repository assets. -Approved initial profiles: +The abandoned source-build laboratory is not runtime authority. Historical source-build findings may remain only as compact evidence; no current command or document may recreate that laboratory as part of normal operation. -```text -A: 6 vCPU / 6144 MB -B: 8 vCPU / 8192 MB -``` +## Validation -Both run the same measurement workload. +Repository/source validation, including an unsigned Release compile and all 43 +native tests, is: -Selection: - -1. profile must satisfy all absolute thresholds; -2. lower P99 wins; -3. if P99 differs by <3%, lower host CPU usage wins; -4. selected profile is written to the runtime manifest. - -No third profile is added unless both A and B fail an explicit resource constraint. - ---- - -# 12. Frozen generality acceptance corpus - -The corpus is selected now, before graphics implementation. - -Implementation agents may not substitute easier tests. - -## Corpus A — Khronos OpenGL ES CTS 3.2 - -Source: - -```text -KhronosGroup/VK-GL-CTS -Tag: opengl-es-cts-3.2.14.1 -Release commit prefix published by Khronos: 067e883 -License: Apache-2.0 -API: OpenGL ES 3.2 / EGL +```sh +/bin/zsh scripts/verify-tftmac.command ``` -Official: -- https://github.com/KhronosGroup/VK-GL-CTS/releases -- https://github.com/KhronosGroup/VK-GL-CTS - -Before any implementation mutation, bootstrap resolves the full immutable tag commit with: +Current-host installed/runtime/signing validation is deliberately separate: -```bash -git rev-list -n 1 opengl-es-cts-3.2.14.1 +```sh +/bin/zsh scripts/verify-installed-runtime.command ``` -and writes it into `STACK.lock.yaml`. - -Pass condition: - -- build the Android GLES32 CTS; -- run the official GLES 3.2 mustpass case list applicable to the declared context; -- zero `Fail`; -- zero `Crash`; -- zero `Timeout`; -- `NotSupported` is allowed only when the CTS itself treats the feature as optional for the declared API/extension set; -- produce the complete `.qpa` log and summary artifact. - -## Corpus B — Khronos Vulkan CTS 1.4.6.1 - -Source: - -```text -KhronosGroup/VK-GL-CTS -Tag: vulkan-cts-1.4.6.1 -Commit: -5c8aae22885448d70a2873e94a93b24b49505c32 -License: Apache-2.0 -API: Vulkan -``` - -Official: -- https://chromium.googlesource.com/external/github.com/KhronosGroup/VK-GL-CTS/+/refs/tags/vulkan-cts-1.4.6.1 - -TFTMAC does not claim full Vulkan 1.4 conformance merely by running this corpus. - -The project freezes a **runtime-required Vulkan case list** before implementation, derived from: - -- features needed by the GLES 3.2 probe; -- core draw/compute/image/synchronization paths used by TFTMAC; -- dynamic rendering and WSI paths used by the host/guest stack. - -The exact case list is stored as: - -```text -ssot/vulkan-required-cases.txt -``` - -and hashed in `STACK.lock.yaml`. - -Pass condition: - -```text -zero Fail -zero Crash -zero Timeout -all cases in vulkan-required-cases.txt executed -``` - -Unsupported optional cases are not added to the required list after results are known. - -## Corpus C — Khronos Vulkan Samples - -Source: - -```text -KhronosGroup/Vulkan-Samples -Commit: -89dd3af22d41f9244eeab6e0650460112285c0e1 -License: Apache-2.0 -Android ARM64 -``` - -Pinned workloads: - -```text -dynamic_rendering -compute_nbody -``` - -Official: -- https://github.com/KhronosGroup/Vulkan-Samples - -Pass condition for each workload: - -1. build the pinned Android sample APK; -2. run at 1920×1080; -3. complete 10,000 rendered frames or 10 minutes, whichever occurs first; -4. zero process crash; -5. zero Vulkan device loss; -6. zero validation-layer errors classified as ERROR; -7. application remains responsive to injected Back/Home/input probe; -8. capture startup, mid-run, and final screenshots; each must contain non-uniform rendered content and the application surface must remain present. - -The corpus therefore contains both standardized conformance evidence and app-like Vulkan workloads. - ---- - -# 13. Patch policy - -The patch order is: - -```text -1. Existing supported configuration -2. Small local patch -3. Harvest a known donor implementation -4. New compatibility implementation -``` - -Every patch records: - -```text -component -upstream commit -reason -probe failing before -probe passing after -performance delta -upstreamability -``` - -## 13.1 MoltenVK geometry rule - -If the host Vulkan probe reports a required geometry/cull capability missing: - -1. reproduce the failure; -2. inspect current MoltenVK; -3. inspect UTM/CrossOver donor work; -4. port the smallest compatible patch; -5. add direct geometry regression coverage; -6. explicitly test indirect-draw interaction; -7. reject the patch if it only changes feature reporting without executing the workload. - ---- - -# 14. Package authority and update chain - -Google Play is the semantic owner of production app acquisition. - -For each TFT version, TFTMAC records: - -```json -{ - "packageName": "com.riotgames.league.teamfighttactics", - "installer": "com.android.vending", - "versionName": "...", - "versionCode": 0, - "capturedAt": "...", - "apks": [ - { - "path": "...", - "size": 0, - "sha256": "..." - } - ], - "signingCertificateSHA256": "..." -} -``` - -The manifest stays outside Git if it contains private local paths. - -Normal production updates happen through Google Play in the production AVD. - -A runtime update and a game update are separate operations. - ---- - -# 15. Native macOS shell - -Final normal-use architecture: - -```text -Android runtime hidden - | -emulator controller - | -shared/MMAP frame transport - | -Metal texture - | -native macOS view -``` - -The shell owns: - -- Play/Stop; -- Back/Home; -- fullscreen/window; -- volume; -- microphone; -- keyboard/mouse/touch; -- screenshot; -- diagnostics; -- Google Play/update navigation; -- rollback/runtime status. - -Production gRPC control uses authenticated local discovery. - -An unauthenticated fixed gRPC port is development-only. - ---- - -# 16. Audio and microphone - -## Audio - -Start with the emulator's direct CoreAudio path. - -Only replace it with gRPC `streamAudio` if measured latency/underrun evidence demonstrates an improvement. - -## Microphone - -Final path: - -```text -macOS CoreAudio input --> macOS microphone permission --> TFTMAC capture --> emulator controller injectAudio --> Android virtual microphone --> Android app -``` - -Mic acceptance uses a dedicated Android test app before relying on a game feature. - -TFTMAC can guarantee microphone transport; it cannot guarantee that every Android application exposes in-app voice chat. - ---- - -# 17. Diagnostics - -Default local root: - -```text -~/Library/Application Support/TFTMAC/Logs/ -``` - -Each session records: - -- runtime build ID; -- AEMU manifest hash; -- qemu/AEMU/gfxstream commits; -- MoltenVK commit and patches; -- Android image revision; -- ANGLE identity; -- app identity/version; -- display/resource profile; -- host model/macOS; -- renderer strings; -- Vulkan/GLES capability hashes; -- boot/launch times; -- frame statistics; -- CPU/memory; -- audio/mic state; -- input latency; -- errors/crashes; -- clean shutdown. - -No remote telemetry is required. - ---- - -# 18. Update and rollback - -Persistent runtime slots: - -```text -Runtime/current -Runtime/previous -``` - -Promotion: - -```text -candidate build --> source/lock verification --> capability probes --> generality corpus smoke subset --> TFT launch smoke --> performance regression gate --> promote current --> old current becomes previous -``` - -Failure: - -```text -candidate rejected -current remains active -``` - -AVD userdata is not destroyed by a runtime binary promotion. - ---- - -# 19. Machine SSOT - -`STACK.lock.yaml` contains exact resolved values. - -Required fields include: - -```yaml -schema: 2 -frozen_at: "2026-08-26" - -aemu: - authority_branch: "emu-master-dev" - resolved_manifest_sha256: null - qemu_commit: null - aemu_commit: null - gfxstream_commit: null - integrated_angle_commit: null - integrated_moltenvk_commit: null - -android: - api: 37 - play_image_package: "system-images;android-37.0;google_apis_playstore_ps16k;arm64-v8a" - play_image_revision: null - -moltenvk: - reference_tag: "v1.4.2" - selected_commit: null - patch_series_sha256: null - -generality: - gles_cts_tag: "opengl-es-cts-3.2.14.1" - gles_cts_commit: null - vulkan_cts_tag: "vulkan-cts-1.4.6.1" - vulkan_cts_commit: "5c8aae22885448d70a2873e94a93b24b49505c32" - vulkan_required_cases_sha256: null - vulkan_samples_commit: "89dd3af22d41f9244eeab6e0650460112285c0e1" - -performance: - fps_min: 58.0 - p95_ms_max: 20.0 - p99_ms_max: 33.334 - jank_threshold_ms: 33.334 - jank_pct_max: 1.0 - severe_stall_ms: 100.0 - severe_stalls_per_600s_max: 3 -``` - -Every `null` critical-path field must be resolved during Phase 0 **before implementation mutation begins**. - -After Phase 0: - -```text -STACK.lock.yaml contains no unresolved critical-path null. -``` - ---- - -# 20. Requirement-to-proof traceability - -| Requirement | Semantic owner / phase | Proof artifact | Pass condition | -|---|---|---|---| -| Reproducible toolchain/source | Phase 0 | `STACK.lock.yaml`, resolved manifest, hashes | No unresolved critical-path fields; hashes match | -| One authoritative AEMU branch | Phase 0 | `guestangle-authority.json` | Locked `emu-master-dev` source proves required GuestAngle semantics | -| Source-built AEMU | Phase 1 | build/test logs | Build succeeds; unit tests pass; production AVD boots | -| Real host Vulkan capability | Phase 2–3 | `host-vulkan.json` | Required executable feature probes pass | -| Correct gfxstream transport | Phase 4 | `vulkan-transport-delta.json` | No required host-green capability is guest-red | -| Genuine GLES 3.2 | Phase 5 | `guest-gles32.json`, render image | Real 3.2 context + every required executable case passes | -| No capability spoof | Phase 5 | runtime env/source audit | Nonconformant exposure absent in shipping config | -| Current official TFT works | Phase 6 | Google Play manifest + gameplay acceptance | Current client launches, queues, renders, 20+ min live session | -| 1920×1080 performance | Phase 7 | `frame-performance.json` | Meets every frozen absolute threshold | -| No performance regression | Phase 7 | benchmark comparison | Meets every frozen regression threshold | -| Audio | Phase 7 | audio diagnostic | Continuous output; no blocking underrun/error | -| Microphone transport | Phase 7 | mic loopback/test artifact | CoreAudio→Android input path verified | -| Input | Phase 7/8 | `input-latency.json` | Meets absolute and relative latency bounds | -| Native Mac single window | Phase 8 | UI acceptance + latency comparison | One native window; hidden emulator chrome; latency gate passes | -| Diagnostics | Phase 7–8 | session bundle | Required fields emitted; crash/error evidence retained | -| Google Play update path | Phase 9 | update acceptance | Update through Play retains runtime and package authority | -| Rollback | Phase 9 | rollback acceptance | Failed candidate leaves current active; previous restorable | -| General GLES compatibility | Phase 9 | GLES CTS `.qpa` | Corpus A pass criteria | -| General Vulkan compatibility | Phase 9 | Vulkan CTS + samples logs | Corpora B/C pass criteria | -| Release packaging | Phase 9 | codesign/notary receipts | Signed/notarized app passes local launch | -| No protected binary/data leakage | All phases | Git scan / package audit | No Riot APKs, credentials, userdata, tokens committed or bundled | - -This matrix is the canonical requirement-to-proof map. - ---- - -# 21. Known measured unknowns - -These are not architecture decisions left open. - -They are deterministic measurements: - -1. exact resolved commits on `emu-master-dev`; -2. exact selected API 37 Play image revision; -3. exact Vulkan feature delta; -4. whether selected MoltenVK already provides every host requirement; -5. whether gfxstream loses any required feature; -6. whether Android built-in ANGLE creates genuine ES 3.2 after Vulkan is correct; -7. whether the conditional custom ANGLE adapter is needed; -8. current TFT requirements beyond GLES 3.2, if any; -9. which of the two frozen guest resource profiles wins; -10. whether native MMAP/Metal presentation satisfies the frozen latency gate. - -Each item has one evidence-producing phase in the companion plan. - ---- - -# 22. Final authority rule - -There is no architecture-precedence workaround between documents. - -This SSOT and `TFTMAC_FULL_IMPLEMENTATION_PLAN.md` must describe the same production architecture. - -For machine-resolved values: - -```text -STACK.lock.yaml -``` - -wins over remembered prose after Phase 0. - -For capability decisions: - -```text -probe output -``` - -wins over hypotheses. - -For performance: - -```text -frozen acceptance contract + benchmark evidence -``` - -wins over subjective judgment. - -Any future architecture change updates the SSOT and implementation plan together before implementation proceeds. +The 2026-08-31 current-host audit confirmed matching Build 8 executable and +emulator-host hashes, but found zero available local signing identities and +`CSSMERR_TP_NOT_TRUSTED`. That local verifier remains non-passing until a +separate signing-identity repair; historical release acceptance remains intact. diff --git a/Tests/TFTMACTests/CombatBenchmarkAnalysisTests.swift b/Tests/TFTMACTests/CombatBenchmarkAnalysisTests.swift new file mode 100644 index 0000000..f514192 --- /dev/null +++ b/Tests/TFTMACTests/CombatBenchmarkAnalysisTests.swift @@ -0,0 +1,157 @@ +import XCTest + +final class CombatBenchmarkAnalysisTests: XCTestCase { + func testHomeRunRequiresBroadPacingImprovement() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(), + candidate: metrics( + weightedFPS: 58, + onePercentLowFPS: 18, + p95IntervalMilliseconds: 24, + p99IntervalMilliseconds: 36, + jankRate: 0.05, + severeRate: 0.001, + missedVsyncRate: 0.07 + ) + ) + + XCTAssertEqual(analysis.decision, .homeRun) + XCTAssertEqual(analysis.deltas.weightedFPSPercent, 16, accuracy: 0.001) + XCTAssertEqual(analysis.deltas.p99IntervalPercent, -28, accuracy: 0.001) + } + + func testPromisingAcceptsOneMaterialImprovementWithoutRegression() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(), + candidate: metrics(weightedFPS: 53, onePercentLowFPS: 17) + ) + + XCTAssertEqual(analysis.decision, .promising) + } + + func testRejectsMaterialFramePacingRegression() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(), + candidate: metrics(p99IntervalMilliseconds: 60) + ) + + XCTAssertEqual(analysis.decision, .reject) + } + + func testCompletedScreeningBelowFivePercentWeightedFPSImprovementRejects() { + let baseline = metrics() + let analysis = CombatBenchmarkAnalysis(baseline: baseline, candidate: baseline) + + XCTAssertEqual(analysis.decision, .reject) + } + + func testInvalidCandidateIsInconclusiveAndReportsEveryFailure() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(), + candidate: metrics( + combatDurationSeconds: 299, + surfaceAvailability: 0.94, + clockCoverage: 0.94, + p95ClockRoundTripMilliseconds: 10.1, + frameHistoryTruncated: true, + exactLayerStable: false, + weightedFPS: 100, + onePercentLowFPS: 100, + p95IntervalMilliseconds: 1, + p99IntervalMilliseconds: 1, + jankRate: 0, + severeRate: 0, + missedVsyncRate: 0 + ) + ) + + XCTAssertEqual(analysis.decision, .inconclusive) + XCTAssertEqual( + analysis.candidateValidity.failures, + [.combatDurationTooShort, .insufficientSurfaceAvailability, .insufficientClockCoverage, .clockRoundTripTooHigh, .frameHistoryTruncated, .exactLayerChangedOrAmbiguous] + ) + } + + func testDeltasUsePercentagePointsForRates() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(jankRate: 0.10, severeRate: 0.01, missedVsyncRate: 0.12), + candidate: metrics(jankRate: 0.08, severeRate: 0.007, missedVsyncRate: 0.10) + ) + + XCTAssertEqual(analysis.deltas.jankRatePercentagePoints, -0.02, accuracy: 0.000_001) + XCTAssertEqual(analysis.deltas.severeRatePercentagePoints, -0.003, accuracy: 0.000_001) + XCTAssertEqual(analysis.deltas.missedVsyncRatePercentagePoints, -0.02, accuracy: 0.000_001) + } + + func testCorrectnessRegressionRejectsAnOtherwiseFastCandidate() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(), + candidate: metrics( + correctnessPassed: false, + weightedFPS: 70, + onePercentLowFPS: 30, + p95IntervalMilliseconds: 10, + p99IntervalMilliseconds: 10, + jankRate: 0.01, + severeRate: 0.001, + missedVsyncRate: 0.01 + ) + ) + + XCTAssertEqual(analysis.decision, .reject) + } + + func testThresholdGapIsInconclusive() { + let analysis = CombatBenchmarkAnalysis( + baseline: metrics(), + candidate: metrics(weightedFPS: 53, onePercentLowFPS: 16) + ) + + XCTAssertEqual(analysis.decision, .inconclusive) + } + + func testOnePercentLowUsesMeanOfSlowestOnePercent() { + let intervals = Array(repeating: 16.667, count: 198) + [100, 200] + + XCTAssertEqual( + CombatBenchmarkMetrics.onePercentLowFPS(from: intervals), + 6.666_667, + accuracy: 0.000_001 + ) + } + + private func metrics( + combatDurationSeconds: Double = 480, + surfaceAvailability: Double = 0.98, + clockCoverage: Double = 0.99, + p95ClockRoundTripMilliseconds: Double = 5, + frameHistoryTruncated: Bool = false, + exactLayerStable: Bool = true, + correctnessPassed: Bool = true, + weightedFPS: Double = 50, + onePercentLowFPS: Double = 15, + p95IntervalMilliseconds: Double = 32, + p99IntervalMilliseconds: Double = 50, + jankRate: Double = 0.10, + severeRate: Double = 0.005, + missedVsyncRate: Double = 0.12 + ) -> CombatBenchmarkMetrics { + CombatBenchmarkMetrics( + combatDurationSeconds: combatDurationSeconds, + surfaceAvailability: surfaceAvailability, + clockCoverage: clockCoverage, + p95ClockRoundTripMilliseconds: p95ClockRoundTripMilliseconds, + frameHistoryTruncated: frameHistoryTruncated, + exactLayerStable: exactLayerStable, + correctnessPassed: correctnessPassed, + weightedFPS: weightedFPS, + onePercentLowFPS: onePercentLowFPS, + p95IntervalMilliseconds: p95IntervalMilliseconds, + p99IntervalMilliseconds: p99IntervalMilliseconds, + jankRate: jankRate, + severeRate: severeRate, + missedVsyncRate: missedVsyncRate + ) + } + +} diff --git a/Tests/TFTMACTests/GameFrameTelemetryTests.swift b/Tests/TFTMACTests/GameFrameTelemetryTests.swift new file mode 100644 index 0000000..7c28a73 --- /dev/null +++ b/Tests/TFTMACTests/GameFrameTelemetryTests.swift @@ -0,0 +1,105 @@ +import XCTest + +final class GameFrameTelemetryTests: XCTestCase { + private let layer = "SurfaceView[com.riotgames.league.teamfighttactics/com.epicgames.unreal.GameActivity](BLAST)#42" + private let refresh: UInt64 = 16_666_667 + + func testSteadySixtyHertzProducesSixtyFPSWindow() throws { + var sampler = GameFrameTelemetrySampler() + XCTAssertEqual(sampler.updateLayerList(layer), .available) + _ = sampler.ingestLatency(latency(timestamps: timestamps(count: 1)), observedMonotonicNS: 1) + let update = sampler.ingestLatency(latency(timestamps: timestamps(count: 61)), observedMonotonicNS: 1_000_000_001) + let window = try XCTUnwrap(update.window) + XCTAssertEqual(window.frameCount, 60) + XCTAssertEqual(window.effectiveFPS, 60, accuracy: 0.001) + XCTAssertEqual(window.p95MS ?? 0, 16.666667, accuracy: 0.001) + XCTAssertEqual(window.jankCount, 0) + XCTAssertEqual(window.missedVsyncEquivalents, 0) + } + + func testHitchCountsJankAndMissedVsyncs() throws { + var sampler = GameFrameTelemetrySampler() + _ = sampler.updateLayerList(layer) + _ = sampler.ingestLatency(latency(timestamps: timestamps(count: 1)), observedMonotonicNS: 1) + var values = timestamps(count: 31) + let hitchTimestamp = values.last! + 150_000_003 + values += [hitchTimestamp, hitchTimestamp + refresh, hitchTimestamp + (2 * refresh)] + let window = try XCTUnwrap(sampler.ingestLatency(latency(timestamps: values), observedMonotonicNS: 1_000_000_001).window) + XCTAssertEqual(window.maximumMS ?? 0, 150, accuracy: 0.001) + XCTAssertEqual(window.jankCount, 1) + XCTAssertEqual(window.severeCount, 1) + XCTAssertEqual(window.missedVsyncEquivalents, 8) + XCTAssertEqual(window.onePercentLowFPS ?? 0, 6.666666, accuracy: 0.001) + } + + func testOverlappingPollsDoNotDuplicateIntervals() throws { + var sampler = GameFrameTelemetrySampler() + _ = sampler.updateLayerList(layer) + let first = sampler.ingestLatency(latency(timestamps: timestamps(count: 40)), observedMonotonicNS: 1) + XCTAssertEqual(first.intervals.count, 0) + let secondValues = Array(timestamps(count: 40).suffix(20)) + timestamps(count: 20, start: 41 * refresh) + let second = sampler.ingestLatency(latency(timestamps: secondValues), observedMonotonicNS: 500_000_001) + XCTAssertEqual(second.intervals.count, 20) + XCTAssertNil(second.window) + let thirdValues = Array(secondValues.suffix(20)) + timestamps(count: 30, start: 61 * refresh) + let third = sampler.ingestLatency(latency(timestamps: thirdValues), observedMonotonicNS: 1_000_000_001) + XCTAssertEqual(third.intervals.count, 30) + XCTAssertEqual(try XCTUnwrap(third.window).frameCount, 50) + } + + func testLayerResetPreventsCrossLayerInterval() { + var sampler = GameFrameTelemetrySampler() + _ = sampler.updateLayerList(layer) + _ = sampler.ingestLatency(latency(timestamps: timestamps(count: 3)), observedMonotonicNS: 100) + _ = sampler.updateLayerList(layer.replacingOccurrences(of: "#42", with: "#43")) + let update = sampler.ingestLatency(latency(timestamps: timestamps(count: 3, start: 9_000_000_000)), observedMonotonicNS: 200) + XCTAssertEqual(update.intervals.count, 0) + } + + func testMissingAndMultipleLayersFailClosed() { + XCTAssertEqual(GameFrameTelemetry.selectTFTSurfaceViewLayer(from: "unrelated"), .unavailable(.noTFTSurfaceView)) + XCTAssertEqual(GameFrameTelemetry.selectTFTSurfaceViewLayer(from: "\(layer)\n\(layer.replacingOccurrences(of: "#42", with: "#43"))"), .unavailable(.multipleTFTSurfaceViews)) + var sampler = GameFrameTelemetrySampler() + XCTAssertEqual(sampler.updateLayerList("unrelated"), .unavailable(.noTFTSurfaceView)) + XCTAssertNil(sampler.ingestLatency(latency(timestamps: timestamps(count: 2)), observedMonotonicNS: 1_000_000_000).window) + } + + func testAndroidSixteenRequestedLayerPrefixIsPreservedForExactQuery() { + let requested = "RequestedLayerState{d6240da \(layer) parentId=128}" + XCTAssertEqual( + GameFrameTelemetry.selectTFTSurfaceViewLayer(from: requested), + .selected("d6240da \(layer)") + ) + XCTAssertEqual( + GameFrameTelemetry.surfaceFlingerLatencyShellCommand(layerName: "d6240da \(layer)"), + "dumpsys SurfaceFlinger --latency 'd6240da \(layer)'" + ) + } + + func testNoNewFrameWindowIsAnActualZeroNotUnavailable() throws { + var sampler = GameFrameTelemetrySampler() + _ = sampler.updateLayerList(layer) + _ = sampler.ingestLatency(latency(timestamps: timestamps(count: 2)), observedMonotonicNS: 100) + let update = sampler.ingestLatency(latency(timestamps: timestamps(count: 2)), observedMonotonicNS: 1_000_000_100) + let window = try XCTUnwrap(update.window) + XCTAssertEqual(window.status, .available) + XCTAssertEqual(window.frameCount, 0) + let empty = sampler.ingestLatency(latency(timestamps: timestamps(count: 2)), observedMonotonicNS: 2_000_000_100) + XCTAssertEqual(try XCTUnwrap(empty.window).frameCount, 0) + XCTAssertEqual(try XCTUnwrap(empty.window).effectiveFPS, 0) + } + + func testZeroAndSentinelRowsAreFiltered() throws { + let raw = "\(refresh)\n0 0 0\n9223372036854775807 9223372036854775807 9223372036854775807\n18446744073709551615 18446744073709551615 18446744073709551615\n1 2 3\n" + let poll = try XCTUnwrap(GameFrameTelemetry.parseSurfaceFlingerLatency(raw)) + XCTAssertEqual(poll.samples, [GameFrameLatencySample(desiredPresentNS: 1, actualPresentNS: 2, frameReadyNS: 3)]) + } + + private func timestamps(count: Int, start: UInt64 = 16_666_667) -> [UInt64] { + (0.. String { + ([String(refresh)] + timestamps.map { "\($0 + 1) \($0) \($0 + 2)" }).joined(separator: "\n") + } +} diff --git a/Tests/TFTMACTests/GraphicsStackReceiptTests.swift b/Tests/TFTMACTests/GraphicsStackReceiptTests.swift new file mode 100644 index 0000000..1706daa --- /dev/null +++ b/Tests/TFTMACTests/GraphicsStackReceiptTests.swift @@ -0,0 +1,40 @@ +import XCTest + +final class GraphicsStackReceiptTests: XCTestCase { + func testCanonicalJSONAndHashIgnoreFieldInsertionOrder() { + let surface = GraphicsStackReceiptField(value: "EXACT_LAYER_ACTIVE", source: "SURFACEFLINGER", confidence: "PROVEN") + let angle = GraphicsStackReceiptField(value: "PROVEN_ACTIVE", source: "EMULATOR_STDERR", confidence: "PROVEN") + + let first = GraphicsStackReceipt(fields: ["surface": surface, "angle": angle]) + let second = GraphicsStackReceipt(fields: ["angle": angle, "surface": surface]) + + XCTAssertEqual(first.canonicalJSON, second.canonicalJSON) + XCTAssertEqual(first.sha256, second.sha256) + } + + func testFieldValueChangeChangesHash() { + let baseline = GraphicsStackReceipt(fields: [ + "gfxstream": GraphicsStackReceiptField(value: "PROVEN_ACTIVE", source: "EMULATOR_STDOUT", confidence: "PROVEN") + ]) + let changed = GraphicsStackReceipt(fields: [ + "gfxstream": GraphicsStackReceiptField(value: "NOT_OBSERVED", source: "EMULATOR_STDOUT", confidence: "PROVEN") + ]) + + XCTAssertNotEqual(baseline.sha256, changed.sha256) + } + + func testCompletenessAndExplicitUnknownsAreDeterministic() { + let receipt = GraphicsStackReceipt(fields: [ + "surface": GraphicsStackReceiptField(value: "EXACT_LAYER_ACTIVE", source: "SURFACEFLINGER", confidence: "PROVEN"), + "moltenvk": GraphicsStackReceiptField(value: "", source: "EMULATOR_STDERR", confidence: "UNKNOWN"), + "angle": GraphicsStackReceiptField(value: "", source: "ADB_GETPROP", confidence: "UNKNOWN") + ]) + + XCTAssertEqual(receipt.explicitUnknownKeys(), ["angle", "moltenvk"]) + XCTAssertEqual(receipt.unresolvedRequiredKeys(requiredKeys: ["surface", "moltenvk", "gfxstream"]), ["gfxstream", "moltenvk"]) + XCTAssertEqual(receipt.completeness(requiredKeys: ["surface", "moltenvk", "gfxstream"]), .partial) + XCTAssertEqual(receipt.completeness(requiredKeys: ["moltenvk", "gfxstream"]), .unknown) + XCTAssertEqual(receipt.completeness(requiredKeys: ["surface"]), .complete) + } + +} diff --git a/Tests/TFTMACTests/TFTMACGate1Tests.swift b/Tests/TFTMACTests/TFTMACGate1Tests.swift new file mode 100644 index 0000000..028e25f --- /dev/null +++ b/Tests/TFTMACTests/TFTMACGate1Tests.swift @@ -0,0 +1,267 @@ +import CoreGraphics +import XCTest + +final class TFTMACGate1Tests: XCTestCase { + func testAspectFitCentersSixteenByNineInsideMatchingViewport() { + let mapper = ViewportMapper( + sourceSize: CGSize(width: 1920, height: 1080), + viewportSize: CGSize(width: 1600, height: 900) + ) + XCTAssertEqual(mapper.displayedRect, CGRect(x: 0, y: 0, width: 1600, height: 900)) + } + + func testLetterboxRegionDoesNotProduceAndroidTouch() { + let mapper = ViewportMapper( + sourceSize: CGSize(width: 1920, height: 1080), + viewportSize: CGSize(width: 1600, height: 1000) + ) + XCTAssertNil(mapper.sourcePoint(for: CGPoint(x: 800, y: 20))) + } + + func testViewportCenterMapsToSourceCenter() throws { + let mapper = ViewportMapper( + sourceSize: CGSize(width: 1920, height: 1080), + viewportSize: CGSize(width: 1600, height: 1000) + ) + let source = try XCTUnwrap(mapper.sourcePoint(for: CGPoint(x: 800, y: 500))) + XCTAssertEqual(source.x, 960, accuracy: 0.001) + XCTAssertEqual(source.y, 540, accuracy: 0.001) + } + + func testPrimaryTouchKeepsItsIdentifierUntilZeroPressureRelease() { + var sequence = PrimaryTouchSequence() + let point = TouchPoint(x: 1716, y: 898) + let contact = sequence.contact(at: point) + let release = sequence.release(at: nil) + + XCTAssertEqual(contact?.identifier, TouchInput.primaryIdentifier) + XCTAssertEqual(release?.identifier, contact?.identifier) + XCTAssertEqual(release.map { TouchPoint(x: $0.x, y: $0.y) }, point) + XCTAssertEqual(contact?.pressure, 1) + XCTAssertEqual(release?.pressure, 0) + XCTAssertNil(sequence.release(at: nil)) + } + + func testNativeRGBAFrameContractAcceptsExact1080pFrame() throws { + XCTAssertNoThrow(try FrameContract.validate( + width: 1920, + height: 1080, + byteCount: 1920 * 1080 * 4 + )) + } + + func testNativeRGBAFrameContractRejectsTruncatedFrame() { + XCTAssertThrowsError(try FrameContract.validate( + width: 1920, + height: 1080, + byteCount: 1920 * 1080 * 4 - 1 + )) { error in + XCTAssertEqual( + error as? FrameContractError, + .wrongByteCount(expected: 1920 * 1080 * 4, actual: 1920 * 1080 * 4 - 1) + ) + } + } + + func testLatestFrameMailboxIsBoundedToNewestFrame() { + let mailbox = LatestFrameMailbox() + let first = EmulatorFrame( + pixels: Data(count: 4), width: 1, height: 1, sequence: 1, + emulatorTimestampMicroseconds: 1, receivedMonotonicNanoseconds: 1 + ) + let second = EmulatorFrame( + pixels: Data(count: 4), width: 1, height: 1, sequence: 2, + emulatorTimestampMicroseconds: 2, receivedMonotonicNanoseconds: 2 + ) + mailbox.publish(first) + mailbox.publish(second) + XCTAssertEqual(mailbox.takeLatest()?.sequence, 2) + XCTAssertNil(mailbox.takeLatest()) + XCTAssertEqual(mailbox.snapshot().replacedBeforePresentation, 1) + } + + func testAVDRestoreAllowsOnlyTheAppliedConfiguration() throws { + XCTAssertEqual( + try AVDTransactionGuard.restoreDecision( + currentSHA256: "applied", originalSHA256: "original", appliedSHA256: "applied" + ), + .restoreBackup + ) + } + + func testAVDRestoreDoesNotOverwriteAConflictingConfiguration() { + XCTAssertThrowsError(try AVDTransactionGuard.restoreDecision( + currentSHA256: "changed-by-someone-else", + originalSHA256: "original", + appliedSHA256: "applied" + )) { error in + XCTAssertEqual(error as? AVDTransactionGuardError, .conflictingCurrentConfiguration) + } + } + + func testRuntimeProfileRejectsUnsupportedValues() { + let baseline = TFTMACRuntimeProfile.playable + let candidate = baseline.with(vCPU: 99, ramMiB: 1, refreshHz: 144, asgDrawFlushInterval: 7) + XCTAssertEqual(candidate.vCPU, baseline.vCPU) + XCTAssertEqual(candidate.ramMiB, baseline.ramMiB) + XCTAssertEqual(candidate.refreshHz, baseline.refreshHz) + XCTAssertEqual(candidate.asgDrawFlushInterval, baseline.asgDrawFlushInterval) + XCTAssertEqual(candidate.width, 1920) + XCTAssertEqual(candidate.height, 1080) + } + + func testRuntimeProfileAcceptsSafeExperimentValues() { + let candidate = TFTMACRuntimeProfile.playable.with( + vCPU: 8, + ramMiB: 6144, + refreshHz: 30, + asgDrawFlushInterval: 400 + ) + XCTAssertEqual(candidate.vCPU, 8) + XCTAssertEqual(candidate.ramMiB, 6144) + XCTAssertEqual(candidate.refreshHz, 30) + XCTAssertEqual(candidate.asgDrawFlushInterval, 400) + XCTAssertEqual(candidate.identifier, "tftmac_native_6144m_8c_30hz_flush400") + } + + func testRapidCombatExperimentHasExactlyTwoNamedPresets() { + XCTAssertEqual(RuntimeExperimentPreset.selectableCases.map(\.rawValue), ["control", "combat_latency_a"]) + } + + func testCombatLatencyAChangesOnlyTheHostSchedulingRequest() { + let control = TFTMACRuntimeProfile.playable.with(experimentPreset: .control) + let candidate = TFTMACRuntimeProfile.playable.with(experimentPreset: .combatLatencyA) + XCTAssertEqual(control.effectiveEmulatorFeatures, RuntimeExperimentPreset.baselineEmulatorFeatures) + XCTAssertEqual(candidate.effectiveEmulatorFeatures, RuntimeExperimentPreset.baselineEmulatorFeatures) + XCTAssertEqual(control.vCPU, candidate.vCPU) + XCTAssertEqual(control.ramMiB, candidate.ramMiB) + XCTAssertEqual(control.asgDrawFlushInterval, candidate.asgDrawFlushInterval) + XCTAssertFalse(control.experimentPreset.requestsHostLatencyQoS) + XCTAssertTrue(candidate.experimentPreset.requestsHostLatencyQoS) + XCTAssertEqual(control.comparisonConfigurationSHA256, candidate.comparisonConfigurationSHA256) + XCTAssertNotEqual(control.experimentConfigurationReceipt.sha256, candidate.experimentConfigurationReceipt.sha256) + } + + func testRetiredPerformanceModePresetMigratesToControl() throws { + let suiteName = "tftmac-tests-\(UUID().uuidString)" + let suite = try XCTUnwrap(UserDefaults(suiteName: suiteName)) + defer { suite.removePersistentDomain(forName: suiteName) } + suite.set("home_run_a", forKey: "runtime.experimentPreset") + XCTAssertEqual(RuntimeExperimentPreset.load(from: suite), .control) + } + + func testGuestPowerReceiptRequiresPoweredStayOnAndAwake() throws { + let ready = try XCTUnwrap(GuestPowerState.parse(""" + mIsPowered=true + mStayOn=true + mWakefulness=Awake + """)) + XCTAssertTrue(ready.isGameplayReady) + let sleeping = try XCTUnwrap(GuestPowerState.parse(""" + mIsPowered=false + mStayOn=false + mWakefulness=Asleep + """)) + XCTAssertFalse(sleeping.isGameplayReady) + } + + func testHostSchedulingReceiptVerifiesUserInteractiveRequest() throws { + let receipt = try XCTUnwrap(HostSchedulingReceipt.parse(""" + TFTMAC_HOST_QOS_REQUESTED=user_interactive + TFTMAC_HOST_QOS_SET_RESULT=0 + TFTMAC_HOST_QOS_EFFECTIVE=user_interactive + TFTMAC_HOST_QOS_RELATIVE_PRIORITY=0 + """)) + XCTAssertTrue(receipt.userInteractiveVerified) + } + + func testCombatComparisonNormalizesDynamicSurfaceLayerTokens() throws { + let first = "fe46e7c SurfaceView[com.riotgames.league.teamfighttactics/com.epicgames.unreal.GameActivity](BLAST)#136" + let second = "991abcd SurfaceView[com.riotgames.league.teamfighttactics/com.epicgames.unreal.GameActivity](BLAST)#42" + XCTAssertEqual( + CombatLayerIdentity.comparable(first), + CombatLayerIdentity.comparable(second) + ) + XCTAssertNil(CombatLayerIdentity.comparable("NexusLauncher#1")) + } + + func testRuntimeLeaseRejectsASecondLiveOwner() throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("tftmac-lease-test-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: root) } + let first = try TFTMACRuntimeLease.acquire(stateRoot: root) + defer { first.release() } + XCTAssertThrowsError(try TFTMACRuntimeLease.acquire(stateRoot: root)) { error in + guard case RuntimeLeaseError.alreadyOwned = error else { + return XCTFail("Unexpected error: \(error)") + } + } + } + + func testAVDRecoveryRejectsBackupOutsideCaptureRoot() { + XCTAssertThrowsError(try AVDTransactionGuard.validateRecoveryPaths( + markerConfigURL: URL(fileURLWithPath: "/runtime/TFT.avd/config.ini"), + expectedConfigURL: URL(fileURLWithPath: "/runtime/TFT.avd/config.ini"), + backupURL: URL(fileURLWithPath: "/tmp/untrusted/avd-config.before.ini"), + captureRoot: URL(fileURLWithPath: "/captures", isDirectory: true) + )) { error in + XCTAssertEqual(error as? AVDTransactionGuardError, .unexpectedRecoveryPath) + } + } + + func testMemoryKillClassifierAcceptsConfirmedVictims() { + XCTAssertTrue(TelemetrySignalClassifier.isConfirmedGuestMemoryKill( + "08-30 04:10:00.000 I lmkd: Kill 'com.riotgames.league.teamfighttactics' (4024), uid 10123, oom_score_adj 900" + )) + XCTAssertTrue(TelemetrySignalClassifier.isConfirmedGuestMemoryKill( + "08-30 04:10:00.000 I lowmemorykiller: Killing 'com.example.background' (4025), adj 950" + )) + XCTAssertTrue(TelemetrySignalClassifier.isConfirmedGuestMemoryKill( + "kernel: Out of memory: Killed process 4024 (TFTMain) total-vm:1234kB" + )) + } + + func testMemoryKillClassifierRejectsBootAndSetupNoise() { + let nonKills = [ + "lmkd: Connection with lmkd established", + "lowmemorykiller: lowmemorykiller data connection established", + "lmkd: memevent failed to attach", + "lmkd: android_trigger_vendor_lmk_kill tracepoint unavailable", + "lmkd: Using psi monitors for memory pressure detection", + "com.riotgames.league.teamfighttactics: java.lang.OutOfMemoryError", + "ActivityManager: Killing com.riotgames.league.teamfighttactics for cached #17", + "kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null)" + ] + for line in nonKills { + XCTAssertFalse(TelemetrySignalClassifier.isConfirmedGuestMemoryKill(line), line) + } + } + + func testPipelineClassifierRejectsNormalConfigurationReceipts() { + let normalLines = [ + "gfxstream: using Vulkan host renderer", + "virtio-gpu-asg write buffer size 1048576", + "MoltenVK version 1.4 initialized", + "shader cache directory ready", + "sync fence support enabled" + ] + for line in normalLines { + XCTAssertEqual(TelemetrySignalClassifier.pipelineSignals(in: line), PipelineLogSignals(), line) + } + } + + func testPipelineClassifierNamesDiagnosticBoundaries() { + XCTAssertEqual( + TelemetrySignalClassifier.pipelineSignals(in: "gfxstream warning: host queue stalled"), + PipelineLogSignals(gfxstreamWarningCount: 1) + ) + XCTAssertEqual( + TelemetrySignalClassifier.pipelineSignals(in: "virtio-gpu-asg timeout waiting for ring fence"), + PipelineLogSignals(asgStallCount: 1, fenceTimeoutCount: 1) + ) + XCTAssertEqual( + TelemetrySignalClassifier.pipelineSignals(in: "[MVK] Vulkan error: shader compilation failed"), + PipelineLogSignals(vulkanErrorCount: 1, moltenVKWarningCount: 1, shaderErrorCount: 1) + ) + } +} diff --git a/Vendor/AndroidEmulator/SOURCE.json b/Vendor/AndroidEmulator/SOURCE.json new file mode 100644 index 0000000..e75e3e5 --- /dev/null +++ b/Vendor/AndroidEmulator/SOURCE.json @@ -0,0 +1,28 @@ +{ + "schema": 1, + "observedAt": "2026-08-30T03:58:38.083Z", + "authority": "INSTALLED_ANDROID_EMULATOR", + "emulatorVersion": "Android emulator version 37.1.11.0 (build_id 15917651) (CL:N/A)", + "emulatorBinary": "/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk/emulator/emulator", + "installedProtoPath": "/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk/emulator/lib/emulator_controller.proto", + "installedProtoSHA256": "1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f", + "vendoredProtoPath": "Vendor/AndroidEmulator/emulator_controller.proto", + "vendoredProtoSHA256": "1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f", + "generator": { + "protocVersion": "36.0", + "swiftProtobufVersion": "1.38.1", + "grpcSwiftProtobufVersion": "2.4.1", + "script": "scripts/generate-emulator-proto.command" + }, + "generatedSources": { + "Generated/EmulatorController/emulator_controller.pb.swift": "55641fbefbf3baae5ab309dc722dcad5c7e9ea90644a8c027c78fbbadf3d8204", + "Generated/EmulatorController/emulator_controller.grpc.swift": "653febae34de04ce4ebdc2a38f46b9a0e015b2f5bb55b66a24c785f24b46e02f" + }, + "aemuAuthority": { + "branch": "emu-master-dev", + "qemuCommit": "ae9d18d2b6261179fbd57fffec720a04f7bfb053", + "aemuCommit": "3c1ced8a369417db591eb7cd083af5bb2c317975", + "manifestSHA256": "28865cd8a162178ba462b296f5714b6b7b6916e0cafcddfc0c5e44aa03f8e8d3" + }, + "contract": "The vendored protocol bytes come directly from the installed stock Android Emulator runtime used by TFTMAC. Generated Swift sources are reproducible from that frozen protocol with the recorded pinned generator toolchain. Build/generation must fail on byte drift until the authority is intentionally refreshed." +} diff --git a/Vendor/AndroidEmulator/emulator_controller.proto b/Vendor/AndroidEmulator/emulator_controller.proto new file mode 100644 index 0000000..d148312 --- /dev/null +++ b/Vendor/AndroidEmulator/emulator_controller.proto @@ -0,0 +1,1865 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Note that if you add/remove methods in this file you must update +// the metrics sql as well ./android/scripts/gen-grpc-sql.py +// +// Please group deleted methods in a block including the date (MM/DD/YY) +// it was removed. This enables us to easily keep metrics around after removal +// +// List of deleted methods +// rpc iWasDeleted (03/12/12) +// ... +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "com.android.emulator.control"; +option objc_class_prefix = "AEC"; + +package android.emulation.control; +import "google/protobuf/empty.proto"; + +// An EmulatorController service lets you control the emulator. +// Note that this is currently an experimental feature, and that the +// service definition might change without notice. Use at your own risk! +// +// We use the following rough conventions: +// +// streamXXX --> streams values XXX (usually for emulator lifetime). Values +// are updated as soon as they become available. +// getXXX --> gets a single value XXX +// setXXX --> sets a single value XXX, does not returning state, these +// usually have an observable lasting side effect. +// sendXXX --> send a single event XXX, possibly returning state information. +// android usually responds to these events. +service EmulatorController { + // set/get/stream the sensor data + // This RPC is not implemented in EmulatorController and will return an unimplemented error. + rpc streamSensor(SensorValue) returns (stream SensorValue) {} + // Gets the current value of a specified sensor. + // + // The following gRPC error codes can be returned: + // - INTERNAL (code 13) if `getSensorSize` fails to retrieve sensor dimensions. + // + // The `status` field in the `SensorValue` reply indicates the operational state: + // - `OK` (0): Sensor data retrieved successfully. + // - `UNKNOWN` (2): Unknown sensor type (should not happen if using valid `SensorType`). + // - `DISABLED` (3): The sensor is disabled. + // - `NO_SERVICE` (4): The `qemud` service responsible for sensors is not available or initiated. + rpc getSensor(SensorValue) returns (SensorValue) {} + // Sets the override value for a specified sensor. This operation is asynchronous + // and executed on the emulator's main looper. An immediate subsequent `getSensor` + // call might not reflect the newly set value. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + // No specific gRPC error codes are returned by this method itself, but underlying + // operations might log warnings if the sensor agent is unavailable. + rpc setSensor(SensorValue) returns (google.protobuf.Empty) {} + + // set/get/stream the physical model, this is likely the one you are + // looking for when you wish to modify the device state. + // + // This operation is asynchronous and executed on the emulator's main looper. An immediate subsequent `getPhysicalModel` + // call might not reflect the newly set value. The `interpolation` field from the request is used to determine the + // physical interpolation method, mapped using `abs((static_cast(physicalValue.interpolation())) - 1)`. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + rpc setPhysicalModel(PhysicalModelValue) returns (google.protobuf.Empty) {} + // Gets the current value of a specified physical model parameter. + // + // The `status` field in the `PhysicalModelValue` reply indicates the operational state: + // - `OK` (0): Physical model data retrieved successfully. + // - `UNKNOWN` (2): Unknown physical type (should not happen if using valid `PhysicalType`). + // - `NO_SERVICE` (3): The `qemud` service responsible for physical parameters is not available or initiated. + rpc getPhysicalModel(PhysicalModelValue) returns (PhysicalModelValue) {} + // This RPC is not implemented in EmulatorController and will return an unimplemented error. + rpc streamPhysicalModel(PhysicalModelValue) + returns (stream PhysicalModelValue) {} + + // Atomically sets the current primary clipboard data. This operation is asynchronous + // and executed on the emulator's main looper. It triggers a `ClipboardEvent` + // to all listeners (except the originating channel) indicating the new content. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + rpc setClipboard(ClipData) returns (google.protobuf.Empty) {} + // Retrieves the current primary clipboard data. This is a synchronous operation. + // + // This method returns `OK` (code 0) and the current `ClipData` upon success. + rpc getClipboard(google.protobuf.Empty) returns (ClipData) {} + + // Streams real-time updates of the clipboard content. Upon subscription, + // it immediately sends the current clipboard state. Subsequent updates are + // streamed as new content becomes available from the guest or is set via `setClipboard` + // from a different client. Events originating from the same client that initiated + // the stream are filtered out to prevent echoing. + // + // It is possible to miss very rapid clipboard updates. The stream will block + // awaiting new events after the initial state is sent. + // + // This method returns a server-side streaming reactor. + rpc streamClipboard(google.protobuf.Empty) returns (stream ClipData) {} + + // Sets the emulator's battery state to the provided values. This operation is + // executed asynchronously on the main looper. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling + // of the operation. No explicit gRPC error codes are returned by this method. + rpc setBattery(BatteryState) returns (google.protobuf.Empty) {} + // Retrieves the current battery state from the emulator. This is a + // synchronous operation that waits for completion. + // + // This method returns `OK` (code 0) and populates the `BatteryState` reply + // with the current information upon success. No explicit gRPC error codes + // are returned by this method. + rpc getBattery(google.protobuf.Empty) returns (BatteryState) {} + + // Sets the state of the GPS in the emulator. This operation is asynchronous + // and executed on the main looper. It updates the emulator's GPS location + // (latitude, longitude, altitude, speed, bearing, and satellites) and sets + // the timestamp. + // + // Note: Setting the GPS position will not be immediately reflected in the user + // interface. Android typically samples GPS at 1 Hz. + // This method returns `OK` (code 0) upon successful asynchronous scheduling + // of the operation. + rpc setGps(GpsState) returns (google.protobuf.Empty) {} + + // Gets the latest GPS state as reported by the emulator. This includes data + // delivered by previous `setGps` calls or from the location UI if active. + // This is a synchronous operation that waits for completion. + // + // Note: The returned GPS state is not necessarily the exact coordinate + // visible at the time due to Android's typical 1 Hz GPS sample frequency. + // + // This method returns `OK` (code 0) and populates the `GpsState` reply + // with the retrieved information upon success. No explicit gRPC error codes + // are returned by this method. + rpc getGps(google.protobuf.Empty) returns (GpsState) {} + + // Simulates a touch event on the fingerprint sensor. This operation is + // executed asynchronously on the main looper. + // + // The `isTouching` field indicates whether the fingerprint sensor is being + // touched, and `touchId` specifies the identifier of the registered + // fingerprint. The `setTouch` agent function is used to apply these values. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling + // of the operation. No explicit gRPC error codes are returned by this method. + rpc sendFingerprint(Fingerprint) returns (google.protobuf.Empty) {} + + // Sends a keyboard event to the emulator. This operation is asynchronous + // and executed on the main looper. + // + // The `KeyboardEvent` message allows specifying input using `keyCode`, `key` (W3C standard string), or `text` (UTF-8 string). + // The `KeyEventSender` prioritizes `key`, then `keyCode`, then `text`. + // - If `key` is a non-printable W3C key string (e.g., "Backspace", "ArrowUp"), it's translated to an evdev keycode and sent. + // - If `key` is a printable Unicode character, it's converted to a sequence of evdev keydown/keyup events, handling modifiers. + // - If `keyCode` is provided, it's translated from its `codeType` (Usb, Evdev, XKB, Win, Mac) to an evdev keycode and sent as keydown/keyup pairs. + // - If `text` is provided, each UTF-8 character is converted to evdev keypress (keydown then keyup) events. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + rpc sendKey(KeyboardEvent) returns (google.protobuf.Empty) {} + // Sends touch events to the emulator. This operation is asynchronous and executed on the main looper. + // + // The `TouchEvent` contains a list of `Touch` objects, each with `x`, `y` coordinates, `identifier`, `pressure`, `touch_major`, `touch_minor`, `expiration`, and `orientation`. + // Coordinates are scaled to the emulator's display resolution. Each `Touch` `identifier` is mapped to an internal Linux multitouch slot. + // Expiration handling ensures that inactive touch events are properly "lifted" after 120 seconds. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. Warnings are logged if no touch slots are available. + rpc sendTouch(TouchEvent) returns (google.protobuf.Empty) {} + // Sends mouse events to the emulator. This operation is asynchronous and executed on the main looper. + // + // The `MouseEvent` specifies `x`, `y` coordinates, `buttons` state (bitmask: 1 for left, 2 for right), and `display` ID. + // In certain virtual input configurations (VirtioInput enabled, but VirtioMouse and VirtioTablet disabled), `buttons` may be masked to only consider the primary button. + // The event is sent to the emulator's `user_event_agent`. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + rpc sendMouse(MouseEvent) returns (google.protobuf.Empty) {} + // Injects a stream of wheel events into the emulator. This is a server-side streaming RPC. + // + // Each `WheelEvent` contains `dx`, `dy` (delta values, `dy` is pre-multiplied by 120 for scroll clicks), and `display` ID. + // If the emulator's input device has rotary capabilities (e.g., AVD_WEAR flavor), `dy` is scaled and sent as a rotary event. + // Otherwise, it's sent as a standard mouse wheel event. + // + // This method returns a server-side streaming reactor. + rpc injectWheel(stream WheelEvent) returns (google.protobuf.Empty) {} + + // Streams a series of generic input events to the emulator. The events are processed in the order they arrive. + // This is a server-side streaming RPC that supports various input types encapsulated within `InputEvent`'s `oneof type` field. + // + // Supported input types: + // - `key_event` (KeyboardEvent): Processed by `KeyEventSender` as described in `sendKey`. + // - `touch_event` (TouchEvent): Processed by `TouchEventSender` as described in `sendTouch`. + // - `mouse_event` (MouseEvent): Processed by `MouseEventSender` as described in `sendMouse`. + // - `android_event` (AndroidEvent): Raw Linux input events (`type`, `code`, `value`) sent directly to the kernel. + // - `pen_event` (PenEvent): Processed by `PenEventSender` for pen input, including pressure, orientation, and button state. + // - `wheel_event` (WheelEvent): Processed by `WheelEventSender` as described in `injectWheel`. + // - `xr_command` (XrCommand): Executes XR-specific commands (e.g., `RECENTER` to recenter the viewport). Unknown actions are logged as warnings. + // - `xr_head_rotation_event` (RotationRadian): Sends head rotation data in radians. Logs an error if the agent call fails. + // - `xr_head_movement_event` (Translation): Sends head movement data in meters. Logs an error if the agent call fails. + // - `xr_head_angular_velocity_event` (AngularVelocity): Sends head angular velocity data in radians per second. Logs an error if the agent call fails. + // - `xr_head_velocity_event` (Velocity): Sends head velocity data in meters per second. Logs an error if the agent call fails. + // - `touchpad_event` (TouchpadEvent): Processed by `TouchpadEventSender` for touchpad input, similar to touch events but for a touchpad device. + // + // All underlying input sending operations for XR events are executed asynchronously on the main looper. + // + // Returns `INVALID_ARGUMENT` (code 3) if an unrecognized input event type is received, indicating a potential out-of-date emulator. + // The stream reactor automatically deletes itself upon completion. + rpc streamInputEvent(stream InputEvent) returns (google.protobuf.Empty) {} + + // Initiates or manipulates a phone call in the emulator. This is a synchronous + // operation executed on the main looper. + // + // The `PhoneCall` message specifies the `operation` (e.g., InitCall, AcceptCall) + // and the target phone `number`. The `telephonyCmd` agent function handles the + // actual call action. + // + // Returns a `PhoneResponse` indicating the outcome: + // - `OK` (0): The operation was successful. + // - `BadOperation` (1): The provided `operation` enum is out of range. + // - `BadNumber` (2): The provided `number` is malformed. + // - `InvalidAction` (3): The requested `operation` is invalid given the current call state (e.g., disconnecting when no call is active). + // - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + // - `RadioOff` (5): The emulator's radio is turned off. + rpc sendPhone(PhoneCall) returns (PhoneResponse) {} + + // Sends an SMS message to the emulator. This is a synchronous operation. + // + // The `SmsMessage` contains the `srcAddress` (source phone number) and the `text` of the message. + // The `srcAddress` is validated for GSM formatting. The message `text` is converted into SMS PDUs + // (Protocol Data Units) and delivered via the modem agent. + // + // Returns a `PhoneResponse` indicating the outcome: + // - `OK` (0): The SMS message was successfully delivered. + // - `BadNumber` (2): The `srcAddress` is malformed. + // - `ActionFailed` (4): An internal error occurred, e.g., if the modem agent is unavailable or PDU creation fails. + // + // Other error cases from PDU creation (e.g., invalid characters in text) also result in `ActionFailed`. + rpc sendSms(SmsMessage) returns (PhoneResponse) {} + + // Sets the emulator's phone number. This is a synchronous operation. + // + // The `PhoneNumber` message contains the new `number` to be set. The modem agent's + // `amodem_update_phone_number` function is used for this update. + // + // Returns a `PhoneResponse` indicating the outcome: + // - `OK` (0): The phone number was successfully updated. + // - `BadNumber` (2): The provided `number` is invalid for the modem. + // - `ActionFailed` (4): An internal error occurred, typically if the modem agent is unavailable. + rpc setPhoneNumber(PhoneNumber) returns (PhoneResponse) {} + + // Retrieves the current status of the emulator. This includes comprehensive + // information about the virtual machine's configuration and the guest operating system's state. + // + // The `EmulatorStatus` reply contains: + // - `version`: The emulator version string. + // - `uptime`: The time the emulator has been active in milliseconds. + // - `booted`: A boolean indicating if the device has completed booting. + // - `vmConfig`: Detailed `VmConfiguration` (hypervisor type, CPU cores, RAM size). + // - `hardwareConfig`: Key-value pairs describing the emulator's hardware configuration. + // - `heartbeat`: A monotonically increasing number indicating guest activity (incremented approximately once per second). + // - `guestConfig`: A map of key-value pairs with guest-specific configurations, including: + // - "multidisplay": "available" or "unavailable" based on display pipe readiness. + // - "androidVersion": The Android version of the guest OS. + // - "hypervisorVersion": The hypervisor version used by the guest. + // + // This method returns `OK` (code 0) upon successful retrieval of the emulator status. + rpc getStatus(google.protobuf.Empty) returns (EmulatorStatus) {} + + // Retrieves a single screenshot in the desired format. + // + // The image will be scaled to the `desiredWidth` and `desiredHeight` specified in `ImageFormat`, while maintaining + // the aspect ratio. If `width` or `height` are 0, the device's current display dimensions are used. + // The returned image will never exceed the device's actual display resolution, but can be smaller. + // + // The `display` field in `ImageFormat` specifies the target display; 0 (or omitted) indicates the main display. + // For folded Pixel Fold devices, the display ID might be internally remapped, and `foldedDisplay` information will be populated. + // The resulting image is properly oriented based on the device's coarse-grained orientation, derived from sensor state. + // + // The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery, but if the shared memory region + // is too small, a `FAILED_PRECONDITION` error will be returned. + // + // This method returns: + // - `OK` (code 0) and an `Image` object upon success. + // - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + // - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation (an expensive operation). + // - `FAILED_PRECONDITION` (code 9) if the guest has not posted a new frame yet (for non-PNG formats when fast path is used). + // + // This method will return an image with width 0 and height 0 if the display is not visible. + rpc getScreenshot(ImageFormat) returns (Image) {} + + // Streams a series of screenshots in the desired format. This is a server-side streaming RPC. + // + // A new frame is delivered whenever the device produces a new frame or when sensor changes occur. + // Initial state: The first frame delivered might be an empty image (width 0, height 0) if the display is inactive. + // Subsequent frames are delivered when new content is available or a sensor event triggers an update. + // If the display becomes inactive, an empty image will be delivered again. Images resume when the display becomes active. + // + // `ImageFormat` parameters (format, width, height, display) behave as described in `getScreenshot`. + // The `transport` field in `ImageFormat` can specify `MMAP` for shared memory delivery. If `MMAP` is used and the + // provided shared memory handle is valid and mapped, pixel data will be written directly to it. + // The `image` field in the `Image` reply will be empty if `MMAP` transport is used. + // + // Performance considerations: + // - Streaming can produce a significant amount of data. + // - `PNG` format is CPU-intensive due to encoding overhead. + // - Using `MMAP` transport can significantly reduce gRPC overhead for pixel data. + // + // This method returns: + // - A server-side streaming reactor. + // - `INVALID_ARGUMENT` (code 3) if the specified `display` ID is invalid or disabled. + // - `CANCELLED` (code 1) if the gRPC context is cancelled during the operation. + rpc streamScreenshot(ImageFormat) returns (stream Image) {} + + // Streams a series of audio packets in the desired format. This is a server-side streaming RPC. + // A new frame is delivered approximately every 20-30ms when the emulated device produces audio. + // If `samplingRate` is 0, it defaults to 44100 Hz. + // The stream may block indefinitely if the emulator ceases to produce audio. + // Packets are allocated with a buffer size calculated based on the audio format and a 30ms frame time. + // + // This method returns `OK` (code 0) upon successful streaming initiation. + rpc streamAudio(AudioFormat) returns (stream AudioPacket) {} + + // Injects a series of audio packets into the Android microphone. This is a client-side streaming RPC. + // Audio packets are processed at a rate determined by the emulator's request for frames. + // An internal buffer can hold approximately 300ms of audio. + // + // Notes: + // - Only the `AudioFormat` from the first received packet is honored. Subsequent `AudioFormat` changes are ignored. + // - `MODE_REAL_TIME` is experimental: incoming data may overwrite existing data if the client does not control timing properly. + // - The circular buffer attempts to deliver all queued samples upon stream closure by writing silence for up to 300ms. + // + // Returns the following gRPC error codes: + // - `FAILED_PRECONDITION` (code 9): If another microphone is already active, or if unable to register the microphone. + // - `INVALID_ARGUMENT` (code 3): If the desired `samplingRate` exceeds 48kHz, or if an `AudioPacket` is too large for the internal buffer. + // + // This method returns `OK` (code 0) upon successful completion of the stream. + rpc injectAudio(stream AudioPacket) returns (google.protobuf.Empty) {} + + // Retrieves the current settings for the microphone + rpc getMicrophoneState(google.protobuf.Empty) returns (MicrophoneState) {} + + // Sets the state for the microphone + rpc setMicrophoneState(MicrophoneState) returns (google.protobuf.Empty) {} + + // Deprecated, please use the streamLogcat method instead. + rpc getLogcat(LogMessage) returns (LogMessage) { + option deprecated = true; + } + + // Streams the logcat output from the emulator in real-time. This is a server-side streaming RPC. + // The stream sources its data from the `logcat` command executed via `AdbShellStream` in the Android guest. + // Log lines are processed individually as they arrive. + // + // The `LogMessage.sort` field determines the output format: + // - If `LogMessage.sort` is `Parsed`, each incoming log line is parsed into a structured `LogcatEntry` object, + // and these entries are returned in the `LogMessage.entries` field. Structured parsing is typically + // available for Android API Level 23 (Marshmallow) and later. + // - If `LogMessage.sort` is `Text` (or unspecified), the raw log line is returned in the `LogMessage.contents` field. + // + // The stream continues as long as the underlying `logcat` process is running and the client is connected. + // The stream will naturally terminate if the `logcat` process stops or the connection is lost. + // No explicit gRPC error codes are returned by this method during active streaming. + rpc streamLogcat(LogMessage) returns (stream LogMessage) {} + + // Transitions the virtual machine to the desired state. This operation is scheduled + // asynchronously. + // + // Transition commands: + // - `RUNNING` or `START`: Starts or resumes execution of the virtual machine. + // - `PAUSED` or `STOP`: Pauses execution of the virtual machine, halting CPU cycles. + // - `RESET` or `RESTART`: Performs a warm reset of the virtual machine. + // - `SHUTDOWN`: Initiates a graceful shutdown of the emulator. + // - `TERMINATE`: Forcefully terminates the emulator process immediately without cleanup. + // This can cause data corruption and must be used with caution. + // + // Note that write-only actions (`START`, `STOP`, `RESET`, `RESTART`) perform the + // transition but are not observed as states in `getVmState`. Purely observable states + // (`UNKNOWN`, `RESTORE_VM`, `SAVE_VM`, `INTERNAL_ERROR`) cannot be set directly. + // + // Returns once the operation has been scheduled. + // Note: The call may not return when the state is set to `TERMINATE` or `SHUTDOWN`. + rpc setVmState(VmRunState) returns (google.protobuf.Empty) {} + + // Retrieves the current execution state of the virtual machine. + // + // The returned `VmRunState` represents the current condition of the VM: + // - `RUNNING`: The virtual machine is actively executing. + // - `PAUSED`: Execution is suspended (CPUs are halted). + // - `SHUTDOWN`: The guest operating system has initiated a shutdown. + // - `RESTORE_VM`: The virtual machine is paused while restoring its state (e.g., loading a snapshot). + // - `SAVE_VM`: The virtual machine is paused while saving its state (e.g., taking a snapshot). + // - `INTERNAL_ERROR`: Execution has stopped due to a guest panic, I/O error, or hypervisor failure. + // - `UNKNOWN`: The virtual machine is in an unmapped or transient state. + // + // This is a synchronous query operation. + rpc getVmState(google.protobuf.Empty) returns (VmRunState) {} + + // Atomically changes the current multi-display configuration. This operation applies + // the provided `DisplayConfigurations`, with special handling for secondary displays. + // Display ID 0 (the primary display) cannot be modified via this RPC. + // + // Preconditions: + // - The `android::featurecontrol::MultiDisplay` feature must be enabled. + // + // Input Validation: + // - `INVALID_ARGUMENT` (code 3): If duplicate `display` IDs are found in the request. + // - `INVALID_ARGUMENT` (code 3): If any `DisplayConfiguration` (width, height, dpi, flags) + // is outside valid ranges as determined by `multiDisplayParamValidate`. + // - `INVALID_ARGUMENT` (code 3): If any `display` ID is outside the configurable range + // `[1, userConfigurable]`. + // + // Atomic Update and Rollback: + // The system attempts to apply each display configuration. If any update fails + // (e.g., after multiple retries for transient pipe errors), a rollback mechanism + // is initiated: successfully updated displays are reverted to their previous state, + // and newly added displays are deleted. In such cases, an `INTERNAL` error is returned. + // + // Deletion of Unrequested Displays: + // Any displays that were active before this call but are not present in the new + // `request.displays` (and are not display ID 0) will be deleted. + // + // Notifications: + // Upon successful completion, `notifyDisplayChanges()` is called to inform + // third-party subscribers. + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful application of all configurations, returning the final active `DisplayConfigurations`. + // - `FAILED_PRECONDITION` (code 9): If the multi-display feature is not available. + // - `INTERNAL` (code 13): If an internal emulator failure occurs during display modification or rollback. + rpc setDisplayConfigurations(DisplayConfigurations) + returns (DisplayConfigurations) {} + + // Returns all currently valid logical displays configured in the emulator. This is a synchronous operation. + // + // Preconditions: + // - The `android::featurecontrol::MultiDisplay` feature must be enabled. + // + // The `DisplayConfigurations` reply contains: + // - `displays`: A repeated field of `DisplayConfiguration` objects, each detailing + // the width, height, DPI, flags, and ID of an active display. + // For Pixel Fold devices, only the main display configuration might be relevant. + // - `userConfigurable`: The maximum number of user-configurable displays (IDs from 1 up to this value). + // - `maxDisplays`: The total maximum number of displays the emulator supports. + // + // This method returns: + // - `OK` (code 0) and the current `DisplayConfigurations` upon success. + // - `FAILED_PRECONDITION` (code 9): If the AVD does not support the multi-display feature. + rpc getDisplayConfigurations(google.protobuf.Empty) + returns (DisplayConfigurations) {} + + // Notifies the client of various emulator state changes in real-time. This is a server-side streaming RPC. + // Upon subscription, the current states of virtual scene camera, foldable posture, boot completion, + // and XR options are immediately sent. The stream then continuously delivers new notifications + // when relevant events occur. `UniqueEventStreamWriter` ensures that only distinct state changes are streamed. + // + // Notifications include: + // - `CameraNotification`: Reports virtual scene camera activation/deactivation and associated display. + // - `DisplayConfigurationsChangedNotification`: Triggered when display configurations are modified via the extended UI. Does not fire for changes made through console or gRPC. + // - `Posture`: Reports changes in the device's foldable posture. + // - `BootCompletedNotification`: Indicates when the emulator has finished booting. + // - `BrightnessValue`: Reports changes in backlight brightness (LCD, keyboard, or button). + // - `TextViewFocus`: Sent when a text view gains or loses focus, or immediately on subscription. + // - `XrOptions`: Reports changes in XR-related settings (environment, passthrough coefficient), or immediately on subscription. + // - `MicrophoneState` : Reports changes in the microphone state (currently whether host microphone access is allowed) + // - `LedIndicator`: Reports changes in AI Glasses XR LED light state on subscription. + // + // This method returns a server-side streaming reactor. + rpc streamNotification(google.protobuf.Empty) + returns (stream Notification) {} + + // Rotates the virtual scene camera relative to its current orientation. This operation is asynchronous + // and executed on the main looper. + // + // The `RotationRadian` message specifies angles in radians around the x, y, and z axes. The z component of rotation is currently unused. + // The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + // This operation only succeeds if the virtual scene camera is actively connected. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + rpc rotateVirtualSceneCamera(RotationRadian) + returns (google.protobuf.Empty) {} + // Sets the absolute velocity of the virtual scene camera. This operation is asynchronous + // and executed on the main looper. + // + // The `Velocity` message specifies components in meters per second along the x, y, and z axes. + // The coordinate system is right-handed: x-axis points right, y-axis points up, and z-axis points towards the viewer. + // The transition to these target velocity values may be smoothed over time by the implementation. + // This operation only succeeds if the virtual scene camera is actively connected. + // + // This method returns `OK` (code 0) upon successful asynchronous scheduling of the operation. + rpc setVirtualSceneCameraVelocity(Velocity) + returns (google.protobuf.Empty) {} + // Sets the foldable posture of the device. This operation is asynchronous + // and executed on the main looper. + // + // The `Posture` message contains a `PostureValue` enum, defining the desired + // physical configuration of the foldable device. + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + // - `FAILED_PRECONDITION` (code 9): If the emulator is unable to set the specified posture. + rpc setPosture(Posture) returns (google.protobuf.Empty) {} + + // Retrieves the current backlight brightness for a specified light type. This is a synchronous operation. + // + // The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is queried. + // Internal mapping converts these to string names like "lcd_backlight". + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful retrieval, populating the `BrightnessValue` reply with the `target` and current `value`. + // - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `getBrightness` function is unavailable. + rpc getBrightness(BrightnessValue) returns (BrightnessValue) {} + + // Sets the backlight brightness for a specified light type. This operation is asynchronous + // and executed on the main looper. + // + // The `BrightnessValue.target` field (e.g., `LCD`, `KEYBOARD`, `BUTTON`) determines which light's brightness is set. + // The `BrightnessValue.value` specifies the desired intensity, ranging from 0 to 255. + // Internal mapping converts `LightType` to string names like "lcd_backlight". + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + // - `FAILED_PRECONDITION` (code 9): If the AVD does not support the `hw-control` agent or its `setBrightness` function is unavailable. + // - `INVALID_ARGUMENT` (code 3): If the `brightness` value exceeds the valid range (0-255). + rpc setBrightness(BrightnessValue) returns (google.protobuf.Empty) {} + + // Returns the current mode of the primary display of a resizable AVD. This is a synchronous operation. + // + // Preconditions: + // - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + // + // The `DisplayMode.value` field will be populated with the current `DisplayModeValue`, + // derived from the `getResizableActiveConfigId()`. + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful retrieval of the display mode. + // - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + rpc getDisplayMode(google.protobuf.Empty) returns (DisplayMode) {} + + // Sets the size of the primary display of a resizable AVD to the specified `DisplayModeValue`. This operation is asynchronous + // and executed on the main looper. + // + // Preconditions: + // - The AVD must be configured to support resizable displays (`resizableEnabled()` must be true). + // - The emulator's `changeResizableDisplay` agent function must be available. + // + // The `DisplayMode.value` field specifies the desired display configuration (e.g., `PHONE`, `FOLDABLE`, `TABLET`, `DESKTOP`). + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + // - `FAILED_PRECONDITION` (code 9): If the AVD is not resizable. + // - `INTERNAL` (code 13): If the internal window agent function `changeResizableDisplay` is not available. + rpc setDisplayMode(DisplayMode) returns (google.protobuf.Empty) {} + + // Changes the XR-related settings of the emulator. This operation is asynchronous + // and executed on the main looper. + // + // The `XrOptions` message specifies the `environment` (e.g., `LIVING_ROOM_DAY`, `LIVING_ROOM_NIGHT`) + // and `passthrough_coefficient` (a float between 0.0 and 1.0 for real/artificial environment visibility). + // Values for `passthrough_coefficient` outside [0.0, 1.0] are ignored. + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful asynchronous scheduling of the operation. + // - `FAILED_PRECONDITION` (code 9): If the `setXrOptions` agent function fails (e.g., if XR is not supported). + rpc setXrOptions(XrOptions) returns (google.protobuf.Empty) {} + + // Retrieves the current state of XR-related settings from the emulator. This is a synchronous operation. + // + // Preconditions: + // - XR mode must be supported in the current AVD (`isXrGuestOs()` must be true). + // (Note: Current implementation of `isXrGuestOs()` always returns true). + // + // The `XrOptions` reply contains the current `environment` and `passthrough_coefficient`. + // + // Returns the following gRPC error codes: + // - `OK` (code 0): Upon successful retrieval of the XR options. + // - `FAILED_PRECONDITION` (code 9): If XR mode is not supported in the current AVD or if the `getXrOptions` agent function fails. + rpc getXrOptions(google.protobuf.Empty) returns (XrOptions) {} + + // Sets the environment background for AI glasses. + rpc setEnvironment(Environment) returns (google.protobuf.Empty) {} + + // Gets the current environment configuration for AI glasses. Values that are not set in environment.ini + // will be set to default values. + rpc getEnvironment(google.protobuf.Empty) returns (Environment) {} + + // Returns the list of video cameras connected to the host machine. + rpc getHostCameras(google.protobuf.Empty) returns (CameraList) {} +} + +// Represents the execution state of the virtual machine, used for both querying the +// current status and requesting state transitions. +message VmRunState { + enum RunState { + // Purely observable state. The virtual machine is in an unmapped or transient state. + // Cannot be set directly. + UNKNOWN = 0; + + // Active execution state. Setting this state resumes the virtual machine. + RUNNING = 1; + + // Purely observable state. The virtual machine is paused while restoring its state + // (e.g., loading a snapshot). Cannot be set directly. + RESTORE_VM = 2; + + // Suspended execution state. Setting this state pauses the virtual machine and halts + // CPU cycles. + PAUSED = 3; + + // Purely observable state. The virtual machine is paused while saving its state + // (e.g., taking a snapshot). Cannot be set directly. + SAVE_VM = 4; + + // Graceful shutdown state. Setting this state initiates a clean shutdown of the + // virtual machine. + SHUTDOWN = 5; + + // Write-only command. Immediately terminates the emulator process without cleanup. + // Can cause data corruption and must be used with caution. + TERMINATE = 7; + + // Write-only command. Triggers a warm reset of the virtual machine. + RESET = 9; + + // Purely observable state. The virtual machine has stopped execution due to a guest + // panic, internal error, or I/O failure. Cannot be set directly. + INTERNAL_ERROR = 10; + + // Write-only command. Triggers a warm reset of the virtual machine (equivalent to `RESET`). + RESTART = 11; + + // Write-only command. Starts or resumes execution of the virtual machine (equivalent + // to `RUNNING`). + START = 12; + + // Write-only command. Stops execution of the virtual machine (equivalent to `PAUSED`). + STOP = 13; + } + + RunState state = 1; +} + +message ParameterValue { + repeated float data = 1 [packed = true]; +} + +message PhysicalModelValue { + enum State { + OK = 0; + NO_SERVICE = -3; // qemud service is not available/initiated. + DISABLED = -2; // Sensor is disabled. + UNKNOWN = -1; // Unknown sensor (should not happen) + } + + // Details on the sensors documentation can be found here: + // https://developer.android.com/reference/android/hardware/Sensor.html#TYPE_ + // The types must follow the order defined in + // "external/qemu/android/hw-sensors.h" + enum PhysicalType { + POSITION = 0; + + // All values are angles in degrees. + // values = [x,y,z] + ROTATION = 1; + + MAGNETIC_FIELD = 2; + + // Temperature in °C + TEMPERATURE = 3; + + // Proximity sensor distance measured in centimeters + PROXIMITY = 4; + + // Ambient light level in SI lux units + LIGHT = 5; + + // Atmospheric pressure in hPa (millibar) + PRESSURE = 6; + + // Relative ambient air humidity in percent + HUMIDITY = 7; + + VELOCITY = 8; + AMBIENT_MOTION = 9; + + // Describing a hinge angle sensor in degrees. + HINGE_ANGLE0 = 10; + HINGE_ANGLE1 = 11; + HINGE_ANGLE2 = 12; + + ROLLABLE0 = 13; + ROLLABLE1 = 14; + ROLLABLE2 = 15; + + // Describing the device posture; the value should be an enum defined + // in Posture::PostureValue. + POSTURE = 16; + + // Heart rate in bpm + HEART_RATE = 17; + + // Ambient RGBC light intensity. Values are in order (Red, Green, Blue, + // Clear). + RGBC_LIGHT = 18; + + // Wrist tilt gesture (1 = gaze, 0 = ungaze) + WRIST_TILT = 19; + } + PhysicalType target = 1; + + // [Output Only] + State status = 2; + + // Value interpretation depends on sensor. + ParameterValue value = 3; + + enum Interpolation { + SMOOTH = 0; + STEP = 1; + } + + // [Input Only] How to transition to the target value. + Interpolation interpolation = 4; +} + +// A single sensor value. +message SensorValue { + enum State { + OK = 0; + NO_SERVICE = -3; // qemud service is not available/initiated. + DISABLED = -2; // Sensor is disabled. + UNKNOWN = -1; // Unknown sensor (should not happen) + } + + // These are the various sensors that can be available in an emulated + // devices. + enum SensorType { + // Measures the acceleration force in m/s2 that is applied to a device + // on all three physical axes (x, y, and z), including the force of + // gravity. + ACCELERATION = 0; + // Measures a device's rate of rotation in rad/s around each of the + // three physical axes (x, y, and z). + GYROSCOPE = 1; + // Measures the ambient geomagnetic field for all three physical axes + // (x, y, z) in μT. + MAGNETIC_FIELD = 2; + // Measures degrees of rotation that a device makes around all three + // physical axes (x, y, z) + ORIENTATION = 3; + // Measures the temperature of the device in degrees Celsius (°C). + TEMPERATURE = 4; + // Measures the proximity of an object in cm relative to the view screen + // of a device. This sensor is typically used to determine whether a + // handset is being held up to a person's ear. + PROXIMITY = 5; + // Measures the ambient light level (illumination) in lx. + LIGHT = 6; + // Measures the ambient air pressure in hPa or mbar. + PRESSURE = 7; + // Measures the relative ambient humidity in percent (%). + HUMIDITY = 8; + MAGNETIC_FIELD_UNCALIBRATED = 9; + GYROSCOPE_UNCALIBRATED = 10; + + // HINGE_ANGLE0 (11), HINGE_ANGLE1 (12), HINGE_ANGLE2 (13) are + // skipped; clients should use get/setPhysicalModel() instead for these + // "sensors". + + // Measures the heart rate in bpm. + HEART_RATE = 14; + // Measures the ambient RGBC light intensity. + // Values are in order (Red, Green, Blue, Clear). + RGBC_LIGHT = 15; + // WIRST_TILT (16) is skipped; clients should use get/setPhysicalModel() + // instead. + // Measures acceleration force and provides bias data. + ACCELERATION_UNCALIBRATED = 17; + // A sensor of this type measures the direction in which the device is + // pointing relative to true north in degrees. + HEADING = 18; + } + + // Type of sensor + SensorType target = 1; + + // [Output Only] + State status = 2; + + // Value interpretation depends on sensor enum. + ParameterValue value = 3; +} + +// A single backlight brightness value. +message BrightnessValue { + enum LightType { + // Display backlight. This will affect all displays. + LCD = 0; + KEYBOARD = 1; + BUTTON = 2; + } + + // Type of light + LightType target = 1; + + // Light intensity, ranges from 0-255. + uint32 value = 2; +} + +// in line with android/emulation/resizable_display_config.h +enum DisplayModeValue { + PHONE = 0; + FOLDABLE = 1; + TABLET = 2; + DESKTOP = 3; +} + +message DisplayMode { + DisplayModeValue value = 1; +} + +message XrOptions { + enum Environment { + LIVING_ROOM_DAY = 0; + LIVING_ROOM_NIGHT = 1; + // More environments may be added later. + } + + // The currently active artificial surrounding environment (a.k.a. + // passthrough environment). + Environment environment = 1; + + // A value of 0.0 means that the real or artificial surrounding environment + // (a.k.a. passthrough environment) is not visible. A value of 1.0 means + // that the passthrough environment is fully visible. Any value outside of + // the range [0.0-1.0] is ignored and leaves the state of passthrough + // unchanged. For simplicity of the implementation, this number may be + // rounded to an integer before applying. + float passthrough_coefficient = 2; + + float dimming_value = 4; +} + +// The state of one LED indicator of AI glasses. +message LedIndicator { + enum Facing { + INSIDE = 0; + OUTSIDE = 1; + } + + enum State { + OFF = 0; + ON = 1; + } + + // The indicator's identifier. + optional uint32 id = 1; + // Determines whether the indicator is inside or outside of the glasses frames. + optional Facing facing = 2; + // Determines whether the indicator is on or off. + optional State state = 3; + // Color in RGB format. + optional uint32 color = 4; +} + +message LogMessage { + // [Output Only] The contents of the log output. + string contents = 1; + // The starting byte position of the output that was returned. This + // should match the start parameter sent with the request. If the serial + // console output exceeds the size of the buffer, older output will be + // overwritten by newer content and the start values will be mismatched. + int64 start = 2 [deprecated = true]; + //[Output Only] The position of the next byte of content from the serial + // console output. Use this value in the next request as the start + // parameter. + int64 next = 3 [deprecated = true]; + + // Set the sort of response you are interested it in. + // It the type is "Parsed" the entries field will contain the parsed + // results. otherwise the contents field will be set. + LogType sort = 4; + + // [Output Only] The parsed logcat entries so far. Only set if sort is + // set to Parsed + repeated LogcatEntry entries = 5; + + enum LogType { + Text = 0; + Parsed = 1; + } +} + +// A parsed logcat entry. +message LogcatEntry { + // The possible log levels. + enum LogLevel { + UNKNOWN = 0; + DEFAULT = 1; + VERBOSE = 2; + DEBUG = 3; + INFO = 4; + WARN = 5; + ERR = 6; + FATAL = 7; + SILENT = 8; + } + + // A Unix timestamps in milliseconds (The number of milliseconds that + // have elapsed since January 1, 1970 (midnight UTC/GMT), not counting + // leap seconds) + uint64 timestamp = 1; + + // Process id. + uint32 pid = 2; + + // Thread id. + uint32 tid = 3; + LogLevel level = 4; + string tag = 5; + string msg = 6; +} + +// Information about the hypervisor that is currently in use. +message VmConfiguration { + enum VmHypervisorType { + // An unknown hypervisor + UNKNOWN = 0; + + // No hypervisor is in use. This usually means that the guest is + // running on a different CPU than the host, or you are using a + // platform where no hypervisor is available. + NONE = 1; + + // The Kernel based Virtual Machine + // (https://www.linux-kvm.org/page/Main_Page) + KVM = 2; + + // Intel® Hardware Accelerated Execution Manager (Intel® HAXM) + HAXM = 3 [deprecated = true]; + + // Hypervisor Framework. + // https://developer.apple.com/documentation/hypervisor + HVF = 4; + + // Window Hypervisor Platform + // https://docs.microsoft.com/en-us/virtualization/api/ + WHPX = 5; + + AEHD = 6; + } + + VmHypervisorType hypervisorType = 1; + int32 numberOfCpuCores = 2; + int64 ramSizeBytes = 3; +} + +// Representation of a clipped data object on the clipboard. +message ClipData { + // UTF-8 Encoded text. + string text = 1; +} + +// The Touch interface represents a single contact point on a +// touch-sensitive device. The contact point is commonly a finger or stylus +// and the device may be a touchscreen or trackpad. +message Touch { + // The horizontal coordinate. This is the physical location on the + // screen For example 0 indicates the leftmost coordinate. + int32 x = 1; + + // The vertical coordinate. This is the physical location on the screen + // For example 0 indicates the top left coordinate. + int32 y = 2; + + // The identifier is an arbitrary non-negative integer that is used to + // identify and track each tool independently when multiple tools are + // active. For example, when multiple fingers are touching the device, + // each finger should be assigned a distinct tracking id that is used as + // long as the finger remains in contact. Tracking ids may be reused + // when their associated tools move out of range. + // + // The emulator currently supports up to 10 concurrent touch events. The + // identifier can be any uninque value and will be mapped to the next + // available internal identifier. + int32 identifier = 3; + + // Reports the physical pressure applied to the tip of the tool or the + // signal strength of the touch contact. + // + // The values reported must be non-zero when the tool is touching the + // device and zero otherwise to indicate that the touch event is + // completed. + // + // Make sure to deliver a pressure of 0 for the given identifier when + // the touch event is completed, otherwise the touch identifier will not + // be unregistered! + int32 pressure = 4; + + // Optionally reports the cross-sectional area of the touch contact, or + // the length of the longer dimension of the touch contact. + int32 touch_major = 5; + + // Optionally reports the length of the shorter dimension of the touch + // contact. This axis will be ignored if touch_major is reporting an + // area measurement greater than 0. + int32 touch_minor = 6; + + enum EventExpiration { + // The system will use the default time of 120s to track + // the touch event with the given identifier. If no update happens + // within this timeframe the identifier is considered expired + // and can be made available for re-use. This means that a touch event + // with pressure 0 for this identifier will be send to the emulator. + EVENT_EXPIRATION_UNSPECIFIED = 0; + + // Never expire the given slot. You must *ALWAYS* close the identifier + // by sending a touch event with 0 pressure. + NEVER_EXPIRE = 1; + } + + EventExpiration expiration = 7; + + // The orientation of the contact, if any. + int32 orientation = 8; +} + +// A Pen is similar to a touch, with the addition +// of button and rubber information. +message Pen { + Touch location = 1; + + // True if the button is pressed or not + bool button_pressed = 2; + + // True if it is a rubber pointer. + bool rubber_pointer = 3; +} + +// A TouchEvent contains a list of Touch objects that are in contact with +// the touch surface. +// +// Touch events are delivered in sequence as specified in the touchList. +// +// TouchEvents are delivered to the emulated devices using ["Protocol +// B"](https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt) +message TouchEvent { + // The list of Touch objects, note that these do not need to be unique + repeated Touch touches = 1; + + // The display device where the touch event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 2; +} + +// A TouchpadEvent contains a list of Touch objects that are in contact with +// the touchpad surface. +// +// Touchpad events are delivered in sequence as specified in the touchList. +// +// TouchpadEvents are delivered to the emulated devices using ["Protocol +// B"](https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt) +message TouchpadEvent { + // The list of Touch objects, note that these do not need to be unique + repeated Touch touches = 1; + + // The touchpad device where the touch event occurred. + // Omitting or using the value 0 indicates the main touchpad. + int32 touchpad = 2; +} + +message PenEvent { + // The list of Pen objects, note that these do not need to be unique + repeated Pen events = 1; + + // The display device where the pen event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 2; +} + +// The MouseEvent interface represents events that occur due to the user +// interacting with a pointing device (such as a mouse). +message MouseEvent { + // The horizontal coordinate. This is the physical location on the + // screen, where 0 indicates the leftmost coordinate. + int32 x = 1; + + // The vertical coordinate. This is the physical location on the screen, + // where 0 indicates the topmost coordinate. + int32 y = 2; + + // Indicates which buttons are pressed. + // 0: No button was pressed + // 1: Primary button (left) + // 2: Secondary button (right) + int32 buttons = 3; + + // The display device where the mouse event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 4; +} + +message WheelEvent { + // The value indicating how much the mouse wheel is rotated. Scaled so that + // 120 equals to 1 wheel click. (120 is chosen as a multiplier often used to + // represent wheel movements less than 1 wheel click. e.g. + // https://doc.qt.io/qt-5/qwheelevent.html#angleDelta) Positive delta value + // is assigned to dx when the top of wheel is moved to left. Similarly + // positive delta value is assigned to dy when the top of wheel is moved + // away from the user. + int32 dx = 1; + int32 dy = 2; + + // The display device where the mouse event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 3; +} + +// KeyboardEvent objects describe a user interaction with the keyboard; each +// event describes a single interaction between the user and a key (or +// combination of a key with modifier keys) on the keyboard. +// This follows the pattern as set by +// (javascript)[https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent] +// +// Note: that only keyCode, key, or text can be set and that the semantics +// will slightly vary. +message KeyboardEvent { + // Code types that the emulator can receive. Note that the emulator + // will do its best to translate the code to an evdev value that + // will be send to the emulator. This translation is based on + // the chromium translation tables. See + // (this)[https://android.googlesource.com/platform/external/qemu/+/refs/heads/emu-master-dev/android/android-grpc/android/emulation/control/keyboard/keycode_converter_data.inc] + // for details on the translation. + enum KeyCodeType { + Usb = 0; + Evdev = 1; + XKB = 2; + Win = 3; + Mac = 4; + } + + enum KeyEventType { + // Indicates that this keyevent should be send to the emulator + // as a key down event. Meaning that the key event will be + // translated to an EvDev event type and bit 11 (0x400) will be + // set before it is sent to the emulator. + keydown = 0; + + // Indicates that the keyevent should be send to the emulator + // as a key up event. Meaning that the key event will be + // translated to an EvDev event type and + // sent to the emulator. + keyup = 1; + + // Indicates that the keyevent will be send to the emulator + // as e key down event and immediately followed by a keyup event. + keypress = 2; + } + + // Type of keycode contained in the keyCode field. + KeyCodeType codeType = 1; + + // The type of keyboard event that should be sent to the emulator + KeyEventType eventType = 2; + + // This property represents a physical key on the keyboard (as opposed + // to the character generated by pressing the key). In other words, this + // property is a value which isn't altered by keyboard layout or the + // state of the modifier keys. This value will be interpreted by the + // emulator depending on the KeyCodeType. The incoming key code will be + // translated to an evdev code type and send to the emulator. + // The values in key and text will be ignored. + int32 keyCode = 3; + + // The value of the key pressed by the user, taking into consideration + // the state of modifier keys such as Shift as well as the keyboard + // locale and layout. This follows the w3c standard used in browsers. + // You can find an accurate description of valid values + // [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) + // + // Note that some keys can result in multiple evdev events that are + // delivered to the emulator. for example the Key "A" will result in a + // sequence: + // ["Shift", "a"] -> [0x2a, 0x1e] whereas "a" results in ["a"] -> [0x1e]. + // + // Not all documented keys are understood by android, and only printable + // ASCII [32-127) characters are properly translated. + // + // Keep in mind that there are a set of key values that result in android + // specific behavior + // [see](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#Phone_keys): + // + // - "AppSwitch": Behaves as the "Overview" button in android. + // - "GoBack": The Back button. + // - "GoHome": The Home button, which takes the user to the phone's main + // screen (usually an application launcher). + // - "Power": The Power button. + string key = 4; + + // Series of utf8 encoded characters to send to the emulator. An attempt + // will be made to translate every character will an EvDev event type and + // send to the emulator as a keypress event. The values in keyCode, + // eventType, codeType and key will be ignored. + // + // Note that most printable ASCII characters (range [32-127) can be send + // individually with the "key" param. Do not expect arbitrary UTF symbols to + // arrive in the emulator (most will be ignored). + // + // Note that it is possible to overrun the keyboard buffer by slamming this + // endpoint with large quantities of text (>1kb). The clipboard api is + // better suited for transferring large quantities of text. + string text = 5; +} + +message XrCommand { + enum Action { + // Recenter the viewport position and rotation. + RECENTER = 0; + } + + Action action = 1; +} + +// An input event that can be delivered to the emulator. +message InputEvent { + oneof type { + KeyboardEvent key_event = 1; + TouchEvent touch_event = 2; + MouseEvent mouse_event = 3; + AndroidEvent android_event = 4; + PenEvent pen_event = 5; + WheelEvent wheel_event = 6; + MouseEvent xr_hand_event = 7; + MouseEvent xr_eye_event = 8; + XrCommand xr_command = 9; + RotationRadian xr_head_rotation_event = 10; + Translation xr_head_movement_event = 11; + AngularVelocity xr_head_angular_velocity_event = 12; + Velocity xr_head_velocity_event = 13; + TouchpadEvent touchpad_event = 14; + } +}; + +// The android input event system is a framework for handling input from a +// variety of devices by generating events that describe changes in the +// state of the devices and forwarding them to user space applications. +// +// An AndroidEvents will be delivered directly to the kernel as is. +message AndroidEvent { + // The type of the event. The types of the event are specified + // by the android kernel. Some examples are: + // EV_SYN, EV_KEY, EV_SW, etc.. + // The exact definitions can be found in the input.h header file. + int32 type = 1; + + // The actual code to be send to the kernel. The actual meaning + // of the code depends on the type definition. + int32 code = 2; + + // The actual value of the event. + int32 value = 3; + + // The display id associated with this input event. + int32 display = 4; +}; + +message Fingerprint { + // True when the fingprint is touched. + bool isTouching = 1; + + // The identifier of the registered fingerprint. + int32 touchId = 2; +} + +message GpsState { + // Setting this to false will disable auto updating from the LocationUI, + // otherwise the location UI will override the location at a frequency of + // 1hz. + // + // - This is unused if the emulator is launched with -no-window, or when he + // location ui is disabled. + // - This will BREAK the location ui experience if it is set to false. For + // example routing will no longer function. + bool passiveUpdate = 1; + + // The latitude, in degrees. + double latitude = 2; + + // The longitude, in degrees. + double longitude = 3; + + // The speed if it is available, in meters/second over ground + double speed = 4; + + // gets the horizontal direction of travel of this device, and is not + // related to the device orientation. It is guaranteed to be in the + // range [0.0, 360.0] if the device has a bearing. 0=North, 90=East, + // 180=South, etc.. + double bearing = 5; + + // The altitude if available, in meters above the WGS 84 reference + // ellipsoid. + double altitude = 6; + + // The number of satellites used to derive the fix + int32 satellites = 7; +} + +message BatteryState { + enum BatteryStatus { + UNKNOWN = 0; + CHARGING = 1; + DISCHARGING = 2; + NOT_CHARGING = 3; + FULL = 4; + } + + enum BatteryCharger { + NONE = 0; + AC = 1; + USB = 2; + WIRELESS = 3; + } + + enum BatteryHealth { + GOOD = 0; + FAILED = 1; + DEAD = 2; + OVERVOLTAGE = 3; + OVERHEATED = 4; + } + + bool hasBattery = 1; + bool isPresent = 2; + BatteryCharger charger = 3; + int32 chargeLevel = 4; + BatteryHealth health = 5; + BatteryStatus status = 6; +} + +// An ImageTransport allows for specifying a side channel for +// delivering image frames versus using the standard bytes array that is +// returned with the gRPC request. +message ImageTransport { + enum TransportChannel { + // Return full frames over the gRPC transport + TRANSPORT_CHANNEL_UNSPECIFIED = 0; + + // Write images to the a file/shared memory handle. + MMAP = 1; + } + + // The desired transport channel used for delivering image frames. Only + // relevant when streaming screenshots. + TransportChannel channel = 1; + + // Handle used for writing image frames if transport is mmap. The client + // sets and owns this handle. It can be either a shm region, or a mmap. A + // mmap should be a url that starts with `file:///` Note: the mmap can + // result in tearing. + string handle = 2; +} + +// The aspect ratio (width/height) will be different from the one +// where the device is unfolded. +message FoldedDisplay { + uint32 width = 1; + uint32 height = 2; + // It is possible for the screen to be folded in different ways depending + // on which surface is shown to the user. So xOffset and yOffset indicate + // the top left corner of the folded screen within the original unfolded + // screen. + uint32 xOffset = 3; + uint32 yOffset = 4; +} + +message ImageFormat { + enum ImgFormat { + // Portable Network Graphics format + // (https://en.wikipedia.org/wiki/Portable_Network_Graphics) + PNG = 0; + + // Three-channel RGB color model supplemented with a fourth alpha + // channel. https://en.wikipedia.org/wiki/RGBA_color_model + // Each pixel consists of 4 bytes. + RGBA8888 = 1; + + // Three-channel RGB color model, each pixel consists of 3 bytes + RGB888 = 2; + } + + // The (desired) format of the resulting bytes. + ImgFormat format = 1; + + // [Output Only] The rotation of the image. The image will be rotated + // based upon the coarse grained orientation of the device. + Rotation rotation = 2; + + // The (desired) width of the image. When passed as input + // the image will be scaled to match the given + // width, while maintaining the aspect ratio of the device. + // The returned image will never exceed the given width, but can be less. + // Omitting this value (or passing in 0) will result in no scaling, + // and the width of the actual device will be used. + uint32 width = 3; + + // The (desired) height of the image. When passed as input + // the image will be scaled to match the given + // height, while maintaining the aspect ratio of the device. + // The returned image will never exceed the given height, but can be less. + // Omitting this value (or passing in 0) will result in no scaling, + // and the height of the actual device will be used. + uint32 height = 4; + + // The (desired) display id of the device. Setting this to 0 (or omitting) + // indicates the main display. + uint32 display = 5; + + // Set this if you wish to use a different transport channel to deliver + // image frames. + ImageTransport transport = 6; + + // [Output Only] Display configuration when screen is folded. The value is + // the original configuration before scaling. + FoldedDisplay foldedDisplay = 7; + + // [Output Only] Display mode when AVD is resizable. + DisplayModeValue displayMode = 8; +} + +message Image { + ImageFormat format = 1; + + uint32 width = 2 [deprecated = true]; // width is contained in format. + uint32 height = 3 [deprecated = true]; // height is contained in format. + + // The organization of the pixels in the image buffer is from left to + // right and bottom up. This will be empty if an alternative image transport + // is requested in the image format. In that case the side channel should + // be used to obtain the image data. + bytes image = 4; + + // [Output Only] Monotonically increasing sequence number in a stream of + // screenshots. The first screenshot will have a sequence of 0. A single + // screenshot will always have a sequence number of 0. The sequence is not + // necessarily contiguous, and can be used to detect how many frames were + // dropped. An example sequence could be: [0, 3, 5, 7, 9, 11]. + uint32 seq = 5; + + // [Output Only] Unix timestamp in microseconds when the emulator estimates + // the frame was generated. The timestamp is before the actual frame is + // copied and transformed. This can be used to calculate variance between + // frame production time, and frame depiction time. + uint64 timestampUs = 6; +} + +message Rotation { + enum SkinRotation { + PORTRAIT = 0; // 0 degrees + LANDSCAPE = 1; // 90 degrees + REVERSE_PORTRAIT = 2; // -180 degrees + REVERSE_LANDSCAPE = 3; // -90 degrees + } + + // The rotation of the device, derived from the sensor state + // of the emulator. The derivation reflects how android observes + // the rotation state. + SkinRotation rotation = 1; + + // Specifies the angle of rotation, in degrees [-180, 180] + double xAxis = 2; + double yAxis = 3; + double zAxis = 4; +} + +message PhoneCall { + enum Operation { + InitCall = 0; + AcceptCall = 1; + RejectCallExplicit = 2; + RejectCallBusy = 3; + DisconnectCall = 4; + PlaceCallOnHold = 5; + TakeCallOffHold = 6; + } + Operation operation = 1; + string number = 2; +} + +message PhoneResponse { + enum Response { + OK = 0; + BadOperation = 1; // Enum out of range + BadNumber = 2; // Mal-formed telephone number + InvalidAction = 3; // E.g., disconnect when no call is in progress + ActionFailed = 4; // Internal error + RadioOff = 5; // Radio power off + } + Response response = 1; +} + +message Entry { + string key = 1; + string value = 2; +} + +message EntryList { + repeated Entry entry = 1; +} + +message EmulatorStatus { + // The emulator version string. + string version = 1; + + // The time the emulator has been active in .ms + uint64 uptime = 2; + + // True if the device has completed booting. + // For P and later this information will accurate, + // for older images we rely on adb. + bool booted = 3; + + // The current vm configuration + VmConfiguration vmConfig = 4; + + // Use platformConfig instead + EntryList hardwareConfig = 5 [deprecated = true]; + + // Some guests will produce a heart beat, that can be used to + // detect if the guest is active. + // This is a monotonically increasing number that gets incremented + // around once a second. + uint64 heartbeat = 6; + + // The configuration of services in the guest, this map + // contains key value pairs that are specific to the image + // used by the guest. + map guestConfig = 7; + + // Configuration of the emulator hardware + map platformConfig = 8; +} + +message AudioFormat { + enum SampleFormat { + AUD_FMT_U8 = 0; // Unsigned 8 bit + AUD_FMT_S16 = 1; // Signed 16 bit (little endian) + } + + enum Channels { + Mono = 0; + Stereo = 1; + } + + enum DeliveryMode { + // The audio queue will block and wait until the emulator requests + // packets. The client does not have to throttle and can push packets at + // will. This can result in the client falling behind. + MODE_UNSPECIFIED = 0; + // Audio packets will be delivered in real time (when possible). The + // audio queue will be overwritten with incoming data if data is made + // available. This means the client needs to control timing properly, or + // packets will get overwritten. + MODE_REAL_TIME = 1; // + } + // Sampling rate to use, defaulting to 44100 if this is not set. + // Note, that android devices typically will not use a sampling + // rate higher than 48kHz. See + // https://developer.android.com/ndk/guides/audio. + uint64 samplingRate = 1; + Channels channels = 2; + SampleFormat format = 3; + + // [Input Only] + // The mode used when delivering audio packets. + DeliveryMode mode = 4; +} + +message AudioPacket { + AudioFormat format = 1; + + // Unix epoch in us when this frame was captured. + uint64 timestamp = 2; + + // Contains a sample in the given audio format. + bytes audio = 3; +} + +message MicrophoneState { + // Whether or not host microphone access is enabled + bool realAudioEnabled = 1; +} + +message SmsMessage { + // The source address where this message came from. + // + // The address should be a valid GSM-formatted address as specified by + // 3GPP 23.040 Sec 9.1.2.5. + // + // For example: +3106225412 or (650) 555-1221 + string srcAddress = 1; + + // A utf8 encoded text message that should be delivered. + string text = 2; +} + +// A DisplayConfiguration describes a primary or secondary +// display available to the emulator. The screen aspect ratio +// cannot be longer (or wider) than 21:9 (or 9:21). Screen sizes +// larger than 4k will be rejected. +// +// Common configurations (w x h) are: +// - 480p (480x720) 142 dpi +// - 720p (720x1280) 213 dpi +// - 1080p (1080x1920) 320 dpi +// - 4K (2160x3840) 320 dpi +// - 4K (2160x3840) 640 dpi (upscaled) +// +// The behavior of the virtual display depends on the flags that are provided to +// this method. By default, virtual displays are created to be private, +// non-presentation and unsecure. +message DisplayConfiguration { + // These are the set of known android flags and their respective values. + // you can combine the int values to (de)construct the flags field below. + enum DisplayFlags { + DISPLAYFLAGS_UNSPECIFIED = 0; + + // When this flag is set, the virtual display is public. + // A public virtual display behaves just like most any other display + // that is connected to the system such as an external or wireless + // display. Applications can open windows on the display and the system + // may mirror the contents of other displays onto it. see: + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC + VIRTUAL_DISPLAY_FLAG_PUBLIC = 1; + + // When this flag is set, the virtual display is registered as a + // presentation display in the presentation display category. + // Applications may automatically project their content to presentation + // displays to provide richer second screen experiences. + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION + VIRTUAL_DISPLAY_FLAG_PRESENTATION = 2; + + // When this flag is set, the virtual display is considered secure as + // defined by the Display#FLAG_SECURE display flag. The caller promises + // to take reasonable measures, such as over-the-air encryption, to + // prevent the contents of the display from being intercepted or + // recorded on a persistent medium. + // see: + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_SECURE + VIRTUAL_DISPLAY_FLAG_SECURE = 4; + + // This flag is used in conjunction with VIRTUAL_DISPLAY_FLAG_PUBLIC. + // Ordinarily public virtual displays will automatically mirror the + // content of the default display if they have no windows of their own. + // When this flag is specified, the virtual display will only ever show + // its own content and will be blanked instead if it has no windows. See + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = 8; + + // Allows content to be mirrored on private displays when no content is + // being shown. + // This flag is mutually exclusive with + // VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY. If both flags are specified + // then the own-content only behavior will be applied. + // see: + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR = 16; + } + + // The width of the display, restricted to: + // 320 * (dpi / 160) <= width + uint32 width = 1; + + // The heigh of the display, restricted to: + // * 320 * (dpi / 160) <= height + uint32 height = 2; + + // The pixel density (dpi). + // See https://developer.android.com/training/multiscreen/screendensities + // for details. This value should be in the range [120, ..., 640] + uint32 dpi = 3; + + // A combination of virtual display flags. These flags can be constructed + // by combining the DisplayFlags enum described above. + // + // The behavior of the virtual display depends on the flags. By default + // virtual displays are created to be private, non-presentation and + // unsecure. + uint32 flags = 4; + + // The id of the display. + // The primary (default) display has the display ID of 0. + // A secondary display has a display ID not 0. + // + // A display with the id in the range [1, userConfigurable] + // can be modified. See DisplayConfigurations below for details. + // + // The id can be used to get or stream a screenshot. + uint32 display = 5; +} +// Provides information about all the displays that can be attached +// to the emulator. The emulator will always have at least one display. +// +// The emulator usually has the following display configurations: +// 0: The default display. +// 1 - 3: User configurable displays. These can be added/removed. +// For example the standalone emulator allows you to modify these +// in the extended controls. +// 6 - 11: Fixed external displays. For example Android Auto uses fixed +// displays in this range. +message DisplayConfigurations { + repeated DisplayConfiguration displays = 1; + + // Display configurations with id [1, userConfigurable] are + // user configurable, that is they can be added, removed or + // updated. + uint32 userConfigurable = 2; + + // The maximum number of attached displays this emulator supports. + // This is the total number of displays that can be attached to + // the emulator. + // + // Note: A display with an id that is larger than userConfigurable cannot + // be modified. + uint32 maxDisplays = 3; +} + +message Notification { + // Detailed notification information. + oneof type { + CameraNotification cameraNotification = 2; + DisplayConfigurationsChangedNotification + displayConfigurationsChangedNotification = 3; + Posture posture = 4; + BootCompletedNotification booted = 5; + BrightnessValue brightness = 6; + + // This notification is sent when a TextView receives or loses focus. + // It is also sent immediately in response to the streamNotification + // call. + TextViewFocus textViewFocus = 7; + // This notification is sent when XrOptions change. + // It is also sent immediately in response to the streamNotification + // call. + XrOptions xrOptions = 8; + MicrophoneState microphoneState = 9; + // This notification is sent when AI glasses LED light status change. + LedIndicator ledIndicator = 10; + } +} + +message BootCompletedNotification { + // The time in milliseconds it took for the boot to complete. + // Note that this value can be 0 when you are loading from a snapshot. + int32 time = 1; +} + +// Fired when the virtual scene camera is activated or deactivated and also in +// response to the streamNotification call. +message CameraNotification { + // Indicates whether the camera app was activated or deactivated. + bool active = 1; + // The display the camera app is associated with. + int32 display = 2; +} + +message TextViewFocus { + // Indicates whether a text view currently has focus. + bool textViewHasFocus = 1; + + // If a text view has focus, the display where the text view is located. + // Otherwise, unset. + int32 display = 2; +} + +// Fired when an update to a display event has been fired through the extended +// ui. This does not fire events when the display is changed through the console +// or the gRPC endpoint. +message DisplayConfigurationsChangedNotification { + DisplayConfigurations displayConfigurations = 1; +} + +message RotationRadian { + // Components of the rotation vector in radians. + // Rotation angles are relative to the current orientation. + float x = 1; // Angle of rotation around the x axis in right-handed direction. + float y = 2; // Angle of rotation around the y axis in right-handed direction. + float z = 3; // Angle of rotation around the z axis in right-handed direction. +} + +message Translation { + // Components of the translation vector in meters. + float delta_x = 1; + float delta_y = 2; + float delta_z = 3; +} + +message AngularVelocity { + // Components of the target angular velocity vector in radians per second. + // Transition to these values is implementation dependent, and may be + // smoothed over time. + float omega_x = 1; + float omega_y = 2; + float omega_z = 3; +} + +message Velocity { + // Components of the target velocity vector in meters per second. + // Transition to these values is implementation dependent, and may be + // smoothed over time. + float x = 1; + float y = 2; + float z = 3; +} + +// Must follow the definition in "external/qemu/android/hw-sensors.h" +message Posture { + enum PostureValue { + POSTURE_UNKNOWN = 0; + POSTURE_CLOSED = 1; + POSTURE_HALF_OPENED = 2; + POSTURE_OPENED = 3; + POSTURE_FLIPPED = 4; + POSTURE_TENT = 5; + POSTURE_MAX = 6; + } + PostureValue value = 3; +} + +message PhoneNumber { + // + // The phone number should be a valid GSM-formatted number as specified by + // 3GPP 23.040 Sec 9.1.2.5. + // + // For example: +3106225412 or (650) 555-1221 + string number = 1; +} + +// Specifies environment background for AI glasses. +message Environment { + // Key/value pairs corresponing to the contents of the environment.ini file. + // An empty map means no environment. + map environment = 1; +} + +message Camera { + // Human-readable name for use with UI + string display_name = 1; + // Os Specific identifier + string id = 2; +} + +message CameraList { + repeated Camera cameras = 1; +} diff --git a/artifacts/android-egl-capability-probe/native-clean-boot-output.txt b/artifacts/android-egl-capability-probe/native-clean-boot-output.txt index aa2844c..e1c5bb7 100644 --- a/artifacts/android-egl-capability-probe/native-clean-boot-output.txt +++ b/artifacts/android-egl-capability-probe/native-clean-boot-output.txt @@ -1,4 +1,4 @@ -Mactician native gfxstream GLES capability probe +Retired-donor native gfxstream GLES capability probe UTC date: 2026-08-11 Emulator: 37.1.11.0 build 15917651 AVD: TftPBE, cold boot, GuestAngle disabled, host GPU diff --git a/artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch b/artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch index 2d6e3b5..1b24eef 100644 --- a/artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch +++ b/artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch @@ -2,7 +2,7 @@ Subject: [PROTOTYPE] expose promoted GLES 3.2 command aliases in guest EGL Applies to gfxstream emu-main-dev revision a9184fd (2026-08-04). -This patch implements the 11 static alias candidates found by the Mactician +This patch implements the 11 static alias candidates found by the retired-donor Unreal symbol audit. It reuses existing guest encoder extension opcodes and has passed a standalone Clang syntax check. A later runtime probe showed that the shipped Android EGL loader already resolves all 11 names, so this patch is not diff --git a/benchmark.md b/benchmark.md new file mode 100644 index 0000000..f2dfacb --- /dev/null +++ b/benchmark.md @@ -0,0 +1,1032 @@ +# TFTMAC Benchmark and Analysis Contract + +**Authority date:** 2026-08-31 America/Chicago +**Formula version:** `tftmac-benchmark-v2` +**Current installed runtime:** TFTMAC 2.3.0 build 8 on the M4 Mac mini; automatic graphics lifecycle and complete stack receipts live-verified, including a 42m27s automatic graphics run +**Purpose:** give a developer or AI agent one exact, reproducible process for turning TFTMAC session data into findings, comparisons, decisions, and explicit unknowns. + +This file is the current benchmark-analysis authority. `docs/benchmarks.md` is +the historical M1 Max/userdebug experiment ledger; it must not be mistaken for +current M4 native-runtime evidence. Machine/runtime facts live in `facts.md`, +project history in `project.md`, and active engineering hypotheses in `dev.md`. + +## 1. Answer and operating decision + +Before this file, the analysis process existed but was split across source, +`facts.md`, `project.md`, `dev.md`, telemetry documentation, SQL tables, and the +historical campaign record. An agent could find the pieces, but it did not have +one complete input/formula/output contract. This file closes that documentation +gap. + +TFTMAC recognizes five evidence modes: + +| Mode | Meaning | Authority | +| --- | --- | --- | +| `FULL_RUN` | A complete automatically observed TFT process/layer lifetime | Primary evidence for continuous FPS, complete workload, sustained pressure, correctness, and promotion to normal play; user markers are optional annotations | +| `GRAPHICS_RUN` | Automatically observed TFT process/layer lifetime from start through process/app close | The SQL lifecycle identity used by a full run; no battle classifier or user marker is required | +| `BOUNDED_AB` | A 300–480 second continuous-gameplay window under one named preset | Fast controlled screening of one candidate against a compatible Control; currently implemented by the UI/source named Combat Benchmark | +| `DIAGNOSTIC_ONLY` | Launch, login, lobby, unmarked gameplay, partial capture, or isolated incident | Useful for diagnosis; cannot prove full-run performance or promote a candidate | +| `INVALID` | Missing/corrupt boundaries, inadequate coverage, changed identity, correctness failure, or other declared invalidator | Retain as negative/operational evidence; do not use for a positive performance claim | + +Full runs are preferred because they include the complete performance envelope, +not a hand-selected scene. Every logged frame and resource/pipeline sample +inside the automatic process/layer lifecycle participates. A bounded A/B +remains useful because it produces a faster controlled answer. It does not +require any semantic phase label. A short winner is not promoted until it also +survives a complete automatic full run. A full run can immediately veto a +candidate for correctness or player experience. + +A lobby, a reported `SRC 60`, an `OUT 60`, a successful launch, or an emulator +process is never a gameplay benchmark. + +The graphics-only optimization equation is limited to direct graphics cadence, +tail latency, missed-vsync/severe behavior, source freshness, stack receipts, +and conservative owned-boundary joins. The final native presenter is hidden +correctness context only. CPU, RAM, thermal, +power, and audio may invalidate correctness or explain health context, but are +not optimization variables or a substitute graphics owner in this contract. + +## 2. Evidence and claim discipline + +Every finding must contain these fields: + +```text +claim narrow statement supported by the evidence +evidence session/benchmark IDs, tables, time range, and calculated values +confidence DIRECT | CORRELATED | INFERRED | USER | UNKNOWN +unknowns unmeasured or invalid boundaries +decision KEEP | REJECT | PROMISING | INCONCLUSIVE | NO_DECISION +``` + +The labels mean: + +- `DIRECT`: the claimed value exists in current raw/normalized evidence or is a + deterministic calculation from it. +- `CORRELATED`: two signals are time-aligned closely enough to establish + ordering or co-occurrence, but not sole cause. +- `INFERRED`: the evidence supports a hypothesis but does not observe the + claimed internal boundary directly. +- `USER`: the player directly reported experience or correctness. This is + decisive for playability and not sufficient for internal root cause. +- `UNKNOWN`: the current capture cannot support the claim. + +Mandatory rules: + +1. Exact SurfaceFlinger actual-present intervals for the semantic TFT Unreal + `SurfaceView` are guest gameplay-cadence authority. +2. `SRC` is distinct completed controller images. `OUT` is TFTMAC's final Metal + presentation cadence. Neither is Unreal FPS. +3. A fast final presenter can repeatedly present an old source frame. It is not + a current graphics root-cause candidate and is omitted from causal ranking. +4. Requested configuration, effective receipt, and observed outcome are three + separate facts. +5. A guest-frame stall does not identify Unreal, ANGLE, ASG/gfxstream, + MoltenVK, Metal, or TFTMAC as its cause. +6. Cross-host ordering requires valid clock evidence. A trace does not repair a + bad clock relationship. +7. Never use an average to erase 1% low, p95/p99, severe stalls, any sustained + under-60 period, or direct player rejection. +8. Never inspect or report credentials, typed text, tokens, cookies, PINs, + CAPTCHA/MFA values, login screenshots, or raw frame payloads. + +## 3. Canonical inputs and read order for an AI agent + +An agent analyzing a TFTMAC run reads, in order: + +1. `facts.md` for the immutable/current machine, product, security, and runtime + boundaries. +2. This `benchmark.md` for evidence modes, formulas, validity, SQL, and output + shape. +3. `dev.md` for the active Control, candidate, negative-result ledger, and + hypotheses. +4. The selected session's `TFTMAC_NATIVE_RUNTIME.sqlite` as raw session + authority. +5. `TFTMAC_LAB.sqlite` only for durable benchmark/comparison history. +6. Raw sidecars or traces only when the requested causal question requires them + and their hash/processor/privacy receipts are valid. + +Per-session authority: + +```text +~/Library/Application Support/TFTMAC/Captures// + TFTMAC_NATIVE_RUNTIME.sqlite +``` + +Persistent comparison authority: + +```text +~/Library/Application Support/TFTMAC/TFTMAC_LAB.sqlite +``` + +Before calculation, create an analysis manifest: + +```json +{ + "analysis_schema": "tftmac.benchmark-report.v1", + "formula_version": "tftmac-benchmark-v2", + "evidence_mode": "FULL_RUN|BOUNDED_AB|DIAGNOSTIC_ONLY|INVALID", + "session_id": "", + "session_database": "", + "profile_id": "", + "configuration_sha256": "", + "comparison_identity_sha256": "", + "tft_package_version": "", + "source_commit": "", + "start_event_id": 0, + "end_event_id": 0, + "start_monotonic_ns": 0, + "end_monotonic_ns": 0, + "analysis_created_utc": "" +} +``` + +If any field is unavailable, write `UNKNOWN` or `null`; do not guess it from a +different session. + +## 4. SQL data dictionary + +| Table | Meaning | Analysis use | +| --- | --- | --- | +| `sessions` | app-run identity, profile, lifecycle | top-level session authority | +| `runtime_receipts` | requested/effective runtime facts | configuration and version proof | +| `events` | explicit lifecycle/user/app boundaries | full-run, benchmark, stutter, process, and failure segmentation | +| `game_frame_intervals` | exact TFT actual-present deltas | authoritative FPS/tail calculations | +| `game_frame_windows` | one-second gameplay summaries and availability | incident/worst-window discovery and coverage | +| `stream_freshness_windows` | received, changed, identical, and lost controller frames | distinguish upstream freshness from final output | +| `host_presentation_windows` | Metal submit/complete/reuse/error/latency/GPU time | hidden final-presenter correctness/regression context only | +| `presentation_samples` | cumulative/instant source/output behavior | hidden transport/presenter correctness trend | +| `resource_samples` | QEMU CPU/RSS, TFT PID, foreground activity | host-emulator load and process continuity | +| `guest_memory_samples` | guest available memory and swap | Android pressure trajectory | +| `host_resource_samples` | host memory/compression/swap/pageouts/thermal/power | Mac pressure and comparability | +| `clock_sync_samples` | host midpoint, guest uptime, RTT, offset | cross-boundary eligibility | +| `surfaceflinger_samples` | render rate and cumulative miss counters | deltas between valid explicit boundaries only | +| `audio_samples` | backend, active output, rate, stereo, tracks, underruns | audio correctness evidence | +| `logcat_aggregates` | bounded sanitized ANR/fatal/LMK/renderer/audio counts | failure-class evidence, not sole cause | +| `pipeline_log_aggregates` | gfxstream/ASG/Vulkan/MoltenVK/shader/fence signals | named warning/failure evidence, not proof of absence | +| `graphics_pipeline_snapshots` | effective layer/API/renderer state | comparable-path gate | +| `graphics_runs` | automatic TFT process/layer lifetime, configuration SHA, target FPS, and start/end reason | base graphics scope and lifecycle continuity | +| `graphics_pipeline_incidents` | automatic exact-layer degradation, trace link, conservative first boundary, explicit unknowns | incident triage, never a battle classification | +| `diagnostic_artifacts` | trace path/hash/processor/normalization | bounded causal evidence | +| `combat_benchmarks` | finalized bounded-window identity/validity/metrics | controlled `BOUNDED_AB` result; table name is retained from the implementation | +| `combat_incidents` | bad-window trigger, trace, boundary/unknowns | incident analysis | +| `combat_comparisons` | Control/candidate deltas and code decision | controlled A/B output | +| `game_process_sessions` | TFT PID lifetime | restart and process-stability evidence | + +The current source schema also stores canonical stack-receipt JSON/SHA-256 on +each graphics snapshot and joins intervals to their containing frame window +where available. Build 8 automatic captures **runtime-verify** this schema and +receipt linkage. The SHA proves receipt identity, not that every row shares a +trusted cross-process work ID. + +## 5. Time domains and legal joins + +TFTMAC data contains different clocks. They must not be joined as though they +were the same number. + +### Host monotonic clock + +`events.monotonic_ns`, `game_frame_windows.started_monotonic_ns`, resource +samples, stream/presenter windows, and most SQL sampling boundaries use the host +monotonic clock. Use this clock for automatic lifecycle boundaries, optional +annotations, and ordinary same-host overlap joins. + +### Guest SurfaceFlinger clock + +`game_frame_intervals.actual_present_ns` is the guest SurfaceFlinger actual- +present timestamp. Subtract adjacent values only inside the same stable layer +epoch. Do not compare it directly to a host marker. + +`game_frame_intervals.observed_monotonic_ns` is the host time at which TFTMAC +observed the interval. It is the legal field for assigning intervals to a +host-marked range, with up to approximately one polling window of boundary +uncertainty. Precise event-to-frame attribution requires valid clock mapping or +a common frame ID. + +### UTC + +UTC strings are for human display and cross-file orientation. Never calculate +frame intervals from UTC wall time. + +### Clock quality + +- p95 RTT at or below 2 ms: precise cross-boundary attribution permitted. +- above 2 ms through 10 ms: coarse ordering only. +- above 10 ms, missing bracket, or outside the bracket: cross-host cause is + `UNKNOWN`. + +## 6. Exact frame formulas + +The formulas below mirror `GameFrameTelemetry.swift`, +`CombatBenchmarkStore.swift`, and `CombatBenchmarkAnalysis.swift`. + +For consecutive actual-present timestamps and a SurfaceFlinger refresh period: + +```text +interval_ns[i] = actual_present_ns[i] - actual_present_ns[i-1] +interval_ms[i] = interval_ns[i] / 1,000,000 + +vsyncs[i] = max(1, round(interval_ns[i] / refresh_period_ns)) +missed_vsync_equivalents[i] = max(0, vsyncs[i] - 1) + +janky[i] = interval_ns[i] > 1.5 * refresh_period_ns +severe[i] = interval_ns[i] >= 3.0 * refresh_period_ns +``` + +At the current 60 Hz target, these boundaries are approximately: + +```text +janky > 25.000 ms +severe >= 50.000 ms +``` + +For an ordered interval population `I` of size `n`: + +```text +weighted_fps = n / (sum(I_ms) / 1000) + +nearest_rank_percentile(I, q) = + sort(I)[min(n - 1, max(0, ceil(n * q) - 1))] + +slow_count = max(1, ceil(n * 0.01)) +one_percent_low_fps = 1000 / mean(slowest slow_count intervals) + +jank_rate = count(janky) / max(1, n) +severe_rate = count(severe) / max(1, n) +missed_vsync_rate = sum(missed_vsync_equivalents) / max(1, n) +``` + +The one-second window metric is deliberately different: + +```text +effective_fps = interval_count * 1,000,000,000 / + max(1, window_end_ns - window_start_ns) +``` + +Always name which FPS formula is being reported. Do not average per-window FPS +to create a whole-run FPS when raw intervals are available. + +### Continuous 60 FPS target + +The product target is not “good average FPS.” It is a useful-frame cadence of +at least 60 FPS throughout the complete automatic run: + +```text +target_fps = 60 +target_frame_budget_ns = 1,000,000,000 / target_fps +target_frame_budget_ms = 16.6666667 + +budget_miss[i] = interval_ns[i] > target_frame_budget_ns +budget_overrun_ms[i] = max(0, interval_ms[i] - target_frame_budget_ms) + +budget_miss_rate = count(budget_miss) / max(1, n) +total_budget_overrun_ms = sum(budget_overrun_ms) +fps_deficit = max(0, target_fps - measured_fps) +``` + +The analyzer must also calculate the longest consecutive budget-miss run and a +five-second rolling weighted FPS at one-second steps. A continuous-60 claim +requires the complete automatic run—not just its mean—to meet the target, with no +missed-vsync equivalents or severe stalls. Until then, report the exact deficit +and improvement; do not redefine success downward. + +Every interval remains in the calculation. Worst intervals are ranked to choose +where to debug first, never to exclude the rest of the run. + +### Availability and clock formulas + +For an analysis range `[start,end]`: + +```text +overlap(window, range) = + max(0, min(window_end, end) - max(window_start, start)) + +surface_coverage = + sum(overlap for AVAILABLE exact-layer windows) / + sum(overlap for all measured windows) + +clock_coverage = + max(0, min(end, latest_clock_midpoint) - + max(start, earliest_clock_midpoint)) / + (end - start) +``` + +The code's short-benchmark surface availability denominator is measured-window +time, not marker duration. Reports should also state unmeasured boundary time so +an apparently perfect measured-window ratio is not misleading. + +### Candidate deltas + +For FPS and interval metrics: + +```text +delta_percent = ((candidate - control) / control) * 100 +``` + +Positive FPS/1%-low delta is better. Positive p95/p99 interval delta is worse. +Jank, severe, and missed-vsync deltas stored by the current code are fraction +differences (percentage-point changes when multiplied by 100), not relative +percent reductions. + +For a relative rate reduction used by the Home Run rule: + +```text +relative_reduction = (control_rate - candidate_rate) / control_rate +``` + +## 7. Full-run collection and continuous analysis + +### Collection + +1. Launch one clean named profile. The automatic logger begins before the + emulator/TFT path and opens a `GRAPHICS_RUN` from the observed TFT + process/layer lifecycle. +2. Do not change a restart-bound setting during the run. +3. Play normally. The automatic graphics run is the dataset; do not wait for or label a + particular game phase. +4. Optional Match Entry/End and Visible Stutter annotations may add player + context. Absence of a marker never means + absence of stutter. +5. Analyze the complete automatic run after normal + app shutdown and AVD rollback. + +The current native menu writes optional `MATCH_ENTRY` and `MATCH_END` annotations. +They never determine full-run validity. No battle or semantic phase classifier +participates in collection, validity, or causal analysis. + +### Full-run validity + +A full automatic graphics run is valid product evidence when: + +- the expected TFT Unreal `SurfaceView` is stable and unambiguous; +- exact-layer measured coverage is at least 95%; +- no SurfaceFlinger history truncation affects the range; +- the effective package/profile/configuration are identified; +- no render/input/audio/login/crash correctness failure invalidates play. + +Bad clock quality does not erase direct same-boundary guest-frame performance. +It makes cross-host causal attribution invalid. A full run can therefore be +valid product evidence while its cause remains `UNKNOWN`. + +### Continuous timeline and under-target periods + +Analyze every raw frame interval and its available `graphics_run_id`, +frame-window, stack-receipt-SHA, source, presenter, clock, and structured-error +join across the range. There is no phase-selection or battle-classifier gate. +Resource, memory, thermal, power, and audio rows remain correctness/health +context; do not rank them as graphics weak links. + +For each graphics run/window, publish `TFT` (exact SurfaceFlinger +actual-present) and `PIPE` (controller freshness/delivery). The native Mac +presenter remains hidden correctness context. Where joins cannot prove ordering +or a trusted work handoff is absent, output `UNKNOWN` rather than an owner. + +Before interpreting performance, build a completeness matrix for every table in +the SQL data dictionary: row count inside the range, first/last timestamp, +maximum sampling gap, expected cadence where applicable, null/unavailable count, +and boundary coverage. A missing signal becomes an explicit `UNKNOWN`; an agent +may not silently omit it because another signal appears easier to explain. + +Produce one-second metrics for the complete timeline, five-second rolling +weighted FPS at one-second steps, and fixed 30-second summaries for readable +trend comparison. Group adjacent windows below the 60 FPS target into +`UNDER_TARGET` episodes. Mark any episode containing a severe frame as +`SEVERE_STALL`. These labels describe measured performance only. + +For each 30-second interval calculate weighted FPS, FPS deficit from 60, +budget-miss rate, total budget overrun, p95/p99/max, 1% low, jank rate, severe +rate, missed-vsync rate, source freshness, host-presenter behavior, CPU, memory, +thermal, audio, and structured failure counts. Rank intervals for debugging by: + +1. lowest weighted FPS; +2. highest severe rate; +3. highest jank rate; +4. highest missed-vsync rate; +5. largest maximum interval. + +Report the full-run distribution, every `UNDER_TARGET`/`SEVERE_STALL` episode, +the worst six 30-second intervals, and every interval overlapping +`VISIBLE_STUTTER`. Ranking only controls investigation order; it never removes +the remaining data from the result. + +### Full-run report order + +1. Manifest and automatic process/layer lifecycle boundaries; list any markers + only as optional annotations. +2. Configuration/package/layer identity and correctness. +3. Coverage, clock quality, and invalidators. +4. Whole-run exact frame distribution and continuous-60 target deficit. +5. Complete one-second/rolling timeline and all under-target episodes. +6. Worst 30-second intervals and visible-stutter neighborhoods. +7. Source freshness across the same full timeline; preserve final-presenter + data only as hidden correctness context. +8. CPU/memory/thermal/power/audio and structured failures across the same full timeline. +9. Valid cross-boundary correlations; otherwise explicit unknowns. +10. Claim ledger and next one-factor candidate. + +## 8. Bounded A/B protocol + +The current app/source calls this feature Combat Benchmark for compatibility. +Its analysis does not require semantic combat detection. It is simply a bounded +continuous-gameplay A/B used when a full run is not needed for the first screen. + +The existing code's short-benchmark validity gate is exactly: + +```text +duration >= 300 seconds +surface_availability >= 0.95 +clock_coverage >= 0.95 +p95_clock_rtt_ms <= 10 +history_truncated == false +exact_layer_stable == true +correctness_passed == true +``` + +It automatically ends at 480 seconds. It records one 20-second/32-MiB start +trace and permits at most two 15-second/32-MiB incident traces. An automatic +incident requires two adjacent bad one-second windows where 1% low is below +30 FPS, p99 is at least 50 ms, or a severe interval exists. Trace cooldown is +120 seconds and traces never overlap. + +Control matching in current code requires: + +- the newest earlier valid `control` among the latest 20 Controls; +- the same `comparison_identity_sha256`; +- the same TFT package version; +- semantic layer identity equal to + `SurfaceView[com.riotgames.league.teamfighttactics/com.epicgames.unreal.GameActivity]`; +- Control ended before candidate. + +It does not prove equivalent whole-run workload distribution, power/thermal +state, trace overhead, or full-run context. The report must show those as +compatibility fields rather than silently assume them. + +### Implemented decision engine + +| Decision | Exact implemented rule | +| --- | --- | +| `INCONCLUSIVE` | either run invalid; baseline correctness false; or valid values land between all resolving rules | +| `REJECT` | candidate correctness false; p95 or p99 interval is at least 10% worse; or weighted FPS gain is below 5% | +| `HOME_RUN` | after the 5% FPS guard: 1%-low gain at least 20%; jank and severe rates each fall at least 30% relative; and weighted FPS rises at least 10% **or** p95 interval falls at least 15% | +| `PROMISING` | weighted FPS rises at least 5%; 1%-low rises at least 10%; p95 and p99 intervals do not worsen | + +Current code calculates/persists `observer_overhead_invalid` when trace-active +versus trace-inactive FPS or p95 differs by more than 5% with at least ten +intervals in each population. It does not currently include that flag in the +decision engine. The report must therefore show both: + +```text +code_decision: +causal_interpretation: INVALID_OBSERVER_OVERHEAD | ELIGIBLE +``` + +A `HOME_RUN` or `PROMISING` bounded result requires one cold confirmation and +one complete automatic full run before promotion to normal play. + +These relative decisions select whether a change is worth retaining; they do +not redefine the product goal. Every report must separately emit: + +```text +continuous_60_status: TARGET_MET | TARGET_NOT_MET | INVALID +``` + +`TARGET_MET` requires the full-run continuous-60 contract in Section 6. A +candidate may be a measurable improvement while the overall graphics objective +remains unfinished. + +## 9. Reproducible SQL recipes + +Use bound parameters rather than copying example IDs into a new analysis. + +### Select complete automatic full runs + +```sql +SELECT graphics_run_id, session_id, game_pid, started_utc, ended_utc, + start_reason, end_reason, configuration_sha256, target_fps, + exact_layer_name +FROM graphics_runs +WHERE session_id=:session_id + AND ended_utc IS NOT NULL +ORDER BY started_monotonic_ns; +``` + +### Find optional Match Entry/End annotation pairs + +```sql +WITH marked AS ( + SELECT id, session_id, kind, observed_utc, monotonic_ns, + lead(id) OVER (PARTITION BY session_id ORDER BY monotonic_ns) AS next_id, + lead(kind) OVER (PARTITION BY session_id ORDER BY monotonic_ns) AS next_kind, + lead(observed_utc) OVER (PARTITION BY session_id ORDER BY monotonic_ns) AS next_utc, + lead(monotonic_ns) OVER (PARTITION BY session_id ORDER BY monotonic_ns) AS next_ns + FROM events + WHERE kind IN ('MATCH_ENTRY','MATCH_END') +) +SELECT * +FROM marked +WHERE kind='MATCH_ENTRY' AND next_kind='MATCH_END' +ORDER BY session_id, monotonic_ns; +``` + +### Calculate exact whole-range frame metrics + +Use `observed_monotonic_ns` for the selected automatic lifecycle range. Optional +host markers may annotate that range but must not determine validity; never +compare them directly to guest `actual_present_ns`. + +```sql +WITH raw AS ( + SELECT interval_ms, is_janky, is_severe, missed_vsync_equivalents + FROM game_frame_intervals + WHERE session_id=:session_id + AND observed_monotonic_ns BETWEEN :start_ns AND :end_ns +), ranked AS ( + SELECT *, + row_number() OVER (ORDER BY interval_ms) AS rn, + row_number() OVER (ORDER BY interval_ms DESC) AS slow_rn, + count(*) OVER () AS n + FROM raw +), summary AS ( + SELECT count(*) AS n, + sum(interval_ms) AS total_ms, + max(CASE WHEN rn=(n+1)/2 THEN interval_ms END) AS p50_ms, + max(CASE WHEN rn=(n*95+99)/100 THEN interval_ms END) AS p95_ms, + max(CASE WHEN rn=(n*99+99)/100 THEN interval_ms END) AS p99_ms, + max(interval_ms) AS max_ms, + sum(is_janky) AS jank_count, + sum(is_severe) AS severe_count, + sum(missed_vsync_equivalents) AS missed_vsync_count + FROM ranked +), slow AS ( + SELECT avg(interval_ms) AS slow_mean_ms + FROM ranked + WHERE slow_rn <= (n+99)/100 +) +SELECT n, + 1000.0*n/total_ms AS weighted_fps, + 1000.0/slow_mean_ms AS one_percent_low_fps, + p50_ms, p95_ms, p99_ms, max_ms, + 1.0*jank_count/n AS jank_rate, + 1.0*severe_count/n AS severe_rate, + 1.0*missed_vsync_count/n AS missed_vsync_rate +FROM summary, slow; +``` + +### Calculate the continuous 60 FPS deficit + +```sql +WITH raw AS ( + SELECT actual_present_ns, interval_ns, interval_ms, + CASE WHEN interval_ns > (1000000000.0/60.0) THEN 1 ELSE 0 END AS miss + FROM game_frame_intervals + WHERE session_id=:session_id + AND observed_monotonic_ns BETWEEN :start_ns AND :end_ns +), grouped AS ( + SELECT *, + row_number() OVER (ORDER BY actual_present_ns) - + row_number() OVER (PARTITION BY miss ORDER BY actual_present_ns) AS grp + FROM raw +), runs AS ( + SELECT miss, grp, count(*) AS length + FROM grouped + GROUP BY miss, grp +) +SELECT (SELECT count(*) FROM raw) AS intervals, + (SELECT sum(miss) FROM raw) AS budget_misses, + 1.0*(SELECT sum(miss) FROM raw)/(SELECT count(*) FROM raw) + AS budget_miss_rate, + (SELECT sum(max(0,interval_ms-(1000.0/60.0))) FROM raw) + AS total_budget_overrun_ms, + (SELECT max(length) FROM runs WHERE miss=1) + AS longest_consecutive_budget_miss_run; +``` + +```sql +SELECT count(*) AS complete_windows, + sum(effective_fps < 60.0) AS windows_below_60, + 1.0*sum(effective_fps < 60.0)/count(*) AS below_60_rate, + sum(effective_fps < 50.0) AS windows_below_50, + sum(effective_fps < 40.0) AS windows_below_40, + sum(severe_count > 0) AS windows_with_severe_stall, + min(effective_fps) AS minimum_window_fps +FROM game_frame_windows +WHERE session_id=:session_id + AND status='AVAILABLE' + AND started_monotonic_ns>=:start_ns + AND ended_monotonic_ns<=:end_ns; +``` + +### Validate exact-layer coverage + +```sql +SELECT + sum(max(0,min(ended_monotonic_ns,:end_ns)- + max(started_monotonic_ns,:start_ns))) AS measured_ns, + sum(CASE WHEN status='AVAILABLE' THEN + max(0,min(ended_monotonic_ns,:end_ns)- + max(started_monotonic_ns,:start_ns)) ELSE 0 END) AS available_ns, + count(DISTINCT CASE WHEN status='AVAILABLE' THEN layer_name END) AS exact_layers, + sum(history_truncated) AS truncated_windows +FROM game_frame_windows +WHERE session_id=:session_id + AND ended_monotonic_ns>=:start_ns + AND started_monotonic_ns<=:end_ns; +``` + +### Find worst one-second windows + +```sql +SELECT started_monotonic_ns, ended_monotonic_ns, status, unavailable_reason, + effective_fps, one_percent_low_fps, p50_interval_ms, + p95_interval_ms, p99_interval_ms, maximum_interval_ms, + jank_count, severe_count, missed_vsync_equivalents, + history_truncated, layer_name +FROM game_frame_windows +WHERE session_id=:session_id + AND started_monotonic_ns>=:start_ns + AND ended_monotonic_ns<=:end_ns +ORDER BY maximum_interval_ms DESC, p99_interval_ms DESC +LIMIT 20; +``` + +### Build deterministic 30-second continuous summaries + +```sql +WITH raw AS ( + SELECT CAST((observed_monotonic_ns-:start_ns)/30000000000 AS INTEGER) AS bin, + interval_ms, is_janky, is_severe, missed_vsync_equivalents + FROM game_frame_intervals + WHERE session_id=:session_id + AND observed_monotonic_ns BETWEEN :start_ns AND :end_ns +) +SELECT bin, + 1000.0*count(*)/sum(interval_ms) AS weighted_fps, + 1.0*sum(is_janky)/count(*) AS jank_rate, + 1.0*sum(is_severe)/count(*) AS severe_rate, + 1.0*sum(missed_vsync_equivalents)/count(*) AS missed_vsync_rate, + max(interval_ms) AS max_interval_ms +FROM raw +GROUP BY bin +ORDER BY weighted_fps ASC, severe_rate DESC, jank_rate DESC, + missed_vsync_rate DESC, max_interval_ms DESC; +``` + +### Check clock eligibility + +```sql +WITH c AS ( + SELECT host_midpoint_ns, round_trip_ns, + row_number() OVER (ORDER BY round_trip_ns) AS rn, + count(*) OVER () AS n + FROM clock_sync_samples + WHERE session_id=:session_id + AND host_midpoint_ns BETWEEN :start_ns AND :end_ns +) +SELECT count(*) AS samples, + max(CASE WHEN rn=(n*95+99)/100 THEN round_trip_ns END)/1e6 AS p95_rtt_ms, + 1.0*(max(host_midpoint_ns)-min(host_midpoint_ns))/ + (:end_ns-:start_ns) AS in_range_clock_coverage +FROM c; +``` + +### Correlate a guest window with source and presenter windows + +```sql +SELECT g.started_monotonic_ns, g.ended_monotonic_ns, + g.effective_fps, g.p99_interval_ms, g.maximum_interval_ms, + s.content_changes, s.identical_frames, s.longest_identical_run_ms, + h.unique_source_uploads, h.repeated_source_presents, + h.drawable_misses, h.command_errors, + h.p95_completion_latency_ms, h.p95_gpu_time_ms +FROM game_frame_windows g +LEFT JOIN stream_freshness_windows s + ON s.session_id=g.session_id + AND s.started_monotonic_ns<=g.ended_monotonic_ns + AND s.ended_monotonic_ns>=g.started_monotonic_ns +LEFT JOIN host_presentation_windows h + ON h.session_id=g.session_id + AND h.started_monotonic_ns<=g.ended_monotonic_ns + AND h.ended_monotonic_ns>=g.started_monotonic_ns +WHERE g.session_id=:session_id + AND g.started_monotonic_ns>=:start_ns + AND g.ended_monotonic_ns<=:end_ns; +``` + +SurfaceFlinger miss counters are cumulative. Use only max-minus-min deltas +between explicit boundaries, never their absolute value as a full-run metric. + +## 10. Required AI-readable output + +Every analysis must produce the following logical shape, whether rendered as +JSON, SQL rows, or Markdown: + +```json +{ + "manifest": {}, + "validity": { + "product_evidence": "VALID|PARTIAL|INVALID", + "comparison_evidence": "VALID|INCONCLUSIVE|NOT_APPLICABLE", + "surface_coverage": 0.0, + "clock_coverage": 0.0, + "p95_clock_rtt_ms": 0.0, + "invalid_reasons": [] + }, + "whole_run": { + "duration_seconds": 0.0, + "intervals": 0, + "weighted_fps": 0.0, + "one_percent_low_fps": 0.0, + "p50_ms": 0.0, + "p95_ms": 0.0, + "p99_ms": 0.0, + "max_ms": 0.0, + "jank_rate": 0.0, + "severe_rate": 0.0, + "missed_vsync_rate": 0.0, + "target_fps": 60.0, + "budget_miss_rate": 0.0, + "total_budget_overrun_ms": 0.0, + "longest_budget_miss_run": 0 + }, + "under_target_episodes": [ + { + "episode_id": "...", + "label": "UNDER_TARGET|SEVERE_STALL|VISIBLE_STUTTER", + "confidence": "DIRECT|USER", + "start_ns": 0, + "end_ns": 0, + "metrics": {}, + "incidents": [] + } + ], + "pipeline_boundaries": { + "guest_actual_present": {}, + "source_freshness": {}, + "hidden_presenter_correctness": {}, + "resources": {}, + "clock_eligibility": "PRECISE|COARSE|UNKNOWN" + }, + "comparison": { + "control_id": null, + "candidate_id": null, + "deltas": null, + "code_decision": "NO_DECISION", + "promotion_status": "NOT_ELIGIBLE", + "continuous_60_status": "TARGET_MET|TARGET_NOT_MET|INVALID" + }, + "findings": [ + { + "claim": "...", + "evidence": [], + "confidence": "DIRECT|CORRELATED|INFERRED|USER|UNKNOWN", + "unknowns": [], + "decision": "KEEP|REJECT|PROMISING|INCONCLUSIVE|NO_DECISION" + } + ], + "privacy": { + "credentials_stored": false, + "raw_frames_stored": false, + "raw_sidecars_local_only": true + } +} +``` + +Never omit `invalid_reasons`, `unknowns`, or the difference between the code +decision and the engineering/promotion decision. + +## 11. Current full-run findings + +### 11.1 Latest automatic Build 8 graphics finding + +Capture `2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200` +contains an automatic 42m27s exact TFT process/layer run (PID 2774), with +144,364 frame intervals, 99.629% exact-layer coverage, 189 degradation +incidents, 56.98 weighted FPS, 21.49 FPS 1% low, 21.510 ms p95, 33.434 ms p99, +and 53.72% missed 16.667-ms frame budgets. This is valid direct gameplay +performance evidence without a Match Entry/End marker. + +It does **not** identify an internal graphics root. Build 8 has no shared work +identity across guest Vulkan, gfxstream, host Vulkan, MoltenVK, and Metal; its +source-level incident owner remains `UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE`. +The native Mac presenter remained near 60 Hz and is retained only as hidden +correctness context. + +### 11.2 Build 7 Combat Latency A historical finding + +#### Identity and boundaries + +| Field | Direct finding | +| --- | --- | +| Session | `2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494` | +| Session state at analysis | `RUNNING`; the marked range is saved, but the complete capture is not yet sealed | +| Database | `Captures//TFTMAC_NATIVE_RUNTIME.sqlite` | +| Profile | `tftmac_5gb_native_v1_preset_combat_latency_a` | +| Configuration SHA-256 | `05039d1fd0987f46fc7da8de5f483d8c7ffaf8f39bd1eaecdd1aee11603bbb07` | +| `MATCH_ENTRY` | event 1442, `2026-08-31T03:19:25Z`, host monotonic `262537257186708` | +| `MATCH_END` | event 3065, `2026-08-31T03:51:00Z`, host monotonic `264432310804375` | +| Marked duration | 1,895.054 seconds / 31m35.054s | +| Evidence mode | `FULL_RUN` | + +This is a complete marked run, so every frame and supporting sample inside the +range participates. It has no formal bounded-A/B row, which affects comparison +only; it does not reduce the full-run evidence. The marker rows and bounded data +are queryable now. Calculate a final whole-database hash only after normal app +shutdown seals the session. + +### Data completeness inventory + +| Signal family | Rows in/overlapping marked range | +| --- | ---: | +| Events | 1,624 | +| Frame samples | 1,562 | +| Source frame-interval windows | 1,871 | +| Presentation samples | 1,878 | +| Exact game-frame intervals | 93,724 | +| Exact game-frame windows | 1,695 | +| Stream-freshness windows | 1,871 | +| Host-presentation windows | 1,876 | +| QEMU/TFT resource samples | 357 | +| Guest-memory samples | 357 | +| Host-resource samples | 357 | +| Clock-sync samples | 59 | +| SurfaceFlinger samples | 59 | +| Audio samples | 59 | +| Logcat aggregates | 357 | +| Pipeline-log aggregates | 355 | +| Graphics-pipeline snapshots | 59 | +| Input metadata samples | 15,394 | +| TFT process lifetimes overlapping range | 1 | +| Diagnostic artifacts | 0 | + +The session also has 39 startup/runtime receipts. It has zero bounded benchmark, +incident, or comparison rows because that optional feature was not started. That +does not remove any continuous full-run telemetry; it means trace-based cause +and matched A/B decision fields are unavailable. + +### Exact guest-frame result + +| Metric | Finding | +| --- | ---: | +| Actual-present intervals | 93,724 | +| Weighted FPS | **49.449** | +| 1% low | **16.300 FPS** | +| p50 | 16.965 ms | +| p95 | **33.822 ms** | +| p99 | **48.746 ms** | +| Maximum | **1,254.162 ms** | +| Janky intervals | 17,911 / **19.110%** | +| Severe intervals | 572 / **0.610%** | +| Missed-vsync equivalents | 20,004 / 0.2134 per interval | +| Intervals over the 60 FPS frame budget | 58,925 / **62.871%** | +| Total 60 FPS budget overrun | **357,921.976 ms** | +| Longest consecutive budget-miss run | **325 intervals** | +| Complete one-second windows below 60 FPS | 1,599 / **94.448%** | +| Windows below 50 / below 40 FPS | 678 / 314 | +| Windows containing a severe stall | 353 | + +Tail distribution: + +| Interval range | Count | Share | +| --- | ---: | ---: | +| at or below 16.667 ms | 34,812 | 37.143% | +| above 16.667 through 20 ms | 39,487 | 42.131% | +| above 20 through 33.334 ms | 12,188 | 13.004% | +| above 33.334 through 50 ms | 6,665 | 7.111% | +| above 50 through 100 ms | 543 | 0.579% | +| above 100 through 250 ms | 23 | 0.025% | +| above 250 ms | 6 | 0.006% | + +The exact TFT layer was stable, all 1,695 overlapping windows were available, +measured overlap coverage was 100%, and no window reported history truncation. +The interval sum is about 0.297 seconds longer than the marker duration because +intervals arrive in polling batches at the range edges; weighted FPS uses the +source-defined interval formula, not marker duration. + +### Worst continuous 30-second intervals + +| Match time | Weighted FPS | Jank | Severe | Missed vsync | Max interval | +| --- | ---: | ---: | ---: | ---: | ---: | +| 17:00–17:30 | 38.490 | 44.38% | 4.464% | 652 | 102.091 ms | +| 18:30–19:00 | 39.863 | 45.56% | 1.962% | 592 | 81.526 ms | +| 20:00–20:30 | 39.147 | 43.90% | 1.212% | 615 | 666.743 ms | +| 21:30–22:00 | **33.436** | **57.60%** | 2.582% | **798** | **1,254.162 ms** | +| 22:30–23:00 | 39.189 | 47.40% | 1.759% | 633 | 66.334 ms | +| 25:30–26:00 | 37.781 | 53.99% | 0.964% | 671 | 91.910 ms | + +The worst interval was 21:30–22:00. It is direct evidence of a sustained bad +performance period. Its first late internal graphics boundary remains unknown. + +### Source, final presenter, and resources + +| Boundary | Direct finding | +| --- | --- | +| Source image rate | mean 49.472 FPS; no sequence-drop increase during marked interval | +| Final Metal output | mean 59.968 FPS | +| Host presentation | 113,618 submitted and completed; 90,387 unique uploads; 23,231 repeated-source presents | +| Presenter correctness | zero drawable misses; zero command errors | +| Final Metal cost | maximum completion latency 7.494 ms; maximum GPU time 3.267 ms | +| Emulator CPU | mean 513.65%; range 385.9–589.0% | +| Emulator RSS | mean 4,429.4 MiB; maximum 5,843.3 MiB | +| Guest memory | minimum 777.7 MiB available / 15.81%; maximum swap used 669.5 MiB | +| Host state | AC power; thermal state always `NOMINAL` | +| Audio | CoreAudio, active 48 kHz stereo, one active track, zero underruns | +| Structured faults | 14 confirmed guest memory-kill signatures in one aggregate near +27:17.774; no match-range ANR, input timeout, TFT fatal, ANGLE/Vulkan warning, or audio-error count | +| Pipeline aggregates | zero named gfxstream warning, ASG stall, Vulkan error, MoltenVK warning, shader error, or fence-timeout counts | + +The presenter completed near 60 Hz while reusing 23,231 source frames and while +guest actual presentation was irregular. This directly makes TFTMAC's final +Metal pass a poor explanation for the missing useful frames in this match. It +does not distinguish Unreal, ANGLE, ASG/gfxstream, or MoltenVK upstream. + +The 14 memory-kill signatures identify actual guest victims by classifier +syntax, but the normalized aggregate does not store victim identity. Their +relationship to TFT frame loss is therefore `UNKNOWN`; do not say TFT itself was +killed. + +### Validity and decision + +| Gate | Result | +| --- | --- | +| Full-run product evidence | **VALID** for direct player-facing frame distribution | +| Exact layer/coverage/history | pass | +| In-range clock coverage | 97.494% | +| Clock p95 RTT | **86.757 ms**, above 10 ms | +| Precise/coarse cross-host cause | **INVALID / UNKNOWN** | +| Matched Control | absent | +| Formal short benchmark row | absent | +| Candidate performance decision | **NO_DECISION / INCONCLUSIVE** | +| Continuous 60 FPS status | **TARGET_NOT_MET** | + +Direct conclusion: the marked run did not hold 60 FPS. More than 62% of raw +intervals exceeded the 60 FPS frame budget and more than 94% of complete +one-second windows were below 60, despite a near-60 final output cadence. Combat +Latency A is neither promoted nor rejected by this single unmatched run. The run +is a valid candidate baseline and product-performance problem record; exact +internal ownership remains unknown because the clock gate failed and the +frame-ID boundary ring does not yet exist. + +## 12. Comparison and promotion policy + +For a one-factor candidate: + +1. Preserve one current valid Control full run. +2. Run the candidate under the same package, display, High/60/OFF game settings, + power state, and comparable play pattern. +3. Compare the complete whole-run distributions, continuous timelines, + under-target episodes, and resource/pipeline correlations. +4. Use the bounded code decision when a valid `BOUNDED_AB` pair exists. +5. Reject immediately for any boot/render/input/audio/login/cleanup regression or + direct unacceptable player experience. +6. Cold-confirm a short winner. +7. Require one complete automatic full run before normal-play promotion. + +A relative winner below the continuous 60 FPS target is retained as progress, +not described as the graphics problem being fixed. + +Full runs need not have identical length. Compare: + +- whole-run weighted/tail metrics with coverage shown; +- 60 FPS budget-miss rate, total overrun, and longest miss run; +- complete one-second and five-second rolling distributions; +- every under-target episode and the median/worst quartile of fixed intervals; +- worst one-second incidents; +- sustained resource/thermal/memory state; +- correctness and direct player report. + +Never declare a gain from one isolated best window, different package/settings, +different semantic layer, lobby-only data, or output cadence alone. Missing +markers alone never invalidates an automatic full run. + +## 13. Retention and privacy + +Retain: + +- latest accepted Control full run; +- latest candidate full run and any matching bounded A/B; +- current package/runtime/configuration receipts; +- every rejected candidate's compact metrics and reason; +- unresolved incident evidence; +- normalized trace summaries and SHA-256 links when valid. + +Older raw captures may be compacted only after preserving session ID, automatic +lifecycle range, optional marker annotations, configuration/package/runtime +identity, formula version, verdict, key +metrics, source database hash, and artifact hashes. + +Raw logcat, raw traces, and any sensitive sidecars remain local/private. Reports +must never contain credentials, tokens, typed content, login screenshots, or raw +frames. + +## 14. Known analysis gaps + +1. Current comparison matching does not enforce equivalent whole-run workload, + thermal/power state, or full-run context. +2. `observer_overhead_invalid` is stored but does not alter the code decision. +3. Cold-confirmation/promotion linkage is policy, not a normalized SQL field. +4. Clock RTT is too high in the current full run for cross-host cause. +5. No common work ID currently spans guest submit through owned emulator + transport/translation completion. The allocation-free source-built + correlation ring remains a planned, gated next + layer; stack-receipt/frame-window joins do not replace it. + +These gaps limit attribution and automation; they do not erase the direct +player-facing frame distribution already captured. diff --git a/branding/concepts/mactician-origami-ribbon-reference.png b/branding/concepts/mactician-origami-ribbon-reference.png deleted file mode 100644 index 87d3933..0000000 Binary files a/branding/concepts/mactician-origami-ribbon-reference.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon-1024.png b/branding/generated/EmulatorIcon-1024.png deleted file mode 100644 index ad5b2d5..0000000 Binary files a/branding/generated/EmulatorIcon-1024.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.icns b/branding/generated/EmulatorIcon.icns deleted file mode 100644 index 73e6c80..0000000 Binary files a/branding/generated/EmulatorIcon.icns and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_128x128.png b/branding/generated/EmulatorIcon.iconset/icon_128x128.png deleted file mode 100644 index 369f93e..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_128x128.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_128x128@2x.png b/branding/generated/EmulatorIcon.iconset/icon_128x128@2x.png deleted file mode 100644 index 749db90..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_128x128@2x.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_16x16.png b/branding/generated/EmulatorIcon.iconset/icon_16x16.png deleted file mode 100644 index 93a0e9f..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_16x16.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_16x16@2x.png b/branding/generated/EmulatorIcon.iconset/icon_16x16@2x.png deleted file mode 100644 index 3f76261..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_16x16@2x.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_256x256.png b/branding/generated/EmulatorIcon.iconset/icon_256x256.png deleted file mode 100644 index 749db90..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_256x256.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_256x256@2x.png b/branding/generated/EmulatorIcon.iconset/icon_256x256@2x.png deleted file mode 100644 index 7aa82a2..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_256x256@2x.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_32x32.png b/branding/generated/EmulatorIcon.iconset/icon_32x32.png deleted file mode 100644 index 3f76261..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_32x32.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_32x32@2x.png b/branding/generated/EmulatorIcon.iconset/icon_32x32@2x.png deleted file mode 100644 index 3f7b043..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_32x32@2x.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_512x512.png b/branding/generated/EmulatorIcon.iconset/icon_512x512.png deleted file mode 100644 index 7aa82a2..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_512x512.png and /dev/null differ diff --git a/branding/generated/EmulatorIcon.iconset/icon_512x512@2x.png b/branding/generated/EmulatorIcon.iconset/icon_512x512@2x.png deleted file mode 100644 index ad5b2d5..0000000 Binary files a/branding/generated/EmulatorIcon.iconset/icon_512x512@2x.png and /dev/null differ diff --git a/branding/generated/Mactician-1024.png b/branding/generated/Mactician-1024.png deleted file mode 100644 index 686bd30..0000000 Binary files a/branding/generated/Mactician-1024.png and /dev/null differ diff --git a/branding/generated/Mactician.icns b/branding/generated/Mactician.icns deleted file mode 100644 index 3b0a8d3..0000000 Binary files a/branding/generated/Mactician.icns and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_128x128.png b/branding/generated/Mactician.iconset/icon_128x128.png deleted file mode 100644 index 227bfda..0000000 Binary files a/branding/generated/Mactician.iconset/icon_128x128.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_128x128@2x.png b/branding/generated/Mactician.iconset/icon_128x128@2x.png deleted file mode 100644 index c95cf9e..0000000 Binary files a/branding/generated/Mactician.iconset/icon_128x128@2x.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_16x16.png b/branding/generated/Mactician.iconset/icon_16x16.png deleted file mode 100644 index f3abd84..0000000 Binary files a/branding/generated/Mactician.iconset/icon_16x16.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_16x16@2x.png b/branding/generated/Mactician.iconset/icon_16x16@2x.png deleted file mode 100644 index 964df7a..0000000 Binary files a/branding/generated/Mactician.iconset/icon_16x16@2x.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_256x256.png b/branding/generated/Mactician.iconset/icon_256x256.png deleted file mode 100644 index c95cf9e..0000000 Binary files a/branding/generated/Mactician.iconset/icon_256x256.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_256x256@2x.png b/branding/generated/Mactician.iconset/icon_256x256@2x.png deleted file mode 100644 index bc2462d..0000000 Binary files a/branding/generated/Mactician.iconset/icon_256x256@2x.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_32x32.png b/branding/generated/Mactician.iconset/icon_32x32.png deleted file mode 100644 index 964df7a..0000000 Binary files a/branding/generated/Mactician.iconset/icon_32x32.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_32x32@2x.png b/branding/generated/Mactician.iconset/icon_32x32@2x.png deleted file mode 100644 index 433e25f..0000000 Binary files a/branding/generated/Mactician.iconset/icon_32x32@2x.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_512x512.png b/branding/generated/Mactician.iconset/icon_512x512.png deleted file mode 100644 index bc2462d..0000000 Binary files a/branding/generated/Mactician.iconset/icon_512x512.png and /dev/null differ diff --git a/branding/generated/Mactician.iconset/icon_512x512@2x.png b/branding/generated/Mactician.iconset/icon_512x512@2x.png deleted file mode 100644 index 686bd30..0000000 Binary files a/branding/generated/Mactician.iconset/icon_512x512@2x.png and /dev/null differ diff --git a/branding/generated/mactician-favicon-64.png b/branding/generated/mactician-favicon-64.png deleted file mode 100644 index 380a3ee..0000000 Binary files a/branding/generated/mactician-favicon-64.png and /dev/null differ diff --git a/branding/generated/mactician-favicon.ico b/branding/generated/mactician-favicon.ico deleted file mode 100644 index a5c646f..0000000 Binary files a/branding/generated/mactician-favicon.ico and /dev/null differ diff --git a/branding/generated/mactician-open-graph.png b/branding/generated/mactician-open-graph.png deleted file mode 100644 index a82603f..0000000 Binary files a/branding/generated/mactician-open-graph.png and /dev/null differ diff --git a/branding/generated/mactician-product-hero.png b/branding/generated/mactician-product-hero.png deleted file mode 100644 index 6f1edab..0000000 Binary files a/branding/generated/mactician-product-hero.png and /dev/null differ diff --git a/branding/generated/mactician-small-size-test.png b/branding/generated/mactician-small-size-test.png deleted file mode 100644 index 75a01ec..0000000 Binary files a/branding/generated/mactician-small-size-test.png and /dev/null differ diff --git a/branding/generated/mactician-social-preview.png b/branding/generated/mactician-social-preview.png deleted file mode 100644 index f4c4f82..0000000 Binary files a/branding/generated/mactician-social-preview.png and /dev/null differ diff --git a/branding/mactician-app-icon.svg b/branding/mactician-app-icon.svg deleted file mode 100644 index b691731..0000000 --- a/branding/mactician-app-icon.svg +++ /dev/null @@ -1,76 +0,0 @@ - - Mactician app icon - A premium navy macOS icon with two folded ribbons forming an M and two small green Ready lights. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/branding/mactician-favicon.svg b/branding/mactician-favicon.svg deleted file mode 100644 index 8571190..0000000 --- a/branding/mactician-favicon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/branding/mactician-game-host-icon.svg b/branding/mactician-game-host-icon.svg deleted file mode 100644 index 360f241..0000000 --- a/branding/mactician-game-host-icon.svg +++ /dev/null @@ -1,31 +0,0 @@ - - Mactician game host icon - The Mactician app icon with a luminous cyan play badge for an active TFT game. - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/branding/mactician-mark-dark.svg b/branding/mactician-mark-dark.svg deleted file mode 100644 index b9ba330..0000000 --- a/branding/mactician-mark-dark.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/branding/mactician-mark-light.svg b/branding/mactician-mark-light.svg deleted file mode 100644 index fbe66a5..0000000 --- a/branding/mactician-mark-light.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/branding/mactician-mark-monochrome.svg b/branding/mactician-mark-monochrome.svg deleted file mode 100644 index 2c07289..0000000 --- a/branding/mactician-mark-monochrome.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/branding/mactician-mark.svg b/branding/mactician-mark.svg deleted file mode 100644 index ab966ae..0000000 --- a/branding/mactician-mark.svg +++ /dev/null @@ -1,47 +0,0 @@ - - Mactician mark - Two folded magenta and violet ribbons meet to create an M, with two small green Ready lights. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/branding/mactician-open-graph.svg b/branding/mactician-open-graph.svg deleted file mode 100644 index f0605c8..0000000 --- a/branding/mactician-open-graph.svg +++ /dev/null @@ -1,39 +0,0 @@ - - Mactician: Keep playing TFT PBE on your Mac - A free, open-source launcher built so Apple Silicon players can keep enjoying TFT PBE. - - - - - - - - - - - - - - - - - - MACTICIAN · TFT PBE LAUNCHER - Keep playing TFT PBE - on your Mac. - A native, open-source macOS experience for Apple Silicon. - Built for two tacticians. Shared with everyone. - - - - - - - - - - - - - - diff --git a/branding/mactician-product-hero.svg b/branding/mactician-product-hero.svg deleted file mode 100644 index 8fad663..0000000 --- a/branding/mactician-product-hero.svg +++ /dev/null @@ -1,55 +0,0 @@ - - Mactician application hero - An abstract tactical field with two folded ribbons forming an M and two small green Ready lights. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/branding/mactician-small-size-test.svg b/branding/mactician-small-size-test.svg deleted file mode 100644 index a6200d8..0000000 --- a/branding/mactician-small-size-test.svg +++ /dev/null @@ -1,30 +0,0 @@ - - Mactician mark small-size test sheet - - - - - - - - - - - - - - - - - - - Mactician mark · small-size test - 16 px · 24 px · 32 px · 64 px · 128 px - - - - - - Mactician - READY · TOGETHER - diff --git a/branding/mactician-social-preview.svg b/branding/mactician-social-preview.svg deleted file mode 100644 index 8a82fbf..0000000 --- a/branding/mactician-social-preview.svg +++ /dev/null @@ -1,38 +0,0 @@ - - Mactician social preview - Mactician, a native open-source TFT PBE launcher for Apple Silicon. - - - - - - - - - - - - - - - - - - MACTICIAN · APPLE SILICON - Keep playing - TFT PBE on your Mac. - Native. Open source. Built for two tacticians. - - - - - - - - - - - - - - diff --git a/branding/mactician-wordmark.svg b/branding/mactician-wordmark.svg deleted file mode 100644 index a1b2d09..0000000 --- a/branding/mactician-wordmark.svg +++ /dev/null @@ -1,29 +0,0 @@ - - Mactician - - - - - - - - - - - - - - - - - - - - - - - - - Mactician - - diff --git a/dev.md b/dev.md new file mode 100644 index 0000000..867c900 --- /dev/null +++ b/dev.md @@ -0,0 +1,693 @@ +# TFTMAC Developer Record + +**Development baseline:** TFTMAC 2.3.0 build 8 installed and live-verified with automatic PID/layer logging and complete stack receipts; historical release hashes match, while current-host signing trust is blocked by the absent local identity +**Control:** High / 60 FPS / Riot Performance Mode OFF +**Active candidate:** `combat_latency_a` +**Primary objective:** hold at least 60 useful FPS across the complete run while preserving the proven native app. + +This is the engineering working file. It contains code ownership, measurement +contracts, confirmed and rejected experiments, active hypotheses, and the next +implementation gates. Facts that must not drift live in `facts.md`; project +history and handoff live in `project.md`; exact full-run/A/B formulas and +current findings live in `benchmark.md`. + +## 1. Developer charter + +We are not trying to prove that frame loss exists; the complete automatic run has +already established it. Development must process all captured data, identify the +first boundary that becomes late, change an owned boundary, and demonstrate a +repeatable whole-run gain toward continuous 60 FPS without correctness, login, +audio, memory, launch, or cleanup regression. + +Rules: + +1. Preserve the working launcher/runtime/app before changing performance code. +2. Change one attributable factor per candidate unless an explicitly named + screening composite is being tested. +3. Record requested, effective, and observed state separately. +4. Use exact TFT SurfaceFlinger actual-present intervals as gameplay frame truth. +5. Treat SRC, OUT, guest actual-present, and panel visibility as separate clocks. +6. Averages never override 1% low, p95/p99, severe stalls, or the player's + visible-stutter report. +7. Do not claim Unreal, ANGLE, ASG, gfxstream, MoltenVK, Metal, or the final + presenter caused a stall until that boundary is the first valid divergence. +8. Do not modify Riot's signed package, shaders, credentials, or process. +9. Retain negative results so they are not recycled as “new” ideas. +10. A launch receipt proves setup, not performance. + +**Build 8 process-observer invariant:** invoke Android `pidof` as direct ADB +arguments (`adb ... shell pidof com.riotgames.league.teamfighttactics`). Do not +route this through an unquoted `sh -c` argument: live acceptance proved that +form can discard the package argument, leave `game_pid` null, and suppress the +periodic stack-receipt refresh even while layer-based frame logging continues. + +“Write a driver” in this project means implementing an owned, measured adapter +or scheduling/transport/cache change in TFTMAC, gfxstream/AEMU, ANGLE, or +MoltenVK when the evidence names that owner. It does not mean overclocking the +M4, modifying Riot's signed shaders, or adding an unmeasured kernel extension. + +## 2. Runtime and code ownership map + +| Boundary | Owner | Current implementation | +| --- | --- | --- | +| macOS application lifecycle | TFTMAC | `tftmac/App/TFTMACApplication.swift`, `AppCoordinator.swift` | +| native window/fullscreen | TFTMAC | `tftmac/App/MainWindowController.swift` | +| performance settings UI | TFTMAC | `tftmac/App/RuntimeSettingsWindowController.swift` | +| Metal output and Mac input capture | TFTMAC | `tftmac/Presentation/EmbeddedEmulatorView.swift` | +| frame contracts/geometry | TFTMAC | `FrameContract.swift`, `ViewportMapper.swift`, `TouchInput.swift` | +| runtime/ADB/controller orchestration | TFTMAC | `tftmac/Runtime/TFTMACRuntime.swift` | +| profiles/configuration hashes | TFTMAC | `tftmac/Runtime/RuntimeProfile.swift` | +| AVD mutation/rollback | TFTMAC | `AVDTransactionGuard.swift` | +| exclusive runtime ownership | TFTMAC | `RuntimeLease.swift` | +| exact game-frame windows | TFTMAC | `GameFrameTelemetry.swift` | +| signal classification | TFTMAC | `TelemetrySignalClassifier.swift` | +| combat persistence/decision | TFTMAC | `CombatBenchmarkStore.swift`, `CombatBenchmarkAnalysis.swift` | +| logged-in-session emulator launch | packaged host | `RuntimeHost/main.c` | +| controller wire contract | Android Emulator | `Vendor/AndroidEmulator/emulator_controller.proto`, generated Swift | +| Unreal game workload | Riot/TFT | official package; observe only | +| GLES-to-Vulkan translation | ANGLE | emulator/runtime component | +| guest-to-host command transport | gfxstream/ASG | emulator/runtime component | +| Vulkan-to-Metal translation | MoltenVK | emulator/runtime component | +| GPU execution | Apple Metal/M4 | host OS/hardware | +| official delivery/auth | Google Play/Riot | official guest UI only | + +## 3. Current control and candidate + +### Control + +```text +preset: control +display: 1920x1080 @ 320 dpi / 60 Hz +guest: 6 vCPU / 5120 MiB +TFT: High / 60 FPS / Performance Mode OFF +GPU/audio: host / CoreAudio +transport: virtio-gpu-asg +ASG: 1 MiB buffer / 16 KiB write step / 32 KiB ring / 800 us flush +ANGLE enabled: exposeNonConformantExtensionsAndVersions:exposeES32ForTesting +ANGLE disabled: preferSubmitAtFBOBoundary +MoltenVK: async submit / 64 active command buffers / fast math +``` + +### Combat Latency A + +```text +preset: combat_latency_a +all Control graphics, CPU, RAM, display, audio and transport values unchanged +LSSupportsGameMode: true +packaged host requests QOS_CLASS_USER_INTERACTIVE before execv +host records requested, set result, effective class and relative priority +``` + +Current direct receipt: + +```text +requested=user_interactive +set_result=0 +pre_exec_effective=user_interactive +relative_priority=0 +QEMU child-thread inheritance=NOT_CLAIMED_WITHOUT_COMBAT_EVIDENCE +``` + +The candidate configuration SHA-256 is +`05039d1fd0987f46fc7da8de5f483d8c7ffaf8f39bd1eaecdd1aee11603bbb07`. +It has passed launch/readiness and was the active observed preset in the latest +Build 8 automatic run. That observation is not a performance promotion or a +controlled comparison. + +## 4. Graphics pipeline and observability + +```text +Unreal Vulkan render/RHI (current TFT receipt) + -> guest Vulkan + -> gfxstream encoder + ASG guest transport + -> host gfxstream decode/queues + -> host Vulkan submit + -> MoltenVK translation/pipeline state + -> Metal command buffer/GPU completion + -> Android SurfaceFlinger actual-present + -> EmulatorController completed RGBA image + -> TFTMAC mailbox/Metal presenter + -> macOS drawable completion +``` + +ANGLE is installed/observable for conditional GLES/EGL paths, but it is not a +node in the current TFT game route unless a new per-run receipt proves that +selection. + +### What each signal can and cannot prove + +| Signal | Can prove | Cannot prove alone | +| --- | --- | --- | +| TFT SurfaceFlinger actual-present timestamps | player-facing guest frame cadence and stalls | which upstream component caused lateness | +| SRC distinct-image rate | completed images reaching TFTMAC | Unreal simulation/render timing | +| hidden native-presenter receipt | correctness/regression context only | game-frame or emulator GPU ownership | +| QEMU CPU/RSS | host emulator load/pressure direction | exact worker or serialized wait owner | +| guest TFT PID/memory | process lifetime and memory pressure | GPU/transport stall owner | +| SurfaceFlinger HWC/GPU misses | display-composition pressure | ASG/MoltenVK root cause | +| logcat aggregate | occurrence of known failure classes | absence of all hidden stalls | +| Perfetto scheduler/GPU/process trace | time-correlated execution and waits | valid cross-host cause when clock RTT/observer gate fails | +| requested emulator/MoltenVK flags | intended configuration | effective internal behavior | + +The final presenter is a locked non-bottleneck for current work. Preserve its +raw correctness receipt, but do not display, rank, or optimize it. It does not +distinguish Unreal, ASG, gfxstream, or MoltenVK. + +## 5. SQL and capture contract + +`benchmark.md` is authoritative for legal time-domain joins, raw-interval and +continuous-60 formulas, complete-timeline analysis, exact SQL, AI output shape, and +promotion rules. The queries below are an operational index, not an alternate +formula specification. + +Session authority: + +```text +~/Library/Application Support/TFTMAC/Captures// + TFTMAC_NATIVE_RUNTIME.sqlite +``` + +Persistent comparison authority: + +```text +~/Library/Application Support/TFTMAC/TFTMAC_LAB.sqlite +``` + +### Required cadences + +| Cadence | Measurements | +| --- | --- | +| 1 second | exact TFT FPS, 1% low, p50/p95/p99/max, jank, severe, missed-vsync equivalents, layer identity/history status | +| 1 second | source freshness/repetition/loss; native presenter submit/complete/reuse/errors/latency/GPU time | +| 5 seconds | QEMU CPU/RSS, TFT PID/activity, guest memory/swap, host memory/compression/swap/pageouts, thermal/power | +| 30 seconds | host/guest clock sync, renderer/feature receipts, SurfaceFlinger, display geometry/refresh, audio | +| event | automatic graphics-run/process/layer/stack-receipt/incident events; optional benchmark/stutter/trace and package/ANR/OOM/crash/correctness events | +| boundary | complete app/runtime/emulator/system-image/TFT/profile/input/audio identity | + +### Privacy contract + +Store no typed text, credential values, screenshots of login forms, raw frame +payloads, tokens, cookies, PINs, CAPTCHA, MFA, or unrelated app data. Raw logcat +and trace files stay local and require sanitization before any excerpt leaves the +Mac. + +### Useful SQL + +```sql +-- Exact session and frozen configuration +SELECT * FROM sessions; +SELECT receipt_key, receipt_value, confidence +FROM runtime_receipts ORDER BY id; + +-- Actual TFT frame truth +SELECT started_monotonic_ns, status, unavailable_reason, + effective_fps, one_percent_low_fps, + p50_interval_ms, p95_interval_ms, p99_interval_ms, + maximum_interval_ms, jank_count, severe_count, + missed_vsync_equivalents +FROM game_frame_windows +ORDER BY started_monotonic_ns; + +-- Separate upstream freshness from final presentation +SELECT * FROM stream_freshness_windows ORDER BY started_monotonic_ns; +SELECT * FROM host_presentation_windows ORDER BY started_monotonic_ns; + +-- User/benchmark boundaries +SELECT kind, observed_utc, monotonic_ns, payload_json +FROM events +WHERE kind IN ('MATCH_ENTRY','COMBAT_START','VISIBLE_STUTTER','MATCH_END', + 'COMBAT_BENCHMARK_STARTED','COMBAT_BENCHMARK_ENDED') +ORDER BY monotonic_ns; + +-- Fault counts without exposing raw log lines +SELECT * FROM logcat_aggregates +WHERE anr_count + fatal_count + memory_kill_count + input_timeout_count + + angle_warning_count + vulkan_warning_count + audio_error_count > 0; + +SELECT * FROM pipeline_log_aggregates +WHERE gfxstream_warning_count + asg_stall_count + vulkan_error_count + + moltenvk_warning_count + shader_error_count + fence_timeout_count > 0; + +-- Durable combat evidence +SELECT benchmark_id, session_id, preset_id, configuration_sha256, + duration_seconds, surface_availability, clock_coverage, + p95_clock_rtt_ms, correctness_passed, weighted_fps, + one_percent_low_fps, p95_interval_ms, p99_interval_ms, + max_interval_ms, jank_rate, severe_rate, + observer_overhead_invalid, valid, invalid_reason +FROM combat_benchmarks; + +SELECT * FROM combat_incidents ORDER BY observed_monotonic_ns; +SELECT * FROM combat_comparisons ORDER BY rowid; +``` + +## 6. Full-run and bounded A/B validity and decisions + +### Full match + +- Markers are optional user annotations. Automatic `graphics_runs` are the + primary full-session record and remain valid without any marker. +- Full runs process every frame and every resource/pipeline sample and are + required before normal-play promotion. +- No semantic phase markers are required. Fixed intervals and + rolling windows expose every sustained under-60 period directly. +- A bad clock gate leaves direct guest-frame results valid but makes cross-host + ownership `UNKNOWN`. +- A full candidate run without a compatible Control is a baseline, not an A/B + decision. + +### Valid run + +- at least 300 seconds of representative continuous gameplay; +- automatic end at 480 seconds; +- at least 95% exact TFT SurfaceView coverage; +- at least 95% clock coverage; +- stable semantic layer identity; +- untruncated frame history; +- same current TFT package and comparison configuration; +- correctness passed; +- clock RTT at most 10 ms for any cross-host ordering claim. + +Dynamic SurfaceFlinger tokens differ after every process launch. Control matching +must compare the stable semantic identity—package, activity, SurfaceView/BLAST +role—not the ephemeral token prefix/suffix. + +### Trace policy + +- one 20-second/32-MiB trace at benchmark start; +- up to two 15-second/32-MiB incident traces; +- automatic trigger requires two adjacent bad one-second windows; +- bad window: 1% low below 30 FPS, p99 at least 50 ms, or severe stalls; +- 120-second cooldown; no concurrent traces; +- SHA-256 seal and normalize with pinned `trace_processor_shell` v58.2; +- if trace overhead changes frame metrics by more than 5%, performance remains + useful but causal conclusions become `OBSERVER_OVERHEAD_INVALID`. + +### Decision engine + +| Decision | Rule | +| --- | --- | +| HOME_RUN | after the weighted-FPS +5% guard: 1% low +20%, jank and severe each -30% relative, and either weighted FPS +10% or p95 interval -15% | +| PROMISING | weighted FPS +5%, 1% low +10%, and p95/p99 intervals no worse | +| REJECT | weighted FPS gain below 5%, p95/p99 interval +10% worse, or candidate correctness/usability failure | +| INCONCLUSIVE | invalid/mismatched workload, coverage, clock, observer, or threshold gap | + +Any HOME_RUN/PROMISING result needs a five-minute cold confirmation. Rollback is +select Control and restart. A failed active candidate records correctness +rejection and saves Control automatically. + +Relative decisions select the better implementation; they do not lower the +goal. Report `TARGET_NOT_MET` until a complete automatic run holds at least 60 +useful FPS throughout with no missed-vsync equivalents or severe stalls. + +## 7. Retained results + +### Historical campaign winners + +| Candidate | Result | Decision | +| --- | --- | --- | +| ASG vs pipe | 40.1 FPS / 34.85 ms p95 vs 29.6 / 49.75 at same stage | keep ASG | +| 67% effects/LOD | 45.20 / 38.50 / 33.80 FPS at Trial 1-2/1-5/1-8 | historical winner | +| ASG write step 16 KiB | 41.3–43.0 / 34.1–35.1 at 1-5/1-8; paired 4 KiB 38.0 / 32.8 | keep 16 KiB | +| ANGLE `preferSubmitAtFBOBoundary` disabled | 46.90 / 36.10 / 29.60 first pass | retained in current stack; old run alone was provisional | + +These were collected on the historical M1 Max/userdebug campaign. Do not use the +numbers as M4 native Build 7 measurements. + +### Home Run A rejection + +| Metric | Value | +| --- | ---: | +| Duration | 480.646 s | +| Weighted FPS | 56.665 | +| 1% low | 17.698 FPS | +| p50 / p95 / p99 | 16.703 / 21.760 / 34.335 ms | +| Maximum | 517.488 ms | +| Jank / severe rate | 4.554% / 0.290% | +| Incident 1% lows | 1.932 and 4.629 FPS | +| Validity | invalid for comparison/cause: clock RTT too high; observer overhead invalid | +| Usability | user rejected as worst-ever experience | + +The configuration combined Riot Performance Mode Beta with +`NativeTextureDecompression` and `NoDelayCloseColorBuffer`; formal evidence +cannot allocate blame among those factors. Operationally, the complete preset is +barred and should not be decomposed unless new evidence gives a specific reason. + +### Build 7 Combat Latency A marked full run + +| Metric | Value | +| --- | ---: | +| Duration | 1,895.054 s / 31m35.054s | +| Exact actual-present intervals | 93,724 | +| Weighted FPS | 49.449 | +| 1% low | 16.300 FPS | +| p50 / p95 / p99 | 16.965 / 33.822 / 48.746 ms | +| Maximum | 1,254.162 ms | +| Jank / severe rate | 19.110% / 0.610% | +| Over-60-budget intervals | 58,925 / 62.871% | +| One-second windows below 60 FPS | 1,599 of 1,693 / 94.448% | +| Total budget overrun / longest miss run | 357,921.976 ms / 325 intervals | +| Exact-layer coverage/history | 100% measured overlap / no truncation | +| Final Metal output | 59.968 FPS mean; zero drawable/command errors; 3.267 ms max GPU time | +| Repeated-source presentations | 23,231 | +| Clock | 97.494% in-range bracket; 86.757 ms p95 RTT | +| Decision | full-run candidate baseline; no matched Control; cross-host cause `UNKNOWN` | + +This is direct proof that the current run does not hold 60 FPS and that final +OUT cadence masks repeated upstream frames. It does not prove whether Combat +Latency A improved or regressed against Control. Complete formulas, the entire +timeline, resources, and claim limits are retained in `benchmark.md`. + +## 8. Negative-result ledger + +Do not repeat these without a changed mechanism and explicit new evidence: + +| Candidate | Retained reason | +| --- | --- | +| pipe transport | materially slower than ASG | +| MoltenVK 128 | strong first run failed cold/sustained reproduction; worse tails | +| MoltenVK 256 | incomplete and included 133 ms frame | +| synchronous MoltenVK submit | about -10.3% in retained input test | +| guest submit thread | regression | +| shader prewarm / submit+prewarm | failed promotion | +| upstream ASG screen | failed promotion | +| 50% scale | no complete advantage over 67% profile | +| isolated/extreme effects or LOD | neutral/incomplete/regressive tails | +| ASG 2/4 ms flush | inconsistent or worse tail/reproduction | +| ASG 8/32 KiB steps | failed screen; 32 KiB long frame | +| ASG 64/128 KiB rings | no reproducible gain | +| ASG 512 KiB buffer | two startup failures, `Failed to unbox VkPipeline` | +| `VirtioGpuNativeSync` | regression | +| `VirtioGpuNext` | neutral | +| descriptor batching disabled | regression; keep batching | +| forced half-rate skeletal animation | worse tails | +| `r.OneFrameThreadLag=0` | -21.9% | +| disabled async composition | slower, no proven latency gain | +| explicit native swapchain | no-op/not promoted | +| MSAA2 | black 3D pass | +| material quality 1 | neutral/noisier | +| active-consumer host patch | 11.2 FPS / 334 ms p95 lobby regression | +| native GLES 3.0/3.1 | crash/capability failure | +| direct TFT Vulkan | did not solve verified problem | +| extra RAM / eighth vCPU | did not solve verified problem | +| audio disabled | neutral/slower; sound required | +| Riot Performance Mode Beta | direct user rejection and terrible incident tails | + +## 9. Research council and model evidence + +Specialist work was performed across Unreal, ANGLE, gfxstream/ASG, MoltenVK, +Metal, and transferable Fortnite/Unreal behavior. ZoeMC v0.2 ranked 10,000 +modeled architecture worlds. Its priors were subjective, so its output is a +hypothesis queue—not a measured performance result. + +One major branch was resolved empirically: authenticated raw gRPC can deliver a +correct 1920×1080 image and native input. That removes zero-copy/MMAP as a +prerequisite. The council's remaining useful output is the strict ownership map, +the requirement for fencing before MMAP, and the ordering of frame correlation +before deeper transport/translation patches. + +Fortnite/Unreal sources may inform shader/pipeline-cache behavior, trace +categories, device-profile reasoning, and render-thread/RHI hypotheses. They do +not establish which path the current signed TFT build chose or authorize shader +replacement. + +## 10. Active hypotheses and code gates + +### H1 — Combat Latency A / host scheduling + +**Mechanism:** the emulator launch thread may enter QEMU with a latency-oriented +QoS class, reducing scheduling delay in critical host work. + +**Implemented:** `RuntimeHost/main.c`, profile/receipt/rollback in native Swift, +Game Mode eligibility, unit tests. + +**Evidence now held:** historical candidate evidence plus the latest automatic +Build 8 run with exact actual-present, source, correctness-context presenter, +and stack receipts. + +**Status:** **DEFERRED / NOT CURRENT ACTION.** The preset is observed but not +promoted. Do not spend the next development cycle on another broad scheduling +comparison while the internal path remains uninstrumented. + +**Accept:** HOME_RUN/PROMISING plus cold confirmation. +**Reject:** no gain, worse tails, or any correctness/login/audio/cleanup issue. +**Critical unknown:** QEMU worker inheritance and worker-specific scheduling. + +### H2 — advanced causal work-ID instrumentation + +**Mechanism:** an allocation-free, source-instrumented work-ID ring carries +owned transport work through guest Vulkan encode, ASG/gfxstream receipt, +decoder, host Vulkan submit, MoltenVK enqueue, Metal completion, and Android +buffer release. It names the earliest owned divergent site or reports the exact +unowned/missing boundary as `UNKNOWN`. + +**Implementation contract:** + +```text +capacity sized from measured submit rate for five seconds of prehistory +no allocation on render/decoder hot paths +transport work ID + generation + overwrite/loss counter +guest submit timestamp and ASG write/flush state +host receipt/decode/queue timestamp +Vulkan submit, MoltenVK enqueue, and Metal completion timestamp +QSRI/color-buffer release timestamp +queue depth at each owned handoff +static source-site ID mapped to commit/blob/function/line in a sealed manifest +``` + +**Status:** **PLANNED, GATED NEXT LAYER.** The current automatic graphics logger +adds `graphics_runs`, stack-receipt SHA, per-window joins, and conservative +`TFT`/`PIPE` views; the presenter remains hidden correctness context. Those do +not create a shared work ID. + +**Gate:** the 42-minute Build 8 automatic run established an unresolved internal +causal gap below the SurfaceFlinger authority. Implement only in isolated +source-built `tftmac-runtime` at +commit `c8aa26e`, never by replacing normal-play Build 8. Clock mapping, +source/binary manifest, and overwrite counts are mandatory. Do not log shaders, +frame contents, or credentials. + +**Outcome:** identify the first valid owned divergent boundary. If lateness +begins before host receipt, report `UNREAL_OR_GUEST_UPSTREAM_UNKNOWN` unless a +diagnostic guest hook provides evidence. ANGLE is second-line only if a TFT +receipt proves it is active. + +### H3 — adaptive ASG transport + +**Mechanism:** fixed batching may either wake too often or hold a frame/release +command too long. Adapt around frame boundaries and queue occupancy rather than +blindly increasing buffers. + +**Candidate behavior:** + +- flush immediately for frame boundary/release-image work; +- batch while occupancy and latency are healthy; +- wake consumer early as occupancy rises; +- stop batching when guest waits for space; +- prevent one context from monopolizing decode; +- record occupancy, wait duration, wake reason, batch size, and loss. + +**Do not do:** enlarge every ring/buffer. Larger prior values did not improve and +512 KiB write buffer failed startup. + +**Gate:** frame-ID ring must first show lateness at ASG/host receipt. + +### H4 — gfxstream decoder/submission thread scheduling + +**Mechanism:** critical decoder/render/submission workers may be delayed or +serialized on the M4 host even when total CPU capacity exists. + +**Needed evidence:** per-thread name/ID, runnable delay, wakeup-to-run latency, +queue depth, lock/wait owner, and correlation to bad game-frame windows. + +**Candidate:** apply explicit latency QoS to the identified critical worker only, +not every QEMU thread. Keep I/O/background work lower. Verify effective class on +the actual thread. + +**Gate:** valid scheduler trace/frame ring shows the worker is the first late +boundary. Reject if CPU contention, thermals, audio, input, or tails regress. + +### H5 — persistent MoltenVK pipeline cache + +**Mechanism:** repeated SPIR-V-to-MSL/pipeline-state work may cause first-use or +combat-effect stalls. Reusing equivalent pipelines could remove them. + +**Cache key must include:** TFT package/build, shader/pipeline hash, MoltenVK +build, M4 GPU identity, effective graphics configuration, and cache schema. + +**Instrumentation:** pipeline lookup/create duration, hit/miss, translation time, +Metal pipeline creation time, compile thread, warm/cold state, and invalidation. + +**Safety:** no Riot shader modification; cache only owned translation products. +Fail closed on version/hash mismatch. Bound disk size and support complete +invalidations. + +**Gate:** frame-ID/trace evidence places first lateness after host receipt and +around pipeline creation. Blind prewarming remains rejected. + +### H6 — frame submission and pacing + +**Mechanism:** too many/few frames in flight, release signaling, swapchain image +count, queue bubbles, or commit cadence can produce poor tails without saturating +the GPU. + +**Variables:** present mode, swapchain images, frames in flight, fence polling vs +callbacks, deferred commands, release-image signaling, command-buffer commit +cadence, and host mailbox depth. + +**Evidence:** queue depth, submit-to-complete, complete-to-present, repeated +source, drawable errors, actual-present intervals. + +**Gate:** isolate one factor; never reintroduce synchronous submit or 128/256 +buffers without a new mechanism. + +### H7 — Vulkan/ANGLE capability and Unreal device selection + +**Mechanism:** the official game may select a conservative or incompatible +device profile based on exposed Vulkan/GLES/GPU/texture/surface capabilities. + +**Method:** compare effective reported capabilities and live code path; implement +only features that pass representative shader/render tests. Never lie that an +unsupported feature exists. + +**Evidence needed:** active TFT package/version, Vk/GLES identity/extensions, +selected surface formats/present modes, shader/renderer warnings, and visual +correctness. + +Historical PBE DeviceProfiles are reference evidence only. Do not mount an old +overlay into the current signed production client. + +### H8 — internal render resolution + +**Mechanism:** fewer rendered pixels can help a truly GPU-fill-bound scene while +TFTMAC still outputs fullscreen 1920×1080. + +**Current caution:** live TFT SurfaceView has often been 1280×720 inside the +1920×1080 guest, and a historical 2560×1440/1600×900 A/B barely changed FPS. +Lowering resolution may therefore reduce quality without fixing CPU/RHI/ +transport stalls. + +**Gate:** prove Metal/game GPU saturation or pixel-dependent scaling first. Test +100/83/75% one factor at a time, record exact SurfaceView buffer size, and reject +quality loss without tail gain. + +### H9 — MMAP/shared-memory frame delivery + +**Mechanism:** remove the final raw gRPC copy or reduce frame age/host CPU. + +**Preconditions:** producer readiness/fencing, stable stride/format/color, +bounded ownership, no overwrite while Metal reads, tear/corruption detector, +sequence/age measurement, and clean rollback to raw gRPC. + +**Gate:** isolated A/B shows material CPU/frame-age gain with zero integrity or +input regression. Raw gRPC remains production control otherwise. + +### H10 — startup phase latency + +**User observation:** native startup felt slow. No valid phase budget has yet +been measured. + +**Instrumentation:** record duration for lease/preflight, ADB server, host/QEMU +launch, controller discovery, Android boot, power gate, secure unlock wait, +package receipt, logger health, TFT process start, and first ready frame. + +**Code rule:** optimize only the measured slow owned phase. Never move logging +after launch, bypass ADB authorization, skip the power/package/controller gates, +or weaken AVD rollback merely to report a smaller startup number. + +## 11. Fastest next development sequence + +1. Preserve Build 8 capture + `2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200` + by ID, size, hash, and normalized metrics only; keep the raw database private. +2. Keep stock Build 8 as normal-play authority and keep the final Mac presenter + out of displayed/ranked causal views. +3. Implement H2's work-ID/source-site instrumentation only in isolated + `tftmac-runtime@c8aa26e`, with parity, clock, loss, stream-seal, and observer- + overhead gates. +4. Have the diagnostic run identify the first owned divergent boundary or name + the exact missing/unowned boundary as `UNKNOWN`. +5. Use that evidence to choose exactly one owned performance change from H3, + H4, H5, or H6. +6. Screen that one change with a bounded A/B when useful, then require a + complete automatic full run and direct player/correctness acceptance before + promotion. + +The current run has already shown that Build 8 boundary joins are insufficient +for internal attribution. The work-ID ring is the shortest path to writing the +correct deeper code instead of another blind emulator flag. + +## 12. Login reliability track + +Keep login reliability separate from graphics performance: + +- record exact Riot activity and WebView version; +- primary click remains Android touch; +- `show_ime_with_hard_keyboard=0` is the known recovered state; +- if `MobileFREWebViewActivity` hits an input-dispatch ANR, restart only Riot's + process and reopen the official Splash activity; +- do not restart a healthy emulator or destroy the current AVD/session; +- never inspect or persist credential values; +- count ANR/input timeouts in SQL and invalidate any affected correctness run. + +The repository contains older credential-automation helpers from historical +work. They conflict with the current strict manual-auth boundary and must not be +used as product behavior or benchmark prerequisites. + +## 13. Build, test, release, and rollback + +Authoritative commands: + +```text +scripts/test-native-app.command +scripts/build-native-app.command +scripts/verify-tftmac.command +scripts/verify-installed-runtime.command +scripts/summarize-native-session.command +``` + +Toolchain: + +```text +Xcode: /Applications/Xcode-26.6.0.app +Swift: 6.3.3 +target: arm64 macOS +app: /Applications/TFTMAC.app +runtime: /Volumes/MAC MINI M4/TFTMAC/Runtime +``` + +Verification claims must be scoped: + +- unit tests prove parsers, configuration, comparison logic, and local contracts; +- source verification proves the unsigned Release build and 43 native tests; +- installed-runtime verification proves current local hashes/runtime/signing and + is presently expected to fail on the missing local signing identity; +- launch receipts prove runtime readiness, not combat gain; +- SQL combat comparison proves measured change, not internal cause when clock or + observer gates fail; +- user acceptance proves playability/experience, not the first software boundary. + +Build 6 rollback was preserved at: + +```text +/Applications/TFTMAC.app.build6-backup-20260830-2150 +``` + +Do not replace or restart the currently running app during live user gameplay. +Apply restart-bound changes only after the user is out of a game and the current +session can seal and restore its AVD transaction. + +## 14. Current gaps to close + +- first-boundary frame correlation during the worst sustained under-60 period; +- source-site receipts and complete work-ID joins across owned diagnostic stages; +- recurrent Riot WebView/IME reliability; +- audible-sound user acceptance; +- repair of the local signing identity for current-host trust verification; +- public signing/notarization if distribution beyond this Mac becomes a goal; +- source-level gfxstream/MoltenVK performance changes only after the diagnostic + logger names the relevant owned boundary; +- measured startup-phase budget if the user's slow-launch observation persists. + +The current engineering posture is: preserve the proven app, reject generic +averages and recycled settings, and write the next code at the first boundary +that the synchronized full-run data actually shows is late. diff --git a/docs/TFTMAC_GRAPHICS_ARCHITECTURE.md b/docs/TFTMAC_GRAPHICS_ARCHITECTURE.md index 8673a5f..0415d68 100644 --- a/docs/TFTMAC_GRAPHICS_ARCHITECTURE.md +++ b/docs/TFTMAC_GRAPHICS_ARCHITECTURE.md @@ -1,177 +1,37 @@ # TFTMAC Graphics Architecture -## Win condition - -TFTMAC must provide the best practical Teamfight Tactics experience on Apple-silicon Macs while preserving a clean trust boundary: - -- Riot's game binary remains official and unmodified whenever possible. -- Google Play remains the live-client acquisition/update authority. -- Engine-specific graphics changes are isolated behind explicit runtime adapters. -- A working live path must never be broken by experimental Unreal tuning. - -## Runtime model - -TFTMAC uses a two-layer architecture: - -1. **Engine-neutral Mac shell** - - owns the native SwiftUI launcher; - - owns Android emulator lifecycle; - - owns display size, DPI, CPU/RAM, window behavior, input, audio and local diagnostics; - - detects the active Riot engine/runtime; - - exposes safe graphics presets. - -2. **Engine adapter** - - `LiveNativeAdapter`: current Google Play live client, stock Riot runtime; - - `UnrealEnhancedAdapter`: future/live Unreal client, harvesting the proven Mactician PBE renderer work; - - adapters are selected by evidence, never by version-string guesswork. - -## Engine detection - -Evidence is evaluated in order: - -1. resolved Android launch activity; -2. package/native-library inventory; -3. running-process mappings when accessible; -4. SurfaceFlinger layer/activity evidence; -5. app-managed patch/runtime files when observable; -6. Unreal markers such as `libUnreal.so`, `UECommandLine.txt`, `UnrealGame`, `DeviceProfiles.ini`, or Unreal-specific log output. - -Current live evidence after the completed in-game patch: - -- package: `com.riotgames.league.teamfighttactics` -- launch activity: `com.riotgames.leagueoflegends.RiotNativeActivity` -- Google Play package version: `16.16.8042660` -- base APK SHA-256: `9ed691e1b7e976394cc0dd91c0717df954429320a323e751e4848f6214c34919` -- native payload includes `libleagueoflegends.so` -- no `libUnreal.so` or `assets/UECommandLine.txt` in the Play Store base APK -- post-patch process/storage inspection found no Unreal runtime evidence -- the current live client therefore remains on Riot's native Android runtime for this build - -The Unreal adapter remains prepared from Mactician donor work and must activate only when future live runtime evidence proves the engine transition. - -## Graphics presets - -Graphics presets are engine-neutral and map to display geometry plus resource allocation. - -| Preset | Framebuffer | DPI | Intent | -| --- | --- | ---: | --- | -| **Enhanced 1080p** | **1920×1080** | **280** | **Primary target; 617dp tablet-class short side** | -| 1440p | 2560×1440 | 416 | Experimental only | -| 1800p | 3200×1800 | 520 | Experimental only | -| 4K | 3840×2160 | 640 | Experimental only | - -The launcher applies both emulator geometry and guest `wm size` / `wm density` so Android layout and the host window agree. TFTMAC deliberately targets 1080p rather than using supersampled resolution as a substitute for renderer quality. GPU budget should be spent first on anti-aliasing, texture filtering, stable frame pacing, shader behavior and renderer quality. - -## LiveNativeAdapter - -The current safe path uses: - -- official Google Play system image; -- Apple-silicon ARM64 Android emulator; -- `-gpu host`; -- Riot's untouched package; -- no APK overlay; -- no root requirement; -- normal Google Play and Riot authentication. - -Safe enhancement surface: - -- resolution and DPI; -- host GPU mode; -- guest CPU and memory; -- window sizing/fullscreen; -- input mapping; -- audio handling; -- emulator transport flags proven not to alter Riot code; -- frame pacing measurement. - -## UnrealEnhancedAdapter - -When live TFT exposes the Unreal runtime, harvest Mactician's proven PBE work rather than rediscovering it. - -Candidate stack: - -`Unreal -> OpenGL ES / ANGLE -> gfxstream -> Metal` - -or where supported: - -`Unreal -> Vulkan -> gfxstream / MoltenVK -> Metal` - -Donor capabilities already present in this repository include: - -- `run-tft-root-affinity.command`; -- `run-tft-angle-opengl.command`; -- `run-tft-best-verified.command`; -- temporary verified APK overlay support; -- `DeviceProfiles.ini` injection with rollback; -- `UECommandLine.txt` overlay preparation; -- MoltenVK queue and fast-math tuning; -- ANGLE feature controls; -- ASG transport tuning; -- frame-pacing and input-latency instrumentation; -- 1080p/1440p/1800p/4K profiles. - -The adapter must only activate when the running live build proves the expected Unreal artifact/layout. A hash/version/layout mismatch must fall back to `LiveNativeAdapter`, not attempt a stale overlay. - -## Future enhancement ladder - -### Tier 1 — safe/live now - -- Enhanced 1080p target at 1920×1080 / 280 DPI (617dp tablet class); -- 8 vCPU / 8 GB guest RAM on the validated Mac Mini profile; -- host GPU acceleration; -- OpenGL ES 3.2 capability (`196610`); -- ANGLE EGL with Vulkan 1.3 on Apple M4; -- native fullscreen/window fill; -- FPS overlay and frame-time diagnostics. - -### Tier 2 — Unreal enhanced - -- verified ANGLE/OpenGL path; -- guest DeviceProfiles tuning; -- anisotropic filtering and anti-aliasing profiles; -- shader/prewarm experiments; -- asynchronous MoltenVK queue submission where validated; -- bounded Metal command-buffer tuning; -- ASG transport tuning. - -### Tier 3 — measured quality optimizer - -TFTMAC should benchmark candidate profiles instead of assuming "higher" means "better". Score each profile on: - -- median FPS; -- p95/p99 frame time; -- janky frames; -- input latency; -- visual resolution; -- crash-free session duration; -- host CPU/GPU pressure. - -The winning profile is the highest visual-quality configuration that remains inside an explicit frame-pacing envelope. - -## Non-goals - -- modifying Riot gameplay logic; -- bypassing Riot authentication or anti-cheat; -- intercepting Riot credentials; -- embedding third-party or mirrored Riot APKs in Git; -- blindly applying PBE hashes/configuration to live builds; -- claiming an Unreal profile is valid without runtime evidence. - -## Acceptance gates - -### Current live gate - -- official Google Play installer authority confirmed; -- live TFT starts from TFTMAC; -- Riot login works; -- a full live match can be entered and completed; -- no Riot binary modification is required. - -### Unreal-enhanced gate - -- post-patch live client proves Unreal runtime/layout; -- donor overlay/profile assumptions are revalidated against the exact live build; -- graphics adapter is reversible and hash-gated; -- quality profile materially improves visual quality without unacceptable frame pacing; -- fallback to stock live remains one click away. +## Current normal-play path + +```text +Official TFT Unreal Vulkan workload + -> guest Vulkan + -> gfxstream / virtio-gpu ASG + -> host Vulkan + -> MoltenVK + -> Metal / Apple M4 GPU + -> Android SurfaceFlinger actual-present + -> authenticated raw RGBA transport + -> TFTMAC native presenter (hidden correctness receipt) +``` + +The stock Build 8 runtime is the normal-play authority. TFTMAC owns the native +window, launcher/session control, input, transport, and evidence collection; +it does not modify Riot's package, credentials, shaders, or process. + +ANGLE is conditional. A receipt may show ANGLE is present in the guest, but the +currently observed TFT game path is direct Unreal Vulkan. Do not attribute TFT +rendering to ANGLE unless a future per-run receipt proves it. + +## Diagnostic boundary + +The current logger proves exact SurfaceFlinger degradation and continuous +process/layer coverage, but it has no shared work ID across guest submission, +gfxstream, MoltenVK, and Metal. It therefore cannot name an internal root cause. +The next planned layer is an isolated source-built `tftmac-runtime` diagnostic +stack pinned at `c8aa26e`; it is never the normal-play runtime and its results +are non-comparable to stock until parity gates pass. + +For current facts and data contracts, use [../facts.md](../facts.md), +[../benchmark.md](../benchmark.md), and [../dev.md](../dev.md). The prior +adapter architecture is archived at +`history/2026-08-31-pre-build8/TFTMAC_GRAPHICS_ARCHITECTURE.md`. diff --git a/docs/TFTMAC_GRAPHICS_PIPELINE_RESEARCH.md b/docs/TFTMAC_GRAPHICS_PIPELINE_RESEARCH.md new file mode 100644 index 0000000..acff164 --- /dev/null +++ b/docs/TFTMAC_GRAPHICS_PIPELINE_RESEARCH.md @@ -0,0 +1,259 @@ +# TFTMAC graphics-pipeline research and implementation boundary + +Authority date: 2026-08-31 + +> **Historical/conditional research; not current root-cause authority.** The +> GLES/ANGLE and fixed-stage material below remains useful design evidence, but +> must not override the current Build 8 receipt or the automatic-run analysis +> contract in `facts.md`, `benchmark.md`, and `dev.md`. + +## Current Build 8 status + +- The observed TFT path is **direct Unreal Vulkan → guest Vulkan → gfxstream / + ASG → host Vulkan → MoltenVK → Metal → SurfaceFlinger**. +- ANGLE is conditional and must not be treated as TFT's active rendering path + without a new per-run receipt. +- Automatic process/layer logging is verified; Match Entry/End and combat + classification are optional annotations, not collection or validity gates. +- The final native Mac presenter is hidden correctness context only, never a + displayed weak link or root-cause candidate. +- Build 8 proves degradation at exact TFT SurfaceFlinger presentation, but the + internal root remains **UNKNOWN**: no shared work ID crosses guest Vulkan, + gfxstream, host Vulkan, MoltenVK, and Metal. +- Advanced source-level work-ID/site instrumentation is **PLANNED** only in the + isolated `tftmac-runtime` diagnostic environment; it is not a normal-play + Build 8 feature and cannot be claimed from current captures. + +This is a graphics-observability design note. It records conditional mechanisms +and research constraints. It neither changes the signed TFT APK nor treats a +requested emulator option as proof that a renderer was selected. + +## 1. Pipeline map: proven versus conditional + +The app can prove the following owned path during a session: + +```text +TFT GameActivity process + -> exact GameActivity BLAST SurfaceView + -> SurfaceFlinger actual-present timestamps + -> authenticated EmulatorController image transport + -> TFTMAC Metal presenter completion/present +``` + +The `TFT` SurfaceFlinger clock is guest-compositor truth. The controller-image +and final-Metal clocks are useful, but cannot be renamed as TFT/Unreal FPS. +Metal completion means the GPU finished the app's command buffer; it is not a +receipt that the guest produced a new frame. Apple's command-buffer API defines +the completed-handler boundary directly. [Apple: `addCompletedHandler`](https://developer.apple.com/documentation/metal/mtlcommandbuffer/addcompletedhandler(_:)) + +The following are historical or conditional routes, not the Build 8 active-path +claim: + +```text +Historical Candidate A: TFT GLES/EGL -> guest ANGLE -> Vulkan -> gfxstream -> host Vulkan + -> MoltenVK -> Metal +Current receipt: TFT Vulkan -> guest Vulkan -> gfxstream -> host Vulkan + -> MoltenVK -> Metal +Candidate C: TFT GLES/EGL -> native guest GLES -> gfxstream -> host renderer +``` + +The supplied historical findings establish a successful GLES 3.2/ANGLE/gfxstream/ +MoltenVK-to-Metal session on an earlier recorded target. They do not establish +that the current Build 8 TFT run selects Candidate A. ANGLE is active only when +the game chose GLES/EGL *and* Android chose ANGLE for that package; MoltenVK is +active only when the host backend actually loaded it. +The authoritative implementation sources are [Android's ANGLE module](https://android.googlesource.com/platform/packages/modules/ANGLE/+/refs/heads/main/), +[Google gfxstream](https://github.com/google/gfxstream), and +[Khronos MoltenVK](https://github.com/KhronosGroup/MoltenVK). + +## 2. Active-path receipts required per launch + +Do not promote a candidate route from launch intent. Record these receipts at +process start, first exact layer, layer replacement/loss, and app close: + +| Boundary | Receipt | What it proves | What it does not prove | +| --- | --- | --- | --- | +| TFT | package version/code, signed package receipt, `pidof` result, resumed activity | The intended package/process/activity is present | Unreal branch, RHI, or shader work | +| SurfaceFlinger | exactly one normalized `GameActivity` BLAST layer plus `--latency` rows | TFT actual-present timing for that layer | Guest CPU/GPU cause of a miss | +| ANGLE | package-specific selection/effective guest setting and an ANGLE runtime identity line | ANGLE was selected for this run | That ANGLE caused or fixed pacing | +| gfxstream | host startup/runtime identity line and pipeline log aggregates | The observed host component initialized or emitted a diagnostic | Queue depth or per-frame transfer latency | +| MoltenVK | host runtime identity line | MoltenVK was observed in this host process | Pipeline-cache hit rate, queue wait, or cause | +| host renderer | selected Vulkan device/composition/swapchain receipt | Observed host renderer configuration | Metal saturation in the emulator renderer | +| TFTMAC presenter | command-buffer scheduled/completed timing, drawable/encoder/command errors | Hidden native correctness receipt | TFT actual presentation or emulator-root ownership | + +`dumpsys SurfaceFlinger --latency` is accepted only for the exact selected +layer. Zero, sentinel, malformed, missing, or ambiguous samples are stored as +unavailable rather than converted into fabricated frame intervals. This matches +the purpose of Android's compositor/frame diagnostics: FrameTimeline separates +expected and actual timelines, but its own documentation also warns that +SurfaceViews are not currently supported by that data source. Therefore the +existing exact-layer actual-present collector remains the direct frame authority +for TFT's SurfaceView. [Perfetto: FrameTimeline](https://perfetto.dev/docs/data-sources/frametimeline) + +## 3. Supported controls and commands by layer + +| Layer | Supported control or observation | Runtime use / limit | +| --- | --- | --- | +| Android Emulator launch | `emulator ... -gpu ` selects the emulator graphics-emulation mode. | This is a documented startup option, not proof of a particular guest RHI or host renderer. [Android Emulator command line](https://developer.android.com/studio/run/emulator-commandline) | +| Android Emulator acceleration | Graphics acceleration and mode selection are runtime/AVD dependent. | Verify the effective run; do not infer acceleration from an AVD setting alone. [Android hardware acceleration](https://developer.android.com/studio/run/emulator-acceleration) | +| Guest/package | `adb shell pidof `, `dumpsys activity`, `dumpsys package`, `getprop`, and package-scoped settings receipts. | Identity and configuration evidence only; never a claim about hidden Unreal internals. | +| Guest compositor | `adb shell dumpsys SurfaceFlinger --list` and exact-layer `--latency`. | One-second actual-present windows; fail closed on no/ambiguous layer. | +| Guest trace | `adb shell perfetto --txt -c - -o `. | Bounded incident capture only; configured sources are SurfaceFlinger frame/layers, FrameTimeline, GPU memory, process/system stats, and selected ftrace scheduling events. Availability remains device/build dependent. Perfetto data-source names must match the producing device. [Perfetto GPU sources](https://perfetto.dev/docs/data-sources/gpu) | +| Host pipeline | session-scoped emulator stdout/stderr and structured startup receipts. | Counts warnings/errors and captures identity; warning counts alone are not latency measurements. | +| Final presenter | Metal `MTLCommandBuffer` scheduled/completed handlers. | Preserve hidden correctness evidence separately from guest production; do not use it for current causal ranking. [Apple: command structure](https://developer.apple.com/documentation/metal/setting-up-a-command-structure) | + +Current runtime-only options such as `-feature`, guest properties, ANGLE feature +overrides, and `MVK_CONFIG_*` environment variables are implementation-specific +experiment inputs. They may be retained as configuration receipts, but this note +does not label them portable Android Emulator, ANGLE, or MoltenVK contracts +without a version-matched upstream receipt. + +## 4. Collection policy + +### Always on for the owned app session + +- private SQLite session, append-only event sidecar, emulator stdout/stderr; +- process/layer lifecycle events and `game_process_sessions` closure at app + seal; +- exact-layer SurfaceFlinger polls: intervals when available and explicit + unavailable windows otherwise; +- host pipeline-log aggregates, graphics pipeline snapshots at lifecycle edges + and a modest periodic cadence while TFT has a PID; +- controller ingress/freshness and hidden final Metal correctness windows, + separately labeled; +- capture health, layer identity changes, and bounded clock-alignment samples. + +This is lightweight evidence collection, not profiling. It must start before +TFT activity launch when sources are available, continue through the first +process/layer observation, and end only after the final lifecycle snapshot and +the owned emulator/process shutdown receipt. + +### Incident only + +- Perfetto traces; current configuration is intentionally a ring buffer with a + short fixed duration and bounded size; +- additional diagnostic snapshots around a proven degradation; +- any source-built component's verbose per-frame spans. + +An automatic graphics incident requires: a current TFT PID, one exact active +layer, two adjacent bad actual-present windows, no capture in progress, and an +independent per-process budget/cooldown. It must write a generic graphics event +and a `diagnostic_artifact`, not fabricate a `combat_incident` whose schema +requires a manual benchmark ID. The existing combat benchmark remains an +opt-in A/B validity protocol. + +## 5. Weak-link attribution rules + +| Claimed weak link | Minimum evidence gate | Prohibited shortcut | +| --- | --- | --- | +| TFT display pacing | Exact TFT actual-present intervals plus an incident trace when needed | Calling controller ingress or Mac presentation "game FPS" | +| Guest scheduling / Unreal CPU | Correlated process runnable/running pressure; named Unreal spans only if the signed build exposes them | Inferring GameThread, RenderThread, or RHIThread ownership | +| ANGLE | Active-path receipt plus one-factor controlled improvement without correctness regression | Blaming the requested setting or a log mention | +| gfxstream / ASG | Instrumented queue depth or shared frame correlation crossing guest-to-host boundary | Treating a warning count as queue latency | +| MoltenVK | Active-path receipt plus its own queue/pipeline-cache timing or a controlled build | Naming MoltenVK because a host Vulkan device exists | +| Emulator renderer / Metal | Host renderer completion crossing a display deadline with supported counters | Using TFTMAC presenter's GPU time as emulator GPU time | +| TFTMAC presenter | Hidden correctness regression only | Treating a guest hitch as a Mac copy problem or selecting it as the current root | + +Perfetto GPU data availability is producer and device specific; the documented +GPU source names can include hardware-specific suffixes and require exact +matching. A missing track is `UNKNOWN`, not evidence that the subsystem was +idle. [Perfetto: GPU data sources](https://perfetto.dev/docs/data-sources/gpu) + +## 6. Allocation-free frame/batch correlation ring (future owned-source work) + +The present runtime has no cross-process per-frame ID, so it can bracket a +hitch but cannot attribute a single frame across guest submit, gfxstream, +host rendering, and final present. Add this only in source-built components +TFTMAC is authorized to modify; it must never inject into Riot's signed process. + +### Design + +```text +producer stage -> fixed slot ring -> batch drain -> SQLite/event writer + ^ + monotonically increasing sequence +``` + +- Use a preallocated power-of-two ring per producer process/thread domain; + slots are fixed-width POD records, not strings, Swift arrays, closures, or + heap-backed dictionaries. +- Slot payload: `sequence`, shared `frame_id` when a trusted handoff has one, + `stage`, monotonic timestamp, queue-depth/byte-count fields, status/flags, + and a producer-local loss counter. Use `frame_id = 0`/unknown until a real + owned handoff creates it. +- A producer reserves with an atomic monotonically increasing write sequence, + writes its slot, then release-publishes the sequence. The single drain worker + acquire-reads only fully published slots. Each ring records overwrite/loss + rather than blocking a render or compositor thread. +- Drain on a timed/batch threshold into one SQL transaction. Formatting, + JSON, hashes, file I/O, locks that can contend, and trace requests are all + outside the producer path. +- Correlate only records carrying the same trusted `frame_id`; otherwise retain + time-bounded observations as separate evidence. Do not manufacture a join + from adjacent timestamps across clocks. +- Version the binary batch schema and retain the producer build SHA, clock + domain, sequence ranges, capacity, overwritten count, and flush loss count. + +This design protects graphics execution from observer allocations and gives an +explicit data-loss receipt. It is intentionally smaller than a universal tracing +framework: no dynamic registration, no per-frame string labels, no unbounded +queue, and no retry that stalls rendering. + +## 7. What TFTMAC must not change + +- Do not patch, re-sign, instrument, inject into, or alter Riot's signed TFT + APK/process. +- Do not claim TFT Unreal branch, RHI, thread spans, shader/PSO keys, or cache + hits without a direct session receipt. +- Do not turn Perfetto into an always-on observer; retain bounded incident + traces and report observer-overhead validity separately. +- Do not change CPU, RAM, audio, login, network, package authority, or gameplay + behavior under this graphics-only effort. +- Do not treat AVD/launch flags, a healthy controller, normal log lines, or a + single fast scene as proof of a graphics cause. +- Do not export raw traces/logcat, private AVD data, or account-bearing content + as research artifacts. + +## 8. Explicit unknowns, licensing, and privacy + +Unknowns include TFT's exact Unreal branch/RHI/features, Game/Render/RHI spans, +PSO/shader keys and compile timing, gfxstream guest-submit-to-host-receive +latency, MoltenVK pipeline/cache/queue timing, and emulator-renderer Metal +saturation. These are not filled by a comparable Unreal title or an emulator +configuration request. + +TFTMAC source is governed by this repository's `LICENSE`; third-party +components retain their own terms and notices. Android Emulator, AOSP/ANGLE, +gfxstream, Perfetto, MoltenVK, Metal, and Teamfight Tactics are independently +licensed or trademarked. Consult each component's authoritative repository or +vendor terms before distributing a modified build, trace processor, or source +instrumentation. + +Captures are local-first and must exclude credentials, tokens, cookies, account +identifiers, private Android userdata, unrelated app data, and unfiltered game +logs. Store raw capture files in the private per-session directory, persist only +necessary aggregates/metadata to SQLite, hash retained incident artifacts, and +sanitize any excerpt before sharing. + +## 9. Phased implementation path + +1. **Lifecycle closure (native app only).** Add graphics PID/layer start, + replacement, unavailable, and end events/snapshots; retain the current + continuous exact-layer windows and pipeline aggregates. Take the final + graphics snapshot before logcat/ADB teardown. +2. **Automatic graphics incidents (native app only).** Split graphics trace + admission from manual combat admission. Require exact active layer + current + PID + two bad windows, use an independent small budget/cooldown, and persist + generic graphics events plus diagnostic artifacts. +3. **Evidence review.** For real incidents, classify only the first failing + observed boundary. If all evidence stops at the guest/host handoff, preserve + the gap rather than guessing the owner. +4. **Source-built gfxstream/AEMU, only for a demonstrated gap.** Add the fixed + correlation ring and trusted handoff frame ID. Prove loss/overhead behavior + before using it for causality. +5. **Conditional ANGLE/MoltenVK work.** Instrument one component only after a + current run proves that component active and the correlation evidence points + to its boundary. +6. **Controlled change acceptance.** Make one graphics-only factor change, + retain correctness and actual-present evidence, cold-confirm any promotion, + and stop when the claimed boundary is proven. diff --git a/docs/TFTMAC_NATIVE_RUNTIME_KNOWLEDGE_BASE.md b/docs/TFTMAC_NATIVE_RUNTIME_KNOWLEDGE_BASE.md new file mode 100644 index 0000000..5cd84cd --- /dev/null +++ b/docs/TFTMAC_NATIVE_RUNTIME_KNOWLEDGE_BASE.md @@ -0,0 +1,453 @@ +# TFTMAC Native Runtime Knowledge Base + +**Authority date:** 2026-08-31 + +**Product target:** Native macOS TFT client experience backed by the official Android TFT package + +**Current profile:** `tftmac_5gb_native_v1` + +## 1. Current outcome ledger + +| Claim | State | Decisive evidence | +| --- | --- | --- | +| TFTMAC is a real native Mac app | VERIFIED | AppKit `NSWindow`, native macOS full-screen space, Metal presenter, normal menus/window behavior | +| Full display is 1920x1080 | VERIFIED | AX window receipt was origin `0,0`, size `1920x1080`, `AXFullScreen=true`; gRPC frames are exactly 1920x1080 RGBA | +| Correctly oriented live Android video | VERIFIED | Live screenshot and input mapping on native Metal output | +| Donor-compatible launch architecture | VERIFIED | Packaged `TFTMAC Emulator Host.app` launched with `/usr/bin/open -n -W --env ... --args ...` in the logged-in Mac session | +| Correct ADB identity | VERIFIED | ADB server `5038`, console `5582`, serial `emulator-5582`; observed transition `offline -> unauthorized -> device` | +| No manual ADB key injection | VERIFIED | `ADB_VENDOR_KEYS` absent in launch receipt; inherited service socket/address variables cleared | +| Authenticated hidden-emulator control | VERIFIED | Exact PID-bound `pid_*.ini`, loopback gRPC `8554`, bearer token used in memory only, Emulator `37.1.11.0` authenticated | +| Native frame transport | VERIFIED | Raw gRPC `RGBA8888`, 8,294,400 bytes per 1920x1080 frame; 16-MiB request/response limits on pinned gRPC transport | +| Native presentation near 60 Hz in lobby | VERIFIED | Live session observed source-window max `61.1` and Metal-output max `60.5`; these are transport/output metrics, not Unreal FPS | +| Official current TFT launches | VERIFIED | Package `com.riotgames.league.teamfighttactics`, version `18.1-5402721`, SplashActivity then `com.epicgames.unreal.GameActivity`, PID observed | +| Riot account can reach the TFT lobby | VERIFIED | Live rendered lobby on the existing signed-in official app state | +| Primary touch and keyboard transport | VERIFIED | Mac primary-pointer down/drag/up uses EmulatorController `TouchEvent` with stable identifier `0` and pressure `1 -> 0`; keyboard remains gRPC evdev input; SQL records coordinates, pressure, counts and special keys, never typed content | +| CoreAudio software path | VERIFIED | Emulator launched `-audio coreaudio`; active AudioFlinger output, stereo, 48 kHz on the live check, one active track, zero partial/empty underruns | +| User can hear sound | USER ACCEPTANCE REQUIRED | The software path is healthy; only the person at the Mac can confirm audible output | +| Automatic full-session graphics evidence | LIVE VERIFIED | Latest Build 8 automatic process/layer run lasted 42m27s with 144,364 exact intervals and 99.629% coverage; markers were not required | +| Historical Build 8 release acceptance | VERIFIED HISTORICAL | All 43 native tests passed and `/Applications/TFTMAC.app` was deep-code-sign-valid when the signed 2.3.0 build 8 release receipt was created. | +| Current-host installed-runtime audit | BLOCKED | Main and emulator-host hashes still match Build 8, but the login keychain exposes zero valid signing identities and current deep/strict verification reports `CSSMERR_TP_NOT_TRUSTED`. Repair is a separate operational task. | +| Build 7 live launch | VERIFIED | Capture `2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494` reached authorized ADB, 1920x1080 first frame, powered/stay-awake guest, healthy SQL logger, official TFT and `TFT_READY_FOR_USER` under Combat Latency A | +| Build 8 automatic graphics logger | LIVE VERIFIED | Capture `2026-08-31T21-39-18.396Z-fe34e3a1-fb91-44eb-804f-4ca8519dfc31` proves automatic PID/layer admission, `COMPLETE` stack receipts, and direct run/hash/window/receipt linkage for every observed frame fact. | +| Internal graphics root cause | UNKNOWN / PLANNED | Build 8 has no shared work ID across guest Vulkan, gfxstream, host Vulkan, MoltenVK and Metal. It cannot name an internal owner; isolated diagnostic instrumentation is planned. | +| Combat Latency A performance | OBSERVED / NOT PROMOTED | Latest Build 8 run captured the preset at High/60/Performance OFF, but no controlled gain is established; scheduling testing is deferred behind causal instrumentation | +| External-runtime permission retention | VERIFIED | Stable designated requirement installed; a clean second launch immediately reopened `/Volumes/MAC MINI M4/TFTMAC/Runtime` and started QEMU without another drive-access dialog | +| Secure-unlock display | VERIFIED | Secure unlock stays manual and logged, while non-error runtime instructions are suppressed from the Android display; live signed launch showed no TFTMAC center overlay | + +Primary live acceptance capture: + +```text +~/Library/Application Support/TFTMAC/Captures/ + 2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94/ +``` + +Final release receipts: + +```text +Verified Build 8 executable SHA-256: + d3bf7c249a3e5f11b81f778b063e1a8cfe2e7fdeec0537ee6bd8447b1c2268d2 + +Build 8 automatic-logger live acceptance: + 2026-08-31T21-39-18.396Z-fe34e3a1-fb91-44eb-804f-4ca8519dfc31 + +Build 7 live-ready launch: + 2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494 + +Live match/lobby plus clean shutdown: + 2026-08-30T09-25-17.519Z-1a9d0227-3cf8-4a19-b353-c0f135ccf31c + +Stable-signed release launch plus clean shutdown: + 2026-08-30T20-24-24.969Z-998c4e53-ff91-4cf3-8002-21543dc5d46f + +Second launch proving retained drive consent: + 2026-08-30T20-25-43.388Z-8373817a-5c4c-4b47-9459-8c2a8751b096 + +Build 4 primary-touch/login investigation: + 2026-08-30T20-41-22.662Z-4bdb8a3f-813d-4e70-b8bd-67c0b6b5766f +``` + +Official launcher artwork is the full-bleed 1254×1254 PNG at +`tftmac/Assets/TFTMAC-Official-Icon.png`, generated with the built-in ImageGen +tool from the requested penguin-samurai, single-sword and exact stacked +`TFT`/`MAC` brief. The master has no baked-in rounded rectangle or outer gutter; +macOS owns the final corner mask. Build 7 derives and signs every `.icns` +representation from this hash-sealed source. + +The first receipt reached Unreal `GameActivity`, rendered a live match and the +fully colored post-match lobby, recorded source/output maxima of 60.95/60.53, +zero sequence drops, active 48-kHz stereo output with zero underruns, and zero +confirmed memory kills. Its normal Quit sealed SQL as `STOPPED`, confirmed the +owned emulator exit, restored the exact AVD hash, and removed both transaction +and lease markers. The second receipt covers the last runtime-identical signed +build. The subsequent icon release changed release metadata and signed launcher +artwork. Build 7 then added the Combat Latency A scheduling receipt, permanent +guest gameplay-power gate and cross-session combat-layer fix. Its live-ready +capture is recorded above; it is not yet a performance result. The earlier live +native window was re-verified at origin +`0,0`, size `1920x1080`, +`AXFullScreen=true`; SQL later observed source/output maxima of 60.99/60.33, +active 48-kHz stereo output with one active track and zero underruns, and zero +confirmed memory kills. Four cumulative sequence gaps appeared only after +repeatedly backgrounding the app and switching Spaces for release captures; the +separate uninterrupted match/lobby receipt retained zero sequence drops. The +installed-release session also captured one real +Riot `MobileFREWebViewActivity` input-dispatch ANR after a five-second +MotionEvent timeout. That process recovered automatically from PID 2348 to PID +4439; the session remained `RUNNING`, with no fatal, Vulkan, or audio-error +aggregate. Secure Android PIN entry remains deliberately manual. + +## 2. Non-negotiable runtime invariants + +These values are product authority, not suggestions: + +```text +Engine fact: Unreal Engine +Runtime root: /Volumes/MAC MINI M4/TFTMAC/Runtime +Emulator: Google Android Emulator 37.1.11 / build 15917651 +AVD: TFT_Ultra_Tablet / API36 Google Play ARM64 +Package: com.riotgames.league.teamfighttactics +Launcher: /usr/bin/open -n -W -> packaged TFTMAC Emulator Host.app +ADB server: 5038 +Console: 5582 +Serial: emulator-5582 +ADB_VENDOR_KEYS: absent +Controller: authenticated loopback gRPC, default port 8554 +Display: 1920x1080 / 320 dpi / 60-Hz default +CPU/RAM default: 6 vCPU / 5120 MiB +GPU/audio: host / CoreAudio +Graphics transport: virtio-gpu-asg +ASG: 1 MiB write buffer / 16 KiB write step / 32 KiB ring / flush 800 +ANGLE enabled: exposeNonConformantExtensionsAndVersions:exposeES32ForTesting +ANGLE disabled: preferSubmitAtFBOBoundary +MoltenVK requested: synchronous submits 0 / max active command buffers 64 / fast math 1 +``` + +The previous `5040 / 5592 / emulator-5592` service-context route is a historical regression. It must remain only as failure evidence. It is not a fallback and must never overwrite current authority. + +## 3. Architecture and ownership boundaries + +```text +Official TFT Unreal GameActivity + -> Android application graphics contract + -> ANGLE GLES compatibility layer + -> guest Vulkan/ranchu + -> virtio-gpu-asg + gfxstream + -> host Vulkan + -> emulator-bundled MoltenVK + -> Apple Metal executes emulator rendering + -> authenticated EmulatorController raw RGBA stream + -> latest-only TFTMAC mailbox + -> persistent native Metal queue / triple texture storage + -> AppKit full-screen Mac window +``` + +There are two Metal owners: + +1. MoltenVK uses Metal internally for the emulator's host Vulkan work. +2. TFTMAC uses its own Metal presenter for the completed Android image. + +Do not assign a stall to Unreal, ANGLE, gfxstream, MoltenVK, TFTMAC Metal, or macOS presentation from a metric owned by a different boundary. + +## 4. What frame telemetry means + +Player-facing causal reporting uses: + +```text +TFT · PIPE +``` + +- `TFT` is derived from exact SurfaceFlinger actual-present timestamps. +- `PIPE` reports completed-source freshness through authenticated gRPC. +- Raw native-presenter cadence/GPU data remains stored only as hidden + correctness/regression context; it is not displayed or ranked as a root. +- Neither controller-source nor native-presenter cadence is Unreal engine FPS. +- Deep attribution requires the planned shared work-ID/source-site diagnostic + logger; optional markers and aggregate Perfetto counts cannot supply it. +- `dumpsys gfxinfo` is blind to the native Unreal/Vulkan workload and must not be used as primary FPS authority. + +## 5. Native logging system + +Every app launch creates a private session directory: + +```text +~/Library/Application Support/TFTMAC/Captures// +``` + +The directory is mode `0700`. Queryable authority is `TFTMAC_NATIVE_RUNTIME.sqlite`; local sidecars include `native-events.jsonl`, emulator stdout/stderr, a reversible AVD backup, and session-scoped `logcat.raw.txt`. + +| SQL table | What it establishes | Normal cadence | +| --- | --- | --- | +| `sessions` | Start/end/status/profile | One row per app run | +| `runtime_receipts` | Ports, launcher, AVD, renderer requests, profile | At startup | +| `events` | Lifecycle, package, ADB transitions, markers, failures | Event-driven | +| `frame_samples` | Visual/hash checkpoints, dimensions, sequence | First frame and bounded checkpoints | +| `frame_interval_windows` | Source ingress count, drops, mean/p95/max interval | One-second windows | +| `presentation_samples` | SRC/OUT rate, mailbox replacement and drops | About once per second | +| `game_frame_intervals` | Exact TFT SurfaceView actual-present intervals, jank/severe flags and missed-vsync equivalents | Every newly observed guest frame | +| `game_frame_windows` | TFT FPS, 1% low, p50/p95/p99/max, jank/severe counts and explicit unavailable status | One-second windows | +| `stream_freshness_windows` | Sampled content changes, repeated images, longest static run and transport loss | One-second windows | +| `host_presentation_windows` | Final Metal completion/GPU timing, unique/repeated source use, drawable misses and command errors | One-second windows | +| `resource_samples` | QEMU CPU/RSS, TFT PID, resumed activity | Five seconds | +| `guest_memory_samples` | MemTotal/MemAvailable/swap | Five seconds | +| `host_resource_samples` | Host available/compressed/swap/pageouts, thermal state and AC/battery source | Five seconds | +| `clock_sync_samples` | Host/guest monotonic alignment and RTT | Thirty seconds | +| `surfaceflinger_samples` | Render rate and cumulative total/HWC/GPU misses | Start/end and 30 seconds during gameplay | +| `audio_samples` | CoreAudio receipt plus active output/rate/stereo/tracks/underruns | Start/end and 30 seconds during gameplay | +| `logcat_aggregates` | Counts only: ANR, input timeout, fatal, LMK/OOM, skipped frames, ANGLE/Vulkan warnings, PCM errors | Five seconds | +| `pipeline_log_aggregates` | Counts only real gfxstream/ASG/Vulkan/MoltenVK/shader/fence warnings, errors, stalls and timeouts | Five seconds | +| `graphics_runs` | Automatic TFT PID/layer lifecycle, configuration SHA, target FPS and close reason | Process/layer transition | +| `graphics_pipeline_snapshots` | Canonical stack receipt/SHA plus TFT SurfaceView, Unreal graphics API, ANGLE, gfxstream, MoltenVK, host Vulkan and Metal identity | Start/change/end and thirty seconds during gameplay | +| `graphics_pipeline_incidents` | Exact-layer degradation, admitted trace link, first observed boundary, conservative causal owner/confidence, and explicit unknowns | Automatic event | +| `diagnostic_artifacts` | Raw and normalized trace paths/hashes, pinned processor hash, normalized SQL summary and analysis state | Event-driven | +| `combat_benchmarks` | Named preset, complete configuration hash, coverage, validity, exact layer and combat metrics | Benchmark boundary | +| `combat_incidents` | Bad-window or visible-stutter trigger, frame state, trace sequence and explicit unknown boundary | Event-driven during benchmark | +| `combat_comparisons` | Control/candidate deltas, correctness, observer overhead and HOME_RUN/PROMISING/REJECT/INCONCLUSIVE decision | Candidate benchmark end | +| `game_process_sessions` | TFT PID start/end | PID transition | +| `input_samples` | Primary-touch coordinate/pressure, secondary-mouse button, and keyboard character count/special key | Input event; no typed content | + +Raw logcat is local sensitive data. It is excluded from SQL and must never be uploaded or pasted without deliberate sanitization. It begins at a guest timestamp taken after ADB authorization so stale ring-buffer events do not contaminate the run. + +### Automatic graphics logging and optional A/B (2.3.0 build 8 installed) + +The base logger requires no menu action. It starts when the TFT process or exact +GameActivity layer is observed, records the complete graphics lifecycle, and +closes with the process/app. `Mark Visible Stutter` is optional context. The +renamed controlled A/B window remains valid after 300 seconds and closes at 480 +seconds. Automatic incident traces require two adjacent bad one-second windows, +use a 120-second cooldown, and are capped at two per graphics run. During an +active A/B those incidents share the benchmark's two-incident budget in addition +to its one 20-second start trace. + +`Control` is the exact 1920x1080/320-dpi/60-Hz, 6-vCPU, 5120-MiB proven +configuration with TFT High / 60 FPS / Performance Mode OFF. `Combat Latency A` +changes only the packaged emulator host's requested macOS QoS class to +`user_interactive`. The host records the requested value, return code and +pre-exec effective class before replacing itself with Android Emulator. It does +not claim that every QEMU worker inherited the class without combat evidence. +All graphics, ASG, MoltenVK, CPU, RAM, resolution and audio values remain locked. + +`Home Run A` and Riot Performance Mode Beta are retired. The completed +480.65-second run recorded 56.665 weighted FPS but only 17.698 FPS 1% low, +517.488 ms maximum frame interval and two automatic degradation incidents. The +user rejected the experience as unacceptable. Cross-boundary causal attribution +remains unknown because clock RTT and trace observer-overhead gates failed; that +does not override the direct usability rejection. + +Every valid Perfetto artifact is processed locally by the packaged +`trace_processor_shell` v58.2 ARM64 binary. Its pinned SHA-256 is +`d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa`. +If normalization fails or that receipt changes, TFTMAC removes the unprocessed +raw trace and records the failure; it does not retain a trace indefinitely in a +raw-only state. The persistent comparison authority is: + +```text +~/Library/Application Support/TFTMAC/TFTMAC_LAB.sqlite +``` + +The build, SQL schema, preset invariants and decision engine are statically +verified. The current 42m27s automatic Build 8 run proves logging and the +performance deficit while leaving internal attribution unknown. The next +development gate is isolated source instrumentation, not another mandatory +match-marker or combat-pair run. + +### Riot login input contract + +Build 4 removed the desktop-mouse assumption from primary interaction. A Mac +left press, drag and release now becomes Android multitouch identifier `0`, +with non-zero pressure while in contact and an unconditional zero-pressure +release at the last valid Android coordinate. The live build-4 capture logged +`PRIMARY_TOUCH_INPUT_ACTIVE`; pressure-down and pressure-up rows were persisted +without credential content. + +The same investigation found the guest WebView provider at +`133.0.6943.137`. Google Play exposed and installed the signed stable update +`151.0.7922.199`; Riot's `MobileFREWebViewActivity` restarted on that provider. +WebView is a mutable guest dependency and its exact version belongs in runtime +receipts whenever login behavior changes. + +Riot's official form labels the first field `USERNAME`. It requires the private +Riot account login username, not an email address and not the public Riot ID +(`Name#Tag`). Riot's official recovery flow starts from the account email and +sends back the username: . +Passwords, usernames, email addresses and form screenshots are never retained +as TFTMAC evidence. + +`memory_kill_count` is intentionally conservative: it counts only an LMKD line +that names an actual kill victim or a kernel `Out of memory: Killed process` +line. LMKD connection, memevent, tracepoint, monitor and policy setup messages +remain in the private raw log but do not become false-positive kills in SQL. + +Useful queries: + +```sql +-- Run and exact profile +SELECT * FROM sessions; +SELECT receipt_key, receipt_value, confidence +FROM runtime_receipts ORDER BY id; + +-- Frame ingress and native output +SELECT started_monotonic_ns, frame_count, sequence_drop_count, + mean_interval_ms, p95_interval_ms, maximum_interval_ms +FROM frame_interval_windows ORDER BY started_monotonic_ns; + +SELECT sampled_monotonic_ns, source_fps, presentation_fps, + mailbox_replacements, sequence_drops +FROM presentation_samples ORDER BY sampled_monotonic_ns; + +-- Player-visible TFT guest frame truth +SELECT started_monotonic_ns, status, unavailable_reason, + effective_fps, one_percent_low_fps, + p95_interval_ms, p99_interval_ms, maximum_interval_ms, + jank_count, severe_count, missed_vsync_equivalents +FROM game_frame_windows ORDER BY started_monotonic_ns; + +-- Delivery freshness is causal context; final Mac presenter is hidden correctness context +SELECT * FROM stream_freshness_windows ORDER BY started_monotonic_ns; +SELECT * FROM host_presentation_windows ORDER BY started_monotonic_ns; + +-- Optional user annotations; never required for automatic full-run validity +SELECT kind, monotonic_ns FROM events +WHERE kind IN ('MATCH_ENTRY','COMBAT_START','VISIBLE_STUTTER','MATCH_END') +ORDER BY monotonic_ns; + +-- SurfaceFlinger deltas belong inside the automatic graphics-run lifetime +SELECT sample_label, monotonic_ns, render_rate_hz, + total_missed_frames, hwc_missed_frames, gpu_missed_frames +FROM surfaceflinger_samples ORDER BY monotonic_ns; + +-- Sound-health regressions +SELECT sample_label, active_output, sample_rate_hz, stereo_output, + active_tracks, partial_underruns, empty_underruns +FROM audio_samples ORDER BY monotonic_ns; + +-- Crash, memory and renderer signals without exposing raw log text +SELECT * FROM logcat_aggregates +WHERE anr_count + fatal_count + memory_kill_count + + angle_warning_count + vulkan_warning_count + audio_error_count > 0; + +SELECT * FROM pipeline_log_aggregates +WHERE gfxstream_warning_count + asg_stall_count + vulkan_error_count + + moltenvk_warning_count + shader_error_count + fence_timeout_count > 0; +``` + +Run `scripts/summarize-native-session.command` with no argument for the latest +capture or pass one capture directory. It validates SQLite first, summarizes +every evidence layer, and reports missing telemetry explicitly without printing +raw logs, credentials or tokens. + +## 6. Performance Lab controls + +The app's `Performance Lab…` window persists only validated, restart-bound values. Restart is mandatory so the logger has one attributable profile and the AVD transaction remains reversible. + +| Variable | Safe UI domain | Default | Evidence rule | +| --- | --- | --- | --- | +| vCPU | `4, 6, 8` | `6` | One-factor test; watch host CPU and frame windows | +| Guest RAM | `4096, 5120, 6144 MiB` | `5120` | 5120 is KEEP; 4096 remains deferred unless a deliberate test is run | +| Refresh target | `30, 60 Hz` | `60` | Do not confuse refresh target with engine FPS | +| ASG draw flush | `400, 800` | `800` | 400 is experimental; score SurfaceFlinger delta and host CPU overhead | + +The following remain fixed in the UI: 1920x1080, 320 dpi, ports, AVD/image, official package, host GPU, CoreAudio, ASG buffer/step/ring, ANGLE compatibility flags, MoltenVK requests and native presenter design. + +The Telemetry menu records exact user-observed boundaries: + +```text +Command-Shift-1 MATCH_ENTRY +Command-Shift-2 COMBAT_START +Command-Shift-3 VISIBLE_STUTTER +Command-Shift-4 MATCH_END +``` + +`COMBAT_START` and `VISIBLE_STUTTER` also request a bounded 15-second Perfetto +ring trace. Severe actual-present degradation may request a rate-limited trace +automatically. A raw trace is evidence to normalize later, not an automatic +root-cause verdict. + +## 7. Experiment protocol + +1. Start from `tftmac_5gb_native_v1`. +2. Change exactly one restart-bound variable. +3. Quit cleanly and relaunch; never mutate an AVD profile mid-match. +4. Keep the same TFT build, graphics preset, FPS cap, workload phase and login state where practical. +5. Let the automatic process/layer logger define the full run. Match, combat and + visible-stutter markers are optional annotations only. +6. Compare exact full-run cadence/tails, source freshness, stack receipts and + valid owned diagnostic spans. Resource/audio data remains correctness context. +7. Reject any boot, ADB, package, crash, memory, audio or usability regression. +8. KEEP only after a comparable repeat plus cold confirmation. +9. Record why a candidate was kept or rejected; never promote from a single lobby sample. + +Current product decisions: + +- KEEP 5120 MiB. Sustained donor runs showed lower pressure direction than 6144 MiB while retaining guest headroom. +- DEFER 4096 MiB. It lacks sufficient heavy-game safety margin. +- KEEP High / 60 / Performance OFF as the user-confirmed current in-game control. +- REJECT Ultra High for current usability; direct user observation found severe lag, without fabricating a numeric FPS. +- RETAIN 800 µs ASG flush in Control; do not recycle the historical 400 µs screen as a new result. +- DEFER Combat Latency A promotion; its latest active observation is not a + controlled gain and the causal logger is now the next development layer. +- KEEP raw gRPC as the working native presentation transport now. +- DEFER MMAP until producer readiness, tear-free integrity, frame-age and performance are empirically proven. + +A transient black/white end-of-match frame was investigated before release. +The following guest-side screenshot and native Metal frame both showed the same +fully colored lobby, so no graphics feature override was promoted from that +single transient. `BypassVulkanDeviceFeatureOverrides` remains an unproven +one-factor diagnostic candidate only; use it only if a same-moment guest/native +capture proves repeatable missing board materials during active combat. + +## 8. ZoeMC and graphics-council conclusion + +The earlier 10,000-world simulation was reproducible but used subjective priors. It was correctly retained as hypothesis-ordering evidence, not as proof of a winning architecture. ZoeMC v0.2 concluded that real authenticated frame delivery was the decisive next test. + +That test has now resolved the first branch: + +- Raw authenticated gRPC is empirically viable for correct 1920x1080 native lobby presentation and input. +- MMAP remains a possible later efficiency optimization, not a requirement to play TFT. +- Direct MMAP zero-copy is invalid without producer-readiness/integrity fencing. +- Direct MoltenVK texture handoff has no supported resource-sharing contract. +- Encoded video/scrcpy and an external emulator window do not satisfy the native product target. + +Specialist boundary findings retained from the council: + +- Unreal owns game/render/RHI workload behavior, not macOS panel visibility. +- ANGLE owns the GLES-to-Vulkan compatibility boundary; exposed ES3.2 is a named nonconformant workload adapter, not conformance proof. +- gfxstream/ASG owns guest-to-host graphics command transport; requested ring/buffer values are not per-frame latency proof. +- MoltenVK owns host Vulkan-to-Metal translation; environment values are requested until effective readback is available. +- TFTMAC owns the final frame copy, orientation, scaling, input transform, output cadence and native Mac experience. +- Fortnite/Unreal guidance supplies transferable measurement categories, not TFT-specific capability or performance proof. + +The complete Unreal/Fortnite transfer map, attribution gates, owned patch +points and current unknown ledger are maintained in +`docs/TFTMAC_UNREAL_PIPELINE_OBSERVABILITY.md`. + +Source research artifacts: + +- `outputs/ZoeMC_TFTMAC_Unreal_Graphics_v0.2/REPORT.md` +- `outputs/ZoeMC_TFTMAC_Unreal_Graphics_v0.2/VARIABLE_MANIFEST.json` +- `outputs/ZoeMC_TFTMAC_Unreal_Graphics_v0.2/NEXT_TESTS.md` +- `outputs/TFTMAC_UNREAL_GRAPHICS_COUNCIL_FINDINGS.md` + +## 9. Safety and recovery + +- TFTMAC acquires an exclusive interprocess lease before touching the shared AVD. +- Launch fails closed if `TFT_Ultra_Tablet`, console `5582`, or controller `8554` is already occupied. +- The emulator command line carries a unique per-session marker. +- Cleanup kills only the exact process that still matches that ownership marker. +- AVD configuration is backed up, hashed, applied atomically and restored only after the owned emulator exits. +- Interrupted recovery accepts only the exact discovered AVD config path and a backup inside TFTMAC's capture root. +- A repeated Quit remains `terminateLater`; it cannot bypass telemetry sealing or AVD restoration. +- Google/Riot passwords, MFA, CAPTCHA and consent remain manual official-UI actions and are never logged or automated. +- Android secure-lock content may be intentionally blank in the authenticated screenshot stream. TFTMAC wakes the display and accepts manual PIN keyboard input without placing instructions over Android. SQL records the unlock-required state and input character counts, never the PIN. + +## 10. Remaining decisive gaps + +1. User confirms sound is audible at the Mac speakers/headphones. +2. Implement source-level causal work-ID instrumentation in the isolated + diagnostic runtime; normal-play Build 8 evidence must not fabricate an owner. +3. Correlate bounded diagnostic traces with SQL clock sync and SurfaceFlinger + deltas before attributing a graphics bottleneck. +4. Validate whether MMAP improves CPU/frame age without tearing; keep raw gRPC if it does not. +5. Treat source rate, output rate, guest frame timing and panel visibility as separate clocks and claims. +6. If black/white or missing board materials recur during active combat, mark `VISIBLE_STUTTER` and take guest/native screenshots at the same instant before changing ANGLE, Vulkan, gfxstream or MoltenVK flags. + +These are optimization and final user-acceptance gaps. They do not undo the proven native launch, lobby, rendering, input, official-package or software-audio path. diff --git a/docs/TFTMAC_UNREAL_PIPELINE_OBSERVABILITY.md b/docs/TFTMAC_UNREAL_PIPELINE_OBSERVABILITY.md new file mode 100644 index 0000000..c8f7d07 --- /dev/null +++ b/docs/TFTMAC_UNREAL_PIPELINE_OBSERVABILITY.md @@ -0,0 +1,202 @@ +# TFTMAC Unreal graphics observability and patch map + +Authority date: 2026-08-31 + +## Fixed premise and engineering objective + +TFT uses Epic's Unreal Engine technology. Fortnite and other shipped Unreal +Android titles are therefore valid architecture and experiment references. The +reference does not make every TFT runtime choice identical: the exact Unreal +branch, RHI, renderer features, shader keys, PSO cache, swapchain, device +profile, and CVars must be identified from this TFT run before a connector is +changed. + +The absence of Riot's signed game source changes the interception point. It +does not make the graphics pipeline unknowable. TFTMAC can observe and change +the components it owns: the Android/emulator configuration, AEMU/gfxstream, +ANGLE when active, MoltenVK when active, and the native frame transport. The +final Metal presenter is hidden correctness context only. A future isolated +source-built gfxstream runtime can add a shared +`frame_id` at every guest/host handoff without modifying Riot's APK. + +## Transferable Unreal model + +The useful Unreal reference pipeline is: + +```text +GameThread simulation + -> RenderThread command construction + -> optional RHIThread / graphics API submission + -> guest GPU queue and swapchain + -> Android compositor actual presentation +``` + +Unreal Insights can name GameThread, RenderThread, RHIThread, GPU and PSO work +when the application build exposes those trace channels. TFTMAC must leave +those internal spans `UNKNOWN` when the signed TFT build does not expose them. +Android platform traces still establish CPU scheduling, compositor deadlines, +GPU memory, and what the player actually saw. + +## Runtime paths that evidence must distinguish + +Only a per-session pipeline snapshot may promote one of these candidates: + +1. **Current receipt:** Unreal Vulkan -> guest Vulkan -> gfxstream -> host Vulkan -> MoltenVK -> Metal. +2. Conditional: Unreal GLES/EGL -> ANGLE Vulkan -> gfxstream -> host Vulkan -> MoltenVK -> Metal. +3. Unreal GLES/EGL -> native guest GLES -> gfxstream -> host rendering. +4. Another renderer selected by the installed TFT/emulator build. + +ANGLE applies only when the game selected a GLES/EGL path and Android selected +ANGLE for that package. MoltenVK applies only when the host Vulkan backend +actually loaded it. Requested launch flags are hypotheses; normal runtime log +identity and effective guest properties are evidence. + +## Causal clocks and hidden correctness context + +| Label | Measurement | Valid claim | +| --- | --- | --- | +| `TFT` | Exact TFT `GameActivity` BLAST SurfaceView, SurfaceFlinger actual-present timestamps | Frames the guest compositor actually presented for TFT | +| `PIPE` | Authenticated EmulatorController image arrivals plus sampled content identity | Transport delivery and whether delivered images changed | + +The final native Metal loop is retained privately as correctness/regression +context, not exposed as a causal clock. A healthy PIPE or native-presenter rate +can coexist with a frozen/repeated TFT frame; neither is Unreal FPS. Reporting +must show `TFT —` when the exact SurfaceView cannot be selected or read and must +never substitute another clock. + +## Implemented data contract + +Every native app start creates a private SQLite session. The 2.3 telemetry +extension adds: + +| Table | Evidence | +| --- | --- | +| `game_frame_intervals` | Every newly observed actual-present interval, direct active stack SHA, frame-window link, jank flag, severe flag and missed-vsync equivalent | +| `game_frame_windows` | One-second TFT FPS, direct active stack SHA, 1% low, p50/p95/p99/max, jank, severe stalls, missed vsyncs, layer identity, refresh period and explicit availability reason | +| `stream_freshness_windows` | Changed/identical sampled content, longest identical run and sequence loss | +| `host_presentation_windows` | Final Metal submissions/completions, unique/repeated sources, drawable/command errors, completion and GPU timing | +| `pipeline_log_aggregates` | Counts of real warning/error/stall lines at gfxstream, ASG, Vulkan, MoltenVK, shader and fence boundaries; normal configuration lines do not become failures | +| `graphics_runs` | Automatic TFT process/layer lifetime, start/end reason, configuration SHA, target FPS and exact-layer receipt | +| `graphics_pipeline_snapshots` | Per-snapshot stack receipt, canonical receipt JSON/SHA-256, completeness/unknown fields, and observed active-path identity | +| `graphics_pipeline_incidents` | Automatic exact-layer degradation window, admitted trace link, first observed boundary, and explicit causal unknowns | +| `diagnostic_artifacts` | SHA-256, byte count, trigger and analysis state for bounded Perfetto captures | +| `host_resource_samples` | Host available/compressed/swap/pageouts, thermal state and power source at five seconds | +| `combat_benchmarks` | Named preset, configuration SHA, 300/480-second boundaries, coverage, validity and summary metrics | +| `combat_incidents` | Optional controlled-A/B visible-stutter/benchmark evidence; not the authority for base graphics logging | +| `combat_comparisons` | Control/candidate deltas and deterministic HOME_RUN/PROMISING/REJECT/INCONCLUSIVE result | + +SurfaceFlinger history polls overlap. The collector establishes a first-poll +boundary, de-duplicates actual-present timestamps, resets on layer replacement, +and records bounded-history loss explicitly rather than inventing one giant +frame interval. Interval inserts are committed once per poll so the logger does +not create dozens of disk transactions per second. + +The logger begins automatically with the observed TFT process/layer lifecycle +and remains active until process/app close. It does not require a user marker, +battle classifier, or Combat Benchmark. The fixed Combat Benchmark remains an +optional controlled A/B path with one 20-second/32-MiB start trace and up to two +15-second/32-MiB incident traces. Automatic graphics incidents require an +observed TFT process, exact active layer, two adjacent bad one-second windows, +and, outside a controlled A/B, a separate graphics-run budget/cooldown. During +an active A/B the start trace and automatic/manual incidents share that +benchmark's fixed three-trace ceiling. Traces +include SurfaceFlinger, FrameTimeline, layers, Android GPU memory, scheduler +switch/wakeup/waking events, process/system stats, TFT process identity and +guest CPU-frequency events when exposed. Every retained trace is hash-sealed +and normalized immediately with pinned Perfetto `trace_processor_shell` v58.2; +normalization output and hashes are written to SQL. Failure deletes the +unprocessed raw trace and records a trace failure. + +`graphics_runs`, stack-receipt SHA-256s, and frame-window joins are implemented +and runtime-verified by Build 8 automatic captures. They establish lifecycle +and observation integrity, not a causal owner. + +## Attribution gates + +| Candidate blocker | Evidence required before naming it | +| --- | --- | +| Guest display/frame pacing | TFT actual-present misses or long intervals, with bounded SurfaceFlinger/FrameTimeline evidence | +| Unreal CPU thread | A missed frame correlated with named Unreal spans, or at minimum TFT process runnable/running pressure; without named spans report only process CPU pressure | +| gfxstream/ASG transport | Queue depth or submit-to-host receive time from an instrumented gfxstream build; warning counts alone are not latency proof | +| Host emulator GPU | Host renderer command completion crossing display deadlines plus supported Metal/GPU counters | +| PSO/shader hitch | A PSO compile/miss event coincident with the hitch in a controlled UE trace; a fight-only spike is not sufficient | +| ANGLE mismatch | A session snapshot proves ANGLE is active and a one-factor controlled change improves actual-present percentiles without a correctness regression | +| MoltenVK behavior | A snapshot proves MoltenVK is active and its own queue/pipeline-cache instrumentation or a controlled build isolates the change | + +The conservative weakest-boundary view may report only the first observed +unhealthy/missing receipt among `TFT` and `PIPE`; it must never turn a +time-adjacent warning, CPU/RAM/audio health sample, or incomplete stack receipt +into a graphics causal claim. CPU/RAM/audio remain correctness and health +context, outside the graphics-only optimization equation. + +## Safe code and configuration intervention points + +1. Native logger/controller: markers, clock alignment, capture health, loss + counters, SurfaceFlinger timing and Perfetto lifecycle. This is implemented. +2. **Planned, gated next layer:** isolated `tftmac-runtime` (`c8aa26e`) may emit + one transport work ID and source-site receipt at guest submit, host receive, + host queue, host Vulkan submit, MoltenVK enqueue, Metal completion and + Android buffer release. It is diagnostic-only and cannot replace or be + performance-compared with normal-play Build 8 until parity gates pass. +3. ANGLE source/build, only if active: log renderer/feature selection, shader + translation/cache time, queue waits and relevant extension negotiation. +4. MoltenVK source/build, only if active: log pipeline creation/cache hits, + queue submit, command-buffer scheduling/completion and swapchain behavior. +5. TFTMAC's final Metal presenter: preserve only as a hidden correctness receipt; + it is not a current graphics optimization or root-cause target. +6. A controlled Unreal Android reference app on the identical emulator stack: + expose Unreal Insights and systematic PSO/frame-pacing experiments. It + proves stack capability and causal signatures, not TFT internals by proxy. + +Do not patch the signed TFT APK, inject code into Riot's process, copy +Fortnite's PSO cache, or label a guessed shader as Riot's. Unreal/Fortnite +practices remain valuable as test hypotheses: PSO precaching, work off the +interactive frame, stable Android frame pacing, device-profile quality tiers, +and one-factor RHI experiments. + +## Automatic full-run and bounded A/B protocol + +1. Let automatic process/layer logging run for the full TFT session. Markers and + classifiers are optional context, not causal gates. +2. Compare complete timelines and automatic degradation episodes using TFT + actual-present, source freshness, stack receipts, and valid diagnostic spans. +3. Use a bounded A/B only when it screens one named configuration factor. +4. Do not include the Mac presenter in causal ranking. +5. Classify the first divergent boundary; leave later boundaries as effects. +6. If Combat Latency A wins, run the five-minute cold confirmation before promotion. +7. Repeat a comparable fight and a cold-start confirmation. Reject correctness, + boot, ADB, audio, memory, login, or usability regressions. +8. Use the automatic trace-active/trace-inactive comparison; over 5% change marks causal trace conclusions `OBSERVER_OVERHEAD_INVALID` while retaining performance data. + +## Current unknowns to close with data + +- Exact TFT Unreal branch and mobile renderer feature set. +- Active per-run RHI and all negotiated graphics extensions/features. +- Game/Render/RHI thread spans when the signed build does not expose them. +- TFT shader/PSO keys, cache hit rate and compile timing. +- gfxstream queue depth and per-frame guest-submit-to-host-receive latency. +- MoltenVK pipeline-cache and queue timing inside the emulator process. +- Host Metal saturation counters for the emulator renderer, independent of the + hidden final-presenter correctness receipt. +- Observer overhead during a comparable automatic run. + +An unknown is not filled by a Fortnite assumption. Fortnite/Unreal tells us +which mechanism and signature to test; the correlated TFT run tells us whether +that mechanism is active and failing here. + +## Primary technical references + +- Epic, platform-native Android profiling: +- Epic, Unreal Insights channels: +- Epic, mobile frame pacing: +- Epic, PSO cache optimization: +- Epic, PSO precaching: +- Android, game frame-rate optimization: +- Android, frame pacing: +- Perfetto, FrameTimeline: +- Perfetto, GPU data sources: +- Google gfxstream source: +- ANGLE source: +- Khronos Vulkan specification: +- MoltenVK runtime guide: +- Apple, Metal performance analysis: diff --git a/docs/architecture.md b/docs/architecture.md index db84039..2d2b81c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,196 +1,59 @@ -# Architecture - -Mactician is a native SwiftUI application that installs and orchestrates -a pinned Android Emulator runtime. It keeps mutable Android data outside the -application bundle so a full app replacement does not replace the AVD or game -state. - -## Components - -- `MacticianApp.swift` owns application startup, the main window, About, and shutdown. -- `LauncherView.swift`, `LauncherStateViews.swift`, and the supporting component - files render install, ready, running, failure, and settings surfaces. -- `LauncherModel.swift` is the main-actor presentation model and state machine. - It validates saved settings, starts installer/runtime operations, maps errors - to recovery actions, and coordinates hotkeys and login repaint repair. -- `InstallerService.swift` checks the host, verifies game inputs, downloads and - hashes Android archives, installs the SDK layout, creates the AVD, provisions - TFT, and persists `InstallState` after each durable stage. -- `RuntimeController.swift` validates the pinned game state, refreshes the small - launcher-owned runtime project, prepares a verified overlay, starts - `launcher-runtime.command`, and decodes its JSON-line events. -- `run-tft-root-affinity.command` and `scripts/run-asg-experiment.command` own the - low-level emulator session, reversible AVD edits, guest overlays, Android - graphics configuration, and cleanup. -- `InputBridgeService.swift` provides scoped macOS hotkeys only while the - packaged emulator is active and Android reports TFT's `GameActivity`. -- `RiotLoginAnimationRepairService.swift` removes two completed login-form CSS - animations through a temporary loopback-only DevTools forward. It does not - read or modify field values. -- `LauncherUpdateController.swift` exposes Sparkle's update UI. -- `LauncherTelemetryService.swift` owns the bounded retry queue, anonymous - launch/session events, strict message responses, and safe image decoding. -- `LauncherAnnouncementView.swift` renders server-selected messages without - HTML or executable content. -- `EmulatorHost/main.c` is a minimal app-bundle host for Google's Emulator, used - to retain the intended Dock identity and icon. - -## State and installation - -The UI state and the durable install state are related but separate: - -```mermaid -stateDiagram-v2 - [*] --> NeedsInstall - NeedsInstall --> Installing: Accept terms and Install - Installing --> Ready: manifest, SDK, AVD, and game verified - Installing --> NeedsInstall: cancel; partial downloads remain resumable - Installing --> Failed: integrity or process failure - Ready --> Launching: Play - Launching --> Playing: runtime ready event - Launching --> Failed: fail-closed runtime error - Playing --> Stopping: Stop or app termination - Stopping --> Ready: overlays and AVD config restored - Failed --> Installing: Repair - Failed --> Launching: retry supported launch failure +# TFTMAC Architecture + +TFTMAC is a native macOS application controlling a known-good stock Google Android Emulator runtime. + +## Product boundary + +```text +TFTMAC.app + -> AppKit application/window + -> Metal presentation + -> authenticated local EmulatorController client + -> stock Google Android Emulator + -> official Google Play ARM64 guest + -> official TFT package ``` -`InstallState.Stage` progresses through `empty`, `downloading`, `sdk_installed`, -`avd_created`, and `ready`. The JSON state also records installed component -versions, pinned game version/base hash, overlay hash, schema version, and last -update time. A partially written or incompatible state fails back to an empty -state. +The runtime root is external to the repository so application source changes do not replace AVD userdata, Google Play state, Riot sign-in, or installed game data. -Installer outputs are staged before replacement. The runtime project is -refreshed by moving the old directory aside, moving the new copy into place, -and restoring the previous copy if activation fails. Downloads are resumable; -hashes and expected sizes are verified before use. +## Native application -## Runtime data layout +`tftmac/App/` owns application lifecycle and the main window. +`tftmac/Presentation/` owns the Metal presentation shell and viewport mapping. +The source target is Apple Silicon and the application bundle identifier is +`com.flashls1.tftmac`. -Default root: `$HOME/Library/Application Support/Mactician`. +The presentation layer maintains the game aspect ratio and maps native viewport coordinates into the 1920x1080 Android source coordinate space while rejecting input in letterbox regions. -```text -sdk/ pinned Platform Tools, Emulator, and system image -avd/TftPBE.avd/ Android virtual-device state and game data -runtime-project/ refreshed Mactician-owned scripts and profiles -downloads/ resumable component archives during installation -.staging/ transactional temporary files -logs/launcher.log launcher/runtime diagnostics -install-state.json durable installation state -``` +## Emulator control -Game APKs are build-time application resources; they are verified against -`release-manifest.json` before installation. They are not stored in this source -repository. - -## Launch and stop - -```mermaid -sequenceDiagram - participant UI as SwiftUI launcher - participant Model as LauncherModel - participant Runtime as RuntimeController - participant Host as Emulator host - participant Guest as Android guest - - UI->>Model: Play with profile/language/resources - Model->>Runtime: Validate install state and settings - Runtime->>Runtime: Patch verified Dock-icon instruction - Runtime->>Runtime: Refresh runtime project and verified overlay - Runtime->>Host: Start launcher-runtime.command - Host->>Guest: Cold boot dedicated AVD - Host->>Guest: Apply reversible ASG, ANGLE, profile, locale, and UI scale - Guest-->>Runtime: booting / emulator_started / ready events - Runtime-->>Model: update UI and start scoped services - Guest-->>Runtime: game process absent for three checks - Runtime-->>Model: game_stopped and exact session duration boundary - UI->>Model: Stop - Model->>Runtime: emulator kill and TERM - Host->>Guest: unmount overlays and restore properties/config - Runtime-->>Model: stopped -``` +TFTMAC uses the exact EmulatorController protocol shipped by the installed stock emulator. The protocol snapshot and provenance live under `Vendor/AndroidEmulator/`. Production control must be authenticated and local; an unauthenticated fixed gRPC control endpoint is not an accepted architecture. -Normal shutdown, TERM, and the next launch all participate in recovery. Durable -sidecar backups allow `run-asg-experiment.command` to repair an interrupted -configuration before another run. Lock ownership prevents concurrent mutation -of the same AVD. - -## Repair, Reset, and updates - -Repair repeats host, manifest, component, and game verification; refreshes -Mactician-owned scripts; and reprovisions missing or invalid installation -pieces. It keeps the existing AVD unless corruption requires the explicit Reset -path. The streaming-cache repair removes only `StreamingInstalls` when its -public `Metadata.manifest` is zero bytes. - -Reset deletes the complete launcher data root after confirmation. This removes -the AVD, downloads, Riot sign-in, game data, logs, and install state. - -Sparkle replaces the complete application bundle atomically. The runtime root -and `UserDefaults` remain outside that bundle, preserving user state. The -current identifiers are `dev.sergeinaumov.mactician`, -`~/Library/Application Support/Mactician`, and -`https://sergeinaumov.dev/mactician/updates/appcast.xml`. - -## Telemetry and operator messages - -A game session begins only on the runtime `ready` event and ends once on -`game_stopped`, `stopped`, or application shutdown. Its first completion creates -one unlinkable `first_game_session` event. The event is synchronously persisted -before the request, retried with the same event UUID, and terminally completed -after success, duplicate acknowledgement, unrecoverable 4xx, or seven days. - -An independent, bounded queue stores `game_session_diagnostics` only while -consent version 1 is granted. Revocation synchronously removes that queue before -another request can begin. Diagnostic events contain applied launcher settings -and coarse host properties, but receive independent event UUIDs and no -installation identifier. On migration, the legacy installation UUID and queued -`launcher_started`/`game_session` records are deleted. The full state machine -and payload schemas are documented in [Telemetry and privacy](telemetry.md). - -Message lookups use separate `launcher_started` and `game_closed` triggers. -One-time message IDs are remembered in a bounded 128-entry set. The client -refuses redirects and non-HTTPS/cross-origin image URLs, caps JSON at 16 KiB and -images at 2 MiB, accepts only PNG/JPEG, and checks image dimensions and total -pixels with ImageIO before decoding. Message text is rendered as plain SwiftUI -`Text`, never HTML. - -## Host/guest boundary and graphics - -The macOS host owns SwiftUI, downloads, manifests, the emulator process, -Hypervisor Framework, input filtering, update verification, and transactional -AVD configuration. Android owns app installation, locale, TFT processes, the -official Riot WebView, game state, and guest scheduling. - -The selected graphics chain is: - -```mermaid -flowchart LR - A["TFT OpenGL ES"] --> B["Guest ANGLE"] - B --> C["Vulkan encoder"] - C --> D["gfxstream transport"] - D --> E["MoltenVK"] - E --> F["Metal"] -``` +## Android/package authority + +The normal guest is an official Google Play ARM64 image. TFT application installation and updates are owned by Google Play. Riot's application owns Riot authentication and content initialization. + +TFTMAC does not mirror, bundle, patch, re-sign, or privately update Riot binaries. + +## Runtime storage + +Bulk runtime state is outside Git under `/Volumes/MAC MINI M4/TFTMAC/Runtime`. Repository source contains only code, tests, protocol snapshots, compact evidence, and configuration that is safe to version. + +The source-built emulator development tree is not part of normal product +architecture. The stock Build 8 runtime is the only normal-play authority. An +isolated source-built `tftmac-runtime` at `c8aa26e` is eligible solely for future +source-level causal diagnostics; it cannot replace or be performance-compared +with the stock runtime until separate parity and correctness gates pass. + +## Diagnostics + +Raw runtime telemetry is captured append-only, then normalized for analysis. +Build 8 identifies exact SurfaceFlinger degradation but cannot identify an +internal graphics root because no work ID crosses its guest/host pipeline. +Source-level causal instrumentation is planned; current measurements must leave +that owner `UNKNOWN`. Performance changes are one-variable, reversible A/B +experiments with explicit KEEP/REJECT decisions. + +## Failure boundaries -Android HWUI/WebView uses Skia OpenGL to avoid a verified WebView Vulkan -deadlock; this does not disable Vulkan below TFT's ANGLE renderer. - -Fixed-stage and cold-boot measurements identify the guest command-serialization, -MMIO/kick, readback, and synchronization boundary as the dominant graphics -bottleneck: the host decoder is usually waiting for work rather than saturating -the transport bandwidth. A shorter guest GLES encoder route was prototyped, but -the current guest/host gfxstream capability contract exposes only ES 3.0 while -TFT actively requires ES 3.1 compute, image, barrier, and texture-buffer -semantics. The evidence, rejected variants, source patches, and implementation -alternatives are recorded in [Native GLES transport experiment](native-gles-transport-experiment.md). - -## Fail-closed checks - -The launcher refuses to continue on manifest schema errors, unsafe archive -paths, size/hash mismatches, unsupported architecture, insufficient resources, -missing Hypervisor support, an unexpected game version, invalid overlay/profile -hashes, unknown graphics transport, conflicting AVD ownership, incomplete -rollback, unknown autonomous UI states, visible CAPTCHA/MFA, or an unsigned -production update build. Recovery never silently patches an unknown game build. +TFTMAC fails closed when the expected runtime, protocol authority, package identity, installer authority, or protected external storage is missing or inconsistent. Unknown effects are not replayed blindly. User-required Google/Riot authentication is surfaced through official UI rather than automated around. diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 767109c..e680286 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -1,5 +1,9 @@ # Benchmarks +> **Historical evidence only.** This is the M1 Max/API36 userdebug fixed-stage +> campaign. It is not authority for the M4 Build 8 stock runtime, its current +> direct Unreal Vulkan receipt, or current automatic full-session analysis. + This document summarizes the retained performance evidence without promoting single runs or comparisons from different scenes. diff --git a/docs/building.md b/docs/building.md index adb5a8c..33c047e 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,131 +1,79 @@ -# Building +# Building TFTMAC ## Requirements -- Apple Silicon Mac with macOS 12.0 or later -- Xcode Command Line Tools (`xcrun swiftc`, `xcrun clang`, `codesign`, `plutil`) -- zsh, `jq`, `rg`, `curl`, `tar`, `zip`, `unzip`, `shasum`, and `xmllint` -- Node.js only for `scripts/login-tft-from-keychain.command` -- Four exact unmodified TFT PBE `18.1-5212127` APK splits in a private local - directory; names, sizes, and hashes are in - `launcher/Resources/release-manifest.json` +- Apple Silicon Mac +- macOS 15 or later +- Xcode 26.6 +- zsh +- Node.js 24 +- `jq` and `ripgrep` -Production binaries target `arm64-apple-macosx12.0`. Unit-test binaries target -the current macOS host architecture so CI can run on either Intel or Apple -Silicon runners; production and release targets remain Apple Silicon-only. +The working stock Android runtime is external to the repository and is not rebuilt during normal native application compilation. -## Sparkle preparation +## Build -`scripts/prepare-sparkle.command` downloads Sparkle 2.9.4 from its upstream -GitHub release into `launcher/.build/`, verifies SHA-256 -`ce89daf967db1e1893ed3ebd67575ed82d3902563e3191ca92aaec9164fbdef9`, -validates its signature, and stages the framework and release tools. A valid -cached copy is reused. +Repository/CI verification does not require an installed app, external runtime, +credentials, or signing identity. For an intentional local signed package, +create or repair TFTMAC's stable local-only signing identity first: ```sh -./scripts/prepare-sparkle.command +/bin/zsh scripts/ensure-local-signing-identity.command ``` -## Fast validation +The identity remains in the current user's login Keychain so macOS can retain +the removable-volume grant across changed local builds. It is not a Developer +ID and is not suitable for public distribution. + +Current-host status (2026-08-31): the identity is absent, the installed Build 8 +hashes still match their historical release receipt, and deep/strict trust +verification reports `CSSMERR_TP_NOT_TRUSTED`. Do not rebuild or re-sign the +playable app merely to make source verification pass. ```sh -./scripts/verify-repository.command -./scripts/test-mactician.command +/bin/zsh scripts/build-native-app.command ``` -Together these commands check repository policy, shell syntax, plist/strings -and JSON syntax, Markdown links, executable bits, version consistency, C -syntax, unit tests, and full Swift typechecking. The test script also verifies -runtime shutdown classification, update safeguards, the scoped login repaint -repair, English/Russian UI resources, resource selectors, overlay preparation, and -state serialization. +The Release application is produced under the ignored native build directory, +copied to `dist/TFTMAC.app`, and signed as `TFTMAC Local Code Signing`. -Equivalent focused checks include: +## Test ```sh -find . -type f \( -name '*.command' -o -name '*.sh' \) -print0 \ - | xargs -0 -n 1 zsh -o NO_BG_NICE -n -find launcher -type f \( -name '*.plist' -o -name '*.strings' \) -print0 \ - | xargs -0 -n 1 plutil -lint -find . -type f -name '*.json' -print0 | xargs -0 -n 1 jq empty -xcrun clang -target arm64-apple-macosx12.0 -fsyntax-only launcher/EmulatorHost/main.c +/bin/zsh scripts/test-native-app.command ``` -## Local ad-hoc build +The current native tests cover the Gate 1 viewport/input mapping contract and run on Apple Silicon macOS. + +## Full validation ```sh -PROJECT_DIR="$PWD" -TFT_GAME_APK_DIR="$PROJECT_DIR/private/tft-pbe-apks" \ - ./scripts/build-mactician.command +/bin/zsh scripts/verify-tftmac.command ``` -The builder verifies all APK hashes, compiles the SwiftUI app and emulator host, -copies the pinned Sparkle framework and runtime template, rejects embedded -developer paths, signs nested code from the inside out with an ad-hoc identity, -verifies the app, and creates the DMG in `dist/`. - -The resulting DMG embeds the four verified game APK splits. - -## Provisioning integration test - -Build the app first, close every Android Emulator process, then run: +Local installed/runtime/signing validation is separate and currently expected +to report the signing blocker: ```sh -./scripts/integration-test-mactician.command +/bin/zsh scripts/verify-installed-runtime.command ``` -The script downloads and verifies the three large Android fixtures, compiles -`InstallerIntegration.swift`, provisions a temporary SDK/AVD, and validates the -ready state. It is deliberately excluded from normal pull-request CI. +Validation checks: -## Optional Developer ID and notarized build +- TFTMAC bundle identity; +- frozen EmulatorController protocol provenance/hash; +- pinned Swift package graph; +- retained script and JavaScript syntax; +- performance-lab and engineering-map self-tests; +- native Release build; +- native tests; +- Git whitespace integrity; +- no tracked private runtime artifacts. -Store notarization credentials with `notarytool` under a Keychain profile, then -export the identity/profile in the invoking environment: +A transient `TFTMAC_FORBIDDEN_TOKEN` may be supplied for the final ownership-completeness scan. The token itself must not be committed merely to test for its absence. -```sh -PROJECT_DIR="$PWD" -: "${MACTICIAN_CODESIGN_IDENTITY:?Set MACTICIAN_CODESIGN_IDENTITY in the environment}" -: "${MACTICIAN_NOTARY_PROFILE:?Set MACTICIAN_NOTARY_PROFILE in the environment}" -TFT_GAME_APK_DIR="$PROJECT_DIR/private/tft-pbe-apks" \ - ./scripts/build-mactician.command -``` +## Runtime data + +Do not commit SDK packages, AVD userdata, Google/Riot credentials, tokens, APKs, runtime disks, logs containing sensitive data, or generated native build products. -The script requires a Developer ID Application identity when either public -release setting is present. It enables hardened runtime, uses secure timestamps, -submits the DMG, waits for acceptance, staples and validates the ticket, and -assesses the distribution. - -The initial v1 release remains ad-hoc signed; this section documents the future -Developer ID-capable path rather than the current release identity. - -## Environment variables - -| Variable | Purpose | -| --- | --- | -| `TFT_GAME_APK_DIR` | Required build input directory containing four pinned APK splits | -| `MACTICIAN_CODESIGN_IDENTITY` | Developer ID Application identity; default `-` is ad hoc | -| `MACTICIAN_NOTARY_PROFILE` | notarytool Keychain profile for a public release | -| `TFT_ANDROID_SDK_ROOT` / `TFT_ROOT_SDK` | Explicit Android SDK for source launch scripts | -| `ANDROID_SDK_ROOT`, `ANDROID_HOME` | Standard Android SDK discovery fallbacks | -| `TFT_ADB`, `TFT_EMULATOR` | Explicit tool binaries | -| `TFT_AVD_HOME`, `TFT_ROOT_AVD_HOME`, `TFT_AVD_NAME` | External AVD selection | -| `TFT_JQ` | Non-standard `jq` path | -| `MACTICIAN_KEYCHAIN_SERVICE` | Optional login-helper Keychain service | -| `MACTICIAN_SPARKLE_ACCOUNT` | Sparkle Ed25519 Keychain account for appcast generation | -| `MACTICIAN_UPDATE_*` | Release URLs, SSH destination, remote root, work directory, and inputs | - -## Common failures - -- **Sparkle download/hash failure:** remove only the partial cache file and - retry on a trusted network; do not change the pinned hash to match a download. -- **Missing APK input:** set `TFT_GAME_APK_DIR`; the repository intentionally - does not contain game packages. -- **APK mismatch:** use the exact pinned release or update the manifest only as - part of a separately verified game-version change. -- **No signing identity:** use the default ad-hoc mode for local testing or - install the intended Developer ID identity and set its exact reported name. -- **Module-cache or stale output issue:** remove ignored `launcher/.build/` or - `dist/` output, then rerun; source files are unaffected. -- **Integration test refuses to start:** close other Emulator processes to avoid - shared host/ADB state. +The normal runtime root is `/Volumes/MAC MINI M4/TFTMAC/Runtime`. The application must not silently create bulk runtime/build state on the internal disk when the required external runtime authority is unavailable. diff --git a/docs/history/2026-08-31-pre-build8/TFTMAC.md b/docs/history/2026-08-31-pre-build8/TFTMAC.md new file mode 100644 index 0000000..4b78d60 --- /dev/null +++ b/docs/history/2026-08-31-pre-build8/TFTMAC.md @@ -0,0 +1,54 @@ +# TFTMAC + +TFTMAC runs the official live Teamfight Tactics Android client on Apple-silicon Macs using a local high-end Android tablet runtime. Riot's game binary is not modified, mirrored, or committed to Git. + +## Current working profile + +- Runtime: Google Play ARM64 Android 16 / API 36 +- Game package: `com.riotgames.league.teamfighttactics` +- Render target: **1920×1080** +- Density: **280 DPI** (617dp tablet-class short side) +- Guest resources: **8 vCPU / 8 GB RAM** +- GPU: host accelerated +- OpenGL ES: **3.2** (`196610`) +- EGL: **ANGLE** +- Vulkan: **1.3**, backed by Apple M4 through the Android emulator graphics stack +- Fallback: Compatibility 1080p without the enhanced ES 3.2 boot flags + +The installed application is `/Applications/TFTMAC.app`. Its persistent Android runtime is stored under `~/Library/Application Support/TFTMAC`. + +## Use + +Open **TFTMAC** from Applications. The launcher starts the high-end tablet runtime and opens live TFT automatically. + +- **Enhanced 1080p** is the default renderer profile. +- **Compatibility 1080p** is the fallback if a future Android/Riot update rejects the enhanced renderer flags. +- **Google Play / Update** opens Riot's official TFT listing inside the Android runtime. +- **Stop Android** cleanly shuts down the virtual tablet. + +Google credentials are entered only inside Google Play. Riot credentials are entered only inside TFT. + +## Trust boundary + +TFTMAC keeps the acquisition and game runtime local: + +1. Google Play installs Riot's official package. +2. The acquisition helper verifies Google Play installer authority and records SHA-256 evidence locally. +3. Riot APKs, Android userdata, credentials, and AVD disks remain under ignored/protected local paths and are never committed. +4. The Mac launcher controls emulator lifecycle, display geometry, resource allocation, and graphics capability only. + +## Development gates + +```text +node tools/clara-task.mjs test +node tools/clara-task.mjs build +node tools/clara-task.mjs acceptance +``` + +`acceptance` installs the runtime under Application Support, installs `TFTMAC.app` into Applications, launches it, and requires live TFT to start with the enhanced 1080p contract before succeeding. + +## Unreal transition + +The original retired donor PBE implementation is intentionally preserved as donor/reference code. When Riot's live Android client exposes Unreal runtime evidence, TFTMAC can activate a separately validated `UnrealEnhancedAdapter` using the donor ANGLE/MoltenVK/DeviceProfiles research. Until then, the working native live path remains isolated and is never replaced by an unproven Unreal overlay. + +See `docs/TFTMAC_GRAPHICS_ARCHITECTURE.md` for the graphics and fallback design. diff --git a/docs/history/2026-08-31-pre-build8/TFTMACAPP.md b/docs/history/2026-08-31-pre-build8/TFTMACAPP.md new file mode 100644 index 0000000..60fae07 --- /dev/null +++ b/docs/history/2026-08-31-pre-build8/TFTMACAPP.md @@ -0,0 +1,2043 @@ +# TFTMACAPP.md — Native macOS TFTMAC Full Implementation Plan + +**Status:** AUTHORITATIVE NATIVE-APP IMPLEMENTATION PLAN +**Project:** TFTMAC +**Target:** Apple Silicon / Mac mini M4 +**Product:** One native macOS application containing the complete Android/TFT user experience +**Primary workload:** Current official Google Play Teamfight Tactics client +**Presentation target:** Full 1920×1080 Android framebuffer, native Mac window and native macOS fullscreen +**Performance target:** Stable 60-Hz-class presentation with truthful FPS/frame telemetry +**Architecture rule:** Preserve the proven stock Android Emulator graphics/runtime path; replace only the user-facing wrapper/presentation/control layer unless measurement proves a deeper runtime blocker. + +--- + +## 0. Executive implementation directive + +Build **one real native Mac application, `TFTMAC.app`**, that launches the proven Android runtime invisibly, embeds the full Android display directly inside its own AppKit window, accepts normal Mac mouse/keyboard input, provides all normal emulator/device controls natively, manages Google Play/TFT lifecycle, exposes approved runtime settings, displays and records truthful FPS, keeps the continuous raw logger alive from before emulator launch through stop, seals raw data immediately at shutdown, and can optionally connect to legitimate Riot League Voice through the official League Client if Riot permits concurrent authentication. + +This file supersedes the old production direction based on: + +- a visible QEMU/Qt Android Emulator window; +- window repositioning, cropping, overlays, or Accessibility automation; +- `-scale` tricks to make the emulator fit the Mac screen; +- source-built AEMU as the normal critical path; +- API 37 as the required production guest; +- custom ANGLE as normal production work; +- scrcpy, video encoding, screenshots, or software streaming as the gameplay display path; +- the retired donor's hosted TFT feed/updater/orchestration; +- third-party Riot APK distribution; +- per-match telemetry as the primary performance model. + +The current working stock runtime is the control. The native app must first reproduce it unchanged, then prove that embedding does not materially degrade it. + +--- + +# 1. Product definition + +The shipping user experience is: + +```text +Finder / Dock + -> TFTMAC.app + -> one normal native macOS window + -> full Android/TFT display inside the window + -> native toolbar / menus / optional Device Controls inspector + -> native Settings + -> live FPS overlay + -> official Google Play / Riot sign-in surfaces inside the embedded Android display + -> native macOS fullscreen +``` + +The user must never need to interact with a separate Android Emulator window in normal operation. + +The final product is not a skin around QEMU. QEMU remains the VM process, but TFTMAC owns the presentation, lifecycle, controls, telemetry, state, recovery, and native Mac UX. + +--- + +# 2. Current known-good runtime authority + +The native app begins from the runtime that has already completed real official TFT games on this machine. + +## 2.1 Host + +```text +Machine: Mac mini Mac16,10 +Chip: Apple M4 +Architecture: arm64 +Unified memory: 16 GB +macOS: 26.6.2 +Build: 25G83 +Authoritative Xcode: /Users/flash/Downloads/Xcode.app +Xcode: 26.6 / 17F113 +``` + +Do not allow a stale `/Applications/Xcode.app` selection to control production builds. Native build scripts set: + +```text +DEVELOPER_DIR=/Users/flash/Downloads/Xcode.app/Contents/Developer +``` + +unless machine-state discovery proves that this exact installation has intentionally changed. + +## 2.2 Android runtime + +```text +Bulk runtime root: /Volumes/MAC MINI M4/TFTMAC/Runtime +Android Emulator: 37.1.11.0 +Emulator build: 15917651 +ADB protocol: 1.0.41 +AVD: TFT_Ultra_Tablet +Guest family: official Google Play ARM64 API 36 +Guest display: 1920x1080 +Density: 320 dpi +Refresh target: 60 Hz +CPU baseline: 6 cores +Guest RAM baseline: 5120 MB / 5.0 GB +GPU mode: host +Audio backend: CoreAudio +Graphics transport: virtio-gpu-asg / gfxstream +ASG write buffer: 1 MiB +ASG write step: 16 KiB +ASG data ring: 32 KiB +ASG draw flush baseline: 800 +``` + +Current graphics execution path: + +```text +TFT / Unreal GameActivity + -> ANGLE + -> guest Vulkan/ranchu + -> virtio-gpu-asg / gfxstream + -> host Vulkan + -> MoltenVK + -> Metal + -> Apple M4 +``` + +Current compatibility adapter required by the proven build: + +```text +ANGLE_FEATURE_OVERRIDES_ENABLED= + exposeNonConformantExtensionsAndVersions:exposeES32ForTesting +``` + +This is recorded truthfully as a compatibility adapter. The native-app project does **not** reopen source-AEMU/custom-ANGLE work merely to remove it. A future conformance project may replace it only after the native product is stable and measurement proves that work is necessary. + +## 2.3 Current official TFT package + +Observed working package: + +```text +applicationId/package: com.riotgames.league.teamfighttactics +versionName observed: 18.1-5392842 +versionCode observed: 8392842 +installer observed: com.android.vending +launch activity: com.epicgames.unreal.GameActivity +``` + +The observed version is not permanently pinned. Google Play remains package/update authority. Every launch re-queries package state. + +The signing certificate digest has not yet been promoted as verified project authority. Native-app implementation must capture and record the Google-delivered signer digest before release rather than inventing or assuming it. + +## 2.4 Current performance decisions + +```text +Guest RAM 5.0 GB: KEEP as current development baseline +Guest RAM 4.0 GB: not the default; do not force a further RAM cut +CPU: 6 cores baseline +TFT graphics: High baseline +TFT FPS cap: 60 baseline +TFT Performance Mode (Beta): OFF baseline +Ultra High graphics: REJECTED on current stack for usability; user observed unplayable lag +ASG draw flush 800: baseline +ASG draw flush 400: experimental until controlled evidence promotes it +``` + +Do not silently default the app to Ultra High or Riot Performance Mode Beta. High/60/OFF is the current user-confirmed playable in-game reference until better measured evidence exists. + +--- + +# 3. Non-negotiable architecture rules + +1. **Stock Google Android Emulator remains the normal runtime.** No mandatory source-built AEMU tree. +2. **Retired donor evidence remains untouched as donor/control evidence.** Do not make the shipping app depend on retired donor servers, feed, updater, or orchestration. +3. **Official Google Play/Riot package only.** Do not mirror, repack, patch, or re-sign Riot binaries. +4. **The Android source framebuffer stays 1920×1080.** Native window resizing changes presentation size, not guest resolution. +5. **No visible Qt/QEMU UI in production.** The emulator runs hidden. +6. **No scrcpy/video encode path.** Use EmulatorController raw frame transport and Metal. +7. **No fake fullscreen.** Use ordinary AppKit fullscreen/Spaces. +8. **No Accessibility permission for normal operation.** Input goes through EmulatorController/ADB fallbacks. +9. **Logger starts before emulator.** TFT does not launch if the critical logger gate cannot initialize. +10. **Continuous run is the performance source of truth.** Matches and setting changes are timestamped annotations. +11. **Raw telemetry is sealed before SQL/post-processing.** Analysis can fail without losing the run. +12. **One-variable experimentation.** No combinatorial tuning runs unless evidence requires them. +13. **No source-level zero-copy presentation bridge unless the simple MMAP/Metal path fails the measured performance gate.** +14. **League Voice cannot bypass Riot authentication or entitlement.** It is a separate conditional module and cannot block the core app release. + +--- + +# 4. Production build system + +The current ad-hoc `swiftc` wrapper build is not sufficient for the final embedded app. + +Create a normal Xcode project: + +```text +TFTMAC.xcodeproj +Product: TFTMAC.app +Architecture: arm64 +Minimum macOS: 15.0 +Swift language mode: Swift 6.x +Primary UI: AppKit +Optional hosted UI: SwiftUI where it reduces code, not for window control +Renderer: Metal / MetalKit +Distribution: Developer ID direct distribution +``` + +Why macOS 15.0: the maintained gRPC Swift 2 ecosystem is the intended client stack and currently targets modern macOS. The actual target machine is much newer, so lowering deployment compatibility is not worth complicating the embedded-control build. + +## 4.1 Dependencies + +Use maintained Swift packages only: + +```text +gRPC Swift 2.x +gRPC Swift NIO transport 2.x +gRPC Swift Protobuf 2.x +SwiftProtobuf 1.x +``` + +Implementation procedure: + +1. Resolve versions compatible with Xcode 26.6 and the vendored emulator proto. +2. Pin exact versions and checksums in `Package.resolved`. +3. Never ship floating `main`/branch dependencies. +4. Commit `Package.resolved`. +5. Treat dependency update as an explicit maintenance change, not an automatic build-time upgrade. + +## 4.2 Emulator protocol source + +Vendor the exact compatible EmulatorController protocol source: + +```text +Vendor/AndroidEmulator/emulator_controller.proto +Vendor/AndroidEmulator/SOURCE.json +Generated/EmulatorController/*.swift +``` + +`SOURCE.json` records: + +```text +installed emulator version/build +upstream source URL/revision +proto SHA-256 +generator versions +generated-source SHA-256 +``` + +Generate the Swift protobuf/gRPC client once through a reproducible script and commit the generated code. Normal end-user builds must not depend on downloading Android Emulator source or running an uncontrolled code generator. + +Required RPC capability set to prove against Emulator 37.1.11: + +```text +status / getStatus +raw screenshot/frame streaming +single screenshot +keyboard input +mouse/touch input +wheel input if supported +VM state / restart / shutdown +microphone state where exposed +optional notifications/device-control RPCs needed by Advanced Controls +``` + +Exact RPC names follow the vendored proto, not remembered examples. + +--- + +# 5. Repository structure + +Keep the app modular without creating a forest of frameworks. + +Use one production app target plus tests, one small developer probe target, and generated protocol code. + +```text +TFTMAC.xcodeproj +TFTMAC/ + App/ + TFTMACApplication.swift + AppCoordinator.swift + AppState.swift + MenuBuilder.swift + MainWindowController.swift + Runtime/ + RuntimeLocation.swift + RuntimeProfile.swift + RuntimeSettingDefinition.swift + RuntimeProfileStore.swift + RuntimeController.swift + EmulatorProcessController.swift + AndroidBootVerifier.swift + Emulator/ + EmulatorDiscoveryService.swift + EmulatorControllerClient.swift + EmulatorConnectionState.swift + FrameStreamSession.swift + Presentation/ + EmbeddedEmulatorView.swift + MetalFramePresenter.swift + FrameRing.swift + ViewportMapper.swift + FPSOverlayView.swift + Input/ + AndroidInputRouter.swift + KeyboardMapper.swift + PointerMapper.swift + DeviceControls/ + DeviceActionService.swift + DeviceControlsInspector.swift + AdvancedControlsController.swift + Package/ + PackageStateManager.swift + TFTPackageState.swift + PlayStoreCoordinator.swift + Telemetry/ + CaptureManager.swift + CaptureSeal.swift + FPSAccumulator.swift + HostSampler.swift + AndroidSampler.swift + SurfaceFlingerSampler.swift + TelemetryNormalizer.swift + Voice/ + LeagueVoiceProvider.swift + LeagueClientVoiceProvider.swift + LeagueVoiceState.swift + Resources/ +Generated/ + EmulatorController/ +Vendor/ + AndroidEmulator/ +Probes/ + EmbeddedControlProbe/ +Tests/ + TFTMACTests/ + TFTMACUITests/ +scripts/ + generate-emulator-proto.command + build-native-app.command + test-native-app.command + notarize-native-app.command +ssot/ + native-app-probe-results.json + native-app-release-checklist.md +``` + +Do not recreate a monolithic `TFTMACWindowCoordinator` that owns every responsibility. + +Existing Node tools remain diagnostic/control oracles during migration, but the final user-facing runtime must not require Node for display, input, normal lifecycle, settings, or telemetry capture. + +--- + +# 6. Native application state machine + +The app state must describe what the user can actually do. + +```text +BOOTSTRAPPING + -> EXTERNAL_RUNTIME_MISSING + -> RUNTIME_NEEDS_REPAIR + -> READY_TO_START + +READY_TO_START + -> STARTING_CAPTURE + -> STARTING_EMULATOR + -> ANDROID_BOOTING + -> ANDROID_LOCKED_OR_SETUP_REQUIRED + -> GOOGLE_PLAY_SIGNIN_REQUIRED + -> TFT_INSTALL_REQUIRED + -> TFT_UPDATE_REQUIRED + -> TFT_PATCHING_OR_INITIALIZING + -> READY_TO_PLAY + +READY_TO_PLAY + -> LAUNCHING_TFT + -> PLAYING + +PLAYING + -> RESTARTING_TFT + -> APPLYING_ANDROID_PROFILE + -> STOPPING + -> LOGGER_FAULT + +STOPPING + -> SEALING_CAPTURE + -> READY_TO_START +``` + +Error states expose one specific recovery action. Do not terminate with a generic failure when the package is missing, Play sign-in is needed, Android is locked, or Riot is patching. + +--- + +# 7. Runtime root and storage behavior + +Bulk runtime authority remains: + +```text +/Volumes/MAC MINI M4/TFTMAC/Runtime +``` + +Small native state remains: + +```text +~/Library/Application Support/TFTMAC/ + Profiles/ + State/ + Captures/ + Diagnostics/ + Logs/ + Rollback/ +``` + +Large capture traces may be stored on the external volume with an index under Application Support. + +Rules: + +- if `/Volumes/MAC MINI M4` is not mounted, do not start the emulator; +- never silently build a second SDK/AVD on the internal disk; +- verify the runtime root and AVD before launch; +- preserve the existing runtime instead of reinstalling it on every app build; +- first-run bootstrap only installs/repairs missing official components; +- Android SDK licenses are accepted through the normal Google tooling/user agreement path; +- Riot APKs and private AVD userdata never enter Git or the app bundle. + +--- + +# 8. Hidden Android Emulator launch + +Normal production launch is the current proven runtime plus embedded-control flags. + +Required additional flags/direction: + +```text +-qt-hide-window +-grpc-use-token +-idle-grpc-timeout 300 +``` + +Use isolated ADB: + +```text +ANDROID_ADB_SERVER_PORT=5038 +``` + +Preserve the current renderer/audio/CPU/RAM/display/compatibility arguments from the active RuntimeProfile. + +Do not rely on: + +```text +-scale +emulator-user.ini window placement +Finder desktop dimensions +Accessibility window APIs +off-screen QEMU positioning +cover windows / fake chrome +``` + +The app tracks the emulator process directly by PID and owns its lifecycle. + +## 8.1 gRPC discovery and authentication + +On each launch: + +1. Locate the registration/discovery record belonging to the new emulator process. +2. Read the actual local gRPC endpoint. +3. Read the token into memory only. +4. Determine the exact authentication-header form required by the installed emulator. +5. Call status/getStatus. +6. Verify the endpoint belongs to the expected emulator PID/AVD. +7. Store neither token nor auth header in logs, SQL, crash metadata, or profiles. +8. Discard credentials when the emulator terminates. + +A hidden emulator with no authenticated controller connection is a hard architecture failure. Do not continue building product UI around it. + +--- + +# 9. Android boot and Google Play preflight + +After process start, native TFTMAC verifies: + +```text +ADB serial present +boot_completed=1 +user 0 unlocked or actionable lock/setup state shown +1920x1080 display +320 dpi +expected CPU count +expected guest memory range +Google Play Store package present +Google Play Services present +network active +host/guest clock reasonably synchronized +automatic time enabled +automatic timezone enabled +``` + +Date/time is a first-class Play preflight because Play installation/auth failures can be caused by clock drift. + +Before opening Play installation/sign-in: + +- enable Android automatic time/time-zone settings when permitted; +- compare host UTC with guest UTC; +- if the guest is materially wrong, repair through the normal emulator/Android time path or restart the guest; +- do not proceed with Play account flow while the clock is obviously wrong. + +Do not automate Google credentials, CAPTCHA, consent, MFA, or Riot credentials. + +--- + +# 10. Official TFT package lifecycle + +`PackageStateManager` is a required native service. + +States: + +```text +MISSING +PLAY_AVAILABLE +INSTALLING +INSTALLED_UNKNOWN_VERSION +INSTALLED_VERIFIED +UPDATE_AVAILABLE +PATCHING_OR_INITIALIZING +READY_FOR_LAUNCH +PACKAGE_DAMAGED_OR_INCOMPLETE +``` + +Every package observation captures: + +```text +package/applicationId +versionName +versionCode +installer package +base APK path +split APK paths +first install time +last update time +launch activity +base/split SHA-256 when readable +signing certificate digest(s) +``` + +Verification rules: + +- package name must be `com.riotgames.league.teamfighttactics`; +- installer must be `com.android.vending` for the production authority path; +- versionName/versionCode are observed from the installed package, not hardcoded forever; +- signing digest is extracted from the actual Google-delivered APK set and stored as package evidence; +- on every update, re-record signer and hashes; +- a signer mismatch is a hard package-integrity warning requiring review; +- never re-sign or modify the package. + +If TFT is missing or out of date, TFTMAC opens the official Google Play surface **inside the embedded Android display**. The user signs into Play normally there. + +Once TFT is installed, TFTMAC launches the official activities and allows Riot's own patch/init flow to complete. Riot login remains inside the official game surface. + +--- + +# 11. Full embedded display architecture + +The user-facing Android display is a native `MTKView` or equivalent Metal-backed AppKit view. + +```text +Android Emulator hidden Qt UI + -> EmulatorController raw frame stream + -> MMAP/shared-memory source + -> bounded host copy + -> 2/3-frame Metal texture ring + -> MTKView + -> TFTMAC NSWindow +``` + +Initial stream request: + +```text +display: 0 +width: native / 0 where proto means native +height: native / 0 where proto means native +format: raw RGBA8888 or exact native raw format exposed by proto +transport: MMAP/shared memory +``` + +The app verifies the received source is 1920×1080 landscape. + +Do not use PNG, H.264, VP8, scrcpy, periodic screenshots, or Android software encoding as the primary display. + +## 11.1 Why the first implementation uses one copy + +A 1920×1080 RGBA frame is about 8.3 MB. Shared-memory producers can overwrite their region while a client is rendering. The safe first implementation copies each new complete frame into a bounded Metal presentation ring. + +This gives deterministic ownership and prevents half-written frames/tearing. + +Only if measured embedded-vs-direct A/B proves this single copy materially harms performance may the project activate a source-level IOSurface/zero-copy adapter. Do not build that preemptively. + +## 11.2 Metal presenter + +`MetalFramePresenter` must: + +- detect each new source sequence number; +- copy/swap into an available presentation buffer without blocking the frame-receive loop; +- use a bounded 2- or 3-buffer ring; +- present only complete new frames; +- retain source sequence/timestamp metadata with the presented frame; +- record receive, submit, and present/completion monotonic timestamps; +- avoid unnecessary color conversions; +- preserve normal sRGB-looking output unless measured source metadata requires another color space; +- never perform heavy telemetry/SQL work on the render thread. + +--- + +# 12. Full-size window, fit, 100%, and fullscreen behavior + +The source Android display always stays 1920×1080. + +Main `NSWindow` style: + +```text +.titled +.closable +.miniaturizable +.resizable +.fullSizeContentView +``` + +Required behavior: + +- real macOS red/yellow/green traffic lights; +- title `TFTMAC`; +- normal Dock identity; +- normal menu-bar identity; +- native unified/compact toolbar; +- `.fullScreenPrimary` collection behavior; +- green traffic-light fullscreen works; +- `View > Enter Full Screen` works; +- `Control-Command-F` works; +- no fake fullscreen or menu-bar/Dock hiding code. + +Display modes: + +```text +FIT + Aspect-fit the entire 1920x1080 source into available content. + Never crop or stretch. + +100% + One source pixel equals one view pixel. + If the window is smaller than 1920x1080, use a native scroll container rather than scaling/cropping silently. + +FULLSCREEN + Keep source at 1920x1080. + Aspect-fit into the native fullscreen content area. + On a 16:9 display, fill the complete content area without Qt/QEMU chrome. +``` + +Letterboxing is allowed only when the Mac content region is not 16:9. + +--- + +# 13. Native input system + +Normal gameplay input must not depend on fixed Android screen coordinates. + +Input mapping: + +```text +Mac event point + -> EmbeddedEmulatorView local point + -> actual displayed-image rectangle + -> normalized source coordinates + -> Android 1920x1080 coordinates + -> EmulatorController input RPC +``` + +Required input: + +```text +left click / touch down-up +drag / touch move +mouse movement where useful +wheel/scroll +keyboard keys +text entry +Back +Home +Overview +``` + +Rules: + +- ignore letterbox regions for Android touch; +- preserve Mac command shortcuts (`Command-*`) for TFTMAC menus; +- send normal unhandled gameplay keys to Android; +- keep input ordering deterministic; +- do not block input while telemetry writes; +- gRPC is primary for pointer/key transport where supported; +- isolated ADB is allowed as a service-control fallback, not as brittle coordinate automation. + +--- + +# 14. Native toolbar, menus, and Device Controls inspector + +The game gets the full content width by default. All emulator functionality remains available through native controls. + +Primary toolbar: + +```text +Back +Home +Overview +Device Controls toggle +Volume/Mute +Screenshot +Restart TFT +FPS indicator +League Voice +``` + +The Device Controls inspector opens on the right when requested and may contain: + +```text +Power / Wake +Back +Home +Overview +Rotate Left +Rotate Right +Volume Up +Volume Down +Mute +Screenshot +Fit +100% +Restart TFT +Restart Android +Pause/Resume VM where supported +Advanced Emulator Controls +``` + +When the inspector is visible, the Android view aspect-fits into the remaining content. It is never stretched. + +Application menus: + +```text +TFTMAC + About TFTMAC + Settings... + Quit TFTMAC + +File + Screenshot + Open Capture Folder + +View + Fit + 100% + Enter Full Screen + Show FPS Overlay + Show Device Controls + +Device + Power / Wake + Back + Home + Overview + Volume Up + Volume Down + Mute + Rotate Left + Rotate Right + Restart TFT + Restart Android + Advanced Emulator Controls... + +Voice + League Voice + Disconnect Voice + +Help +``` + +Every visible control receives an explicit release test. No button may silently do nothing. + +--- + +# 15. Advanced emulator controls + +Do not recreate Google's Qt Extended Controls UI pixel-for-pixel. Recreate the **functional capabilities** the native app may need. + +`AdvancedControlsController` capability-discovers the current EmulatorController and exposes supported controls such as: + +```text +location/GPS +battery state +cellular/network simulation +microphone state +clipboard +camera/sensor status where exposed +VM pause/resume/restart +additional display/device state available in the exact installed proto +``` + +For each action: + +1. Prefer the supported EmulatorController RPC. +2. Use a deterministic ADB fallback only if it is safer and already proven. +3. Hide/disable capabilities not exposed by the installed emulator rather than presenting dead controls. +4. Never require the hidden Qt toolbar. + +--- + +# 16. Runtime profile system + +Runtime tuning is a product feature, not a source-edit workflow. + +Persistent profiles live in: + +```text +~/Library/Application Support/TFTMAC/Profiles/ +``` + +Each `RuntimeProfile` is canonical JSON with a deterministic SHA-256 config hash. + +Minimum fields: + +```text +profileVersion +name +cpuCores +ramMB +guestWidth +guestHeight +densityDpi +refreshHz +gpuMode +audioBackend +graphicsTransport +asgWriteBuffer +asgWriteStep +asgDataRing +asgDrawFlushInterval +angleCompatibilityFeatures +configHash +``` + +Each session writes an immutable copy as `runtime-profile.json`. + +## 16.1 Declarative settings registry + +Each `RuntimeSettingDefinition` contains: + +```text +id +label +group +value type +allowed values/range +default +unit +apply mode +restart requirement +runtime mapping +validation rule +safety class +log key +help text +``` + +Apply modes: + +```text +LIVE +RESTART_TFT +RESTART_ANDROID +NEXT_SESSION +``` + +Safety badges: + +```text +BASELINE +VALIDATED +EXPERIMENTAL +UNSUPPORTED +``` + +## 16.2 CPU cores + +Expose exactly the values implemented by the current native profile: + +```text +4, 6, 8 +``` + +Default: `6` +Apply: `RESTART_ANDROID` + +Pre-release validation tests the CPU dimension one value at a time against the baseline profile. Do not test the Cartesian product of every CPU/RAM combination. + +## 16.3 Guest RAM + +Expose exactly the values implemented by the current native profile: + +```text +4.0 +5.0 +6.0 +GB +``` + +Default: `5.0 GB / 5120 MB` +Apply: `RESTART_ANDROID` + +The UI is a validated dropdown; unsupported intermediate values are not silently accepted. + +Pre-release validation smoke-tests each value independently against the baseline CPU/graphics configuration. This proves launch compatibility without requiring nine full TFT matches. + +5.0 GB remains the performance baseline until evidence changes it. + +## 16.4 ASG draw flush interval + +Expose: + +```text +800 — BASELINE +400 — EXPERIMENTAL until promoted +``` + +Apply: `RESTART_ANDROID` + +Do not expose arbitrary ASG values in v1. + +## 16.5 Locked baseline values + +Show but do not make editable in the first release: + +```text +1920x1080 +320 dpi +60 Hz target +host GPU +virtio-gpu-asg +1 MiB ASG write buffer +16 KiB ASG write step +32 KiB ASG ring +CoreAudio +ANGLE compatibility feature set +MoltenVK configuration +``` + +New knobs are added only when measurement names a causal variable. + +## 16.6 Apply & Restart workflow + +When a restart-required value changes: + +1. Save the pending profile. +2. Mark `Restart required`. +3. User selects `Apply & Restart Android`. +4. Record `CONFIG_CHANGE_REQUESTED`. +5. Seal the current raw capture using the bounded shutdown protocol. +6. Stop Android cleanly. +7. Persist new profile and config hash. +8. Start a new logger/capture **before** the new emulator process. +9. Start hidden emulator with the new profile. +10. Verify requested vs observed CPU/RAM/config. +11. Record `CONFIG_CHANGE_APPLIED`. +12. Relaunch TFT when Android/package state is ready. + +One runtime configuration never silently mixes into another capture. + +--- + +# 17. TFT in-game graphics settings + +Known user-facing values: + +```text +Graphics: Low / Medium / High / Ultra High +FPS cap: 30 / 60 / None +Performance Mode (Beta): On / Off +``` + +Current baseline: + +```text +High / 60 / OFF +``` + +Current Ultra High verdict: + +```text +REJECTED for current usability because it produced unplayable lag. +``` + +Do not automate these with blind screen-coordinate taps. + +Implementation sequence: + +1. Inspect logcat and Riot/Unreal runtime output for deterministic setting-change events. +2. Inspect any stable local configuration/preferences interface legally and non-invasively accessible from the Play guest. +3. If a deterministic interface exists, expose the setting through TFTMAC and timestamp every apply automatically. +4. If no stable write interface exists but logcat exposes changes, observe and timestamp them automatically. +5. If neither exists, provide an explicit native `Game Settings Observation` control that records the user's declared current values without pretending TFTMAC applied them. + +Manual in-game setting changes may never be assigned a fabricated timestamp or value. + +--- + +# 18. FPS and presentation telemetry — required product feature + +`gfxinfo` is not the primary FPS source because TFT renders through a native Unreal/Vulkan SurfaceView path. + +The embedded frame stream supplies always-on presentation metrics. SurfaceFlinger/Perfetto calibrates and deepens them. + +Track separately: + +```text +emulator/source frame rate +frame stream delivery rate +Metal presented FPS +sequence gaps / stream drops +source frame intervals +source-to-receive latency +receive-to-submit latency +submit-to-present latency +source-to-present latency +Mac display refresh rate +``` + +## 18.1 User-visible FPS + +Default HUD: + +```text +FPS 59.8 +``` + +Default user number = distinct **Metal presented FPS**, because this is what the user actually sees in TFTMAC. + +Expanded diagnostic HUD: + +```text +FPS 59.8 +SRC 60.0 +DROP 0.0% +P95 17.4 ms +``` + +The overlay: + +- is native AppKit/Metal overlay content; +- is not rendered inside Android; +- does not alter guest resolution; +- does not intercept normal gameplay outside its own small bounds; +- is toggleable from `View > Show FPS Overlay`; +- persists the user's preference. + +## 18.2 FPS accumulator + +Every source frame captures: + +```text +source sequence +source timestampUs +host receive monotonic time +Metal submit monotonic time +Metal present/completion monotonic time when available +``` + +Maintain rolling windows: + +```text +1 second +5 seconds +30 seconds +``` + +Do not write one database row per rendered frame during gameplay. + +## 18.3 Raw `fps.jsonl` + +Append one compact record per second: + +```text +utc +host_mono_ns +source_fps_1s +source_fps_5s +presented_fps_1s +presented_fps_5s +source_frame_count +delivered_frame_count +presented_frame_count +stream_dropped_frame_count +stream_dropped_percent +source_frame_interval_mean_ms +source_frame_interval_p95_ms +source_to_present_mean_ms +source_to_present_p95_ms +display_refresh_hz +emulator_seq_first +emulator_seq_last +runtime_config_hash +``` + +## 18.4 Calibration gate + +Before calling emulator stream timestamps the authoritative **game/display source FPS**: + +1. Capture a stable TFT animation interval. +2. Record gRPC source seq/timestamps. +3. Simultaneously capture bounded SurfaceFlinger/FrameTimeline Perfetto evidence. +4. Compare frame count/timing. +5. Repeat during known heavy/stutter gameplay. + +If the streams track closely, source FPS is promoted. If not, SurfaceFlinger remains source/display-production authority and the gRPC source number is labeled transport/source-stream FPS. + +Do not lie with labels. + +--- + +# 19. Continuous raw telemetry architecture + +One emulator/logger start-to-stop interval is one authoritative continuous performance run. + +Matches, wins, placements, TFT restarts, quality reports, graphics changes, FPS-cap changes, Performance Mode changes, and traces are annotations inside that continuous timeline. + +No match-start marker is required for the run to be useful. + +Required capture root: + +```text +~/Library/Application Support/TFTMAC/Captures// +``` + +Required or attempted raw artifacts: + +```text +session.json +runtime-profile.json +runtime-state.json +package-state.json +renderer-state.json +clock-sync.jsonl +host-events.jsonl +markers.jsonl +fps.jsonl +embed-present.jsonl +host-process.jsonl +host-memory.jsonl +surfaceflinger/counters.jsonl +logcat.raw.txt +logcat.filtered.txt +emulator.stdout.log +emulator.stderr.log +perfetto/ # bounded traces when requested/needed +capture.seal.json +manifest.sha256 # post-seal integrity pass +``` + +Critical always-on streams: + +```text +FPS/presentation +host process/CPU/RSS +host memory/compression/swap/pageouts +Android/logcat +SurfaceFlinger miss counters +clock sync +runtime events +``` + +The native app owns enough telemetry directly that a played session cannot become totally valueless because a separate analysis command failed. + +--- + +# 20. Logger gate and fault behavior + +Before TFT launch, `CaptureManager` must prove: + +```text +session created +runtime profile frozen +host sampler advancing +host memory sampler advancing +frame/FPS telemetry ready once display stream starts +logcat collector available once Android is ready +capture directory writable +``` + +If the critical gate fails before gameplay, do not launch TFT. + +During gameplay: + +- restart a failed non-render sampler automatically once; +- record `LOGGER_STREAM_RESTARTED`; +- keep native FPS/presentation recording independent of logcat/process samplers; +- surface `LOGGER DEGRADED` if a critical stream cannot recover; +- never destroy the game merely because post-processing is unavailable. + +--- + +# 21. Bounded raw-first stop/seal protocol + +Shutdown must be fast and deterministic. It must never recursively inventory the Android SDK/AVD before protecting gameplay data. + +When Stop/Quit/Apply-Restart is requested: + +1. Record `STOP_REQUESTED` and final monotonic timestamp. +2. Stop accepting new experiment annotations. +3. Stop/flush critical telemetry producers. +4. `fsync`/close raw files where practical. +5. Write `session.json` final raw state. +6. Atomically write `capture.seal.json` containing: + - session id; + - start/end timestamps; + - exact runtime config hash; + - package identity; + - immutable raw file list; + - final byte counts; + - capture state `RAW_SEALED`; + - integrity state `PENDING`. +7. Once `capture.seal.json` exists, the raw run is protected and may not be rewritten. +8. Stop/terminate the emulator if the requested action requires it. +9. Compute SHA-256 integrity manifest **after** raw seal at background priority. +10. Normalize/ingest SQLite **after** raw seal. +11. Storage/BOM inventory, compression, long analysis, and report generation are post-processing only. + +A full SDK/AVD recursive size walk is explicitly forbidden on the critical stop path. + +If SHA/SQL/post-processing fails, raw capture remains `RAW_SEALED` and the app writes a secondary error artifact. + +If the app or Mac dies before normal seal, next launch recovers the abandoned `CAPTURING` directory as `RECOVERED_PARTIAL` without modifying existing raw bytes. + +For macOS application termination, use `applicationShouldTerminate` / terminate-later semantics so the app gets a short bounded seal opportunity rather than exiting mid-write. + +--- + +# 22. SQL performance laboratory integration + +Use SQLite as post-session normalized evidence, not as the high-rate capture transport. + +Add/maintain a time-series FPS table logically equivalent to: + +```sql +fps_samples( + session_id, + observed_utc, + host_mono_ns, + source_fps_1s, + source_fps_5s, + presented_fps_1s, + presented_fps_5s, + stream_dropped_frames, + stream_dropped_percent, + source_interval_mean_ms, + source_interval_p95_ms, + source_to_present_mean_ms, + source_to_present_p95_ms, + display_refresh_hz, + config_hash +) +``` + +Index: + +```text +(session_id, host_mono_ns) +``` + +Post-session rollups include: + +```text +source_fps_mean +source_fps_p5 +source_fps_p50 +source_fps_p95 +source_fps_min +presented_fps_mean +presented_fps_p5 +presented_fps_min +percent_time_below_60 +percent_time_below_50 +percent_time_below_30 +stream_drop_percent +source_to_present_p95_ms +longest_sub_30fps_interval_seconds +CPU mean/p95/max +RSS mean/p95/max +host available/compressed/swap +pageout delta/rate +SurfaceFlinger/HWC/GPU miss deltas +ANR/fatal/OOM/restart counts +``` + +Schema migrations are idempotent and tested against the current live performance DB. A normalization/migration failure must never invalidate raw capture sealing. + +--- + +# 23. Deep graphics diagnostics + +Always-on FPS tells us **that** a frame problem exists. It does not always tell us **why**. + +Keep bounded deep diagnostics separate: + +```text +SurfaceFlinger frame/frametimeline/layers +GPU memory +Perfetto process/system stats where needed +host CPU/memory +ASG/gfxstream instrumentation when needed +MoltenVK instrumentation when needed +``` + +Native trace actions remain developer diagnostics, not something every player session must run at maximum intensity. + +No source-built AEMU or custom renderer work is activated until these measurements identify that layer as the first causal blocker. + +--- + +# 24. Audio and microphone + +Core TFT audio remains the current proven explicit backend: + +```text +CoreAudio +``` + +Native app launch must preserve it and record the observed backend. + +Acceptance: + +```text +TFT audible through selected Mac output +no recurring emulator PCM I/O error +no recurring mixer underrun problem +audio survives native display embedding +audio survives fullscreen +audio survives TFT restart +``` + +Android volume controls alter Android audio state; native Mac system volume remains user-controlled by macOS. + +Microphone passthrough is implemented only where required by an Android feature or diagnostics. League Voice uses native/official League Client audio rather than routing voice through the Android guest. + +--- + +# 25. League Voice integration — separate conditional module + +Include a native `League Voice` toolbar/menu location from the beginning, but do not allow it to delay the core TFTMAC app. + +## 25.1 Allowed architecture + +Preferred path: + +```text +TFTMAC League Voice button + -> detect official Riot/League Client on Mac + -> user signs in through Riot's own UI + -> discover local League Client API endpoint + -> keep local credentials only in memory + -> dynamically verify current premade-voice capability + -> ask official League Client/plugin to join the account's legitimate party voice session + -> Riot/Vivox auth/media remains owned by Riot's client +``` + +Never: + +- fake Riot/Vivox tokens; +- hardcode private voice endpoints as permanent authority; +- persist LCU credentials; +- capture League passwords; +- bypass party entitlement; +- work around a Riot same-account concurrent-session restriction. + +## 25.2 Provider isolation + +Define: + +```swift +protocol LeagueVoiceProvider { + func probe() async -> LeagueVoiceCapability + func connect() async throws + func disconnect() async + func setSelfMuted(_ muted: Bool) async throws + func setParticipantVolume(id: String, value: Double) async throws +} +``` + +The rest of TFTMAC depends only on this interface. + +## 25.3 Mandatory feasibility gate + +Prove on the target Mac: + +```text +official League Client available +user authenticates only in Riot UI +local client API discoverable +premade voice endpoints currently exist +same user can remain authenticated in League Client while Android TFT is active +joining voice does not log TFT out +TFT activity does not tear down voice +Mac mic/output work +self mute works +participant mute/volume works +son/other party member on PC can hear user and user can hear them +``` + +The decisive gate is same-account concurrency. + +If Riot rejects concurrent session use: + +```text +League Voice = UNAVAILABLE — RIOT SESSION LIMIT +``` + +Ship TFTMAC without an authentication bypass. + +--- + +# 26. Security, privacy, and credential handling + +TFTMAC may handle local ephemeral control credentials but must not become a credential store. + +Rules: + +```text +Google credentials: only inside official Google Play Android UI +Riot credentials: only inside official Riot/TFT/League Client UI +emulator gRPC token: memory only, never logged +League Client local API token: memory only, never logged +Riot/Vivox voice token: never persist/log +Riot APKs: never commit/bundle/redistribute +AVD userdata: never commit +capture logs: redact obvious auth headers/tokens before filtered diagnostics +``` + +Bind emulator control to local loopback only. + +The native app should use Hardened Runtime and least required entitlements. Do not enable the App Sandbox if it prevents required child-process, external-volume, local gRPC, ADB, or runtime behavior; this is a direct Developer ID application, not a Mac App Store-first product. + +--- + +# 27. Repair and rollback model + +Keep repair simple and targeted. + +User-facing repair actions: + +```text +Repair Runtime +Restart Display Connection +Restart TFT +Restart Android +Open Google Play +Reset Runtime Profile to Baseline +Open Diagnostics +``` + +Runtime repair verifies: + +```text +external root mounted +emulator binary/version present +platform-tools present +AVD exists +Google Play image metadata matches expected family +AVD config is readable +TFT package state +``` + +Do not delete/recreate the AVD automatically because one probe failed. + +Profile rollback: + +- keep `Baseline` immutable; +- keep last-known-good profile; +- if a new restart configuration fails boot/readiness, automatically revert the pending profile and offer `Restart with Last Known Good`; +- unsupported values never silently clamp to a different value. + +Application rollback: + +- retain the last notarized release artifact; +- keep the current direct-window control build available for engineering A/B until native embedding passes release gates; +- the direct-window control is not a second shipping UX after native acceptance. + +--- + +# 28. Native build/sign/notarize/release + +Production output: + +```text +dist/TFTMAC.app +dist/TFTMAC.dmg +``` + +Build script: + +```text +scripts/build-native-app.command +``` + +Requirements: + +- use authoritative Xcode path; +- build arm64 Release; +- preserve/discover the existing TFTMAC bundle identifier rather than inventing a second app identity; +- set semantic app version/build number; +- sign all app executables/frameworks with Developer ID Application; +- Hardened Runtime enabled; +- notarize with Apple notary service; +- staple notarization ticket; +- validate with `codesign`, `spctl`, and launch smoke test; +- install test copy to `/Applications/TFTMAC.app`; +- verify Dock/Finder identity is TFTMAC. + +Do not bundle Google Play system images or Riot game binaries into the DMG. The app uses/bootstraps the user's licensed official Android runtime installation on the external runtime root. + +--- + +# 29. Test strategy + +Avoid a combinatorial test explosion. Test each independent variable against the known-good baseline and reserve full real matches for release-critical paths. + +## 29.1 Unit tests + +Required deterministic tests: + +```text +runtime profile canonicalization/hash +setting allowed values +CPU/RAM conversion +viewport/letterbox coordinate mapping +1920x1080 touch mapping +FPS rolling-window math +sequence-gap detection +latency math +state-machine transitions +capture seal atomicity +abandoned-capture recovery +package metadata parsing +redaction +gRPC discovery parsing +``` + +## 29.2 Integration tests + +```text +hidden emulator boot +controller authentication +status/getStatus +1920x1080 raw stream +10-minute animated frame stream +Metal presentation +mouse/touch round trip +keyboard round trip +Back/Home/Overview +Power/Wake +rotate both directions +volume/mute +screenshot +TFT restart +Android restart +CoreAudio +logger start-before-emulator +raw seal +post-seal SQL +``` + +## 29.3 Runtime settings smoke matrix + +Do not test all combinations. + +RAM dimension: + +```text +4.0 / 5.0 / 6.0 GB +``` + +For each, hold CPU=6 and all other baseline values. Verify Android boots and TFT reaches a stable ready/lobby state without OOM/ANR. + +CPU dimension: + +```text +4 / 6 / 8 +``` + +For each, hold RAM=5.0 GB and all other baseline values. Verify boot/readiness. + +ASG dimension: + +```text +800 / 400 +``` + +Hold CPU=6, RAM=5.0 GB, High/60/OFF. + +Full-match performance validation is required only for the baseline and any candidate being promoted, not every valid dropdown choice. + +## 29.4 Real user acceptance + +At least one complete official TFT match must be played entirely through `TFTMAC.app` with: + +```text +embedded display only +native input +CoreAudio +logger healthy +FPS visible/logging +no visible Qt/QEMU +native fullscreen tested +normal window mode tested +restart controls tested outside active match +raw capture sealed correctly +``` + +--- + +# 30. Performance acceptance contract + +The native embed layer must not become the reason TFT is slower. + +For a stable gameplay measurement: + +```text +warm-up exclusion: first 120 seconds +measurement: next 600 continuous seconds +Average presented FPS >= 58.0 +P95 frame interval <= 20.0 ms +P99 frame interval <= 33.334 ms +janky frame = interval > 33.334 ms +janky frames <= 1.0% +severe stall = interval > 100 ms +severe stalls <= 3 in 600 seconds +no renderer crash +no Vulkan device loss +no recurring graphics error storm +``` + +Embedded-vs-direct control regression gate under equivalent runtime/workload: + +```text +Average/source FPS regression <= 1.0 FPS +P99 frame interval regression <= 3% +jank regression <= 0.25 percentage points +median synthetic input-to-present <= direct control + 5 ms +P95 synthetic input-to-present <= direct control + 8.334 ms +absolute median input-to-present <= 50 ms +absolute P95 input-to-present <= 83.334 ms +``` + +If the simple MMAP/Metal presentation cannot pass this gate, investigate the embed transport first. Do not randomly tune guest RAM, renderer, and graphics settings simultaneously. + +--- + +# 31. Strict implementation gates + +## Gate 0 — preserve the control + +Deliverables: + +```text +record exact current runtime config +retain current direct-window control build for engineering A/B +preserve current telemetry/performance evidence +mark old window-hack production path superseded +``` + +Exit: known-good control remains launchable and recoverable. + +## Gate 1 — Xcode/native project and protocol lock + +Implement: + +```text +TFTMAC.xcodeproj +SwiftPM locks +vendored compatible emulator proto +generated Swift client +basic native NSWindow +unit-test target +EmbeddedControlProbe target +``` + +Exit: clean Release build on authoritative Xcode. + +## Gate 2 — hidden emulator controller probe + +Probe only; no product polish. + +```text +start logger +launch current 5-GB/6-core emulator hidden +locate/authenticate gRPC +get status +verify AVD/runtime +clean shutdown +``` + +Exit: no visible Qt/QEMU and authenticated controller works reliably. + +Stop if this fails. + +## Gate 3 — full 1920×1080 raw display + +```text +MMAP raw stream +bounded copy +Metal ring +MTKView +complete 1920x1080 image +resize/aspect-fit +10-minute TFT foreground animation +``` + +Exit: no recurring corruption, tearing, gRPC disconnect, or renderer change. + +## Gate 4 — native input + +```text +mouse/touch +keyboard +wheel +Back/Home/Overview +``` + +Exit: Android/TFT can be operated without a visible emulator UI. + +## Gate 5 — FPS truth/calibration + +```text +source seq/timestamps +Metal present counters +fps.jsonl +native HUD +SurfaceFlinger/Perfetto calibration +``` + +Exit: displayed FPS label is truthful and raw FPS data advances once/sec. + +## Gate 6 — full native Mac UX + +```text +real traffic lights +native toolbar +native menus +normal resize +FIT/100% +native fullscreen +Device Controls inspector +all side/device controls +advanced controls capability sheet +``` + +Exit: every visible control has a deterministic PASS test. + +## Gate 7 — Play/TFT lifecycle + +```text +clock/time preflight +Play sign-in-needed state +official install/update flow +package applicationId/versionCode/versionName +installer verification +signer digest capture +Riot patch/init state +Riot login surface +TFT restart +``` + +Exit: clean machine/runtime state can get from app launch to official TFT ready without developer terminal intervention except human authentication. + +## Gate 8 — runtime profiles/settings + +```text +Baseline/Custom profiles +config hashes +CPU 4/6/8 +RAM 4.0/5.0/6.0 GB +ASG 800/400 +Apply & Restart +last-known-good rollback +requested-vs-observed verification +``` + +Exit: approved runtime experiments require no source edit. + +## Gate 9 — raw-first logger and SQL + +```text +continuous source-of-truth run +capture seal +post-seal hashes +FPS SQL schema +normalization +forced SQL-failure recovery test +abandoned-capture recovery +``` + +Exit: SQL/post-processing failure cannot lose or delay protection of the raw run. + +## Gate 10 — embedded-vs-direct performance A/B + +Run current baseline through both paths with the same Android/TFT configuration. + +Compare: + +```text +source FPS +presented FPS +frame intervals +stream drops +source-to-present latency +SurfaceFlinger/HWC/GPU misses +CPU/RSS +host compression/swap/pageouts +guest memory +input latency +audio +network stability +logger health +``` + +Exit: native embed passes the frozen regression gate. + +Only now is the direct-window wrapper retired as a product path. + +## Gate 11 — full real-match acceptance + +Complete a real official match entirely in the native app and seal/analyze the run. + +Exit: native TFTMAC is `PLAYABLE_NATIVE`. + +## Gate 12 — League Voice feasibility + +Run only after core app is stable. + +Exit: either `SUPPORTED` with legitimate official-client proof or `UNAVAILABLE — RIOT SESSION LIMIT/NO SUPPORTED INTERFACE` with no bypass attempt. + +## Gate 13 — signing/notarization/release + +```text +Release build +Developer ID signing +notarization +stapling +/Applications launch +release checklist +rollback artifact +``` + +Exit: production-shippable native app artifact. + +--- + +# 32. Release acceptance checklist + +Core native release is not complete until every item below is proven: + +```text +[ ] one Finder/Dock TFTMAC app identity +[ ] real red/yellow/green controls +[ ] normal resizable Mac window +[ ] native fullscreen via green button / Control-Command-F +[ ] no visible QEMU title bar +[ ] no visible Android Emulator toolbar +[ ] no Accessibility permission required +[ ] full Android 1920x1080 display embedded in TFTMAC +[ ] complete game UI visible +[ ] correct aspect ratio in windowed mode +[ ] full content fill on 16:9 fullscreen +[ ] FIT works +[ ] 100% works +[ ] mouse/touch works +[ ] keyboard works +[ ] wheel/scroll works +[ ] Back works +[ ] Home works +[ ] Overview works +[ ] Power/Wake works +[ ] Rotate Left works +[ ] Rotate Right works +[ ] Volume Up works +[ ] Volume Down works +[ ] Mute works +[ ] Screenshot works +[ ] Restart TFT works +[ ] Restart Android works +[ ] implemented Advanced Emulator Controls work +[ ] current Google Play ARM64 image verified +[ ] Android date/time preflight works +[ ] TFT applicationId verified +[ ] installer verified as Google Play +[ ] current versionName/versionCode captured +[ ] signer digest captured and retained as package evidence +[ ] official Play install/update flow works +[ ] Riot login/patch flow works +[ ] CPU 4/6/8 UI works +[ ] RAM 4.0/5.0/6.0-GB UI works +[ ] ASG 800/400 UI works +[ ] profile persistence/hash works +[ ] Apply & Restart seals old capture and starts new logger first +[ ] High/60/OFF baseline represented correctly +[ ] Ultra High remains rejected until new evidence promotes it +[ ] live FPS visible +[ ] fps.jsonl advances once/sec +[ ] stream drops measured +[ ] presentation latency measured +[ ] SurfaceFlinger counters continue +[ ] runtime config/hash attached to every run +[ ] logger starts before emulator/gameplay +[ ] Stop/Quit writes raw seal before post-processing +[ ] no SDK/AVD recursive inventory on critical stop path +[ ] SQL failure cannot invalidate a raw-sealed run +[ ] CoreAudio works through a long session +[ ] no recurring network disconnect caused by embed layer +[ ] complete official TFT match succeeds in native app +[ ] embedded presentation passes direct-control performance gate +[ ] app is Developer ID signed +[ ] app is notarized/stapled +[ ] /Applications/TFTMAC.app launch passes +``` + +League Voice has its own separate release checklist and may remain unavailable without blocking these core requirements. + +--- + +# 33. Explicit stop conditions + +Stop adding layers and diagnose the first broken boundary if any of these occurs: + +```text +hidden emulator cannot authenticate gRPC +frame stream changes guest resolution or renderer path +MMAP source repeatedly corrupts/tears after bounded-copy protection +gRPC display adds material source-FPS/input regression +native presentation requires software video encoding +raw logger becomes dependent on the render/UI thread +SQL/post-processing can delay or prevent raw sealing +Google Play package cannot be verified as official authority +normal device controls require visible Qt UI and no safe gRPC/ADB equivalent exists +runtime-setting application silently changes more than the selected variable +League Voice requires forging/replaying Riot/Vivox authentication +League Voice breaks the active Android TFT session because of Riot session rules +``` + +These are architecture faults, not reasons to add wrapper hacks. + +--- + +# 34. Zen Gate / no-overengineering rules + +The implementation remains deliberately small: + +- one native app; +- one official Google Play AVD; +- one current stock Emulator runtime; +- one embedded display path; +- one bounded frame copy before considering zero-copy; +- one data-driven settings registry; +- one continuous logger architecture; +- one official package authority; +- one optional isolated League Voice provider; +- no source AEMU build unless a measured blocker demands it; +- no custom ANGLE build unless a measured blocker demands it; +- no giant cross-product performance matrix; +- no duplicate runtime on internal storage; +- no second updater/feed/orchestration service; +- no web/Electron shell; +- no fake fullscreen/window management system. + +Every new component must either satisfy a user-visible native-app requirement or own a measured causal failure. + +--- + +# 35. Required implementation artifacts + +Implementation is expected to leave durable evidence, not just code. + +Required artifacts by completion: + +```text +TFTMACAPP.md +TFTMAC.xcodeproj +Package.resolved +Vendor/AndroidEmulator/emulator_controller.proto +Vendor/AndroidEmulator/SOURCE.json +Generated/EmulatorController/*.swift +ssot/native-app-probe-results.json +ssot/native-app-release-checklist.md +ssot/native-app-performance-ab.json +ssot/tft-package-authority.json +ssot/emulator-controller-authority.json +runtime profile schema/version +FPS raw schema/version +SQLite FPS migration +unit/integration/UI tests +dist/TFTMAC.app +dist/TFTMAC.dmg +notarization evidence +``` + +--- + +# 36. Final execution order + +A fresh implementation agent should execute this file from top to bottom with this priority: + +```text +1. Preserve current control. +2. Create normal Xcode app and lock protocol/dependencies. +3. Prove hidden emulator + authenticated controller. +4. Prove full 1920x1080 raw Metal display. +5. Prove native input. +6. Add truthful FPS and calibration. +7. Build complete native window/toolbar/device controls. +8. Integrate Play/TFT lifecycle and package verification. +9. Add runtime profiles/settings. +10. Harden continuous logging and raw-first stop/recovery. +11. Run direct-vs-embedded performance A/B. +12. Complete real native-app TFT match acceptance. +13. Run League Voice feasibility separately. +14. Sign, notarize, install, and release. +``` + +Do not ask the user to choose architecture already resolved by this file. Machine-state discovery should resolve paths, package state, component versions, runtime state, and available controller capabilities automatically. + +The only expected human pauses are official Google/Riot authentication, MFA/CAPTCHA/consent, and real gameplay/subjective quality confirmation when a release acceptance test specifically requires it. + +--- + +# 37. Definition of done + +TFTMAC native app work is done when the user can click **TFTMAC** in Finder/Dock, see one normal native Mac window, use the entire full-resolution Android/TFT experience inside it, enter normal macOS fullscreen, play an official current TFT match with mouse/keyboard and CoreAudio, operate all required emulator/device controls without a second window, change supported runtime resources from native Settings, see truthful live FPS, finish/quit with the raw run immediately protected, and reopen the app without developer-terminal cleanup. + +At that point: + +```text +PLAYABLE_NATIVE = YES +FULL_SIZE_EMBEDDED_DISPLAY = YES +NATIVE_FULLSCREEN = YES +OFFICIAL_PLAY_TFT = YES +CONTINUOUS_FPS_TELEMETRY = YES +SAFE_RAW_CAPTURE_SEAL = YES +RUNTIME_SETTINGS = YES +PRODUCTION_SIGNED/NOTARIZED = YES +LEAGUE_VOICE = SUPPORTED or TRUTHFULLY_UNAVAILABLE, never bypassed +``` + +That is the TFTMACAPP ship target. diff --git a/docs/history/2026-08-31-pre-build8/TFTMAC_CLEAN_OWNERSHIP_PLAN.md b/docs/history/2026-08-31-pre-build8/TFTMAC_CLEAN_OWNERSHIP_PLAN.md new file mode 100644 index 0000000..38982d3 --- /dev/null +++ b/docs/history/2026-08-31-pre-build8/TFTMAC_CLEAN_OWNERSHIP_PLAN.md @@ -0,0 +1,1057 @@ +# TFTMAC Clean Ownership, Runtime Convergence, and Storage Reclamation Plan + +**Status:** AUTHORITATIVE IMPLEMENTATION PLAN +**Project authority:** `flashls1/TFTMAC` +**Target host:** Apple Silicon Mac mini M4 +**Date frozen:** 2026-08-29 +**Scope:** source identity, runtime authority, validation/CI, storage reclamation, redundant-project retirement, and safe continuation of the native application build. + +**Mandatory execution preflight:** `TFTMAC_CLEAN_OWNERSHIP_PREFLIGHT.md` + +> Implementation agents must read the preflight before repository discovery. The preflight contains the exact already-discovered files, symbols, hashes, branch state, storage paths, validation coupling, migration hazards, and KEEP/DELETE/REWRITE/MIGRATE classifications. Do not repeat discovery already recorded there unless its referenced source/hash has changed or the item is explicitly marked REMEASURE. + +--- + +## 0. Executive directive + +TFTMAC becomes one independent product and one independent engineering system. + +The shipping project must contain only TFTMAC-owned product identity, TFTMAC runtime controls, official Google Android Emulator/Google Play integration, Riot's official TFT package/update path, TFTMAC telemetry, TFTMAC native macOS UI, and the minimum source/evidence required to build and maintain those functions. + +The legacy donor product is no longer a runtime dependency, launcher, control application, update authority, branding source, test authority, CI authority, project identity, or installed dependency. Transferable facts that TFTMAC has independently reproduced may remain as TFTMAC facts under TFTMAC naming. Legacy implementation material that is not required by the working architecture is removed. + +The abandoned source-built emulator laboratory is also retired from the normal TFTMAC system. The working product uses the released Google Android Emulator and the exact installed EmulatorController protocol. The large development-source tree created by earlier experiments is reclaimed after safety gates prove that nothing in the active product depends on it. + +--- + +# 1. Concrete win condition + +The implementation is complete only when all of the following are true: + +```text +ONE_ACTIVE_PROJECT = TFTMAC +APP_IDENTITY = TFTMAC +BUNDLE_ID = com.flashls1.tftmac +LEGACY_PRODUCT_UI_REFERENCES = 0 +LEGACY_PRODUCT_SOURCE_REFERENCES = 0 in the current tracked tree +LEGACY_LAUNCHERS = 0 +LEGACY_UPDATE_FEEDS = 0 +LEGACY_RUNTIME_DEPENDENCIES = 0 +OFFICIAL_TFT_INSTALLER = com.android.vending +RIOT_PACKAGE_REPACKING = 0 +CUSTOM_RIOT_PATCHING = 0 +SOURCE_BUILT_AEMU_REQUIRED = NO +WORKING_RUNTIME_ROOT_PRESERVED = YES +WORKING_AVD_PRESERVED = YES +CURRENT_NATIVE_GATE1_WORK_PRESERVED = YES +ABANDONED_BUILD_TREE_RECLAIMED = YES +REDUNDANT_PROJECTS_RETIRED = YES after unique-content gates +NATIVE_TFTMAC_IMPLEMENTATION_CAN_CONTINUE = YES +``` + +The user must be able to continue building and playing through TFTMAC after cleanup without reinstalling or reconstructing the known-good runtime. + +--- + +# 2. Preflight findings + +## 2.1 Current repository authority + +Observed current authority: + +```text +repository: flashls1/TFTMAC +default branch: master +master SHA: fdfb325100db508f795ed5b83cea48887a88cb71 +local/remote: aligned and clean +``` + +The current native implementation change is: + +```text +change: 14d01e7a-b64d-4b7f-b856-e3366885b381 +branch: clara/implement-tftmac-native-mac-app-from-tft-14d01e7a +state: active dirty worktree +``` + +That change contains the new native Xcode/AppKit/Metal work and must be preserved before any destructive cleanup. + +Already proven in that change: + +- normal `TFTMAC.xcodeproj` exists; +- arm64 Release build passes under Xcode 26.6; +- native unit tests pass; +- Swift 6 concurrency issue found by the compiler was repaired; +- gRPC/SwiftPM dependency graph resolves successfully; +- the installed Android Emulator 37.1.11 controller protocol was frozen directly from the working runtime; +- frozen `emulator_controller.proto` SHA-256 is `1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f`; +- the stock runtime remains the control while the native presentation layer is built. + +## 2.2 Legacy identity contamination is broad, not cosmetic + +Repository search found hundreds of current-tree references tied to the old donor product. They exist in multiple classes: + +```text +README / contribution / support documentation +launcher source and resources +bundle identifiers +application metadata +old icons and social artwork +build scripts +release/update scripts +CI validation +integration tests +runtime-control commands +process detection +profile/config identifiers +performance SQL primary/foreign keys +historical engineering-map rows +old experiment scripts +old updater/feed logic +old keychain/service names +old test names +old package/update terminology +``` + +Therefore this cannot be implemented as a blind text replacement. Some records are executable behavior, some are SQL identities, some are obsolete code that should be deleted, and some are historical evidence that must be compacted or migrated before removal. + +## 2.3 Current validation is still coupled to legacy tests + +The registered TFTMAC production validation still executes the old validation script. GitHub CI also invokes that legacy test path. + +This must be changed before legacy source is deleted, otherwise a correct cleanup would intentionally break the project's own validation authority. + +Final validation authority must be TFTMAC-native and must not depend on a deleted launcher tree. + +## 2.4 Abandoned source-build storage is the main reclaim target + +The earlier from-source emulator effort created bulk development data under: + +```text +/Volumes/MAC MINI M4/TFTMAC/Build +``` + +Project research recorded the development source/build tree at approximately **131 GB**. + +That path includes or historically included: + +```text +AEMU multi-repository source checkout +large emulator prebuilts +source-build objects +CMake/Ninja outputs +CTS/reference source trees +host compatibility wrappers +build logs +failed/partial build state +source synchronization metadata +``` + +The source build reached a real 9,854-step compile and later failed during that obsolete path. Those compiled/source artifacts are no longer required by the working architecture. + +The released stock emulator path is already proven and the native project now freezes its protocol from the installed runtime itself. Therefore the large source checkout has no normal product role. + +## 2.5 Exact live free-space measurement is a cleanup-time gate + +The current Clara registry advertises an external-volume disk probe, but the live allowlist rejected that probe during this preflight. The plan therefore does **not** invent a current free-space number. + +Before deletion, implementation must perform one read-only byte inventory through an approved host path and record: + +```text +filesystem free bytes before cleanup +size of each candidate directory +protected runtime size +capture size +native DerivedData/build-cache size +redundant project roots/worktrees size +filesystem free bytes after cleanup +exact reclaimed bytes +``` + +No cleanup acceptance may use an estimated reclaimed figure when an exact post-delete measurement is available. + +## 2.6 Redundant Clara project identities + +Three TFT-related project identities currently exist: + +```text +TFTMAC -> authoritative project +TFTMAC Runtime -> older source/runtime project +TFTMAC2 -> essentially empty shell project +``` + +`TFTMAC2` currently has no active changes, no operations, and only minimal foundation files. + +`TFTMAC Runtime` cannot be deleted immediately because it still contains one published but unmerged compatibility-laboratory change: + +```text +change: 7c24d6df-bb78-4a4d-9c8f-1b4028871f60 +head: 994cf8d8524b87cc03c7decb4b253fc7be2253c7 +``` + +That change must receive a unique-content review before retirement. Any still-useful fact must be reproduced/migrated into TFTMAC or explicitly classified as superseded before the old project is retired. + +## 2.7 Licensing boundary + +The current repository contains MIT-licensed material with an upstream copyright notice. + +Product independence does not authorize false authorship or removal of legally required notices from substantial retained code. + +Required rule: + +1. Audit which current files, if any, remain copied or substantially derived from the legacy open-source code. +2. Prefer deleting obsolete copied implementation rather than carrying it forward. +3. If substantial licensed portions remain, retain the required copyright and MIT permission notice in a neutral third-party notice location. +4. The old product name is not required by the MIT text and does not need to remain in the product identity. +5. If no substantial licensed portion remains after replacement/removal, update the project's primary licensing/notice structure to accurately describe the resulting TFTMAC codebase. + +--- + +# 3. Non-negotiable implementation rules + +1. **TFTMAC is the sole product identity.** +2. **No old launcher ships, starts, or remains callable from TFTMAC.** +3. **No old logo, icon, wordmark, favicon, social artwork, hero image, or fallback icon remains.** +4. **No old hosted game feed or updater remains.** +5. **No third-party Riot APK mirroring, repacking, re-signing, or patching.** +6. **Google Play is package installation/update authority.** +7. **Riot's own application performs its own patch/content initialization.** +8. **The working stock Android Emulator remains runtime authority.** +9. **No source-built AEMU checkout is needed for normal build, launch, test, repair, or release.** +10. **Do not delete the working SDK, AVD, Google account state, TFT install, or active native work during storage cleanup.** +11. **Do not delete a redundant project until unique-source and active-change checks pass.** +12. **Do not rewrite Git history as part of this cleanup.** Current tracked source, artifacts, metadata, and shipping outputs must be clean; destructive historical-object rewriting is not needed for product independence. +13. **Delete recreation paths before deleting large storage.** Old commands must not be able to silently regenerate the 100+ GB source tree. +14. **Every destructive delete requires an exact candidate path and pre-delete classification.** No broad `/Volumes/.../TFTMAC` recursive deletion. +15. **Raw evidence is retained only when it still protects a current decision.** Superseded duplicate experiments are not permanent product assets. + +--- + +# 4. Final TFTMAC architecture after convergence + +```text +TFTMAC.app + -> native AppKit window + -> Metal presentation + -> TFTMAC runtime controller + -> authenticated local EmulatorController + -> stock Google Android Emulator + -> official Google Play ARM64 guest + -> official Google Play TFT package + -> Riot official authentication/content lifecycle + -> CoreAudio + -> TFTMAC raw-first telemetry +``` + +Development source: + +```text +flashls1/TFTMAC + TFTMAC.xcodeproj + TFTMAC/ + Vendor/AndroidEmulator/ + Generated/EmulatorController/ + Probes/ + Tests/ + scripts/ + ssot/ +``` + +Normal product development does **not** require: + +```text +source-built emulator checkout +legacy launcher tree +legacy application installation +legacy update server/feed +legacy branding tree +legacy release builder +legacy hosted Riot package flow +large CTS/source research checkout +``` + +--- + +# 5. Preservation matrix + +## 5.1 Protected — never delete during initial cleanup + +```text +/Volumes/MAC MINI M4/TFTMAC/Runtime/SDK +/Volumes/MAC MINI M4/TFTMAC/Runtime/AVD +current official Google Play system image +current TFT_Ultra_Tablet userdata +Google Play account/session state inside the AVD +current official TFT package and Riot data inside the AVD +current runtime manifests required by active stock-runtime control +~/Library/Application Support/TFTMAC/Profiles +~/Library/Application Support/TFTMAC/State +current package-authority evidence +current native implementation worktree 14d01e7a... +TFTMAC.xcodeproj source +Vendor/AndroidEmulator frozen protocol +Package.resolved +current source repository and .git +``` + +## 5.2 Preserve compact evidence, then allow raw-data pruning + +Keep a compact canonical record for: + +```text +latest known-good playable baseline +latest 5 GB baseline decision +current Medium / 60 / Performance OFF decision +Ultra High rejection +current package identity / installer / signer evidence +current stock-emulator version +current frozen EmulatorController proto hash +current CoreAudio decision +current raw-seal contract +one representative valid performance capture +one representative native trace if still used by an active decision +``` + +Older duplicate raw captures, abandoned traces, repeated screenshots, rejected experiment runs, and duplicate derived SQL can be deleted only after their decision/result is represented in current TFTMAC SSOT with source hashes where useful. + +## 5.3 Disposable after verification + +Primary reclaim candidates: + +```text +/Volumes/MAC MINI M4/TFTMAC/Build + including AEMU source checkout + objects / Ninja / CMake outputs + large prebuilts downloaded only for source compilation + CTS/reference source trees used only by abandoned source-build work + phase-build logs + compatibility wrappers used only by source compilation + +obsolete native DerivedData/build caches +completed obsolete Clara worktrees whose commits already exist on master +old donor checkout directories if present +old installed donor application if present +old donor Application Support/runtime data if present +old external donor runtime directories if present +obsolete launcher dist/DMG/update output +superseded test campaigns and rejected experimental runtime copies +``` + +The entire `Build` root may be removed only after the recreation-path scan in Gate 3 proves the current native/runtime system no longer references it for required operations. + +--- + +# 6. Source ownership cleanup + +## 6.1 Delete obsolete product layers instead of renaming them + +Remove the old launcher implementation where TFTMAC no longer uses it: + +```text +legacy launcher app source +legacy launcher resources +legacy release manifest for that launcher +legacy updater/feed code +legacy Sparkle integration if TFTMAC does not use it +legacy release builder +legacy update publisher +legacy game-package publisher +legacy launcher integration tests +legacy localization files tied only to the removed launcher +legacy game-host helper app if native TFTMAC no longer requires it +legacy branding source and generated output +``` + +Do not preserve dead code merely to keep old tests green. + +## 6.2 Replace current repository docs with TFTMAC docs + +Rewrite or replace: + +```text +README.md +CONTRIBUTING.md +SUPPORT.md +SECURITY.md +NOTICE/third-party notices +docs/architecture.md +docs/building.md +docs/releasing.md +docs/reproducibility.md +docs/telemetry.md +docs/troubleshooting.md +GitHub repository metadata +pull-request template +``` + +These documents describe TFTMAC only. + +Historical lessons that still matter are rewritten as independently verified TFTMAC facts. The old product is not treated as continuing architectural authority. + +## 6.3 Remove old process/runtime controls + +Delete controls whose only purpose is to start, stop, inspect, or compare the old application. + +From TFTMAC runtime tooling remove: + +```text +legacy-app process classification +legacy-app launch command +legacy-app stop command +legacy-app runtime audit +legacy ADB-port cleanup paths that exist only for that app +legacy Application Support path discovery +legacy installed-app lookup +legacy icon fallback +legacy named compatibility-source labels +``` + +No command advertised by `tftmac-direct-control.mjs` or other TFTMAC tooling may start the retired application. + +## 6.4 Rename TFTMAC runtime identities semantically + +Do not carry old names inside data models. + +Use TFTMAC-owned identifiers, for example: + +```text +tftmac_official_baseline_v1 +tftmac_5gb_baseline_v1 +tftmac_5gb_flush400_exp_v1 +``` + +Rename code concepts such as: + +```text +DONOR_PROFILE -> BASELINE_PROFILE +startDonorControl -> startBaselineControl +prepareDonorAVD -> prepareBaselineAVD +restoreDonorAVD -> restoreBaselineAVD +``` + +Exact final names should remain short and descriptive. + +## 6.5 SQL migration, not destructive string replacement + +Performance-lab and engineering-map identifiers are relational keys. + +Implement an idempotent migration that: + +1. inserts new TFTMAC-owned config IDs; +2. remaps foreign keys in experiments/sessions/evidence; +3. preserves measured values and timestamps; +4. removes superseded old IDs after referential-integrity checks pass; +5. updates lab metadata/current-baseline keys; +6. runs `PRAGMA foreign_key_check`; +7. proves no old identifier remains in active schema/seed data. + +Do not destroy useful measurements just to rename their primary key. + +## 6.6 Repository-wide forbidden-reference gate + +The forbidden legacy product token must be supplied transiently to the validation command and must **not** be committed into the repository solely for the purpose of testing itself. + +Validation scans: + +```text +tracked file paths +tracked text content +bundle metadata +compiled app strings where practical +CI files +scripts +source code +SQL seed data +assets/resources +project metadata +``` + +Acceptance: zero current-tree/product matches. + +--- + +# 7. Official update and package authority + +TFTMAC's package lifecycle is intentionally simple: + +```text +Google Play system image + -> Google account through official Android UI + -> Play Store listing for com.riotgames.league.teamfighttactics + -> Install / Update through com.android.vending + -> launch official Riot activity + -> Riot performs its own patch/content initialization +``` + +Required verification per installed/update state: + +```text +package = com.riotgames.league.teamfighttactics +installer = com.android.vending +versionName observed +versionCode observed +signer digest observed +base/split paths observed +launch activity observed +``` + +Never reintroduce: + +```text +hosted APK feed +private updater +bundled game APK +manual Riot patch bundle +re-signing +binary patching +screen-coordinate package install automation +``` + +If Play requires user authentication, consent, MFA, or CAPTCHA, TFTMAC surfaces the official Play UI and pauses only for that human action. + +--- + +# 8. Validation and CI cutover + +## 8.1 Replace legacy validation authority first + +Before removing old test source, create TFTMAC-native validation: + +```text +scripts/verify-tftmac.command +``` + +It should run only relevant checks: + +```text +repository forbidden-reference scan +TFTMAC Xcode project parse/build +native unit tests +frozen proto SHA verification +Package.resolved presence/pin verification +TFTMAC bundle identity verification +no legacy launcher/update/feed paths +performance-lab schema self-test +engineering-map/SSOT integrity checks still relevant to current architecture +shell/script syntax for retained scripts +``` + +Do not make normal CI boot Android or require a live game. + +## 8.2 GitHub CI + +Change CI from legacy launcher validation to: + +```text +verify repository ownership +build native TFTMAC +run native unit tests +verify generated/frozen protocol authority +run current SQL/SSOT static checks +reject dirty/generated drift +``` + +## 8.3 Clara validation + +Update the TFTMAC Clara project validation command only after the new verifier passes locally on the exact change. + +The production validator must no longer reference a deleted legacy test script. + +--- + +# 9. Safe storage reclamation procedure + +Storage cleanup is a controlled effect with evidence before and after. + +## 9.1 Freeze active work first + +Before any delete: + +1. review native change `14d01e7a...`; +2. checkpoint all current native Gate 1 source; +3. ensure `Package.resolved`, frozen proto, Xcode project, tests, and scripts are included; +4. capture current worktree SHA/status; +5. prove the working Runtime root is outside every planned deletion target. + +No cleanup starts while unique native source exists only as uncheckpointed local files. + +## 9.2 Process quiescence gate + +Before deleting build/runtime-adjacent directories: + +```text +no source-build worker alive +no repo sync alive +no CMake/Ninja source build alive +no process has an open file under the candidate build tree +no TFTMAC game/emulator process requires the candidate path +``` + +The working stock emulator may remain installed; cleanup should preferably occur with TFTMAC stopped to reduce ambiguity. + +## 9.3 Exact size inventory + +Create a machine-readable record such as: + +```text +ssot/storage-reclamation-preflight.json +``` + +For every candidate record: + +```text +path +realpath +bytes +classification = PROTECTED | KEEP_EVIDENCE | DISPOSABLE | REVIEW +reason +referenced_by_current_source = true/false +active_process_reference = true/false +planned_action +``` + +The deletion engine accepts only exact paths classified `DISPOSABLE`. + +## 9.4 Remove source-build recreation paths + +Before deleting `/Volumes/MAC MINI M4/TFTMAC/Build`: + +- remove normal commands that initialize/sync/build AEMU from source; +- remove current product docs instructing source builds; +- remove CI/validation dependence on source-build artifacts; +- remove active SSOT statements that declare the Build tree mandatory; +- retain only compact historical outcome facts if still useful; +- verify current native build and stock-runtime controls do not reference the Build root. + +Only then remove the abandoned Build tree. + +## 9.5 Tiered deletion order + +Recommended order: + +```text +Tier 1: obsolete local build caches / DerivedData +Tier 2: closed obsolete TFTMAC worktrees already merged or superseded +Tier 3: old launcher dist/update/build artifacts +Tier 4: abandoned source-build / AEMU / CTS development tree (~131 GB historical footprint) +Tier 5: obsolete donor application/runtime/data installations +Tier 6: superseded raw captures/traces after compact evidence retention +Tier 7: redundant project local roots after retirement gates +``` + +After each tier, re-measure free space and verify the working stock runtime still resolves. + +## 9.6 Runtime smoke after large deletion + +Immediately after the abandoned Build tree is removed: + +```text +external Runtime root exists +SDK emulator executable exists +ADB exists +TFT_Ultra_Tablet AVD exists +Google Play image exists +frozen proto still matches installed controller proto +native TFTMAC Release build still passes +runtime inventory succeeds +``` + +Do not wait until the very end to discover that a supposedly disposable path was still referenced. + +--- + +# 10. Capture and diagnostics retention policy + +The logger remains valuable, but unlimited raw research growth is not. + +Adopt a bounded retention policy: + +```text +KEEP: + latest successful playable baseline raw capture + latest native-app acceptance capture + current package-authority evidence + current promoted A/B evidence + current crash/failure capture for an unresolved blocker + +COMPACT THEN DELETE RAW: + superseded baseline runs + rejected experiments already summarized + duplicate screenshots + repeated diagnostic dumps + old traces not tied to an open hypothesis + abandoned setup/install captures +``` + +Compact record retains: + +```text +session id +dates +runtime config hash +result/verdict +key metrics +manifest/hash references when useful +reason raw data was removed +``` + +No automated age-based deletion is required in the first cleanup. Use decision relevance, not arbitrary days. + +--- + +# 11. Redundant-project retirement + +## 11.1 TFTMAC2 + +Preflight indicates this project is effectively an empty shell: + +```text +no active changes +no operations +no release +minimal foundation files only +``` + +Retirement procedure: + +1. one final unique-file/commit comparison; +2. confirm nothing needs migration; +3. close/remove local worktrees/root; +4. archive remote repository rather than leaving it as an active engineering target; +5. remove or mark retired in Clara so it is no longer selectable as an active TFT project. + +## 11.2 TFTMAC Runtime + +This project requires a stronger gate because it has an unmerged published change. + +Procedure: + +1. inspect published head `994cf8d8524b87cc03c7decb4b253fc7be2253c7`; +2. compare its unique files/findings with current TFTMAC master + native implementation branch; +3. for each unique item classify: + - `MIGRATE_TO_TFTMAC`, + - `ALREADY_SUPERSEDED`, or + - `REJECTED/OBSOLETE`; +4. migrate only genuinely useful current facts/code; +5. validate migrated TFTMAC source independently; +6. close the old change; +7. archive the remote project; +8. delete local project/worktree/runtime-local build artifacts; +9. remove or mark retired in Clara. + +Final Clara project catalog should present **TFTMAC as the only active TFT engineering authority**. + +--- + +# 12. Implementation sequence and gates + +## Gate 0 — preserve current native work + +Deliverables: + +```text +checkpoint active native Gate 1 work +record exact head and diff +freeze Package.resolved +freeze installed controller proto provenance +``` + +Exit: no unique active work can be lost by cleanup. + +## Gate 1 — ownership inventory + +Deliverables: + +```text +current-tree legacy-reference inventory +legacy path inventory +legal/code-origin inventory +runtime dependency map +SQL identity migration map +``` + +Exit: every current reference has one action: DELETE, REWRITE, MIGRATE, or LEGAL_NOTICE. + +## Gate 2 — new TFTMAC validation authority + +Implement `verify-tftmac.command`, update native tests, and prove it passes before removing old tests. + +Exit: TFTMAC can validate itself without the legacy launcher. + +## Gate 3 — source/product separation + +Delete obsolete launcher/updater/feed/branding code, rename runtime/profile identifiers, migrate SQL, rewrite docs/metadata, remove executable legacy controls. + +Exit: + +```text +forbidden-reference scan = 0 +native Release build = PASS +native tests = PASS +SQL integrity = PASS +``` + +## Gate 4 — stock-runtime independence proof + +Using the current working runtime: + +```text +inventory runtime +verify SDK/AVD +verify Google Play package authority +verify installed controller proto +launch/stop control path if safe +``` + +Exit: no runtime operation requires the abandoned Build tree or retired application. + +## Gate 5 — storage preflight and source-build retirement + +Produce exact storage inventory and remove all source-build recreation paths. + +Exit: `/Volumes/MAC MINI M4/TFTMAC/Build` is classified disposable in full or has explicitly listed small exceptions. + +## Gate 6 — reclaim bulk storage + +Delete approved candidates by tier and record exact reclaimed bytes. + +Exit: + +```text +large abandoned source/build tree removed +stock Runtime intact +native source intact +post-delete runtime smoke PASS +``` + +## Gate 7 — compact/prune superseded telemetry + +Retain active evidence, compact obsolete decision evidence, remove no-longer-useful raw bulk. + +Exit: captures have a bounded, explainable product role. + +## Gate 8 — retire redundant projects + +Retire empty project first. Retire old runtime project only after unique-content migration gate. + +Exit: TFTMAC is the sole active TFT project in Clara. + +## Gate 9 — resume native implementation + +Rebase/continue the native implementation on the clean ownership baseline. + +Next product gate remains: + +```text +hidden stock emulator +authenticated EmulatorController +getStatus +correct PID/AVD proof +clean shutdown +``` + +The cleanup does not replace or postpone the native-app roadmap; it makes that roadmap operate on the correct clean foundation. + +--- + +# 13. Failure handling and rollback + +## Source cleanup failure + +If native build/test fails after deleting legacy source: + +- restore only the exact deleted source files from Git; +- do not restore old installed applications or large external build trees automatically; +- identify the actual dependency and either migrate it into TFTMAC or remove the dependency. + +## SQL migration failure + +If foreign-key or semantic checks fail: + +- rollback transaction; +- keep the old database untouched; +- repair the migration map; +- never hand-edit primary/foreign IDs in a partially migrated live DB. + +## Storage cleanup failure + +If a candidate cannot be proven disposable: + +```text +classification -> REVIEW +no delete +continue with other proven-disposable candidates +``` + +If the stock runtime fails after a deletion tier: + +- stop further deletion; +- restore only the minimum missing configuration/artifact if it was actually removed; +- never regenerate the full source-build tree unless a current measured blocker specifically requires a new source-build project. + +## Redundant-project retirement failure + +If unique source is found: + +- migrate/reproduce it into TFTMAC first; +- do not delete that old project until the migration commit is validated and durable. + +--- + +# 14. Acceptance tests + +Ownership: + +```text +[ ] app name is TFTMAC +[ ] bundle id is com.flashls1.tftmac +[ ] only TFTMAC branding/assets exist +[ ] current-tree forbidden legacy product scan has zero matches +[ ] no legacy launcher executable or command exists +[ ] no legacy updater/feed exists +[ ] no legacy hosted game package path exists +[ ] CI does not reference legacy tests +[ ] Clara TFTMAC validation does not reference legacy tests +``` + +Official package/runtime: + +```text +[ ] stock Google Android Emulator is runtime authority +[ ] current SDK/AVD remain on external Runtime root +[ ] EmulatorController proto authority remains installed Emulator 37.1.11 +[ ] official TFT package is com.riotgames.league.teamfighttactics +[ ] installer is com.android.vending +[ ] version/signing evidence captured +[ ] Google Play handles app update +[ ] Riot handles Riot content initialization +[ ] no custom Riot binary patch/repack/resign path exists +``` + +Storage: + +```text +[ ] exact pre-clean storage inventory captured +[ ] active processes checked before deletion +[ ] active native work checkpointed +[ ] abandoned source-build recreation paths removed +[ ] abandoned source/build tree removed +[ ] exact reclaimed bytes captured +[ ] protected Runtime root unchanged +[ ] protected AVD unchanged +[ ] native Release build passes after cleanup +[ ] runtime inventory passes after cleanup +``` + +Project convergence: + +```text +[ ] TFTMAC2 unique-content check complete and project retired +[ ] TFTMAC Runtime unique published change reviewed +[ ] useful unique items migrated or proven superseded +[ ] old project retired only after migration gate +[ ] TFTMAC is sole active TFT project authority +``` + +--- + +# 15. Required durable cleanup evidence + +Implementation should leave small evidence artifacts: + +```text +ssot/storage-reclamation-preflight.json +ssot/storage-reclamation-result.json +ssot/ownership-migration.json +ssot/runtime-authority.json +ssot/project-retirement.json +ssot/retained-evidence-index.json +``` + +`storage-reclamation-result.json` records: + +```text +startedAt +completedAt +filesystemFreeBytesBefore +filesystemFreeBytesAfter +reclaimedBytes +removedPaths[] +protectedPathsVerified[] +postCleanupNativeBuild +postCleanupRuntimeInventory +``` + +Do not keep giant cleanup logs when a compact hash-addressed result is enough. + +--- + +# 16. Zen Gate evaluation + +Evaluated using the exact retrieved Zen Gate v2.2 scoring engine. + +## Hard gates + +```text +H1 Win Condition: PASS +H2 Source of Truth: PASS +H3 Material Ambiguity: PASS +H4 Safety/Reversibility: PASS +H5 Validation Integrity: PASS +Project-specific absolute constraints: PASS +H = 1 +``` + +Material ambiguity is resolved by fail-closed execution gates: exact storage bytes are measured before deletion; redundant projects receive unique-content checks; legal attribution is determined by retained-code audit; no uncertain path is deleted. + +## Weighted quality score + +| Dimension | Weight | Score / 5 | Contribution | +|---|---:|---:|---:| +| Win Condition Alignment | 15 | 5.0 | 15.0 | +| Source-of-Truth Integrity | 12 | 5.0 | 12.0 | +| Ambiguity Resolution | 13 | 5.0 | 13.0 | +| Simplicity / Minimality | 16 | 4.5 | 14.4 | +| Complexity Justification | 10 | 5.0 | 10.0 | +| Architecture / Boundaries | 9 | 5.0 | 9.0 | +| Failure Handling / Observability | 12 | 5.0 | 12.0 | +| Maintainability / Readability | 6 | 4.5 | 5.4 | +| Practical Shipping Slice | 4 | 5.0 | 4.0 | +| Explainability | 3 | 5.0 | 3.0 | + +```text +Q = 97.8 +``` + +Risk penalties: + +```text +O overengineering = 1 +A unresolved ambiguity = 0 +F failure exposure = 2 +I irreversibility = 2 +R = 5 +``` + +Final: + +```text +Z = H × max(0, Q - R) +Z = 1 × (97.8 - 5) +Z = 92.8 +PASS threshold = 85 +ZEN GATE RESULT = PASS +Remediation mode = NONE +``` + +Why the remaining penalty exists: bulk storage deletion and project retirement are intentionally irreversible effects. The plan contains explicit preservation, exact-path classification, unique-content, quiescence, checkpoint, and post-delete smoke gates to contain that risk. + +--- + +# 17. Definition of done + +This convergence is done when TFTMAC has one clean identity, one native build system, one stock official runtime, one official Google Play/Riot package path, one current validation system, one bounded evidence system, and one active Clara project. + +The external drive contains the **working runtime and useful TFTMAC evidence**, not a 100+ GB abandoned source-build laboratory. + +After this plan is completed, engineering proceeds directly into the native application's hidden-emulator/EmulatorController gate without bringing any retired launcher, branding, updater, project, or source-build dependency back into the system. diff --git a/docs/history/2026-08-31-pre-build8/TFTMAC_FULL_IMPLEMENTATION_PLAN.md b/docs/history/2026-08-31-pre-build8/TFTMAC_FULL_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..c7338d6 --- /dev/null +++ b/docs/history/2026-08-31-pre-build8/TFTMAC_FULL_IMPLEMENTATION_PLAN.md @@ -0,0 +1,17 @@ +# TFTMAC Historical Source-Build Plan — RETIRED + +**Status:** SUPERSEDED — DO NOT EXECUTE +**Superseded by:** `TFTMAC_CLEAN_OWNERSHIP_PLAN.md` and `TFTMACAPP.md` + +The former GPU-runtime implementation plan required a source-built Android Emulator/AEMU laboratory. That path was experimentally exercised, consumed substantial external storage, and was later superseded by the proven released Google Android Emulator runtime. + +Current execution authority is: + +1. Complete `TFTMAC_CLEAN_OWNERSHIP_PLAN.md` through project/storage convergence. +2. Continue the native application from `TFTMACAPP.md`. +3. Preserve the released stock Android Emulator as the control/runtime authority. +4. Do not initialize, synchronize, build, or recreate a source-built AEMU checkout during normal TFTMAC work. + +Historical technical conclusions from the retired work remain represented in compact SSOT/evidence records where they still protect a current decision. The large source/build laboratory itself is disposable after the approved independence, measurement, and quiescence gates pass. + +A future source-runtime effort would require a new, separately approved measured blocker proving that the released emulator cannot satisfy a required product function. The existence of old source locks or historical experiments is not sufficient authorization. diff --git a/docs/history/2026-08-31-pre-build8/TFTMAC_GRAPHICS_ARCHITECTURE.md b/docs/history/2026-08-31-pre-build8/TFTMAC_GRAPHICS_ARCHITECTURE.md new file mode 100644 index 0000000..c09a48a --- /dev/null +++ b/docs/history/2026-08-31-pre-build8/TFTMAC_GRAPHICS_ARCHITECTURE.md @@ -0,0 +1,177 @@ +# TFTMAC Graphics Architecture + +## Win condition + +TFTMAC must provide the best practical Teamfight Tactics experience on Apple-silicon Macs while preserving a clean trust boundary: + +- Riot's game binary remains official and unmodified whenever possible. +- Google Play remains the live-client acquisition/update authority. +- Engine-specific graphics changes are isolated behind explicit runtime adapters. +- A working live path must never be broken by experimental Unreal tuning. + +## Runtime model + +TFTMAC uses a two-layer architecture: + +1. **Engine-neutral Mac shell** + - owns the native SwiftUI launcher; + - owns Android emulator lifecycle; + - owns display size, DPI, CPU/RAM, window behavior, input, audio and local diagnostics; + - detects the active Riot engine/runtime; + - exposes safe graphics presets. + +2. **Engine adapter** + - `LiveNativeAdapter`: current Google Play live client, stock Riot runtime; + - `UnrealEnhancedAdapter`: future/live Unreal client, harvesting the proven retired-donor PBE renderer work; + - adapters are selected by evidence, never by version-string guesswork. + +## Engine detection + +Evidence is evaluated in order: + +1. resolved Android launch activity; +2. package/native-library inventory; +3. running-process mappings when accessible; +4. SurfaceFlinger layer/activity evidence; +5. app-managed patch/runtime files when observable; +6. Unreal markers such as `libUnreal.so`, `UECommandLine.txt`, `UnrealGame`, `DeviceProfiles.ini`, or Unreal-specific log output. + +Current live evidence after the completed in-game patch: + +- package: `com.riotgames.league.teamfighttactics` +- launch activity: `com.riotgames.leagueoflegends.RiotNativeActivity` +- Google Play package version: `16.16.8042660` +- base APK SHA-256: `9ed691e1b7e976394cc0dd91c0717df954429320a323e751e4848f6214c34919` +- native payload includes `libleagueoflegends.so` +- no `libUnreal.so` or `assets/UECommandLine.txt` in the Play Store base APK +- post-patch process/storage inspection found no Unreal runtime evidence +- the current live client therefore remains on Riot's native Android runtime for this build + +The Unreal adapter remains prepared from retired-donor work and must activate only when future live runtime evidence proves the engine transition. + +## Graphics presets + +Graphics presets are engine-neutral and map to display geometry plus resource allocation. + +| Preset | Framebuffer | DPI | Intent | +| --- | --- | ---: | --- | +| **Enhanced 1080p** | **1920×1080** | **280** | **Primary target; 617dp tablet-class short side** | +| 1440p | 2560×1440 | 416 | Experimental only | +| 1800p | 3200×1800 | 520 | Experimental only | +| 4K | 3840×2160 | 640 | Experimental only | + +The launcher applies both emulator geometry and guest `wm size` / `wm density` so Android layout and the host window agree. TFTMAC deliberately targets 1080p rather than using supersampled resolution as a substitute for renderer quality. GPU budget should be spent first on anti-aliasing, texture filtering, stable frame pacing, shader behavior and renderer quality. + +## LiveNativeAdapter + +The current safe path uses: + +- official Google Play system image; +- Apple-silicon ARM64 Android emulator; +- `-gpu host`; +- Riot's untouched package; +- no APK overlay; +- no root requirement; +- normal Google Play and Riot authentication. + +Safe enhancement surface: + +- resolution and DPI; +- host GPU mode; +- guest CPU and memory; +- window sizing/fullscreen; +- input mapping; +- audio handling; +- emulator transport flags proven not to alter Riot code; +- frame pacing measurement. + +## UnrealEnhancedAdapter + +When live TFT exposes the Unreal runtime, harvest the proven retired-donor PBE work rather than rediscovering it. + +Candidate stack: + +`Unreal -> OpenGL ES / ANGLE -> gfxstream -> Metal` + +or where supported: + +`Unreal -> Vulkan -> gfxstream / MoltenVK -> Metal` + +Donor capabilities already present in this repository include: + +- `run-tft-root-affinity.command`; +- `run-tft-angle-opengl.command`; +- `run-tft-best-verified.command`; +- temporary verified APK overlay support; +- `DeviceProfiles.ini` injection with rollback; +- `UECommandLine.txt` overlay preparation; +- MoltenVK queue and fast-math tuning; +- ANGLE feature controls; +- ASG transport tuning; +- frame-pacing and input-latency instrumentation; +- 1080p/1440p/1800p/4K profiles. + +The adapter must only activate when the running live build proves the expected Unreal artifact/layout. A hash/version/layout mismatch must fall back to `LiveNativeAdapter`, not attempt a stale overlay. + +## Future enhancement ladder + +### Tier 1 — safe/live now + +- Enhanced 1080p target at 1920×1080 / 280 DPI (617dp tablet class); +- 8 vCPU / 8 GB guest RAM on the validated Mac Mini profile; +- host GPU acceleration; +- OpenGL ES 3.2 capability (`196610`); +- ANGLE EGL with Vulkan 1.3 on Apple M4; +- native fullscreen/window fill; +- FPS overlay and frame-time diagnostics. + +### Tier 2 — Unreal enhanced + +- verified ANGLE/OpenGL path; +- guest DeviceProfiles tuning; +- anisotropic filtering and anti-aliasing profiles; +- shader/prewarm experiments; +- asynchronous MoltenVK queue submission where validated; +- bounded Metal command-buffer tuning; +- ASG transport tuning. + +### Tier 3 — measured quality optimizer + +TFTMAC should benchmark candidate profiles instead of assuming "higher" means "better". Score each profile on: + +- median FPS; +- p95/p99 frame time; +- janky frames; +- input latency; +- visual resolution; +- crash-free session duration; +- host CPU/GPU pressure. + +The winning profile is the highest visual-quality configuration that remains inside an explicit frame-pacing envelope. + +## Non-goals + +- modifying Riot gameplay logic; +- bypassing Riot authentication or anti-cheat; +- intercepting Riot credentials; +- embedding third-party or mirrored Riot APKs in Git; +- blindly applying PBE hashes/configuration to live builds; +- claiming an Unreal profile is valid without runtime evidence. + +## Acceptance gates + +### Current live gate + +- official Google Play installer authority confirmed; +- live TFT starts from TFTMAC; +- Riot login works; +- a full live match can be entered and completed; +- no Riot binary modification is required. + +### Unreal-enhanced gate + +- post-patch live client proves Unreal runtime/layout; +- donor overlay/profile assumptions are revalidated against the exact live build; +- graphics adapter is reversible and hash-gated; +- quality profile materially improves visual quality without unacceptable frame pacing; +- fallback to stock live remains one click away. diff --git a/docs/history/2026-08-31-pre-build8/launch-profiles.md b/docs/history/2026-08-31-pre-build8/launch-profiles.md new file mode 100644 index 0000000..2035922 --- /dev/null +++ b/docs/history/2026-08-31-pre-build8/launch-profiles.md @@ -0,0 +1,40 @@ +# Launch profiles + +The root entrypoints remain in place because they share relative paths with the +runtime, reversible AVD wrapper, experiment profiles, and benchmark harness. +Only the first row is the canonical recommendation. + +| Entrypoint | Status | Purpose | Differences | Safe default | +| --- | --- | --- | --- | --- | +| `run-tft-best-verified.command` | Recommended | Canonical audited source launch | Pins ASG, ANGLE/OpenGL, MoltenVK async/64, control profile, 1440p default, and clears inherited experiment flags | Yes | +| `run-tft-fast-quality.command` | Stable fallback | Customizable stable stack | Same base stack without the canonical argument parser/override reset | No | +| `run-tft-performance-max.command` | App render-base / source entrypoint | Reduce CPU/RHI and guest-host transport work while retaining full-resolution UI | 67% 3D scale, confirmed low-cost effects/LOD, 16 KiB ASG writes, and disabled ANGLE FBO-boundary submit; Trial 1-8 remained 34.1–35.1 FPS in two full transport confirmations | No | +| `run-tft-angle-opengl.command` | Required lower-level profile | Apply verified ANGLE/OpenGL overlay | Renderer delegate used by the stable stack; not a complete safety wrapper by itself | No | +| `run-tft-root-affinity.command` | Diagnostic/lower-level | Direct emulator and guest orchestration | Owns rootable AVD, overlay, PSO scheduling, HWUI repair, and cleanup | No | +| `run-tft-gles32.command` | Legacy stable fallback | Non-root external AVD launch | Older pipe-era AVD, 1600×900, 6 GiB, no root scheduling or selected ASG stack | No | +| `run-tft-mvk128-experimental.command` | Experimental, not promoted | Reproduce MoltenVK 128-buffer candidate | One strong run failed cold and sustained reproducibility | No | +| `run-tft-fast-quality-angle-no-fbo-submit.command` | Provisional experiment | Disable ANGLE FBO-boundary deferred submit | Strong first run; lacks the required cold reproducibility confirmation | No | +| `run-tft-fast-quality-shader-prewarm.command` | Rejected for default | Preload a narrow shader set | Neutral lobby, rejected by fixed-stage campaign; retained for comparison | No | +| `run-tft-fast-quality-submit-thread.command` | Rejected | Move guest Vulkan submission/marshalling | Regressed to 37.40/32.60/25.80 FPS in fixed stages | No | +| `run-tft-fast-quality-submit-thread-control.command` | Diagnostic control | Validate submit wrapper without enabling the candidate | Mesa on-demand behavior through the same wrapper | No | +| `run-tft-fast-quality-upstream-asg.command` | Rejected campaign candidate | Force upstream ASG-related features | Did not pass the campaign gates | No | +| `run-tft-fast-quality-asg-active-consumer.command` | Rejected/historical | Reproduce isolated four-byte host patch | 11.2 FPS / 334 ms p95 versus stable lobby control; requires explicit override | No | +| `run-tft-fast-quality-native-gles.command` | High-risk diagnostic | Disable guest ANGLE | Tests native gfxstream GLES path; no accepted result | No | +| `run-tft-fast-quality-native-gles30.command` | Rejected/historical | Relax the native gate to ES 3.0 | TFT crashes before first frame because required GLES APIs are absent | No | +| `run-tft-fast-quality-native-gles31.command` | Rejected/historical | Relax the native gate to ES 3.1 | Host exposes only native GLES 3.0, so the strict gate fails correctly | No | +| `run-tft-fast-quality-ubo-direct-write.command` | Experimental | Test direct uniform-buffer writes | Isolated risky device profile; no promotion evidence | No | +| `run-tft-fast-quality-ubo-pool.command` | Experimental | Test a larger uniform-buffer pool | Isolated risky device profile; no promotion evidence | No | +| `run-tft-direct-vulkan.command` | Rejected/historical diagnostic | Test direct Unreal Vulkan | The selected Shipping device profile disables direct Vulkan RHI; Vulkan remains below ANGLE | No | + +Resolution status in the canonical launcher is independent of graphics profile: +1440p is verified, 1620p has a verified full-size SurfaceView but no accepted +battle result, 1800p is provisional, and 2160p is experimental. + +The packaged launcher displays a click-through 62×20 FPS HUD at the right edge +of the active Emulator title bar. It samples existing SurfaceFlinger presentation +timestamps once per second, hides when the Emulator loses focus, and stops with +the game session. + +Rejected profiles are retained to make negative results reproducible. Do not +infer recommendation from file naming, and never compare login/lobby FPS with +an active-match result. diff --git a/docs/images/mactician-running-tft-pbe.webp b/docs/images/mactician-running-tft-pbe.webp deleted file mode 100644 index 5161414..0000000 Binary files a/docs/images/mactician-running-tft-pbe.webp and /dev/null differ diff --git a/docs/launch-profiles.md b/docs/launch-profiles.md index 2035922..f89be1f 100644 --- a/docs/launch-profiles.md +++ b/docs/launch-profiles.md @@ -1,40 +1,24 @@ -# Launch profiles +# Launch Profiles -The root entrypoints remain in place because they share relative paths with the -runtime, reversible AVD wrapper, experiment profiles, and benchmark harness. -Only the first row is the canonical recommendation. +## Current profile -| Entrypoint | Status | Purpose | Differences | Safe default | -| --- | --- | --- | --- | --- | -| `run-tft-best-verified.command` | Recommended | Canonical audited source launch | Pins ASG, ANGLE/OpenGL, MoltenVK async/64, control profile, 1440p default, and clears inherited experiment flags | Yes | -| `run-tft-fast-quality.command` | Stable fallback | Customizable stable stack | Same base stack without the canonical argument parser/override reset | No | -| `run-tft-performance-max.command` | App render-base / source entrypoint | Reduce CPU/RHI and guest-host transport work while retaining full-resolution UI | 67% 3D scale, confirmed low-cost effects/LOD, 16 KiB ASG writes, and disabled ANGLE FBO-boundary submit; Trial 1-8 remained 34.1–35.1 FPS in two full transport confirmations | No | -| `run-tft-angle-opengl.command` | Required lower-level profile | Apply verified ANGLE/OpenGL overlay | Renderer delegate used by the stable stack; not a complete safety wrapper by itself | No | -| `run-tft-root-affinity.command` | Diagnostic/lower-level | Direct emulator and guest orchestration | Owns rootable AVD, overlay, PSO scheduling, HWUI repair, and cleanup | No | -| `run-tft-gles32.command` | Legacy stable fallback | Non-root external AVD launch | Older pipe-era AVD, 1600×900, 6 GiB, no root scheduling or selected ASG stack | No | -| `run-tft-mvk128-experimental.command` | Experimental, not promoted | Reproduce MoltenVK 128-buffer candidate | One strong run failed cold and sustained reproducibility | No | -| `run-tft-fast-quality-angle-no-fbo-submit.command` | Provisional experiment | Disable ANGLE FBO-boundary deferred submit | Strong first run; lacks the required cold reproducibility confirmation | No | -| `run-tft-fast-quality-shader-prewarm.command` | Rejected for default | Preload a narrow shader set | Neutral lobby, rejected by fixed-stage campaign; retained for comparison | No | -| `run-tft-fast-quality-submit-thread.command` | Rejected | Move guest Vulkan submission/marshalling | Regressed to 37.40/32.60/25.80 FPS in fixed stages | No | -| `run-tft-fast-quality-submit-thread-control.command` | Diagnostic control | Validate submit wrapper without enabling the candidate | Mesa on-demand behavior through the same wrapper | No | -| `run-tft-fast-quality-upstream-asg.command` | Rejected campaign candidate | Force upstream ASG-related features | Did not pass the campaign gates | No | -| `run-tft-fast-quality-asg-active-consumer.command` | Rejected/historical | Reproduce isolated four-byte host patch | 11.2 FPS / 334 ms p95 versus stable lobby control; requires explicit override | No | -| `run-tft-fast-quality-native-gles.command` | High-risk diagnostic | Disable guest ANGLE | Tests native gfxstream GLES path; no accepted result | No | -| `run-tft-fast-quality-native-gles30.command` | Rejected/historical | Relax the native gate to ES 3.0 | TFT crashes before first frame because required GLES APIs are absent | No | -| `run-tft-fast-quality-native-gles31.command` | Rejected/historical | Relax the native gate to ES 3.1 | Host exposes only native GLES 3.0, so the strict gate fails correctly | No | -| `run-tft-fast-quality-ubo-direct-write.command` | Experimental | Test direct uniform-buffer writes | Isolated risky device profile; no promotion evidence | No | -| `run-tft-fast-quality-ubo-pool.command` | Experimental | Test a larger uniform-buffer pool | Isolated risky device profile; no promotion evidence | No | -| `run-tft-direct-vulkan.command` | Rejected/historical diagnostic | Test direct Unreal Vulkan | The selected Shipping device profile disables direct Vulkan RHI; Vulkan remains below ANGLE | No | +Stock Build 8 is the only normal-play authority. Its runtime base is +`tftmac_5gb_native_v1`, and its current SQL capture configuration is +`tftmac_stock_build8_high60_control`: -Resolution status in the canonical launcher is independent of graphics profile: -1440p is verified, 1620p has a verified full-size SurfaceView but no accepted -battle result, 1800p is provisional, and 2160p is experimental. +- 1920×1080, 320 dpi, 60 Hz +- 6 vCPU, 5120 MiB guest RAM, host GPU, CoreAudio +- `virtio-gpu-asg` with the retained ASG/ANGLE/MoltenVK control values +- TFT High graphics, 60 FPS, Riot Performance Mode OFF -The packaged launcher displays a click-through 62×20 FPS HUD at the right edge -of the active Emulator title bar. It samples existing SurfaceFlinger presentation -timestamps once per second, hides when the Emulator loses focus, and stops with -the game session. +TFTMAC launches through the packaged emulator host using the locked logged-in +macOS-session chain and ADB 5038 / console 5582 identity. It does not use old +script launchers, Node, Clara, or a direct service-context spawn. -Rejected profiles are retained to make negative results reproducible. Do not -infer recommendation from file naming, and never compare login/lobby FPS with -an active-match result. +`combat_latency_a` is a launch-verified but not performance-promoted historical +candidate. Riot Performance Mode Beta and Home Run A are rejected and are not +selectable. A future isolated `advanced_diagnostics` runtime is diagnostic-only, +not a play profile. + +The legacy launcher table is archived at +`history/2026-08-31-pre-build8/launch-profiles.md`. diff --git a/docs/native-gles-transport-experiment.md b/docs/native-gles-transport-experiment.md deleted file mode 100644 index 298d3ac..0000000 --- a/docs/native-gles-transport-experiment.md +++ /dev/null @@ -1,578 +0,0 @@ -# Native GLES and graphics-transport experiment - -Date: 2026-08-10/11 - -## Decision - -The short native-GLES path is not viable with the guest image and gfxstream -binaries shipped by Emulator 37.1.11. The blocker is API capability across the -whole protocol, not launcher wiring: TFT exercises ES 3.1/3.2 core -functionality before the first frame, while the working Metal-backed path and -the outer gfxstream context expose ES 3.0. Advertising ES 3.2 without -implementing those capabilities only moves the failure into Unreal's renderer. - -The supported production path therefore remains: - -```text -TFT / Unreal GLES - -> guest ANGLE (GLES to Vulkan) - -> guest gfxstream Vulkan encoder - -> virtio-gpu ASG - -> host gfxstream Vulkan decoder - -> MoltenVK - -> Metal -``` - -The current bottleneck is more precise than "the chain is long": frame -throughput is limited by serialized RHI/command submission and synchronization -across the guest/host boundary and its downstream Vulkan-to-Metal consumer. An -ASG write can represent packing/copy work, ring backpressure, or a downstream -fence wait; it is an observation point, not proof that byte copying alone is -the root cause. - -## Native-path experiment - -The proposed path was: - -```text -TFT / Unreal GLES - -> gfxstream GLES encoder - -> virtio-gpu ASG - -> host ANGLE - -> Metal -``` - -It would remove guest ANGLE, the Vulkan gfxstream protocol, and MoltenVK from -the frame path. The experiment deliberately separated host capability from the -end-to-end guest path. - -### Launcher wiring - -`run-tft-root-affinity.command` now forwards ANGLE feature overrides into the -packaged emulator host. It also forwards the MoltenVK variables used by the -bounded A/B candidates. This closes a real diagnostic gap: an app-bundled run -now receives the same process-local configuration as a direct emulator run. - -### Host ANGLE probe - -`artifacts/angle-egl-probe.cpp` is now a self-contained dynamic EGL/GLES probe. -It does not depend on development headers absent from the packaged runtime. It: - -- requests ES 3.2, 3.1, and 3.0 contexts independently; -- reports the actual GL and GLSL versions; -- reports selected ES 3.1/3.2 extensions and entrypoints; -- enumerates relevant ANGLE features; -- supports explicit default, Metal, Vulkan, and OpenGL backends; -- applies ANGLE feature overrides at EGL display creation time. - -`scripts/run-host-angle-capability-probe.command` builds it against the -currently installed packaged runtime and runs the `default`, `metal`, `opengl`, -or isolated `swiftshader` matrix entry. The last mode intentionally enables -`exposeNonConformantExtensionsAndVersions` and returns nonzero when the nominal -ES 3.2 geometry/tessellation probes expose the expected zero-limit failure. - -Observed results: - -- the packaged host ANGLE working backend exposes ES 3.0, not ES 3.1/3.2; -- selecting ANGLE Metal advertises the platform extension but fails display - initialization in this build; -- selecting ANGLE Vulkan reaches MoltenVK but aborts during Vulkan buffer - allocation, so it is not a usable shortcut around gfxstream; -- selecting ANGLE Vulkan with the packaged SwiftShader ICD creates ES 3.1 and, - under ANGLE's explicitly non-conformant - `exposeNonConformantExtensionsAndVersions` override, nominal ES 3.2 contexts. - Compute, image, indirect-draw, and texture-buffer entrypoints - are exposed. This isolates part of the limitation: the ANGLE frontend covers - the ES 3.1 functionality exercised by TFT, but its viable macOS hardware - backend and the surrounding gfxstream protocol do not provide the complete - contract; -- version-exposure overrides do not add the missing ES 3.1/3.2 behavior. - -The direct SwiftShader EGL/GLES libraries are not an alternative: unlike ANGLE -over SwiftShader Vulkan, they expose only ES 3.0 and lack compute entrypoints. - -The positive SwiftShader subset is functional rather than a version-string -check. The probe compiles and runs a GLSL ES 3.10 compute shader, reads the -written SSBO back on the CPU, verifies two-pass `imageStore`/`imageLoad` through -an `r32ui` image, and completes a GLES fence/client-wait. In the nominal ES 3.2 -context it also samples a `GL_R32UI` texture buffer from a GLSL ES 3.20 compute -shader. All readbacks match their sentinel values. However, both geometry and -tessellation pipeline probes fail: the reported limits are -`GL_MAX_GEOMETRY_OUTPUT_VERTICES=0`, `GL_MAX_PATCH_VERTICES=0`, and -`GL_MAX_TESS_GEN_LEVEL=0`, and the corresponding extensions are absent. Thus -the override does not make this a conformant or complete ES 3.2 renderer. The -same test cannot start on Metal and cannot request an ES 3.1 context on the -default macOS OpenGL backend. - -The direct Vulkan probe crash was isolated to the probe process. Its macOS -diagnostic report is retained outside the repository under the user's -DiagnosticReports directory. - -### Guest EGL capability probe - -`artifacts/android-egl-capability-probe` is a headerless arm64 Android probe -built by `scripts/build-android-egl-capability-probe.command` with the project's -Go 1.24.3 toolchain. It goes through Android's public `libEGL.so` loader, pins -all EGL calls to one OS thread, creates a pbuffer, reports actual versions and -limits, and resolves all 59 EGL/GLES names found dynamically in `libUnreal.so`. -The no-cgo assembly bridge is diagnostic code, not application runtime code. - -On a cold boot with `GuestAngle` enabled, the loader selected guest ANGLE over -gfxstream Vulkan/MoltenVK. A nominal ES 3.2 context was created, but geometry -and tessellation limits were zero even though the relevant proc addresses were -non-null. On an isolated cold boot with `GuestAngle` disabled and no APK or -library overlay, the public loader selected the native gfxstream GLES driver: - -```text -EGL 1.4 -ES 3.2 request -> EGL_BAD_CONFIG -ES 3.1 request -> EGL_BAD_CONFIG -ES 3.0 request -> OpenGL ES 3.0 (4.1 Metal - 90.5) -renderer -> Android Emulator OpenGL ES Translator (Apple M1 Max) -``` - -The native ES 3.0 context still returned non-null proc addresses and non-zero -geometry/tessellation limit values for names outside its advertised version. -They are therefore dispatch-table evidence, not a usable capability contract. -After the probe was reordered to establish ES 3.0 first, a complete clean-boot -run resolved 56 of the 59 Unreal lookup names, including every one of the 11 -core-to-EXT/OES/IMG alias candidates. The three null results were -`eglGetFrameTimestampsSupportedANDROID`, -`eglQueryTimestampSupportedANDROID`, and `glDebugMessageLogKHR`. The loader -advertises `EGL_KHR_get_all_proc_addresses`, so static library exports -substantially undercount this runtime surface. Explicit ES 3.1 and 3.2 requests -still returned `EGL_BAD_CONFIG` after that collection. An immediate first -post-boot attempt exited with `SIGSEGV`, while the repeated staged run completed; -the probe now prints unbuffered phase markers so a future readiness race can be -distinguished from a specific EGL call. - -The recorded clean-boot output retains the SHA of the exact probe binary used -for that observation. After capture, the source added explicit -`runtime.KeepAlive` barriers for EGL attribute slices passed through the -no-cgo `uintptr` bridge. The hardened source rebuilds reproducibly as an arm64 -Android PIE with SHA-256 -`9ccd84c8af69f015702ac3e04bcda165eafac6bef5d4b926d423f70cb162a31e`; -its only dynamic dependencies remain `libEGL.so` and `libGLESv2.so`. - -### End-to-end guest result - -The strict native gfxstream GLES run successfully proved that guest ANGLE was -absent and the native GLES encoder was selected. It then reported ES 3.0, and -TFT correctly rejected the renderer because it requires ES 3.2. - -A bounded `-gpu swangle` control was then added for native-GLES experiments. -The host side initialized ANGLE over Vulkan/SwiftShader with ES 3.1 and -texture-buffer extensions, but the outer gfxstream context still exposed ES -3.0. The unmodified game therefore stopped at its ES 3.2 gate. - -An isolated `libUnreal.so` gate patch to ES 3.1 progressed into the RHI thread -and then failed before the host could execute the commands. The guest -`libGLESv2_enc.so` validation rejected texture-buffer targets in -`glBindTexture`, `glTexParameteri`, `glTexStorage3D`, and `glTexSubImage3D`, as -well as capability `0x8db9`; the RHI thread then terminated with `SIGTRAP`. -This is the expected failure mode of a capability spoof: the version gate -moves, but the required formats, state, shader behavior, and entrypoints do not -appear. The software control is retained only as a diagnostic and is never a -performance candidate. - -Static inspection of the original 194 MB game library found 151 direct EGL/GLES -imports and 210 unique EGL/GLES names in total. The game directly imports ES -3.1 compute, image, memory-barrier, and indirect-draw APIs; dynamically resolved -names include texture buffers, framebuffer texture, indexed blend, and debug -APIs. The requirement is therefore active renderer behavior, not merely a -conservative version check. - -`scripts/audit-native-gles-coverage.command` makes that inspection -reproducible and compares the game with the shipped guest EGL/GLES wrappers, -the guest encoder, optional upstream protocol/dispatch source, and optional -host ANGLE exports. For this build all 151 direct imports are present across the -shipped guest ABI. Of the 59 dynamic names, 16 have an exact exported surface, -11 have a core-to-EXT/OES/IMG alias candidate (including texture buffers and -indexed blend), and 32 have no exact guest export. These 32 are optional lookup -names until runtime evidence shows they are consumed; their absence is not -treated as 32 mandatory implementation gaps. Conversely, a present symbol is -only ABI coverage and says nothing about caps or enum validation. The observed -texture-buffer validation failure therefore remains the stronger blocker than -symbol count. The later 56/59 runtime lookup result also demonstrates that this -static matrix is a conservative source audit, not an `eglGetProcAddress` -prediction through Android's full loader stack. - -### Source-build boundary - -The current upstream gfxstream host backend was built successfully on this Mac -from `emu-main-dev` (revision `a9184fd`) after two local warning-compatibility -adjustments. The generated standalone dylib is not a drop-in replacement for -the emulator's production backend: it exports substantially fewer integration -symbols than the packaged library. - -More importantly, current source still contains an incomplete GLES 3.2 -capability path. Host version detection is capped at 3.1, the macOS OpenGL path -maps OpenGL 4.1 to ES 3.0, maximum-version handling maps both 3.1 and 3.2 to a -3.1 context, and RenderControl version strings omit 3.2. Generated protocol -code already includes several texture-buffer and indexed-blend extension -opcodes, but that is not the same as coherent context creation, validation, -dispatch, and capability propagation. - -`artifacts/gfxstream-gles32-host-capability-prototype.patch` is a concrete -host-capability prototype against that revision. Its 61 added lines probe an -ES 3.2 context before falling back, add the translator's 3.2 interface and -dispatch group, and propagate `ANDROID_EMU_gles_max_version_3_2` plus the 3.2 -version string through RenderControl. It applies cleanly to `a9184fd`; a clean -standalone `gfxstream_backend` build completed with SHA-256 -`ba86d350b83f7538add45ded2b1cf868a152fb4cc571fc83d55337b71f149c9b` -after the same two AppleClang warning-compatibility allowances. It is retained -as implementation evidence, not as a drop-in binary or a conformance claim. - -`artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch` implements the 11 -mechanical proc-address candidates reported by the static Unreal audit. It -exposes the promoted GLES 3.2 core spellings (including `glTexBuffer` and -indexed blend) while reusing gfxstream's existing EXT/OES/IMG encoder opcodes. -The patch applies cleanly to `a9184fd` and its modified guest EGL translation -unit passes a standalone Clang syntax check. The later runtime probe showed the -shipped Android loader already resolves these aliases, so the patch is retained -as a rejected source-level prototype and should not be applied to this image. -It also could not create capabilities or shader stages in any case. - -The Android guest graphics libraries are also older than the host emulator: -the system image is build 13894323 from 2025-08, while the emulator backend is -build 15917651. The guest encoder's validation decisions depend on per-context -ES-version and extension flags, and those flags remain false in the failed -native run. Building only the macOS host backend cannot repair that side of the -contract. Current upstream guest source already understands the 3.2 -RenderControl token and ES 3.2 EGL context requests, which makes upgrading the -whole source-matched guest stack preferable to binary-patching the older image. - -Consequently, a real native implementation is not a small GLES wrapper. It -requires a source-matched Android guest encoder/EGL stack and host -ANGLE/gfxstream stack with the complete ES 3.1/3.2 contract, including shader, -texture-buffer, sync, gralloc/AHardwareBuffer, EGL surface, validation, dispatch, -and extension capability propagation. That is a separate renderer project, not -a safe launcher patch. - -## Current-path experiments - -All frame-pacing samples use semantic before/after gates, fresh Tocker's Trials -stages, active binary/profile hashes, and verified restoration of AVD config, -hardware config, process wrapper, and ASG backups. Screen candidates stop at -1-5; candidates that survived screening were repeated at 1-8. - -The nearest earlier paired control recorded 38.0 FPS / 34.01 ms p95 at 1-5 and -34.8 FPS / 34.51 ms p95 at 1-8. A later profiled control, after the autonomous -Trial driver was made deterministic enough to reach 1-8 reliably, recorded -40.4 FPS / 34.00 ms p95 at 1-5 and 29.2 FPS / 48.77 ms p95 at 1-8. Absolute -FPS varies with Trial composition, so promotion requires a repeatable gain in -both throughput and tail latency rather than a single favorable board. - -| Candidate | 1-5 result | 1-8 result | Decision | -| --- | --- | --- | --- | -| Large MoltenVK query pools off | 41.2 FPS / 33.54 ms p95 | 33.8 FPS / 35.46 ms p95 | Mixed: early gain, heavy-stage/tail regression | -| `VirtioGpuNext` | 39.2 / 34.72 | 31.8 / 44.74 | Reject | -| Max concurrent compilation | 38.6 / 34.27 | 33.4 / 35.76 | Reject | -| Guest submit inline, first full run | 39.3 / 34.55 | 31.7 / 45.54 | Apparent heavy-stage gain, required repeat | -| Guest submit inline, profiled repeat | 35.6 / 34.76 | 26.4 / 50.62 | Reject: gain did not reproduce; exposes RHI/MMIO latency | -| Forced guest submit thread | 38.4 / 34.91 | not promoted | Reject | -| No async composition | 39.6 / 34.36, 49.46 ms p99 | not promoted | Reject: worse tail | -| Explicitly disable native swapchain | 39.0 / 34.15, 51.20 ms max | not promoted | Neutral/worse tail | -| ASG flush 400 us | 39.5 / 35.02 | not promoted | Reject | -| ASG flush 1200 us | 36.6 / 37.10 | not promoted | Reject; 800 us remains local optimum | -| MoltenVK argument buffers on | 41.3 / 35.44, 48.43 ms p99 | not promoted | Reject: worse tail | -| Metal-event semaphores | 38.3 / 34.60 | not promoted | Reject | -| MoltenVK MTLHeap off | 39.9 / 34.21 | not promoted | Neutral | -| Prefill Metal command buffers | 35.8 / 35.19 | not promoted | Reject | -| ASG data ring 64 KiB | 39.6 / 35.17 | did not reach 1-8 | Reject/incomplete | -| ASG write step 32 KiB, profiled | 39.2 / 34.35 | 31.2 / 46.97 | Reject: no lower write cost, worse 1-5 tail | - -Two configuration findings are important even though they are not performance -wins: - -- `VulkanQueueSubmitWithCommands` is required. Two real-TFT cold boots with it - disabled aborted the emulator while decoding - `VK_STRUCTURE_TYPE_APPLICATION_INFO` before ADB was available. -- several apparent candidates were actual defaults and therefore no-ops: - MoltenVK argument buffers off and single-queue semaphore style 0. - -The fatal queue-submit profile, ignored virtual-queue profile, and both -confirmed MoltenVK no-op controls are excluded from the executable performance -candidate manifest. Their launcher diagnostics and negative findings remain -available for reproduction without allowing an unattended campaign to select -them. - -MoltenVK performance tracking/logging reached the host process but emitted no -useful per-frame performance stream in this packaged build; the diagnostic run -also introduced a 106.93 ms outlier. It is unsuitable as benchmark telemetry. - -### Paired guest and host profiles - -The profiled control (`20260811T002030Z__profile-performance-max-heavy__60232`) -made the residual boundary cost more specific: - -- guest `simpleperf` attributed 11.54% of samples to RHI-thread `writew`, 2.31% - to `ring_buffer_available_read`, and 1.94% to - `AddressSpaceStream::speculativeRead`; -- the host sample showed most gfxstream render threads sleeping in - `RingStream::readRaw`, with only sparse active decode stacks; -- several host stacks were waiting in `vkWaitForFences` through MoltenVK; -- the profile semantic gate and all rollback checks passed. - -This rules out a continuously saturated host decoder as the main explanation -for the guest ASG hotspot. The more likely cost is guest RHI serialization, -MMIO/kicks and readbacks, plus synchronization boundaries that intermittently -stall an otherwise underfed host consumer. - -The inline-submit repeat increased the guest sample share in `writew` to -14.22%, while the 32 KiB ASG step produced 13.07%. Neither reduced the -absolute frame cost repeatably. The on-demand submit thread and the 16 KiB -write step therefore remain the local optima: the thread hides some boundary -latency, while a larger packet step does not remove the relevant work. - -### Auth-independent transport probe - -After the Riot session expired, further match A/B runs could not be performed -without credentials. The scoped Keychain helper found no configured item and -stopped before reading or entering anything. To keep transport testing -independent of Riot state, Android Settings was exercised through the same -guest HWUI/ANGLE -> Vulkan -> ASG -> gfxstream -> MoltenVK stack. Each profile -used six steady-state rounds of 30 alternating scroll animations and Android -`gfxinfo` frame statistics. - -| Gfxstream gate | Mean series time | Warm p95 / p99 | Decision | -| --- | ---: | ---: | --- | -| OSFT control | 6.629 s | 9-11 / 11-13 ms | Control | -| `VulkanBatchedDescriptorSetUpdate` off | 6.627 s | 10 / 11-12 ms | Neutral; keep current default but claim no speedup | -| `VirtioGpuFenceContexts` off | 6.603 s final round | 10 / 12 ms | No benefit versus paired control 9 / 11 ms; reject | - -Emulator startup logs confirmed that the descriptor-batching and fence-context -gates changed the host gfxstream feature set. An earlier run labelled -`VulkanVirtualQueue` off is excluded: the emulator reports that guest use of -that flag is ignored, so it was not a valid feature A/B. - -A later narrow flush sweep used 12 rounds per run, discarded three warm-up -rounds, required at least 120 rendered frames per round, and aggregated valid -runs with `scripts/summarize-android-ui-transport.command`: - -| ASG draw flush | Valid/rejected runs | Warm rounds | Mean / median series | Worst p95 / p99 | Decision | -| --- | ---: | ---: | ---: | ---: | --- | -| 600 us | 2 / 0 | 18 | 6.668 / 6.656 s | 23 / 29 ms | Reject | -| 700 us | 1 / 0 | 9 | 6.783 / 6.750 s | 22 / 36 ms | Reject | -| 800 us | 3 / 0 | 27 | 6.637 / 6.631 s | 23 / 36 ms | Control | -| 850 us | 1 / 0 | 9 | 6.692 / 6.687 s | 10 / 13 ms | No throughput gain; reject | -| 900 us | 2 / 0 | 18 | 6.614 / 6.602 s | 22 / 31 ms | 0.34% mean shift is below run noise; do not promote | -| 1000 us | 1 / 1 | 9 | 6.967 / 6.700 s | 61 / 150 ms | Reject: severe tail and one non-rendering run | - -The rejected 1000 us repeat first stalled for about 98 seconds, then produced -zero new Settings frames even though input commands returned. The probe now -fails fast on such a non-rendering round instead of allowing stale `gfxinfo` -percentiles to look like a fast result. This is direct evidence that extending -the notification interval can cross a sharp pacing boundary rather than -providing monotonic batching gains. - -A previously untested 16 KiB ASG data ring was also worse: 6.728 s warm mean -and 6.728 s median, versus 6.637/6.631 s for the pooled 32 KiB control. Together -with the earlier 64/128 KiB results, 32 KiB remains the local data-ring optimum. - -A final guest-attested feature-gate sweep superseded the earlier label-only -comparison. It exercised Android Settings through HWUI `skiavk` at 2560x1440. -Each cold-booted profile used 12 rounds and discarded the first three. Before -measurement, the probe required the requested profile to equal -`ro.boot.mactician.graphics_profile`; it also recorded the actual ASG transport -and HWUI renderer. Four OSFT controls were interleaved with the candidates, and -the two signals closest to useful (`no-fence` and `no-batching`) were repeated. - -| Vulkan/gfxstream profile | Valid runs / warm rounds | Warm mean / median | Mean delta vs pooled control | Worst p95 / p99 | Janky frames | Decision | -| --- | ---: | ---: | ---: | ---: | ---: | --- | -| OSFT control | 4 / 36 | 6.728 / 6.706 s | control | 21 / 73 ms | 43 | Keep | -| `AsyncComposeSupport` off | 1 / 9 | 6.837 / 6.846 s | +1.62% | 22 / 61 ms | 10 | Reject: slower | -| `VirtioGpuFenceContexts` off | 2 / 18 | 6.737 / 6.712 s | +0.13% | 23 / 69 ms | 16 | Neutral; no transport win | -| `VulkanBatchedDescriptorSetUpdate` off | 2 / 18 | 6.819 / 6.830 s | +1.35% | 23 / 65 ms | 14 | Reject: repeatable latency regression | -| Native-swapchain profile off | 1 / 9 | 6.795 / 6.754 s | +0.99% | 22 / 57 ms | 10 | Reject: no gain | - -The four control means ranged from 6.700 to 6.766 s. The pooled no-fence mean -was only 9 ms slower than control, well inside that 66 ms run-to-run spread; -its first favorable median did not reproduce. Disabling batching produced -fewer counted janky frames per round, but both runs had worse latency and the -worst p95 was not improved. Max-tail comparisons are also biased against the -control because it contains twice to four times as many rounds. No candidate -therefore meets a throughput-plus-tail promotion gate. Every run restored the -known AVD configuration and hardware hashes after shutdown. - -The exact per-run and pooled values are retained in -`artifacts/android-ui-transport-attested-20260811.json`; the larger raw -`gfxinfo` captures remain local runtime evidence. - -The measurement directory labelled -`feature-vulkan-no-queue-submit-commands-a` is excluded from the table. A -subsequent exact cold-boot repeat confirmed the earlier fatal failure before -ADB, so the Settings probe could not have run on that candidate AVD. It most -likely attached to a still-running control instance; its timing is invalid -evidence rather than a neutral feature result. - -The last row is a profile-level result: disabling native swapchain also made -the emulator disable Vulkan composition and `GuestVulkanOnly`, and enabled the -host GLES translator. It therefore proves that the current combined profile is -valuable, but does not isolate one implementation mechanism. The emulator also -reported that `VulkanVirtualQueue` is ignored on every boot, so it was not -misrepresented as a live A/B candidate. - -The probe's earlier `transport: pipe` field is the boot transport property, not -proof that HWUI used GLES. Host logs created Vulkan instances for `android -framework` in these runs. Schema 4 records `debug.hwui.renderer` explicitly as -`hwui_renderer` and requires the requested launcher graphics profile to match -the boot-attested `ro.boot.mactician.graphics_profile`. The attested sweep above -also fixed density at 320 dpi in its launch orchestrator and retained that -invariant in the result artifact. Schema 5 additionally reads `wm density` -inside the guest. The aggregator partitions otherwise identical label groups -by attested profile, active display and density, transport, and HWUI renderer; -legacy summaries without density are marked `legacy-unknown` rather than -silently pooled with new evidence. -Fail-fast Settings-launch, `gfxinfo` parse, and non-rendering failures also -write a schema-5 summary with `rejected_reason`, so failed candidates remain -visible in aggregate valid/rejected counts instead of disappearing as partial -directories. - -Both schema-5 paths were cold-boot validated after implementation. A one-round -OSFT control recorded `display_density: 320` and aggregated as 1 valid / 0 -rejected. A deliberately impossible 999999-frame minimum stopped after its -first rendered round, recorded `non_rendering_round_1`, and aggregated as 0 -valid / 1 rejected with null performance metrics. Both runs stopped QEMU and -restored the exact baseline AVD hashes. - -### AEMU extension-policy control - -The packaged emulator contains an -`androidboot.hardware.aemu_feature_overrides_disabled` list covering several -GLES extensions, including texture buffers, indexed draw buffers, geometry, -tessellation, shader I/O blocks, and base-vertex draws. Two reversible cold-boot -controls tested whether that policy was suppressing capabilities which guest -ANGLE could otherwise use: one removed only `textureBufferEXT`, and one supplied -an empty list. - -The standalone guest EGL probe was not a valid endpoint for this comparison: it -crashed during `eglInitialize` on the normal control as well as both modified -boots. The real TFT process was therefore used as the functional endpoint. All -three profiles reached the running game process, created the main ANGLE/Vulkan -context and four PSO-service contexts, and remained alive without a validation -or renderer crash. Their one-sample cold-start times were 1219 ms for the normal -policy, 1317 ms with only `textureBufferEXT` removed, and 1468 ms with the whole -list cleared. These samples are too few for a performance claim, but neither -modified profile produced a candidate-sized improvement. - -More importantly, `dumpsys gpu` reported the same TFT contract for the normal -and fully cleared profiles: Vulkan API `0x401000`, device-feature mask -`0x400b3fbeff3eef`, and identical instance- and device-extension hash lists. -Both created GLES, Vulkan-device, and Vulkan-swapchain state through ANGLE. This -policy is therefore not a hidden shortcut around guest ANGLE or the Vulkan -transport, and no change is promoted. A future test would need an in-package -capability probe plus a full authenticated-match A/B before revisiting it. - -The remaining hidden features are not useful macOS transport shortcuts in this -binary. Upstream and bundled-backend inspection show that `ExternalBlob` -explicitly aborts with MoltenVK/external-memory-metal; `SystemBlob` depends on -it and is marked Windows-only; host-visible udmabuf requires Linux kernel -support; and the coherent-memory gates add flushes for compatibility. -`VulkanExternalSync` exports FD/Win32 fence handles and does not provide a -Metal-native synchronization path here. - -## Bottleneck assessment - -The evidence supports the following confidence levels: - -- **High:** the limiting class is graphics command generation/submission, - guest-host transport, and downstream synchronization/consumption. -- **Medium:** repeated fence/queue boundaries and scene-dependent RHI work are - more important than raw ASG byte bandwidth alone. -- **Low:** any one currently exposed ANGLE, gfxstream, or MoltenVK flag can - deliver the roughly 2x improvement needed for 57 FPS in the heavy scene. - -The reasons are: - -1. ASG versus pipe remains the largest accepted transport improvement, proving - that the boundary matters. -2. A 16 KiB ASG write step and fewer FBO-boundary submits help, proving that - packetization/submission frequency matters. -3. Larger rings, shorter/longer flushes, queue/semaphore modes, and multiple - MoltenVK scheduling flags are neutral or worse, so the residual bottleneck - is not a single exposed buffer-size knob. -4. FPS drops materially as the board becomes more complex while resolution - scaling is comparatively weak, which implicates command/RHI and - synchronization work more than fill rate. -5. Guest profiles contain `AddressSpaceStream` reads/writes and gfxstream - descriptor work on the RHI thread, while the matching host render threads - are predominantly waiting for input rather than saturated by decode. -6. Disabling fence contexts, virtual queues, or descriptor batching does not - improve an auth-independent steady-state render probe, so the residual cost - is not one exposed gfxstream bookkeeping switch. - -## Implementation alternatives - -### 1. Keep and instrument the current path - -This is the only low-risk near-term choice. Keep the verified Performance Max -stack and instrument both sides of gfxstream before source patches. Required -measurements are writes and bytes per frame, write-size distribution, ring wait -time, kicks, Vulkan submits/fences, host decoder queue/decode time, MoltenVK -submit/completion time, and Metal GPU busy/idle time. - -Only then choose between command coalescing, adaptive kicks, descriptor/state -reduction, or host decoder changes. Removing notifications blindly is already -known to destroy pacing. - -### 2. Build a source-matched GLES 3.2 host renderer - -This remains the highest-upside Android architecture, but its minimum scope is: - -- build current ANGLE Metal with verified ES 3.2 contexts on macOS; -- integrate that ANGLE revision with a source-matched gfxstream host backend; -- build and ship the matching Android guest EGL/GLES encoder libraries or a - matching system image; -- propagate RenderControl caps and guest-advertised ES 3.2 capabilities; -- validate texture buffers, compute, geometry/tessellation, sync, surfaces, - AHardwareBuffer/gralloc, WebView, login, and a full match; -- measure GLES protocol call volume and ASG backpressure against Vulkan. - -This should be a separate R&D branch with conformance-style probes. It is not -appropriate to advertise ES 3.2 in the current binary stack. - -### 3. Direct Unreal Vulkan - -This removes guest ANGLE but retains Vulkan gfxstream, ASG, MoltenVK, and Metal. -The current Shipping APK disables Vulkan RHI and may lack cooked Vulkan shaders -or PSO data. It is viable only with a Riot build that officially enables the -Vulkan renderer; patching a runtime check is insufficient. - -### 4. Native iOS/iPadOS PBE on Apple Silicon - -This removes the entire Android VM graphics boundary and has the highest -performance ceiling. It depends on Riot/TestFlight availability and permission -to run that build on Apple Silicon macOS, so it is strategically attractive but -not controlled by this project. - -### 5. Physical mobile renderer plus streaming - -Run TFT on a phone/tablet, use hardware video encode, and forward input from the -Mac. This trades a separate device and video/input latency for a much simpler -performance model. Unlike scrcpy inside the emulator, encoding does not compete -with TFT in the same VM. - -### 6. Replace the VM shell - -Moving to Virtualization.framework, crosvm, or another VM shell does not by -itself supply Android-to-Metal GPU passthrough. A virtio-gpu/gfxstream-like -protocol, guest driver, gralloc/HWC integration, and host renderer are still -required. This is high cost with low confidence of removing the measured -bottleneck and is not a priority. - -## Recommendation - -Do not promote a spoofed native GLES mode. Keep the current supported stack and -promote only full 1-5/1-8 A/B wins with tail-latency and rollback gates. In -parallel, scope the source-matched GLES 3.2 renderer as an independent project; -its first milestone is an ES 3.2 host ANGLE probe that passes real texture-buffer, -compute, sync, and surface tests before any TFT launch attempt. - -## Upstream references - -- gfxstream source: -- ANGLE platform and feature EGL extensions: - -- MoltenVK runtime configuration: - diff --git a/docs/releasing.md b/docs/releasing.md index 05d694c..5e5c618 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,141 +1,65 @@ -# Releasing +# Releasing TFTMAC -The current metadata is Mactician version 1.0.4, build 40. Version and build -numbers live in `launcher/Info.plist` and the matching emulator-host plist. -Release notes live under `launcher/Resources/release-notes/` using the short -version as the filename. +TFTMAC releases are native macOS application builds. The release process does not publish or redistribute Riot application packages. ## Release identity -Do not casually change: - -- app bundle ID `dev.sergeinaumov.mactician`; -- game-host bundle ID `dev.sergeinaumov.mactician.game-host`; -- app name and executable `Mactician`; -- Application Support and DMG volume name `Mactician`; -- Sparkle feed `https://sergeinaumov.dev/mactician/updates/appcast.xml`; -- pinned Sparkle Ed25519 public key in `launcher/Info.plist`; -- UserDefaults domain and Keychain service `dev.sergeinaumov.mactician`; -- Android package ID, release-manifest hashes, or the `Android_Codex` device - profile identifier. - -These are the only current product identifiers. No old feed, local-data path, -redirect, alias, or compatibility wrapper is part of the release. +```text +Application: TFTMAC +Bundle ID: com.flashls1.tftmac +Architecture: arm64 +Minimum macOS: 15.0 +``` -## Prepare metadata +The working Android SDK/AVD is runtime state outside the application bundle and repository. -1. Update `CFBundleShortVersionString` and monotonically increase - `CFBundleVersion` in both plist files. -2. Add matching Markdown release notes. -3. Update the `Unreleased` section in `CHANGELOG.md`. -4. Update the release manifest only when a pinned Android/game input changes; - verify size, origin, and hash independently. -5. Run the full fast validation and review `git diff --check`. +## Pre-release validation -## Build the current ad-hoc artifact +Run the source/CI contract on the exact release commit: ```sh -PROJECT_DIR="$PWD" -TFT_GAME_APK_DIR="$PROJECT_DIR/private/tft-pbe-apks" \ - ./scripts/build-mactician.command +/bin/zsh scripts/verify-tftmac.command ``` -The v1 build signs nested code and the app with an ad-hoc identity and verifies -the final app and disk image. It is not Apple-notarized, and users must approve -the first launch through System Settings → Privacy & Security → Open Anyway. -The same pipeline can use Developer ID, hardened runtime, notarization, and a -stapled ticket in a future release. No production upload occurs in this step. - -Keep the notarization profile, Developer ID private key, Apple credentials, and -Sparkle private Ed25519 key in Keychain. Never pass secret values as committed -arguments or defaults. - -## Generate the appcast - -Exercise generation without upload: +Before any local install or package promotion, separately run: ```sh -: "${MACTICIAN_SPARKLE_ACCOUNT:?Set MACTICIAN_SPARKLE_ACCOUNT in the environment}" -./scripts/publish-mactician-update.command --prepare-only +/bin/zsh scripts/verify-installed-runtime.command ``` -This copies the DMG and release notes to versioned names, generates Ed25519 -enclosure signatures and deltas, validates XML, and requires an `edSignature`. -Prepare-only never uploads files. +The second contract checks private local machine state and must never run in +GitHub CI. Also verify that the frozen EmulatorController protocol still matches +the intended stock emulator authority and that no private runtime artifacts are +tracked. -## Publish +## Package authority -Production-specific destinations have no secret or machine-specific defaults: +The application does not package or publish TFT APKs. The supported Android package is `com.riotgames.league.teamfighttactics`, installed and updated through `com.android.vending`. Riot owns its own content initialization after launch. -```sh -: "${MACTICIAN_SPARKLE_ACCOUNT:?Set MACTICIAN_SPARKLE_ACCOUNT in the environment}" -: "${MACTICIAN_UPDATE_SSH_TARGET:?Set MACTICIAN_UPDATE_SSH_TARGET in the environment}" -: "${MACTICIAN_UPDATE_SSH_PORT:?Set MACTICIAN_UPDATE_SSH_PORT in the environment}" -: "${MACTICIAN_UPDATE_REMOTE_ROOT:?Set MACTICIAN_UPDATE_REMOTE_ROOT in the environment}" -./scripts/publish-mactician-update.command --allow-adhoc -``` - -Optional public settings are `MACTICIAN_UPDATE_BASE_URL`, -`MACTICIAN_UPDATE_PRODUCT_URL`, `MACTICIAN_UPDATE_WORKDIR`, `MACTICIAN_APP`, -`MACTICIAN_DMG`, and `MACTICIAN_RELEASE_NOTES`. +## Signing -The publisher verifies the ad-hoc app and DMG, uploads immutable versioned -artifacts first, uploads the next appcast under a temporary name, then atomically -moves the appcast into place last. Never overwrite an already published -versioned DMG with different bytes. +Local builds use the stable `TFTMAC Local Code Signing` identity created once by +`scripts/ensure-local-signing-identity.command`. This lets macOS recognize +updated local builds as the same app and retain removable-volume consent. The +private key remains in the user's login Keychain and never enters Git. This +local identity is not a public distribution identity; public distribution still +requires Developer ID signing, hardened runtime, notarization, and stapling. -`--allow-adhoc` accepts only a valid ad-hoc-signed app, verifies the DMG, and -still requires the Sparkle Ed25519 signature. +Current-host status (2026-08-31): Build 8 executable/host hashes match the +historical signed release, but the login keychain has zero valid local signing +identities and deep/strict verification reports `CSSMERR_TP_NOT_TRUSTED`. +Historical acceptance remains valid as historical evidence; a new release is +blocked until the identity is repaired and the installed-runtime verifier passes. -## Publish a TFT PBE game update +## Release evidence -Game releases use a separate signed manifest and do not require a new Mactician -build. Put the complete official split APK set in one directory and run: +Retain compact evidence for: -```sh -: "${MACTICIAN_GAME_APK_DIR:?Set the split APK directory}" -: "${MACTICIAN_GAME_VERSION:?Set the Android version name}" -: "${MACTICIAN_GAME_VERSION_CODE:?Set the Android version code}" -./scripts/publish-game-update.command --prepare-only -``` +- exact source commit; +- native build/test result; +- bundle identity; +- stock emulator/protocol authority; +- package/installer identity when runtime acceptance is part of the release; +- acceptance result and rollback state. -Review the generated payload and APK hashes. To publish, set -`MACTICIAN_UPDATE_SSH_TARGET` and `MACTICIAN_UPDATE_REMOTE_ROOT`, then rerun -without `--prepare-only`. `MACTICIAN_GAME_SIGNING_ACCOUNT` defaults to the -dedicated `mactician-game-updates` Keychain account. - -The publisher uploads immutable APK files before atomically replacing the -signed `game/manifest.json`. Never publish an incomplete split set, reuse a -release URL for different bytes, or roll the version code backwards. - -## Make the repository public - -The canonical repository is `https://github.com/tweet9ra/mactician`. Before -changing its visibility, run `./scripts/verify-repository.command`, the complete -test suite, and the secret/history scan on the exact commit that will become -public. Confirm that the repository contains no ignored build inputs, release -artifacts, credentials, signing material, or local experiment output. - -Apply the description, homepage, topics, and social preview recorded in -`.github/repository-metadata.yml`. Enable Issues and GitHub Private Vulnerability -Reporting, then verify the public Source, Issues, Security, and Releases pages in -a signed-out browser. Protect the default branch against force pushes once the -initial public commit is final. - -Create an immutable `v1.0.0` tag and GitHub release only after the corresponding -DMG and release notes are final. Publish the SHA-256 shown on the product page -with the release, upload the self-hosted Sparkle files, and publish the appcast -last. Repository settings, visibility changes, tags, releases, and uploads are -separate external actions. - -## Validation and rollback - -Before announcing a release, install the DMG on another supported Mac, verify -Gatekeeper assessment, install/update flow, Sparkle signature verification, -runtime-state preservation, first launch, Repair, stop/rollback, and a manual -update check. - -If a release is defective, stop advertising it or publish a higher, fixed -version. Do not reuse a version/build number or rotate the Ed25519 key as an -incident shortcut. A compromised private key requires an explicit security -response. +Do not retain giant generated build trees as release authority. diff --git a/docs/reproducibility.md b/docs/reproducibility.md index cef86c8..21670ad 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -1,86 +1,38 @@ # Reproducibility -This project distinguishes source declarations, release inputs, and mutable -runtime state. A reproducible source checkout does not contain the game -packages, downloaded Android runtime, AVD userdata, or signing credentials. +TFTMAC separates versioned source authority from mutable runtime state. -## Pinned release inputs +## Versioned authority -| Input | Version/build | SHA-256 | -| --- | --- | --- | -| Android Platform Tools | 36.0.2 | `106a5d31fad8c1c0c5a180d06f5779767d129d7d5edbe629005c11a85eec5b4b` | -| Android Emulator | 37.1.11 / 15917651 | `22530de9363f34ea945ecb5cad74523abd4b615f27f3c1a9899efb183ea9e144` | -| Google APIs ARM64 system image | Android 36 revision 7 | `fb47d861d6f87230ee0fe70f610d579935ca77f41a0eefbf391595d3dc4b5ee2` | -| Sparkle | 2.9.4 | `ce89daf967db1e1893ed3ebd67575ed82d3902563e3191ca92aaec9164fbdef9` | +The repository retains: -The game release is `18.1-5212127`, package -`com.riotgames.league.teamfighttactics.pbe`. The four split names, sizes, and -SHA-256 values are in `launcher/Resources/release-manifest.json`; the APK bytes -are deliberately absent from Git. +- native Xcode project and source; +- tests and validation scripts; +- `Package.resolved`; +- the exact installed EmulatorController protocol snapshot; +- compact runtime/performance evidence and relational SSOT data. -The current release manifest itself hashes to: +The current protocol authority is recorded in `Vendor/AndroidEmulator/SOURCE.json` and must match `Vendor/AndroidEmulator/emulator_controller.proto` byte-for-byte. -```text -02cf1042cdc119ed22f8ee4ea3ab5fb5448e600445f176c787e98770dc96470e launcher/Resources/release-manifest.json -``` - -## Active profile hashes - -The retained profile identifiers are compatibility-sensitive because the -Shipping game command line selects `DeviceProfile=Android_Codex`. - -```text -c9c84bec09e60d2ee965f91ef0b7b1eb687521527e478e3df5054f779137c42b artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.ini -3479ffb5482b7e8d79d04627de4ffe052d9f7b9078f4107a690a575db87bea99 artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.shader-prewarm.ini -5ab532b82e2706898d66b4325f880e0ab52982be36e70577a7a6f4aa12d2f8fa artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.performance-max.ini -183d2196f3b79fdcff1d433480e803322c1b580bfaf117991e7229c2754002b5 artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.no-frame-ahead.ini -e92e08b1f1f62b463cdbb43a522929a1cee66145b1abcf7380749e16f93ff451 artifacts/tft-pbe-18.1-5212127-direct-vulkan/Android_Codex.DeviceProfiles.ini -``` - -Other experiment profiles remain in `artifacts/` and can be hashed directly -with `shasum -a 256`; documentation does not pin hashes for inactive files. +## Mutable runtime state -## Three manifests/states +The repository does not contain: -- **Source manifest:** the committed JSON records downloadable Android - components, game split metadata, disk requirement, and UI launch profiles. -- **Release manifest:** the copy embedded in a built app. The build copies it - byte-for-byte and verifies every private game input before packaging. -- **Runtime state:** `install-state.json` in Application Support records what was - successfully installed on one Mac. It is mutable, private, and never a source - of release hashes. +- Google/Riot credentials or tokens; +- AVD userdata; +- Riot APKs or application data; +- SDK/runtime disk images; +- private session state; +- generated native build products. -The runtime also generates effective AVD configuration, downloaded package -`source.properties`, overlay hashes, and rollback sidecars. Those values prove a -specific run; they are not committed build inputs. +The normal runtime root is `/Volumes/MAC MINI M4/TFTMAC/Runtime`. -## Verify an environment +## Verification ```sh -./scripts/verify-repository.command -./scripts/test-mactician.command -./scripts/verify-environment.sh -./run-tft-best-verified.command --print-config +/bin/zsh scripts/verify-tftmac.command ``` -For an accepted benchmark, retain the effective display/density, graphics flags, -APK/profile/runtime hashes, power/thermal state, semantic before/after gates, -frame summary, and cleanup/rollback result. `--print-config` is read-only and -shows the recommended source profile without starting the emulator. - -## Release verification - -After building, compare the embedded manifest to the source, verify nested code -signatures, and hash the DMG: - -```sh -cmp launcher/Resources/release-manifest.json \ - "dist/Mactician.app/Contents/Resources/release-manifest.json" -codesign --verify --deep --strict --verbose=2 "dist/Mactician.app" -shasum -a 256 "dist/Mactician-1.0.0.dmg" -``` +For runtime acceptance, retain the exact emulator version, AVD identity, package version/code, installer authority, signer evidence, selected runtime profile, and compact telemetry/capture hashes used for the decision. -A public build additionally requires the Developer ID authority, notarization -acceptance, and a stapled ticket. Rebuilding a DMG can change container bytes; -published versioned artifacts are therefore immutable rather than assumed to be -bit-for-bit reproducible across machines. +Performance claims require the same workload and configuration on both sides of an A/B comparison. Promising results are cold-confirmed before promotion. diff --git a/docs/research-log.md b/docs/research-log.md index 355e302..38131f1 100644 --- a/docs/research-log.md +++ b/docs/research-log.md @@ -1,5 +1,10 @@ # Research log +> **Historical research only.** This log preserves earlier GLES/ANGLE, donor, +> overlay, and fixed-stage work. It must not override current Build 8 facts, +> direct Unreal Vulkan receipts, stock-runtime normal-play authority, or the +> current no-marker automatic-logging policy. + This edited log preserves the useful technical chronology and negative results. Benchmark tables and acceptance rules are summarized separately in [Benchmarks](benchmarks.md). diff --git a/docs/telemetry-contract/README.md b/docs/telemetry-contract/README.md index d88145d..7fd45c3 100644 --- a/docs/telemetry-contract/README.md +++ b/docs/telemetry-contract/README.md @@ -1,10 +1,7 @@ -# Telemetry schema v2 contract +# Historical telemetry schema v2 fixtures -These JSON files are the canonical public wire examples for Mactician telemetry. -The Swift tests verify the encoded key sets and the private API repository keeps -byte-identical copies for its HTTP contract tests. Run -`scripts/verify-telemetry-contract.command` before a server or launcher release. +These JSON files are retained only as historical contract fixtures from the retired launcher. They are not part of TFTMAC's current runtime telemetry path. -Schema v2 intentionally contains no installation identifier, account identity, -network address, host name, serial number, MAC address, or game logs. Unknown -fields are rejected by the server. +Current TFTMAC diagnostics are local-first and raw-first as documented in `docs/telemetry.md`. No remote telemetry service is required for current runtime or acceptance. + +The retained fixture schemas intentionally contain no installation identifier, account identity, network address, host name, serial number, MAC address, or game logs. diff --git a/docs/telemetry.md b/docs/telemetry.md index 74ed7f4..8356df8 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -1,102 +1,78 @@ -# Telemetry and privacy - -Mactician has two independent telemetry levels. The term "unlinkable" here -means that the JSON has no stable identifier with which to connect events. That -reduces privacy risk but does not by itself determine consent requirements in -every jurisdiction. - -## Basic first-session event - -After the first session that reached the runtime `ready` state has ended, -Mactician sends `first_game_session` once per retained macOS preferences domain: - -```json -{ - "schema_version": 2, - "event_id": "random-uuid", - "event": "first_game_session", - "occurred_on": "2026-08-09", - "duration_bucket": "30_60m", - "launcher_version": "1.0.0", - "launcher_build": "33" -} -``` +# TFTMAC Telemetry and Diagnostics + +TFTMAC diagnostics are local-first and evidence-driven. + +## Native capture + +Each native app start creates a private `0700` session directory under: -Allowed buckets are `under_5m`, `5_15m`, `15_30m`, `30_60m`, `60_120m`, -`120_240m`, and `over_240m`. The event does not contain an installation or -device identifier, exact duration/time, device properties, launcher settings, -language, identity, logs, or network addresses. - -The pending event is written to `telemetry.firstSession.pending.v2` before the -request. A retry reuses its original `event_id`. HTTP 2xx or a duplicate -acknowledgement completes it; 408, 429, 5xx, and network failures retain it. An -unrecoverable 4xx is terminal. A pending event older than seven days is deleted -without replacement. Resetting Android/TFT data does not change this state. - -The server aggregates the event immediately by received date, launcher -version/build, and duration bucket. The dashboard name is **Approximate -activated installations**. It is approximate because macOS accounts can count -separately, clearing preferences or reinstalling can count again, installs with -no completed session are absent, and an event can expire during extended -offline use. It must not be labelled `unique_users`, `people`, or `MAU`. - -## Optional extended diagnostics - -Extended diagnostics are created and sent only when -`telemetry.extendedConsent.state.v1` is `granted` for consent version 1. -`unknown` and `denied` both prohibit creation and transmission. - -Each completed session then sends an independent event: - -```json -{ - "schema_version": 2, - "event_id": "random-uuid", - "event": "game_session_diagnostics", - "occurred_at": "2026-08-09T12:34:56Z", - "duration_seconds": 2871, - "launcher_version": "1.0.0", - "launcher_build": "33", - "consent_version": 1, - "launcher_settings": { - "profile_id": "quality", - "effects_quality_id": "performance", - "display_width": 2560, - "display_height": 1440, - "display_density": 416, - "ui_scale_percent": 100, - "guest_memory_mb": 8192, - "guest_cpu_cores": 6 - }, - "device": { - "model_identifier": "Mac16,1", - "macos_version": "26.0.0", - "physical_memory_mb": 32768, - "logical_cpu_count": 10 - } -} +```text +~/Library/Application Support/TFTMAC/Captures// ``` -It never contains an installation ID, Mac name, serial number, MAC address, -Apple ID, macOS username, Riot ID, IP field, application list, or game logs. -Turning diagnostics off immediately stops event creation and deletes the entire -local diagnostic queue; it does not affect the pending basic event. A change to -the diagnostic field set increments `consent_version` and invalidates prior -consent. - -## Server retention and processing - -The API strictly rejects unknown fields and out-of-range values, bounds request -size, and deduplicates by `event_id`. It does not persist source IP or -User-Agent and never logs an invalid request body. Client addresses exist only -in the in-memory rate limiter with a short TTL. - -Basic payloads are not retained as raw events. Their aggregates are retained; -only SHA-256 event-ID hashes remain for deduplication and expire after 14 days. -Raw extended diagnostic events are retained for 90 days. Longer-lived -aggregates must avoid small identifiable cohorts. - -The release order is server compatibility, schema-v2 verification, public -privacy-policy publication, then the launcher update. No legacy event contract -is retained because the previous API and its data belonged only to the local -pre-release laboratory. +`TFTMAC_NATIVE_RUNTIME.sqlite` is the queryable session authority. `native-events.jsonl`, emulator stdout/stderr and session-scoped `logcat.raw.txt` are local sidecars. Raw logcat is sensitive, never copied into SQLite, and must not be published. + +Useful evidence may include: + +- host monotonic timestamps; +- emulator/runtime state; +- one-second native frame-ingress interval windows and visual checkpoints; +- raw-gRPC source freshness plus native Metal presentation internals retained as + hidden correctness/regression context and never called Unreal FPS; +- aggregate logcat fault counts with raw lines kept outside SQL; +- SurfaceFlinger render-rate and cumulative missed/HWC/GPU counters sampled at boundaries and every 30 seconds during gameplay; +- AudioFlinger active output, sample rate, stereo state, tracks and underruns; +- host CPU/RSS/memory-pressure samples; +- guest `/proc/meminfo` and host/guest monotonic clock calibration; +- renderer/graphics state; +- package version, installer, and signer evidence; +- explicit user stutter markers. + +The base graphics logger is automatic: it opens from the observed TFT +process/layer lifecycle, continues through process/layer replacement or loss, +and seals only at TFT process or app close. It does not wait for a match marker, +a battle classifier, or a Combat Benchmark. The Telemetry menu's +`MATCH_ENTRY`, `VISIBLE_STUTTER`, and `MATCH_END` remain optional user context; +the controlled Combat Benchmark remains an optional A/B protocol. `gfxinfo` is +not Unreal/Vulkan frame authority. Perfetto remains a bounded incident +diagnostic rather than an always-on observer. + +The current source schema associates automatically captured samples with +`graphics_runs`, records a canonical graphics-stack receipt and SHA-256 at each +snapshot, and links exact guest intervals to their containing frame window when +available. Every exact guest interval and one-second game window also carries +the active immutable `stack_sha256`, so stack identity survives incomplete +window joins and later layer changes. These source-level changes are +**VERIFIED CURRENT runtime** evidence through the Build 8 automatic captures. A stack receipt establishes +the observed route/configuration for that sample; it does not prove causal +ownership of a slow frame. + +## Retention + +Keep only evidence that protects a current product decision: + +- latest successful playable baseline; +- latest native-app acceptance capture; +- current package-authority evidence; +- current promoted A/B evidence; +- current unresolved crash/failure capture. + +Superseded runs should be compacted to their session ID, configuration hash, verdict, key metrics, and relevant source hashes before raw bulk is removed. + +## Privacy + +Diagnostics must not intentionally capture or publish Google/Riot credentials, tokens, cookies, account identifiers, private Android userdata, or unrelated application data. Sanitize any excerpt before sharing it. + +No remote telemetry service is required for the current TFTMAC runtime or acceptance path. + +## Conservative graphics views + +The automatic logger may construct per-window stack joins through +`graphics_run_id`, direct per-frame `stack_sha256`, frame-window linkage, and +the matching snapshot receipt. Its user-facing views are conservative: `TFT` +identifies exact SurfaceFlinger presentation and `PIPE` identifies controller +freshness/transport delivery. The final TFTMAC presenter is retained only as a +hidden correctness receipt. A report must use `UNKNOWN` when a trusted work +handoff is missing; current Build 8 evidence cannot identify an internal +graphics owner. CPU, RAM, thermal, power, and audio samples remain +health/correctness context only in this graphics-only optimization effort. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 5d263a0..c836088 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,97 +1,33 @@ -# Troubleshooting +# TFTMAC Troubleshooting -Use the launcher's recovery actions before manually changing AVD files. Keep -diagnostics narrow and sanitized. +Use TFTMAC's verified runtime controls and diagnostics before changing protected AVD/runtime state manually. -## Installation does not complete +## Native build fails -Check available disk space and Internet access, then retry. Partial component -downloads are resumable. **Repair Installation** rechecks the host, manifest, -component layout, hashes, runtime template, AVD, and game installation without -clearing an otherwise valid AVD. +Run: -The launcher log is under -`$HOME/Library/Application Support/Mactician/logs/launcher.log`. -Share only the small section around the error after removing home paths, -identifiers, and unrelated output. +```sh +/bin/zsh scripts/verify-tftmac.command +``` -## Component hash verification fails +Repair only the reported source, dependency, protocol, or test failure. Do not recreate the retired source-built emulator laboratory as a generic repair step. -Do not edit the expected hash. Remove only the named incomplete archive from -the launcher's `downloads/` directory and retry on a trusted network. A repeat -failure can mean an upstream archive changed; that requires a reviewed manifest -update, not a local bypass. +## Stock emulator does not start -For build-time APK mismatches, confirm `TFT_GAME_APK_DIR` contains the exact -four pinned, unmodified splits listed in `release-manifest.json`. +Verify that `/Volumes/MAC MINI M4/TFTMAC/Runtime` is mounted and that its SDK and AVD are intact. Do not silently create a replacement runtime on the internal disk. -## Emulator does not start +## Google Play or TFT requires authentication -- Confirm the Mac is Apple Silicon, macOS is 12.0 or later, and Hypervisor - Framework is available. -- Run `./scripts/verify-environment.sh` for the source-tree runtime. -- Set `TFT_ANDROID_SDK_ROOT`, `ANDROID_SDK_ROOT`, or `ANDROID_HOME` when the SDK - is outside the standard macOS directory. -- Set `TFT_AVD_HOME` and `TFT_AVD_NAME` when using an external source-tree AVD. -- Use **Repair Installation** for missing launcher-managed components. -- Use **Reset** only when the launcher reports corrupted AVD state and preserving - Riot/game state is less important than recreating the AVD. +Complete sign-in, MFA, consent, or CAPTCHA through the official Android/Google/Riot UI. Do not automate around those gates. -## Another emulator process is running +## TFT package is missing or stale -Close other Android Emulator or PlayDroid/OSFT virtual machines before launch or -provisioning. The project isolates its ADB server on port 5038, but simultaneous -VMs still compete for host CPU/GPU and can mutate shared expectations. Do not -kill an unrelated process unless you have identified it. +Use the official Google Play path. Confirm the package is `com.riotgames.league.teamfighttactics` and the installer is `com.android.vending`. Do not use third-party APK mirrors, repackaging, re-signing, or binary patching. -## TFT restarts inside the AVD +## Performance is poor -The launcher keeps the verified APK and device-profile overlays mounted for the -whole AVD session so a Riot disconnect or TFT process restart retains the -selected graphics path. UI scale and game locale are applied during launcher -startup; if an independent restart does not retain the intended presentation, -close the emulator and press Play again. - -## Hotkeys do not work - -Grant Accessibility permission to **Mactician**, not the emulator, in -System Settings. The bridge retries after permission is granted. Hotkeys are -deliberately inactive outside TFT's exact `GameActivity`; the official Riot -login WebView and unknown activities pass input through unchanged. - -## Repair versus Reset - -**Repair Installation** preserves the AVD, Riot sign-in, game assets, and -preferences while re-verifying and refreshing launcher-owned pieces. - -**Reset** removes the entire Application Support directory, including the AVD, -Riot sign-in, game data, downloads, state, and logs. It cannot be undone by the -launcher. - -## Streaming-install cache is damaged - -An interrupted public asset download can leave a zero-byte -`StreamingInstalls/Metadata.manifest`. The runtime detects exactly this case and -removes only `StreamingInstalls` so TFT can download public assets again. It -does not clear Riot sign-in or other app data. Use Repair to refresh this logic -on an existing installation. - -## Pinned game version does not match - -The launcher intentionally refuses to mount an OpenGL overlay over an unknown -`base.apk`. Install a launcher release built for the new TFT version. Do not -replace hashes or patch the installed APK to force compatibility. - -## Update check fails - -Confirm Internet access and system time, then retry **Check for Updates…**. Do -not disable Ed25519 verification or replace the appcast URL. If the feed is -temporarily unavailable, the installed launcher remains usable; updates are not -automatic without confirmation. +Use the existing raw-first capture tooling. Compare one variable at a time against the accepted baseline and cold-confirm any improvement before keeping it. High / 60 / Performance OFF is the user-confirmed current playable configuration. Riot Performance Mode Beta and Ultra High are rejected for current usability on the target M4 host. ## Safe diagnostics -Prefer launcher status, `wm size`, `wm density`, filtered process lists, -`dumpsys input`, `dumpsys display`, `dumpsys gfxinfo`, and short, purpose-built -captures. Never post complete game logs, AVD images, `/data/anr` archives, -Keychain output, cookies, tokens, credentials, or unfiltered crash memory. +Prefer bounded process/runtime state, SurfaceFlinger metrics, filtered logs, package metadata, and purpose-built captures. Never share complete game logs, AVD images, credentials, tokens, Keychain output, cookies, or unfiltered crash memory. diff --git a/facts.md b/facts.md new file mode 100644 index 0000000..c12abc3 --- /dev/null +++ b/facts.md @@ -0,0 +1,564 @@ +# TFTMAC Facts + +**Authority date:** 2026-08-31 America/Chicago +**Observed runtime evidence through:** 2026-08-31T23:13:30Z +**Purpose:** preserve facts and hard boundaries that future TFTMAC work must not casually reinterpret. + +This file separates durable product facts from mutable observations and historical +results. A statement becomes a project fact only when it has direct machine, +runtime, source, SQL, or user-acceptance evidence. Requested settings are not +effective settings; presentation cadence is not Unreal FPS; a hypothesis is not a +result. + +Exact benchmark formulas, automatic full-run analysis, AI-readable output +shape, and current findings live in `benchmark.md`. + +## Evidence vocabulary + +| Label | Meaning | +| --- | --- | +| **LOCKED** | Product or safety contract. Change only through an explicit, evidence-backed project decision. | +| **VERIFIED CURRENT** | Directly observed on the current Mac, installed app, source tree, or live runtime. Mutable software/version facts must retain an observation date. | +| **VERIFIED HISTORICAL** | Direct result from an earlier capture or campaign. Useful evidence, but not automatically transferable to the current M4/native runtime. | +| **USER ACCEPTED/REJECTED** | Direct usability evidence supplied by the person playing TFT. It is decisive for playability but does not by itself identify a software boundary. | +| **INFERRED** | Evidence-supported explanation that has not been observed at the claimed internal boundary. | +| **UNKNOWN** | Not measured or not validly attributable. Never silently promote this to fact. | + +## 1. Host machine + +| Fact | State | Evidence | +| --- | --- | --- | +| Computer | **VERIFIED CURRENT:** Mac mini, model identifier `Mac16,10`, model number `MU9D3LL/A` | `system_profiler SPHardwareDataType`, 2026-08-30 | +| SoC | **VERIFIED CURRENT:** Apple M4 | `system_profiler` | +| CPU topology | **VERIFIED CURRENT:** 10 physical/logical cores: 4 performance and 6 efficiency | `system_profiler`; `sysctl hw.physicalcpu`, `hw.perflevel*` | +| GPU | **VERIFIED CURRENT:** integrated Apple M4 GPU, 10 cores, Metal supported | `system_profiler SPDisplaysDataType` | +| Unified memory | **VERIFIED CURRENT:** 16 GB; exact `hw.memsize` is 17,179,869,184 bytes | `system_profiler`; `sysctl hw.memsize` | +| CPU/GPU clock | **VERIFIED CURRENT BOUNDARY:** this Mac does not expose a fixed project-controlled CPU or GPU MHz value through the audited `system_profiler`/`sysctl` fields. Frequency is not a TFTMAC emulator knob. Do not invent an 800 MHz or 1600 MHz virtual-GPU fact. | Live host audit | +| Power | **VERIFIED CURRENT at audit:** AC power | `pmset -g batt` | +| Thermal/power warnings | **VERIFIED CURRENT at audit:** no thermal, performance, or CPU-power warning had been recorded | `pmset -g therm` | +| macOS | **VERIFIED CURRENT:** macOS 26.6.2, build 25G83; Darwin 25.6.0 | `sw_vers`; `system_profiler` | +| Architecture | **VERIFIED CURRENT:** arm64 | Xcode/Swift target and stack lock | +| Xcode | **VERIFIED CURRENT:** Xcode 26.6, build 17F113, authoritative path `/Applications/Xcode-26.6.0.app/Contents/Developer` | `xcodebuild -version`; `ssot/STACK.lock.yaml` | +| Swift | **VERIFIED CURRENT:** Apple Swift 6.3.3, arm64 macOS target | `swift --version` | + +The current Mac is not the same host as the older M1 Max performance campaign. +Historical M1 Max results remain valid for their recorded environment, but they +must never be presented as measurements of this M4 Mac mini. + +## 2. Product identity and user experience + +- **LOCKED:** TFTMAC is its own native macOS application. It is not a Clara app, + does not ship through Node, and does not depend on a Clara service to play. +- **LOCKED:** the application uses AppKit window behavior, a native Metal-backed + display, normal macOS menus and window controls, and native macOS fullscreen. +- **LOCKED:** the Android Emulator's Qt window stays hidden in normal operation. +- **LOCKED:** the complete Android display is shown aspect-correctly. The normal + guest/output target is 1920×1080; a 16:9 fullscreen content region is filled. +- **LOCKED:** no scrcpy or encoded-video path is the production display path. +- **VERIFIED CURRENT:** bundle identifier `com.flashls1.tftmac`. +- **VERIFIED CURRENT:** installed release `/Applications/TFTMAC.app`, version + 2.3.0, build 8, arm64. Capture + `2026-08-31T21-39-18.396Z-fe34e3a1-fb91-44eb-804f-4ca8519dfc31` + directly proves authorized ADB, official TFT PID observation, exact + `GameActivity` SurfaceView selection, automatic graphics-run admission, + `COMPLETE` stack receipts, and frame-to-run/hash/window/receipt linkage. +- **VERIFIED CURRENT:** `/Users/flash/Desktop/TFTMAC.app` is a symlink to the + installed `/Applications/TFTMAC.app`, so the Desktop launcher opens the same + release rather than a second copy. +- **VERIFIED CURRENT:** `LSSupportsGameMode=true` is present in the installed + bundle. This establishes Game Mode eligibility, not proof that macOS activated + it during a particular match. +- **VERIFIED CURRENT:** the official full-bleed penguin-samurai icon is embedded. + The source, 1024-pixel output, and ICNS hashes are frozen in + `ssot/runtime-authority.json`. +- **VERIFIED HISTORICAL RELEASE ACCEPTANCE:** Build 8 was signed with the local + `TFTMAC Local Code Signing` identity and passed deep/strict verification when + the release receipt was created. It is not notarized for public distribution. +- **VERIFIED CURRENT HOST AUDIT (2026-08-31T23:13:30Z):** the installed TFTMAC + executable and emulator-host hashes still match that Build 8 receipt, but the + login keychain now exposes zero valid code-signing identities and + `codesign --verify --deep --strict` fails with `CSSMERR_TP_NOT_TRUSTED`. + Current-host installed-runtime verification is therefore blocked until the + local identity is repaired in a separate operational task. This does not + rewrite the historical acceptance result. + +## 3. Launch and ADB architecture + +- **LOCKED:** launch the emulator through the packaged `TFTMAC Emulator Host.app` + in the logged-in user's macOS session: + + ```text + /usr/bin/open -n -W --env ... --args ... + ``` + +- **LOCKED:** do not replace that host chain with direct Node/Clara `spawn()`. + The discarded direct-service path changed the ADB execution/session identity + and produced an unauthorized guest. +- **LOCKED:** ADB server port `5038`; emulator console port `5582`; serial + `emulator-5582`. +- **LOCKED:** do not inject `ADB_VENDOR_KEYS`. The launcher uses the logged-in + user's established ADB identity and clears inherited `ADB_SERVER_SOCKET` and + `ANDROID_ADB_SERVER_ADDRESS` values. +- **VERIFIED HISTORICAL ROOT CAUSE:** the incorrect direct path used `5040`, + `5592`, and `emulator-5592`, leading to `unauthorized` and eventually + `Timed out waiting for emulator ADB device.` The Android runtime, AVD, GPU, + RAM, CPU, and proven host launcher were not the cause of that failure. +- **LOCKED:** authenticated EmulatorController is loopback-only, discovered from + the launched emulator's PID-bound registration, and authenticated with a token + held only in memory. +- **VERIFIED CURRENT:** controller port request `8554`; emulator option + `-grpc-use-token`; raw controller frame limit raised to 16 MiB because a + 1920×1080 RGBA frame is 8,294,400 bytes and exceeds gRPC's former 4 MiB + default. + +## 4. Android runtime and official package + +| Item | Current authority | +| --- | --- | +| Runtime root | `/Volumes/MAC MINI M4/TFTMAC/Runtime` | +| SDK root | `/Volumes/MAC MINI M4/TFTMAC/Runtime/SDK` | +| Emulator | Android Emulator 37.1.11.0, build 15917651 | +| AVD | `TFT_Ultra_Tablet` | +| Guest | Official Google Play ARM64 system image, API 36, image revision 7 | +| Platform tools | 37.0.1 | +| WebView provider | `com.google.android.webview` | +| WebView version observed | 151.0.7922.199 | +| TFT package | `com.riotgames.league.teamfighttactics` | +| TFT version observed | `18.1-5402721`, version code `8402721` | +| Installer authority | `com.android.vending` / Google Play | +| Unreal activity | `com.epicgames.unreal.GameActivity` | +| Riot login activity | `com.riotgames.platformui.mobilefre.MobileFREWebViewActivity` | + +- **LOCKED USER FACT:** current TFT uses Unreal Engine. Engineering work must + reason about the current Unreal-based client, not the retired legacy engine. +- **LOCKED USER PREMISE:** the former native Mac path no longer supplies the + current playable client after the engine transition; TFTMAC exists to present + the official Android Unreal client as a native Mac experience. +- **LOCKED:** Google Play owns install/update delivery; Riot owns authentication, + game content, and the signed TFT package. +- **LOCKED:** do not mirror, modify, patch, inject into, re-sign, or privately + distribute Riot's APK or signed assets. +- **LOCKED:** Riot credentials, Google credentials, CAPTCHA, MFA, and consent are + entered manually in the official UI. Do not log or automate them. +- **MUTABLE:** package and WebView versions can change through their official + stores. Every session must record what was actually installed. +- **UNKNOWN:** the TFT signer digest has been observed in Android package output, + but has not yet been promoted as a durable project-authority digest. + +## 5. Current graphics and audio pipeline + +```text +TFT Unreal direct-Vulkan workload + -> guest Vulkan command stream + -> gfxstream over virtio-gpu ASG + -> MoltenVK Vulkan-to-Metal translation + -> Apple Metal / M4 GPU + -> Android SurfaceFlinger actual presentation + -> authenticated raw RGBA frame stream + -> TFTMAC Metal presenter + -> native macOS fullscreen window +``` + +Boundary rules: + +- Unreal owns game simulation, effects, render-thread/RHI workload, and the + game's actual frame production. +- **VERIFIED CURRENT PATH:** the latest stack receipt identifies + `UNREAL_ENGINE_VULKAN`. ANGLE may be present for another guest/package path, + but is not assumed to render TFT unless a per-run receipt proves it. +- ANGLE owns GLES-to-Vulkan translation only when a game selects GLES/EGL and + Android selects ANGLE for that package. Its ES 3.2 exposure is a compatibility + route, not general conformance proof. +- gfxstream/ASG owns guest-to-host graphics command transport. Configured ring, + buffer, and flush values do not prove per-frame transport latency. +- MoltenVK owns host Vulkan-to-Metal translation. Environment values are + requested values unless effective behavior is directly measured. +- TFTMAC owns host launch/session control, the final completed-frame copy, + orientation/scaling, native input mapping, output cadence, and local evidence. +- Two different components use Metal: MoltenVK for emulated Vulkan work and + TFTMAC for final image presentation. A fast final presenter does not prove the + upstream game/transport pipeline is fast. + +## 6. Proven Control profile + +The current control is the following complete configuration. Change one +declared test factor at a time; do not silently mix profiles. + +| Variable | Control | +| --- | --- | +| Profile | `tftmac_5gb_native_v1` | +| Guest display | 1920×1080 | +| Density | 320 dpi | +| Refresh target | 60 Hz | +| vCPU | 6 | +| Guest RAM | 5120 MiB | +| GPU | host | +| Audio | CoreAudio | +| Transport | `virtio-gpu-asg` | +| ASG write buffer | 1,048,576 bytes / 1 MiB | +| ASG write step | 16,384 bytes / 16 KiB | +| ASG data ring | 32,768 bytes / 32 KiB | +| ASG draw flush | 800 microseconds | +| Emulator features | `GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts` | +| ANGLE enabled | `exposeNonConformantExtensionsAndVersions:exposeES32ForTesting` | +| ANGLE disabled | `preferSubmitAtFBOBoundary` | +| MoltenVK synchronous submits | 0 / disabled | +| MoltenVK max active command buffers | 64 | +| MoltenVK fast math | 1 / enabled | +| TFT graphics | High | +| TFT FPS cap | 60 | +| Riot Performance Mode Beta | OFF | + +The current settings UI safely exposes only: + +- vCPU: 4, 6, 8; +- guest RAM: 4096, 5120, 6144 MiB; +- refresh target: 30 or 60 Hz; +- ASG draw flush: 400 or 800 microseconds; +- named experiment: Control or Combat Latency A. + +The existence of a UI choice does not mean it is faster. All restart-bound +values require a clean, logged relaunch. + +## 7. Native input, login, power, and audio + +- **VERIFIED CURRENT:** Mac primary-button down/drag/up maps to Android + EmulatorController `TouchEvent`, identifier `0`, non-zero contact pressure, + and explicit zero-pressure release. Desktop mouse events are not the primary + touch path. +- **VERIFIED CURRENT:** keyboard input uses EmulatorController `sendKey`. + SQLite stores character count and special-key name only, never typed text. +- **VERIFIED HISTORICAL:** the Riot login form expects the private Riot account + username, not an email address and not the public `Name#Tag` Riot ID. +- **VERIFIED CURRENT RISK:** Riot's WebView can ANR on input dispatch. The narrow + proven recovery keeps TFTMAC and the emulator alive, restores + `show_ime_with_hard_keyboard=0`, restarts only Riot's failed process, and + reopens the official activity. This is recovery evidence, not proof that the + mutable WebView/IME defect can never recur. +- **LOCKED:** secure Android PIN unlock stays manual. TFTMAC may record that + unlock is required, but never the PIN or typed content. +- **VERIFIED CURRENT:** Build 7 establishes virtual AC power, Android stay-awake + state, and `mWakefulness=Awake` before unlock/gameplay. It logs + `GUEST_GAMEPLAY_POWER_READY` or fails clearly. +- **VERIFIED CURRENT:** emulator requests `-audio coreaudio`; prior live evidence + observed active 48 kHz stereo output and no partial/empty underruns. +- **UNKNOWN/USER ACCEPTANCE:** software audio health does not prove the user can + hear sound at their selected speakers/headphones. + +## 8. Frame-rate truth + +The on-screen overlay labels two host-facing rates: + +```text +SRC · OUT +``` + +- `SRC` is distinct authenticated gRPC images received per second. +- `OUT` is TFTMAC Metal presentation cadence. +- Neither is automatically Unreal gameplay FPS. +- A repeated/static image can be presented at 60 OUT while TFT produces fewer + useful frames. +- `dumpsys gfxinfo` is not authoritative for TFT's native Unreal/Vulkan + SurfaceView. +- Player-visible game-frame authority is the exact TFT SurfaceFlinger layer's + actual-present timestamps, summarized into one-second windows. +- p95, p99, maximum interval, 1% low, jank, severe stalls, repeated images, and + visible-stutter markers matter more than a flattering average. + +## 9. Logging and SQL authority + +Every app launch creates a private mode-0700 capture directory: + +```text +~/Library/Application Support/TFTMAC/Captures// + TFTMAC_NATIVE_RUNTIME.sqlite + native-events.jsonl + emulator.stdout.log + emulator.stderr.log + logcat.raw.txt + bounded diagnostic artifacts +``` + +Persistent normalized comparison authority: + +```text +~/Library/Application Support/TFTMAC/TFTMAC_LAB.sqlite +``` + +The implemented “SQL logging system” is local SQLite. It is not a MySQL server +and does not require a network database service. + +The session database is raw query authority. The persistent lab links raw +captures by session/configuration/artifact identity rather than duplicating +credential-bearing or full raw data. + +| SQL table | Fact represented | Normal cadence | +| --- | --- | --- | +| `sessions` | session start/end/status/profile | boundary | +| `runtime_receipts` | requested/effective launch, package, ports, renderer, profile | startup and receipts | +| `events` | lifecycle, ADB, package, markers, failures, process/layer changes | event-driven | +| `frame_samples` | bounded image/hash/dimension/sequence checkpoints | boundary/checkpoint | +| `frame_interval_windows` | source ingress intervals/drops | 1 second | +| `presentation_samples` | labeled SRC/OUT/mailbox behavior | about 1 second | +| `game_frame_intervals` | exact TFT actual-present intervals and flags | each observed guest frame | +| `game_frame_windows` | FPS, 1% low, p50/p95/p99/max, jank/severe/misses | 1 second | +| `stream_freshness_windows` | changed/repeated frames and transport loss | 1 second | +| `host_presentation_windows` | final Metal submissions/completions/GPU time/errors | 1 second | +| `resource_samples` | QEMU CPU/RSS, TFT PID/activity | 5 seconds | +| `guest_memory_samples` | guest memory and swap | 5 seconds | +| `host_resource_samples` | host memory pressure, thermal, power source | 5 seconds | +| `clock_sync_samples` | host/guest midpoint and round-trip error | 30 seconds | +| `surfaceflinger_samples` | render rate and cumulative miss counters | boundaries and 30 seconds | +| `audio_samples` | audio output/rate/stereo/tracks/underruns | boundaries and 30 seconds | +| `logcat_aggregates` | ANR/fatal/LMK/renderer/audio counts | 5 seconds | +| `pipeline_log_aggregates` | gfxstream/ASG/Vulkan/MoltenVK/shader/fence counts | 5 seconds | +| `graphics_pipeline_snapshots` | effective layer and graphics identities | 30 seconds | +| `graphics_runs` | automatic TFT process/layer lifetime, configuration SHA, target FPS, start/end reason | process/layer lifecycle | +| `graphics_pipeline_incidents` | automatic exact-layer degradation and conservative causal unknowns | bounded incident | +| `diagnostic_artifacts` | trace path/hash/processor/normalization status | event-driven | +| `combat_benchmarks` | complete benchmark configuration, coverage, validity, metrics | benchmark end | +| `combat_incidents` | trigger metrics, trace link, first divergent boundary/unknowns | incident | +| `combat_comparisons` | control/candidate deltas and decision | comparison | +| `game_process_sessions` | TFT PID lifetime | process transition | +| `input_samples` | touch metadata and keyboard counts only | each input event | + +- **VERIFIED CURRENT:** base graphics logging opens automatically from the + observed TFT process/layer and seals only at process/app close. It is + independent of match markers, Combat Benchmark controls, and battle + classification. +- **VERIFIED CURRENT:** stack receipts, SHA-256, `graphics_run_id`, and exact + interval/window joins are written continuously. They prove scope and receipt + integrity, not internal causal ownership. +- **LOCKED:** the final native Mac presenter remains a hidden correctness + receipt. It is not displayed, ranked, or selected as a graphics root cause. +- **UNKNOWN / PLANNED:** no shared work ID or source-site span currently crosses + guest submit, ASG/gfxstream, host Vulkan, MoltenVK, and Metal. The current + logger cannot identify an internal root; advanced causal instrumentation is + planned in an isolated diagnostic runtime. + +Privacy facts: + +- Raw logcat and raw traces are sensitive local sidecars and are not pasted or + published without deliberate sanitization. +- No screenshot, raw frame payload, username, password, email, token, cookie, + PIN, CAPTCHA, MFA value, or typed content belongs in SQL or project history. +- No remote telemetry service is required. + +## 10. Benchmark contract + +- **LOCKED:** complete automatic TFT process/layer runs are preferred + product-performance evidence. Every logged frame and resource/pipeline sample + inside the lifecycle participates; match/combat markers and classifiers are + optional annotations only. +- **LOCKED:** the current UI/source-named Combat Benchmark remains a faster + 5–8 minute bounded A/B screen; it does not gate base graphics logging or + replace a full-run promotion check. +- **LOCKED:** graphics optimization decisions use graphics cadence and boundary + evidence. CPU/RAM/audio observations remain health/correctness context only; + this effort does not optimize them. +- **LOCKED:** the product target is useful-frame cadence of at least 60 FPS + throughout the complete run, not a flattering average or selected scene. +- **LOCKED:** `benchmark.md` is the formula and reporting authority. Historical + `docs/benchmarks.md` results retain their original M1 Max/userdebug scope. + +- Start the bounded A/B during representative continuous gameplay; no semantic + phase marker is required. +- Minimum valid combat duration: 300 seconds. +- Automatic close: 480 seconds. +- Exact TFT layer/timestamp availability: at least 95%. +- Clock coverage: at least 95%. +- p95 clock RTT at or below 2 ms permits precise cross-boundary attribution; + 2–10 ms permits coarse ordering; above 10 ms means cross-host cause UNKNOWN. +- Start trace: one bounded 20-second, 32-MiB Perfetto trace. +- Incident traces: at most two 15-second, 32-MiB traces. +- Automatic incident trigger: two adjacent one-second windows with 1% low below + 30 FPS, p99 at least 50 ms, or severe stalls. +- Trace cooldown: 120 seconds; no concurrent traces. +- If trace-active versus trace-inactive windows differ by more than 5%, retain + performance data but mark trace causality `OBSERVER_OVERHEAD_INVALID`. +- Valid traces are SHA-256 sealed and normalized by pinned + `trace_processor_shell` v58.2. Pinned SHA-256: + `d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa`. + +Decision rules: + +- **HOME_RUN:** after the weighted-FPS +5% guard, 1% low +20% or more, jank and + severe rates each -30% or more relative, and either weighted FPS +10% or p95 + interval -15%. +- **PROMISING:** weighted FPS +5% or more and 1% low +10% or more, with no + correctness or tail regression. +- **REJECT:** gain below 5%, p95/p99 worsens at least 10%, or any boot, render, + input, audio, login, memory, cleanup, or usability regression. +- **INCONCLUSIVE:** invalid workload/coverage/synchronization, incompatible + configuration identity, or result between thresholds. +- A winning candidate still requires a five-minute cold confirmation before + normal-use promotion. +- **LOCKED:** `HOME_RUN`/`PROMISING` are relative candidate decisions, not proof + that the product target is met. The separate full-run status remains + `TARGET_NOT_MET` until useful-frame cadence holds at least 60 FPS throughout. + +## 11. Verified results and decisions + +### Current native/runtime evidence + +- **VERIFIED CURRENT:** automatic capture + `2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200` recorded an + uninterrupted 42m27s TFT graphics run (PID 2774) with 144,364 exact + SurfaceFlinger intervals, 99.629% exact-layer coverage, and 189 degradation + incidents. Weighted FPS was 56.98, 1% low 21.49 FPS, p95 21.510 ms, p99 + 33.434 ms, and 53.72% of intervals missed the 60-FPS budget. +- **VERIFIED CURRENT BOUNDARY:** final native presentation remained near 60 Hz + during this run, so it is retained only as hidden correctness context. The + first internal graphics boundary remains `UNKNOWN`; present evidence localizes + lateness upstream of or at the exact TFT SurfaceFlinger layer. + +- **VERIFIED CURRENT:** Build 7 live capture + `2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494` + reached authenticated ADB, a 1920×1080 RGBA first frame, awake/stay-on guest + power, logger health, official TFT package receipt, and + `TFT_READY_FOR_USER` under `combat_latency_a`. +- **VERIFIED CURRENT:** configuration SHA-256 + `05039d1fd0987f46fc7da8de5f483d8c7ffaf8f39bd1eaecdd1aee11603bbb07`. +- **VERIFIED CURRENT:** host pre-exec QoS request returned 0 and read back + `user_interactive`, relative priority 0. +- **UNKNOWN:** this does not prove every QEMU worker inherited that scheduling + class or that combat performance improved. +- **VERIFIED CURRENT:** the same live session recorded Riot WebView/input ANR + aggregates and a narrow Riot-only recovery. Do not misclassify that as a + graphics improvement or as a whole-emulator crash. +- **VERIFIED CURRENT:** the user marked one complete Build 7 match in that same + session from event 1442 at `2026-08-31T03:19:25Z` through event 3065 at + `2026-08-31T03:51:00Z`, a 1,895.054-second / 31m35.054s range. +- **VERIFIED CURRENT:** the marked run contains 93,724 exact TFT + actual-present intervals: weighted FPS 49.449, 1% low 16.300 FPS, p95 + 33.822 ms, p99 48.746 ms, maximum 1,254.162 ms, 19.110% jank, and 0.610% + severe intervals. Exact-layer measured coverage was 100%, with one stable + layer and no history truncation. +- **VERIFIED CURRENT:** 58,925 intervals (62.871%) exceeded the 60 FPS frame + budget; total budget overrun was 357,921.976 ms; the longest consecutive + budget-miss run was 325 intervals; and 1,599 of 1,693 complete one-second + windows (94.448%) were below 60 FPS. +- **VERIFIED CURRENT:** final TFTMAC Metal output averaged 59.968 FPS with zero + drawable/command errors and maximum recorded final-presenter GPU time + 3.267 ms, while 23,231 presentations reused a source frame. This makes the + final pass a poor explanation for the missing useful frames in this match; it + does not identify which upstream component was first late. +- **UNKNOWN:** this full run is not a candidate-vs-Control decision. It has no + matched Control/formal benchmark row, and p95 clock RTT was 86.757 ms, so + cross-host ownership is invalid. `benchmark.md` preserves the complete result. + +### Rejected Home Run A / Riot Performance Mode Beta + +- **USER REJECTED:** worst gameplay/graphics experience; do not select again. +- **VERIFIED HISTORICAL:** 480.646 seconds; weighted FPS 56.665; 1% low 17.698; + p95 21.760 ms; p99 34.335 ms; maximum frame 517.488 ms; jank rate 4.554%; + severe rate 0.290%. +- **VERIFIED HISTORICAL:** incident 1% lows 1.932 and 4.629 FPS; one incident + included a 517.488 ms p99/max interval. +- **INVALID FOR CAUSAL ATTRIBUTION:** p95 clock RTT 87.318 ms and observer + overhead invalidated cross-boundary ownership claims. +- **LOCKED DECISION:** `home_run_a` remains only as a historical migration/raw + receipt enum. It is not selectable. Riot Performance Mode Beta stays OFF. + +### Earlier fixed-stage graphics campaign + +These are **VERIFIED HISTORICAL** results from a different M1 Max/userdebug +campaign, useful for candidate selection but not current M4 runtime performance: + +- ASG versus pipe at exact stage 1-1: 40.1 FPS / 34.85 ms p95 versus + 29.6 FPS / 49.75 ms p95. ASG selected. +- Three controls at Trial 1-2/1-5/1-8: 40.60 / 36.03 / 27.83 FPS. +- Confirmed 67% effects/LOD profile: 45.20 / 38.50 / 33.80 FPS; stage-1-8 + p95 35.07–35.95 ms. +- 16 KiB ASG write step beat paired 4 KiB control and was retained. +- 2560×1440 versus 1600×900 changed a controlled stage-1-5 result only from + 30.5 to 31.3 FPS despite 2.56× source pixels, indicating that scene was not + primarily pixel-fill bound. +- The historical selected stack did not achieve the 57 FPS heavy-scene goal and + did not reproduce the user's worst approximately 15 FPS gameplay period. + +### Graphics council and ZoeMC simulation + +- **VERIFIED HISTORICAL:** specialist research covered Unreal, ANGLE, gfxstream/ + ASG, MoltenVK, Metal, and transferable Fortnite/Unreal measurement categories. +- **VERIFIED HISTORICAL:** the 10,000-world ZoeMC v0.2 run used declared + subjective priors. It is hypothesis-ordering evidence, not empirical proof. +- **RESOLVED BRANCH:** the simulation's decisive next test—authenticated native + frame delivery—was completed. Raw authenticated gRPC is viable for correct + 1920×1080 native presentation and input. +- **NOT PROMOTED:** direct MMAP zero-copy without producer fencing, direct + MoltenVK texture sharing without a supported contract, encoded video/scrcpy, + and external emulator-window presentation. +- Fortnite/Unreal research can suggest counters, trace categories, pipeline-cache + ideas, and workload hypotheses. It does not prove TFT-specific capabilities or + justify altering Riot's signed shaders. + +### Settings that must not be recycled without new causal evidence + +- Pipe transport. +- MoltenVK 128/256 command buffers. +- Synchronous MoltenVK queue submission. +- Guest submit thread and broad shader prewarm. +- Native GLES and direct Vulkan routes. +- `VirtioGpuNativeSync`, `VirtioGpuNext`, disabled Vulkan descriptor batching. +- ASG 8/32 KiB write steps, 64/128 KiB rings, 512 KiB write buffer, and 2/4 ms + flush screens. +- Active-consumer host patch. +- More guest RAM or an eighth vCPU as an assumed optimization. +- Audio disabled. +- MSAA2, material quality 1, `r.OneFrameThreadLag=0`, forced half-rate skeletal + animation, extreme effects/LOD, blind 50% scale, and blind PSO prewarm. +- Riot Performance Mode Beta and the combined Home Run A host flags. + +## 12. Current experiment: Combat Latency A + +- **LOCKED TEST SHAPE:** Control remains High/60/OFF and the complete proven + emulator stack. +- **CANDIDATE:** changes only the packaged emulator host's requested pre-exec + QoS to `user_interactive`; declares Game Mode eligibility. +- **VERIFIED IMPLEMENTATION:** candidate/configuration hashing, stale-preset + migration, host QoS receipt parsing, guest power gate, correctness rollback, + and stable semantic TFT-layer comparison have native tests. +- **VERIFIED LAUNCH:** Build 7 established the requested QoS at the host + pre-exec boundary and reached TFT ready with logging active. +- **UNKNOWN:** comparative combat benefit. One full candidate match is now + measured, but no matched valid Control/Combat Latency A pair exists. Never + call it faster until a compatible comparison exists. + +## 13. Explicit unknowns and open acceptance + +1. Combat Latency A's effect on continuous whole-run FPS, 1% low, p95/p99, + frame-budget misses, and visible stutter versus Control. +2. Whether QEMU decoder/render/submission workers actually receive beneficial + scheduling after exec. +3. The first divergent boundary in the run's worst sustained under-60 periods. +4. Exact ASG-versus-gfxstream-versus-MoltenVK ownership without a frame-ID + correlation ring and valid synchronized trace. Direct per-frame stack SHA + and per-window joins improve scope integrity but do not close this gap. +5. Whether persistent MoltenVK pipeline caching removes meaningful frame stalls + in this exact shipping path. +6. Whether MMAP can reduce host copy/frame age without tearing or ownership + corruption. Raw gRPC remains the working control. +7. Whether current sound is audibly correct at the user's output device. +8. Whether Riot WebView input ANR recurs after future TFT/WebView updates. +9. Public distribution acceptance: Build 8 has a historical local-signing + receipt, current-host trust is blocked, and no notarization is claimed. +10. Whether any owned candidate can hold the complete automatic run at the 60 FPS + target without correctness, audio, login, memory, or cleanup regression. + +## 14. Authority and update rule + +Use this precedence for current truth: + +1. direct current machine/runtime/SQL evidence; +2. `ssot/runtime-authority.json` and `ssot/STACK.lock.yaml` after reconciliation; +3. current native source and tests; +4. `benchmark.md` for formulas, validity, analysis output, and current findings; +5. `docs/TFTMAC_NATIVE_RUNTIME_KNOWLEDGE_BASE.md` and `dev.md`; +6. historical campaign docs and SQL, explicitly labeled historical; +7. plans, simulations, and research as hypotheses only. + +`TFTMAC.md`, old launchers, old Node helpers, old source-build plans, and old +Medium-profile records are not current runtime authority. When a mutable fact +changes, record the observation time and evidence; do not silently rewrite a +historical result to look current. diff --git a/launcher/EmulatorHost/main.c b/launcher/EmulatorHost/main.c deleted file mode 100644 index 7f62bc0..0000000 --- a/launcher/EmulatorHost/main.c +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) { - const char *emulator = getenv("TFT_EMULATOR"); - if (emulator == NULL || emulator[0] == '\0' || access(emulator, X_OK) != 0) { - fputs("Mactician Game Host could not find its Android Emulator executable.\n", stderr); - return EXIT_FAILURE; - } - - argv[0] = (char *)emulator; - execv(emulator, argv); - - fprintf(stderr, "Mactician Game Host could not start Android Emulator: %s\n", strerror(errno)); - return EXIT_FAILURE; -} diff --git a/launcher/Info.plist b/launcher/Info.plist deleted file mode 100644 index d3d6fa6..0000000 --- a/launcher/Info.plist +++ /dev/null @@ -1,58 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - Mactician - CFBundleExecutable - Mactician - CFBundleIconFile - Mactician.icns - CFBundleIdentifier - dev.sergeinaumov.mactician - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - ru - - CFBundleName - Mactician - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0.4 - CFBundleVersion - 40 - LSApplicationCategoryType - public.app-category.games - LSMinimumSystemVersion - 12.0 - NSHighResolutionCapable - - NSAppTransportSecurity - - NSAllowsLocalNetworking - - - NSHumanReadableCopyright - Mactician is free and open source. TFT and Riot Games are property of their respective owners. - SUAllowsAutomaticUpdates - - SUAutomaticallyUpdate - - SUEnableAutomaticChecks - - SUFeedURL - https://sergeinaumov.dev/mactician/updates/appcast.xml - SUPublicEDKey - 77t8YuvP4mvvP/3oMpVR/TqGRMCcUlrpWFIZGcWqokY= - SUScheduledCheckInterval - 86400 - SUVerifyUpdateBeforeExtraction - - - diff --git a/launcher/README.md b/launcher/README.md deleted file mode 100644 index df0ea10..0000000 --- a/launcher/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Launcher source - -This directory contains the native SwiftUI application, unit and provisioning -tests, application resources, the minimal emulator host, and release metadata. - -Use the repository-level commands and documentation: - -- [Build and validation](../docs/building.md) -- [Architecture](../docs/architecture.md) -- [Release process](../docs/releasing.md) -- [Contributing](../CONTRIBUTING.md) - -Private game APK inputs, downloaded Android components, Sparkle build cache, -compiled apps, and disk images are intentionally excluded from Git. diff --git a/launcher/Resources/EmulatorIcon-1024.png b/launcher/Resources/EmulatorIcon-1024.png deleted file mode 100644 index ad5b2d5..0000000 Binary files a/launcher/Resources/EmulatorIcon-1024.png and /dev/null differ diff --git a/launcher/Resources/EmulatorIcon.icns b/launcher/Resources/EmulatorIcon.icns deleted file mode 100644 index 73e6c80..0000000 Binary files a/launcher/Resources/EmulatorIcon.icns and /dev/null differ diff --git a/launcher/Resources/Mactician-1024.png b/launcher/Resources/Mactician-1024.png deleted file mode 100644 index 686bd30..0000000 Binary files a/launcher/Resources/Mactician-1024.png and /dev/null differ diff --git a/launcher/Resources/Mactician.icns b/launcher/Resources/Mactician.icns deleted file mode 100644 index 3b0a8d3..0000000 Binary files a/launcher/Resources/Mactician.icns and /dev/null differ diff --git a/launcher/Resources/MacticianHero.png b/launcher/Resources/MacticianHero.png deleted file mode 100644 index 6f1edab..0000000 Binary files a/launcher/Resources/MacticianHero.png and /dev/null differ diff --git a/launcher/Resources/QEMU-Hypervisor.entitlements b/launcher/Resources/QEMU-Hypervisor.entitlements deleted file mode 100644 index 154f330..0000000 --- a/launcher/Resources/QEMU-Hypervisor.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.hypervisor - - - diff --git a/launcher/Resources/emulator-host.command b/launcher/Resources/emulator-host.command deleted file mode 100755 index 022eec1..0000000 --- a/launcher/Resources/emulator-host.command +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -if [[ -z "${TFT_EMULATOR:-}" || ! -x "$TFT_EMULATOR" ]]; then - print -u2 "Mactician Game Host could not find its Android Emulator executable." - exit 1 -fi - -exec "$TFT_EMULATOR" "$@" diff --git a/launcher/Resources/en.lproj/Localizable.strings b/launcher/Resources/en.lproj/Localizable.strings deleted file mode 100644 index 8549a8e..0000000 --- a/launcher/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,151 +0,0 @@ -"header.subtitle" = "TFT PBE launcher for Apple Silicon"; -"header.pbe_version_format" = "PBE %@"; -"settings.open" = "Open Mactician settings"; -"about.descriptor" = "TFT PBE launcher for Apple Silicon"; - -"hero.eyebrow" = "MACTICIAN · TFT PBE LAUNCHER"; -"hero.title" = "Keep playing TFT PBE on your Mac."; -"hero.description" = "Mactician prepares the environment, starts the game, and keeps your local setup in one place."; - -"footer.private_build" = "Free and open source"; -"footer.apple_silicon" = "Apple silicon"; -"footer.developer_website" = "Developer website"; -"footer.legal" = "TFT and Riot Games are property of their respective owners"; - -"ready.title" = "Ready to play"; -"ready.description" = "TFT PBE is installed and ready."; -"ready.advanced_summary_format" = "%d%% · %d GB · %d vCPU"; -"ready.advanced_open" = "Open advanced settings"; - -"field.game_language" = "Game language"; -"field.graphics_profile" = "Graphics profile"; -"field.resolution" = "Resolution"; -"field.effects_quality" = "Detail and effects"; -"field.advanced_settings" = "Advanced settings"; -"field.interface_scale" = "Interface scale"; -"field.android_ram" = "Android RAM"; -"field.vcpu" = "vCPU"; -"field.vcpu_value_format" = "%d cores"; - -"effects_quality.high" = "High"; -"effects_quality.high.detail" = "Full detail"; -"effects_quality.performance" = "Performance"; -"effects_quality.performance.detail" = "Fewer effects and simpler LOD"; - -"action.play" = "Play"; -"action.update_game" = "Update game"; -"action.install" = "Install"; -"action.pause" = "Pause"; -"action.resume" = "Resume"; -"action.cancel" = "Cancel"; -"action.stop" = "Stop"; -"action.stop_game" = "Stop game"; -"action.view_log" = "View log"; -"action.data_folder" = "Data folder"; -"action.repair_installation" = "Repair installation"; -"action.reset_all_data" = "Reset all data…"; -"action.retry_installation" = "Retry installation"; -"action.try_again" = "Try again"; -"action.restart_game" = "Restart game"; -"action.copy" = "Copy"; -"action.close" = "Close"; -"action.ok" = "OK"; - -"game_update.result.title" = "Game update"; -"game_update.updated.message" = "TFT PBE was updated to version %@ and is ready to play."; -"game_update.current.message" = "No update is available. Installed TFT PBE version %@ already matches the hosted version."; - -"telemetry.notice.title" = "Usage statistics and diagnostics"; -"telemetry.notice.body" = "Mactician sends one statistic after the first successful game launch: the launcher version and a rounded session-duration range. The event contains no persistent identifier or device characteristics."; -"telemetry.extended.toggle" = "Send extended diagnostics: Mac characteristics, launcher settings, and the duration of every game session."; -"telemetry.extended.data_link" = "Data sent"; -"telemetry.privacy_policy" = "Privacy Policy"; -"telemetry.continue" = "Continue"; -"telemetry.settings.title" = "Privacy and diagnostics"; -"telemetry.settings.description" = "Extended diagnostics are optional and do not affect launcher functionality."; -"telemetry.settings.basic_note" = "Mactician sends one unlinkable first-session event with the launcher version and a rounded duration range. Resetting Android or game data does not reset it."; - -"updates.title" = "Mactician Updates"; -"updates.description" = "Updates are verified, installed atomically, and never remove Android or Riot data."; -"updates.automatic_checks" = "Checks at launch and once per day"; -"updates.check" = "Check for Updates…"; - -"install.required.title" = "Installation required"; -"install.required.description" = "Install the verified Android environment and TFT PBE once."; -"install.cancelled.title" = "Installation stopped"; -"install.cancelled.description" = "Incomplete downloads are kept and can be resumed when you install again."; -"install.requirement.download" = "Download"; -"install.requirement.free_space" = "Free space required"; -"install.requirement.platform" = "Platform"; -"install.requirement.apple_silicon_value" = "Apple silicon"; -"install.what_is_installed" = "What will be installed"; -"install.android_api_format" = "Android API %@ · ARM64"; -"install.emulator_format" = "Emulator %@"; -"install.clean_profile" = "Clean profile"; -"install.license.accept" = "I accept the Android SDK License Agreement"; -"install.license.terms" = "View Google terms"; -"install.license.hint" = "Accept the Android SDK license to enable installation."; - -"installing.title" = "Installing TFT PBE"; -"installing.phase.checking" = "Checking your Mac and installation files…"; -"installing.phase.downloading" = "Downloading required components…"; -"installing.phase.extracting" = "Extracting and verifying files…"; -"installing.phase.creating_device" = "Creating the Android device…"; -"installing.phase.installing_game" = "Installing TFT PBE…"; -"installing.phase.finished" = "Installation complete"; -"installing.phase.paused" = "Download paused"; - -"launching.title" = "Launching TFT PBE"; -"playing.title" = "Game running"; -"playing.description_format" = "TFT PBE is open in %@."; -"playing.fill_window_tip" = "Control + Fn + F fills the game window"; -"stopping.title" = "Stopping TFT PBE"; -"stopping.description" = "Closing the emulator and restoring temporary settings…"; - -"hotkeys.title" = "Game hotkeys"; -"hotkeys.description" = "Always enabled while the game window is focused. Accessibility access belongs to Mactician, not the emulator."; -"hotkeys.grant_access" = "Allow Accessibility"; -"hotkeys.status.label" = "Status"; -"hotkeys.status.permission_required" = "Hotkeys are off — allow Accessibility for Mactician"; -"hotkeys.status.ready" = "Enabled for the next game launch"; -"hotkeys.status.active" = "Enabled while the game window is focused"; -"hotkeys.status.unavailable" = "Hotkeys are off — the input bridge is unavailable"; -"hotkeys.shortcuts" = "Space: Shop · D: Reroll · F: Buy XP · Tab: Items / traits · V: Players / damage\nControl + Fn + F: Fill window"; - -"settings.title" = "Mactician settings"; -"settings.description" = "Game, performance, hotkeys, and maintenance."; -"settings.runtime_locked" = "Available after TFT PBE is stopped."; -"settings.game.title" = "Game"; - -"settings.performance.title" = "Performance"; -"settings.performance.host_format" = "This Mac: %d GB unified memory · %d logical CPU"; -"settings.performance.recommended" = "Recommended for this Mac"; -"settings.performance.recommended_format" = "%d GB RAM · %d vCPU"; -"settings.performance.apply" = "Apply recommended"; -"settings.performance.help.title" = "How resource allocation works"; -"settings.performance.help.balanced.title" = "Balanced starting point"; -"settings.performance.help.balanced.body" = "Start with the recommendation so macOS, Metal, ANGLE, and background apps retain enough resources."; -"settings.performance.help.ram.title" = "When to add RAM"; -"settings.performance.help.ram.body" = "Add 2 GB for 4K or confirmed Android memory pressure. More RAM does not improve FPS by itself; avoid assigning more than half of unified memory."; -"settings.performance.help.cpu.title" = "When to add vCPU"; -"settings.performance.help.cpu.body" = "Increase one core at a time for sustained CPU limits. Keep at least two host cores available to reduce frame-time spikes."; - -"settings.maintenance.title" = "Maintenance"; -"settings.maintenance.description" = "Repair the verified installation or open Mactician diagnostics."; -"settings.maintenance.reset_description" = "Reset removes Android, TFT, cache, and the saved Riot sign-in."; - -"reset.confirmation.title" = "Reset all data?"; -"reset.confirmation.message" = "This removes the Android SDK, TFT, game cache, and saved sign-in. This action cannot be undone."; -"reset.confirmation.action" = "Delete Android and Riot data"; - -"error.installation.title" = "Installation failed"; -"error.installation.summary" = "The required game environment could not be installed."; -"error.launch.title" = "Couldn’t launch TFT PBE"; -"error.launch.summary" = "Mactician could not start the verified game environment."; -"error.runtime.title" = "TFT PBE stopped unexpectedly"; -"error.runtime.summary" = "The game or emulator reported an unexpected runtime error."; -"error.validation.title" = "Installation needs repair"; -"error.validation.summary" = "A verified component is missing, damaged, or does not match this Mactician build."; -"error.reset.title" = "Couldn’t delete Mactician data"; -"error.reset.summary" = "Some Mactician data could not be removed. Review the technical details before trying again."; -"error.technical_details" = "Technical details"; diff --git a/launcher/Resources/launcher-runtime.command b/launcher/Resources/launcher-runtime.command deleted file mode 100755 index 47bfa3b..0000000 --- a/launcher/Resources/launcher-runtime.command +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/zsh -set -uo pipefail -unsetopt BG_NICE - -readonly REQUIRED_ENV=( - TFT_RUNTIME_PROJECT TFT_LAUNCH_LOG TFT_ADB TFT_AVD_HOME TFT_AVD_NAME - TFT_SERIAL TFT_DISPLAY_SIZE TFT_DISPLAY_DENSITY TFT_GAME_LANGUAGE - TFT_CPU_CORES TFT_MEMORY_MB TFT_UI_SCALE -) -for required_name in "${REQUIRED_ENV[@]}"; do - if [[ -z "${(P)required_name:-}" ]]; then - print -r -- '{"event":"error","message":"Runtime environment is incomplete","code":2}' - exit 2 - fi -done - -case "$TFT_GAME_LANGUAGE" in - en-US|ru-RU|de-DE|fr-FR|es-ES|es-MX|pt-BR|it-IT|pl-PL|cs-CZ|hu-HU|ro-RO|el-GR|tr-TR|ar-AE|ja-JP|ko-KR|zh-CN|zh-SG|zh-TW|vi-VN|th-TH|id-ID) - ;; - *) - print -r -- '{"event":"error","message":"Unsupported game language","code":2}' - exit 2 - ;; -esac - -emit() { - print -r -- "$1" -} - -typeset child_pid="" -typeset stop_requested=0 -stop_child() { - stop_requested=1 - if [[ -n "$child_pid" ]] && kill -0 "$child_pid" >/dev/null 2>&1; then - kill -TERM "$child_pid" >/dev/null 2>&1 || true - fi -} -trap stop_child INT TERM HUP - -mkdir -p "${TFT_LAUNCH_LOG:h}" -emit '{"event":"booting","message":"Starting Android…"}' - -"$TFT_RUNTIME_PROJECT/scripts/run-asg-experiment.command" >>"$TFT_LAUNCH_LOG" 2>&1 & -child_pid=$! - -typeset emulator_pid="" -typeset emitted_pid=0 -typeset emitted_ready=0 -typeset locale_applied=0 -typeset missing_game_checks=0 -while kill -0 "$child_pid" >/dev/null 2>&1; do - if (( emitted_pid == 0 )); then - emulator_pid="$(pgrep -f 'qemu-system-aarch64.*-port 5582' 2>/dev/null | head -n 1 || true)" - if [[ -z "$emulator_pid" ]]; then - emulator_pid="$(pgrep -f 'emulator.*-port 5582' 2>/dev/null | head -n 1 || true)" - fi - if [[ "$emulator_pid" == <-> ]]; then - emit "{\"event\":\"emulator_started\",\"pid\":$emulator_pid,\"serial\":\"$TFT_SERIAL\"}" - emitted_pid=1 - fi - fi - if (( locale_applied == 0 )) \ - && "$TFT_ADB" -s "$TFT_SERIAL" get-state >/dev/null 2>&1 \ - && [[ "$("$TFT_ADB" -s "$TFT_SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]]; then - "$TFT_ADB" -s "$TFT_SERIAL" shell cmd locale set-app-locales \ - com.riotgames.league.teamfighttactics.pbe "$TFT_GAME_LANGUAGE" \ - >>"$TFT_LAUNCH_LOG" 2>&1 || true - locale_applied=1 - fi - if (( emitted_ready == 0 )) \ - && "$TFT_ADB" -s "$TFT_SERIAL" get-state >/dev/null 2>&1 \ - && [[ -n "$("$TFT_ADB" -s "$TFT_SERIAL" shell pidof com.riotgames.league.teamfighttactics.pbe 2>/dev/null | tr -d '\r')" ]]; then - emit "{\"event\":\"ready\",\"message\":\"TFT PBE is open\",\"serial\":\"$TFT_SERIAL\"}" - emitted_ready=1 - fi - if (( emitted_ready == 1 )); then - if [[ -n "$("$TFT_ADB" -s "$TFT_SERIAL" shell pidof com.riotgames.league.teamfighttactics.pbe 2>/dev/null | tr -d '\r')" ]]; then - missing_game_checks=0 - else - (( missing_game_checks += 1 )) - if (( missing_game_checks >= 3 )); then - emit "{\"event\":\"game_stopped\",\"message\":\"TFT PBE closed\",\"serial\":\"$TFT_SERIAL\"}" - break - fi - fi - fi - sleep 1 -done - -wait "$child_pid" -readonly child_status=$? -typeset normal_stop=0 -if (( stop_requested == 1 || emitted_ready == 1 )); then - normal_stop=1 -fi -if (( child_status == 42 && stop_requested == 0 )); then - emit '{"event":"error","message":"The TFT PBE version changed. The old OpenGL overlay will not be applied. Prepare a new verified private launcher build.","code":42}' -elif (( child_status != 0 && normal_stop == 0 )); then - emit "{\"event\":\"error\",\"message\":\"Could not launch TFT. Open the log or repair the installation.\",\"code\":$child_status}" -fi -emit "{\"event\":\"stopped\",\"code\":$child_status}" -if (( normal_stop == 1 )); then - exit 0 -fi -exit "$child_status" diff --git a/launcher/Resources/release-manifest.json b/launcher/Resources/release-manifest.json deleted file mode 100644 index 8e70e61..0000000 --- a/launcher/Resources/release-manifest.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "schemaVersion": 1, - "minimumFreeBytes": 26843545600, - "components": [ - { - "id": "platform-tools", - "version": "36.0.2", - "url": "https://dl.google.com/android/repository/platform-tools_r36.0.2-darwin.zip", - "size": 15711207, - "sha256": "106a5d31fad8c1c0c5a180d06f5779767d129d7d5edbe629005c11a85eec5b4b", - "archiveRoot": "platform-tools", - "installPath": "sdk/platform-tools" - }, - { - "id": "emulator", - "version": "37.1.11", - "url": "https://dl.google.com/android/repository/emulator-darwin_aarch64-15917651.zip", - "size": 394555844, - "sha256": "22530de9363f34ea945ecb5cad74523abd4b615f27f3c1a9899efb183ea9e144", - "archiveRoot": "emulator", - "installPath": "sdk/emulator" - }, - { - "id": "system-image", - "version": "android-36-google_apis-arm64-v8a-r07", - "url": "https://dl.google.com/android/repository/sys-img/google_apis/arm64-v8a-36_r07.zip", - "size": 1872691175, - "sha256": "fb47d861d6f87230ee0fe70f610d579935ca77f41a0eefbf391595d3dc4b5ee2", - "archiveRoot": "arm64-v8a", - "installPath": "sdk/system-images/android-36/google_apis/arm64-v8a" - } - ], - "game": { - "packageName": "com.riotgames.league.teamfighttactics.pbe", - "version": "18.1-5212127", - "baseSHA256": "2f4996a620623d0b958383bfe58bdec78fb70cca095099ca2474f3d08c62ff18", - "apks": [ - { - "name": "base.apk", - "size": 102984963, - "sha256": "2f4996a620623d0b958383bfe58bdec78fb70cca095099ca2474f3d08c62ff18" - }, - { - "name": "split_config.arm64_v8a.apk", - "size": 92943423, - "sha256": "a7332efdddf070e724f75c41adb1a829bc3c442bd53ae79fe8cc55075e951939" - }, - { - "name": "split_config.en.apk", - "size": 41369, - "sha256": "b57210f98779e12d6f6874c378942f67a55459e75549bbf97487a2d6e0084cd5" - }, - { - "name": "split_config.hdpi.apk", - "size": 90687, - "sha256": "f3b653ba01a8d352f6a25ad6ecbae8892b13aa44daad6b74aebdb5a5be05d756" - } - ] - }, - "profiles": [ - { - "id": "balanced", - "title": "1080p", - "width": 1920, - "height": 1080, - "density": 320, - "memoryMB": 6144 - }, - { - "id": "quality", - "title": "1440p", - "width": 2560, - "height": 1440, - "density": 416, - "memoryMB": 6144 - }, - { - "id": "ultra", - "title": "1800p", - "width": 3200, - "height": 1800, - "density": 520, - "memoryMB": 6144 - }, - { - "id": "4k", - "title": "4K", - "width": 3840, - "height": 2160, - "density": 640, - "memoryMB": 6144 - } - ] -} diff --git a/launcher/Resources/release-notes/1.0.0.md b/launcher/Resources/release-notes/1.0.0.md deleted file mode 100644 index 2deb211..0000000 --- a/launcher/Resources/release-notes/1.0.0.md +++ /dev/null @@ -1,16 +0,0 @@ -# Mactician Release Notes — 1.0.0 - -Mactician 1.0.0 is the initial public version. - -- Native SwiftUI experience for preparing and playing TFT PBE on Apple Silicon. -- Verified Android Emulator, ANGLE, Vulkan, and MoltenVK runtime components. -- Built-in installation, Play, Repair, Reset, settings, hotkeys, and secure updates. -- Runtime data stored in `~/Library/Application Support/Mactician`. - -## Verify the download - -`Mactician-1.0.0.dmg` SHA-256: - -`c83a95ee84434f05e40425f3fa8a3f1a57d207a218ac62742d20b3d0c32a9a28` - -Free and open source. Built for two tacticians. Shared with everyone. diff --git a/launcher/Resources/release-notes/1.0.1.md b/launcher/Resources/release-notes/1.0.1.md deleted file mode 100644 index 2607b24..0000000 --- a/launcher/Resources/release-notes/1.0.1.md +++ /dev/null @@ -1,15 +0,0 @@ -# Mactician Release Notes — 1.0.1 - -- Adds a dedicated **Update game** action for TFT PBE. -- Downloads signed game manifests and split APK files from `sergeinaumov.dev`. -- Verifies the feed signature, HTTPS origin, file sizes, and SHA-256 hashes before installation. -- Installs game updates without uninstalling TFT, preserving Riot sign-in and local game data. -- Keeps a verified bundled APK fallback for first installation when the update service is unavailable. - -## Verify the download - -`Mactician-1.0.1.dmg` SHA-256: - -`56db03369a3e65b6a20474fc2bc6b8c4ffe016cfc889084012e50d322ec92cfa` - -Free and open source. Built for two tacticians. Shared with everyone. diff --git a/launcher/Resources/release-notes/1.0.2.md b/launcher/Resources/release-notes/1.0.2.md deleted file mode 100644 index 296608d..0000000 --- a/launcher/Resources/release-notes/1.0.2.md +++ /dev/null @@ -1,14 +0,0 @@ -# Mactician Release Notes — 1.0.2 - -- Checks for Mactician updates at every launch as well as on the daily schedule. -- Shows an explicit result after every TFT PBE update check. -- Displays the installed game version when no newer hosted build is available. -- Records completed game update checks in the launcher log for diagnostics. - -## Verify the download - -`Mactician-1.0.2.dmg` SHA-256: - -`5f364644acdcf6302b569f96f8ecdda00238bda8b65a9a4da5b93f5f041e2f02` - -Free and open source. Built for two tacticians. Shared with everyone. diff --git a/launcher/Resources/release-notes/1.0.3.md b/launcher/Resources/release-notes/1.0.3.md deleted file mode 100644 index 47fdfd4..0000000 --- a/launcher/Resources/release-notes/1.0.3.md +++ /dev/null @@ -1,14 +0,0 @@ -# Mactician Release Notes — 1.0.3 - -- Checks the signed TFT PBE update feed automatically when the launcher is ready. -- Shows **Update game** instead of **Play** only when a newer verified game version is available. -- Keeps the update action hidden when the installed game is current. -- Prevents launching a known-outdated game build until its update completes. - -## Verify the download - -`Mactician-1.0.3.dmg` SHA-256: - -`82be8dde6097aed2e87af33a2d1dd1017d4019f2ed7ee094588999bd5924b12d` - -Free and open source. Built for two tacticians. Shared with everyone. diff --git a/launcher/Resources/release-notes/1.0.4.md b/launcher/Resources/release-notes/1.0.4.md deleted file mode 100644 index 9ed990f..0000000 --- a/launcher/Resources/release-notes/1.0.4.md +++ /dev/null @@ -1,16 +0,0 @@ -# Mactician Release Notes — 1.0.4 - -- Restores four asynchronous OpenGL shader compiler services after the latest - TFT PBE update disabled them in its inherited Android device profile. -- Moves first-use PSO compilation away from the gameplay render path to reduce - recurring combat freezes and improve frame pacing while the shader cache warms. -- Keeps the Performance and High detail profiles protected against the same - upstream profile regression. - -## Verify the download - -`Mactician-1.0.4.dmg` SHA-256: - -`6b935ee77d109c6bac1969fc926d639890967cac5951309b30e83d44b8d11e4f` - -Free and open source. Built for two tacticians. Shared with everyone. diff --git a/launcher/Resources/ru.lproj/Localizable.strings b/launcher/Resources/ru.lproj/Localizable.strings deleted file mode 100644 index 52e2193..0000000 --- a/launcher/Resources/ru.lproj/Localizable.strings +++ /dev/null @@ -1,151 +0,0 @@ -"header.subtitle" = "Лаунчер TFT PBE для Mac с Apple Silicon"; -"header.pbe_version_format" = "PBE %@"; -"settings.open" = "Открыть настройки Mactician"; -"about.descriptor" = "Лаунчер TFT PBE для Mac с Apple Silicon"; - -"hero.eyebrow" = "MACTICIAN · ЛАУНЧЕР TFT PBE"; -"hero.title" = "Продолжайте играть в TFT PBE на Mac."; -"hero.description" = "Mactician готовит окружение, запускает игру и хранит локальные настройки в одном месте."; - -"footer.private_build" = "Бесплатно и с открытым исходным кодом"; -"footer.apple_silicon" = "Apple Silicon"; -"footer.developer_website" = "Сайт разработчика"; -"footer.legal" = "TFT и Riot Games принадлежат их правообладателям"; - -"ready.title" = "Можно играть"; -"ready.description" = "TFT PBE установлен и готов к запуску."; -"ready.advanced_summary_format" = "%d%% · %d ГБ · %d vCPU"; -"ready.advanced_open" = "Открыть расширенные настройки"; - -"field.game_language" = "Язык игры"; -"field.graphics_profile" = "Профиль графики"; -"field.resolution" = "Разрешение"; -"field.effects_quality" = "Детализация и эффекты"; -"field.advanced_settings" = "Расширенные настройки"; -"field.interface_scale" = "Масштаб интерфейса"; -"field.android_ram" = "Память Android"; -"field.vcpu" = "vCPU"; -"field.vcpu_value_format" = "%d ядер"; - -"effects_quality.high" = "Высокие"; -"effects_quality.high.detail" = "Полная детализация"; -"effects_quality.performance" = "Производительность"; -"effects_quality.performance.detail" = "Меньше эффектов и более простой LOD"; - -"action.play" = "Играть"; -"action.update_game" = "Обновить игру"; -"action.install" = "Установить"; -"action.pause" = "Приостановить"; -"action.resume" = "Продолжить"; -"action.cancel" = "Отмена"; -"action.stop" = "Остановить"; -"action.stop_game" = "Остановить игру"; -"action.view_log" = "Открыть журнал"; -"action.data_folder" = "Папка данных"; -"action.repair_installation" = "Восстановить установку"; -"action.reset_all_data" = "Сбросить все данные…"; -"action.retry_installation" = "Повторить установку"; -"action.try_again" = "Попробовать снова"; -"action.restart_game" = "Перезапустить игру"; -"action.copy" = "Копировать"; -"action.close" = "Закрыть"; -"action.ok" = "ОК"; - -"game_update.result.title" = "Обновление игры"; -"game_update.updated.message" = "TFT PBE обновлён до версии %@ и готов к запуску."; -"game_update.current.message" = "Обновление не найдено. Установленная версия TFT PBE %@ совпадает с версией на сервере."; - -"telemetry.notice.title" = "Статистика использования и диагностика"; -"telemetry.notice.body" = "Mactician один раз отправляет статистику первого успешного запуска: версию лаунчера и округлённую длительность сессии. Событие не содержит постоянного идентификатора или характеристик устройства."; -"telemetry.extended.toggle" = "Отправлять расширенную диагностику: характеристики Mac, настройки лаунчера и длительность каждой игровой сессии."; -"telemetry.extended.data_link" = "Перечень данных"; -"telemetry.privacy_policy" = "Политика конфиденциальности"; -"telemetry.continue" = "Продолжить"; -"telemetry.settings.title" = "Конфиденциальность и диагностика"; -"telemetry.settings.description" = "Расширенная диагностика необязательна и не влияет на работу лаунчера."; -"telemetry.settings.basic_note" = "Mactician один раз отправляет несвязываемое событие первой сессии с версией лаунчера и округлённой длительностью. Сброс Android или игровых данных его не сбрасывает."; - -"updates.title" = "Обновления Mactician"; -"updates.description" = "Обновления проверяются, устанавливаются атомарно и не удаляют данные Android или Riot."; -"updates.automatic_checks" = "Проверка при запуске и раз в сутки"; -"updates.check" = "Проверить обновления…"; - -"install.required.title" = "Требуется установка"; -"install.required.description" = "Один раз установите проверенное окружение Android и TFT PBE."; -"install.cancelled.title" = "Установка остановлена"; -"install.cancelled.description" = "Незавершённые загрузки сохранены и продолжатся при следующей установке."; -"install.requirement.download" = "Размер загрузки"; -"install.requirement.free_space" = "Требуется места"; -"install.requirement.platform" = "Платформа"; -"install.requirement.apple_silicon_value" = "Apple Silicon"; -"install.what_is_installed" = "Что будет установлено"; -"install.android_api_format" = "Android API %@ · ARM64"; -"install.emulator_format" = "Эмулятор %@"; -"install.clean_profile" = "Чистый профиль"; -"install.license.accept" = "Я принимаю лицензионное соглашение Android SDK"; -"install.license.terms" = "Условия Google"; -"install.license.hint" = "Примите лицензию Android SDK, чтобы включить установку."; - -"installing.title" = "Установка TFT PBE"; -"installing.phase.checking" = "Проверяем Mac и файлы установки…"; -"installing.phase.downloading" = "Загружаем необходимые компоненты…"; -"installing.phase.extracting" = "Распаковываем и проверяем файлы…"; -"installing.phase.creating_device" = "Создаём устройство Android…"; -"installing.phase.installing_game" = "Устанавливаем TFT PBE…"; -"installing.phase.finished" = "Установка завершена"; -"installing.phase.paused" = "Загрузка приостановлена"; - -"launching.title" = "Запуск TFT PBE"; -"playing.title" = "Игра запущена"; -"playing.description_format" = "TFT PBE открыт на языке: %@."; -"playing.fill_window_tip" = "Control + Fn + F заполняет окно игры"; -"stopping.title" = "Остановка TFT PBE"; -"stopping.description" = "Закрываем эмулятор и восстанавливаем временные настройки…"; - -"hotkeys.title" = "Горячие клавиши игры"; -"hotkeys.description" = "Всегда включены, когда окно игры в фокусе. Accessibility-разрешение получает Mactician, а не эмулятор."; -"hotkeys.grant_access" = "Разрешить Accessibility"; -"hotkeys.status.label" = "Состояние"; -"hotkeys.status.permission_required" = "Хоткеи выключены — разрешите Accessibility для Mactician"; -"hotkeys.status.ready" = "Включены для следующего запуска игры"; -"hotkeys.status.active" = "Включены, пока окно игры в фокусе"; -"hotkeys.status.unavailable" = "Хоткеи выключены — мост ввода недоступен"; -"hotkeys.shortcuts" = "Space: Магазин · D: Обновить · F: Купить опыт · Tab: Предметы / трейты · V: Игроки / урон\nControl + Fn + F: Развернуть окно"; - -"settings.title" = "Настройки Mactician"; -"settings.description" = "Игра, производительность, горячие клавиши и обслуживание."; -"settings.runtime_locked" = "Доступно после остановки TFT PBE."; -"settings.game.title" = "Игра"; - -"settings.performance.title" = "Производительность"; -"settings.performance.host_format" = "Этот Mac: %d ГБ объединённой памяти · %d логических CPU"; -"settings.performance.recommended" = "Рекомендовано для этого Mac"; -"settings.performance.recommended_format" = "%d ГБ памяти · %d vCPU"; -"settings.performance.apply" = "Применить"; -"settings.performance.help.title" = "Как распределяются ресурсы"; -"settings.performance.help.balanced.title" = "Сбалансированная отправная точка"; -"settings.performance.help.balanced.body" = "Начните с рекомендации, чтобы macOS, Metal, ANGLE и фоновые приложения получили достаточно ресурсов."; -"settings.performance.help.ram.title" = "Когда добавлять память"; -"settings.performance.help.ram.body" = "Добавьте 2 ГБ для 4K или при подтверждённой нехватке памяти Android. Дополнительная память сама по себе не повышает FPS; не выделяйте больше половины объединённой памяти."; -"settings.performance.help.cpu.title" = "Когда добавлять vCPU"; -"settings.performance.help.cpu.body" = "При устойчивом ограничении CPU добавляйте по одному ядру. Оставляйте системе хотя бы два ядра, чтобы уменьшить скачки времени кадра."; - -"settings.maintenance.title" = "Обслуживание"; -"settings.maintenance.description" = "Восстановление проверенной установки и диагностика Mactician."; -"settings.maintenance.reset_description" = "Сброс удалит Android, TFT, кэш и сохранённый вход Riot."; - -"reset.confirmation.title" = "Сбросить все данные?"; -"reset.confirmation.message" = "Будут удалены Android SDK, TFT, кэш игры и сохранённый вход. Это действие нельзя отменить."; -"reset.confirmation.action" = "Удалить данные Android и Riot"; - -"error.installation.title" = "Не удалось установить игру"; -"error.installation.summary" = "Не удалось установить необходимое игровое окружение."; -"error.launch.title" = "Не удалось запустить TFT PBE"; -"error.launch.summary" = "Mactician не смог запустить проверенное игровое окружение."; -"error.runtime.title" = "TFT PBE неожиданно завершился"; -"error.runtime.summary" = "Игра или эмулятор сообщили о неожиданной ошибке выполнения."; -"error.validation.title" = "Установку нужно восстановить"; -"error.validation.summary" = "Проверенный компонент отсутствует, повреждён или не соответствует этой сборке Mactician."; -"error.reset.title" = "Не удалось удалить данные Mactician"; -"error.reset.summary" = "Часть данных Mactician не удалось удалить. Изучите технические подробности перед повторной попыткой."; -"error.technical_details" = "Технические подробности"; diff --git a/launcher/Sources/CoreModels.swift b/launcher/Sources/CoreModels.swift deleted file mode 100644 index c165723..0000000 --- a/launcher/Sources/CoreModels.swift +++ /dev/null @@ -1,366 +0,0 @@ -import Foundation - -enum MacticianIdentity { - static let appName = "Mactician" - static let bundleIdentifier = "dev.sergeinaumov.mactician" - static let applicationSupportDirectory = "Mactician" - static let userDefaultsDomain = bundleIdentifier - static let keychainService = bundleIdentifier - static let loggingSubsystem = bundleIdentifier - static let websiteURL = URL(string: "https://sergeinaumov.dev/mactician")! - static let privacyPolicyURL = URL(string: "https://sergeinaumov.dev/mactician/privacy")! - static let extendedDiagnosticsURL = URL( - string: "https://sergeinaumov.dev/mactician/privacy#extended-diagnostics" - )! - static let sourceURL = URL(string: "https://github.com/tweet9ra/mactician")! - static let technicalStoryURL = URL( - string: "https://sergeinaumov.dev/writing/how-i-built-mactician" - )! - static let issueURL = URL(string: "https://github.com/tweet9ra/mactician/issues/new/choose")! - static let gameUpdateURL = URL( - string: "https://sergeinaumov.dev/mactician/updates/game/manifest.json" - )! - static let gameUpdatePublicKeyBase64 = "Nadxne/Zs1kndXT8OpaShZCEgK/LqUtMv4aqGQNzCcM=" -} - -struct ReleaseManifest: Codable, Equatable { - let schemaVersion: Int - let minimumFreeBytes: Int64 - let components: [SDKComponent] - let game: GameRelease - let profiles: [LaunchProfile] - - func validate() throws { - guard schemaVersion == 1 else { - throw LauncherError.invalidManifest("Unsupported release manifest schema: \(schemaVersion)") - } - guard minimumFreeBytes >= 10 * 1024 * 1024 * 1024 else { - throw LauncherError.invalidManifest("Invalid minimum disk space") - } - guard Set(components.map(\.id)).count == components.count, - Set(profiles.map(\.id)).count == profiles.count else { - throw LauncherError.invalidManifest("Duplicate identifiers") - } - for component in components { - try component.validate() - } - try game.validate() - guard Set(profiles.map(\.id)) == Set(["balanced", "quality", "ultra", "4k"]) else { - throw LauncherError.invalidManifest("Required profiles are missing") - } - for profile in profiles { - try profile.validate() - } - } -} - -struct SDKComponent: Codable, Equatable, Identifiable { - let id: String - let version: String - let url: URL - let size: Int64 - let sha256: String - let archiveRoot: String - let installPath: String - - func validate() throws { - guard url.scheme == "https", url.host == "dl.google.com" else { - throw LauncherError.invalidManifest("Component \(id) uses an unofficial URL") - } - guard size > 0, sha256.isLowercaseSHA256 else { - throw LauncherError.invalidManifest("Component \(id) has an invalid size or hash") - } - guard Self.isSafeRelativePath(archiveRoot), Self.isSafeRelativePath(installPath) else { - throw LauncherError.invalidManifest("Component \(id) contains an unsafe path") - } - } - - private static func isSafeRelativePath(_ path: String) -> Bool { - !path.isEmpty && !path.hasPrefix("/") && !path.split(separator: "/").contains("..") - } -} - -struct GameRelease: Codable, Equatable { - let packageName: String - let version: String - let versionCode: Int? - let baseSHA256: String - let apks: [GameAPK] - - func validate() throws { - guard packageName == "com.riotgames.league.teamfighttactics.pbe", - !version.isEmpty, - baseSHA256.isLowercaseSHA256, - (1 ... 32).contains(apks.count), - Set(apks.map(\.name)).count == apks.count, - apks.first?.name == "base.apk", - apks.first?.sha256 == baseSHA256 else { - throw LauncherError.invalidManifest("Invalid TFT PBE release") - } - for apk in apks { - guard !apk.name.contains("/"), apk.name.hasSuffix(".apk"), - apk.size > 0, apk.sha256.isLowercaseSHA256 else { - throw LauncherError.invalidManifest("Invalid APK description for \(apk.name)") - } - } - } -} - -struct GameAPK: Codable, Equatable { - let name: String - let size: Int64 - let sha256: String - let url: URL? -} - -struct LaunchProfile: Codable, Equatable, Identifiable { - let id: String - let title: String - let width: Int - let height: Int - let density: Int - let memoryMB: Int - - var displaySize: String { "\(width)x\(height)" } - var displayResolution: String { LauncherMetadata.resolution(width: width, height: height) } - - func validate() throws { - guard ["balanced", "quality", "ultra", "4k"].contains(id), - width >= 1280, height >= 720, - (120 ... 640).contains(density), - memoryMB == 6144 else { - throw LauncherError.invalidManifest("Invalid profile \(id)") - } - } -} - -enum EffectsQuality: String, CaseIterable, Identifiable { - case high - case performance - - var id: String { rawValue } - - var title: String { - switch self { - case .high: - return LauncherL10n.text("effects_quality.high") - case .performance: - return LauncherL10n.text("effects_quality.performance") - } - } - - var detail: String { - switch self { - case .high: - return LauncherL10n.text("effects_quality.high.detail") - case .performance: - return LauncherL10n.text("effects_quality.performance.detail") - } - } - - var profileFilename: String { - "Android_Codex.DeviceProfiles.effects-\(rawValue).ini" - } - - static func selection(saved: String?) -> EffectsQuality { - EffectsQuality(rawValue: saved ?? "") ?? .high - } -} - -struct GameLanguage: Equatable, Identifiable { - let id: String - let title: String - - static let supported: [GameLanguage] = [ - .init(id: "en-US", title: "English"), - .init(id: "ru-RU", title: "Russian"), - .init(id: "de-DE", title: "German"), - .init(id: "fr-FR", title: "French"), - .init(id: "es-ES", title: "Spanish (Spain)"), - .init(id: "es-MX", title: "Spanish (Latin America)"), - .init(id: "pt-BR", title: "Portuguese (Brazil)"), - .init(id: "it-IT", title: "Italian"), - .init(id: "pl-PL", title: "Polish"), - .init(id: "cs-CZ", title: "Czech"), - .init(id: "hu-HU", title: "Hungarian"), - .init(id: "ro-RO", title: "Romanian"), - .init(id: "el-GR", title: "Greek"), - .init(id: "tr-TR", title: "Turkish"), - .init(id: "ar-AE", title: "Arabic"), - .init(id: "ja-JP", title: "Japanese"), - .init(id: "ko-KR", title: "Korean"), - .init(id: "zh-CN", title: "Chinese (Simplified)"), - .init(id: "zh-SG", title: "Chinese (Singapore)"), - .init(id: "zh-TW", title: "Chinese (Traditional)"), - .init(id: "vi-VN", title: "Vietnamese"), - .init(id: "th-TH", title: "Thai"), - .init(id: "id-ID", title: "Indonesian") - ] - - static let english = supported[0] - - static func language(withID id: String?) -> GameLanguage { - supported.first(where: { $0.id == id }) ?? english - } -} - -struct InstallState: Codable, Equatable { - enum Stage: String, Codable { - case empty - case downloading - case sdkInstalled = "sdk_installed" - case avdCreated = "avd_created" - case ready - } - - var schemaVersion: Int = 1 - var stage: Stage = .empty - var installedComponents: [String: String] = [:] - var gameVersion: String? - var gameVersionCode: Int? - var gameBaseSHA256: String? - var overlaySHA256: String? - var updatedAt: Date = Date() - - var isReady: Bool { schemaVersion == 1 && stage == .ready } -} - -struct RuntimeEvent: Codable, Equatable { - enum Kind: String, Codable { - case downloading - case booting - case installingGame = "installing_game" - case emulatorStarted = "emulator_started" - case ready - case gameStopped = "game_stopped" - case stopped - case error - } - - let event: Kind - var message: String? - var pid: Int32? - var serial: String? - var code: Int32? -} - -enum LauncherError: LocalizedError, Equatable { - case invalidManifest(String) - case preflight(String) - case integrity(String) - case process(String) - case unsupportedGame(String) - case cancelled - - var errorDescription: String? { - switch self { - case let .invalidManifest(message), let .preflight(message), - let .integrity(message), let .process(message), - let .unsupportedGame(message): - return message - case .cancelled: - return "Operation cancelled" - } - } -} - -enum HostSizing { - static func guestCPUCores(logicalCPUCount: Int) -> Int { - min(6, max(4, logicalCPUCount - 2)) - } - - static func guestCPUList(logicalCPUCount: Int) -> String { - "0-\(guestCPUCores(logicalCPUCount: logicalCPUCount) - 1)" - } - - static func guestCPUMask(logicalCPUCount: Int) -> String { - let cores = guestCPUCores(logicalCPUCount: logicalCPUCount) - return String((1 << cores) - 1, radix: 16) - } -} - -enum GuestResourceOptions { - static let defaultMemoryMB = 6144 - - static func memoryMB(physicalMemoryBytes: UInt64) -> [Int] { - let hostMemoryMB = Int(physicalMemoryBytes / 1_048_576) - let maximum = min(16_384, max(4_096, hostMemoryMB - 4_096)) - return Array(stride(from: 4_096, through: maximum, by: 2_048)) - } - - static func cpuCores(logicalCPUCount: Int) -> [Int] { - let maximum = min(16, max(1, logicalCPUCount)) - if maximum == 1 { return [1] } - return Array(2 ... maximum) - } - - static func selection(saved: Int, options: [Int], fallback: Int) -> Int { - if options.contains(saved) { return saved } - if options.contains(fallback) { return fallback } - return options.last ?? fallback - } - - static func recommended( - physicalMemoryBytes: UInt64, - logicalCPUCount: Int - ) -> GuestResourceConfiguration { - let memoryOptions = memoryMB(physicalMemoryBytes: physicalMemoryBytes) - let hostMemoryGB = Int(physicalMemoryBytes / 1_073_741_824) - let targetMemoryMB = hostMemoryGB >= 24 ? 8192 : 6144 - let recommendedMemoryMB = selection( - saved: targetMemoryMB, - options: memoryOptions, - fallback: defaultMemoryMB - ) - - let cpuOptions = cpuCores(logicalCPUCount: logicalCPUCount) - let targetCPUCores: Int - if logicalCPUCount >= 12 { - targetCPUCores = 8 - } else if logicalCPUCount >= 8 { - targetCPUCores = 6 - } else { - targetCPUCores = max(2, logicalCPUCount - 2) - } - let recommendedCPUCores = selection( - saved: targetCPUCores, - options: cpuOptions, - fallback: HostSizing.guestCPUCores(logicalCPUCount: logicalCPUCount) - ) - return GuestResourceConfiguration( - memoryMB: recommendedMemoryMB, - cpuCores: recommendedCPUCores - ) - } -} - -struct GuestResourceConfiguration: Equatable { - let memoryMB: Int - let cpuCores: Int -} - -enum InterfaceScaleOptions { - static let defaultPercent = 100 - static let percentages = [100, 125, 150, 175, 200] - - static func selection(saved: Int) -> Int { - percentages.contains(saved) ? saved : defaultPercent - } - - static func runtimeValue(percent: Int) -> String? { - switch percent { - case 100: "1.0" - case 125: "1.25" - case 150: "1.5" - case 175: "1.75" - case 200: "2.0" - default: nil - } - } -} - -extension String { - fileprivate var isLowercaseSHA256: Bool { - count == 64 && allSatisfy { ("0" ... "9").contains(String($0)) || ("a" ... "f").contains(String($0)) } - } -} diff --git a/launcher/Sources/EmulatorAudioRecoveryService.swift b/launcher/Sources/EmulatorAudioRecoveryService.swift deleted file mode 100644 index a5cb7b1..0000000 --- a/launcher/Sources/EmulatorAudioRecoveryService.swift +++ /dev/null @@ -1,404 +0,0 @@ -import CoreGraphics -import Foundation - -struct EmulatorWindowSize: Equatable { - let width: Int - let height: Int -} - -enum EmulatorAudioRecoveryReason: Equatable { - case startup - case windowResize - case halWriteFailure - - var logDescription: String { - switch self { - case .startup: - return "game startup" - case .windowResize: - return "game window resize" - case .halWriteFailure: - return "repeated Audio HAL write failures" - } - } -} - -enum EmulatorAudioFailureClassifier { - static func isHALWriteFailure(_ line: String) -> Bool { - let isWriteFailure = line.contains("pcmWrite:") || line.contains("pcm_writei") - return isWriteFailure - && (line.contains("failure: -1") - || line.contains("I/O error") - || line.contains("failed")) - } -} - -struct EmulatorAudioRecoveryPolicy { - private let startupDelay: TimeInterval - private let settleDelay: TimeInterval - private let cooldown: TimeInterval - private let failureThreshold: Int - private let failureWindow: TimeInterval - private var startedAt: TimeInterval? - private var startupRecoveryPending = true - private var lastSize: EmulatorWindowSize? - private var resizeSettledAt: TimeInterval? - private var lastRecoveryAt: TimeInterval? - private var failureBurstStartedAt: TimeInterval? - private var failureCount = 0 - - init( - startupDelay: TimeInterval = 1.5, - settleDelay: TimeInterval = 0.75, - cooldown: TimeInterval = 5, - failureThreshold: Int = 6, - failureWindow: TimeInterval = 1.5 - ) { - self.startupDelay = startupDelay - self.settleDelay = settleDelay - self.cooldown = cooldown - self.failureThreshold = failureThreshold - self.failureWindow = failureWindow - } - - mutating func observe( - size: EmulatorWindowSize?, - at now: TimeInterval - ) -> EmulatorAudioRecoveryReason? { - if startedAt == nil { startedAt = now } - - if startupRecoveryPending, - let startedAt, - now - startedAt >= startupDelay { - return requestRecovery(for: .startup, at: now) - } - - guard let size else { return nil } - guard let lastSize else { - self.lastSize = size - return nil - } - if size != lastSize { - self.lastSize = size - resizeSettledAt = now + settleDelay - return nil - } - guard let resizeSettledAt, now >= resizeSettledAt else { return nil } - self.resizeSettledAt = nil - return requestRecovery(for: .windowResize, at: now) - } - - mutating func observeHALWriteFailure(at now: TimeInterval) -> EmulatorAudioRecoveryReason? { - if failureBurstStartedAt.map({ now - $0 > failureWindow }) ?? true { - failureBurstStartedAt = now - failureCount = 1 - return nil - } - failureCount += 1 - guard failureCount >= failureThreshold else { return nil } - failureBurstStartedAt = nil - failureCount = 0 - return requestRecovery(for: .halWriteFailure, at: now) - } - - private mutating func requestRecovery( - for reason: EmulatorAudioRecoveryReason, - at now: TimeInterval - ) -> EmulatorAudioRecoveryReason? { - if let lastRecoveryAt, now - lastRecoveryAt < cooldown { return nil } - startupRecoveryPending = false - lastRecoveryAt = now - failureBurstStartedAt = nil - failureCount = 0 - return reason - } -} - -private struct EmulatorAudioRecoveryConfiguration { - let targetPID: pid_t - let adb: URL - let log: URL -} - -private final class EmulatorAudioRecoverySession { - private static let pollInterval: TimeInterval = 0.25 - - private let configuration: EmulatorAudioRecoveryConfiguration - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.audio-recovery") - private let lock = NSLock() - private var policy = EmulatorAudioRecoveryPolicy() - private var stopped = false - private var timer: DispatchSourceTimer? - private var recoveryProcess: Process? - private var monitorProcess: Process? - private var monitorPipe: Pipe? - private var monitorBuffer = Data() - - init(configuration: EmulatorAudioRecoveryConfiguration) { - self.configuration = configuration - } - - func start() { - queue.async { [self] in - installAudioMonitor() - installTimer() - } - } - - func stop() { - let (timer, recoveryProcess, monitorProcess, monitorPipe) = withLock { - stopped = true - let timer = self.timer - self.timer = nil - let recoveryProcess = self.recoveryProcess - self.recoveryProcess = nil - let monitorProcess = self.monitorProcess - self.monitorProcess = nil - let monitorPipe = self.monitorPipe - self.monitorPipe = nil - return (timer, recoveryProcess, monitorProcess, monitorPipe) - } - timer?.setEventHandler {} - timer?.cancel() - monitorPipe?.fileHandleForReading.readabilityHandler = nil - if recoveryProcess?.isRunning == true { recoveryProcess?.terminate() } - if monitorProcess?.isRunning == true { monitorProcess?.terminate() } - } - - private func installTimer() { - guard !isStopped else { return } - let timer = DispatchSource.makeTimerSource(queue: queue) - timer.setEventHandler { [weak self] in self?.poll() } - timer.schedule( - deadline: .now(), - repeating: Self.pollInterval, - leeway: .milliseconds(75) - ) - timer.resume() - let installed = withLock { - guard !stopped else { return false } - self.timer = timer - return true - } - if !installed { timer.cancel() } - } - - private func poll() { - guard !isStopped else { return } - ensureAudioMonitor() - let size = Self.mainWindowSize(for: configuration.targetPID) - if let reason = policy.observe( - size: size, - at: ProcessInfo.processInfo.systemUptime - ) { - recoverAudio(reason: reason) - } - } - - private func installAudioMonitor() { - guard !isStopped else { return } - let process = Process() - let pipe = Pipe() - process.executableURL = configuration.adb - process.arguments = [ - "-P", "5038", "-s", "emulator-5582", - "logcat", "-v", "brief", "-T", "1", - "android.hardware.audio@7.1-impl.ranchu:E", - "AudioSystem:E", - "*:S" - ] - process.standardOutput = pipe - process.standardError = FileHandle.nullDevice - process.environment = Self.adbEnvironment - pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in - let data = handle.availableData - guard !data.isEmpty else { return } - self?.queue.async { self?.consumeMonitorOutput(data) } - } - process.terminationHandler = { [weak self] finishedProcess in - self?.queue.async { - self?.monitorDidTerminate(finishedProcess) - } - } - - let registered = withLock { - guard !stopped, monitorProcess == nil else { return false } - monitorProcess = process - monitorPipe = pipe - return true - } - guard registered else { - pipe.fileHandleForReading.readabilityHandler = nil - return - } - - do { - try process.run() - } catch { - pipe.fileHandleForReading.readabilityHandler = nil - withLock { - if monitorProcess === process { - monitorProcess = nil - monitorPipe = nil - } - } - guard !isStopped else { return } - SystemServices.appendLog( - "Emulator audio monitor could not start: \(error.localizedDescription)", - to: configuration.log - ) - } - } - - private func ensureAudioMonitor() { - let isRunning = withLock { monitorProcess?.isRunning == true } - if !isRunning { installAudioMonitor() } - } - - private func monitorDidTerminate(_ process: Process) { - withLock { - guard monitorProcess === process else { return } - monitorPipe?.fileHandleForReading.readabilityHandler = nil - monitorProcess = nil - monitorPipe = nil - } - } - - private func consumeMonitorOutput(_ data: Data) { - guard !isStopped else { return } - monitorBuffer.append(data) - while let newline = monitorBuffer.firstIndex(of: 0x0A) { - let lineData = monitorBuffer[.. EmulatorWindowSize? { - guard let windows = CGWindowListCopyWindowInfo( - [.optionOnScreenOnly, .excludeDesktopElements], - CGWindowID(kCGNullWindowID) - ) as? [[CFString: Any]] else { return nil } - - var best: (size: EmulatorWindowSize, area: Int)? - for window in windows { - guard (window[kCGWindowOwnerPID] as? NSNumber)?.int32Value == targetPID, - (window[kCGWindowLayer] as? NSNumber)?.intValue == 0, - let bounds = window[kCGWindowBounds] as? NSDictionary, - let rawWidth = bounds["Width"] as? NSNumber, - let rawHeight = bounds["Height"] as? NSNumber else { - continue - } - let width = Int(rawWidth.doubleValue.rounded()) - let height = Int(rawHeight.doubleValue.rounded()) - guard width >= 500, height >= 300, width > height else { continue } - let area = width * height - if best.map({ area > $0.area }) ?? true { - best = (EmulatorWindowSize(width: width, height: height), area) - } - } - return best?.size - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } -} - -final class EmulatorAudioRecoveryService { - private let lock = NSLock() - private var session: EmulatorAudioRecoverySession? - - deinit { - stop() - } - - func start(targetPID: pid_t, adb: URL, log: URL) { - stop() - let session = EmulatorAudioRecoverySession(configuration: .init( - targetPID: targetPID, - adb: adb, - log: log - )) - withLock { self.session = session } - session.start() - } - - func stop() { - let session = withLock { - defer { self.session = nil } - return self.session - } - session?.stop() - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } -} diff --git a/launcher/Sources/EmulatorBrandingPatch.swift b/launcher/Sources/EmulatorBrandingPatch.swift deleted file mode 100644 index 29b2f0c..0000000 --- a/launcher/Sources/EmulatorBrandingPatch.swift +++ /dev/null @@ -1,189 +0,0 @@ -import Foundation - -enum EmulatorBrandingPatch { - // Google Emulator 37.1.11 (build 15917651), darwin-aarch64. - // The instruction patch disables the single call to - // QGuiApplication::setWindowIcon(QIcon const&) in - // EmulatorQtWindow::slot_setWindowIcon. Leaving the rest of the method - // intact preserves QIcon cleanup and QSemaphore release. - // - // Android Emulator also formats its native title from the AVD name. The - // replacement removes implementation details from the user-facing window - // and uses the same product-first naming as the rest of the app. - static let sourceSHA256 = "eaa97a970b81f81640db73ed79e19ee173323653a0a1203217e1199d078011f3" - static let iconInstructionOffset: UInt64 = 0x5af688 - static let sourceIconInstruction = Data([0xf4, 0xbf, 0x4d, 0x94]) - static let patchedIconInstruction = Data([0x1f, 0x20, 0x03, 0xd5]) - static let titleFormatOffset: UInt64 = 0x1a1e891 - static let sourceTitleFormat = fixedWidthData("%s Emulator - %s:%d", byteCount: 20) - static let patchedTitleFormat = fixedWidthData("Mactician: TFT PBE", byteCount: 20) - - static func isSupportedOrPatched(_ qemu: URL) -> Bool { - guard let iconInstruction = try? data( - at: iconInstructionOffset, - count: patchedIconInstruction.count, - in: qemu - ), let titleFormat = try? data( - at: titleFormatOffset, - count: sourceTitleFormat.count, - in: qemu - ) else { - return false - } - - if iconInstruction == sourceIconInstruction && titleFormat == sourceTitleFormat { - return (try? SystemServices.sha256(of: qemu)) == sourceSHA256 - } - guard iconInstruction == patchedIconInstruction, - titleFormat == sourceTitleFormat || titleFormat == patchedTitleFormat else { - return false - } - return (try? verifySignedExecutable(qemu)) != nil - } - - static func apply(at qemu: URL, entitlements: URL) throws { - let iconInstruction = try data( - at: iconInstructionOffset, - count: patchedIconInstruction.count, - in: qemu - ) - let titleFormat = try data( - at: titleFormatOffset, - count: sourceTitleFormat.count, - in: qemu - ) - if iconInstruction == patchedIconInstruction && titleFormat == patchedTitleFormat { - try verifyPatchedExecutable(qemu) - return - } - - if iconInstruction == sourceIconInstruction { - guard titleFormat == sourceTitleFormat, - try SystemServices.sha256(of: qemu) == sourceSHA256 else { - throw LauncherError.integrity( - "The Android Emulator binary failed verification. Use Repair Installation." - ) - } - } else if iconInstruction == patchedIconInstruction { - guard titleFormat == sourceTitleFormat else { - throw LauncherError.integrity( - "The Android Emulator branding patch is invalid. Use Repair Installation." - ) - } - try verifySignedExecutable(qemu) - } else { - throw LauncherError.integrity( - "The Android Emulator binary is not the supported 37.1.11 build. Use Repair Installation." - ) - } - guard FileManager.default.fileExists(atPath: entitlements.path) else { - throw LauncherError.integrity("The launcher hypervisor entitlement resource is missing") - } - - let fileManager = FileManager.default - let parent = qemu.deletingLastPathComponent() - let nonce = UUID().uuidString - let candidate = parent.appendingPathComponent(".qemu-system-aarch64.\(nonce).next") - let previous = parent.appendingPathComponent(".qemu-system-aarch64.\(nonce).previous") - defer { - try? fileManager.removeItem(at: candidate) - try? fileManager.removeItem(at: previous) - } - - try fileManager.copyItem(at: qemu, to: candidate) - let handle = try FileHandle(forWritingTo: candidate) - do { - if iconInstruction == sourceIconInstruction { - try handle.seek(toOffset: iconInstructionOffset) - try handle.write(contentsOf: patchedIconInstruction) - } - try handle.seek(toOffset: titleFormatOffset) - try handle.write(contentsOf: patchedTitleFormat) - try handle.close() - } catch { - try? handle.close() - throw error - } - guard try data( - at: iconInstructionOffset, - count: patchedIconInstruction.count, - in: candidate - ) == patchedIconInstruction, - try data( - at: titleFormatOffset, - count: patchedTitleFormat.count, - in: candidate - ) == patchedTitleFormat else { - throw LauncherError.integrity("Could not apply the Android Emulator branding patch") - } - - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/codesign"), - [ - "--force", "--sign", "-", "--timestamp=none", - "--preserve-metadata=identifier", - "--entitlements", entitlements.path, - candidate.path - ] - ) - try verifyPatchedExecutable(candidate) - - try fileManager.moveItem(at: qemu, to: previous) - do { - try fileManager.moveItem(at: candidate, to: qemu) - } catch { - try? fileManager.moveItem(at: previous, to: qemu) - throw error - } - try fileManager.removeItem(at: previous) - try verifyPatchedExecutable(qemu) - } - - private static func fixedWidthData(_ string: String, byteCount: Int) -> Data { - var result = Data(string.utf8) - precondition(result.count < byteCount) - result.append(contentsOf: repeatElement(0, count: byteCount - result.count)) - return result - } - - private static func data(at offset: UInt64, count: Int, in executable: URL) throws -> Data { - let handle = try FileHandle(forReadingFrom: executable) - defer { try? handle.close() } - try handle.seek(toOffset: offset) - let result = try handle.read(upToCount: count) ?? Data() - guard result.count == count else { - throw LauncherError.integrity("The Android Emulator executable is truncated") - } - return result - } - - private static func verifyPatchedExecutable(_ executable: URL) throws { - guard try data( - at: iconInstructionOffset, - count: patchedIconInstruction.count, - in: executable - ) == patchedIconInstruction, - try data( - at: titleFormatOffset, - count: patchedTitleFormat.count, - in: executable - ) == patchedTitleFormat else { - throw LauncherError.integrity("The Android Emulator branding patch is missing") - } - try verifySignedExecutable(executable) - } - - private static func verifySignedExecutable(_ executable: URL) throws { - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/codesign"), - ["--verify", "--strict", executable.path] - ) - let entitlementOutput = try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/codesign"), - ["-d", "--entitlements", "-", executable.path] - ) - guard entitlementOutput.contains("com.apple.security.hypervisor") else { - throw LauncherError.integrity("The patched emulator lost its Hypervisor entitlement") - } - } -} diff --git a/launcher/Sources/FPSOverlayService.swift b/launcher/Sources/FPSOverlayService.swift deleted file mode 100644 index 1f6b1d5..0000000 --- a/launcher/Sources/FPSOverlayService.swift +++ /dev/null @@ -1,386 +0,0 @@ -import AppKit -import CoreGraphics -import Foundation - -struct SurfaceFlingerFPSResult: Equatable { - let framesPerSecond: Double - let newestTimestamp: UInt64 -} - -enum SurfaceFlingerFPS { - private static let maximumTimestamp: UInt64 = 9_000_000_000_000_000_000 - private static let initialWindowNanoseconds: UInt64 = 1_250_000_000 - private static let maximumFrameNanoseconds: UInt64 = 1_000_000_000 - - static func gameLayer(from output: String, package: String) -> String? { - let needle = "SurfaceView[\(package)/com.epicgames.unreal.GameActivity](BLAST)" - for rawLine in output.split(separator: "\n").reversed() { - let line = String(rawLine).trimmingCharacters(in: .whitespacesAndNewlines) - guard line.contains(needle) else { continue } - guard line.hasPrefix("RequestedLayerState{") else { return line } - - var payload = String(line.dropFirst("RequestedLayerState{".count)) - if let parentRange = payload.range(of: " parentId=", options: .backwards) { - payload = String(payload[.. [UInt64] { - output.split(separator: "\n").compactMap { line in - let columns = line.split(whereSeparator: { $0.isWhitespace }) - guard columns.count >= 2, - let timestamp = UInt64(columns[1]), - timestamp > 0, - timestamp < maximumTimestamp else { - return nil - } - return timestamp - } - } - - static func estimate( - timestamps: [UInt64], - after previousTimestamp: UInt64? - ) -> SurfaceFlingerFPSResult? { - let ordered = Array(Set(timestamps)).sorted() - guard let newest = ordered.last, - previousTimestamp.map({ newest > $0 }) ?? true else { - return nil - } - - let initialFloor = newest > initialWindowNanoseconds - ? newest - initialWindowNanoseconds - : 0 - var anchors = ordered.filter { timestamp in - if let previousTimestamp { return timestamp > previousTimestamp } - return timestamp >= initialFloor - } - if let previousTimestamp, - newest - previousTimestamp <= maximumFrameNanoseconds, - anchors.first.map({ previousTimestamp < $0 }) ?? false { - anchors.insert(previousTimestamp, at: 0) - } - guard anchors.count >= 3 else { return nil } - - var total: UInt64 = 0 - var count = 0 - for index in 1 ..< anchors.count { - let delta = anchors[index] - anchors[index - 1] - guard delta > 0, delta <= maximumFrameNanoseconds else { continue } - total += delta - count += 1 - } - guard count >= 2 else { return nil } - - let meanNanoseconds = Double(total) / Double(count) - let fps = 1_000_000_000 / meanNanoseconds - guard fps >= 1, fps <= 240 else { return nil } - return SurfaceFlingerFPSResult(framesPerSecond: fps, newestTimestamp: newest) - } -} - -private struct FPSOverlayUpdate { - let fps: Int? - let emulatorWindow: CGRect? -} - -private final class FPSOverlayView: NSView { - var value = "— FPS" { - didSet { needsDisplay = true } - } - - override func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - - let background = NSBezierPath(roundedRect: bounds, xRadius: 6, yRadius: 6) - NSColor.black.withAlphaComponent(0.42).setFill() - background.fill() - NSColor.white.withAlphaComponent(0.10).setStroke() - background.lineWidth = 1 - background.stroke() - - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .medium), - .foregroundColor: NSColor.white.withAlphaComponent(0.78) - ] - let size = value.size(withAttributes: attributes) - value.draw( - at: NSPoint( - x: (bounds.width - size.width) / 2, - y: (bounds.height - size.height) / 2 - ), - withAttributes: attributes - ) - } -} - -private final class FPSOverlayPanel { - private static let size = NSSize(width: 62, height: 20) - private let panel: NSPanel - private let content: FPSOverlayView - - init() { - _ = NSApplication.shared - content = FPSOverlayView(frame: NSRect(origin: .zero, size: Self.size)) - panel = NSPanel( - contentRect: NSRect(origin: .zero, size: Self.size), - styleMask: [.borderless, .nonactivatingPanel], - backing: .buffered, - defer: false - ) - panel.backgroundColor = .clear - panel.isOpaque = false - panel.hasShadow = false - panel.ignoresMouseEvents = true - panel.level = .floating - panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary, .ignoresCycle] - panel.contentView = content - } - - func update(_ update: FPSOverlayUpdate, targetPID: pid_t) { - guard let window = update.emulatorWindow, - NSRunningApplication(processIdentifier: targetPID)?.isActive == true, - let origin = Self.panelOrigin(for: window) else { - hide() - return - } - - content.value = update.fps.map { "\($0) FPS" } ?? "— FPS" - panel.setFrameOrigin(origin) - if !panel.isVisible { panel.orderFrontRegardless() } - } - - func hide() { - if panel.isVisible { panel.orderOut(nil) } - } - - private static func panelOrigin(for quartzWindow: CGRect) -> NSPoint? { - var selected: (screen: NSScreen, quartz: CGRect, area: CGFloat)? - for screen in NSScreen.screens { - guard let number = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] - as? NSNumber else { continue } - let quartzScreen = CGDisplayBounds(CGDirectDisplayID(number.uint32Value)) - let intersection = quartzScreen.intersection(quartzWindow) - guard !intersection.isNull else { continue } - let area = intersection.width * intersection.height - if selected.map({ area > $0.area }) ?? true { - selected = (screen, quartzScreen, area) - } - } - guard let selected else { return nil } - - let left = selected.screen.frame.minX + quartzWindow.minX - selected.quartz.minX - let top = selected.screen.frame.maxY - quartzWindow.minY + selected.quartz.minY - return NSPoint( - x: left + quartzWindow.width - Self.size.width - 10, - y: top - Self.size.height - 5 - ) - } -} - -private struct FPSOverlayConfiguration { - let targetPID: pid_t - let adb: URL -} - -private final class FPSOverlaySession { - private static let package = "com.riotgames.league.teamfighttactics.pbe" - private static let pollInterval: TimeInterval = 1 - - private let configuration: FPSOverlayConfiguration - private let publish: (FPSOverlayUpdate) -> Void - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.fps-overlay") - private let lock = NSLock() - private var stopped = false - private var timer: DispatchSourceTimer? - private var process: Process? - private var layer: String? - private var previousTimestamp: UInt64? - - init(configuration: FPSOverlayConfiguration, publish: @escaping (FPSOverlayUpdate) -> Void) { - self.configuration = configuration - self.publish = publish - } - - func start() { - queue.async { [self] in installTimer() } - } - - func stop() { - let (timer, process) = withLock { - stopped = true - let timer = self.timer - self.timer = nil - let process = self.process - self.process = nil - return (timer, process) - } - timer?.setEventHandler {} - timer?.cancel() - if process?.isRunning == true { process?.terminate() } - } - - private func installTimer() { - guard !isStopped else { return } - let timer = DispatchSource.makeTimerSource(queue: queue) - timer.setEventHandler { [weak self] in self?.poll() } - timer.schedule(deadline: .now(), repeating: Self.pollInterval, leeway: .milliseconds(150)) - timer.resume() - let installed = withLock { - guard !stopped else { return false } - self.timer = timer - return true - } - if !installed { timer.cancel() } - } - - private func poll() { - guard !isStopped else { return } - let window = Self.mainWindowBounds(for: configuration.targetPID) - guard window != nil else { - publish(FPSOverlayUpdate(fps: nil, emulatorWindow: nil)) - return - } - - if layer == nil { - layer = queryLayer() - previousTimestamp = nil - } - guard let layer else { - publish(FPSOverlayUpdate(fps: nil, emulatorWindow: nil)) - return - } - guard let output = runADB(["shell", "dumpsys SurfaceFlinger --latency \(Self.shellQuote(layer))"]) else { - publish(FPSOverlayUpdate(fps: nil, emulatorWindow: window)) - return - } - - let timestamps = SurfaceFlingerFPS.presentationTimestamps(from: output) - guard !timestamps.isEmpty else { - self.layer = nil - previousTimestamp = nil - publish(FPSOverlayUpdate(fps: nil, emulatorWindow: nil)) - return - } - if let result = SurfaceFlingerFPS.estimate( - timestamps: timestamps, - after: previousTimestamp - ) { - previousTimestamp = result.newestTimestamp - publish(FPSOverlayUpdate( - fps: Int(result.framesPerSecond.rounded()), - emulatorWindow: window - )) - } else { - publish(FPSOverlayUpdate(fps: nil, emulatorWindow: window)) - } - } - - private func queryLayer() -> String? { - guard let output = runADB(["shell", "dumpsys SurfaceFlinger --list"]) else { return nil } - return SurfaceFlingerFPS.gameLayer(from: output, package: Self.package) - } - - private func runADB(_ arguments: [String]) -> String? { - let process = Process() - let stdout = Pipe() - process.executableURL = configuration.adb - process.arguments = ["-P", "5038", "-s", "emulator-5582"] + arguments - process.standardOutput = stdout - process.standardError = FileHandle.nullDevice - process.environment = ProcessInfo.processInfo.environment.merging([ - "ANDROID_ADB_SERVER_PORT": "5038", - "ADB_MDNS_AUTO_CONNECT": "" - ]) { _, new in new } - - let registered = withLock { - guard !stopped else { return false } - self.process = process - return true - } - guard registered else { return nil } - defer { - withLock { - if self.process === process { self.process = nil } - } - } - - do { - try process.run() - } catch { - return nil - } - let data = stdout.fileHandleForReading.readDataToEndOfFile() - process.waitUntilExit() - guard !isStopped, process.terminationStatus == 0 else { return nil } - return String(data: data, encoding: .utf8) - } - - private var isStopped: Bool { withLock { stopped } } - - private static func mainWindowBounds(for targetPID: pid_t) -> CGRect? { - guard let windows = CGWindowListCopyWindowInfo( - [.optionOnScreenOnly, .excludeDesktopElements], - CGWindowID(kCGNullWindowID) - ) as? [[CFString: Any]] else { return nil } - - var best: (bounds: CGRect, area: CGFloat)? - for window in windows { - guard (window[kCGWindowOwnerPID] as? NSNumber)?.int32Value == targetPID, - (window[kCGWindowLayer] as? NSNumber)?.intValue == 0, - let rawBounds = window[kCGWindowBounds] as? NSDictionary, - let bounds = CGRect(dictionaryRepresentation: rawBounds), - bounds.width >= 500, - bounds.height >= 300, - bounds.width > bounds.height else { - continue - } - let area = bounds.width * bounds.height - if best.map({ area > $0.area }) ?? true { best = (bounds, area) } - } - return best?.bounds - } - - private static func shellQuote(_ value: String) -> String { - "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'" - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } -} - -final class FPSOverlayService { - private let panel = FPSOverlayPanel() - private var session: FPSOverlaySession? - - deinit { - stop() - } - - func start(targetPID: pid_t, adb: URL) { - stop() - let session = FPSOverlaySession( - configuration: FPSOverlayConfiguration(targetPID: targetPID, adb: adb) - ) { [weak self] update in - DispatchQueue.main.async { - self?.panel.update(update, targetPID: targetPID) - } - } - self.session = session - session.start() - } - - func stop() { - session?.stop() - session = nil - panel.hide() - } -} diff --git a/launcher/Sources/HostedGameUpdate.swift b/launcher/Sources/HostedGameUpdate.swift deleted file mode 100644 index 5561ad4..0000000 --- a/launcher/Sources/HostedGameUpdate.swift +++ /dev/null @@ -1,75 +0,0 @@ -import CryptoKit -import Foundation - -struct HostedGameFeedEnvelope: Codable, Equatable { - let schemaVersion: Int - let payload: String - let signature: String -} - -struct HostedGameFeed: Codable, Equatable { - let schemaVersion: Int - let publishedAt: String - let release: GameRelease - - func validate() throws { - guard schemaVersion == 1, - ISO8601DateFormatter().date(from: publishedAt) != nil, - let versionCode = release.versionCode, - versionCode > 0 else { - throw LauncherError.invalidManifest("Invalid hosted TFT PBE feed") - } - try release.validate() - for apk in release.apks { - guard let url = apk.url, - url.scheme == "https", - url.host == MacticianIdentity.gameUpdateURL.host, - url.user == nil, - url.password == nil, - url.query == nil, - url.fragment == nil, - url.path.hasPrefix("/mactician/updates/game/releases/") else { - throw LauncherError.invalidManifest("APK \(apk.name) uses an untrusted URL") - } - } - } -} - -enum HostedGameUpdate { - static func isNewer(_ release: GameRelease, than state: InstallState) -> Bool { - if let remoteVersionCode = release.versionCode, - let installedVersionCode = state.gameVersionCode { - return remoteVersionCode > installedVersionCode - } - return release.version != state.gameVersion - } - - static func decodeAndVerify( - _ envelopeData: Data, - publicKeyBase64: String = MacticianIdentity.gameUpdatePublicKeyBase64 - ) throws -> HostedGameFeed { - let envelope = try JSONDecoder().decode(HostedGameFeedEnvelope.self, from: envelopeData) - guard envelope.schemaVersion == 1, - let payload = Data(base64Encoded: envelope.payload), - let signature = Data(base64Encoded: envelope.signature), - let publicKeyData = Data(base64Encoded: publicKeyBase64) else { - throw LauncherError.invalidManifest("Invalid hosted TFT PBE feed envelope") - } - let publicKey: Curve25519.Signing.PublicKey - do { - publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKeyData) - } catch { - throw LauncherError.integrity("The TFT PBE feed public key is invalid") - } - guard publicKey.isValidSignature(signature, for: payload) else { - throw LauncherError.integrity("The TFT PBE feed signature is invalid") - } - let feed = try JSONDecoder().decode(HostedGameFeed.self, from: payload) - try feed.validate() - return feed - } - - static func loadVerifiedFeed(from url: URL) throws -> HostedGameFeed { - try decodeAndVerify(Data(contentsOf: url)) - } -} diff --git a/launcher/Sources/InputBridgeService.swift b/launcher/Sources/InputBridgeService.swift deleted file mode 100644 index 08ff6ec..0000000 --- a/launcher/Sources/InputBridgeService.swift +++ /dev/null @@ -1,627 +0,0 @@ -import AppKit -import ApplicationServices -import Darwin -import Foundation - -private struct BridgeConfiguration { - let targetPID: pid_t - let targetBundleID: String - let adb: URL - let width: Int - let height: Int -} - -private enum BridgeAction { - case shop - case reroll - case buyXP - case toggleItemsAndTraits - case togglePlayersAndDamage -} - -struct BridgeRelativePoint: Equatable { - let x: Double - let y: Double - - func pixels(width: Int, height: Int) -> (x: Int, y: Int) { - ( - min(width - 1, max(0, Int((Double(width) * x).rounded()))), - min(height - 1, max(0, Int((Double(height) * y).rounded()))) - ) - } -} - -enum BridgeHotkeyTarget { - static let shop = BridgeRelativePoint(x: 0.96, y: 0.93) - static let reroll = BridgeRelativePoint(x: 0.955, y: 0.79) - static let buyXP = BridgeRelativePoint(x: 0.032, y: 0.925) - static let traits = BridgeRelativePoint(x: 0.029, y: 0.04) - static let items = BridgeRelativePoint(x: 0.059, y: 0.04) - static let damage = BridgeRelativePoint(x: 0.947, y: 0.04) - static let players = BridgeRelativePoint(x: 0.975, y: 0.04) -} - -enum BridgeKeyboardBinding { - static let reroll: UInt16 = 2 - static let buyXP: UInt16 = 3 - static let playersAndDamage: UInt16 = 9 - static let tab: UInt16 = 48 - static let shop: UInt16 = 49 - - fileprivate static func action(for keyCode: UInt16) -> BridgeAction? { - switch keyCode { - case shop: return .shop - case reroll: return .reroll - case buyXP: return .buyXP - case playersAndDamage: return .togglePlayersAndDamage - case tab: return .toggleItemsAndTraits - default: return nil - } - } - - static func isActionKey(_ keyCode: UInt16) -> Bool { - action(for: keyCode) != nil - } -} - -enum BridgeForegroundActivityState: Equatable { - case unknown - case gameplay - case nonGameplay -} - -enum BridgeAndroidActivityClassifier { - private static let gameplayActivity = "com.epicgames.unreal.GameActivity" - - static func classify(dumpsysOutput: String) -> BridgeForegroundActivityState { - guard let line = dumpsysOutput.split(separator: "\n").first(where: { - $0.contains("topResumedActivity=") - }), - let marker = line.range(of: "topResumedActivity=") else { - return .unknown - } - - let payload = line[marker.upperBound...] - for rawToken in payload.split(whereSeparator: { $0.isWhitespace }) { - let token = rawToken.trimmingCharacters(in: CharacterSet(charactersIn: "{}(),")) - guard let slash = token.firstIndex(of: "/") else { continue } - let activity = String(token[token.index(after: slash)...]) - guard !activity.isEmpty else { return .unknown } - return activity == gameplayActivity ? .gameplay : .nonGameplay - } - return .unknown - } -} - -enum BridgeKeyboardEventDisposition: Equatable { - case passThrough - case consume - case hotkey -} - -enum BridgeKeyboardEventPolicy { - private static let passthroughModifiers: NSEvent.ModifierFlags = [ - .command, .control, .option - ] - - static func disposition( - for keyCode: UInt16, - activity: BridgeForegroundActivityState, - modifierFlags: NSEvent.ModifierFlags - ) -> BridgeKeyboardEventDisposition { - guard activity == .gameplay, - modifierFlags.intersection(passthroughModifiers).isEmpty else { - return .passThrough - } - return BridgeKeyboardBinding.isActionKey(keyCode) ? .hotkey : .consume - } -} - -private final class BridgeAndroidActivityMonitor { - private static let pollInterval: TimeInterval = 1 - private static let stateMaxAge: TimeInterval = 3 - - private let configuration: BridgeConfiguration - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.input-bridge.activity") - private let lock = NSLock() - private var state = BridgeForegroundActivityState.unknown - private var stateUpdatedAt: TimeInterval = 0 - private var stopped = true - private var process: Process? - - init(configuration: BridgeConfiguration) { - self.configuration = configuration - } - - func start() { - let shouldStart = withLock { - guard stopped else { return false } - stopped = false - return true - } - guard shouldStart else { return } - queue.async { [self] in poll() } - } - - func stop() { - let process = withLock { - stopped = true - state = .unknown - stateUpdatedAt = ProcessInfo.processInfo.systemUptime - let process = self.process - self.process = nil - return process - } - if process?.isRunning == true { process?.terminate() } - } - - var currentState: BridgeForegroundActivityState { - // The event-tap callback must never wait for ADB. Fail open while a - // poll is publishing a new activity state. - guard lock.try() else { return .unknown } - defer { lock.unlock() } - let stateAge = ProcessInfo.processInfo.systemUptime - stateUpdatedAt - return stateAge <= Self.stateMaxAge ? state : .unknown - } - - private var isStopped: Bool { withLock { stopped } } - - private func poll() { - guard !isStopped else { return } - - let newState = queryState() - guard !isStopped else { return } - publish(newState) - queue.asyncAfter(deadline: .now() + Self.pollInterval) { [self] in poll() } - } - - private func queryState() -> BridgeForegroundActivityState { - let process = Process() - let stdout = Pipe() - process.executableURL = configuration.adb - process.arguments = [ - "-P", "5038", "-s", "emulator-5582", - "shell", "dumpsys activity activities 2>/dev/null | grep -m 1 'topResumedActivity='" - ] - process.standardOutput = stdout - process.standardError = FileHandle.nullDevice - process.environment = ProcessInfo.processInfo.environment.merging([ - "ANDROID_ADB_SERVER_PORT": "5038", - "ADB_MDNS_AUTO_CONNECT": "" - ]) { _, new in new } - - let registered = withLock { - guard !stopped else { return false } - self.process = process - return true - } - guard registered else { return .unknown } - defer { - withLock { - if self.process === process { self.process = nil } - } - } - - do { - try process.run() - } catch { - return .unknown - } - if isStopped { - if process.isRunning { process.terminate() } - process.waitUntilExit() - return .unknown - } - - let data = stdout.fileHandleForReading.readDataToEndOfFile() - process.waitUntilExit() - guard process.terminationStatus == 0, - let output = String(data: data, encoding: .utf8) else { - return .unknown - } - return BridgeAndroidActivityClassifier.classify(dumpsysOutput: output) - } - - private func publish(_ newState: BridgeForegroundActivityState) { - withLock { - guard !stopped else { return } - state = newState - stateUpdatedAt = ProcessInfo.processInfo.systemUptime - } - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } -} - -private final class BridgeTapDispatcher { - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.adb-input") - private let configuration: BridgeConfiguration - private var shell: Process? - private var input: FileHandle? - private var stopped = false - private var showTraitsNext = true - private var showDamageNext = true - - init(configuration: BridgeConfiguration) { - self.configuration = configuration - } - - func warmUp() { - queue.async { [weak self] in - guard let self, !stopped else { return } - do { - try ensureShell(configuration) - } catch { - stopShell() - } - } - } - - func send(_ action: BridgeAction) { - queue.async { [weak self] in - guard let self, !stopped else { return } - let point = target(for: action).pixels( - width: configuration.width, - height: configuration.height - ) - do { - try ensureShell(configuration) - try input?.write(contentsOf: Data("input tap \(point.x) \(point.y)\n".utf8)) - } catch { - stopShell() - } - } - } - - func stop() { - queue.async { [self] in - stopped = true - stopShell() - } - } - - private func target(for action: BridgeAction) -> BridgeRelativePoint { - switch action { - case .shop: - return BridgeHotkeyTarget.shop - case .reroll: - return BridgeHotkeyTarget.reroll - case .buyXP: - return BridgeHotkeyTarget.buyXP - case .toggleItemsAndTraits: - defer { showTraitsNext.toggle() } - return showTraitsNext ? BridgeHotkeyTarget.traits : BridgeHotkeyTarget.items - case .togglePlayersAndDamage: - defer { showDamageNext.toggle() } - return showDamageNext ? BridgeHotkeyTarget.damage : BridgeHotkeyTarget.players - } - } - - private func ensureShell(_ configuration: BridgeConfiguration) throws { - if shell?.isRunning == true, input != nil { return } - stopShell() - let process = Process() - let stdin = Pipe() - process.executableURL = configuration.adb - process.arguments = ["-P", "5038", "-s", "emulator-5582", "shell"] - process.standardInput = stdin - process.standardOutput = FileHandle.nullDevice - process.standardError = FileHandle.nullDevice - process.environment = ProcessInfo.processInfo.environment.merging([ - "ANDROID_ADB_SERVER_PORT": "5038", - "ADB_MDNS_AUTO_CONNECT": "" - ]) { _, new in new } - try process.run() - shell = process - input = stdin.fileHandleForWriting - } - - private func stopShell() { - try? input?.close() - input = nil - if shell?.isRunning == true { shell?.terminate() } - shell = nil - } -} - -struct BridgeSessionGeneration { - private(set) var current: UInt64 = 0 - - mutating func advance() -> UInt64 { - current &+= 1 - return current - } - - func accepts(_ generation: UInt64) -> Bool { - generation == current - } -} - -private struct BridgeEventTapResources { - let tap: CFMachPort - let source: CFRunLoopSource - let runLoop: CFRunLoop -} - -private final class BridgeEventTapSession { - private weak var owner: InputBridgeService? - private let generation: UInt64 - private let configuration: BridgeConfiguration - private let dispatcher: BridgeTapDispatcher - private let activityMonitor: BridgeAndroidActivityMonitor - private let lock = NSLock() - private var stopped = false - private var resources: BridgeEventTapResources? - - init( - owner: InputBridgeService, - generation: UInt64, - configuration: BridgeConfiguration - ) { - self.owner = owner - self.generation = generation - self.configuration = configuration - dispatcher = BridgeTapDispatcher(configuration: configuration) - activityMonitor = BridgeAndroidActivityMonitor(configuration: configuration) - } - - func start() { - activityMonitor.start() - dispatcher.warmUp() - Thread.detachNewThread { [self] in run() } - } - - func stop() { - let resources = withLock { - stopped = true - return takeResources() - } - activityMonitor.stop() - dispatcher.stop() - tearDown(resources, stopRunLoop: true) - } - - fileprivate func handle(type: CGEventType, event: CGEvent) -> Unmanaged? { - if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput { - if let tap = withLock({ resources?.tap }), CFMachPortIsValid(tap) { - CGEvent.tapEnable(tap: tap, enable: true) - } - return Unmanaged.passUnretained(event) - } - guard !isStopped, - let frontmost = NSWorkspace.shared.frontmostApplication else { - return Unmanaged.passUnretained(event) - } - let emulatorIsFrontmost = frontmost.processIdentifier == configuration.targetPID - || frontmost.bundleIdentifier == configuration.targetBundleID - guard emulatorIsFrontmost else { return Unmanaged.passUnretained(event) } - switch type { - case .rightMouseDown, .rightMouseUp, .rightMouseDragged: - return nil - case .keyDown, .keyUp: - guard let cocoaEvent = NSEvent(cgEvent: event) else { - return Unmanaged.passUnretained(event) - } - switch BridgeKeyboardEventPolicy.disposition( - for: cocoaEvent.keyCode, - activity: activityMonitor.currentState, - modifierFlags: cocoaEvent.modifierFlags - ) { - case .passThrough: - return Unmanaged.passUnretained(event) - case .consume: - return nil - case .hotkey: - if let action = BridgeKeyboardBinding.action(for: cocoaEvent.keyCode), - type == .keyDown, - !cocoaEvent.isARepeat { - dispatcher.send(action) - } - return nil - } - default: - return Unmanaged.passUnretained(event) - } - } - - private func run() { - while !isStopped { - if installTap() { - owner?.reportStatus( - generation: generation, - eventTapActive: true, - eventTapAttemptFailed: false - ) - CFRunLoopRun() - tearDownCurrentTap() - owner?.reportStatus( - generation: generation, - eventTapActive: false, - eventTapAttemptFailed: false - ) - } else if !isStopped { - owner?.reportStatus( - generation: generation, - eventTapActive: false, - eventTapAttemptFailed: true - ) - } - if !isStopped { Thread.sleep(forTimeInterval: 2) } - } - } - - private var isStopped: Bool { withLock { stopped } } - - private func installTap() -> Bool { - guard !isStopped else { return false } - let types: [CGEventType] = [ - .keyDown, .keyUp, - .rightMouseDown, .rightMouseUp, .rightMouseDragged - ] - let mask = types.reduce(CGEventMask(0)) { $0 | (CGEventMask(1) << $1.rawValue) } - guard let tap = CGEvent.tapCreate( - tap: .cgAnnotatedSessionEventTap, - place: .headInsertEventTap, - options: .defaultTap, - eventsOfInterest: mask, - callback: launcherBridgeCallback, - userInfo: Unmanaged.passUnretained(self).toOpaque() - ) else { return false } - - CGEvent.tapEnable(tap: tap, enable: false) - guard let runLoop = CFRunLoopGetCurrent(), - let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else { - CFMachPortInvalidate(tap) - return false - } - CFRunLoopAddSource(runLoop, source, .commonModes) - let resources = BridgeEventTapResources(tap: tap, source: source, runLoop: runLoop) - let installed = withLock { - guard !stopped else { return false } - self.resources = resources - return true - } - guard installed else { - tearDown(resources, stopRunLoop: false) - return false - } - CGEvent.tapEnable(tap: tap, enable: true) - if isStopped { - tearDownCurrentTap() - return false - } - return true - } - - private func tearDownCurrentTap() { - let resources = withLock { takeResources() } - tearDown(resources, stopRunLoop: false) - } - - private func takeResources() -> BridgeEventTapResources? { - defer { resources = nil } - return resources - } - - private func tearDown(_ resources: BridgeEventTapResources?, stopRunLoop: Bool) { - guard let resources else { return } - CGEvent.tapEnable(tap: resources.tap, enable: false) - CFRunLoopRemoveSource(resources.runLoop, resources.source, .commonModes) - CFMachPortInvalidate(resources.tap) - if stopRunLoop { CFRunLoopStop(resources.runLoop) } - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } -} - -final class InputBridgeService { - typealias StatusHandler = (_ eventTapActive: Bool, _ eventTapAttemptFailed: Bool) -> Void - - private let lock = NSLock() - private var sessionGeneration = BridgeSessionGeneration() - private var session: BridgeEventTapSession? - private var statusHandler: StatusHandler? - - static func permissionFacts( - eventTapActive: Bool, - eventTapAttemptFailed: Bool - ) -> LauncherHotkeyFacts { - LauncherHotkeyFacts( - accessibilityTrusted: AXIsProcessTrusted(), - eventTapActive: eventTapActive, - eventTapAttemptFailed: eventTapAttemptFailed - ) - } - - func observeStatus(_ handler: @escaping StatusHandler) { - withLock { statusHandler = handler } - } - - func requestPermissions() { - let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String - let trusted = AXIsProcessTrustedWithOptions([promptKey: true] as CFDictionary) - guard !trusted, - let settingsURL = URL( - string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - ) else { return } - NSWorkspace.shared.open(settingsURL) - } - - func start(targetPID: pid_t, adb: URL, width: Int, height: Int) { - stop() - let configuration = BridgeConfiguration( - targetPID: targetPID, - targetBundleID: "dev.sergeinaumov.mactician.game-host", - adb: adb, - width: width, - height: height - ) - let session = withLock { - let generation = sessionGeneration.advance() - let session = BridgeEventTapSession( - owner: self, - generation: generation, - configuration: configuration - ) - self.session = session - return session - } - session.start() - } - - func stop() { - let (session, generation) = withLock { - let session = self.session - self.session = nil - return (session, sessionGeneration.advance()) - } - session?.stop() - reportStatus( - generation: generation, - eventTapActive: false, - eventTapAttemptFailed: false - ) - } - - fileprivate func reportStatus( - generation: UInt64, - eventTapActive: Bool, - eventTapAttemptFailed: Bool - ) { - DispatchQueue.main.async { [weak self] in - guard let self else { return } - let handler: StatusHandler? = self.withLock { - guard self.sessionGeneration.accepts(generation) else { return nil } - return self.statusHandler - } - handler?(eventTapActive, eventTapAttemptFailed) - } - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } -} - -private func launcherBridgeCallback( - proxy _: CGEventTapProxy, - type: CGEventType, - event: CGEvent, - userInfo: UnsafeMutableRawPointer? -) -> Unmanaged? { - guard let userInfo else { return Unmanaged.passUnretained(event) } - return Unmanaged - .fromOpaque(userInfo) - .takeUnretainedValue() - .handle(type: type, event: event) -} diff --git a/launcher/Sources/InstallerService.swift b/launcher/Sources/InstallerService.swift deleted file mode 100644 index a0dc7c7..0000000 --- a/launcher/Sources/InstallerService.swift +++ /dev/null @@ -1,958 +0,0 @@ -import Darwin -import Foundation - -struct InstallerProgress { - enum Phase: Equatable { - case checking - case downloading - case extracting - case creatingAVD - case installingGame - case finished - case paused - } - - let phase: Phase - let message: String - let fraction: Double -} - -struct GameUpdateResult { - let state: InstallState - let release: GameRelease - let changed: Bool -} - -struct GameUpdateAvailability { - let release: GameRelease - let isAvailable: Bool -} - -final class InstallerService { - typealias ProgressHandler = (InstallerProgress) -> Void - typealias CompletionHandler = (Result) -> Void - - private let paths: LauncherPaths - private let manifest: ReleaseManifest - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.installer", qos: .userInitiated) - private let lock = NSLock() - private var activeProcess: Process? - private var cancelled = false - private var paused = false - - init(paths: LauncherPaths, manifest: ReleaseManifest) { - self.paths = paths - self.manifest = manifest - } - - static func curlArguments(component: SDKComponent, destination: URL) -> [String] { - [ - "-fL", "--retry", "3", "--retry-delay", "2", "--continue-at", "-", - component.url.absoluteString, "-o", destination.path - ] - } - - static func hostedDownloadArguments(url: URL, destination: URL) -> [String] { - [ - "-fL", "--retry", "3", "--retry-delay", "2", "--continue-at", "-", - "--proto", "=https", "--tlsv1.2", url.absoluteString, "-o", destination.path - ] - } - - static func extractArchive(_ archive: URL, to destination: URL) throws { - // ditto rejects the valid ZIP64 Android system-image archive with a - // "Couldn't read pkzip signature" error. The macOS unzip utility - // handles all pinned Android SDK archives and preserves their modes. - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/unzip"), - ["-q", archive.path, "-d", destination.path] - ) - } - - static func adbArguments(_ arguments: [String]) -> [String] { - ["-P", "5038"] + arguments - } - - static func gameInstallArguments(apkPaths: [String]) -> [String] { - adbArguments([ - "-s", "emulator-5582", "install-multiple", "--no-streaming", "-r", "-g" - ] + apkPaths) - } - - static func emulatorArguments(initializeData: Bool, logicalCPUCount: Int) -> [String] { - var arguments = [ - "@TftPBE", "-id", "TFT-TftPBE", "-port", "5582", "-gpu", "host", - "-skin", "1920x1080", "-vsync-rate", "60", - "-dns-server", "1.1.1.1,8.8.8.8", - "-cores", "\(HostSizing.guestCPUCores(logicalCPUCount: logicalCPUCount))", - "-memory", "6144", "-no-snapshot", "-no-metrics", "-no-boot-anim", - "-no-window", "-no-audio", "-crash-report-mode", "disabled" - ] - if initializeData { - // A newly created QCOW2 disk is intentionally empty. The emulator - // must format and seed it before adbd can complete its first boot. - arguments.append("-wipe-data") - } - return arguments - } - - func install(repair: Bool, progress: @escaping ProgressHandler, completion: @escaping CompletionHandler) { - lock.lock() - guard activeProcess == nil else { - lock.unlock() - completion(.failure(LauncherError.process("Installation is already in progress"))) - return - } - cancelled = false - paused = false - lock.unlock() - - queue.async { [self] in - do { - let state = try performInstall(repair: repair, progress: progress) - DispatchQueue.main.async { completion(.success(state)) } - } catch { - SystemServices.appendLog("Installer error: \(error.localizedDescription)", to: paths.launcherLog) - DispatchQueue.main.async { completion(.failure(error)) } - } - } - } - - func updateGame( - currentState: InstallState, - progress: @escaping ProgressHandler, - completion: @escaping (Result) -> Void - ) { - lock.lock() - guard activeProcess == nil else { - lock.unlock() - completion(.failure(LauncherError.process("Installation is already in progress"))) - return - } - cancelled = false - paused = false - lock.unlock() - - queue.async { [self] in - do { - let result = try performGameUpdate(currentState: currentState, progress: progress) - DispatchQueue.main.async { completion(.success(result)) } - } catch { - SystemServices.appendLog("Game updater error: \(error.localizedDescription)", to: paths.launcherLog) - DispatchQueue.main.async { completion(.failure(error)) } - } - } - } - - func checkGameUpdateAvailability( - currentState: InstallState, - completion: @escaping (Result) -> Void - ) { - lock.lock() - guard activeProcess == nil else { - lock.unlock() - completion(.failure(LauncherError.process("Installation is already in progress"))) - return - } - cancelled = false - paused = false - lock.unlock() - - queue.async { [self] in - do { - try FileManager.default.createDirectory( - at: paths.downloads, - withIntermediateDirectories: true - ) - let hosted = try fetchHostedGameFeed(progress: { _ in }) - let release = hosted.feed.release - if let installedVersionCode = currentState.gameVersionCode, - let remoteVersionCode = release.versionCode, - remoteVersionCode < installedVersionCode { - throw LauncherError.unsupportedGame( - "The hosted TFT PBE release is older than the installed game" - ) - } - let availability = GameUpdateAvailability( - release: release, - isAvailable: HostedGameUpdate.isNewer(release, than: currentState) - ) - DispatchQueue.main.async { completion(.success(availability)) } - } catch { - SystemServices.appendLog( - "Game update availability check failed: \(error.localizedDescription)", - to: paths.launcherLog - ) - DispatchQueue.main.async { completion(.failure(error)) } - } - } - } - - func pause() { - lock.lock() - defer { lock.unlock() } - guard let process = activeProcess, process.isRunning, !paused else { return } - Darwin.kill(process.processIdentifier, SIGSTOP) - paused = true - } - - func resume() { - lock.lock() - defer { lock.unlock() } - guard let process = activeProcess, process.isRunning, paused else { return } - Darwin.kill(process.processIdentifier, SIGCONT) - paused = false - } - - func cancel() { - lock.lock() - defer { lock.unlock() } - cancelled = true - if let process = activeProcess, process.isRunning { - Darwin.kill(process.processIdentifier, SIGCONT) - process.terminate() - } - } - - private func performInstall(repair: Bool, progress: @escaping ProgressHandler) throws -> InstallState { - progressOnMain(progress, .init(phase: .checking, message: "Checking your Mac and files…", fraction: 0)) - try SystemServices.checkHost(minimumFreeBytes: manifest.minimumFreeBytes, root: paths.root) - try Self.prepareDirectories(at: paths) - - let hosted: (data: Data, feed: HostedGameFeed)? - do { - hosted = try fetchHostedGameFeed(progress: progress) - } catch { - SystemServices.appendLog( - "Hosted TFT PBE feed unavailable, using bundled fallback: \(error.localizedDescription)", - to: paths.launcherLog - ) - hosted = nil - } - let gameRelease = hosted?.feed.release ?? manifest.game - if hosted == nil { - try verifyGame(release: gameRelease, in: paths.gameResources) - } - - var state = SystemServices.loadState(from: paths.stateFile) - state.stage = .downloading - try SystemServices.saveState(state, to: paths.stateFile) - - let usesBundledGame = hosted?.feed.release.baseSHA256 == manifest.game.baseSHA256 - let gameDownloadBytes = usesBundledGame - ? 0 - : hosted?.feed.release.apks.reduce(Int64(0)) { $0 + $1.size } ?? 0 - let totalBytes = manifest.components.reduce(Int64(0)) { $0 + $1.size } + gameDownloadBytes - var completedBytes: Int64 = 0 - for component in manifest.components { - try checkCancellation() - let target = paths.root.appendingPathComponent(component.installPath, isDirectory: true) - let isInstalled = componentLayoutIsValid(component, at: target) - if !isInstalled { - try installComponent( - component, - completedBytes: completedBytes, - totalBytes: totalBytes, - progress: progress - ) - } - completedBytes += component.size - state.installedComponents[component.id] = component.version - try SystemServices.saveState(state, to: paths.stateFile) - } - - let gameResources: URL - if let hosted, !usesBundledGame { - gameResources = try downloadHostedGame( - hosted.feed.release, - completedBytes: completedBytes, - totalBytes: totalBytes, - progress: progress - ) - } else { - gameResources = paths.gameResources - } - - try verifySDKLayout() - try EmulatorBrandingPatch.apply( - at: paths.qemuSystem, - entitlements: paths.qemuHypervisorEntitlements - ) - state.stage = .sdkInstalled - try SystemServices.saveState(state, to: paths.stateFile) - - progressOnMain(progress, .init(phase: .extracting, message: "Preparing secure runtime…", fraction: 0.88)) - try Self.refreshRuntimeProject(at: paths) - let overlayHash = try buildOverlay(release: gameRelease, resources: gameResources) - - progressOnMain(progress, .init(phase: .creatingAVD, message: "Creating a clean Android device…", fraction: 0.91)) - try createAVDIfNeeded() - state.stage = .avdCreated - state.overlaySHA256 = overlayHash - try SystemServices.saveState(state, to: paths.stateFile) - - progressOnMain(progress, .init(phase: .installingGame, message: "Installing TFT PBE…", fraction: 0.94)) - try provisionGame(release: gameRelease, resources: gameResources) - state.stage = .ready - state.gameVersion = gameRelease.version - state.gameVersionCode = gameRelease.versionCode - state.gameBaseSHA256 = gameRelease.baseSHA256 - state.overlaySHA256 = overlayHash - try SystemServices.saveState(state, to: paths.stateFile) - if let hosted { - try saveHostedGameFeed(hosted.data) - } - try? FileManager.default.removeItem(at: paths.downloads) - - progressOnMain(progress, .init(phase: .finished, message: "Done", fraction: 1)) - return state - } - - private func performGameUpdate( - currentState: InstallState, - progress: @escaping ProgressHandler - ) throws -> GameUpdateResult { - progressOnMain(progress, .init( - phase: .checking, - message: "Checking for TFT PBE updates…", - fraction: 0 - )) - try Self.prepareDirectories(at: paths) - let hosted = try fetchHostedGameFeed(progress: progress) - let release = hosted.feed.release - if let installedVersionCode = currentState.gameVersionCode, - let remoteVersionCode = release.versionCode, - remoteVersionCode < installedVersionCode { - throw LauncherError.unsupportedGame("The hosted TFT PBE release is older than the installed game") - } - - if currentState.gameVersion == release.version, - currentState.gameBaseSHA256 == release.baseSHA256 { - var state = currentState - state.gameVersionCode = release.versionCode - try SystemServices.saveState(state, to: paths.stateFile) - try saveHostedGameFeed(hosted.data) - progressOnMain(progress, .init(phase: .finished, message: "TFT PBE is up to date", fraction: 1)) - return GameUpdateResult(state: state, release: release, changed: false) - } - - let resources = try downloadHostedGame( - release, - completedBytes: 0, - totalBytes: release.apks.reduce(Int64(0)) { $0 + $1.size }, - progress: progress - ) - - try verifySDKLayout() - progressOnMain(progress, .init( - phase: .extracting, - message: "Preparing the new game version…", - fraction: 0.88 - )) - try Self.refreshRuntimeProject(at: paths) - let overlayHash = try buildOverlay(release: release, resources: resources) - progressOnMain(progress, .init( - phase: .installingGame, - message: "Updating TFT PBE…", - fraction: 0.94 - )) - try provisionGame(release: release, resources: resources) - - var state = currentState - state.stage = .ready - state.gameVersion = release.version - state.gameVersionCode = release.versionCode - state.gameBaseSHA256 = release.baseSHA256 - state.overlaySHA256 = overlayHash - try SystemServices.saveState(state, to: paths.stateFile) - try saveHostedGameFeed(hosted.data) - try? FileManager.default.removeItem(at: paths.downloads) - progressOnMain(progress, .init(phase: .finished, message: "TFT PBE updated", fraction: 1)) - return GameUpdateResult(state: state, release: release, changed: true) - } - - static func prepareDirectories(at paths: LauncherPaths) throws { - let fileManager = FileManager.default - for directory in [paths.root, paths.downloads, paths.gameCache, paths.logDirectory, paths.avdHome] { - try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) - } - if fileManager.fileExists(atPath: paths.staging.path) { - try fileManager.removeItem(at: paths.staging) - } - try fileManager.createDirectory(at: paths.staging, withIntermediateDirectories: true) - } - - private func verifyGame(release: GameRelease, in resources: URL) throws { - for apk in release.apks { - let url = resources.appendingPathComponent(apk.name) - let attributes = try FileManager.default.attributesOfItem(atPath: url.path) - let size = (attributes[.size] as? NSNumber)?.int64Value ?? -1 - guard size == apk.size else { - throw LauncherError.integrity("Invalid size for \(apk.name)") - } - let hash = try SystemServices.sha256(of: url) - guard hash == apk.sha256 else { - throw LauncherError.integrity("SHA-256 mismatch for \(apk.name)") - } - } - } - - private func fetchHostedGameFeed( - progress: @escaping ProgressHandler - ) throws -> (data: Data, feed: HostedGameFeed) { - let partial = paths.downloads.appendingPathComponent("hosted-game-feed.json.partial") - try? FileManager.default.removeItem(at: partial) - try downloadHostedFile( - url: MacticianIdentity.gameUpdateURL, - to: partial, - displayName: "TFT PBE update information", - expectedSize: nil, - completedBytes: 0, - totalBytes: 1, - progress: progress - ) - let attributes = try FileManager.default.attributesOfItem(atPath: partial.path) - let size = (attributes[.size] as? NSNumber)?.int64Value ?? -1 - guard (1 ... 1_048_576).contains(size) else { - throw LauncherError.integrity("The TFT PBE update information is too large") - } - let data = try Data(contentsOf: partial) - let feed = try HostedGameUpdate.decodeAndVerify(data) - try? FileManager.default.removeItem(at: partial) - return (data, feed) - } - - private func downloadHostedGame( - _ release: GameRelease, - completedBytes: Int64, - totalBytes: Int64, - progress: @escaping ProgressHandler - ) throws -> URL { - let fileManager = FileManager.default - let resources = paths.gameReleaseDirectory(baseSHA256: release.baseSHA256) - try fileManager.createDirectory(at: resources, withIntermediateDirectories: true) - var downloadedBeforeThisAPK = completedBytes - - for apk in release.apks { - try checkCancellation() - guard let sourceURL = apk.url else { - throw LauncherError.invalidManifest("Hosted APK \(apk.name) has no URL") - } - let destination = resources.appendingPathComponent(apk.name) - if fileManager.fileExists(atPath: destination.path) { - let attributes = try fileManager.attributesOfItem(atPath: destination.path) - let size = (attributes[.size] as? NSNumber)?.int64Value ?? -1 - if size == apk.size, try SystemServices.sha256(of: destination) == apk.sha256 { - downloadedBeforeThisAPK += apk.size - continue - } - try fileManager.removeItem(at: destination) - } - - let partial = paths.downloads.appendingPathComponent("game-\(apk.sha256).partial") - try downloadHostedFile( - url: sourceURL, - to: partial, - displayName: apk.name, - expectedSize: apk.size, - completedBytes: downloadedBeforeThisAPK, - totalBytes: max(totalBytes, 1), - progress: progress - ) - let attributes = try fileManager.attributesOfItem(atPath: partial.path) - let size = (attributes[.size] as? NSNumber)?.int64Value ?? -1 - guard size == apk.size else { - try? fileManager.removeItem(at: partial) - throw LauncherError.integrity("Invalid size for hosted APK \(apk.name)") - } - guard try SystemServices.sha256(of: partial) == apk.sha256 else { - try? fileManager.removeItem(at: partial) - throw LauncherError.integrity("SHA-256 mismatch for hosted APK \(apk.name)") - } - try fileManager.moveItem(at: partial, to: destination) - downloadedBeforeThisAPK += apk.size - } - try verifyGame(release: release, in: resources) - return resources - } - - private func saveHostedGameFeed(_ data: Data) throws { - try FileManager.default.createDirectory( - at: paths.hostedGameFeed.deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try data.write(to: paths.hostedGameFeed, options: .atomic) - } - - private func downloadHostedFile( - url: URL, - to partial: URL, - displayName: String, - expectedSize: Int64?, - completedBytes: Int64, - totalBytes: Int64, - progress: @escaping ProgressHandler - ) throws { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/curl") - process.arguments = Self.hostedDownloadArguments(url: url, destination: partial) - process.standardOutput = FileHandle.nullDevice - process.standardError = try SystemServices.appendHandle(for: paths.launcherLog) - lock.lock() - activeProcess = process - lock.unlock() - defer { - lock.lock() - activeProcess = nil - paused = false - lock.unlock() - } - try process.run() - while process.isRunning { - try checkCancellation() - let downloaded = ((try? FileManager.default.attributesOfItem(atPath: partial.path)[.size]) as? NSNumber)?.int64Value ?? 0 - let boundedDownload = expectedSize.map { min(downloaded, $0) } ?? 0 - let overall = min(0.86, Double(completedBytes + boundedDownload) / Double(totalBytes) * 0.86) - let isPaused = withLock { paused } - progressOnMain(progress, .init( - phase: isPaused ? .paused : .downloading, - message: isPaused ? "Download paused" : "Downloading \(displayName)…", - fraction: overall - )) - Thread.sleep(forTimeInterval: 0.25) - } - process.waitUntilExit() - guard process.terminationStatus == 0 else { - throw LauncherError.process("Could not download \(displayName). Check your connection and try again.") - } - } - - private func installComponent( - _ component: SDKComponent, - completedBytes: Int64, - totalBytes: Int64, - progress: @escaping ProgressHandler - ) throws { - let fileManager = FileManager.default - let partial = paths.downloads.appendingPathComponent("\(component.id).zip.partial") - let archive = paths.downloads.appendingPathComponent("\(component.id).zip") - if fileManager.fileExists(atPath: archive.path), - try SystemServices.sha256(of: archive) != component.sha256 { - try fileManager.removeItem(at: archive) - } - - if !fileManager.fileExists(atPath: archive.path) { - try download( - component, - to: partial, - completedBytes: completedBytes, - totalBytes: totalBytes, - progress: progress - ) - let hash = try SystemServices.sha256(of: partial) - guard hash == component.sha256 else { - try? fileManager.removeItem(at: partial) - throw LauncherError.integrity("SHA-256 mismatch for component \(component.id)") - } - try fileManager.moveItem(at: partial, to: archive) - } - - let extractRoot = paths.staging.appendingPathComponent("\(component.id)-\(UUID().uuidString)", isDirectory: true) - defer { try? fileManager.removeItem(at: extractRoot) } - try fileManager.createDirectory(at: extractRoot, withIntermediateDirectories: true) - progressOnMain(progress, .init( - phase: .extracting, - message: "Extracting \(component.id)…", - fraction: Double(completedBytes + component.size) / Double(totalBytes) * 0.86 - )) - try Self.extractArchive(archive, to: extractRoot) - let extracted = extractRoot.appendingPathComponent(component.archiveRoot, isDirectory: true) - guard fileManager.fileExists(atPath: extracted.path) else { - throw LauncherError.integrity("Archive \(component.id) does not contain \(component.archiveRoot)") - } - let target = paths.root.appendingPathComponent(component.installPath, isDirectory: true) - try fileManager.createDirectory(at: target.deletingLastPathComponent(), withIntermediateDirectories: true) - if fileManager.fileExists(atPath: target.path) { - try fileManager.removeItem(at: target) - } - try fileManager.moveItem(at: extracted, to: target) - try fileManager.removeItem(at: archive) - } - - private func download( - _ component: SDKComponent, - to partial: URL, - completedBytes: Int64, - totalBytes: Int64, - progress: @escaping ProgressHandler - ) throws { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/curl") - process.arguments = Self.curlArguments(component: component, destination: partial) - process.standardOutput = FileHandle.nullDevice - process.standardError = try SystemServices.appendHandle(for: paths.launcherLog) - lock.lock() - activeProcess = process - lock.unlock() - defer { - lock.lock() - activeProcess = nil - paused = false - lock.unlock() - } - try process.run() - while process.isRunning { - try checkCancellation() - let downloaded = ((try? FileManager.default.attributesOfItem(atPath: partial.path)[.size]) as? NSNumber)?.int64Value ?? 0 - let overall = min(0.86, Double(completedBytes + min(downloaded, component.size)) / Double(totalBytes) * 0.86) - let isPaused = withLock { paused } - progressOnMain(progress, .init( - phase: isPaused ? .paused : .downloading, - message: isPaused ? "Download paused" : "Downloading \(component.id)…", - fraction: overall - )) - Thread.sleep(forTimeInterval: 0.25) - } - process.waitUntilExit() - guard process.terminationStatus == 0 else { - throw LauncherError.process("Could not download \(component.id). Check your connection and press Retry.") - } - } - - private func verifySDKLayout() throws { - for executable in [paths.adb, paths.emulator, paths.qemuImg] { - guard FileManager.default.isExecutableFile(atPath: executable.path) else { - throw LauncherError.integrity("Executable \(executable.lastPathComponent) was not found") - } - } - guard FileManager.default.fileExists(atPath: paths.systemImage.appendingPathComponent("system.img").path) else { - throw LauncherError.integrity("The Android system image was not fully extracted") - } - let output = try SystemServices.run(paths.emulator, ["-accel-check"]) - guard output.localizedCaseInsensitiveContains("installed and usable") || output.contains("accel:\n0") else { - throw LauncherError.preflight("Android Emulator did not confirm hardware virtualization: \(output)") - } - } - - private func componentLayoutIsValid(_ component: SDKComponent, at target: URL) -> Bool { - let fileManager = FileManager.default - guard fileManager.fileExists(atPath: target.path) else { return false } - switch component.id { - case "platform-tools": - return fileManager.isExecutableFile(atPath: target.appendingPathComponent("adb").path) - && sourceProperties(at: target).contains("Pkg.Revision=36.0.2") - case "emulator": - return fileManager.isExecutableFile(atPath: target.appendingPathComponent("emulator").path) - && fileManager.isExecutableFile(atPath: target.appendingPathComponent("qemu-img").path) - && sourceProperties(at: target).contains("Pkg.Revision=37.1.11") - && EmulatorBrandingPatch.isSupportedOrPatched( - target.appendingPathComponent("qemu/darwin-aarch64/qemu-system-aarch64") - ) - case "system-image": - return fileManager.fileExists(atPath: target.appendingPathComponent("system.img").path) - && fileManager.fileExists(atPath: target.appendingPathComponent("encryptionkey.img").path) - && sourceProperties(at: target).contains("Pkg.Revision=7") - default: - return false - } - } - - private func sourceProperties(at directory: URL) -> String { - (try? String(contentsOf: directory.appendingPathComponent("source.properties"), encoding: .utf8)) ?? "" - } - - static func refreshRuntimeProject(at paths: LauncherPaths) throws { - let fileManager = FileManager.default - try fileManager.createDirectory(at: paths.staging, withIntermediateDirectories: true) - let next = paths.staging.appendingPathComponent("runtime-project-\(UUID().uuidString)", isDirectory: true) - defer { try? fileManager.removeItem(at: next) } - try fileManager.copyItem(at: paths.runtimeTemplate, to: next) - - guard fileManager.fileExists(atPath: paths.runtimeProject.path) else { - try fileManager.moveItem(at: next, to: paths.runtimeProject) - return - } - - let previous = paths.staging.appendingPathComponent( - "runtime-project-\(UUID().uuidString).previous", - isDirectory: true - ) - try fileManager.moveItem(at: paths.runtimeProject, to: previous) - do { - try fileManager.moveItem(at: next, to: paths.runtimeProject) - } catch { - try? fileManager.moveItem(at: previous, to: paths.runtimeProject) - throw error - } - try fileManager.removeItem(at: previous) - } - - static func unrealCommandLine(for language: GameLanguage) -> String { - "-project=\"../../../TFT/TFT.uproject\" -opengl -novsync -ResX=1600 -ResY=900 DeviceProfile=Android_Codex -culture=\(language.id)\n" - } - - static func prepareOverlay( - source: URL, - expectedSourceSHA256: String, - destination: URL, - stagingRoot: URL, - language: GameLanguage - ) throws -> String { - let fileManager = FileManager.default - let originalHash = try SystemServices.sha256(of: source) - guard originalHash == expectedSourceSHA256 else { - throw LauncherError.integrity("The source TFT base.apk failed verification") - } - try fileManager.createDirectory(at: destination.deletingLastPathComponent(), withIntermediateDirectories: true) - try fileManager.createDirectory(at: stagingRoot, withIntermediateDirectories: true) - - let candidate = destination.deletingLastPathComponent().appendingPathComponent( - ".\(destination.lastPathComponent).\(UUID().uuidString).next" - ) - defer { try? fileManager.removeItem(at: candidate) } - try fileManager.copyItem(at: source, to: candidate) - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/zip"), - ["-q", "-d", candidate.path, "assets/UECommandLine.txt"] - ) - - let patchRoot = stagingRoot.appendingPathComponent("overlay-\(UUID().uuidString)", isDirectory: true) - defer { try? fileManager.removeItem(at: patchRoot) } - let assets = patchRoot.appendingPathComponent("assets", isDirectory: true) - try fileManager.createDirectory(at: assets, withIntermediateDirectories: true) - let commandLine = unrealCommandLine(for: language) - try Data(commandLine.utf8).write(to: assets.appendingPathComponent("UECommandLine.txt"), options: .atomic) - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/zip"), - ["-q", "-9", candidate.path, "assets/UECommandLine.txt"], - currentDirectory: patchRoot - ) - let extracted = try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/unzip"), - ["-p", candidate.path, "assets/UECommandLine.txt"] - ) - guard extracted == commandLine else { - throw LauncherError.integrity("The ANGLE/OpenGL overlay contains an unexpected UECommandLine") - } - let hash = try SystemServices.sha256(of: candidate) - try SystemServices.run( - URL(fileURLWithPath: "/bin/mv"), - ["-f", candidate.path, destination.path] - ) - return hash - } - - private func buildOverlay(release: GameRelease, resources: URL) throws -> String { - try Self.prepareOverlay( - source: resources.appendingPathComponent("base.apk"), - expectedSourceSHA256: release.baseSHA256, - destination: paths.overlayAPK, - stagingRoot: paths.staging, - language: .english - ) - } - - private func createAVDIfNeeded() throws { - let fileManager = FileManager.default - let essentials = [ - paths.avdDirectory.appendingPathComponent("config.ini"), - paths.avdDirectory.appendingPathComponent("userdata-qemu.img"), - paths.avdDirectory.appendingPathComponent("encryptionkey.img") - ] - if fileManager.fileExists(atPath: paths.avdINI.path), - essentials.allSatisfy({ fileManager.fileExists(atPath: $0.path) }) { - return - } - if fileManager.fileExists(atPath: paths.avdINI.path) { - throw LauncherError.integrity("The AVD is corrupted. Use Reset Data to create it again.") - } - if fileManager.fileExists(atPath: paths.avdDirectory.path) { - try fileManager.removeItem(at: paths.avdDirectory) - } - let nextAVD = paths.staging.appendingPathComponent("TftPBE-\(UUID().uuidString).avd", isDirectory: true) - defer { try? fileManager.removeItem(at: nextAVD) } - try fileManager.createDirectory(at: nextAVD, withIntermediateDirectories: true) - let config = """ - AvdId=TftPBE - avd.ini.displayname=Mactician - abi.type=arm64-v8a - hw.cpu.arch=arm64 - hw.cpu.ncore=6 - hw.lcd.density=320 - hw.lcd.height=1080 - hw.lcd.width=1920 - hw.ramSize=6144 - hw.vmHeapSize=576 - hw.gpu.enabled=yes - hw.gpu.mode=host - hw.gltransport=pipe - hw.keyboard=yes - skin.name=1920x1080 - showDeviceFrame=no - disk.dataPartition.size=12288M - image.sysdir.1=system-images/android-36/google_apis/arm64-v8a/ - tag.id=google_apis - tag.display=Google APIs - PlayStore.enabled=false - fastboot.forceColdBoot=yes - fastboot.forceFastBoot=no - avd.ini.encoding=UTF-8 - """ - try Data(config.utf8).write(to: nextAVD.appendingPathComponent("config.ini"), options: .atomic) - let ini = """ - avd.ini.encoding=UTF-8 - path=\(paths.avdDirectory.path) - path.rel=TftPBE.avd - target=android-36 - """ - try SystemServices.run(paths.qemuImg, [ - "create", "-f", "qcow2", - nextAVD.appendingPathComponent("userdata-qemu.img").path, - "12G" - ]) - try fileManager.copyItem( - at: paths.systemImage.appendingPathComponent("encryptionkey.img"), - to: nextAVD.appendingPathComponent("encryptionkey.img") - ) - try fileManager.createDirectory(at: paths.avdHome, withIntermediateDirectories: true) - try fileManager.moveItem(at: nextAVD, to: paths.avdDirectory) - try Data(ini.utf8).write(to: paths.avdINI, options: .atomic) - } - - private func provisionGame(release: GameRelease, resources: URL) throws { - let environment = androidEnvironment() - _ = try SystemServices.run(paths.adb, Self.adbArguments(["start-server"]), environment: environment) - if (try? SystemServices.run( - paths.adb, - Self.adbArguments(["-s", "emulator-5582", "get-state"]), - environment: environment - )) != nil { - throw LauncherError.process("The TFT emulator is already running. Close it and retry the installation.") - } - - let emulator = Process() - emulator.executableURL = paths.emulator - emulator.arguments = Self.emulatorArguments( - initializeData: !FileManager.default.fileExists(atPath: paths.avdBootCompleted.path), - logicalCPUCount: ProcessInfo.processInfo.processorCount - ) - emulator.environment = ProcessInfo.processInfo.environment.merging(environment) { _, new in new } - let log = try SystemServices.appendHandle(for: paths.launcherLog) - emulator.standardOutput = log - emulator.standardError = log - try emulator.run() - lock.lock() - activeProcess = emulator - lock.unlock() - defer { - if emulator.isRunning { emulator.terminate() } - lock.lock() - if activeProcess === emulator { activeProcess = nil } - lock.unlock() - try? log.close() - } - - try waitFor(timeout: 180, description: "ADB") { - guard emulator.isRunning else { throw LauncherError.process("Android Emulator exited during provisioning") } - return (try? SystemServices.run( - self.paths.adb, - Self.adbArguments(["-s", "emulator-5582", "get-state"]), - environment: environment - )) != nil - } - try waitFor(timeout: 180, description: "Android boot") { - let value = try? SystemServices.run( - self.paths.adb, - Self.adbArguments(["-s", "emulator-5582", "shell", "getprop", "sys.boot_completed"]), - environment: environment - ) - return value?.trimmingCharacters(in: .whitespacesAndNewlines) == "1" - } - _ = try SystemServices.run( - paths.adb, - Self.adbArguments(["-s", "emulator-5582", "root"]), - environment: environment - ) - try waitFor(timeout: 30, description: "root adbd") { - let value = try? SystemServices.run( - self.paths.adb, - Self.adbArguments(["-s", "emulator-5582", "shell", "id", "-u"]), - environment: environment - ) - return value?.trimmingCharacters(in: .whitespacesAndNewlines) == "0" - } - - let apkPaths = release.apks.map { resources.appendingPathComponent($0.name).path } - _ = try SystemServices.run( - paths.adb, - Self.gameInstallArguments(apkPaths: apkPaths), - environment: environment - ) - let packagePaths = try SystemServices.run(paths.adb, Self.adbArguments([ - "-s", "emulator-5582", "shell", "pm", "path", release.packageName - ]), environment: environment) - guard let basePath = packagePaths - .split(separator: "\n") - .map(String.init) - .first(where: { $0.hasSuffix("/base.apk") })? - .replacingOccurrences(of: "package:", with: "") else { - throw LauncherError.integrity("TFT base.apk was not found after installation") - } - let guestHash = try SystemServices.run(paths.adb, Self.adbArguments([ - "-s", "emulator-5582", "shell", "sha256sum", basePath - ]), environment: environment).split(separator: " ").first.map(String.init) - guard guestHash == release.baseSHA256 else { - throw LauncherError.unsupportedGame("The installed TFT PBE version does not match verified version \(release.version)") - } - // Android 16 can acknowledge a large split-APK install before all - // /data/app extents reach the virtual disk. Stopping QEMU immediately - // leaves PackageManager metadata behind while the APK files disappear - // on the next cold boot. Flush the guest filesystem before emu kill. - _ = try SystemServices.run( - paths.adb, - Self.adbArguments(["-s", "emulator-5582", "shell", "sync"]), - environment: environment - ) - Thread.sleep(forTimeInterval: 2) - _ = try? SystemServices.run( - paths.adb, - Self.adbArguments(["-s", "emulator-5582", "emu", "kill"]), - environment: environment - ) - let deadline = Date().addingTimeInterval(30) - while emulator.isRunning && Date() < deadline { Thread.sleep(forTimeInterval: 0.25) } - } - - private func waitFor(timeout: TimeInterval, description: String, condition: () throws -> Bool) throws { - let deadline = Date().addingTimeInterval(timeout) - while Date() < deadline { - try checkCancellation() - if try condition() { return } - Thread.sleep(forTimeInterval: 1) - } - throw LauncherError.process("Timed out: \(description)") - } - - private func androidEnvironment() -> [String: String] { - [ - "ANDROID_SDK_ROOT": paths.sdk.path, - "ANDROID_AVD_HOME": paths.avdHome.path, - "ANDROID_ADB_SERVER_PORT": "5038", - "ADB_MDNS_AUTO_CONNECT": "" - ] - } - - private func checkCancellation() throws { - if withLock({ cancelled }) { throw LauncherError.cancelled } - } - - private func withLock(_ body: () -> T) -> T { - lock.lock() - defer { lock.unlock() } - return body() - } - - private func progressOnMain(_ handler: @escaping ProgressHandler, _ value: InstallerProgress) { - DispatchQueue.main.async { handler(value) } - } -} diff --git a/launcher/Sources/LauncherAnnouncementView.swift b/launcher/Sources/LauncherAnnouncementView.swift deleted file mode 100644 index dedf820..0000000 --- a/launcher/Sources/LauncherAnnouncementView.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -struct LauncherAnnouncementView: View { - let announcement: LauncherAnnouncement - let dismiss: () -> Void - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - if let image = announcement.image { - Image(nsImage: image) - .resizable() - .scaledToFit() - .frame(maxWidth: .infinity, maxHeight: 300) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - } - - Text(announcement.title) - .font(.system(size: 24, weight: .bold)) - .foregroundStyle(LauncherTheme.ColorToken.textPrimary) - - ScrollView { - Text(announcement.text) - .font(.system(size: 15)) - .foregroundStyle(LauncherTheme.ColorToken.textSecondary) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(maxHeight: 240) - - HStack { - Spacer() - Button(LauncherL10n.text("action.close"), action: dismiss) - .keyboardShortcut(.defaultAction) - } - } - .padding(28) - .frame(width: 600) - .background(LauncherTheme.ColorToken.surface) - } -} diff --git a/launcher/Sources/LauncherComponents.swift b/launcher/Sources/LauncherComponents.swift deleted file mode 100644 index 4510b05..0000000 --- a/launcher/Sources/LauncherComponents.swift +++ /dev/null @@ -1,431 +0,0 @@ -import AppKit -import SwiftUI - -struct LauncherBrandMark: View { - var body: some View { - MacticianMark() - .frame(width: 34, height: 34) - .accessibilityHidden(true) - } -} - -struct MacticianMark: View { - var body: some View { - GeometryReader { geometry in - let width = geometry.size.width - let height = geometry.size.height - let ribbonStyle = StrokeStyle( - lineWidth: width * 0.135, - lineCap: .round, - lineJoin: .round - ) - ZStack { - RoundedRectangle(cornerRadius: width * 0.24, style: .continuous) - .fill(LauncherTheme.ColorToken.window) - Path { path in - path.move(to: CGPoint(x: width * 0.18, y: height * 0.80)) - path.addLine(to: CGPoint(x: width * 0.18, y: height * 0.58)) - path.addLine(to: CGPoint(x: width * 0.30, y: height * 0.25)) - path.addLine(to: CGPoint(x: width * 0.40, y: height * 0.25)) - path.addLine(to: CGPoint(x: width * 0.50, y: height * 0.68)) - } - .stroke( - LinearGradient( - colors: [ - Color(red: 1.00, green: 0.35, blue: 0.78), - Color(red: 0.46, green: 0.24, blue: 0.89) - ], - startPoint: .topLeading, - endPoint: .bottomTrailing - ), - style: ribbonStyle - ) - Path { path in - path.move(to: CGPoint(x: width * 0.82, y: height * 0.80)) - path.addLine(to: CGPoint(x: width * 0.82, y: height * 0.58)) - path.addLine(to: CGPoint(x: width * 0.70, y: height * 0.25)) - path.addLine(to: CGPoint(x: width * 0.60, y: height * 0.25)) - path.addLine(to: CGPoint(x: width * 0.50, y: height * 0.68)) - } - .stroke( - LinearGradient( - colors: [ - Color(red: 0.48, green: 0.36, blue: 1.00), - Color(red: 0.39, green: 0.21, blue: 0.84) - ], - startPoint: .topTrailing, - endPoint: .bottomLeading - ), - style: ribbonStyle - ) - Capsule() - .fill(Color(red: 0.78, green: 0.65, blue: 0.36)) - .frame(width: max(1, width * 0.025), height: height * 0.17) - .position(x: width * 0.50, y: height * 0.64) - Circle() - .fill(LauncherTheme.ColorToken.primaryAction) - .frame(width: width * 0.055, height: width * 0.055) - .position(x: width * 0.18, y: height * 0.75) - Circle() - .fill(LauncherTheme.ColorToken.primaryAction) - .frame(width: width * 0.055, height: width * 0.055) - .position(x: width * 0.82, y: height * 0.75) - } - } - .aspectRatio(1, contentMode: .fit) - } -} - -struct LauncherDragRegion: NSViewRepresentable { - func makeNSView(context _: Context) -> NSView { DragView() } - func updateNSView(_: NSView, context _: Context) { } - - private final class DragView: NSView { - override func mouseDown(with event: NSEvent) { - window?.performDrag(with: event) - } - } -} - -struct LauncherStatusIcon: View { - let symbol: String - let color: Color - var spinning = false - - var body: some View { - ZStack { - Circle() - .fill(color.opacity(0.12)) - Circle() - .stroke(color.opacity(0.42), lineWidth: 1) - if spinning { - ProgressView() - .controlSize(.small) - .tint(color) - } else { - Image(systemName: symbol) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(color) - } - } - .frame(width: 48, height: 48) - .accessibilityHidden(true) - } -} - -struct LauncherStatusHeader: View { - let symbol: String - let color: Color - let title: String - let description: String - var spinning = false - - var body: some View { - HStack(alignment: .top, spacing: LauncherTheme.Spacing.regular) { - LauncherStatusIcon(symbol: symbol, color: color, spinning: spinning) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(title) - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(description) - .font(.system(size: 13)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .fixedSize(horizontal: false, vertical: true) - } - } - .accessibilityElement(children: .combine) - } -} - -struct LauncherSummaryField: View { - let label: String - let value: String - let detail: String? - let content: Content - - init( - label: String, - value: String, - detail: String? = nil, - @ViewBuilder content: () -> Content - ) { - self.label = label - self.value = value - self.detail = detail - self.content = content() - } - - var body: some View { - content - .frame(maxWidth: .infinity, minHeight: 64, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill(LauncherTheme.ColorToken.raisedControl.opacity(0.82)) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .stroke(LauncherTheme.ColorToken.neutralBorder, lineWidth: 1) - ) - .accessibilityLabel(label) - .accessibilityValue(detail.map { "\(value), \($0)" } ?? value) - } -} - -struct LauncherFieldLabel: View { - let text: String - - var body: some View { - Text(text) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - } -} - -struct LauncherMenuControl: View { - let value: String - let showsBackground: Bool - let menuContent: MenuContent - - init( - value: String, - showsBackground: Bool = true, - @ViewBuilder content: () -> MenuContent - ) { - self.value = value - self.showsBackground = showsBackground - menuContent = content() - } - - var body: some View { - Menu { - menuContent - } label: { - HStack(spacing: LauncherTheme.Spacing.small) { - Text(value) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - .lineLimit(1) - Spacer(minLength: LauncherTheme.Spacing.small) - } - .padding(.horizontal, showsBackground ? LauncherTheme.Spacing.medium : 0) - .frame(maxWidth: .infinity, minHeight: 30) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill( - showsBackground - ? LauncherTheme.ColorToken.raisedControl.opacity(0.82) - : .clear - ) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .stroke( - showsBackground ? LauncherTheme.ColorToken.neutralBorder : .clear, - lineWidth: 1 - ) - ) - .contentShape(Rectangle()) - } - .menuStyle(.borderlessButton) - .accessibilityValue(value) - } -} - -struct LauncherSectionHeader: View { - let title: String - let description: String? - - init(_ title: String, description: String? = nil) { - self.title = title - self.description = description - } - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(title) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - if let description { - Text(description) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .fixedSize(horizontal: false, vertical: true) - } - } - } -} - -struct LauncherDivider: View { - var body: some View { - Rectangle() - .fill(LauncherTheme.ColorToken.neutralBorder) - .frame(height: 1) - } -} - -struct LauncherPrimaryButtonStyle: ButtonStyle { - @Environment(\.isEnabled) private var isEnabled - @Environment(\.isFocused) private var isFocused - @Environment(\.accessibilityReduceMotion) private var reduceMotion - @State private var isHovered = false - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.system(size: 15, weight: .bold)) - .foregroundColor(LauncherTheme.ColorToken.surface) - .padding(.horizontal, LauncherTheme.Spacing.large) - .frame(minHeight: LauncherTheme.Metric.primaryControlHeight) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius, style: .continuous) - .fill( - isEnabled - ? LauncherTheme.ColorToken.primaryAction.opacity(isHovered ? 1 : 0.94) - : LauncherTheme.ColorToken.raisedControl - ) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius, style: .continuous) - .stroke(Color.white.opacity(isEnabled ? 0.25 : 0.08), lineWidth: 1) - ) - .shadow( - color: LauncherTheme.ColorToken.primaryAction.opacity( - isEnabled && !configuration.isPressed ? 0.24 : 0 - ), - radius: 8, - y: 3 - ) - .scaleEffect(reduceMotion || !configuration.isPressed ? 1 : 0.99) - .foregroundColor(isEnabled ? LauncherTheme.ColorToken.surface : LauncherTheme.ColorToken.textTertiary) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius + 2) - .stroke( - isFocused ? LauncherTheme.ColorToken.interactive : .clear, - lineWidth: 2 - ) - .padding(-3) - ) - .onHover { isHovered = $0 } - } -} - -struct LauncherSecondaryButtonStyle: ButtonStyle { - @Environment(\.isEnabled) private var isEnabled - @Environment(\.isFocused) private var isFocused - @State private var isHovered = false - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(isEnabled ? LauncherTheme.ColorToken.textPrimary : LauncherTheme.ColorToken.textTertiary) - .padding(.horizontal, LauncherTheme.Spacing.regular) - .frame(minHeight: LauncherTheme.Metric.standardControlHeight) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius, style: .continuous) - .fill( - LauncherTheme.ColorToken.raisedControl.opacity( - configuration.isPressed ? 1 : isEnabled ? (isHovered ? 0.94 : 0.78) : 0.38 - ) - ) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius, style: .continuous) - .stroke(LauncherTheme.ColorToken.neutralBorder, lineWidth: 1) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius + 2) - .stroke(isFocused ? LauncherTheme.ColorToken.interactive : .clear, lineWidth: 2) - .padding(-3) - ) - .onHover { isHovered = $0 } - } -} - -struct LauncherTertiaryButtonStyle: ButtonStyle { - @Environment(\.isEnabled) private var isEnabled - @Environment(\.isFocused) private var isFocused - @State private var isHovered = false - var tint = LauncherTheme.ColorToken.interactive - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(isEnabled ? tint : LauncherTheme.ColorToken.textTertiary) - .padding(.horizontal, LauncherTheme.Spacing.small) - .frame(minHeight: 32) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill(Color.white.opacity(configuration.isPressed ? 0.09 : isHovered ? 0.05 : 0)) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius + 2) - .stroke(isFocused ? LauncherTheme.ColorToken.interactive : .clear, lineWidth: 2) - .padding(-3) - ) - .onHover { isHovered = $0 } - } -} - -struct LauncherDestructiveButtonStyle: ButtonStyle { - @Environment(\.isEnabled) private var isEnabled - @Environment(\.isFocused) private var isFocused - @State private var isHovered = false - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(isEnabled ? LauncherTheme.ColorToken.danger : LauncherTheme.ColorToken.textTertiary) - .padding(.horizontal, LauncherTheme.Spacing.regular) - .frame(minHeight: LauncherTheme.Metric.standardControlHeight) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill( - LauncherTheme.ColorToken.danger.opacity( - configuration.isPressed ? 0.16 : isHovered ? 0.12 : 0.08 - ) - ) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .stroke(LauncherTheme.ColorToken.danger.opacity(isEnabled ? 0.42 : 0.12), lineWidth: 1) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius + 2) - .stroke(isFocused ? LauncherTheme.ColorToken.interactive : .clear, lineWidth: 2) - .padding(-3) - ) - .onHover { isHovered = $0 } - } -} - -struct LauncherIconButtonStyle: ButtonStyle { - @Environment(\.isFocused) private var isFocused - @State private var isHovered = false - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .frame(width: 34, height: 34) - .background( - Circle().fill(Color.white.opacity(configuration.isPressed ? 0.12 : isHovered ? 0.08 : 0.04)) - ) - .overlay(Circle().stroke(LauncherTheme.ColorToken.neutralBorder, lineWidth: 1)) - .overlay( - Circle() - .stroke(isFocused ? LauncherTheme.ColorToken.interactive : .clear, lineWidth: 2) - .padding(-3) - ) - .onHover { isHovered = $0 } - } -} - -struct LauncherProgressBar: View { - let value: Double - - var body: some View { - ProgressView(value: value) - .progressViewStyle(.linear) - .tint(LauncherTheme.ColorToken.interactive) - .accessibilityValue(Text(value, format: .percent.precision(.fractionLength(0)))) - } -} diff --git a/launcher/Sources/LauncherModel.swift b/launcher/Sources/LauncherModel.swift deleted file mode 100644 index 63397f5..0000000 --- a/launcher/Sources/LauncherModel.swift +++ /dev/null @@ -1,694 +0,0 @@ -import AppKit -import Foundation - -@MainActor -final class LauncherModel: ObservableObject { - enum Mode: Equatable { - case needsInstall - case installing - case ready - case launching - case playing - case stopping - case failed - } - - @Published var mode: Mode = .needsInstall - @Published var status = "Preparing…" - @Published var detail = "" - @Published var progress = 0.0 - @Published var licenseAccepted = false - @Published var selectedProfileID: String - @Published var selectedEffectsQualityID: String - @Published var selectedLanguageID: String - @Published var selectedMemoryMB: Int - @Published var selectedCPUCores: Int - @Published var selectedUIScalePercent: Int - @Published var isPaused = false - @Published var installerPhase: InstallerProgress.Phase = .checking - @Published var installationWasCancelled = false - @Published var failure: LauncherFailure? - @Published var hotkeyStatus: LauncherHotkeyStatus = .permissionRequired - @Published var announcement: LauncherAnnouncement? - @Published private(set) var gameUpdateResultMessage: String? - @Published private(set) var isGameUpdateAvailable = false - @Published private(set) var isCheckingGameUpdate = false - @Published var shouldShowTelemetryNotice: Bool - @Published var extendedDiagnosticsEnabled: Bool - @Published private(set) var activeConfiguration: LaunchConfigurationSnapshot? - @Published private(set) var gameRelease: GameRelease - - let paths: LauncherPaths - let manifest: ReleaseManifest - private(set) var installState: InstallState - private let installer: InstallerService - private let runtime: RuntimeController - private let telemetry = LauncherTelemetryService() - private let inputBridge = InputBridgeService() - private let audioRecovery = EmulatorAudioRecoveryService() - private let fpsOverlay = FPSOverlayService() - private let loginAnimationRepair = RiotLoginAnimationRepairService() - private var emulatorPID: pid_t? - private var runtimeHadError = false - private var stopRequested = false - private var installCancellationRequested = false - private var launchProfile: LaunchProfile? - private var launchEffectsQuality: EffectsQuality? - private var hotkeyEventTapActive = false - private var hotkeyEventTapAttemptFailed = false - private var gameSessionTracker = GameSessionTracker() - private var pendingAnnouncements: [LauncherAnnouncement] = [] - - init() { - do { - let paths = try LauncherPaths() - let manifest = try SystemServices.loadManifest(from: paths.manifest) - self.paths = paths - self.manifest = manifest - installState = SystemServices.loadState(from: paths.stateFile) - gameRelease = (try? HostedGameUpdate.loadVerifiedFeed(from: paths.hostedGameFeed).release) - ?? manifest.game - installer = InstallerService(paths: paths, manifest: manifest) - runtime = RuntimeController(paths: paths) - let saved = UserDefaults.standard.string(forKey: "launchProfile") ?? "balanced" - selectedProfileID = manifest.profiles.contains(where: { $0.id == saved }) ? saved : "balanced" - selectedEffectsQualityID = EffectsQuality.selection( - saved: UserDefaults.standard.string(forKey: "effectsQuality") - ).id - selectedLanguageID = GameLanguage.language( - withID: UserDefaults.standard.string(forKey: "gameLanguage") - ).id - let memoryOptions = GuestResourceOptions.memoryMB( - physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory - ) - selectedMemoryMB = GuestResourceOptions.selection( - saved: UserDefaults.standard.integer(forKey: "androidMemoryMB"), - options: memoryOptions, - fallback: GuestResourceOptions.defaultMemoryMB - ) - let cpuOptions = GuestResourceOptions.cpuCores( - logicalCPUCount: ProcessInfo.processInfo.processorCount - ) - selectedCPUCores = GuestResourceOptions.selection( - saved: UserDefaults.standard.integer(forKey: "androidCPUCores"), - options: cpuOptions, - fallback: HostSizing.guestCPUCores( - logicalCPUCount: ProcessInfo.processInfo.processorCount - ) - ) - selectedUIScalePercent = InterfaceScaleOptions.selection( - saved: UserDefaults.standard.integer(forKey: "uiScalePercent") - ) - shouldShowTelemetryNotice = telemetry.shouldShowNotice - extendedDiagnosticsEnabled = telemetry.isExtendedDiagnosticsEnabled - if Self.installationLooksReady( - state: installState, - paths: paths, - gameRelease: gameRelease - ) { - mode = .ready - status = "Ready to play" - detail = "Choose graphics and language, then press Play." - } else { - mode = .needsInstall - status = "Initial installation required" - detail = "Android components will be downloaded directly from dl.google.com." - } - refreshHotkeyStatus() - inputBridge.observeStatus { [weak self] eventTapActive, eventTapAttemptFailed in - self?.updateHotkeyActivity( - eventTapActive: eventTapActive, - eventTapAttemptFailed: eventTapAttemptFailed - ) - } - requestAnnouncement(for: .launcherStarted) - } catch { - fatalError("Launcher resources are invalid: \(error)") - } - } - - var selectedProfile: LaunchProfile { - manifest.profiles.first(where: { $0.id == selectedProfileID }) ?? manifest.profiles[0] - } - - var selectedLanguage: GameLanguage { - GameLanguage.language(withID: selectedLanguageID) - } - - var selectedEffectsQuality: EffectsQuality { - EffectsQuality.selection(saved: selectedEffectsQualityID) - } - - var availableMemoryMB: [Int] { - GuestResourceOptions.memoryMB(physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory) - } - - var availableCPUCores: [Int] { - GuestResourceOptions.cpuCores(logicalCPUCount: ProcessInfo.processInfo.processorCount) - } - - var availableUIScalePercents: [Int] { - InterfaceScaleOptions.percentages - } - - var recommendedResources: GuestResourceConfiguration { - GuestResourceOptions.recommended( - physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory, - logicalCPUCount: ProcessInfo.processInfo.processorCount - ) - } - - var hostResourceSummary: String { - let memoryGB = Int(ProcessInfo.processInfo.physicalMemory / 1_073_741_824) - return LauncherL10n.format( - "settings.performance.host_format", - memoryGB, - ProcessInfo.processInfo.processorCount - ) - } - - var gameDisplayVersion: String { - LauncherMetadata.gameDisplayVersion(from: gameRelease.version) - } - - var downloadSize: String { - LauncherMetadata.byteCount(LauncherMetadata.totalDownloadBytes(in: manifest)) - } - - var requiredFreeSpace: String { - LauncherMetadata.byteCount(manifest.minimumFreeBytes) - } - - var androidSystemSummary: String { - let api = LauncherMetadata.androidAPILevel(in: manifest) ?? "?" - return LauncherL10n.format("install.android_api_format", api) - } - - var emulatorVersion: String { - LauncherMetadata.componentVersion("emulator", in: manifest) ?? "?" - } - - var settingsLocked: Bool { - mode == .launching || mode == .playing || mode == .stopping - } - - var maintenanceLocked: Bool { - mode == .installing || isCheckingGameUpdate || settingsLocked - } - - var selectedConfiguration: LaunchConfigurationSnapshot { - LaunchConfigurationSnapshot( - languageTitle: selectedLanguage.title, - resolution: selectedProfile.displayResolution, - effectsQualityTitle: selectedEffectsQuality.title, - uiScalePercent: selectedUIScalePercent, - memoryMB: selectedMemoryMB, - cpuCores: selectedCPUCores - ) - } - - func selectProfile(_ id: String) { - guard !settingsLocked else { return } - selectedProfileID = id - UserDefaults.standard.set(id, forKey: "launchProfile") - } - - func selectEffectsQuality(_ id: String) { - guard !settingsLocked, let quality = EffectsQuality(rawValue: id) else { return } - selectedEffectsQualityID = quality.id - UserDefaults.standard.set(quality.id, forKey: "effectsQuality") - } - - func selectLanguage(_ id: String) { - guard !settingsLocked else { return } - let language = GameLanguage.language(withID: id) - selectedLanguageID = language.id - UserDefaults.standard.set(language.id, forKey: "gameLanguage") - } - - func selectMemoryMB(_ memoryMB: Int) { - guard !settingsLocked, availableMemoryMB.contains(memoryMB) else { return } - selectedMemoryMB = memoryMB - UserDefaults.standard.set(memoryMB, forKey: "androidMemoryMB") - } - - func selectCPUCores(_ cpuCores: Int) { - guard !settingsLocked, availableCPUCores.contains(cpuCores) else { return } - selectedCPUCores = cpuCores - UserDefaults.standard.set(cpuCores, forKey: "androidCPUCores") - } - - func selectUIScalePercent(_ percent: Int) { - guard !settingsLocked, availableUIScalePercents.contains(percent) else { return } - selectedUIScalePercent = percent - UserDefaults.standard.set(percent, forKey: "uiScalePercent") - } - - func applyRecommendedResources() { - guard !settingsLocked else { return } - let recommendation = recommendedResources - selectMemoryMB(recommendation.memoryMB) - selectCPUCores(recommendation.cpuCores) - } - - func completeTelemetryNotice(extendedDiagnostics: Bool) { - telemetry.completeNotice(extendedDiagnostics: extendedDiagnostics) - extendedDiagnosticsEnabled = telemetry.isExtendedDiagnosticsEnabled - shouldShowTelemetryNotice = false - } - - func setExtendedDiagnosticsEnabled(_ enabled: Bool) { - telemetry.setExtendedDiagnosticsEnabled(enabled) - extendedDiagnosticsEnabled = telemetry.isExtendedDiagnosticsEnabled - } - - func install(repair: Bool = false) { - guard repair || licenseAccepted else { - fail( - "Accept the Android SDK License Agreement before installing.", - origin: .installation - ) - return - } - failure = nil - installationWasCancelled = false - installCancellationRequested = false - mode = .installing - isPaused = false - installerPhase = .checking - status = repair ? "Repairing installation…" : "Installing…" - detail = "You can pause the download." - progress = 0 - installer.install(repair: repair, progress: { [weak self] value in - guard let self else { return } - progress = value.fraction - status = value.message - installerPhase = value.phase - isPaused = value.phase == .paused - }, completion: { [weak self] result in - guard let self else { return } - switch result { - case let .success(state): - installCancellationRequested = false - installState = state - gameRelease = (try? HostedGameUpdate.loadVerifiedFeed(from: paths.hostedGameFeed).release) - ?? manifest.game - isGameUpdateAvailable = false - mode = .ready - progress = 1 - status = "Ready to play" - detail = "Choose graphics and language, then press Play." - case let .failure(error): - if InstallerCompletionPresentation.isUserCancellation( - requested: installCancellationRequested, - error: error - ) { - installCancellationRequested = false - installationWasCancelled = true - mode = .needsInstall - status = "Installation stopped" - detail = "The next installation will resume incomplete downloads." - return - } - fail( - error.localizedDescription, - origin: failureOrigin(for: error, fallback: .installation) - ) - } - }) - } - - func togglePause() { - if isPaused { - installer.resume() - isPaused = false - } else { - installer.pause() - isPaused = true - } - } - - func cancelInstall() { - installCancellationRequested = true - installationWasCancelled = true - installer.cancel() - mode = .needsInstall - status = "Installation stopped" - detail = "The next installation will resume incomplete downloads." - } - - func play() { - guard mode == .ready, - !shouldShowTelemetryNotice, - !isCheckingGameUpdate, - !isGameUpdateAvailable else { return } - failure = nil - runtimeHadError = false - stopRequested = false - emulatorPID = nil - let profile = selectedProfile - let effectsQuality = selectedEffectsQuality - let language = selectedLanguage - launchProfile = profile - launchEffectsQuality = effectsQuality - activeConfiguration = selectedConfiguration - mode = .launching - status = "Launching TFT PBE…" - detail = "Starting the game in \(selectedLanguage.title)." - do { - try runtime.start( - profile: profile, - effectsQuality: effectsQuality, - language: language, - cpuCores: selectedCPUCores, - memoryMB: selectedMemoryMB, - uiScalePercent: selectedUIScalePercent, - state: installState, - gameRelease: gameRelease, - gameResources: paths.gameResources(for: gameRelease) - ) { [weak self] event in - self?.handle(event) - } - } catch { - fail( - error.localizedDescription, - origin: failureOrigin(for: error, fallback: .launch) - ) - } - } - - func stopGame() { - guard mode == .launching || mode == .playing else { return } - stopRequested = true - mode = .stopping - status = "Stopping emulator…" - loginAnimationRepair.stop() - audioRecovery.stop() - fpsOverlay.stop() - inputBridge.stop() - runtime.stop() - } - - func repair() { - guard !maintenanceLocked, !runtime.isRunning else { return } - licenseAccepted = true - install(repair: true) - } - - func updateGame() { - guard mode == .ready, isGameUpdateAvailable, !runtime.isRunning else { return } - failure = nil - gameUpdateResultMessage = nil - installationWasCancelled = false - installCancellationRequested = false - mode = .installing - isPaused = false - installerPhase = .checking - status = "Checking for TFT PBE updates…" - detail = "Updates are downloaded securely from sergeinaumov.dev." - progress = 0 - installer.updateGame(currentState: installState, progress: { [weak self] value in - guard let self else { return } - progress = value.fraction - status = value.message - installerPhase = value.phase - isPaused = value.phase == .paused - }, completion: { [weak self] result in - guard let self else { return } - switch result { - case let .success(update): - installState = update.state - gameRelease = update.release - isGameUpdateAvailable = false - mode = .ready - progress = 1 - status = update.changed ? "TFT PBE updated" : "TFT PBE is up to date" - detail = update.changed - ? "The new game version is ready to play." - : "No game update is available." - let displayVersion = LauncherMetadata.gameDisplayVersion(from: update.release.version) - gameUpdateResultMessage = LauncherL10n.format( - update.changed ? "game_update.updated.message" : "game_update.current.message", - displayVersion - ) - let versionCode = update.release.versionCode.map(String.init) ?? "unknown" - SystemServices.appendLog( - "Game update check completed: \(update.changed ? "installed" : "already current") " - + "\(update.release.version) (versionCode \(versionCode)).", - to: paths.launcherLog - ) - case let .failure(error): - fail( - error.localizedDescription, - origin: failureOrigin(for: error, fallback: .installation) - ) - } - }) - } - - func refreshGameUpdateAvailability() { - guard mode == .ready, !runtime.isRunning, !isCheckingGameUpdate else { return } - isCheckingGameUpdate = true - isGameUpdateAvailable = false - installer.checkGameUpdateAvailability(currentState: installState) { [weak self] result in - guard let self else { return } - isCheckingGameUpdate = false - switch result { - case let .success(availability): - isGameUpdateAvailable = availability.isAvailable - if availability.isAvailable { - let versionCode = availability.release.versionCode.map(String.init) ?? "unknown" - SystemServices.appendLog( - "TFT PBE update available: \(availability.release.version) " - + "(versionCode \(versionCode)).", - to: paths.launcherLog - ) - } - case .failure: - isGameUpdateAvailable = false - } - } - } - - func dismissGameUpdateResult() { - gameUpdateResultMessage = nil - } - - func reset() { - guard !maintenanceLocked, !runtime.isRunning else { return } - loginAnimationRepair.stop() - audioRecovery.stop() - fpsOverlay.stop() - inputBridge.stop() - installer.cancel() - do { - if FileManager.default.fileExists(atPath: paths.root.path) { - try FileManager.default.removeItem(at: paths.root) - } - installState = InstallState() - isGameUpdateAvailable = false - isCheckingGameUpdate = false - mode = .needsInstall - failure = nil - installationWasCancelled = false - activeConfiguration = nil - progress = 0 - status = "Data deleted" - detail = "The next installation will create a clean AVD. Riot sign-in and game data were removed." - } catch { - fail("Could not delete data: \(error.localizedDescription)", origin: .reset) - } - } - - func openDataFolder() { - try? FileManager.default.createDirectory(at: paths.root, withIntermediateDirectories: true) - NSWorkspace.shared.open(paths.root) - } - - func openLog() { - if FileManager.default.fileExists(atPath: paths.launcherLog.path) { - NSWorkspace.shared.open(paths.launcherLog) - } else { - openDataFolder() - } - } - - func requestInputPermissions() { - inputBridge.requestPermissions() - refreshHotkeyStatus() - } - - func refreshHotkeyStatus() { - let facts = InputBridgeService.permissionFacts( - eventTapActive: hotkeyEventTapActive, - eventTapAttemptFailed: hotkeyEventTapAttemptFailed - ) - hotkeyStatus = LauncherHotkeyPresentation.status(for: facts) - } - - func recoverFromFailure() { - guard let failure else { return } - self.failure = nil - switch failure.recoveryAction { - case .retryInstallation, .repairInstallation: - licenseAccepted = true - install(repair: true) - case .tryLaunchAgain, .restartGame: - mode = .ready - play() - case .none: - break - } - } - - func shutdown() { - installer.cancel() - loginAnimationRepair.stop() - audioRecovery.stop() - fpsOverlay.stop() - inputBridge.stop() - finishGameSession(showAnnouncement: false) - runtime.stop() - } - - func dismissAnnouncement() { - if pendingAnnouncements.isEmpty { - announcement = nil - } else { - announcement = pendingAnnouncements.removeFirst() - } - } - - private func handle(_ event: RuntimeEvent) { - switch event.event { - case .booting: - mode = .launching - status = event.message ?? "Booting Android…" - case .emulatorStarted: - emulatorPID = event.pid - case .ready: - mode = .playing - gameSessionTracker.start() - status = "TFT PBE is open" - detail = "Space — shop • D — reroll • F — XP • Tab — items/traits • V — players/damage • Control + Fn + F — fill window." - loginAnimationRepair.start(adb: paths.adb, log: paths.launcherLog) - if let emulatorPID { - let profile = launchProfile ?? selectedProfile - audioRecovery.start( - targetPID: emulatorPID, - adb: paths.adb, - log: paths.launcherLog - ) - fpsOverlay.start(targetPID: emulatorPID, adb: paths.adb) - inputBridge.start( - targetPID: emulatorPID, - adb: paths.adb, - width: profile.width, - height: profile.height - ) - } - case .error: - guard !stopRequested else { return } - runtimeHadError = true - loginAnimationRepair.stop() - audioRecovery.stop() - fpsOverlay.stop() - inputBridge.stop() - fail(event.message ?? "Runtime error", origin: .runtime) - case .gameStopped: - finishGameSession(showAnnouncement: true) - stopGame() - case .stopped: - loginAnimationRepair.stop() - audioRecovery.stop() - fpsOverlay.stop() - inputBridge.stop() - finishGameSession(showAnnouncement: true) - emulatorPID = nil - if stopRequested || !runtimeHadError { - mode = .ready - activeConfiguration = nil - launchProfile = nil - launchEffectsQuality = nil - status = "Emulator closed" - detail = "Temporary overlays and settings were restored." - } - stopRequested = false - runtimeHadError = false - case .downloading, .installingGame: - break - } - } - - private func updateHotkeyActivity(eventTapActive: Bool, eventTapAttemptFailed: Bool) { - hotkeyEventTapActive = eventTapActive - hotkeyEventTapAttemptFailed = eventTapAttemptFailed - refreshHotkeyStatus() - } - - private func finishGameSession(showAnnouncement: Bool) { - let endedAt = Date() - guard let duration = gameSessionTracker.finish(at: endedAt) else { return } - let profile = launchProfile ?? selectedProfile - let effectsQuality = launchEffectsQuality ?? selectedEffectsQuality - telemetry.recordGameSession( - durationSeconds: duration, - launcherSettings: LauncherTelemetrySettings( - profile: profile, - effectsQuality: effectsQuality, - uiScalePercent: selectedUIScalePercent, - androidMemoryMB: selectedMemoryMB, - androidCPUCores: selectedCPUCores - ), - endedAt: endedAt - ) - if showAnnouncement { - requestAnnouncement(for: .gameClosed) - } - } - - private func requestAnnouncement(for trigger: LauncherMessageTrigger) { - telemetry.fetchAnnouncement(trigger: trigger) { [weak self] message in - guard let self, let message else { return } - if announcement == nil { - announcement = message - } else if pendingAnnouncements.count < 4 { - pendingAnnouncements.append(message) - } - } - } - - private func fail(_ message: String, origin: LauncherFailureOrigin) { - mode = .failed - status = "Unable to continue" - detail = message - failure = LauncherFailure(origin: origin, technicalDetails: message) - } - - private func failureOrigin(for error: Error, fallback: LauncherFailureOrigin) -> LauncherFailureOrigin { - guard let launcherError = error as? LauncherError else { return fallback } - switch launcherError { - case .invalidManifest, .integrity, .unsupportedGame: - return .validation - case .preflight, .process: - return fallback - case .cancelled: - return .installation - } - } - - private static func installationLooksReady( - state: InstallState, - paths: LauncherPaths, - gameRelease: GameRelease - ) -> Bool { - state.isReady - && state.gameVersion == gameRelease.version - && state.gameBaseSHA256 == gameRelease.baseSHA256 - && state.overlaySHA256 != nil - && FileManager.default.isExecutableFile(atPath: paths.adb.path) - && FileManager.default.isExecutableFile(atPath: paths.emulator.path) - && FileManager.default.fileExists(atPath: paths.avdINI.path) - && FileManager.default.fileExists(atPath: paths.overlayAPK.path) - } -} diff --git a/launcher/Sources/LauncherPaths.swift b/launcher/Sources/LauncherPaths.swift deleted file mode 100644 index b95a7ee..0000000 --- a/launcher/Sources/LauncherPaths.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation - -struct LauncherPaths { - let root: URL - let bundleResources: URL - - init(root: URL? = nil, bundle: Bundle = .main, resources: URL? = nil) throws { - let fileManager = FileManager.default - if let root { - self.root = root - } else if let override = ProcessInfo.processInfo.environment["MACTICIAN_DATA_ROOT"], - !override.isEmpty { - self.root = URL(fileURLWithPath: override, isDirectory: true) - } else { - let applicationSupport = try fileManager.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) - self.root = Self.defaultRoot(applicationSupport: applicationSupport) - } - guard let resources = resources ?? bundle.resourceURL else { - throw LauncherError.process("The application Resources directory is missing") - } - bundleResources = resources - } - - static func defaultRoot(applicationSupport: URL) -> URL { - applicationSupport.appendingPathComponent( - MacticianIdentity.applicationSupportDirectory, - isDirectory: true - ) - } - - var sdk: URL { root.appendingPathComponent("sdk", isDirectory: true) } - var emulator: URL { sdk.appendingPathComponent("emulator/emulator") } - var qemuSystem: URL { sdk.appendingPathComponent("emulator/qemu/darwin-aarch64/qemu-system-aarch64") } - var qemuImg: URL { sdk.appendingPathComponent("emulator/qemu-img") } - var adb: URL { sdk.appendingPathComponent("platform-tools/adb") } - var systemImage: URL { sdk.appendingPathComponent("system-images/android-36/google_apis/arm64-v8a", isDirectory: true) } - var avdHome: URL { root.appendingPathComponent("avd", isDirectory: true) } - var avdDirectory: URL { avdHome.appendingPathComponent("TftPBE.avd", isDirectory: true) } - var avdINI: URL { avdHome.appendingPathComponent("TftPBE.ini") } - var avdBootCompleted: URL { avdDirectory.appendingPathComponent("bootcompleted.ini") } - var runtimeProject: URL { root.appendingPathComponent("runtime-project", isDirectory: true) } - var downloads: URL { root.appendingPathComponent("downloads", isDirectory: true) } - var gameCache: URL { root.appendingPathComponent("game", isDirectory: true) } - var hostedGameFeed: URL { gameCache.appendingPathComponent("manifest.json") } - func gameReleaseDirectory(baseSHA256: String) -> URL { - gameCache.appendingPathComponent("releases/\(baseSHA256)", isDirectory: true) - } - func gameResources(for release: GameRelease) -> URL { - let hosted = gameReleaseDirectory(baseSHA256: release.baseSHA256) - if FileManager.default.fileExists(atPath: hosted.appendingPathComponent("base.apk").path) { - return hosted - } - return gameResources - } - var staging: URL { root.appendingPathComponent(".staging", isDirectory: true) } - var stateFile: URL { root.appendingPathComponent("install-state.json") } - var logDirectory: URL { root.appendingPathComponent("logs", isDirectory: true) } - var launcherLog: URL { logDirectory.appendingPathComponent("launcher.log") } - var gameResources: URL { bundleResources.appendingPathComponent("Game", isDirectory: true) } - var runtimeTemplate: URL { bundleResources.appendingPathComponent("RuntimeTemplate", isDirectory: true) } - var manifest: URL { bundleResources.appendingPathComponent("release-manifest.json") } - var shaderProfile: URL { - runtimeProject.appendingPathComponent("artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.shader-prewarm.ini") - } - var performanceMaxProfile: URL { - runtimeProject.appendingPathComponent("artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.performance-max.ini") - } - func effectsProfile(for quality: EffectsQuality) -> URL { - runtimeProject.appendingPathComponent( - "artifacts/tft-pbe-18.1-5212127-angle-opengl/\(quality.profileFilename)" - ) - } - var overlayAPK: URL { - runtimeProject.appendingPathComponent("artifacts/tft-pbe-18.1-5212127-angle-opengl/base-angle-opengl.apk") - } - var runtimeHelper: URL { bundleResources.appendingPathComponent("launcher-runtime.command") } - var qemuHypervisorEntitlements: URL { - bundleResources.appendingPathComponent("QEMU-Hypervisor.entitlements") - } -} diff --git a/launcher/Sources/LauncherPresentation.swift b/launcher/Sources/LauncherPresentation.swift deleted file mode 100644 index 48ff481..0000000 --- a/launcher/Sources/LauncherPresentation.swift +++ /dev/null @@ -1,144 +0,0 @@ -import Foundation - -enum LauncherFailureOrigin: Equatable { - case installation - case launch - case runtime - case reset - case validation -} - -enum LauncherRecoveryAction: Equatable { - case retryInstallation - case tryLaunchAgain - case restartGame - case repairInstallation - case none -} - -struct LauncherFailure: Equatable { - let origin: LauncherFailureOrigin - let technicalDetails: String - - var recoveryAction: LauncherRecoveryAction { - LauncherFailurePresentation.recoveryAction(for: origin) - } -} - -enum LauncherFailurePresentation { - static func recoveryAction(for origin: LauncherFailureOrigin) -> LauncherRecoveryAction { - switch origin { - case .installation: - return .retryInstallation - case .launch: - return .tryLaunchAgain - case .runtime: - return .restartGame - case .validation: - return .repairInstallation - case .reset: - return .none - } - } -} - -enum InstallerCompletionPresentation { - static func isUserCancellation(requested: Bool, error: Error) -> Bool { - if requested { return true } - return error as? LauncherError == .cancelled - } -} - -enum LauncherHotkeyStatus: Equatable { - case permissionRequired - case ready - case active - case unavailable -} - -struct LauncherHotkeyFacts: Equatable { - let accessibilityTrusted: Bool - let eventTapActive: Bool - let eventTapAttemptFailed: Bool -} - -enum LauncherHotkeyPresentation { - static func status(for facts: LauncherHotkeyFacts) -> LauncherHotkeyStatus { - if facts.eventTapActive { return .active } - guard facts.accessibilityTrusted else { return .permissionRequired } - if facts.eventTapAttemptFailed { return .unavailable } - return .ready - } -} - -struct LaunchConfigurationSnapshot: Equatable { - let languageTitle: String - let resolution: String - let effectsQualityTitle: String - let uiScalePercent: Int - let memoryMB: Int - let cpuCores: Int - - var compactSummary: String { - "\(resolution) \u{00b7} \(effectsQualityTitle) \u{00b7} UI \(uiScalePercent)%" - } - - var fullSummary: String { - "\(compactSummary) \u{00b7} \(memoryMB / 1024) GB \u{00b7} \(cpuCores) vCPU" - } -} - -enum LauncherMetadata { - static func gameDisplayVersion(from version: String) -> String { - String(version.split(separator: "-", maxSplits: 1).first ?? Substring(version)) - } - - static func totalDownloadBytes(in manifest: ReleaseManifest) -> Int64 { - manifest.components.reduce(Int64(0)) { $0 + $1.size } - } - - static func resolution(width: Int, height: Int) -> String { - "\(width) \u{00d7} \(height)" - } - - static func androidAPILevel(in manifest: ReleaseManifest) -> String? { - guard let component = manifest.components.first(where: { $0.id == "system-image" }) else { - return nil - } - for part in component.installPath.split(separator: "/") where part.hasPrefix("android-") { - return String(part.dropFirst("android-".count)) - } - return nil - } - - static func componentVersion(_ id: String, in manifest: ReleaseManifest) -> String? { - manifest.components.first(where: { $0.id == id })?.version - } - - static func byteCount(_ value: Int64) -> String { - let formatter = ByteCountFormatter() - formatter.allowedUnits = [.useGB, .useMB] - formatter.countStyle = .file - formatter.includesUnit = true - formatter.isAdaptive = true - return formatter.string(fromByteCount: value) - } -} - -enum LauncherL10n { - static func text(_ key: String) -> String { - NSLocalizedString(key, tableName: nil, bundle: .main, value: key, comment: "") - } - - static func format(_ key: String, _ arguments: CVarArg...) -> String { - String(format: text(key), locale: Locale.current, arguments: arguments) - } -} - -enum LauncherBuildInfo { - static var display: String { - let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev" - let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "local" - return "v\(version) \u{00b7} build \(build)" - } -} diff --git a/launcher/Sources/LauncherSettingsView.swift b/launcher/Sources/LauncherSettingsView.swift deleted file mode 100644 index 673ff03..0000000 --- a/launcher/Sources/LauncherSettingsView.swift +++ /dev/null @@ -1,429 +0,0 @@ -import SwiftUI - -struct LauncherSettingsView: View { - @ObservedObject var model: LauncherModel - @ObservedObject var updateController: LauncherUpdateController - @Binding var showResetConfirmation: Bool - @Environment(\.presentationMode) private var presentationMode - @State private var showsResourceHelp = false - - var body: some View { - VStack(spacing: 0) { - header - LauncherDivider() - ScrollView { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - gameSection - LauncherDivider() - performanceSection - LauncherDivider() - telemetrySection - LauncherDivider() - hotkeySection - LauncherDivider() - updateSection - LauncherDivider() - maintenanceSection - } - .padding(LauncherTheme.Spacing.large) - } - } - .frame(width: LauncherTheme.Metric.sheetWidth, height: 620) - .background(LauncherTheme.ColorToken.surface) - .preferredColorScheme(.dark) - .onAppear { model.refreshHotkeyStatus() } - .alert( - LauncherL10n.text("reset.confirmation.title"), - isPresented: $showResetConfirmation - ) { - Button(LauncherL10n.text("action.cancel"), role: .cancel) { } - Button(LauncherL10n.text("reset.confirmation.action"), role: .destructive) { - model.reset() - } - } message: { - Text(LauncherL10n.text("reset.confirmation.message")) - } - } - - private var header: some View { - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: "gearshape.fill") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(LauncherL10n.text("settings.title")) - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(LauncherL10n.text("settings.description")) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - } - Spacer() - Button { - presentationMode.wrappedValue.dismiss() - } label: { - Image(systemName: "xmark") - } - .buttonStyle(LauncherIconButtonStyle()) - .accessibilityLabel(LauncherL10n.text("action.close")) - .help(LauncherL10n.text("action.close")) - } - .padding(.horizontal, LauncherTheme.Spacing.large) - .frame(height: 72) - } - - private var gameSection: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - LauncherSectionHeader( - LauncherL10n.text("settings.game.title"), - description: model.settingsLocked - ? LauncherL10n.text("settings.runtime_locked") - : nil - ) - settingRow(LauncherL10n.text("field.game_language")) { - LauncherMenuControl(value: model.selectedLanguage.title) { - ForEach(GameLanguage.supported) { language in - Button { - model.selectLanguage(language.id) - } label: { - if language.id == model.selectedLanguageID { - Label(language.title, systemImage: "checkmark") - } else { - Text(language.title) - } - } - } - } - .frame(width: 230) - } - settingRow(LauncherL10n.text("field.effects_quality")) { - LauncherMenuControl(value: model.selectedEffectsQuality.title) { - ForEach(EffectsQuality.allCases) { quality in - Button { - model.selectEffectsQuality(quality.id) - } label: { - if quality.id == model.selectedEffectsQualityID { - Label(quality.title, systemImage: "checkmark") - } else { - Text(quality.title) - } - } - } - } - .frame(width: 230) - } - settingRow(LauncherL10n.text("field.interface_scale")) { - LauncherMenuControl(value: "\(model.selectedUIScalePercent)%") { - ForEach(model.availableUIScalePercents, id: \.self) { percent in - Button { - model.selectUIScalePercent(percent) - } label: { - if percent == model.selectedUIScalePercent { - Label("\(percent)%", systemImage: "checkmark") - } else { - Text("\(percent)%") - } - } - } - } - .frame(width: 230) - } - } - .disabled(model.settingsLocked) - } - - private var performanceSection: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - LauncherSectionHeader( - LauncherL10n.text("settings.performance.title"), - description: model.hostResourceSummary - ) - - HStack(spacing: LauncherTheme.Spacing.regular) { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(LauncherL10n.text("settings.performance.recommended")) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.success) - Text( - LauncherL10n.format( - "settings.performance.recommended_format", - model.recommendedResources.memoryMB / 1024, - model.recommendedResources.cpuCores - ) - ) - .font(.system(size: 16, weight: .semibold, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - } - Spacer() - Button(LauncherL10n.text("settings.performance.apply")) { - model.applyRecommendedResources() - } - .buttonStyle(LauncherSecondaryButtonStyle()) - .disabled(model.settingsLocked || usesRecommendedResources) - } - .padding(LauncherTheme.Spacing.regular) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill(LauncherTheme.ColorToken.success.opacity(0.07)) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .stroke(LauncherTheme.ColorToken.success.opacity(0.26), lineWidth: 1) - ) - - settingRow(LauncherL10n.text("field.android_ram")) { - LauncherMenuControl(value: "\(model.selectedMemoryMB / 1024) GB") { - ForEach(model.availableMemoryMB, id: \.self) { memory in - Button { - model.selectMemoryMB(memory) - } label: { - if memory == model.selectedMemoryMB { - Label("\(memory / 1024) GB", systemImage: "checkmark") - } else { - Text("\(memory / 1024) GB") - } - } - } - } - .frame(width: 230) - } - .disabled(model.settingsLocked) - - settingRow(LauncherL10n.text("field.vcpu")) { - LauncherMenuControl( - value: LauncherL10n.format("field.vcpu_value_format", model.selectedCPUCores) - ) { - ForEach(model.availableCPUCores, id: \.self) { cores in - let title = LauncherL10n.format("field.vcpu_value_format", cores) - Button { - model.selectCPUCores(cores) - } label: { - if cores == model.selectedCPUCores { - Label(title, systemImage: "checkmark") - } else { - Text(title) - } - } - } - } - .frame(width: 230) - } - .disabled(model.settingsLocked) - - DisclosureGroup(isExpanded: $showsResourceHelp) { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - resourceHelp("settings.performance.help.balanced.title", "settings.performance.help.balanced.body") - resourceHelp("settings.performance.help.ram.title", "settings.performance.help.ram.body") - resourceHelp("settings.performance.help.cpu.title", "settings.performance.help.cpu.body") - } - .padding(.top, LauncherTheme.Spacing.medium) - } label: { - Text(LauncherL10n.text("settings.performance.help.title")) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - } - } - } - - private var hotkeySection: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - LauncherSectionHeader( - LauncherL10n.text("hotkeys.title"), - description: LauncherL10n.text("hotkeys.description") - ) - - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: hotkeyStatusSymbol) - .foregroundColor(hotkeyStatusColor) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(LauncherL10n.text("hotkeys.status.label")) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - Text(LauncherL10n.text(model.hotkeyStatus.localizationKey)) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - } - Spacer() - if needsPermissionAction { - Button(LauncherL10n.text("hotkeys.grant_access")) { - model.requestInputPermissions() - } - .buttonStyle(LauncherSecondaryButtonStyle()) - } - } - .padding(LauncherTheme.Spacing.regular) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill( - needsHotkeyAction - ? LauncherTheme.ColorToken.warning.opacity(0.09) - : LauncherTheme.ColorToken.raisedControl.opacity(0.62) - ) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .stroke( - needsHotkeyAction - ? LauncherTheme.ColorToken.warning.opacity(0.36) - : Color.clear, - lineWidth: 1 - ) - ) - - Text(LauncherL10n.text("hotkeys.shortcuts")) - .font(.system(size: 12, weight: .medium, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - } - .accessibilityElement(children: .contain) - } - - private var telemetrySection: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - LauncherSectionHeader( - LauncherL10n.text("telemetry.settings.title"), - description: LauncherL10n.text("telemetry.settings.description") - ) - - HStack(spacing: LauncherTheme.Spacing.regular) { - Toggle( - LauncherL10n.text("telemetry.extended.toggle"), - isOn: Binding( - get: { model.extendedDiagnosticsEnabled }, - set: { model.setExtendedDiagnosticsEnabled($0) } - ) - ) - .toggleStyle(.switch) - Spacer() - if model.extendedDiagnosticsEnabled { - Link( - LauncherL10n.text("telemetry.extended.data_link"), - destination: MacticianIdentity.extendedDiagnosticsURL - ) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - } - } - - Text(LauncherL10n.text("telemetry.settings.basic_note")) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .fixedSize(horizontal: false, vertical: true) - } - } - - private var maintenanceSection: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - LauncherSectionHeader( - LauncherL10n.text("settings.maintenance.title"), - description: LauncherL10n.text("settings.maintenance.description") - ) - - HStack(spacing: LauncherTheme.Spacing.medium) { - Button(LauncherL10n.text("action.repair_installation")) { model.repair() } - .buttonStyle(LauncherSecondaryButtonStyle()) - .disabled(model.maintenanceLocked) - Button(LauncherL10n.text("action.data_folder")) { model.openDataFolder() } - .buttonStyle(LauncherSecondaryButtonStyle()) - Button(LauncherL10n.text("action.view_log")) { model.openLog() } - .buttonStyle(LauncherSecondaryButtonStyle()) - } - - LauncherDivider() - - HStack(alignment: .center, spacing: LauncherTheme.Spacing.regular) { - Text(LauncherL10n.text("settings.maintenance.reset_description")) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .fixedSize(horizontal: false, vertical: true) - Spacer() - Button(LauncherL10n.text("action.reset_all_data")) { - showResetConfirmation = true - } - .buttonStyle(LauncherDestructiveButtonStyle()) - .disabled(model.maintenanceLocked) - } - } - } - - private var updateSection: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.regular) { - LauncherSectionHeader( - LauncherL10n.text("updates.title"), - description: LauncherL10n.text("updates.description") - ) - - HStack(spacing: LauncherTheme.Spacing.regular) { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(LauncherBuildInfo.display) - .font(.system(size: 13, weight: .semibold, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(LauncherL10n.text("updates.automatic_checks")) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - } - Spacer() - Button(LauncherL10n.text("updates.check")) { - updateController.checkForUpdates() - } - .buttonStyle(LauncherSecondaryButtonStyle()) - .disabled(!updateController.canCheckForUpdates) - } - } - } - - private func settingRow( - _ title: String, - @ViewBuilder content: () -> Content - ) -> some View { - HStack(spacing: LauncherTheme.Spacing.regular) { - Text(title) - .font(.system(size: 13, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Spacer() - content() - } - .frame(minHeight: LauncherTheme.Metric.standardControlHeight) - } - - private func resourceHelp(_ titleKey: String, _ bodyKey: String) -> some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(LauncherL10n.text(titleKey)) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(LauncherL10n.text(bodyKey)) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .fixedSize(horizontal: false, vertical: true) - } - } - - private var usesRecommendedResources: Bool { - model.selectedMemoryMB == model.recommendedResources.memoryMB - && model.selectedCPUCores == model.recommendedResources.cpuCores - } - - private var needsHotkeyAction: Bool { - model.hotkeyStatus == .permissionRequired - || model.hotkeyStatus == .unavailable - } - - private var needsPermissionAction: Bool { - model.hotkeyStatus == .permissionRequired - } - - private var hotkeyStatusColor: Color { - switch model.hotkeyStatus { - case .ready, .active: return LauncherTheme.ColorToken.success - case .permissionRequired, .unavailable: return LauncherTheme.ColorToken.warning - } - } - - private var hotkeyStatusSymbol: String { - switch model.hotkeyStatus { - case .ready, .active: return "checkmark.circle.fill" - case .permissionRequired, .unavailable: return "exclamationmark.circle.fill" - } - } -} diff --git a/launcher/Sources/LauncherStateViews.swift b/launcher/Sources/LauncherStateViews.swift deleted file mode 100644 index 66787c9..0000000 --- a/launcher/Sources/LauncherStateViews.swift +++ /dev/null @@ -1,687 +0,0 @@ -import AppKit -import SwiftUI - -struct LauncherStateDeck: View { - @ObservedObject var model: LauncherModel - @Binding var showSettings: Bool - - var body: some View { - Group { - switch model.mode { - case .needsInstall: - LauncherInstallRequiredView(model: model) - case .installing: - LauncherInstallingView(model: model) - case .ready: - LauncherReadyView(model: model, showSettings: $showSettings) - case .launching: - LauncherLaunchingView(model: model) - case .playing: - LauncherPlayingView(model: model) - case .stopping: - LauncherStoppingView(model: model) - case .failed: - LauncherFailureView(model: model) - } - } - .padding(LauncherTheme.Metric.stateDeckPadding) - .frame(maxWidth: .infinity, alignment: .leading) - .launcherSurface() - .shadow(color: Color.black.opacity(0.34), radius: 18, y: 8) - } -} - -private struct LauncherInstallRequiredView: View { - @ObservedObject var model: LauncherModel - @State private var showsInstallDetails = false - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - LauncherStatusHeader( - symbol: model.installationWasCancelled ? "pause.circle.fill" : "arrow.down.circle.fill", - color: model.installationWasCancelled - ? LauncherTheme.ColorToken.warning - : LauncherTheme.ColorToken.interactive, - title: LauncherL10n.text( - model.installationWasCancelled ? "install.cancelled.title" : "install.required.title" - ), - description: LauncherL10n.text( - model.installationWasCancelled ? "install.cancelled.description" : "install.required.description" - ) - ) - - HStack(spacing: LauncherTheme.Spacing.medium) { - requirement( - symbol: "arrow.down.circle", - value: model.downloadSize, - label: LauncherL10n.text("install.requirement.download") - ) - requirement( - symbol: "externaldrive", - value: model.requiredFreeSpace, - label: LauncherL10n.text("install.requirement.free_space") - ) - requirement( - symbol: "cpu", - value: LauncherL10n.text("install.requirement.apple_silicon_value"), - label: LauncherL10n.text("install.requirement.platform") - ) - } - - DisclosureGroup(isExpanded: $showsInstallDetails) { - HStack(spacing: LauncherTheme.Spacing.large) { - Label(model.androidSystemSummary, systemImage: "cpu") - Label( - LauncherL10n.format("install.emulator_format", model.emulatorVersion), - systemImage: "display" - ) - Label(LauncherL10n.text("install.clean_profile"), systemImage: "lock.shield") - } - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .padding(.top, LauncherTheme.Spacing.small) - } label: { - Text(LauncherL10n.text("install.what_is_installed")) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - } - - HStack(alignment: .center, spacing: LauncherTheme.Spacing.regular) { - Toggle( - LauncherL10n.text("install.license.accept"), - isOn: $model.licenseAccepted - ) - .toggleStyle(.checkbox) - .font(.system(size: 13)) - - Link( - LauncherL10n.text("install.license.terms"), - destination: URL(string: "https://developer.android.com/studio/terms")! - ) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - - Spacer() - - Button(LauncherL10n.text("action.install")) { model.install() } - .buttonStyle(LauncherPrimaryButtonStyle()) - .keyboardShortcut(.defaultAction) - .disabled(!model.licenseAccepted) - .accessibilityHint(LauncherL10n.text("install.license.hint")) - } - } - } - - private func requirement(symbol: String, value: String, label: String) -> some View { - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: symbol) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - Text(value) - .font(.system(size: 13, weight: .semibold, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(label) - .font(.system(size: 11)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - } - } - .padding(.horizontal, LauncherTheme.Spacing.regular) - .frame(maxWidth: .infinity, minHeight: 60, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill(LauncherTheme.ColorToken.raisedControl.opacity(0.72)) - ) - .accessibilityElement(children: .combine) - } -} - -private struct LauncherInstallingView: View { - @ObservedObject var model: LauncherModel - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - HStack(alignment: .top, spacing: LauncherTheme.Spacing.regular) { - LauncherStatusHeader( - symbol: "arrow.down.circle.fill", - color: LauncherTheme.ColorToken.interactive, - title: LauncherL10n.text("installing.title"), - description: phaseTitle - ) - Spacer() - Text(model.progress, format: .percent.precision(.fractionLength(0))) - .font(.system(size: 18, weight: .bold, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .accessibilityHidden(true) - } - - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.small) { - LauncherProgressBar(value: model.progress) - Text(model.status) - .font(.system(size: 12)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - } - - HStack(spacing: LauncherTheme.Spacing.medium) { - Button( - LauncherL10n.text(model.isPaused ? "action.resume" : "action.pause") - ) { model.togglePause() } - .buttonStyle(LauncherSecondaryButtonStyle()) - - Button(LauncherL10n.text("action.cancel")) { model.cancelInstall() } - .buttonStyle(LauncherTertiaryButtonStyle(tint: LauncherTheme.ColorToken.danger)) - - Spacer() - - Button(LauncherL10n.text("action.view_log")) { model.openLog() } - .buttonStyle(LauncherTertiaryButtonStyle()) - } - } - } - - private var phaseTitle: String { - let key: String - switch model.installerPhase { - case .checking: key = "installing.phase.checking" - case .downloading: key = "installing.phase.downloading" - case .extracting: key = "installing.phase.extracting" - case .creatingAVD: key = "installing.phase.creating_device" - case .installingGame: key = "installing.phase.installing_game" - case .finished: key = "installing.phase.finished" - case .paused: key = "installing.phase.paused" - } - return LauncherL10n.text(key) - } -} - -private struct LauncherReadyView: View { - @ObservedObject var model: LauncherModel - @Binding var showSettings: Bool - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - HStack(alignment: .top, spacing: LauncherTheme.Spacing.large) { - LauncherStatusHeader( - symbol: "checkmark", - color: LauncherTheme.ColorToken.success, - title: LauncherL10n.text("ready.title"), - description: LauncherL10n.text("ready.description") - ) - Spacer() - if model.isGameUpdateAvailable { - Button(LauncherL10n.text("action.update_game")) { model.updateGame() } - .buttonStyle(LauncherPrimaryButtonStyle()) - .keyboardShortcut(.defaultAction) - } else { - Button(LauncherL10n.text("action.play")) { model.play() } - .buttonStyle(LauncherPrimaryButtonStyle()) - .keyboardShortcut(.defaultAction) - .disabled(model.isCheckingGameUpdate) - } - } - - HStack(spacing: LauncherTheme.Spacing.medium) { - languageField - profileField - advancedField - } - - hotkeyRow - } - .onAppear { - model.refreshHotkeyStatus() - model.refreshGameUpdateAvailability() - } - .alert( - LauncherL10n.text("game_update.result.title"), - isPresented: Binding( - get: { model.gameUpdateResultMessage != nil }, - set: { isPresented in - if !isPresented { - model.dismissGameUpdateResult() - } - } - ) - ) { - Button(LauncherL10n.text("action.ok"), role: .cancel) { - model.dismissGameUpdateResult() - } - } message: { - Text(model.gameUpdateResultMessage ?? "") - } - } - - private var languageField: some View { - LauncherSummaryField( - label: LauncherL10n.text("field.game_language"), - value: model.selectedLanguage.title - ) { - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: "globe") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .frame(width: 20) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - LauncherFieldLabel(text: LauncherL10n.text("field.game_language")) - LauncherMenuControl( - value: model.selectedLanguage.title, - showsBackground: false - ) { - ForEach(GameLanguage.supported) { language in - Button { - model.selectLanguage(language.id) - } label: { - if language.id == model.selectedLanguageID { - Label(language.title, systemImage: "checkmark") - } else { - Text(language.title) - } - } - } - } - } - } - .padding(.horizontal, LauncherTheme.Spacing.regular) - .padding(.vertical, LauncherTheme.Spacing.small) - .contentShape(Rectangle()) - } - } - - private var profileField: some View { - LauncherSummaryField( - label: LauncherL10n.text("field.resolution"), - value: model.selectedProfile.displayResolution - ) { - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: "display") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .frame(width: 20) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - LauncherFieldLabel(text: LauncherL10n.text("field.resolution")) - LauncherMenuControl( - value: model.selectedProfile.displayResolution, - showsBackground: false - ) { - ForEach(model.manifest.profiles) { profile in - let title = profile.displayResolution - Button { - model.selectProfile(profile.id) - } label: { - if profile.id == model.selectedProfileID { - Label(title, systemImage: "checkmark") - } else { - Text(title) - } - } - } - } - } - } - .padding(.horizontal, LauncherTheme.Spacing.regular) - .padding(.vertical, LauncherTheme.Spacing.small) - .contentShape(Rectangle()) - } - } - - private var advancedField: some View { - LauncherSummaryField( - label: LauncherL10n.text("field.advanced_settings"), - value: model.selectedConfiguration.fullSummary - ) { - Button { showSettings = true } label: { - summaryLabel( - label: LauncherL10n.text("field.advanced_settings"), - value: LauncherL10n.format( - "ready.advanced_summary_format", - model.selectedUIScalePercent, - model.selectedMemoryMB / 1024, - model.selectedCPUCores - ), - detail: LauncherL10n.text("ready.advanced_open"), - symbol: "slider.horizontal.3" - ) - } - .buttonStyle(.plain) - } - } - - private func summaryLabel( - label: String, - value: String, - detail: String?, - symbol: String - ) -> some View { - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: symbol) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .frame(width: 20) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.xSmall) { - LauncherFieldLabel(text: label) - Text(value) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - .lineLimit(1) - if let detail { - Text(detail) - .font(.system(size: 11)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - .lineLimit(1) - } - } - Spacer(minLength: LauncherTheme.Spacing.small) - Image(systemName: "chevron.down") - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .accessibilityHidden(true) - } - .padding(.horizontal, LauncherTheme.Spacing.regular) - .padding(.vertical, LauncherTheme.Spacing.medium) - .contentShape(Rectangle()) - } - - private var hotkeyRow: some View { - HStack(spacing: LauncherTheme.Spacing.medium) { - Image(systemName: hotkeySymbol) - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(hotkeyColor) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: 2) { - Text(LauncherL10n.text("hotkeys.title")) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(hotkeyStatusText) - .font(.system(size: 11)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - Text(LauncherL10n.text("hotkeys.shortcuts")) - .font(.system(size: 10, weight: .medium, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - .lineLimit(2) - .fixedSize(horizontal: false, vertical: true) - } - Spacer() - if needsPermissionAction { - Button(LauncherL10n.text("hotkeys.grant_access")) { model.requestInputPermissions() } - .buttonStyle(LauncherTertiaryButtonStyle()) - } - } - .padding(.horizontal, LauncherTheme.Spacing.regular) - .frame(minHeight: 68) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill( - needsHotkeyAction - ? LauncherTheme.ColorToken.warning.opacity(0.09) - : Color.black.opacity(0.18) - ) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .stroke( - needsHotkeyAction - ? LauncherTheme.ColorToken.warning.opacity(0.36) - : Color.clear, - lineWidth: 1 - ) - ) - .accessibilityElement(children: .combine) - } - - private var needsHotkeyAction: Bool { - model.hotkeyStatus == .permissionRequired - || model.hotkeyStatus == .unavailable - } - - private var needsPermissionAction: Bool { - model.hotkeyStatus == .permissionRequired - } - - private var hotkeyStatusText: String { - LauncherL10n.text(model.hotkeyStatus.localizationKey) - } - - private var hotkeyColor: Color { - switch model.hotkeyStatus { - case .ready, .active: return LauncherTheme.ColorToken.success - case .permissionRequired, .unavailable: return LauncherTheme.ColorToken.warning - } - } - - private var hotkeySymbol: String { - switch model.hotkeyStatus { - case .ready, .active: return "checkmark.circle.fill" - case .permissionRequired, .unavailable: return "exclamationmark.circle.fill" - } - } -} - -private struct LauncherLaunchingView: View { - @ObservedObject var model: LauncherModel - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - LauncherStatusHeader( - symbol: "circle.dotted", - color: LauncherTheme.ColorToken.interactive, - title: LauncherL10n.text("launching.title"), - description: model.status, - spinning: true - ) - configurationSummary(model.activeConfiguration ?? model.selectedConfiguration) - HStack { - Spacer() - Button(LauncherL10n.text("action.view_log")) { model.openLog() } - .buttonStyle(LauncherTertiaryButtonStyle()) - Button(LauncherL10n.text("action.stop")) { model.stopGame() } - .buttonStyle(LauncherSecondaryButtonStyle()) - } - } - } -} - -private struct LauncherPlayingView: View { - @ObservedObject var model: LauncherModel - - var body: some View { - let configuration = model.activeConfiguration ?? model.selectedConfiguration - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - HStack(alignment: .top, spacing: LauncherTheme.Spacing.large) { - LauncherStatusHeader( - symbol: "checkmark", - color: LauncherTheme.ColorToken.success, - title: LauncherL10n.text("playing.title"), - description: LauncherL10n.format( - "playing.description_format", - configuration.languageTitle - ) - ) - Spacer() - Button(LauncherL10n.text("action.stop_game")) { model.stopGame() } - .buttonStyle(LauncherSecondaryButtonStyle()) - } - configurationSummary(configuration, full: true) - HStack(spacing: LauncherTheme.Spacing.medium) { - Label( - LauncherL10n.text(model.hotkeyStatus.localizationKey), - systemImage: hotkeysNeedAttention ? "exclamationmark.triangle.fill" : "keyboard" - ) - .foregroundColor( - hotkeysNeedAttention - ? LauncherTheme.ColorToken.warning - : LauncherTheme.ColorToken.textSecondary - ) - Spacer() - if model.hotkeyStatus == .permissionRequired { - Button(LauncherL10n.text("hotkeys.grant_access")) { - model.requestInputPermissions() - } - .buttonStyle(LauncherTertiaryButtonStyle()) - } - Label( - LauncherL10n.text("playing.fill_window_tip"), - systemImage: "arrow.up.left.and.arrow.down.right" - ) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - } - .font(.system(size: 12)) - .padding(.horizontal, LauncherTheme.Spacing.regular) - .frame(minHeight: 44) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill(Color.black.opacity(0.18)) - ) - } - } - - private var hotkeysNeedAttention: Bool { - model.hotkeyStatus == .permissionRequired || model.hotkeyStatus == .unavailable - } -} - -private struct LauncherStoppingView: View { - @ObservedObject var model: LauncherModel - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - LauncherStatusHeader( - symbol: "circle.dotted", - color: LauncherTheme.ColorToken.warning, - title: LauncherL10n.text("stopping.title"), - description: LauncherL10n.text("stopping.description"), - spinning: true - ) - HStack { - Spacer() - Button(LauncherL10n.text("action.view_log")) { model.openLog() } - .buttonStyle(LauncherTertiaryButtonStyle()) - } - } - } -} - -private struct LauncherFailureView: View { - @ObservedObject var model: LauncherModel - @State private var showsTechnicalDetails = false - - var body: some View { - let failure = model.failure ?? LauncherFailure(origin: .runtime, technicalDetails: model.detail) - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - LauncherStatusHeader( - symbol: "exclamationmark.triangle.fill", - color: failure.origin == .reset - ? LauncherTheme.ColorToken.danger - : LauncherTheme.ColorToken.warning, - title: LauncherL10n.text(failure.titleLocalizationKey), - description: LauncherL10n.text(failure.summaryLocalizationKey) - ) - - DisclosureGroup(isExpanded: $showsTechnicalDetails) { - VStack(alignment: .trailing, spacing: LauncherTheme.Spacing.small) { - ScrollView { - Text(failure.technicalDetails) - .font(.system(size: 12, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(maxHeight: 92) - - Button(LauncherL10n.text("action.copy")) { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(failure.technicalDetails, forType: .string) - } - .buttonStyle(LauncherTertiaryButtonStyle()) - } - .padding(.top, LauncherTheme.Spacing.small) - } label: { - Text(LauncherL10n.text("error.technical_details")) - .font(.system(size: 13, weight: .semibold)) - } - - HStack(spacing: LauncherTheme.Spacing.medium) { - if failure.recoveryAction != .none { - Button(LauncherL10n.text(failure.recoveryLocalizationKey)) { - model.recoverFromFailure() - } - .buttonStyle(LauncherPrimaryButtonStyle()) - .keyboardShortcut(.defaultAction) - } - if failure.origin == .launch || failure.origin == .runtime { - Button(LauncherL10n.text("action.repair_installation")) { model.repair() } - .buttonStyle(LauncherSecondaryButtonStyle()) - } - Spacer() - Button(LauncherL10n.text("action.view_log")) { model.openLog() } - .buttonStyle(LauncherTertiaryButtonStyle()) - Button(LauncherL10n.text("action.data_folder")) { model.openDataFolder() } - .buttonStyle(LauncherTertiaryButtonStyle()) - } - } - } -} - -private func configurationSummary( - _ configuration: LaunchConfigurationSnapshot, - full: Bool = false -) -> some View { - Label( - full ? configuration.fullSummary : configuration.compactSummary, - systemImage: "slider.horizontal.3" - ) - .font(.system(size: 12, weight: .medium, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .padding(.horizontal, LauncherTheme.Spacing.regular) - .frame(maxWidth: .infinity, minHeight: 44, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.controlRadius) - .fill(LauncherTheme.ColorToken.raisedControl.opacity(0.64)) - ) -} - -extension LauncherHotkeyStatus { - var localizationKey: String { - switch self { - case .permissionRequired: return "hotkeys.status.permission_required" - case .ready: return "hotkeys.status.ready" - case .active: return "hotkeys.status.active" - case .unavailable: return "hotkeys.status.unavailable" - } - } -} - -private extension LauncherFailure { - var titleLocalizationKey: String { - switch origin { - case .installation: return "error.installation.title" - case .launch: return "error.launch.title" - case .runtime: return "error.runtime.title" - case .validation: return "error.validation.title" - case .reset: return "error.reset.title" - } - } - - var summaryLocalizationKey: String { - switch origin { - case .installation: return "error.installation.summary" - case .launch: return "error.launch.summary" - case .runtime: return "error.runtime.summary" - case .validation: return "error.validation.summary" - case .reset: return "error.reset.summary" - } - } - - var recoveryLocalizationKey: String { - switch recoveryAction { - case .retryInstallation: return "action.retry_installation" - case .tryLaunchAgain: return "action.try_again" - case .restartGame: return "action.restart_game" - case .repairInstallation: return "action.repair_installation" - case .none: return "" - } - } -} diff --git a/launcher/Sources/LauncherTelemetryNoticeView.swift b/launcher/Sources/LauncherTelemetryNoticeView.swift deleted file mode 100644 index 9991edc..0000000 --- a/launcher/Sources/LauncherTelemetryNoticeView.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI - -struct LauncherTelemetryNoticeView: View { - @ObservedObject var model: LauncherModel - @State private var extendedDiagnostics = false - - var body: some View { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.large) { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.small) { - Image(systemName: "chart.bar.doc.horizontal") - .font(.system(size: 24, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - .accessibilityHidden(true) - Text(LauncherL10n.text("telemetry.notice.title")) - .font(.system(size: 22, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - } - - Text(LauncherL10n.text("telemetry.notice.body")) - .font(.system(size: 13)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .fixedSize(horizontal: false, vertical: true) - - Toggle( - LauncherL10n.text("telemetry.extended.toggle"), - isOn: $extendedDiagnostics - ) - .toggleStyle(.checkbox) - .font(.system(size: 13, weight: .medium)) - - Spacer() - - HStack(spacing: LauncherTheme.Spacing.regular) { - Link( - LauncherL10n.text("telemetry.privacy_policy"), - destination: MacticianIdentity.privacyPolicyURL - ) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - Spacer() - Button(LauncherL10n.text("telemetry.continue")) { - model.completeTelemetryNotice( - extendedDiagnostics: extendedDiagnostics - ) - } - .buttonStyle(LauncherPrimaryButtonStyle()) - } - } - .padding(LauncherTheme.Spacing.xLarge) - .frame(width: 580, height: 390) - .background(LauncherTheme.ColorToken.surface) - .preferredColorScheme(.dark) - } -} diff --git a/launcher/Sources/LauncherTelemetryService.swift b/launcher/Sources/LauncherTelemetryService.swift deleted file mode 100644 index 797f799..0000000 --- a/launcher/Sources/LauncherTelemetryService.swift +++ /dev/null @@ -1,869 +0,0 @@ -import AppKit -import Darwin -import Foundation -import ImageIO - -enum LauncherMessageTrigger: String { - case launcherStarted = "launcher_started" - case gameClosed = "game_closed" -} - -struct LauncherAnnouncement: Identifiable { - let id: String - let title: String - let text: String - let image: NSImage? -} - -struct GameSessionTracker { - private(set) var startedAt: Date? - - mutating func start(at date: Date = Date()) { - guard startedAt == nil else { return } - startedAt = date - } - - mutating func finish(at date: Date = Date()) -> Int64? { - guard let startedAt else { return nil } - self.startedAt = nil - return max(1, Int64(date.timeIntervalSince(startedAt))) - } -} - -enum LauncherTelemetryConsentState: String { - case unknown - case denied - case granted -} - -struct LauncherTelemetrySettings: Codable, Equatable { - let profileID: String - let effectsQualityID: String - let displayWidth: Int - let displayHeight: Int - let displayDensity: Int - let uiScalePercent: Int - let guestMemoryMB: Int - let guestCPUCores: Int - - init( - profile: LaunchProfile, - effectsQuality: EffectsQuality, - uiScalePercent: Int, - androidMemoryMB: Int, - androidCPUCores: Int - ) { - profileID = profile.id - effectsQualityID = effectsQuality.id - displayWidth = profile.width - displayHeight = profile.height - displayDensity = profile.density - self.uiScalePercent = uiScalePercent - guestMemoryMB = androidMemoryMB - guestCPUCores = androidCPUCores - } - - enum CodingKeys: String, CodingKey { - case profileID = "profile_id" - case effectsQualityID = "effects_quality_id" - case displayWidth = "display_width" - case displayHeight = "display_height" - case displayDensity = "display_density" - case uiScalePercent = "ui_scale_percent" - case guestMemoryMB = "guest_memory_mb" - case guestCPUCores = "guest_cpu_cores" - } -} - -struct LauncherTelemetryDevice: Codable, Equatable { - let modelIdentifier: String - let macOSVersion: String - let physicalMemoryMB: Int - let logicalCPUCount: Int - - static func current(processInfo: ProcessInfo = .processInfo) -> LauncherTelemetryDevice { - let version = processInfo.operatingSystemVersion - return LauncherTelemetryDevice( - modelIdentifier: normalizedModelIdentifier(hardwareModelIdentifier()), - macOSVersion: "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)", - physicalMemoryMB: Int(processInfo.physicalMemory / 1_048_576), - logicalCPUCount: processInfo.processorCount - ) - } - - static func normalizedModelIdentifier(_ value: String?) -> String { - guard let value, - value.range( - of: #"^[0-9A-Za-z][0-9A-Za-z,_-]{0,63}$"#, - options: .regularExpression - ) != nil else { - return "unknown" - } - return value - } - - private static func hardwareModelIdentifier() -> String? { - var size = 0 - guard sysctlbyname("hw.model", nil, &size, nil, 0) == 0, - size > 1, - size <= 65 else { - return nil - } - var value = [CChar](repeating: 0, count: size) - guard sysctlbyname("hw.model", &value, &size, nil, 0) == 0 else { - return nil - } - return String(cString: value) - } - - enum CodingKeys: String, CodingKey { - case modelIdentifier = "model_identifier" - case macOSVersion = "macos_version" - case physicalMemoryMB = "physical_memory_mb" - case logicalCPUCount = "logical_cpu_count" - } -} - -final class LauncherTelemetryService { - typealias Loader = ( - _ request: URLRequest, - _ maximumBytes: Int, - _ completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void - ) -> Void - - static let currentConsentVersion = 1 - - private enum Constant { - static let apiBaseURL = URL(string: "https://sergeinaumov.dev/mactician/api/")! - static let firstSessionPendingKey = "telemetry.firstSession.pending.v2" - static let firstSessionCompletedKey = "telemetry.firstSession.completed.v2" - static let noticeShownKey = "telemetry.noticeShown.v1" - static let extendedConsentStateKey = "telemetry.extendedConsent.state.v1" - static let extendedConsentVersionKey = "telemetry.extendedConsent.version.v1" - static let extendedPendingEventsKey = "telemetry.extended.pendingEvents.v2" - static let legacyPendingEventsKey = "telemetry.pendingEvents.v1" - static let legacyInstallationIDKey = "telemetry.installationID.v1" - static let shownMessagesKey = "telemetry.shownMessages.v1" - static let maxExtendedPendingEvents = 16 - static let maxPendingBytes = 64 * 1024 - static let firstSessionLifetime: TimeInterval = 7 * 24 * 60 * 60 - static let maxShownMessages = 128 - static let maxMessageBytes = 16 * 1024 - static let maxImageBytes = 2 * 1024 * 1024 - static let maxImageDimension = 4_096 - static let maxImagePixels = 16_000_000 - } - - private struct FirstSessionEvent: Codable { - let schemaVersion: Int - let eventID: String - let event: String - let occurredOn: String - let durationBucket: String - let launcherVersion: String - let launcherBuild: String - - enum CodingKeys: String, CodingKey { - case schemaVersion = "schema_version" - case eventID = "event_id" - case event - case occurredOn = "occurred_on" - case durationBucket = "duration_bucket" - case launcherVersion = "launcher_version" - case launcherBuild = "launcher_build" - } - } - - private struct PendingFirstSession: Codable { - let createdAt: Date - let event: FirstSessionEvent - - enum CodingKeys: String, CodingKey { - case createdAt = "created_at" - case event - } - } - - private struct DiagnosticsEvent: Codable { - let schemaVersion: Int - let eventID: String - let event: String - let occurredAt: Date - let durationSeconds: Int64 - let launcherVersion: String - let launcherBuild: String - let consentVersion: Int - let launcherSettings: LauncherTelemetrySettings - let device: LauncherTelemetryDevice - - enum CodingKeys: String, CodingKey { - case schemaVersion = "schema_version" - case eventID = "event_id" - case event - case occurredAt = "occurred_at" - case durationSeconds = "duration_seconds" - case launcherVersion = "launcher_version" - case launcherBuild = "launcher_build" - case consentVersion = "consent_version" - case launcherSettings = "launcher_settings" - case device - } - } - - private enum PendingEvent { - case firstSession(PendingFirstSession) - case diagnostics(DiagnosticsEvent) - - var eventID: String { - switch self { - case let .firstSession(pending): return pending.event.eventID - case let .diagnostics(event): return event.eventID - } - } - } - - private struct MessageResponse: Decodable { - let schemaVersion: Int - let id: String - let trigger: String - let title: String - let text: String - let imageURL: URL? - let showOnce: Bool - - enum CodingKeys: String, CodingKey { - case schemaVersion = "schema_version" - case id - case trigger - case title - case text - case imageURL = "image_url" - case showOnce = "show_once" - } - } - - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.telemetry") - private let defaults: UserDefaults - private let apiBaseURL: URL - private let launcherVersion: String - private let launcherBuild: String - private let device: LauncherTelemetryDevice - private let loader: Loader - private var isFlushing = false - - init( - defaults: UserDefaults = .standard, - apiBaseURL: URL = Constant.apiBaseURL, - bundle: Bundle = .main, - device: LauncherTelemetryDevice = .current(), - loader: Loader? = nil - ) { - self.defaults = defaults - self.apiBaseURL = apiBaseURL - launcherVersion = Self.safeVersion( - bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, - fallback: "0.0.0" - ) - launcherBuild = Self.safeBuild( - bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String, - fallback: "local" - ) - self.device = device - self.loader = loader ?? { request, maximumBytes, completion in - SafeBoundedDataLoader.load( - request: request, - maximumBytes: maximumBytes, - completion: completion - ) - } - migrateLegacyTelemetry() - enforceConsentVersion() - discardExpiredFirstSession(now: Date()) - queue.async { [weak self] in self?.flushNextEvent() } - } - - var shouldShowNotice: Bool { - !defaults.bool(forKey: Constant.noticeShownKey) - } - - var isExtendedDiagnosticsEnabled: Bool { - consentState == .granted - && defaults.integer(forKey: Constant.extendedConsentVersionKey) - == Self.currentConsentVersion - } - - func completeNotice(extendedDiagnostics: Bool) { - queue.sync { - setConsentState(extendedDiagnostics ? .granted : .denied) - defaults.set(true, forKey: Constant.noticeShownKey) - _ = defaults.synchronize() - flushNextEvent() - } - } - - func setExtendedDiagnosticsEnabled(_ enabled: Bool) { - queue.sync { - setConsentState(enabled ? .granted : .denied) - _ = defaults.synchronize() - flushNextEvent() - } - } - - func recordGameSession( - durationSeconds: Int64, - launcherSettings: LauncherTelemetrySettings, - endedAt: Date = Date() - ) { - let duration = max(1, durationSeconds) - queue.sync { - createFirstSessionIfNeeded(durationSeconds: duration, endedAt: endedAt) - if isExtendedDiagnosticsEnabled { - var events = loadDiagnosticsEvents() - events.append(DiagnosticsEvent( - schemaVersion: 2, - eventID: UUID().uuidString.lowercased(), - event: "game_session_diagnostics", - occurredAt: endedAt, - durationSeconds: duration, - launcherVersion: launcherVersion, - launcherBuild: launcherBuild, - consentVersion: Self.currentConsentVersion, - launcherSettings: launcherSettings, - device: device - )) - saveDiagnosticsEvents(Array(events.suffix(Constant.maxExtendedPendingEvents))) - } else { - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - } - _ = defaults.synchronize() - flushNextEvent() - } - } - - func fetchAnnouncement( - trigger: LauncherMessageTrigger, - completion: @escaping (LauncherAnnouncement?) -> Void - ) { - guard let url = Self.messageURL( - apiBaseURL: apiBaseURL, - trigger: trigger, - launcherVersion: launcherVersion - ) else { - DispatchQueue.main.async { completion(nil) } - return - } - var request = URLRequest(url: url, timeoutInterval: 5) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Accept") - SafeBoundedDataLoader.load(request: request, maximumBytes: Constant.maxMessageBytes) { - [weak self] result in - guard let self else { return } - guard case let .success((data, response)) = result else { - DispatchQueue.main.async { completion(nil) } - return - } - if response.statusCode == 204 { - DispatchQueue.main.async { completion(nil) } - return - } - guard response.statusCode == 200, - Self.isJSON(response), - let message = try? JSONDecoder().decode(MessageResponse.self, from: data), - Self.isValid(message: message, expectedTrigger: trigger) else { - DispatchQueue.main.async { completion(nil) } - return - } - if let imageURL = message.imageURL, Self.isAllowedImageURL(imageURL, apiBaseURL: apiBaseURL) { - self.loadImage(from: imageURL) { image in - self.deliver(message: message, image: image, completion: completion) - } - } else { - self.deliver(message: message, image: nil, completion: completion) - } - } - } - - static func messageURL( - apiBaseURL: URL, - trigger: LauncherMessageTrigger, - launcherVersion: String - ) -> URL? { - let endpoint = apiBaseURL.appendingPathComponent("v1/messages", isDirectory: false) - guard var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false) else { - return nil - } - components.queryItems = [ - URLQueryItem(name: "trigger", value: trigger.rawValue), - URLQueryItem(name: "version", value: launcherVersion) - ] - return components.url - } - - private func flushNextEvent() { - guard !isFlushing else { return } - discardExpiredFirstSession(now: Date()) - let pending: PendingEvent? - if let firstSession = loadFirstSession() { - pending = .firstSession(firstSession) - } else if isExtendedDiagnosticsEnabled { - pending = loadDiagnosticsEvents().first.map(PendingEvent.diagnostics) - } else { - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - pending = nil - } - guard let pending else { return } - let body: Data? - switch pending { - case let .firstSession(value): - body = Self.eventEncoder.encodeOrNil(value.event) - case let .diagnostics(value): - body = Self.eventEncoder.encodeOrNil(value) - } - guard let url = URL(string: "v1/events", relativeTo: apiBaseURL)?.absoluteURL, - let body else { - removePendingEvent(pending) - flushNextEvent() - return - } - var request = URLRequest(url: url, timeoutInterval: 5) - request.httpMethod = "POST" - request.httpBody = body - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("application/json", forHTTPHeaderField: "Accept") - isFlushing = true - loader(request, 1_024) { [weak self] result in - self?.queue.async { - guard let self else { return } - self.isFlushing = false - guard case let .success((_, response)) = result else { return } - let status = response.statusCode - if (200..<300).contains(status) || status == 409 { - self.completePendingEvent(pending) - } else if (400..<500).contains(status), status != 408, status != 429 { - self.terminatePendingEvent(pending) - } else { - return - } - _ = self.defaults.synchronize() - self.flushNextEvent() - } - } - } - - private func createFirstSessionIfNeeded(durationSeconds: Int64, endedAt: Date) { - guard !defaults.bool(forKey: Constant.firstSessionCompletedKey) else { return } - if loadFirstSession() != nil { - discardExpiredFirstSession(now: Date()) - return - } - let pending = PendingFirstSession( - createdAt: Date(), - event: FirstSessionEvent( - schemaVersion: 2, - eventID: UUID().uuidString.lowercased(), - event: "first_game_session", - occurredOn: Self.utcDay(for: endedAt), - durationBucket: Self.durationBucket(for: durationSeconds), - launcherVersion: launcherVersion, - launcherBuild: launcherBuild - ) - ) - guard let data = Self.eventEncoder.encodeOrNil(pending), - data.count <= Constant.maxPendingBytes else { - defaults.set(true, forKey: Constant.firstSessionCompletedKey) - return - } - defaults.set(data, forKey: Constant.firstSessionPendingKey) - } - - private func loadFirstSession() -> PendingFirstSession? { - guard let data = defaults.data(forKey: Constant.firstSessionPendingKey), - data.count <= Constant.maxPendingBytes, - let pending = try? Self.eventDecoder.decode(PendingFirstSession.self, from: data) else { - defaults.removeObject(forKey: Constant.firstSessionPendingKey) - return nil - } - return pending - } - - private func loadDiagnosticsEvents() -> [DiagnosticsEvent] { - guard isExtendedDiagnosticsEnabled, - let data = defaults.data(forKey: Constant.extendedPendingEventsKey), - data.count <= Constant.maxPendingBytes, - let events = try? Self.eventDecoder.decode([DiagnosticsEvent].self, from: data) else { - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - return [] - } - return Array(events.suffix(Constant.maxExtendedPendingEvents)) - } - - private func saveDiagnosticsEvents(_ events: [DiagnosticsEvent]) { - guard !events.isEmpty else { - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - return - } - guard let data = Self.eventEncoder.encodeOrNil(events), - data.count <= Constant.maxPendingBytes else { - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - return - } - defaults.set(data, forKey: Constant.extendedPendingEventsKey) - } - - private func completePendingEvent(_ pending: PendingEvent) { - switch pending { - case .firstSession: - defaults.set(true, forKey: Constant.firstSessionCompletedKey) - defaults.removeObject(forKey: Constant.firstSessionPendingKey) - case .diagnostics: - removePendingEvent(pending) - } - } - - private func terminatePendingEvent(_ pending: PendingEvent) { - if case .firstSession = pending { - defaults.set(true, forKey: Constant.firstSessionCompletedKey) - } - removePendingEvent(pending) - } - - private func removePendingEvent(_ pending: PendingEvent) { - switch pending { - case .firstSession: - defaults.removeObject(forKey: Constant.firstSessionPendingKey) - case .diagnostics: - saveDiagnosticsEvents( - loadDiagnosticsEvents().filter { $0.eventID != pending.eventID } - ) - } - } - - private func migrateLegacyTelemetry() { - defaults.removeObject(forKey: Constant.legacyInstallationIDKey) - defaults.removeObject(forKey: Constant.legacyPendingEventsKey) - } - - private var consentState: LauncherTelemetryConsentState { - LauncherTelemetryConsentState( - rawValue: defaults.string(forKey: Constant.extendedConsentStateKey) ?? "" - ) ?? .unknown - } - - private func enforceConsentVersion() { - if consentState == .granted, - defaults.integer(forKey: Constant.extendedConsentVersionKey) - != Self.currentConsentVersion { - defaults.set( - LauncherTelemetryConsentState.unknown.rawValue, - forKey: Constant.extendedConsentStateKey - ) - defaults.removeObject(forKey: Constant.extendedConsentVersionKey) - defaults.removeObject(forKey: Constant.noticeShownKey) - } - if consentState != .granted { - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - } - } - - private func setConsentState(_ state: LauncherTelemetryConsentState) { - defaults.set(state.rawValue, forKey: Constant.extendedConsentStateKey) - if state == .granted { - defaults.set( - Self.currentConsentVersion, - forKey: Constant.extendedConsentVersionKey - ) - } else { - defaults.removeObject(forKey: Constant.extendedConsentVersionKey) - defaults.removeObject(forKey: Constant.extendedPendingEventsKey) - } - } - - private func discardExpiredFirstSession(now: Date) { - guard let pending = loadFirstSession(), - now.timeIntervalSince(pending.createdAt) > Constant.firstSessionLifetime else { - return - } - defaults.removeObject(forKey: Constant.firstSessionPendingKey) - defaults.set(true, forKey: Constant.firstSessionCompletedKey) - } - - static func durationBucket(for durationSeconds: Int64) -> String { - switch max(1, durationSeconds) { - case ..<300: return "under_5m" - case ..<900: return "5_15m" - case ..<1_800: return "15_30m" - case ..<3_600: return "30_60m" - case ..<7_200: return "60_120m" - case ...14_400: return "120_240m" - default: return "over_240m" - } - } - - private static func utcDay(for date: Date) -> String { - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - let components = calendar.dateComponents([.year, .month, .day], from: date) - return String( - format: "%04d-%02d-%02d", - components.year ?? 1970, - components.month ?? 1, - components.day ?? 1 - ) - } - - private func loadImage(from url: URL, completion: @escaping (NSImage?) -> Void) { - var request = URLRequest(url: url, timeoutInterval: 8) - request.httpMethod = "GET" - request.setValue("image/png, image/jpeg", forHTTPHeaderField: "Accept") - SafeBoundedDataLoader.load(request: request, maximumBytes: Constant.maxImageBytes) { result in - guard case let .success((data, response)) = result, - response.statusCode == 200, - Self.isAllowedImageMIME(response.mimeType), - let image = Self.decodeSafeImage(data) else { - completion(nil) - return - } - completion(image) - } - } - - private func deliver( - message: MessageResponse, - image: NSImage?, - completion: @escaping (LauncherAnnouncement?) -> Void - ) { - queue.async { [weak self] in - guard let self else { return } - var shown = defaults.stringArray(forKey: Constant.shownMessagesKey) ?? [] - if message.showOnce && shown.contains(message.id) { - DispatchQueue.main.async { completion(nil) } - return - } - if message.showOnce { - shown.removeAll(where: { $0 == message.id }) - shown.append(message.id) - shown = Array(shown.suffix(Constant.maxShownMessages)) - defaults.set(shown, forKey: Constant.shownMessagesKey) - } - let announcement = LauncherAnnouncement( - id: message.id, - title: message.title, - text: message.text, - image: image - ) - DispatchQueue.main.async { completion(announcement) } - } - } - - private static func safeVersion(_ value: String?, fallback: String) -> String { - guard let value, - value.range( - of: #"^[0-9]{1,5}(?:\.[0-9]{1,5}){0,3}(?:[-+][0-9A-Za-z.-]{1,40})?$"#, - options: .regularExpression - ) != nil else { - return fallback - } - return value - } - - private static func safeBuild(_ value: String?, fallback: String) -> String { - guard let value, - value.range(of: #"^[0-9A-Za-z][0-9A-Za-z._-]{0,31}$"#, options: .regularExpression) != nil else { - return fallback - } - return value - } - - private static func isValid( - message: MessageResponse, - expectedTrigger: LauncherMessageTrigger - ) -> Bool { - message.schemaVersion == 1 - && message.trigger == expectedTrigger.rawValue - && message.id.range( - of: #"^[0-9A-Za-z][0-9A-Za-z._-]{0,63}$"#, - options: .regularExpression - ) != nil - && !message.title.isEmpty - && message.title.utf8.count <= 120 - && !message.text.isEmpty - && message.text.utf8.count <= 4_000 - } - - private static func isJSON(_ response: HTTPURLResponse) -> Bool { - response.mimeType?.lowercased() == "application/json" - } - - private static func isAllowedImageMIME(_ value: String?) -> Bool { - value?.lowercased() == "image/png" || value?.lowercased() == "image/jpeg" - } - - private static func isAllowedImageURL(_ url: URL, apiBaseURL: URL) -> Bool { - guard url.scheme?.lowercased() == "https", - url.user == nil, - url.password == nil, - url.host?.lowercased() == apiBaseURL.host?.lowercased(), - url.port == nil || url.port == 443, - url.query == nil, - url.fragment == nil else { - return false - } - let prefix = apiBaseURL.appendingPathComponent("v1/images", isDirectory: true).path - return url.path.hasPrefix(prefix) && !url.path.dropFirst(prefix.count).contains("/") - } - - private static func decodeSafeImage(_ data: Data) -> NSImage? { - let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary - guard let source = CGImageSourceCreateWithData(data as CFData, sourceOptions), - CGImageSourceGetCount(source) == 1, - let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, sourceOptions) - as? [CFString: Any], - let width = properties[kCGImagePropertyPixelWidth] as? NSNumber, - let height = properties[kCGImagePropertyPixelHeight] as? NSNumber else { - return nil - } - let pixelWidth = width.intValue - let pixelHeight = height.intValue - guard pixelWidth > 0, - pixelHeight > 0, - pixelWidth <= Constant.maxImageDimension, - pixelHeight <= Constant.maxImageDimension, - pixelWidth <= Constant.maxImagePixels / pixelHeight else { - return nil - } - let decodeOptions = [kCGImageSourceShouldCacheImmediately: true] as CFDictionary - guard let decoded = CGImageSourceCreateImageAtIndex(source, 0, decodeOptions) else { - return nil - } - return NSImage( - cgImage: decoded, - size: NSSize(width: decoded.width, height: decoded.height) - ) - } - - private static let eventEncoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - return encoder - }() - - private static let eventDecoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - return decoder - }() -} - -private enum SafeBoundedDataLoaderError: Error { - case invalidResponse - case responseTooLarge -} - -private final class SafeBoundedDataLoader: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate { - typealias ResultValue = Result<(Data, HTTPURLResponse), Error> - - private let maximumBytes: Int - private var completion: ((ResultValue) -> Void)? - private var response: HTTPURLResponse? - private var data = Data() - private var session: URLSession? - private var exceededLimit = false - - private init(maximumBytes: Int, completion: @escaping (ResultValue) -> Void) { - self.maximumBytes = maximumBytes - self.completion = completion - } - - static func load( - request: URLRequest, - maximumBytes: Int, - completion: @escaping (ResultValue) -> Void - ) { - let loader = SafeBoundedDataLoader(maximumBytes: maximumBytes, completion: completion) - let configuration = URLSessionConfiguration.ephemeral - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - configuration.timeoutIntervalForRequest = request.timeoutInterval - configuration.timeoutIntervalForResource = max(request.timeoutInterval, 8) - configuration.httpShouldSetCookies = false - configuration.urlCache = nil - configuration.httpMaximumConnectionsPerHost = 1 - let delegateQueue = OperationQueue() - delegateQueue.maxConcurrentOperationCount = 1 - let session = URLSession(configuration: configuration, delegate: loader, delegateQueue: delegateQueue) - loader.session = session - session.dataTask(with: request).resume() - } - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void - ) { - guard let response = response as? HTTPURLResponse else { - completionHandler(.cancel) - finish(.failure(SafeBoundedDataLoaderError.invalidResponse)) - return - } - let expected = response.expectedContentLength - guard expected <= Int64(maximumBytes) else { - exceededLimit = true - completionHandler(.cancel) - return - } - self.response = response - if expected > 0 { - data.reserveCapacity(Int(expected)) - } - completionHandler(.allow) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive chunk: Data) { - guard chunk.count <= maximumBytes - data.count else { - exceededLimit = true - dataTask.cancel() - return - } - data.append(chunk) - } - - func urlSession( - _ session: URLSession, - task: URLSessionTask, - didCompleteWithError error: Error? - ) { - if exceededLimit { - finish(.failure(SafeBoundedDataLoaderError.responseTooLarge)) - } else if let error { - finish(.failure(error)) - } else if let response { - finish(.success((data, response))) - } else { - finish(.failure(SafeBoundedDataLoaderError.invalidResponse)) - } - } - - func urlSession( - _ session: URLSession, - task: URLSessionTask, - willPerformHTTPRedirection response: HTTPURLResponse, - newRequest request: URLRequest, - completionHandler: @escaping (URLRequest?) -> Void - ) { - completionHandler(nil) - } - - private func finish(_ result: ResultValue) { - guard let completion else { return } - self.completion = nil - completion(result) - session?.finishTasksAndInvalidate() - session = nil - } -} - -private extension JSONEncoder { - func encodeOrNil(_ value: T) -> Data? { - try? encode(value) - } -} diff --git a/launcher/Sources/LauncherTheme.swift b/launcher/Sources/LauncherTheme.swift deleted file mode 100644 index 98958eb..0000000 --- a/launcher/Sources/LauncherTheme.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI - -enum LauncherTheme { - enum ColorToken { - static let window = Color(red: 0.006, green: 0.012, blue: 0.024) - static let surface = Color(red: 0.018, green: 0.035, blue: 0.065) - static let elevatedSurface = Color(red: 0.035, green: 0.065, blue: 0.105) - static let raisedControl = Color(red: 0.055, green: 0.082, blue: 0.12) - static let textPrimary = Color.white - static let textSecondary = Color(red: 0.64, green: 0.69, blue: 0.75) - static let textTertiary = Color(red: 0.48, green: 0.56, blue: 0.65) - static let primaryAction = Color(red: 0.29, green: 0.96, blue: 0.58) - static let interactive = Color(red: 0.69, green: 0.37, blue: 0.98) - static let success = Color(red: 0.29, green: 0.96, blue: 0.58) - static let warning = Color(red: 0.78, green: 0.65, blue: 0.36) - static let danger = Color(red: 0.94, green: 0.3, blue: 0.32) - static let neutralBorder = Color.white.opacity(0.13) - } - - enum Spacing { - static let xSmall: CGFloat = 4 - static let small: CGFloat = 8 - static let medium: CGFloat = 12 - static let regular: CGFloat = 16 - static let large: CGFloat = 24 - static let xLarge: CGFloat = 32 - } - - enum Metric { - static let outerInset: CGFloat = 16 - static let headerHeight: CGFloat = 56 - static let trafficLightReserve: CGFloat = 56 - static let stateDeckInset: CGFloat = 24 - static let stateDeckPadding: CGFloat = 32 - static let contentMaxWidth: CGFloat = 1120 - static let standardControlHeight: CGFloat = 36 - static let primaryControlHeight: CGFloat = 48 - static let controlRadius: CGFloat = 6 - static let surfaceRadius: CGFloat = 12 - static let sheetWidth: CGFloat = 640 - } -} - -extension View { - func launcherSurface( - radius: CGFloat = LauncherTheme.Metric.surfaceRadius, - fill: Color = LauncherTheme.ColorToken.elevatedSurface.opacity(0.96) - ) -> some View { - background( - RoundedRectangle(cornerRadius: radius, style: .continuous) - .fill(fill) - ) - .overlay( - RoundedRectangle(cornerRadius: radius, style: .continuous) - .stroke(LauncherTheme.ColorToken.neutralBorder, lineWidth: 1) - ) - } -} diff --git a/launcher/Sources/LauncherUpdateController.swift b/launcher/Sources/LauncherUpdateController.swift deleted file mode 100644 index 9ba6fa3..0000000 --- a/launcher/Sources/LauncherUpdateController.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Combine -import Foundation -import Sparkle - -@MainActor -final class LauncherUpdateController: ObservableObject { - let updaterController: SPUStandardUpdaterController - @Published private(set) var canCheckForUpdates = false - private var canCheckForUpdatesSubscription: AnyCancellable? - - init(startingUpdater: Bool = true) { - updaterController = SPUStandardUpdaterController( - startingUpdater: startingUpdater, - updaterDelegate: nil, - userDriverDelegate: nil - ) - canCheckForUpdatesSubscription = updaterController.updater - .publisher(for: \.canCheckForUpdates) - .receive(on: RunLoop.main) - .sink { [weak self] value in - self?.canCheckForUpdates = value - } - - if startingUpdater && updaterController.updater.automaticallyChecksForUpdates { - updaterController.updater.checkForUpdatesInBackground() - } - } - - func checkForUpdates() { - updaterController.checkForUpdates(nil) - } -} diff --git a/launcher/Sources/LauncherView.swift b/launcher/Sources/LauncherView.swift deleted file mode 100644 index 602c1b7..0000000 --- a/launcher/Sources/LauncherView.swift +++ /dev/null @@ -1,218 +0,0 @@ -import AppKit -import SwiftUI - -struct LauncherView: View { - @ObservedObject var model: LauncherModel - @ObservedObject var updateController: LauncherUpdateController - @State private var showSettings = false - @State private var showResetConfirmation = false - - private let heroImage: NSImage? = { - guard let url = Bundle.main.url(forResource: "MacticianHero", withExtension: "png") else { - return nil - } - return NSImage(contentsOf: url) - }() - - var body: some View { - GeometryReader { geometry in - let surfaceWidth = max(0, geometry.size.width - LauncherTheme.Metric.outerInset * 2) - let surfaceHeight = max(0, geometry.size.height - LauncherTheme.Metric.outerInset * 2) - ZStack { - LauncherTheme.ColorToken.window.ignoresSafeArea() - clientSurface( - compactHeight: geometry.size.height < 720, - availableWidth: surfaceWidth, - availableHeight: surfaceHeight - ) - } - } - .preferredColorScheme(.dark) - .sheet(isPresented: $model.shouldShowTelemetryNotice) { - LauncherTelemetryNoticeView(model: model) - .interactiveDismissDisabled() - } - .sheet(isPresented: $showSettings) { - LauncherSettingsView( - model: model, - updateController: updateController, - showResetConfirmation: $showResetConfirmation - ) - } - .sheet(item: $model.announcement) { announcement in - LauncherAnnouncementView( - announcement: announcement, - dismiss: model.dismissAnnouncement - ) - .interactiveDismissDisabled() - } - .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in - model.refreshHotkeyStatus() - } - } - - private func clientSurface( - compactHeight: Bool, - availableWidth: CGFloat, - availableHeight: CGFloat - ) -> some View { - ZStack { - artwork - - VStack(spacing: 0) { - header - hero(compact: compactHeight) - Spacer(minLength: LauncherTheme.Spacing.medium) - LauncherStateDeck(model: model, showSettings: $showSettings) - .frame( - width: min( - LauncherTheme.Metric.contentMaxWidth, - max(0, availableWidth - LauncherTheme.Metric.stateDeckInset * 2) - ) - ) - footer - } - .frame(width: availableWidth) - } - .frame(width: availableWidth, height: availableHeight) - .background(LauncherTheme.ColorToken.surface) - .clipShape( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.surfaceRadius, style: .continuous) - ) - .overlay( - RoundedRectangle(cornerRadius: LauncherTheme.Metric.surfaceRadius, style: .continuous) - .stroke(LauncherTheme.ColorToken.neutralBorder, lineWidth: 1) - ) - .shadow(color: Color.black.opacity(0.52), radius: 22, y: 10) - } - - private var artwork: some View { - ZStack { - if let heroImage { - Image(nsImage: heroImage) - .resizable() - .scaledToFill() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - } else { - LauncherTheme.ColorToken.surface - } - - LinearGradient( - stops: [ - .init(color: LauncherTheme.ColorToken.surface.opacity(0.06), location: 0), - .init(color: LauncherTheme.ColorToken.surface.opacity(0.22), location: 0.42), - .init(color: LauncherTheme.ColorToken.surface.opacity(0.9), location: 0.78), - .init(color: LauncherTheme.ColorToken.surface, location: 1) - ], - startPoint: .top, - endPoint: .bottom - ) - - LinearGradient( - colors: [LauncherTheme.ColorToken.surface.opacity(0.9), .clear], - startPoint: .leading, - endPoint: .trailing - ) - } - .accessibilityHidden(true) - } - - private var header: some View { - HStack(spacing: 0) { - Color.clear - .frame(width: LauncherTheme.Metric.trafficLightReserve) - - LauncherBrandMark() - .padding(.trailing, LauncherTheme.Spacing.medium) - - VStack(alignment: .leading, spacing: 2) { - Text("Mactician") - .font(.system(size: 15, weight: .bold, design: .rounded)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - Text(LauncherL10n.text("header.subtitle")) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - } - - LauncherDragRegion() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .accessibilityHidden(true) - - HStack(spacing: LauncherTheme.Spacing.small) { - Circle() - .fill(LauncherTheme.ColorToken.interactive) - .frame(width: 7, height: 7) - .accessibilityHidden(true) - Text(LauncherL10n.format("header.pbe_version_format", model.gameDisplayVersion)) - .font(.system(size: 12, weight: .semibold, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - } - .padding(.leading, LauncherTheme.Spacing.medium) - .accessibilityElement(children: .combine) - - Text(LauncherBuildInfo.display) - .font(.system(size: 11, weight: .medium, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - .padding(.leading, LauncherTheme.Spacing.medium) - - Button { showSettings = true } label: { - Image(systemName: "gearshape.fill") - } - .buttonStyle(LauncherIconButtonStyle()) - .accessibilityLabel(LauncherL10n.text("settings.open")) - .help(LauncherL10n.text("settings.open")) - .padding(.leading, LauncherTheme.Spacing.medium) - } - .padding(.trailing, LauncherTheme.Spacing.xLarge) - .frame(height: LauncherTheme.Metric.headerHeight) - .background(Color.black.opacity(0.2)) - .overlay(alignment: .bottom) { LauncherDivider() } - } - - private func hero(compact: Bool) -> some View { - HStack { - VStack(alignment: .leading, spacing: LauncherTheme.Spacing.small) { - Text(LauncherL10n.text("hero.eyebrow")) - .font(.system(size: 11, weight: .semibold, design: .rounded)) - .tracking(1.1) - .foregroundColor(LauncherTheme.ColorToken.interactive) - - Text(LauncherL10n.text("hero.title")) - .font(.system(size: compact ? 30 : 34, weight: .black, design: .serif)) - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - .shadow(color: Color.black.opacity(0.45), radius: 6, y: 2) - - if !compact { - Text(LauncherL10n.text("hero.description")) - .font(.system(size: 13)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - } - } - Spacer() - } - .padding(.horizontal, LauncherTheme.Metric.trafficLightReserve) - .frame(height: compact ? 104 : 152, alignment: .leading) - } - - private var footer: some View { - HStack(spacing: LauncherTheme.Spacing.small) { - Label(LauncherL10n.text("footer.private_build"), systemImage: "lock.fill") - Text("\u{00b7}") - Text(LauncherL10n.text("footer.apple_silicon")) - Text("\u{00b7}") - Link(destination: MacticianIdentity.websiteURL) { - Label(LauncherL10n.text("footer.developer_website"), systemImage: "link") - .foregroundColor(LauncherTheme.ColorToken.interactive) - } - .buttonStyle(.plain) - .help(LauncherL10n.text("footer.developer_website")) - Spacer() - Text(LauncherL10n.text("footer.legal")) - } - .font(.system(size: 10, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - .padding(.horizontal, LauncherTheme.Metric.trafficLightReserve) - .frame(height: 36) - } -} diff --git a/launcher/Sources/MacticianApp.swift b/launcher/Sources/MacticianApp.swift deleted file mode 100644 index 13222f9..0000000 --- a/launcher/Sources/MacticianApp.swift +++ /dev/null @@ -1,112 +0,0 @@ -import AppKit -import SwiftUI - -@main -struct MacticianApp: App { - @NSApplicationDelegateAdaptor(LauncherAppDelegate.self) private var appDelegate - @StateObject private var model: LauncherModel - @StateObject private var updateController: LauncherUpdateController - @State private var showAbout = false - - init() { - let model = LauncherModel() - _model = StateObject(wrappedValue: model) - _updateController = StateObject(wrappedValue: LauncherUpdateController()) - LauncherAppDelegate.pendingModel = model - } - - var body: some Scene { - WindowGroup("Mactician") { - LauncherView(model: model, updateController: updateController) - .frame(minWidth: 1080, minHeight: 660) - .sheet(isPresented: $showAbout) { - MacticianAboutView() - } - } - .windowStyle(.hiddenTitleBar) - .commands { - CommandGroup(replacing: .newItem) { } - CommandGroup(replacing: .appInfo) { - Button("About Mactician") { - showAbout = true - } - } - CommandGroup(after: .appInfo) { - Button(LauncherL10n.text("updates.check")) { - updateController.checkForUpdates() - } - .disabled(!updateController.canCheckForUpdates) - } - } - } -} - -struct MacticianAboutView: View { - private var version: String { - Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev" - } - - private var build: String { - Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "local" - } - - var body: some View { - VStack(spacing: LauncherTheme.Spacing.regular) { - MacticianMark() - .frame(width: 88, height: 88) - .accessibilityLabel("Mactician") - VStack(spacing: LauncherTheme.Spacing.xSmall) { - Text("Mactician") - .font(.system(size: 28, weight: .bold, design: .rounded)) - Text(LauncherL10n.text("about.descriptor")) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(LauncherTheme.ColorToken.textSecondary) - Text("Version \(version) (\(build))") - .font(.system(size: 12, design: .monospaced)) - .foregroundColor(LauncherTheme.ColorToken.textTertiary) - } - VStack(spacing: LauncherTheme.Spacing.xSmall) { - Text("Free and open source.") - Text("Built for two tacticians. Shared with everyone.") - } - .font(.system(size: 13)) - .multilineTextAlignment(.center) - HStack(spacing: LauncherTheme.Spacing.regular) { - aboutLink("Website", destination: MacticianIdentity.websiteURL) - aboutLink("GitHub", destination: MacticianIdentity.sourceURL) - aboutLink("Technical story", destination: MacticianIdentity.technicalStoryURL) - aboutLink("Report an issue", destination: MacticianIdentity.issueURL) - } - } - .foregroundColor(LauncherTheme.ColorToken.textPrimary) - .padding(LauncherTheme.Spacing.xLarge) - .frame(width: 560, height: 360) - .background(LauncherTheme.ColorToken.surface) - .preferredColorScheme(.dark) - } - - private func aboutLink(_ title: String, destination: URL) -> some View { - Link(title, destination: destination) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(LauncherTheme.ColorToken.interactive) - } -} - -@MainActor -final class LauncherAppDelegate: NSObject, NSApplicationDelegate { - static weak var pendingModel: LauncherModel? - - func applicationDidFinishLaunching(_ notification: Notification) { - NSApp.setActivationPolicy(.regular) - NSApp.activate(ignoringOtherApps: true) - } - - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - true - } - - func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - Self.pendingModel?.shutdown() - return .terminateNow - } -} diff --git a/launcher/Sources/RiotLoginAnimationRepairService.swift b/launcher/Sources/RiotLoginAnimationRepairService.swift deleted file mode 100644 index aad8938..0000000 --- a/launcher/Sources/RiotLoginAnimationRepairService.swift +++ /dev/null @@ -1,366 +0,0 @@ -import Foundation - -/// Repairs a Chromium compositor artifact in Riot's login WebView. -/// -/// The login page leaves its two field wrappers in finished, fill-forwards CSS -/// animations. With the guest ANGLE renderer those finished animation layers -/// can disappear even though the DOM remains visible and interactive. This -/// service connects to the app's local WebView DevTools socket and removes only -/// those two presentation animations. It never focuses a field, opens the -/// keyboard, or reads/writes form values. -final class RiotLoginAnimationRepairService { - private static let package = "com.riotgames.league.teamfighttactics.pbe" - private static let loginActivity = "com.riotgames.platformui.mobilefre.MobileFREWebViewActivity" - private static let loginComponent = "\(package)/\(loginActivity)" - private static let adbServerPort = "5038" - - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.riot-login-animation-repair") - private let generationLock = NSLock() - private var generation = 0 - - func start(adb: URL, serial: String = "emulator-5582", log: URL) { - let token = nextGeneration() - queue.async { [weak self] in - self?.watch(adb: adb, serial: serial, log: log, generation: token) - } - } - - func stop() { - _ = nextGeneration() - } - - private func watch(adb: URL, serial: String, log: URL, generation token: Int) { - var repairedPID: Int? - var lastFailureLog = Date.distantPast - - while isCurrent(token) { - autoreleasepool { - guard deviceIsReady(adb: adb, serial: serial), - loginIsTopActivity(adb: adb, serial: serial) else { - repairedPID = nil - return - } - - guard let pid = applicationPID(adb: adb, serial: serial) else { - return - } - guard repairedPID != pid else { return } - - do { - let result = try repair(adb: adb, serial: serial, pid: pid) - guard result.fields == 2, result.wrappers == 2, result.remainingAnimations == 0 else { - throw RepairError.incomplete( - "fields=\(result.fields), wrappers=\(result.wrappers), animations=\(result.remainingAnimations)" - ) - } - repairedPID = pid - SystemServices.appendLog( - "Riot login WebView: removed finished field animations; focus, keyboard, and form data were unchanged.", - to: log - ) - } catch { - // The activity becomes top-resumed before Chromium exposes - // its page and DevTools socket. Retry quietly during that - // normal startup window, but leave a bounded diagnostic. - if Date().timeIntervalSince(lastFailureLog) >= 10 { - SystemServices.appendLog( - "Riot login WebView repair is waiting for Chromium: \(error.localizedDescription)", - to: log - ) - lastFailureLog = Date() - } - } - } - Thread.sleep(forTimeInterval: 0.4) - } - } - - private func deviceIsReady(adb: URL, serial: String) -> Bool { - guard let state = try? runADB(adb, serial: serial, arguments: ["get-state"]) else { return false } - return state.trimmingCharacters(in: .whitespacesAndNewlines) == "device" - } - - private func loginIsTopActivity(adb: URL, serial: String) -> Bool { - guard let activities = try? runADB( - adb, - serial: serial, - arguments: ["shell", "dumpsys", "activity", "activities"] - ) else { return false } - guard let topLine = activities.split(separator: "\n").first(where: { - $0.contains("topResumedActivity=ActivityRecord") - }) else { return false } - return topLine.contains(Self.loginComponent) - } - - private func applicationPID(adb: URL, serial: String) -> Int? { - guard let output = try? runADB( - adb, - serial: serial, - arguments: ["shell", "pidof", Self.package] - ) else { return nil } - return output.split(whereSeparator: { $0.isWhitespace }).compactMap { Int($0) }.first - } - - private func repair(adb: URL, serial: String, pid: Int) throws -> RepairResult { - let socket = "localabstract:webview_devtools_remote_\(pid)" - let output = try runADB( - adb, - serial: serial, - arguments: ["forward", "tcp:0", socket] - ) - guard let port = Int(output.trimmingCharacters(in: .whitespacesAndNewlines)) else { - throw RepairError.invalidForward(output) - } - defer { - _ = try? runADB( - adb, - serial: serial, - arguments: ["forward", "--remove", "tcp:\(port)"] - ) - } - - let configuration = URLSessionConfiguration.ephemeral - configuration.connectionProxyDictionary = [:] - configuration.timeoutIntervalForRequest = 3 - configuration.timeoutIntervalForResource = 4 - let session = URLSession(configuration: configuration) - defer { session.invalidateAndCancel() } - - let target = try webViewTarget(port: port, session: session) - let connection = CDPConnection(url: target, session: session) - defer { connection.close() } - try connection.connect() - _ = try connection.call(method: "Runtime.enable") - let response = try connection.call( - method: "Runtime.evaluate", - parameters: [ - "expression": Self.repairExpression, - "returnByValue": true, - "awaitPromise": true - ] - ) - return try RepairResult(response: response) - } - - private func webViewTarget(port: Int, session: URLSession) throws -> URL { - guard let url = URL(string: "http://127.0.0.1:\(port)/json/list") else { - throw RepairError.invalidForward("tcp:\(port)") - } - let data = try session.synchronousData(from: url, timeout: 4) - let targets = try JSONDecoder().decode([DevToolsTarget].self, from: data) - let target = targets.first(where: { $0.type == "page" && $0.webSocketDebuggerUrl != nil }) - ?? targets.first(where: { $0.webSocketDebuggerUrl != nil }) - guard let rawURL = target?.webSocketDebuggerUrl, let socketURL = URL(string: rawURL) else { - throw RepairError.missingTarget - } - return socketURL - } - - private func runADB(_ adb: URL, serial: String, arguments: [String]) throws -> String { - try SystemServices.run( - adb, - ["-s", serial] + arguments, - environment: [ - "ANDROID_ADB_SERVER_PORT": Self.adbServerPort, - "ADB_MDNS_AUTO_CONNECT": "" - ] - ) - } - - private func nextGeneration() -> Int { - generationLock.lock() - defer { generationLock.unlock() } - generation += 1 - return generation - } - - private func isCurrent(_ token: Int) -> Bool { - generationLock.lock() - defer { generationLock.unlock() } - return generation == token - } - - private static let repairExpression = #""" - (() => { - const fields = Array.from(document.querySelectorAll( - 'input[name="username"], input[name="password"]' - )); - const wrappers = Array.from(new Set(fields - .map((field) => field.closest('.field__input.field__input--animate')) - .filter(Boolean))); - if (fields.length !== 2 || wrappers.length !== 2) { - return { fields: fields.length, wrappers: wrappers.length, remainingAnimations: -1 }; - } - - const styleId = 'tft-stable-login-fields'; - let style = document.getElementById(styleId); - if (!style) { - style = document.createElement('style'); - style.id = styleId; - style.textContent = - '.field__input--animate {' + - 'animation: none !important;' + - 'opacity: 1 !important;' + - 'transform: none !important;' + - '}'; - document.head.appendChild(style); - } - - wrappers.forEach((wrapper) => { - wrapper.getAnimations().forEach((animation) => animation.cancel()); - wrapper.style.animation = 'none'; - wrapper.style.opacity = '1'; - wrapper.style.transform = 'none'; - }); - document.documentElement.getBoundingClientRect(); - const remainingAnimations = wrappers.reduce( - (count, wrapper) => count + wrapper.getAnimations().length, - 0 - ); - return { fields: fields.length, wrappers: wrappers.length, remainingAnimations }; - })() - """# -} - -private struct DevToolsTarget: Decodable { - let type: String? - let webSocketDebuggerUrl: String? -} - -private struct RepairResult { - let fields: Int - let wrappers: Int - let remainingAnimations: Int - - init(response: [String: Any]) throws { - guard let result = response["result"] as? [String: Any], - let remoteObject = result["result"] as? [String: Any], - let value = remoteObject["value"] as? [String: Any], - let fields = value["fields"] as? Int, - let wrappers = value["wrappers"] as? Int, - let remainingAnimations = value["remainingAnimations"] as? Int else { - throw RepairError.invalidResponse - } - self.fields = fields - self.wrappers = wrappers - self.remainingAnimations = remainingAnimations - } -} - -private final class CDPConnection { - private let task: URLSessionWebSocketTask - private var nextID = 1 - - init(url: URL, session: URLSession) { - task = session.webSocketTask(with: url) - } - - func connect() throws { - task.resume() - } - - func close() { - task.cancel(with: .normalClosure, reason: nil) - } - - func call(method: String, parameters: [String: Any] = [:]) throws -> [String: Any] { - let id = nextID - nextID += 1 - let request: [String: Any] = ["id": id, "method": method, "params": parameters] - let data = try JSONSerialization.data(withJSONObject: request) - guard let string = String(data: data, encoding: .utf8) else { - throw RepairError.invalidResponse - } - try task.synchronousSend(.string(string), timeout: 4) - - for _ in 0 ..< 20 { - let message = try task.synchronousReceive(timeout: 4) - let responseData: Data - switch message { - case let .string(value): responseData = Data(value.utf8) - case let .data(value): responseData = value - @unknown default: throw RepairError.invalidResponse - } - guard let response = try JSONSerialization.jsonObject(with: responseData) as? [String: Any] else { - continue - } - guard response["id"] as? Int == id else { continue } - if let error = response["error"] as? [String: Any] { - throw RepairError.protocolError(error["message"] as? String ?? "unknown CDP error") - } - return response - } - throw RepairError.timeout - } -} - -private extension URLSession { - func synchronousData(from url: URL, timeout: TimeInterval) throws -> Data { - let semaphore = DispatchSemaphore(value: 0) - var result: Result? - let task = dataTask(with: url) { data, _, error in - if let error { - result = .failure(error) - } else if let data { - result = .success(data) - } else { - result = .failure(RepairError.invalidResponse) - } - semaphore.signal() - } - task.resume() - guard semaphore.wait(timeout: .now() + timeout) == .success else { - task.cancel() - throw RepairError.timeout - } - return try result?.get() ?? { throw RepairError.invalidResponse }() - } -} - -private extension URLSessionWebSocketTask { - func synchronousSend(_ message: Message, timeout: TimeInterval) throws { - let semaphore = DispatchSemaphore(value: 0) - var sendError: Error? - send(message) { error in - sendError = error - semaphore.signal() - } - guard semaphore.wait(timeout: .now() + timeout) == .success else { - throw RepairError.timeout - } - if let sendError { throw sendError } - } - - func synchronousReceive(timeout: TimeInterval) throws -> Message { - let semaphore = DispatchSemaphore(value: 0) - var result: Result? - receive { value in - result = value - semaphore.signal() - } - guard semaphore.wait(timeout: .now() + timeout) == .success else { - throw RepairError.timeout - } - return try result?.get() ?? { throw RepairError.invalidResponse }() - } -} - -private enum RepairError: LocalizedError { - case invalidForward(String) - case missingTarget - case invalidResponse - case incomplete(String) - case protocolError(String) - case timeout - - var errorDescription: String? { - switch self { - case let .invalidForward(value): return "invalid ADB forward: \(value)" - case .missingTarget: return "WebView DevTools target is not ready" - case .invalidResponse: return "invalid WebView DevTools response" - case let .incomplete(value): return "login fields are not ready (\(value))" - case let .protocolError(value): return value - case .timeout: return "WebView DevTools request timed out" - } - } -} diff --git a/launcher/Sources/RuntimeController.swift b/launcher/Sources/RuntimeController.swift deleted file mode 100644 index 110f1c4..0000000 --- a/launcher/Sources/RuntimeController.swift +++ /dev/null @@ -1,153 +0,0 @@ -import Foundation - -final class RuntimeController { - typealias EventHandler = (RuntimeEvent) -> Void - - private let paths: LauncherPaths - private let parsingQueue = DispatchQueue(label: "dev.sergeinaumov.mactician.runtime-events") - private var eventBuffer = Data() - private var process: Process? - private var outputPipe: Pipe? - - init(paths: LauncherPaths) { - self.paths = paths - } - - var isRunning: Bool { process?.isRunning == true } - - func start( - profile: LaunchProfile, - effectsQuality: EffectsQuality, - language: GameLanguage, - cpuCores: Int, - memoryMB: Int, - uiScalePercent: Int, - state: InstallState, - gameRelease: GameRelease, - gameResources: URL, - events: @escaping EventHandler - ) throws { - guard !isRunning else { - throw LauncherError.process("TFT is already launching") - } - guard state.isReady, - state.gameVersion == gameRelease.version, - state.gameBaseSHA256 == gameRelease.baseSHA256, - state.overlaySHA256 != nil else { - throw LauncherError.unsupportedGame("The installed TFT version is not supported by this launcher build") - } - // QEMU replaces the wrapper application's Dock icon and exposes its - // implementation name in the native title bar after Qt starts. Refresh - // the verified branding patch before every launch so existing installs - // are upgraded without requiring Repair. - try EmulatorBrandingPatch.apply( - at: paths.qemuSystem, - entitlements: paths.qemuHypervisorEntitlements - ) - // Refresh small launcher-owned scripts on every start so a launcher - // update can repair an existing AVD without reinstalling Android or TFT. - try InstallerService.refreshRuntimeProject(at: paths) - let overlayHash = try InstallerService.prepareOverlay( - source: gameResources.appendingPathComponent("base.apk"), - expectedSourceSHA256: gameRelease.baseSHA256, - destination: paths.overlayAPK, - stagingRoot: paths.staging, - language: language - ) - let graphicsProfile = paths.effectsProfile(for: effectsQuality) - let profileHash = try SystemServices.sha256(of: graphicsProfile) - guard GuestResourceOptions.cpuCores( - logicalCPUCount: ProcessInfo.processInfo.processorCount - ).contains(cpuCores) else { - throw LauncherError.preflight("The selected Android vCPU count is unavailable on this Mac") - } - guard GuestResourceOptions.memoryMB( - physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory - ).contains(memoryMB) else { - throw LauncherError.preflight("The selected Android RAM size is unavailable on this Mac") - } - guard let uiScale = InterfaceScaleOptions.runtimeValue(percent: uiScalePercent) else { - throw LauncherError.preflight("The selected interface scale is unavailable") - } - let runtime = Process() - let pipe = Pipe() - runtime.executableURL = paths.runtimeHelper - runtime.standardOutput = pipe - runtime.standardError = try SystemServices.appendHandle(for: paths.launcherLog) - runtime.environment = ProcessInfo.processInfo.environment.merging([ - "TFT_RUNTIME_PROJECT": paths.runtimeProject.path, - "TFT_LAUNCH_LOG": paths.launcherLog.path, - "TFT_ADB": paths.adb.path, - "TFT_EMULATOR": paths.emulator.path, - "TFT_ROOT_SDK": paths.sdk.path, - "TFT_ROOT_AVD_HOME": paths.avdHome.path, - "TFT_AVD_HOME": paths.avdHome.path, - "TFT_AVD_NAME": "TftPBE", - "TFT_SERIAL": "emulator-5582", - "TFT_EMULATOR_PORT": "5582", - "TFT_ADB_SERVER_PORT": "5038", - "ANDROID_ADB_SERVER_PORT": "5038", - "ADB_MDNS_AUTO_CONNECT": "", - "TFT_LAUNCHER": paths.runtimeTemplate.appendingPathComponent("run-tft-angle-opengl.command").path, - "TFT_GLTRANSPORT": "virtio-gpu-asg", - "TFT_EXPECTED_GLTRANSPORT_BASELINE": "pipe", - "TFT_AUDIO_ENABLED": "1", - "TFT_ASG_WRITE_STEP_SIZE": "16384", - "TFT_DISPLAY_SIZE": profile.displaySize, - "TFT_DISPLAY_DENSITY": "\(profile.density)", - "TFT_GAME_LANGUAGE": language.id, - "TFT_CPU_CORES": "\(cpuCores)", - "TFT_MEMORY_MB": "\(memoryMB)", - "TFT_UI_SCALE": uiScale, - "TFT_GRAPHICS_PROFILE": "osft", - "TFT_MVK_QUEUE_MODE": "async", - "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE": "64", - "MVK_CONFIG_FAST_MATH_ENABLED": "1", - "TFT_INPUT_BRIDGE_ENABLED": "0", - "TFT_ANGLE_OPENGL_APK": paths.overlayAPK.path, - "TFT_ANGLE_OPENGL_APK_SHA256": overlayHash, - "TFT_ORIGINAL_BASE_APK_SHA256": gameRelease.baseSHA256, - "TFT_ANGLE_OPENGL_PROFILE": graphicsProfile.path, - "TFT_ANGLE_OPENGL_PROFILE_SHA256": profileHash, - "TFT_ANGLE_DISABLED_FEATURES": "preferSubmitAtFBOBoundary" - ]) { _, new in new } - - outputPipe = pipe - process = runtime - eventBuffer.removeAll(keepingCapacity: true) - pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in - let data = handle.availableData - guard !data.isEmpty else { return } - self?.parsingQueue.async { self?.consume(data, events: events) } - } - runtime.terminationHandler = { [weak self] process in - self?.outputPipe?.fileHandleForReading.readabilityHandler = nil - DispatchQueue.main.async { - events(RuntimeEvent(event: .stopped, code: process.terminationStatus)) - } - } - try runtime.run() - } - - func stop() { - guard let process, process.isRunning else { return } - DispatchQueue.global(qos: .userInitiated).async { [paths] in - let environment = ["ANDROID_ADB_SERVER_PORT": "5038"] - _ = try? SystemServices.run(paths.adb, ["-s", "emulator-5582", "emu", "kill"], environment: environment) - } - process.terminate() - } - - private func consume(_ data: Data, events: @escaping EventHandler) { - eventBuffer.append(data) - while let newline = eventBuffer.firstIndex(of: 0x0A) { - let line = eventBuffer[.. ReleaseManifest { - let data = try Data(contentsOf: url) - let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: data) - try manifest.validate() - return manifest - } - - static func sha256(of url: URL) throws -> String { - guard let stream = InputStream(url: url) else { - throw LauncherError.integrity("Could not open \(url.lastPathComponent)") - } - stream.open() - defer { stream.close() } - - var hasher = SHA256() - var buffer = [UInt8](repeating: 0, count: 1024 * 1024) - while stream.hasBytesAvailable { - let count = stream.read(&buffer, maxLength: buffer.count) - if count < 0 { - throw stream.streamError ?? LauncherError.integrity("Could not read \(url.lastPathComponent)") - } - if count == 0 { break } - hasher.update(data: Data(buffer[0 ..< count])) - } - return hasher.finalize().map { String(format: "%02x", $0) }.joined() - } - - @discardableResult - static func run( - _ executable: URL, - _ arguments: [String], - environment: [String: String]? = nil, - currentDirectory: URL? = nil, - captureOutput: Bool = true, - log: URL? = nil - ) throws -> String { - let process = Process() - process.executableURL = executable - process.arguments = arguments - process.currentDirectoryURL = currentDirectory - if let environment { - process.environment = ProcessInfo.processInfo.environment.merging(environment) { _, new in new } - } - - let outputPipe = Pipe() - if captureOutput { - process.standardOutput = outputPipe - process.standardError = outputPipe - } else if let log { - let handle = try appendHandle(for: log) - process.standardOutput = handle - process.standardError = handle - } - try process.run() - process.waitUntilExit() - let output: String - if captureOutput { - output = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - } else { - output = "" - } - guard process.terminationStatus == 0 else { - let detail = output.trimmingCharacters(in: .whitespacesAndNewlines) - throw LauncherError.process(detail.isEmpty - ? "Command \(executable.lastPathComponent) exited with status \(process.terminationStatus)" - : detail) - } - return output - } - - static func appendHandle(for url: URL) throws -> FileHandle { - let fileManager = FileManager.default - try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) - if !fileManager.fileExists(atPath: url.path) { - fileManager.createFile(atPath: url.path, contents: nil) - } - let handle = try FileHandle(forWritingTo: url) - try handle.seekToEnd() - return handle - } - - static func appendLog(_ message: String, to url: URL) { - do { - let handle = try appendHandle(for: url) - defer { try? handle.close() } - let timestamp = ISO8601DateFormatter().string(from: Date()) - try handle.write( - contentsOf: Data( - "[\(timestamp)] [\(MacticianIdentity.loggingSubsystem)] \(message)\n".utf8 - ) - ) - } catch { - // Logging must never block launch or expose guest output elsewhere. - } - } - - static func loadState(from url: URL) -> InstallState { - guard let data = try? Data(contentsOf: url), - let state = try? JSONDecoder.launcher.decode(InstallState.self, from: data), - state.schemaVersion == 1 else { - return InstallState() - } - return state - } - - static func saveState(_ state: InstallState, to url: URL) throws { - let fileManager = FileManager.default - try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) - var updated = state - updated.updatedAt = Date() - let data = try JSONEncoder.launcher.encode(updated) - try data.write(to: url, options: .atomic) - } - - static func availableBytes(at url: URL) throws -> Int64 { - let values = try url.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]) - return values.volumeAvailableCapacityForImportantUsage ?? 0 - } - - static func checkHost(minimumFreeBytes: Int64, root: URL) throws { - guard ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 12 else { - throw LauncherError.preflight("macOS 12 or newer is required") - } - let architecture = try run(URL(fileURLWithPath: "/usr/bin/uname"), ["-m"]) - .trimmingCharacters(in: .whitespacesAndNewlines) - guard architecture == "arm64" else { - throw LauncherError.preflight("This launcher supports Apple Silicon Macs only") - } - guard ProcessInfo.processInfo.physicalMemory >= 15 * 1024 * 1024 * 1024 else { - throw LauncherError.preflight("This profile requires a Mac with 16 GB of memory") - } - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - let freeBytes = try availableBytes(at: root) - guard freeBytes >= minimumFreeBytes else { - let required = ByteCountFormatter.string(fromByteCount: minimumFreeBytes, countStyle: .file) - let available = ByteCountFormatter.string(fromByteCount: freeBytes, countStyle: .file) - throw LauncherError.preflight("Not enough disk space: \(required) required, \(available) available") - } - let hypervisor = try run(URL(fileURLWithPath: "/usr/sbin/sysctl"), ["-n", "kern.hv_support"]) - .trimmingCharacters(in: .whitespacesAndNewlines) - guard hypervisor == "1" else { - throw LauncherError.preflight("Hypervisor Framework is unavailable") - } - } -} - -extension JSONEncoder { - fileprivate static var launcher: JSONEncoder { - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] - encoder.dateEncodingStrategy = .iso8601 - return encoder - } -} - -extension JSONDecoder { - fileprivate static var launcher: JSONDecoder { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - return decoder - } -} diff --git a/launcher/Tests/InstallerIntegration.swift b/launcher/Tests/InstallerIntegration.swift deleted file mode 100644 index cfd089a..0000000 --- a/launcher/Tests/InstallerIntegration.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation - -@main -enum InstallerIntegration { - static func main() throws { - guard CommandLine.arguments.count == 6 else { - throw IntegrationFailure("usage: InstallerIntegration ROOT RESOURCES PLATFORM_ZIP EMULATOR_ZIP SYSTEM_ZIP") - } - let root = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true) - let resources = URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: true) - let paths = try LauncherPaths(root: root, resources: resources) - let manifest = try SystemServices.loadManifest(from: paths.manifest) - try FileManager.default.createDirectory(at: paths.downloads, withIntermediateDirectories: true) - - let archives = Array(CommandLine.arguments[3 ... 5]) - for (component, archivePath) in zip(manifest.components, archives) { - let source = URL(fileURLWithPath: archivePath) - let target = paths.downloads.appendingPathComponent("\(component.id).zip") - guard try SystemServices.sha256(of: source) == component.sha256 else { - throw IntegrationFailure("fixture hash mismatch: \(source.lastPathComponent)") - } - if FileManager.default.fileExists(atPath: target.path) { - try FileManager.default.removeItem(at: target) - } - try FileManager.default.copyItem(at: source, to: target) - } - - var result: Result? - let installer = InstallerService(paths: paths, manifest: manifest) - installer.install(repair: false, progress: { progress in - print("[\(Int(progress.fraction * 100))%] \(progress.message)") - }, completion: { completion in - result = completion - }) - while result == nil { - RunLoop.current.run(until: Date().addingTimeInterval(0.1)) - } - let state = try result!.get() - guard state.isReady, - state.gameBaseSHA256?.range( - of: "^[0-9a-f]{64}$", - options: .regularExpression - ) != nil, - FileManager.default.fileExists(atPath: paths.avdDirectory.appendingPathComponent("hardware-qemu.ini").path) else { - throw IntegrationFailure("installer did not produce a ready AVD") - } - print("Mactician provisioning integration: OK") - print("Integration root: \(root.path)") - } -} - -struct IntegrationFailure: Error, CustomStringConvertible { - let description: String - init(_ description: String) { self.description = description } -} diff --git a/launcher/Tests/LauncherTests.swift b/launcher/Tests/LauncherTests.swift deleted file mode 100644 index 25290b2..0000000 --- a/launcher/Tests/LauncherTests.swift +++ /dev/null @@ -1,1250 +0,0 @@ -import CryptoKit -import Foundation - -@main -enum LauncherTests { - static func main() throws { - guard CommandLine.arguments.count == 3 else { - throw TestFailure("usage: LauncherTests MANIFEST SOURCE_ROOT") - } - let manifestURL = URL(fileURLWithPath: CommandLine.arguments[1]) - let sourceRoot = URL(fileURLWithPath: CommandLine.arguments[2]) - let manifest = try SystemServices.loadManifest(from: manifestURL) - - try expect(manifest.schemaVersion == 1, "manifest schema") - try expect(manifest.components.count == 3, "component count") - try expect(manifest.game.apks.count == 4, "APK count") - let hostedPrivateKey = Curve25519.Signing.PrivateKey() - let hostedRelease = GameRelease( - packageName: "com.riotgames.league.teamfighttactics.pbe", - version: "18.2-test", - versionCode: 8_220_001, - baseSHA256: String(repeating: "a", count: 64), - apks: [ - GameAPK( - name: "base.apk", - size: 100, - sha256: String(repeating: "a", count: 64), - url: URL(string: "https://sergeinaumov.dev/mactician/updates/game/releases/aaaaaaaa/base.apk") - ), - GameAPK( - name: "split_config.arm64_v8a.apk", - size: 50, - sha256: String(repeating: "b", count: 64), - url: URL(string: "https://sergeinaumov.dev/mactician/updates/game/releases/aaaaaaaa/split_config.arm64_v8a.apk") - ) - ] - ) - let hostedPayload = try JSONEncoder().encode(HostedGameFeed( - schemaVersion: 1, - publishedAt: "2026-08-12T12:00:00Z", - release: hostedRelease - )) - let hostedSignature = try hostedPrivateKey.signature(for: hostedPayload) - let hostedEnvelope = try JSONEncoder().encode(HostedGameFeedEnvelope( - schemaVersion: 1, - payload: hostedPayload.base64EncodedString(), - signature: hostedSignature.base64EncodedString() - )) - let verifiedHostedFeed = try HostedGameUpdate.decodeAndVerify( - hostedEnvelope, - publicKeyBase64: hostedPrivateKey.publicKey.rawRepresentation.base64EncodedString() - ) - try expect(verifiedHostedFeed.release == hostedRelease, "signed hosted game feed") - var olderInstallState = InstallState() - olderInstallState.gameVersion = "18.1-old" - olderInstallState.gameVersionCode = 8_210_000 - try expect( - HostedGameUpdate.isNewer(hostedRelease, than: olderInstallState), - "newer hosted game version detection" - ) - var currentInstallState = olderInstallState - currentInstallState.gameVersion = hostedRelease.version - currentInstallState.gameVersionCode = hostedRelease.versionCode - try expect( - !HostedGameUpdate.isNewer(hostedRelease, than: currentInstallState), - "current hosted game version detection" - ) - var newerInstallState = currentInstallState - newerInstallState.gameVersionCode = 8_230_000 - try expect( - !HostedGameUpdate.isNewer(hostedRelease, than: newerInstallState), - "hosted game rollback is not an update" - ) - var invalidSignature = hostedSignature - invalidSignature[invalidSignature.startIndex] ^= 0x01 - let invalidEnvelope = try JSONEncoder().encode(HostedGameFeedEnvelope( - schemaVersion: 1, - payload: hostedPayload.base64EncodedString(), - signature: invalidSignature.base64EncodedString() - )) - do { - _ = try HostedGameUpdate.decodeAndVerify( - invalidEnvelope, - publicKeyBase64: hostedPrivateKey.publicKey.rawRepresentation.base64EncodedString() - ) - throw TestFailure("tampered hosted game feed was accepted") - } catch let error as LauncherError { - try expect(error == .integrity("The TFT PBE feed signature is invalid"), "tampered game feed rejection") - } - try expect( - manifest.profiles.map(\.id) == ["balanced", "quality", "ultra", "4k"], - "profile order" - ) - try expect( - manifest.profiles.map(\.title) == ["1080p", "1440p", "1800p", "4K"], - "resolution profile titles" - ) - try expect( - EffectsQuality.selection(saved: nil) == .high - && EffectsQuality.selection(saved: "unknown") == .high - && EffectsQuality.selection(saved: "performance") == .performance, - "effects quality selection" - ) - try expect(manifest.profiles[0].displaySize == "1920x1080", "balanced resolution") - try expect(manifest.profiles[0].displayResolution == "1920 × 1080", "display resolution formatting") - try expect(manifest.profiles[1].displaySize == "2560x1440", "quality resolution") - try expect(manifest.profiles[2].displaySize == "3200x1800", "ultra resolution") - try expect(manifest.profiles[2].density == 520, "ultra density") - try expect(manifest.profiles[3].displaySize == "3840x2160", "4K resolution") - try expect(manifest.profiles[3].density == 640, "4K density") - try expect( - BridgeHotkeyTarget.shop == BridgeRelativePoint(x: 0.96, y: 0.93) - && BridgeHotkeyTarget.shop.pixels(width: 3840, height: 2160) - == (3686, 2009), - "shop hotkey touch target" - ) - try expect( - BridgeHotkeyTarget.reroll == BridgeRelativePoint(x: 0.955, y: 0.79) - && BridgeHotkeyTarget.reroll.pixels(width: 3840, height: 2160) - == (3667, 1706), - "reroll hotkey touch target" - ) - try expect( - BridgeHotkeyTarget.buyXP == BridgeRelativePoint(x: 0.032, y: 0.925) - && BridgeHotkeyTarget.buyXP.pixels(width: 3840, height: 2160) - == (123, 1998), - "XP hotkey touch target" - ) - try expect( - BridgeKeyboardBinding.isActionKey(BridgeKeyboardBinding.tab) - && BridgeHotkeyTarget.traits.pixels(width: 3840, height: 2160) == (111, 86) - && BridgeHotkeyTarget.items.pixels(width: 3840, height: 2160) == (227, 86), - "Tab items-and-traits hotkey touch targets" - ) - try expect( - BridgeKeyboardBinding.isActionKey(BridgeKeyboardBinding.playersAndDamage) - && BridgeHotkeyTarget.damage.pixels(width: 3840, height: 2160) == (3636, 86) - && BridgeHotkeyTarget.players.pixels(width: 3840, height: 2160) == (3744, 86), - "V players-and-damage hotkey touch targets" - ) - let surfaceLayer = """ - RequestedLayerState{SurfaceView[com.riotgames.league.teamfighttactics.pbe/com.epicgames.unreal.GameActivity](BLAST)#103 parentId=102} - """ - try expect( - SurfaceFlingerFPS.gameLayer( - from: surfaceLayer, - package: "com.riotgames.league.teamfighttactics.pbe" - ) == "SurfaceView[com.riotgames.league.teamfighttactics.pbe/com.epicgames.unreal.GameActivity](BLAST)#103", - "FPS overlay SurfaceFlinger layer selection" - ) - let latencyOutput = """ - 16666666 - 0 1000000000 0 - 0 1016666667 0 - 0 1033333334 0 - 0 1050000001 0 - """ - let frameRate = SurfaceFlingerFPS.estimate( - timestamps: SurfaceFlingerFPS.presentationTimestamps(from: latencyOutput), - after: nil - ) - try expect( - frameRate.map { abs($0.framesPerSecond - 60) < 0.1 } == true - && frameRate?.newestTimestamp == 1_050_000_001, - "FPS overlay frame-rate calculation" - ) - var gameSessionTracker = GameSessionTracker() - let gameSessionStart = Date(timeIntervalSince1970: 1_000) - gameSessionTracker.start(at: gameSessionStart) - gameSessionTracker.start(at: gameSessionStart.addingTimeInterval(30)) - try expect( - gameSessionTracker.finish( - at: gameSessionStart.addingTimeInterval(90.9) - ) == 90 - && gameSessionTracker.finish(at: gameSessionStart.addingTimeInterval(100)) == nil, - "game session duration is recorded once from the first ready event" - ) - var shortGameSession = GameSessionTracker() - shortGameSession.start(at: gameSessionStart) - try expect( - shortGameSession.finish(at: gameSessionStart.addingTimeInterval(0.2)) == 1, - "short game sessions meet the telemetry minimum" - ) - let telemetrySettings = LauncherTelemetrySettings( - profile: manifest.profiles[1], - effectsQuality: .performance, - uiScalePercent: 125, - androidMemoryMB: 8_192, - androidCPUCores: 6 - ) - let telemetryDevice = LauncherTelemetryDevice( - modelIdentifier: "Mac16,1", - macOSVersion: "26.0.0", - physicalMemoryMB: 32_768, - logicalCPUCount: 10 - ) - try expect( - LauncherTelemetryDevice.normalizedModelIdentifier("Mac16,1") == "Mac16,1" - && LauncherTelemetryDevice.normalizedModelIdentifier("Mac name") == "unknown", - "telemetry device model identifier is bounded" - ) - let telemetryDefaultsName = "LauncherTests.telemetry.\(UUID().uuidString)" - guard let telemetryDefaults = UserDefaults(suiteName: telemetryDefaultsName) else { - throw TestFailure("telemetry UserDefaults suite") - } - defer { telemetryDefaults.removePersistentDomain(forName: telemetryDefaultsName) } - telemetryDefaults.set(UUID().uuidString, forKey: "telemetry.installationID.v1") - telemetryDefaults.set(Data("[]".utf8), forKey: "telemetry.pendingEvents.v1") - let telemetryLoader = TelemetryLoaderStub() - let telemetryService = LauncherTelemetryService( - defaults: telemetryDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - device: telemetryDevice, - loader: telemetryLoader.load - ) - try expect( - telemetryService.shouldShowNotice - && !telemetryService.isExtendedDiagnosticsEnabled - && telemetryDefaults.object(forKey: "telemetry.installationID.v1") == nil - && telemetryDefaults.object(forKey: "telemetry.pendingEvents.v1") == nil, - "legacy telemetry is removed and extended diagnostics default to off" - ) - telemetryService.completeNotice(extendedDiagnostics: false) - telemetryService.recordGameSession( - durationSeconds: 90, - launcherSettings: telemetrySettings, - endedAt: gameSessionStart.addingTimeInterval(90) - ) - guard let pendingEventData = telemetryDefaults.data( - forKey: "telemetry.firstSession.pending.v2" - ), - let pendingWrapper = try JSONSerialization.jsonObject(with: pendingEventData) - as? [String: Any], - let pendingEvent = pendingWrapper["event"] as? [String: Any], - let firstEventID = pendingEvent["event_id"] as? String else { - throw TestFailure("first-session telemetry payload") - } - try expect( - (pendingEvent["schema_version"] as? NSNumber)?.intValue == 2 - && pendingEvent["event"] as? String == "first_game_session" - && pendingEvent["occurred_on"] as? String == "1970-01-01" - && pendingEvent["duration_bucket"] as? String == "under_5m" - && Set(pendingEvent.keys) == Set([ - "schema_version", "event_id", "event", "occurred_on", - "duration_bucket", "launcher_version", "launcher_build" - ]) - && telemetryDefaults.data(forKey: "telemetry.extended.pendingEvents.v2") == nil, - "first-session telemetry is unlinkable and contains only allowed fields" - ) - telemetryService.recordGameSession( - durationSeconds: 3_700, - launcherSettings: telemetrySettings - ) - guard let repeatedPendingData = telemetryDefaults.data( - forKey: "telemetry.firstSession.pending.v2" - ), - let repeatedWrapper = try JSONSerialization.jsonObject(with: repeatedPendingData) - as? [String: Any], - let repeatedEvent = repeatedWrapper["event"] as? [String: Any] else { - throw TestFailure("repeated first-session telemetry payload") - } - try expect( - repeatedEvent["event_id"] as? String == firstEventID, - "later sessions reuse the single pending first-session event" - ) - try expect( - LauncherTelemetryService.durationBucket(for: 299) == "under_5m" - && LauncherTelemetryService.durationBucket(for: 300) == "5_15m" - && LauncherTelemetryService.durationBucket(for: 3_600) == "60_120m" - && LauncherTelemetryService.durationBucket(for: 14_400) == "120_240m" - && LauncherTelemetryService.durationBucket(for: 14_401) == "over_240m", - "telemetry duration buckets" - ) - try waitFor("initial telemetry request") { telemetryLoader.requestCount == 1 } - telemetryLoader.completeFirst(statusCode: 503) - let retryLoader = TelemetryLoaderStub() - let retryService = LauncherTelemetryService( - defaults: telemetryDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - device: telemetryDevice, - loader: retryLoader.load - ) - try waitFor("retried telemetry request") { retryLoader.requestCount == 1 } - try expect( - retryLoader.firstEventID == firstEventID, - "network retry preserves the first-session event ID" - ) - retryLoader.completeFirst(statusCode: 202) - try waitFor("successful first-session completion") { - telemetryDefaults.bool(forKey: "telemetry.firstSession.completed.v2") - && telemetryDefaults.data(forKey: "telemetry.firstSession.pending.v2") == nil - } - retryService.recordGameSession( - durationSeconds: 180, - launcherSettings: telemetrySettings - ) - try expect( - retryLoader.requestCount == 1 - && telemetryDefaults.data(forKey: "telemetry.firstSession.pending.v2") == nil, - "completed first-session telemetry is never recreated" - ) - - let diagnosticsDefaultsName = "LauncherTests.diagnostics.\(UUID().uuidString)" - guard let diagnosticsDefaults = UserDefaults(suiteName: diagnosticsDefaultsName) else { - throw TestFailure("diagnostics UserDefaults suite") - } - defer { diagnosticsDefaults.removePersistentDomain(forName: diagnosticsDefaultsName) } - diagnosticsDefaults.set(true, forKey: "telemetry.firstSession.completed.v2") - let diagnosticsService = LauncherTelemetryService( - defaults: diagnosticsDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - device: telemetryDevice, - loader: TelemetryLoaderStub().load - ) - diagnosticsService.completeNotice(extendedDiagnostics: true) - diagnosticsService.recordGameSession( - durationSeconds: 2_871, - launcherSettings: telemetrySettings, - endedAt: gameSessionStart.addingTimeInterval(2_871) - ) - guard let diagnosticsData = diagnosticsDefaults.data( - forKey: "telemetry.extended.pendingEvents.v2" - ), - let diagnosticsEvents = try JSONSerialization.jsonObject(with: diagnosticsData) - as? [[String: Any]], - let diagnosticsEvent = diagnosticsEvents.first, - let settingsPayload = diagnosticsEvent["launcher_settings"] as? [String: Any], - let devicePayload = diagnosticsEvent["device"] as? [String: Any] else { - throw TestFailure("extended diagnostics payload") - } - try expect( - diagnosticsEvent["event"] as? String == "game_session_diagnostics" - && (diagnosticsEvent["consent_version"] as? NSNumber)?.intValue == 1 - && (diagnosticsEvent["duration_seconds"] as? NSNumber)?.int64Value == 2_871 - && settingsPayload["profile_id"] as? String == "quality" - && settingsPayload["effects_quality_id"] as? String == "performance" - && settingsPayload["game_language"] == nil - && (settingsPayload["guest_memory_mb"] as? NSNumber)?.intValue == 8_192 - && (settingsPayload["guest_cpu_cores"] as? NSNumber)?.intValue == 6 - && devicePayload["model_identifier"] as? String == "Mac16,1" - && diagnosticsEvent["installation_id"] == nil, - "consented diagnostics include applied settings without a persistent identifier" - ) - diagnosticsService.setExtendedDiagnosticsEnabled(false) - try expect( - !diagnosticsService.isExtendedDiagnosticsEnabled - && diagnosticsDefaults.data(forKey: "telemetry.extended.pendingEvents.v2") == nil, - "withdrawing consent synchronously clears diagnostics" - ) - - let oldConsentDefaultsName = "LauncherTests.old-consent.\(UUID().uuidString)" - guard let oldConsentDefaults = UserDefaults(suiteName: oldConsentDefaultsName) else { - throw TestFailure("old consent UserDefaults suite") - } - defer { oldConsentDefaults.removePersistentDomain(forName: oldConsentDefaultsName) } - oldConsentDefaults.set("granted", forKey: "telemetry.extendedConsent.state.v1") - oldConsentDefaults.set(0, forKey: "telemetry.extendedConsent.version.v1") - oldConsentDefaults.set(true, forKey: "telemetry.noticeShown.v1") - oldConsentDefaults.set(Data("[]".utf8), forKey: "telemetry.extended.pendingEvents.v2") - let oldConsentService = LauncherTelemetryService( - defaults: oldConsentDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - loader: TelemetryLoaderStub().load - ) - try expect( - oldConsentService.shouldShowNotice - && !oldConsentService.isExtendedDiagnosticsEnabled - && oldConsentDefaults.data(forKey: "telemetry.extended.pendingEvents.v2") == nil, - "a consent-version change stops diagnostics and requests consent again" - ) - - let duplicateDefaultsName = "LauncherTests.duplicate.\(UUID().uuidString)" - guard let duplicateDefaults = UserDefaults(suiteName: duplicateDefaultsName) else { - throw TestFailure("duplicate UserDefaults suite") - } - defer { duplicateDefaults.removePersistentDomain(forName: duplicateDefaultsName) } - let duplicateLoader = TelemetryLoaderStub() - let duplicateService = LauncherTelemetryService( - defaults: duplicateDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - loader: duplicateLoader.load - ) - duplicateService.completeNotice(extendedDiagnostics: false) - duplicateService.recordGameSession( - durationSeconds: 600, - launcherSettings: telemetrySettings - ) - try waitFor("duplicate telemetry request") { duplicateLoader.requestCount == 1 } - duplicateLoader.completeFirst(statusCode: 409) - try waitFor("duplicate telemetry acknowledgement") { - duplicateDefaults.bool(forKey: "telemetry.firstSession.completed.v2") - && duplicateDefaults.data(forKey: "telemetry.firstSession.pending.v2") == nil - } - - let staleDefaultsName = "LauncherTests.stale.\(UUID().uuidString)" - guard let staleDefaults = UserDefaults(suiteName: staleDefaultsName) else { - throw TestFailure("stale UserDefaults suite") - } - defer { staleDefaults.removePersistentDomain(forName: staleDefaultsName) } - let staleLoader = TelemetryLoaderStub() - let staleService = LauncherTelemetryService( - defaults: staleDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - loader: staleLoader.load - ) - staleService.completeNotice(extendedDiagnostics: false) - staleService.recordGameSession( - durationSeconds: 600, - launcherSettings: telemetrySettings - ) - guard let staleData = staleDefaults.data(forKey: "telemetry.firstSession.pending.v2"), - var staleWrapper = try JSONSerialization.jsonObject(with: staleData) - as? [String: Any] else { - throw TestFailure("stale first-session payload") - } - staleWrapper["created_at"] = "2020-01-01T00:00:00Z" - staleDefaults.set( - try JSONSerialization.data(withJSONObject: staleWrapper), - forKey: "telemetry.firstSession.pending.v2" - ) - let staleRetryLoader = TelemetryLoaderStub() - let staleRetryService = LauncherTelemetryService( - defaults: staleDefaults, - apiBaseURL: URL(string: "https://127.0.0.1:1/")!, - loader: staleRetryLoader.load - ) - staleRetryService.recordGameSession( - durationSeconds: 900, - launcherSettings: telemetrySettings - ) - try expect( - staleDefaults.bool(forKey: "telemetry.firstSession.completed.v2") - && staleDefaults.data(forKey: "telemetry.firstSession.pending.v2") == nil - && staleRetryLoader.requestCount == 0, - "a first-session event older than seven days is terminally discarded" - ) - let messageURL = LauncherTelemetryService.messageURL( - apiBaseURL: URL(string: "https://sergeinaumov.dev/mactician/api/")!, - trigger: .gameClosed, - launcherVersion: "1.0.0" - ) - try expect( - messageURL?.absoluteString - == "https://sergeinaumov.dev/mactician/api/v1/messages?trigger=game_closed&version=1.0.0", - "message endpoint query encoding" - ) - let gameStoppedEvent = try JSONDecoder().decode( - RuntimeEvent.self, - from: Data(#"{"event":"game_stopped","code":0}"#.utf8) - ) - try expect( - gameStoppedEvent.event == .gameStopped, - "runtime game-closed event decoding" - ) - let gameActivityOutput = """ - Display #0: - topResumedActivity=ActivityRecord{abc123 u0 com.riotgames.league.teamfighttactics.pbe/com.epicgames.unreal.GameActivity t42} - """ - let loginActivityOutput = """ - mResumedActivity: ActivityRecord{old u0 com.riotgames.league.teamfighttactics.pbe/com.epicgames.unreal.GameActivity t41} - topResumedActivity=ActivityRecord{def456 u0 com.riotgames.league.teamfighttactics.pbe/com.riotgames.platformui.MobileFREWebViewActivity t42} - """ - try expect( - BridgeAndroidActivityClassifier.classify(dumpsysOutput: gameActivityOutput) == .gameplay - && BridgeAndroidActivityClassifier.classify(dumpsysOutput: loginActivityOutput) - == .nonGameplay - && BridgeAndroidActivityClassifier.classify( - dumpsysOutput: "topResumedActivity=null" - ) == .unknown, - "input bridge foreground activity classification" - ) - try expect( - BridgeKeyboardEventPolicy.disposition( - for: 44, - activity: .gameplay, - modifierFlags: [] - ) == .consume - && BridgeKeyboardEventPolicy.disposition( - for: BridgeKeyboardBinding.shop, - activity: .gameplay, - modifierFlags: [.shift] - ) == .hotkey - && BridgeKeyboardEventPolicy.disposition( - for: 44, - activity: .gameplay, - modifierFlags: [.command] - ) == .passThrough - && BridgeKeyboardEventPolicy.disposition( - for: 44, - activity: .gameplay, - modifierFlags: [.control] - ) == .passThrough - && BridgeKeyboardEventPolicy.disposition( - for: 44, - activity: .gameplay, - modifierFlags: [.option] - ) == .passThrough - && BridgeKeyboardEventPolicy.disposition( - for: 44, - activity: .nonGameplay, - modifierFlags: [] - ) == .passThrough - && BridgeKeyboardEventPolicy.disposition( - for: 44, - activity: .unknown, - modifierFlags: [] - ) == .passThrough, - "input bridge keyboard event policy" - ) - var bridgeSessionGeneration = BridgeSessionGeneration() - let stoppedGeneration = bridgeSessionGeneration.advance() - let activeGeneration = bridgeSessionGeneration.advance() - try expect( - !bridgeSessionGeneration.accepts(stoppedGeneration) - && bridgeSessionGeneration.accepts(activeGeneration), - "stale input bridge session status is rejected" - ) - var audioRecoveryPolicy = EmulatorAudioRecoveryPolicy( - startupDelay: 100, - settleDelay: 0.75, - cooldown: 2 - ) - let initialWindowSize = EmulatorWindowSize(width: 1_920, height: 1_120) - let resizedWindowSize = EmulatorWindowSize(width: 1_600, height: 940) - let finalWindowSize = EmulatorWindowSize(width: 1_440, height: 850) - try expect( - audioRecoveryPolicy.observe(size: initialWindowSize, at: 10) == nil - && audioRecoveryPolicy.observe(size: resizedWindowSize, at: 10.1) == nil - && audioRecoveryPolicy.observe(size: finalWindowSize, at: 10.5) == nil - && audioRecoveryPolicy.observe(size: finalWindowSize, at: 11.24) == nil - && audioRecoveryPolicy.observe(size: finalWindowSize, at: 11.25) == .windowResize - && audioRecoveryPolicy.observe(size: finalWindowSize, at: 12) == nil, - "audio recovery is debounced until window resizing settles" - ) - try expect( - audioRecoveryPolicy.observe(size: resizedWindowSize, at: 12.5) == nil - && audioRecoveryPolicy.observe(size: resizedWindowSize, at: 13.24) == nil - && audioRecoveryPolicy.observe(size: initialWindowSize, at: 13.5) == nil - && audioRecoveryPolicy.observe(size: initialWindowSize, at: 14.25) == .windowResize, - "audio recovery cooldown defers repeated window changes" - ) - var startupAudioRecoveryPolicy = EmulatorAudioRecoveryPolicy( - startupDelay: 1.5, - settleDelay: 0.75, - cooldown: 2 - ) - try expect( - startupAudioRecoveryPolicy.observe(size: initialWindowSize, at: 20) == nil - && startupAudioRecoveryPolicy.observe(size: initialWindowSize, at: 21.49) == nil - && startupAudioRecoveryPolicy.observe(size: initialWindowSize, at: 21.5) == .startup - && startupAudioRecoveryPolicy.observe(size: initialWindowSize, at: 24) == nil, - "audio recovery runs once after game startup" - ) - var halFailureRecoveryPolicy = EmulatorAudioRecoveryPolicy( - startupDelay: 100, - settleDelay: 0.75, - cooldown: 2, - failureThreshold: 3, - failureWindow: 0.5 - ) - try expect( - halFailureRecoveryPolicy.observeHALWriteFailure(at: 30) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 30.2) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 30.4) == .halWriteFailure - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 30.6) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 30.8) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 31) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 32.4) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 32.55) == nil - && halFailureRecoveryPolicy.observeHALWriteFailure(at: 32.7) == .halWriteFailure, - "repeated Audio HAL failures trigger recovery with cooldown" - ) - try expect( - EmulatorAudioFailureClassifier.isHALWriteFailure( - "E/android.hardware.audio@7.1-impl.ranchu: pcmWrite:260 failure: -1" - ) - && EmulatorAudioFailureClassifier.isHALWriteFailure( - "pcm_writei failed with 'cannot read/write stream data: I/O error' (-1)" - ) - && !EmulatorAudioFailureClassifier.isHALWriteFailure( - "AudioSystem: AudioFlinger server died!" - ), - "Audio HAL write failure classification" - ) - - let infoPlistData = try Data(contentsOf: sourceRoot.appendingPathComponent("Info.plist")) - guard let infoPlist = try PropertyListSerialization.propertyList(from: infoPlistData, format: nil) - as? [String: Any] else { - throw TestFailure("invalid Info.plist") - } - try expect(infoPlist["CFBundleDevelopmentRegion"] as? String == "en", "launcher language") - try expect( - infoPlist["CFBundleLocalizations"] as? [String] == ["en", "ru"], - "launcher localizations" - ) - try expect(infoPlist["CFBundleDisplayName"] as? String == "Mactician", "app display name") - try expect(infoPlist["CFBundleName"] as? String == "Mactician", "app bundle name") - try expect(infoPlist["CFBundleExecutable"] as? String == "Mactician", "app executable") - try expect( - infoPlist["CFBundleIdentifier"] as? String == MacticianIdentity.bundleIdentifier, - "app bundle identifier" - ) - try expect(infoPlist["CFBundleIconFile"] as? String == "Mactician.icns", "launcher icon name") - try expect(infoPlist["CFBundleShortVersionString"] as? String == "1.0.4", "launcher version") - try expect(infoPlist["CFBundleVersion"] as? String == "40", "launcher build") - try expect( - infoPlist["SUFeedURL"] as? String == "https://sergeinaumov.dev/mactician/updates/appcast.xml", - "Sparkle appcast URL" - ) - try expect( - infoPlist["SUPublicEDKey"] as? String == "77t8YuvP4mvvP/3oMpVR/TqGRMCcUlrpWFIZGcWqokY=", - "Sparkle public key" - ) - try expect( - infoPlist["SUEnableAutomaticChecks"] as? Bool == true - && infoPlist["SUAllowsAutomaticUpdates"] as? Bool == true - && infoPlist["SUAutomaticallyUpdate"] as? Bool == false - && infoPlist["SUScheduledCheckInterval"] as? Int == 86_400, - "Sparkle automatic check policy" - ) - try expect( - infoPlist["SUVerifyUpdateBeforeExtraction"] as? Bool == true, - "Sparkle pre-extraction verification policy" - ) - let updateControllerSource = try String( - contentsOf: sourceRoot.appendingPathComponent("Sources/LauncherUpdateController.swift"), - encoding: .utf8 - ) - try expect( - updateControllerSource.contains("updaterController.updater.automaticallyChecksForUpdates") - && updateControllerSource.contains("updaterController.updater.checkForUpdatesInBackground()"), - "Sparkle update check on every launch" - ) - let appTransportSecurity = infoPlist["NSAppTransportSecurity"] as? [String: Any] - try expect( - appTransportSecurity?["NSAllowsLocalNetworking"] as? Bool == true, - "local WebView DevTools networking" - ) - try expect( - FileManager.default.fileExists( - atPath: sourceRoot.appendingPathComponent("Resources/Mactician.icns").path - ), - "launcher icon resource" - ) - try expect( - FileManager.default.fileExists( - atPath: sourceRoot.appendingPathComponent("Resources/EmulatorIcon.icns").path - ), - "emulator icon resource" - ) - let launcherIcon = try Data( - contentsOf: sourceRoot.appendingPathComponent("Resources/Mactician.icns") - ) - let emulatorIcon = try Data( - contentsOf: sourceRoot.appendingPathComponent("Resources/EmulatorIcon.icns") - ) - try expect( - launcherIcon != emulatorIcon - && FileManager.default.fileExists( - atPath: sourceRoot.appendingPathComponent( - "Resources/EmulatorIcon-1024.png" - ).path - ), - "active game icon is distinct from the launcher icon" - ) - try expect( - FileManager.default.fileExists( - atPath: sourceRoot.appendingPathComponent("Resources/QEMU-Hypervisor.entitlements").path - ), - "QEMU hypervisor entitlement resource" - ) - try expect( - EmulatorBrandingPatch.iconInstructionOffset == 0x5af688, - "QEMU icon patch offset" - ) - try expect( - EmulatorBrandingPatch.sourceIconInstruction == Data([0xf4, 0xbf, 0x4d, 0x94]) - && EmulatorBrandingPatch.patchedIconInstruction == Data([0x1f, 0x20, 0x03, 0xd5]), - "QEMU icon patch instructions" - ) - try expect( - EmulatorBrandingPatch.titleFormatOffset == 0x1a1e891 - && EmulatorBrandingPatch.sourceTitleFormat - == Data("%s Emulator - %s:%d\0".utf8) - && EmulatorBrandingPatch.patchedTitleFormat - == Data("Mactician: TFT PBE\0\0".utf8), - "QEMU window title patch" - ) - let emulatorHostData = try Data( - contentsOf: sourceRoot.appendingPathComponent("Resources/EmulatorHost-Info.plist") - ) - guard let emulatorHostInfo = try PropertyListSerialization.propertyList( - from: emulatorHostData, - format: nil - ) as? [String: Any] else { - throw TestFailure("invalid emulator host Info.plist") - } - try expect( - emulatorHostInfo["CFBundleIconFile"] as? String == "EmulatorIcon.icns", - "emulator host icon name" - ) - try expect( - emulatorHostInfo["CFBundleShortVersionString"] as? String == "1.0.4", - "emulator host version" - ) - try expect(emulatorHostInfo["CFBundleVersion"] as? String == "40", "emulator host build") - try expect( - emulatorHostInfo["CFBundleIdentifier"] as? String - == "dev.sergeinaumov.mactician.game-host", - "game host bundle identifier" - ) - try expect(MacticianIdentity.appName == "Mactician", "application name") - try expect( - MacticianIdentity.userDefaultsDomain == "dev.sergeinaumov.mactician", - "UserDefaults domain" - ) - try expect( - MacticianIdentity.keychainService == "dev.sergeinaumov.mactician", - "Keychain service" - ) - try expect( - MacticianIdentity.loggingSubsystem == "dev.sergeinaumov.mactician", - "logging subsystem" - ) - let supportRoot = LauncherPaths.defaultRoot( - applicationSupport: URL(fileURLWithPath: "/tmp/Application Support", isDirectory: true) - ) - try expect( - supportRoot.path == "/tmp/Application Support/Mactician", - "fresh Application Support root" - ) - - try expect(HostSizing.guestCPUCores(logicalCPUCount: 4) == 4, "minimum CPU count") - try expect(HostSizing.guestCPUCores(logicalCPUCount: 8) == 6, "M1 CPU count") - try expect(HostSizing.guestCPUCores(logicalCPUCount: 12) == 6, "maximum CPU count") - try expect(HostSizing.guestCPUList(logicalCPUCount: 8) == "0-5", "CPU list") - try expect(HostSizing.guestCPUMask(logicalCPUCount: 8) == "3f", "CPU mask") - try expect( - GuestResourceOptions.memoryMB(physicalMemoryBytes: 16 * 1024 * 1024 * 1024) - == [4096, 6144, 8192, 10240, 12288], - "16 GB host RAM choices" - ) - try expect( - GuestResourceOptions.memoryMB(physicalMemoryBytes: 32 * 1024 * 1024 * 1024).last == 16384, - "guest RAM cap" - ) - try expect( - GuestResourceOptions.cpuCores(logicalCPUCount: 8) == Array(2 ... 8), - "8-core host vCPU choices" - ) - try expect( - GuestResourceOptions.selection(saved: 8, options: [4, 6, 8], fallback: 6) == 8 - && GuestResourceOptions.selection(saved: 12, options: [4, 6, 8], fallback: 6) == 6, - "saved guest resource validation" - ) - try expect( - InterfaceScaleOptions.percentages == [100, 125, 150, 175, 200] - && InterfaceScaleOptions.selection(saved: 175) == 175 - && InterfaceScaleOptions.selection(saved: 225) == 100, - "UI scale choices and saved selection validation" - ) - try expect( - InterfaceScaleOptions.runtimeValue(percent: 175) == "1.75" - && InterfaceScaleOptions.runtimeValue(percent: 200) == "2.0" - && InterfaceScaleOptions.runtimeValue(percent: 225) == nil, - "UI scale runtime value" - ) - try expect( - GuestResourceOptions.recommended( - physicalMemoryBytes: 32 * 1024 * 1024 * 1024, - logicalCPUCount: 10 - ) == GuestResourceConfiguration(memoryMB: 8192, cpuCores: 6), - "M1 Max resource recommendation" - ) - try expect( - GuestResourceOptions.recommended( - physicalMemoryBytes: 16 * 1024 * 1024 * 1024, - logicalCPUCount: 8 - ) == GuestResourceConfiguration(memoryMB: 6144, cpuCores: 6), - "16 GB resource recommendation" - ) - try expect(GameLanguage.supported.count == 23, "supported game language count") - try expect(GameLanguage.language(withID: "ru-RU").title == "Russian", "Russian game language") - try expect(GameLanguage.language(withID: "unsupported") == .english, "language fallback") - try expect( - LauncherMetadata.gameDisplayVersion(from: manifest.game.version) == "18.1", - "manifest-derived game display version" - ) - try expect( - LauncherMetadata.totalDownloadBytes(in: manifest) - == manifest.components.reduce(Int64(0)) { $0 + $1.size }, - "manifest-derived download size" - ) - try expect( - !LauncherMetadata.byteCount(LauncherMetadata.totalDownloadBytes(in: manifest)).isEmpty, - "download size formatting" - ) - try expect( - LauncherMetadata.androidAPILevel(in: manifest) == "36" - && LauncherMetadata.componentVersion("emulator", in: manifest) == "37.1.11", - "manifest-derived component metadata" - ) - try expect( - LauncherFailurePresentation.recoveryAction(for: .installation) == .retryInstallation - && LauncherFailurePresentation.recoveryAction(for: .launch) == .tryLaunchAgain - && LauncherFailurePresentation.recoveryAction(for: .runtime) == .restartGame - && LauncherFailurePresentation.recoveryAction(for: .validation) == .repairInstallation - && LauncherFailurePresentation.recoveryAction(for: .reset) == .none, - "failure recovery mapping" - ) - try expect( - InstallerCompletionPresentation.isUserCancellation( - requested: true, - error: LauncherError.process("late completion") - ) - && InstallerCompletionPresentation.isUserCancellation( - requested: false, - error: LauncherError.cancelled - ) - && !InstallerCompletionPresentation.isUserCancellation( - requested: false, - error: LauncherError.process("failed") - ), - "user cancellation completion mapping" - ) - try expect( - LauncherHotkeyPresentation.status(for: .init( - accessibilityTrusted: false, - eventTapActive: false, - eventTapAttemptFailed: false - )) == .permissionRequired - && LauncherHotkeyPresentation.status(for: .init( - accessibilityTrusted: false, - eventTapActive: true, - eventTapAttemptFailed: false - )) == .active - && LauncherHotkeyPresentation.status(for: .init( - accessibilityTrusted: true, - eventTapActive: false, - eventTapAttemptFailed: false - )) == .ready - && LauncherHotkeyPresentation.status(for: .init( - accessibilityTrusted: true, - eventTapActive: true, - eventTapAttemptFailed: false - )) == .active - && LauncherHotkeyPresentation.status(for: .init( - accessibilityTrusted: true, - eventTapActive: false, - eventTapAttemptFailed: true - )) == .unavailable, - "hotkey permission presentation mapping" - ) - try expect( - InstallerService.unrealCommandLine(for: GameLanguage.language(withID: "ru-RU")) - .contains("-culture=ru-RU"), - "Unreal culture argument" - ) - try expect( - InstallerService.adbArguments(["devices"]) == ["-P", "5038", "devices"], - "isolated ADB server arguments" - ) - let gameInstallArguments = InstallerService.gameInstallArguments(apkPaths: ["base.apk", "split.apk"]) - try expect( - gameInstallArguments == [ - "-P", "5038", "-s", "emulator-5582", "install-multiple", - "--no-streaming", "-r", "-g", "base.apk", "split.apk" - ], - "persistent split APK installation" - ) - try expect( - InstallerService.emulatorArguments(initializeData: true, logicalCPUCount: 8).contains("-wipe-data"), - "first boot initializes userdata" - ) - let provisioningArguments = InstallerService.emulatorArguments( - initializeData: false, - logicalCPUCount: 8 - ) - try expect( - provisioningArguments.contains("-dns-server") - && provisioningArguments.contains("1.1.1.1,8.8.8.8"), - "public DNS avoids local network access" - ) - try expect( - !InstallerService.emulatorArguments(initializeData: false, logicalCPUCount: 8).contains("-wipe-data"), - "normal boot preserves userdata" - ) - - let temporary = FileManager.default.temporaryDirectory - .appendingPathComponent("tft-launcher-tests-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: temporary) } - try FileManager.default.createDirectory(at: temporary, withIntermediateDirectories: true) - let abc = temporary.appendingPathComponent("abc.txt") - try Data("abc".utf8).write(to: abc) - try expect( - try SystemServices.sha256(of: abc) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", - "streaming SHA-256" - ) - - let overlaySourceRoot = temporary.appendingPathComponent("overlay-source", isDirectory: true) - let overlayAssets = overlaySourceRoot.appendingPathComponent("assets", isDirectory: true) - let overlaySource = temporary.appendingPathComponent("base-fixture.apk") - let overlayDestination = temporary.appendingPathComponent("base-overlay.apk") - let overlayStaging = temporary.appendingPathComponent("overlay-staging", isDirectory: true) - try FileManager.default.createDirectory(at: overlayAssets, withIntermediateDirectories: true) - try Data("original\n".utf8).write(to: overlayAssets.appendingPathComponent("UECommandLine.txt")) - try Data("fixture".utf8).write(to: overlaySourceRoot.appendingPathComponent("payload.bin")) - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/zip"), - ["-q", "-r", overlaySource.path, "assets", "payload.bin"], - currentDirectory: overlaySourceRoot - ) - let overlayHash = try InstallerService.prepareOverlay( - source: overlaySource, - expectedSourceSHA256: try SystemServices.sha256(of: overlaySource), - destination: overlayDestination, - stagingRoot: overlayStaging, - language: GameLanguage.language(withID: "ru-RU") - ) - try expect( - overlayHash == SystemServices.sha256(of: overlayDestination), - "prepared overlay hash" - ) - try expect( - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/unzip"), - ["-p", overlayDestination.path, "assets/UECommandLine.txt"] - ) == InstallerService.unrealCommandLine(for: GameLanguage.language(withID: "ru-RU")), - "prepared overlay culture" - ) - - var state = InstallState() - state.stage = .avdCreated - state.installedComponents = ["emulator": "37.1.11"] - let stateURL = temporary.appendingPathComponent("install-state.json") - try SystemServices.saveState(state, to: stateURL) - let restored = SystemServices.loadState(from: stateURL) - try expect(restored.stage == .avdCreated, "state stage") - try expect(restored.installedComponents == state.installedComponents, "state components") - try Data("truncated".utf8).write(to: stateURL, options: .atomic) - try expect(SystemServices.loadState(from: stateURL).stage == .empty, "corrupt state fallback") - - let preparedRoot = temporary.appendingPathComponent("prepared-data", isDirectory: true) - let preparedPaths = try LauncherPaths( - root: preparedRoot, - resources: sourceRoot.appendingPathComponent("Resources", isDirectory: true) - ) - try InstallerService.prepareDirectories(at: preparedPaths) - try expect( - FileManager.default.fileExists(atPath: preparedPaths.avdHome.path), - "AVD parent directory" - ) - try expect( - FileManager.default.fileExists(atPath: preparedPaths.staging.path), - "staging directory" - ) - - let runtimeResources = temporary.appendingPathComponent("runtime-resources", isDirectory: true) - let runtimeTemplate = runtimeResources.appendingPathComponent("RuntimeTemplate", isDirectory: true) - let runtimeRoot = temporary.appendingPathComponent("runtime-data", isDirectory: true) - try FileManager.default.createDirectory(at: runtimeTemplate, withIntermediateDirectories: true) - try Data("new runtime".utf8).write(to: runtimeTemplate.appendingPathComponent("marker.txt")) - let runtimePaths = try LauncherPaths(root: runtimeRoot, resources: runtimeResources) - try FileManager.default.createDirectory(at: runtimePaths.runtimeProject, withIntermediateDirectories: true) - try Data("old runtime".utf8).write(to: runtimePaths.runtimeProject.appendingPathComponent("old.txt")) - try InstallerService.refreshRuntimeProject(at: runtimePaths) - try expect( - try String( - contentsOf: runtimePaths.runtimeProject.appendingPathComponent("marker.txt"), - encoding: .utf8 - ) == "new runtime", - "runtime refresh installs the bundled template" - ) - try expect( - !FileManager.default.fileExists(atPath: runtimePaths.runtimeProject.appendingPathComponent("old.txt").path), - "runtime refresh replaces the previous template" - ) - - let rootRuntimeScript = try String( - contentsOf: sourceRoot.deletingLastPathComponent().appendingPathComponent("run-tft-root-affinity.command"), - encoding: .utf8 - ) - try expect( - rootRuntimeScript.contains("Empty TFT streaming manifest detected") - && rootRuntimeScript.contains("rm -rf \"$STREAMING_INSTALL_DIR\""), - "empty streaming manifest self-heal" - ) - try expect( - rootRuntimeScript.contains("ApplicationScale=\" scale") - && rootRuntimeScript.contains("TFT_UI_SCALE"), - "Unreal UI scale launch configuration" - ) - - let component = manifest.components[0] - let partial = temporary.appendingPathComponent("partial.zip") - let curlArguments = InstallerService.curlArguments(component: component, destination: partial) - try expect(curlArguments.contains("--continue-at"), "resumable curl option") - try expect(curlArguments.contains("-"), "resume from current byte") - try expect(curlArguments.last == partial.path, "download destination") - - let archiveSource = temporary.appendingPathComponent("archive-source", isDirectory: true) - let archiveDestination = temporary.appendingPathComponent("archive-destination", isDirectory: true) - let archive = temporary.appendingPathComponent("fixture.zip") - try FileManager.default.createDirectory(at: archiveSource, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: archiveDestination, withIntermediateDirectories: true) - try Data("payload".utf8).write(to: archiveSource.appendingPathComponent("payload.txt")) - try SystemServices.run( - URL(fileURLWithPath: "/usr/bin/zip"), - ["-q", archive.path, "payload.txt"], - currentDirectory: archiveSource - ) - try InstallerService.extractArchive(archive, to: archiveDestination) - try expect( - String(contentsOf: archiveDestination.appendingPathComponent("payload.txt"), encoding: .utf8) == "payload", - "ZIP extraction" - ) - - let unsafe = SDKComponent( - id: "unsafe", - version: "1", - url: URL(string: "https://example.com/file.zip")!, - size: 1, - sha256: String(repeating: "0", count: 64), - archiveRoot: "../escape", - installPath: "/tmp/escape" - ) - do { - try unsafe.validate() - throw TestFailure("unsafe component accepted") - } catch is LauncherError { - // Expected. - } - - let sourceFiles = try recursiveFiles(at: sourceRoot) - for localization in ["en", "ru"] { - let strings = sourceRoot.appendingPathComponent( - "Resources/\(localization).lproj/Localizable.strings" - ) - try expect( - FileManager.default.fileExists(atPath: strings.path), - "\(localization) localization resource" - ) - guard let localizedValues = try PropertyListSerialization.propertyList( - from: Data(contentsOf: strings), - format: nil - ) as? [String: String] else { - throw TestFailure("invalid \(localization) localization") - } - let descriptor = localizedValues["about.descriptor"] ?? "" - try expect(!descriptor.isEmpty, "\(localization) Mactician descriptor") - if localization == "en" { - try expect( - descriptor == "TFT PBE launcher for Apple Silicon", - "English Mactician descriptor" - ) - } - } - let localizationDirectories = try FileManager.default.contentsOfDirectory( - at: sourceRoot.appendingPathComponent("Resources"), - includingPropertiesForKeys: nil - ).filter { - $0.pathExtension == "lproj" - }.map { - $0.deletingPathExtension().lastPathComponent - }.sorted() - try expect( - localizationDirectories == ["en", "ru"], - "Mactician localization resources" - ) - let launcherModelSource = try String( - contentsOf: sourceRoot.appendingPathComponent("Sources/LauncherModel.swift"), - encoding: .utf8 - ) - for defaultsKey in [ - "launchProfile", "effectsQuality", "gameLanguage", "androidMemoryMB", - "androidCPUCores", "uiScalePercent" - ] { - try expect(launcherModelSource.contains("\"\(defaultsKey)\""), "UserDefaults key \(defaultsKey)") - } - let appSource = try String( - contentsOf: sourceRoot.appendingPathComponent("Sources/MacticianApp.swift"), - encoding: .utf8 - ) - for aboutText in [ - "Mactician", - "Free and open source.", - "Built for two tacticians. Shared with everyone.", - "Website", - "GitHub", - "Technical story", - "Report an issue" - ] { - try expect(appSource.contains(aboutText), "About content: \(aboutText)") - } - let buildScript = try String( - contentsOf: sourceRoot.deletingLastPathComponent() - .appendingPathComponent("scripts/build-mactician.command"), - encoding: .utf8 - ) - try expect( - buildScript.contains("Mactician.app") - && buildScript.contains("Mactician-$VERSION.dmg") - && buildScript.contains("-volname \"Mactician\"") - && buildScript.contains("Android_Codex.DeviceProfiles.effects-high.ini") - && buildScript.contains("Android_Codex.DeviceProfiles.effects-performance.ini"), - "release artifact naming" - ) - let profileRoot = sourceRoot.deletingLastPathComponent() - .appendingPathComponent("artifacts/tft-pbe-18.1-5212127-angle-opengl") - let highEffectsProfile = try String( - contentsOf: profileRoot.appendingPathComponent( - "Android_Codex.DeviceProfiles.effects-high.ini" - ), - encoding: .utf8 - ) - let performanceEffectsProfile = try String( - contentsOf: profileRoot.appendingPathComponent( - "Android_Codex.DeviceProfiles.effects-performance.ini" - ), - encoding: .utf8 - ) - for profileText in [highEffectsProfile, performanceEffectsProfile] { - try expect( - profileText.contains("CVars=sg.ResolutionQuality=100") - && profileText.contains("CVars=r.ScreenPercentage=100"), - "effects profiles preserve selected resolution" - ) - try expect( - profileText.contains("CVars=Android.OpenGL.NumRemoteProgramCompileServices=4"), - "effects profiles preserve asynchronous OpenGL PSO compilation" - ) - } - try expect( - performanceEffectsProfile.contains("CVars=sg.EffectsQuality=0") - && performanceEffectsProfile.contains("CVars=r.ParticleLODBias=2"), - "performance effects profile reduces effects and LOD" - ) - let keychainScript = try String( - contentsOf: sourceRoot.deletingLastPathComponent() - .appendingPathComponent("scripts/login-tft-from-keychain.command"), - encoding: .utf8 - ) - try expect( - keychainScript.contains( - "MACTICIAN_KEYCHAIN_SERVICE:-dev.sergeinaumov.mactician" - ), - "Keychain service script" - ) - let cyrillicRange = "[\(UnicodeScalar(0x0400)!)-\(UnicodeScalar(0x04FF)!)]" - for file in sourceFiles where ["swift", "command", "json", "plist"].contains(file.pathExtension) { - let text = try String(contentsOf: file, encoding: .utf8) - let developerPath = "/Users/" + "example-developer" - try expect(!text.contains(developerPath), "developer path in \(file.lastPathComponent)") - try expect( - text.range(of: cyrillicRange, options: .regularExpression) == nil, - "non-English text in \(file.lastPathComponent)" - ) - } - - print("Mactician tests: OK") - } - - private static func recursiveFiles(at root: URL) throws -> [URL] { - guard let enumerator = FileManager.default.enumerator( - at: root, - includingPropertiesForKeys: [.isRegularFileKey], - options: [.skipsHiddenFiles] - ) else { return [] } - return enumerator.compactMap { $0 as? URL }.filter { - (try? $0.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true - } - } - - private static func expect(_ condition: @autoclosure () throws -> Bool, _ message: String) throws { - if try !condition() { throw TestFailure(message) } - } - - private static func waitFor( - _ message: String, - timeout: TimeInterval = 2, - condition: () -> Bool - ) throws { - let deadline = Date().addingTimeInterval(timeout) - while !condition() && Date() < deadline { - Thread.sleep(forTimeInterval: 0.01) - } - try expect(condition(), message) - } -} - -private final class TelemetryLoaderStub { - typealias Completion = (Result<(Data, HTTPURLResponse), Error>) -> Void - - private let lock = NSLock() - private var requests: [URLRequest] = [] - private var completions: [Completion] = [] - - var requestCount: Int { - lock.withLock { requests.count } - } - - var firstEventID: String? { - lock.withLock { - guard let body = requests.first?.httpBody, - let value = try? JSONSerialization.jsonObject(with: body) as? [String: Any] else { - return nil - } - return value["event_id"] as? String - } - } - - func load( - _ request: URLRequest, - _ maximumBytes: Int, - _ completion: @escaping Completion - ) { - lock.withLock { - requests.append(request) - completions.append(completion) - } - } - - func completeFirst(statusCode: Int) { - let completion: Completion? = lock.withLock { - guard !completions.isEmpty else { return nil } - return completions.removeFirst() - } - guard let completion, - let response = HTTPURLResponse( - url: URL(string: "https://127.0.0.1:1/v1/events")!, - statusCode: statusCode, - httpVersion: "HTTP/1.1", - headerFields: ["Content-Type": "application/json"] - ) else { return } - completion(.success((Data(), response))) - } -} - -private extension NSLock { - func withLock(_ operation: () -> T) -> T { - lock() - defer { unlock() } - return operation() - } -} - -struct TestFailure: Error, CustomStringConvertible { - let description: String - init(_ description: String) { self.description = description } -} diff --git a/project.md b/project.md new file mode 100644 index 0000000..79d4f6f --- /dev/null +++ b/project.md @@ -0,0 +1,508 @@ +# TFTMAC Project Record + +**Project:** native macOS TFT client experience using the official Android TFT package +**Current development line:** `codex/native-tftmac-2.0.0` +**Current installed release:** TFTMAC 2.3.0 build 8, installed, live-launched, and automatically logging; release hashes match, while the timestamped current-host signing audit is blocked by the missing local identity +**Project record through:** 2026-08-31 America/Chicago + +This is the continuity document for a new developer or a new chat. It records +what TFTMAC is, why the architecture changed, what has been built, what the +evidence says, and what remains unfinished. Immutable/current facts live in +`facts.md`; exact benchmark formulas and current run findings live in +`benchmark.md`; engineering hypotheses and next code work live in `dev.md`. + +## 1. Goal + +Deliver a premium native Mac application that lets the user launch, log into, +hear, and play official Teamfight Tactics fullscreen on the target M4 Mac mini. +The Android emulator is part of the runtime implementation but is not exposed as +the product UI. The application must also be an engineering laboratory that +captures the complete runtime behavior well enough to make and reject graphics- +pipeline changes based on evidence. + +The completion standard is not “the emulator process exists” and not “the lobby +shows 60 FPS.” The user must be able to play through the native Mac window, and +the logger must preserve every under-target period across the complete run. The +graphics target is at least 60 useful FPS throughout, not only during selected +scenes. + +## 2. Current architecture + +```text +/Applications/TFTMAC.app + -> AppKit window, menus, fullscreen space, settings and benchmark controls + -> native Metal presenter and gameplay-cadence overlay + -> authenticated loopback EmulatorController gRPC + -> packaged TFTMAC Emulator Host.app + -> /usr/bin/open -n -W --env ... --args ... + -> stock Android Emulator 37.1.11 / TFT_Ultra_Tablet + -> official API 36 Google Play ARM64 guest + -> official Google Play TFT package + -> Riot login, Unreal GameActivity, live TFT gameplay +``` + +Runtime files live on the external volume at +`/Volumes/MAC MINI M4/TFTMAC/Runtime`. Captures and the normalized laboratory +stay under `~/Library/Application Support/TFTMAC`. + +The shipping display path is raw authenticated 1920×1080 RGBA from the emulator +controller into a bounded native Metal presentation ring. MMAP/zero-copy remains +a later controlled experiment because readiness fencing, stride/color integrity, +tearing, frame age, and lifetime ownership have not been proven. + +## 3. How the project reached this architecture + +### Initial working donor + +The Mactician donor had already proven that the Android runtime and TFT could +work on this Mac. Its critical behavior was not only its AVD variables. It +launched the emulator through a packaged Mac application host using +`/usr/bin/open -n -W --env ... --args ...`, inside the logged-in user's macOS +session. + +The donor contract was: + +```text +ADB server 5038 +emulator console 5582 +serial emulator-5582 +no manual ADB_VENDOR_KEYS injection +``` + +### The ADB regression + +An early TFTMAC path bypassed that architecture and directly spawned the +emulator from Node/Clara. It also moved to ports 5040/5592. The changed service +context presented an untrusted ADB host identity to Android, producing: + +```text +emulator-5592 unauthorized +Timed out waiting for emulator ADB device. +``` + +The failure was not proof that the Android runtime, AVD, TFT, GPU, RAM, or CPU +were broken. Returning to the proven packaged host/session architecture and the +5038/5582 identity restored the correct boundary. This is why Node/Clara is not +part of the shipping app. + +### Source-build research and retirement + +The repository previously developed an AEMU/gfxstream/ANGLE/MoltenVK source +laboratory. That work produced valuable compatibility and performance evidence, +but it was retired as a required product dependency. The released stock emulator +is the normal runtime. Historical source trees, patches, launchers, and campaigns +remain R&D evidence only unless a future experiment explicitly builds an +isolated variant. + +### Specialist council and ZoeMC + +The project commissioned separate Unreal, ANGLE, gfxstream/ASG, MoltenVK, Metal, +and Fortnite/Unreal research tracks, then used ZoeMC v0.2 to rank 10,000 modeled +architecture worlds. The simulation was useful for ordering tests but used +subjective priors, so it was never treated as a benchmark. Its decisive +recommendation was to prove authenticated native frame delivery before building +a custom zero-copy or driver layer. + +That branch is now resolved: raw authenticated gRPC can deliver correct +1920×1080 frames and native input. MMAP remains optional; direct zero-copy +without producer fencing and direct MoltenVK texture handoff without a supported +sharing contract remain invalid. Fortnite/Unreal material is retained as +transferable observability and pipeline-development guidance, not as proof of a +TFT implementation detail. + +### Native app pivot + +The product moved from “a script that launches an emulator” to one real native +Mac application: + +- AppKit owns normal windowing and fullscreen behavior. +- Metal owns the final completed Android-frame presentation. +- EmulatorController owns authenticated local frames and input. +- the emulator stays hidden; +- CoreAudio stays enabled; +- Google Play/Riot own package and authentication flows; +- local SQL starts before gameplay and survives through clean shutdown. + +## 4. Chronology and major milestones + +| Date | Milestone | Durable outcome | +| --- | --- | --- | +| 2026-08-26 | Repository began from a live high-end tablet/emulator runtime | Preserved a working control instead of treating all earlier work as disposable | +| 2026-08-27 | AEMU/graphics Phase 0 and required-case research | Established component versions, GLES 3.2 compatibility need, and graphics experiment inventory | +| 2026-08-28 | Donor-compatible direct play, sustained telemetry, fixed-stage campaign | Proved ASG over pipe, selected 16 KiB write step, recorded many negative results | +| 2026-08-29 | Native architecture/ownership convergence | Retired Node/source-build production dependency and vendored the exact EmulatorController protocol | +| 2026-08-30 | Native AppKit/Metal runtime became playable | Hidden emulator, authenticated frames, native input, fullscreen, CoreAudio, official TFT, local logging | +| 2026-08-30 | Login/input repair | Primary Mac click became Android touch; WebView updated; credential boundary documented | +| 2026-08-30 | Permission/unlock/icon release work | Stable local signing retained external-volume consent; non-error unlock overlays removed; official icon installed | +| 2026-08-30/31 | Rapid Combat A/B logger | Exact TFT SurfaceFlinger windows, incidents, bounded Perfetto, SQL comparison/decisions implemented | +| 2026-08-31 UTC | Home Run A rejected | Riot Performance Mode Beta experience rejected and made non-selectable | +| 2026-08-31 UTC | Build 7 Combat Latency A | One-factor pre-exec host QoS candidate built, tested, installed, and live-launched; combat gain still unproven | +| 2026-08-31 UTC | Build 8 automatic graphics logger | Signed 2.3.0/8 installed and live-launched; PID 2704 and the exact TFT SurfaceView opened the logger automatically, periodic receipts reached `COMPLETE`, and every observed frame fact resolved through its run, stack hash, window, and receipt | +| 2026-08-31 UTC | Latest automatic graphics run | 42m27s automatic process/layer run recorded 144,364 exact intervals and 189 degradation incidents; it proves the continuous logger and performance deficit, not an internal root cause | + +Relevant Git milestones: + +```text +8d9ce17 Build native full-screen TFTMAC runtime +558c0ea Add rapid combat A/B benchmark and telemetry +2123cd0 Add official TFTMAC penguin samurai icon +6bdb188 Add Build 7 combat latency candidate and project handoff +a9192ea Refocus benchmark analysis on continuous FPS +2889cf0 Finish Build 8 automatic graphics logger +``` + +Build 7 work was developed after `2123cd0` and includes the candidate, guest +power gate, semantic cross-session layer matching, tests, verifier, and authority +updates. + +The first Build 7 candidate attempt correctly failed readiness during a stale +listener/teardown race and auto-restored Control. After ports and lease ownership +were proven free, Control launched, then a clean Combat Latency A relaunch passed. +This was a runtime ownership/transient readiness event, not evidence that the +candidate improved or regressed graphics. + +## 5. Native runtime achievements + +### Window and presentation + +- Native `NSWindow` and macOS fullscreen Space behavior are implemented. +- The emulator's UI is hidden. +- Controller frames are exactly 1920×1080 RGBA8888, 8,294,400 bytes. +- The initial gRPC 4 MiB message ceiling was identified and raised to 16 MiB. +- Earlier live acceptance observed source cadence up to about 61.1 Hz and native + Metal output about 60.5 Hz. Those are transport/output numbers, not Unreal FPS. +- The final presenter is instrumented for submitted/completed frames, repeated + source use, drawable errors, command completion latency, and Metal GPU time. + +### Runtime control + +- The app takes an exclusive runtime lease before touching the shared AVD. +- It fails closed on conflicting AVD/console/controller ownership. +- AVD configuration is backed up and hashed before atomic application. +- Owned-process cleanup checks the unique session marker before termination. +- Clean exit seals telemetry, confirms emulator exit, restores the AVD hash, and + removes lease/transaction state. + +### ADB and controller + +- ADB uses the donor-compatible 5038/5582/emulator-5582 identity. +- Live sessions have observed `offline -> unauthorized -> device` and continued + only after `device` authorization. +- Controller discovery is PID-bound and token-authenticated. +- Tokens are kept in memory and excluded from SQL/log output. + +### Input + +- Primary pointer input is `EmulatorController.sendTouch`, not a desktop mouse + assumption. +- Identifier `0` remains stable through down/drag/up; release pressure is zero. +- Keyboard uses the controller's evdev path. +- Input telemetry records coordinates/pressure, counts, and special-key names, + never typed characters. + +### Audio + +- Emulator launch uses CoreAudio. +- Earlier live evidence saw active 48 kHz stereo output, an active track, and no + partial/empty underruns. +- Audible sound at the selected physical output remains a user-level acceptance, + not something software counters can prove alone. + +### Power and unlock + +- Secure Android PIN unlock remains manual. +- Non-error text overlays that covered the PIN/TFT display were removed. +- Build 7 now proves virtual AC power, stay-awake, and `Awake` wakefulness before + proceeding, preventing the avoidable secure-screen sleep state. + +### External-volume permission + +The original repeated drive-access prompt was addressed through stable local +signing/designated identity rather than changing the proven runtime root. A clean +relaunch retained removable-volume consent. Public Developer ID/notarized +distribution is not yet claimed. + +### Official launcher artwork + +The official icon is a full-bleed square penguin dressed as a samurai with one +sword and stacked `TFT` / `MAC` text. It has no baked outer gutter or rounded +container; macOS supplies the corner mask. The source and derived artwork hashes +are frozen in the release authority. + +## 6. Riot login history + +The first native login issue had two separate parts: + +1. Primary clicks were being delivered as desktop mouse events to an Android + WebView that expected touch. Build 4 replaced the primary path with real + Android touch down/move/up. +2. The login form's `USERNAME` field requires the private Riot account login + username—not the email address and not the public `Name#Tag` Riot ID. + +The Android WebView provider was updated from 133.0.6943.137 to 151.0.7922.199. +The user subsequently logged into the Riot account and played. + +The current Build 7 live session later reproduced a Riot +`MobileFREWebViewActivity` input-dispatch ANR. The proven narrow recovery did not +restart TFTMAC or the emulator: it restored +`show_ime_with_hard_keyboard=0`, force-stopped only Riot's failed process, and +reopened the official Splash/Game activity. The mutable WebView/IME dependency +therefore remains a tracked runtime risk. + +No username, password, email, PIN, cookie, token, screenshot of the form, or +typed content is retained as project evidence. + +## 7. Logger evolution + +### Why the early logger was insufficient + +The early logger could establish runtime health and rough rates, but it could +not truthfully describe the user's core complaint: large FPS loss during major +fights even when averages or the overlay looked high. Lobby/source/output rates +were too easy to misread as gameplay performance. + +### Current logger + +Every launch now creates a private session SQLite database plus bounded local +sidecars. It separates: + +- exact TFT SurfaceFlinger actual-present frame timing; +- raw gRPC source freshness; +- final Metal presentation; +- QEMU/TFT/host resource state; +- audio, ANR, memory, renderer, shader, fence, and transport signal counts; +- host/guest clock synchronization; +- benchmark boundaries, visible-stutter markers, incident traces, and final + decisions. + +The on-screen `SRC` and `OUT` labels are deliberately not called Unreal FPS. +One-second `game_frame_windows` are the gameplay authority and contain effective +FPS, 1% low, p50/p95/p99/max interval, jank, severe stalls, and missed-vsync +equivalents. + +### Combat benchmark + +- The user starts it at representative heavy combat. +- Five minutes makes it valid; eight minutes closes it automatically. +- A 20-second start trace and at most two 15-second incident traces are bounded + to 32 MiB each. +- Incident traces require two adjacent bad windows and a 120-second cooldown. +- SQL rejects invalid duration, layer, clock, history, package/configuration, or + correctness comparisons. +- The result is `HOME_RUN`, `PROMISING`, `REJECT`, or `INCONCLUSIVE`. +- A winning candidate needs a cold confirmation before promotion. + +### Full-run analysis + +Complete automatic process/layer runs are the preferred product-performance +record because every frame and resource/pipeline sample participates. Match, +combat, and visible-stutter markers are optional annotations only. The current +UI/source-named Combat Benchmark remains the faster bounded one-factor A/B +screen. Root `benchmark.md` is the shared human/AI contract for exact +raw-interval and continuous-60 deficit metrics, complete-timeline processing, +legal clock-domain correlation, and claim/evidence/unknown records. + +## 8. Performance development history + +### What was learned from the earlier fixed-stage campaign + +The earlier campaign ran on an M1 Max/userdebug environment and is historical, +not current M4 performance. It remains valuable for avoiding repeated failures. + +- ASG decisively beat pipe at the same Trial stage. +- 16 KiB ASG write steps beat the paired 4 KiB control. +- 1 MiB ASG write buffer and 32 KiB ring remained the stable choice. +- Async MoltenVK submission and 64 active command buffers were retained. +- Effects/LOD changes at 67% improved the controlled Trial proxy. +- Resolution scaling was not automatically decisive: 2560×1440 versus 1600×900 + barely changed one controlled stage despite 2.56× source pixels. +- The selected historical stack still did not meet the 57 FPS heavy-scene goal + or reproduce the user's worst approximately 15 FPS gameplay period. + +The repository preserves full result tables in `docs/benchmarks.md` and the +technical chronology in `docs/research-log.md`. + +### Current native run evidence + +The user played games through the native app, including a retained match/lobby +capture. Those sessions established playability, not adequate combat +performance. Heavy fights still visibly drop frames. + +The first rapid benchmark using combined Home Run A/Riot Performance Mode Beta +looked respectable by weighted average but was unacceptable in play: + +```text +duration 480.646 s +weighted FPS 56.665 +1% low 17.698 FPS +p95 / p99 21.760 / 34.335 ms +maximum interval 517.488 ms +incident 1% lows 1.932 and 4.629 FPS +``` + +The user explicitly rejected that experience. Clock RTT and observer-overhead +gates also made cross-boundary causality invalid. Performance Mode Beta is now +retired and cannot be selected. + +## 9. Build 7 history and current Build 8 state + +Build 7 replaces the rejected composite with `Combat Latency A`: + +- retains 1920×1080, 320 dpi, 60 Hz; +- retains 6 vCPU and 5120 MiB; +- retains host GPU, CoreAudio, ANGLE/ASG/gfxstream/MoltenVK values; +- retains TFT High, 60 FPS, Performance Mode OFF; +- changes only the packaged host's requested pre-exec QoS to + `user_interactive` and declares Game Mode eligibility. + +Implementation additions include: + +- exact host QoS requested/set/effective receipt before `execv`; +- fail/rollback if the candidate cannot establish that boundary; +- explicit refusal to claim QEMU child-thread inheritance; +- Android virtual-AC/stay-on/awake gate; +- stale Home Run A preference migration to Control; +- semantic cross-session TFT-layer comparison so dynamic SurfaceFlinger tokens + do not make every Control/Candidate pair incomparable; +- updated correctness rollback and 40 native tests. + +The current live capture is: + +```text +~/Library/Application Support/TFTMAC/Captures/ + 2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494/ +``` + +Direct evidence from that capture: + +- profile `tftmac_5gb_native_v1_preset_combat_latency_a`; +- ADB authorized on 5038 / `emulator-5582`; +- 1920×1080 RGBA first frame; +- host QoS set call returned 0 and read back `user_interactive` before exec; +- guest powered/stay-on/awake; +- official TFT 18.1-5402721 receipt; +- logger health gate passed; +- `TFT_READY_FOR_USER` with Unreal Engine and CoreAudio; +- one-second and resource/clock/pipeline tables continued advancing; +- Riot WebView ANR was recorded and recovered without restarting the emulator. +- the user later marked one full run from `2026-08-31T03:19:25Z` through + `03:51:00Z` (31m35.054s); +- that match recorded 49.449 weighted FPS, 16.300 FPS 1% low, 33.822 ms p95, + 48.746 ms p99, 1,254.162 ms maximum, 19.110% jank, and 0.610% severe + intervals from 93,724 exact TFT actual-present intervals; +- 58,925 intervals (62.871%) exceeded the 60 FPS frame budget and 1,599 of + 1,693 complete one-second windows (94.448%) were below 60 FPS; +- all overlapping exact-layer windows were available, the TFT layer was stable, + and no frame history was truncated; +- final Metal output remained near 60 Hz with zero drawable/command errors while + reusing 23,231 source frames, showing why OUT cadence cannot stand in for + useful gameplay cadence; +- clock p95 RTT was 86.757 ms, so this match cannot assign the first upstream + cause or serve as a formal matched candidate-vs-Control decision. + +The Desktop launcher `/Users/flash/Desktop/TFTMAC.app` points to the installed +`/Applications/TFTMAC.app`. Runtime process state is intentionally not frozen +as a durable fact; documentation and Git publication do not restart the app or +its emulator. + +What the Build 7 run does **not** prove: a Combat Latency A FPS win. It is one +historical candidate baseline, not a compatible A/B pair. + +The current Build 8 full-session authority is capture +`2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200`: a 42m27s +automatic graphics run with 144,364 exact intervals, 99.629% exact-layer +coverage, 189 incidents, 56.98 weighted FPS, 21.49 FPS 1% low, 21.510 ms p95, +and 33.434 ms p99. It observed `combat_latency_a` with High/60/Performance Mode +OFF; that is an observed active preset, not a performance promotion. The run +proves degradation and continuous logging but leaves internal root attribution +`UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE`. + +The same 2026-08-31 host audit confirmed that the installed main and emulator- +host hashes match the historical Build 8 release receipt. It also found zero +valid local signing identities and a current `CSSMERR_TP_NOT_TRUSTED` result. +Historical signing acceptance and current-host trust are separate facts. + +## 10. Current repository state and authority map + +Current repository observation: + +```text +branch: codex/native-tftmac-2.0.0 +HEAD: 2889cf00b54da28ff62e81fd14a6ae892f37d7cf (Build 8) +remote: github -> https://github.com/flashls1/TFTMAC.git +``` + +The Build 8 line was committed cleanly. Branch/worktree state is mutable and +must be re-observed before it is used as a handoff fact. + +Current authority roles: + +| File | Role | +| --- | --- | +| `facts.md` | facts, hard boundaries, current observations, explicit unknowns | +| `project.md` | project history, architecture pivots, current state, handoff | +| `benchmark.md` | current full-run/bounded-A/B formulas, validity, AI report contract, and findings | +| `dev.md` | developer map, experiments, hypotheses, next code contracts | +| `ssot/runtime-authority.json` | machine-readable current runtime/release evidence | +| `ssot/STACK.lock.yaml` | frozen stack/profile/toolchain selections | +| `docs/TFTMAC_NATIVE_RUNTIME_KNOWLEDGE_BASE.md` | detailed native outcome and operational knowledge | +| `docs/TFTMAC_UNREAL_PIPELINE_OBSERVABILITY.md` | graphics ownership and attribution rules | +| `docs/benchmarks.md` | historical measured campaign results | +| `docs/research-log.md` | historical R&D chronology and negative results | +| `TFTMACAPP.md` | short current native-app status pointer; historical plan is archived | + +The dated archive under `docs/history/2026-08-31-pre-build8/`, retired Node +launch paths, historical source-build directives, and historical Medium-profile +records must not override the current SSOT. + +## 11. Decisions that remain locked + +- Use the packaged Mac host launch chain and ADB 5038/5582 identity. +- Keep official Google Play/Riot package ownership. +- Keep native AppKit/Metal presentation and hidden emulator. +- Keep raw authenticated gRPC as the working presentation control. +- Keep High/60/Performance Mode OFF as the current in-game control. +- Keep 6 vCPU, 5120 MiB, ASG, 16 KiB write step, 32 KiB ring, 1 MiB buffer, + 800 microsecond flush, async MoltenVK, 64 active command buffers, CoreAudio. +- Keep Riot Performance Mode Beta/Home Run A retired. +- Never record or automate credentials. +- Never call SRC/OUT presentation rates Unreal FPS. +- Never assign a graphics bottleneck without the first valid divergent boundary. +- Treat the final Mac presenter as hidden correctness context only, never as a + user-facing graphics root-cause candidate. +- Keep base graphics logging automatic from TFT process/layer start through + process/app close; a controlled Combat Benchmark is optional and never gates + that logging. +- Keep the current optimization equation graphics-only. CPU/RAM/audio remain + health and correctness context, not candidate optimization work. + +## 12. Next decisive work + +1. Preserve the installed Build 8 automatic-graphics logger, its historical + release hashes, and live acceptance capture + `2026-08-31T21-39-18.396Z-fe34e3a1-fb91-44eb-804f-4ca8519dfc31`. +2. Treat automatic `graphics_runs`, stack-receipt SHA, direct per-frame stack + identity, and per-window joins as live-verified Build 8 behavior. The Mac + presenter is hidden correctness context, not a causal view. +3. Preserve the automatic latest-run receipt; match markers are optional + annotations, not a condition of valid full-run evidence. A short bounded + Control/Candidate A/B remains optional screening, not base logging admission. +4. Compare complete graphics timelines, every sustained under-60 episode, + receipt completeness, and conservative boundary views using `benchmark.md`. + CPU/RAM/audio remain correctness context only. +5. The latest automatic run confirms an unresolved internal causal gap below + the SurfaceFlinger authority. Implement the planned source-level work-ID + instrumentation only in the isolated + `tftmac-runtime` diagnostic stack at `c8aa26e`; do not replace Build 8. +6. Use that evidence to choose an owned code target. Current TFT is direct + Unreal Vulkan; ANGLE is second-line only if a run receipt proves it is active. +7. Preserve the recurrent Riot WebView ANR as a separate login reliability issue, + not as a graphics benchmark result. +8. Measure startup phases if the user's observed slow startup remains after login + reliability is stable; do not trade away logger-before-runtime, ADB, power, + package, or controller correctness to make a cosmetic startup number smaller. + +The native app is launchable and playable. The remaining project objective is +not another wrapper or another generic FPS counter; it is a measured improvement +that holds at least 60 useful FPS across the complete run without destroying +correctness or the official package boundary. diff --git a/reference/avd/config.ini b/reference/avd/config.ini index 12ae1e7..d766717 100644 --- a/reference/avd/config.ini +++ b/reference/avd/config.ini @@ -1,5 +1,5 @@ AvdId=TftPBE -avd.ini.displayname=Mactician +avd.ini.displayname=TFTMAC Reference abi.type=arm64-v8a hw.cpu.arch=arm64 hw.lcd.density=260 diff --git a/run-tft-root-affinity.command b/run-tft-root-affinity.command deleted file mode 100755 index fd25026..0000000 --- a/run-tft-root-affinity.command +++ /dev/null @@ -1,1489 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -unsetopt BG_NICE - -readonly PROJECT_DIR="${0:A:h}" -source "$PROJECT_DIR/scripts/android-environment.sh" -readonly ADB_SERVER_PORT="${TFT_ADB_SERVER_PORT:-5038}" -SDK_ROOT="$(tft_resolve_android_sdk_root)" -readonly SDK_ROOT -EMULATOR="$(tft_resolve_emulator)" -readonly EMULATOR -ADB="$(tft_resolve_adb)" -readonly ADB -readonly AVD_HOME="${TFT_ROOT_AVD_HOME:-$(tft_resolve_avd_home)}" -readonly AVD_NAME="${TFT_AVD_NAME:-TftRootAffinity}" -readonly AVD_LOCK_FILE="$AVD_HOME/$AVD_NAME.avd/.mactician-avd.lock" -readonly INHERITED_AVD_LOCK_OWNER="${TFT_AVD_LOCK_OWNER_PID:-}" -readonly SERIAL="${TFT_SERIAL:-emulator-5582}" -readonly EMULATOR_PORT="${TFT_EMULATOR_PORT:-${SERIAL#emulator-}}" -readonly BOOT_TIMEOUT_SECONDS="${TFT_BOOT_TIMEOUT_SECONDS:-120}" -readonly PACKAGE="com.riotgames.league.teamfighttactics.pbe" -readonly ACTIVITY="com.epicgames.unreal.SplashActivity" -readonly ANGLE_BASE_FEATURES="exposeNonConformantExtensionsAndVersions:exposeES32ForTesting" -readonly ANGLE_EXTRA_FEATURES="${TFT_ANGLE_EXTRA_FEATURES:-}" -readonly ANGLE_DISABLED_FEATURES="${TFT_ANGLE_DISABLED_FEATURES:-}" -readonly BATCHED_DESCRIPTORS="${TFT_VULKAN_BATCHED_DESCRIPTORS:-0}" -readonly MVK_QUEUE_MODE="${TFT_MVK_QUEUE_MODE:-default}" -readonly MACOS_GAME_MODE="${TFT_MACOS_GAME_MODE:-0}" -readonly VIRTIO_GPU_NATIVE_SYNC="${TFT_VIRTIO_GPU_NATIVE_SYNC:-0}" -readonly VIRTIO_GPU_NEXT="${TFT_VIRTIO_GPU_NEXT:-0}" -readonly GL_DRAW_FLUSH_INTERVAL="${TFT_GL_DRAW_FLUSH_INTERVAL:-}" -readonly HWUI_RENDERER="${TFT_HWUI_RENDERER:-skiagl}" -readonly GRAPHICS_PROFILE="${TFT_GRAPHICS_PROFILE:-stable}" -readonly DISPLAY_SIZE="${TFT_DISPLAY_SIZE:-1600x900}" -readonly DISPLAY_DENSITY="${TFT_DISPLAY_DENSITY:-260}" -readonly UI_SCALE="${TFT_UI_SCALE:-1.0}" -readonly CPU_CORES="${TFT_CPU_CORES:-7}" -readonly MEMORY_MB="${TFT_MEMORY_MB:-6144}" -readonly GAME_LANGUAGE="${TFT_GAME_LANGUAGE:-en-US}" -readonly AUDIO_ENABLED="${TFT_AUDIO_ENABLED:-1}" -readonly RENDERER="${TFT_RENDERER:-angle}" -readonly HOST_GPU="${TFT_HOST_GPU:-host}" -readonly GUEST_GL_DRIVER="${TFT_GUEST_GL_DRIVER:-angle}" -readonly GUEST_SUBMIT_THREAD="${TFT_GUEST_SUBMIT_THREAD:-default}" -readonly PACKAGED_EMULATOR_APP="$PROJECT_DIR/Mactician Game Host.app" -readonly GAME_MODE_APP="$PROJECT_DIR/tools/Mactician Game Host.app" -readonly INPUT_BRIDGE_ENABLED="${TFT_INPUT_BRIDGE_ENABLED:-1}" -readonly INPUT_DIAGNOSTICS="${TFT_INPUT_DIAGNOSTICS:-0}" -readonly INPUT_DIAGNOSTICS_LOG="${TFT_INPUT_DIAGNOSTICS_LOG:-}" -readonly INPUT_BRIDGE_SOURCE="$PROJECT_DIR/tools/tft-input-bridge.swift" -readonly INPUT_BRIDGE_BINARY="$PROJECT_DIR/runtime/tft-input-bridge" -readonly INPUT_SHOP_POINT="${TFT_INPUT_SHOP_POINT:-0.96,0.93}" -readonly INPUT_REROLL_POINT="${TFT_INPUT_REROLL_POINT:-0.955,0.79}" -readonly INPUT_XP_POINT="${TFT_INPUT_XP_POINT:-0.032,0.925}" -readonly INPUT_TRAITS_POINT="${TFT_INPUT_TRAITS_POINT:-0.029,0.04}" -readonly INPUT_ITEMS_POINT="${TFT_INPUT_ITEMS_POINT:-0.059,0.04}" -readonly INPUT_DAMAGE_POINT="${TFT_INPUT_DAMAGE_POINT:-0.947,0.04}" -readonly INPUT_PLAYERS_POINT="${TFT_INPUT_PLAYERS_POINT:-0.975,0.04}" -readonly DIRECT_VULKAN_DIR="$PROJECT_DIR/artifacts/tft-pbe-18.1-5212127-direct-vulkan" -readonly DIRECT_VULKAN_APK="$DIRECT_VULKAN_DIR/base-direct-vulkan.apk" -readonly DIRECT_VULKAN_PROFILE="$DIRECT_VULKAN_DIR/Android_Codex.DeviceProfiles.ini" -readonly DIRECT_VULKAN_REMOTE_DIR="/data/local/tmp/tft-direct-vulkan" -readonly ANGLE_OPENGL_DIR="$PROJECT_DIR/artifacts/tft-pbe-18.1-5212127-angle-opengl" -readonly ANGLE_OPENGL_APK="${TFT_ANGLE_OPENGL_APK:-$ANGLE_OPENGL_DIR/base-angle-opengl.apk}" -readonly ANGLE_OPENGL_PROFILE_OVERRIDE="${TFT_ANGLE_OPENGL_PROFILE:-}" -readonly ANGLE_OPENGL_PROFILE="${ANGLE_OPENGL_PROFILE_OVERRIDE:-$ANGLE_OPENGL_DIR/Android_Codex.DeviceProfiles.ini}" -readonly ANGLE_OPENGL_PROFILE_SHA256="${TFT_ANGLE_OPENGL_PROFILE_SHA256:-}" -readonly ANGLE_OPENGL_REMOTE_DIR="/data/local/tmp/tft-angle-opengl" -readonly ORIGINAL_BASE_SHA256="${TFT_ORIGINAL_BASE_APK_SHA256:-2f4996a620623d0b958383bfe58bdec78fb70cca095099ca2474f3d08c62ff18}" -readonly DIRECT_VULKAN_SHA256="3cabacef5ba122467d2eea8fb2874f41530a8d0c1b8cda3f391a58134b936236" -readonly ANGLE_OPENGL_SHA256="${TFT_ANGLE_OPENGL_APK_SHA256:-f3a257750e1875298a1203c40e9bb98aaf9521b4466f75a53d16fd2d6ea63865}" -readonly UNREAL_LIB_OVERLAY="${TFT_UNREAL_LIB_OVERLAY:-}" -readonly UNREAL_LIB_OVERLAY_SHA256="${TFT_UNREAL_LIB_OVERLAY_SHA256:-}" -readonly UNREAL_LIB_ORIGINAL_SHA256="dd59c46a07d6f7394add255a1f11ede357489a7f1b6f2f925bd87c878c16ec08" -readonly UNREAL_LIB_REMOTE_DIR="/data/local/tmp/tft-unreal-lib-overlay" -readonly WRAP_PROPERTY="wrap.$PACKAGE" - -OVERLAY_LABEL="" -OVERLAY_APK="" -OVERLAY_PROFILE="" -OVERLAY_REMOTE_DIR="" -OVERLAY_SHA256="" -OVERLAY_BASE_PATH="" -OVERLAY_MOUNTED=0 -PROFILE_DESTINATION="" -PROFILE_BACKUP="" -PROFILE_PRESENT_MARKER="" -PROFILE_ABSENT_MARKER="" -PROFILE_ORIGINAL_OWNER="" -PROFILE_ORIGINAL_MODE="" -PROFILE_WAS_PRESENT=0 -PROFILE_MANAGED=0 -PROFILE_MOUNTED=0 -UNREAL_LIB_TARGET="" -UNREAL_LIB_MOUNTED=0 -ANGLE_SETTINGS_MANAGED=0 -ANGLE_ORIGINAL_PACKAGES="" -ANGLE_ORIGINAL_VALUES="" -ANGLE_ORIGINAL_FEATURES="" -GUEST_WRAP_MANAGED=0 -GUEST_WRAP_ORIGINAL="" -HWUI_RENDERER_MANAGED=0 -HWUI_RENDERER_ORIGINAL="" -AVD_LOCK_HELD=0 - -release_avd_lock() { - if [[ "$AVD_LOCK_HELD" != "1" ]]; then - return - fi - typeset lock_owner="" - if [[ -f "$AVD_LOCK_FILE" ]]; then - IFS= read -r lock_owner < "$AVD_LOCK_FILE" || true - fi - if [[ "$lock_owner" == "$$" ]]; then - rm -f "$AVD_LOCK_FILE" - else - print "Warning: the AVD lock changed owners; the foreign lock was not removed." - fi - AVD_LOCK_HELD=0 -} - -if [[ "$ADB_SERVER_PORT" != <-> ]] \ - || (( ADB_SERVER_PORT < 1024 || ADB_SERVER_PORT > 65534 )); then - print "TFT_ADB_SERVER_PORT must be a TCP port from 1024 to 65534." - exit 2 -fi -unset ADB_SERVER_SOCKET ANDROID_ADB_SERVER_ADDRESS -export TFT_ADB_SERVER_PORT="$ADB_SERVER_PORT" -export ANDROID_ADB_SERVER_PORT="$ADB_SERVER_PORT" -export ADB_MDNS_AUTO_CONNECT="" -"$ADB" -P "$ADB_SERVER_PORT" start-server >/dev/null - -readonly OSFT_QEMU_PATTERN='Application Support/OSFT/sdk/emulator/.*/qemu-system-aarch64.*-avd PlayDroid' -if pgrep -f "$OSFT_QEMU_PATTERN" >/dev/null 2>&1; then - print "OSFT PlayDroid is already running. Close it before TFT fast-quality: two VMs cause severe CPU/GPU contention." - exit 1 -fi - -if [[ "$MACOS_GAME_MODE" != "0" && "$MACOS_GAME_MODE" != "1" ]]; then - print "TFT_MACOS_GAME_MODE must be either 0 or 1." - exit 2 -fi -if [[ "$HWUI_RENDERER" != "skiagl" && "$HWUI_RENDERER" != "skiavk" ]]; then - print "TFT_HWUI_RENDERER must be either skiagl or skiavk." - exit 2 -fi -if [[ "$INPUT_BRIDGE_ENABLED" != "0" && "$INPUT_BRIDGE_ENABLED" != "1" ]]; then - print "TFT_INPUT_BRIDGE_ENABLED must be either 0 or 1." - exit 2 -fi -if [[ "$INPUT_DIAGNOSTICS" != "0" && "$INPUT_DIAGNOSTICS" != "1" ]]; then - print "TFT_INPUT_DIAGNOSTICS must be either 0 or 1." - exit 2 -fi -for input_point_name input_point_value in \ - TFT_INPUT_SHOP_POINT "$INPUT_SHOP_POINT" \ - TFT_INPUT_REROLL_POINT "$INPUT_REROLL_POINT" \ - TFT_INPUT_XP_POINT "$INPUT_XP_POINT" \ - TFT_INPUT_TRAITS_POINT "$INPUT_TRAITS_POINT" \ - TFT_INPUT_ITEMS_POINT "$INPUT_ITEMS_POINT" \ - TFT_INPUT_DAMAGE_POINT "$INPUT_DAMAGE_POINT" \ - TFT_INPUT_PLAYERS_POINT "$INPUT_PLAYERS_POINT"; do - if [[ ! "$input_point_value" =~ '^(0([.][0-9]+)?|1([.]0+)?),(0([.][0-9]+)?|1([.]0+)?)$' ]]; then - print "$input_point_name must be a pair of relative X,Y coordinates from 0 to 1." - exit 2 - fi -done -if [[ "$VIRTIO_GPU_NATIVE_SYNC" != "0" && "$VIRTIO_GPU_NATIVE_SYNC" != "1" ]]; then - print "TFT_VIRTIO_GPU_NATIVE_SYNC must be either 0 or 1." - exit 2 -fi -if [[ "$VIRTIO_GPU_NEXT" != "0" && "$VIRTIO_GPU_NEXT" != "1" ]]; then - print "TFT_VIRTIO_GPU_NEXT must be either 0 or 1." - exit 2 -fi -if [[ -n "$GL_DRAW_FLUSH_INTERVAL" ]]; then - if [[ "$GL_DRAW_FLUSH_INTERVAL" != <-> ]] \ - || (( GL_DRAW_FLUSH_INTERVAL < 100 || GL_DRAW_FLUSH_INTERVAL > 10000 )); then - print "TFT_GL_DRAW_FLUSH_INTERVAL must be an integer from 100 to 10000 microseconds." - exit 2 - fi -fi - -if [[ "$EMULATOR_PORT" != <-> ]] || (( EMULATOR_PORT < 5554 || EMULATOR_PORT > 5682 )) \ - || [[ "$SERIAL" != "emulator-$EMULATOR_PORT" ]]; then - print "TFT_SERIAL/TFT_EMULATOR_PORT must be a matching emulator-PORT and PORT pair." - exit 2 -fi -if [[ "$BOOT_TIMEOUT_SECONDS" != <-> ]] || (( BOOT_TIMEOUT_SECONDS < 30 || BOOT_TIMEOUT_SECONDS > 600 )); then - print "TFT_BOOT_TIMEOUT_SECONDS must be an integer from 30 to 600 seconds." - exit 2 -fi - -if [[ ! "$CPU_CORES" =~ '^[0-9]+$' ]] || (( CPU_CORES < 1 || CPU_CORES > 16 )); then - print "TFT_CPU_CORES must be an integer from 1 to 16." - exit 2 -fi -if [[ ! "$MEMORY_MB" =~ '^[0-9]+$' ]] || (( MEMORY_MB < 2048 || MEMORY_MB > 32768 )); then - print "TFT_MEMORY_MB must be an integer from 2048 to 32768." - exit 2 -fi -case "$GAME_LANGUAGE" in - en-US|ru-RU|de-DE|fr-FR|es-ES|es-MX|pt-BR|it-IT|pl-PL|cs-CZ|hu-HU|ro-RO|el-GR|tr-TR|ar-AE|ja-JP|ko-KR|zh-CN|zh-SG|zh-TW|vi-VN|th-TH|id-ID) - ;; - *) - print "TFT_GAME_LANGUAGE must be a supported locale." - exit 2 - ;; -esac -if [[ ! "$DISPLAY_SIZE" =~ '^[0-9]+x[0-9]+$' ]]; then - print "TFT_DISPLAY_SIZE must use WIDTHxHEIGHT format, for example 1600x900." - exit 2 -fi -if [[ ! "$DISPLAY_DENSITY" =~ '^[0-9]+$' ]] || (( DISPLAY_DENSITY < 120 || DISPLAY_DENSITY > 640 )); then - print "TFT_DISPLAY_DENSITY must be an integer from 120 to 640." - exit 2 -fi -if [[ "$UI_SCALE" != "1.0" && "$UI_SCALE" != "1.25" && "$UI_SCALE" != "1.5" \ - && "$UI_SCALE" != "1.75" && "$UI_SCALE" != "2.0" ]]; then - print "TFT_UI_SCALE must be one of 1.0, 1.25, 1.5, 1.75, or 2.0." - exit 2 -fi -if [[ -n "$ANGLE_EXTRA_FEATURES" && ! "$ANGLE_EXTRA_FEATURES" =~ '^([A-Za-z0-9_]+[*]?)(:[A-Za-z0-9_]+[*]?)*$' ]]; then - print "TFT_ANGLE_EXTRA_FEATURES must be an ANGLE feature or a colon-separated list." - exit 2 -fi -if [[ -n "$ANGLE_DISABLED_FEATURES" \ - && ! "$ANGLE_DISABLED_FEATURES" =~ '^([A-Za-z0-9_]+[*]?)(:[A-Za-z0-9_]+[*]?)*$' ]]; then - print "TFT_ANGLE_DISABLED_FEATURES must be a feature or a colon-separated list." - exit 2 -fi -if [[ "$GUEST_GL_DRIVER" != "angle" && "$GUEST_GL_DRIVER" != "native" ]]; then - print "TFT_GUEST_GL_DRIVER must be either angle or native." - exit 2 -fi -if [[ "$GUEST_SUBMIT_THREAD" != "default" \ - && "$GUEST_SUBMIT_THREAD" != "on-demand" \ - && "$GUEST_SUBMIT_THREAD" != "0" \ - && "$GUEST_SUBMIT_THREAD" != "1" ]]; then - print "TFT_GUEST_SUBMIT_THREAD must be one of default, on-demand, 0, or 1." - exit 2 -fi -if [[ "$GUEST_SUBMIT_THREAD" != "default" && "$GUEST_GL_DRIVER" != "angle" ]]; then - print "The guest Vulkan submit thread is supported only with TFT_GUEST_GL_DRIVER=angle." - exit 2 -fi -if [[ "$HOST_GPU" != "host" && "$HOST_GPU" != "swangle" ]]; then - print "TFT_HOST_GPU must be either host or swangle." - exit 2 -fi -if [[ "$HOST_GPU" == "swangle" && "$GUEST_GL_DRIVER" != "native" ]]; then - print "The isolated swangle control is supported only with TFT_GUEST_GL_DRIVER=native." - exit 2 -fi - -ANGLE_FEATURES="$ANGLE_BASE_FEATURES" -if [[ -n "$ANGLE_EXTRA_FEATURES" ]]; then - ANGLE_FEATURES+=":$ANGLE_EXTRA_FEATURES" -fi -readonly ANGLE_FEATURES - -if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - if [[ "$RENDERER" != "angle-opengl" ]]; then - print "TFT_GUEST_GL_DRIVER=native is supported only with TFT_RENDERER=angle-opengl." - exit 2 - fi - if [[ "$GRAPHICS_PROFILE" != "osft" && "$GRAPHICS_PROFILE" != "osft-no-batching" ]]; then - print "Native gfxstream GLES is incompatible with VulkanNativeSwapchain; use the osft or osft-no-batching profile." - exit 2 - fi - export ANGLE_FEATURE_OVERRIDES_ENABLED="$ANGLE_FEATURES" - if [[ -n "$ANGLE_DISABLED_FEATURES" ]]; then - export ANGLE_FEATURE_OVERRIDES_DISABLED="$ANGLE_DISABLED_FEATURES" - else - unset ANGLE_FEATURE_OVERRIDES_DISABLED - fi -fi - -case "$RENDERER" in - angle) - ;; - direct-vulkan) - OVERLAY_LABEL="direct-Vulkan" - OVERLAY_APK="$DIRECT_VULKAN_APK" - OVERLAY_PROFILE="$DIRECT_VULKAN_PROFILE" - OVERLAY_REMOTE_DIR="$DIRECT_VULKAN_REMOTE_DIR" - OVERLAY_SHA256="$DIRECT_VULKAN_SHA256" - ;; - angle-opengl) - OVERLAY_LABEL="ANGLE/OpenGL" - OVERLAY_APK="$ANGLE_OPENGL_APK" - OVERLAY_PROFILE="$ANGLE_OPENGL_PROFILE" - OVERLAY_REMOTE_DIR="$ANGLE_OPENGL_REMOTE_DIR" - OVERLAY_SHA256="$ANGLE_OPENGL_SHA256" - ;; - *) - print "TFT_RENDERER must be one of angle, angle-opengl, or direct-vulkan." - exit 2 - ;; -esac - -if [[ -n "$OVERLAY_APK" ]]; then - if [[ ! -f "$OVERLAY_APK" || ! -f "$OVERLAY_PROFILE" ]]; then - print "$OVERLAY_LABEL APK/profile artifacts were not found." - exit 1 - fi - if [[ "$(shasum -a 256 "$OVERLAY_APK" | awk '{ print $1 }')" != "$OVERLAY_SHA256" ]]; then - print "The $OVERLAY_LABEL APK SHA-256 does not match the verified value." - exit 1 - fi -fi -if [[ -n "$ANGLE_OPENGL_PROFILE_OVERRIDE" ]]; then - if [[ "$RENDERER" != "angle-opengl" ]] \ - || [[ ! "$ANGLE_OPENGL_PROFILE_SHA256" =~ '^[0-9a-f]{64}$' ]]; then - print "The custom ANGLE/OpenGL profile requires TFT_RENDERER=angle-opengl and a verified SHA-256." - exit 2 - fi - if [[ "$(shasum -a 256 "$ANGLE_OPENGL_PROFILE" | awk '{ print $1 }')" != "$ANGLE_OPENGL_PROFILE_SHA256" ]]; then - print "The custom ANGLE/OpenGL profile SHA-256 does not match the expected value." - exit 1 - fi -fi -if [[ -n "$UNREAL_LIB_OVERLAY" ]]; then - if [[ "$GUEST_GL_DRIVER" != "native" ]]; then - print "TFT_UNREAL_LIB_OVERLAY is allowed only for the isolated native-GLES test." - exit 2 - fi - if [[ ! -f "$UNREAL_LIB_OVERLAY" ]] \ - || [[ ! "$UNREAL_LIB_OVERLAY_SHA256" =~ '^[0-9a-f]{64}$' ]]; then - print "TFT_UNREAL_LIB_OVERLAY/SHA256 do not reference a verified artifact." - exit 2 - fi - if [[ "$(shasum -a 256 "$UNREAL_LIB_OVERLAY" | awk '{ print $1 }')" != "$UNREAL_LIB_OVERLAY_SHA256" ]]; then - print "The temporary libUnreal.so overlay SHA-256 does not match the expected value." - exit 1 - fi -fi - -if [[ "$MACOS_GAME_MODE" == "1" && ( "$BATCHED_DESCRIPTORS" != "0" || "$MVK_QUEUE_MODE" != "default" ) ]]; then - print "Game Mode, descriptor batching, and the MoltenVK queue mode must be tested one at a time." - exit 2 -fi - -case "$MVK_QUEUE_MODE" in - default) - unset MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS - ;; - async) - export MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS=0 - ;; - sync) - export MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS=1 - ;; - *) - print "TFT_MVK_QUEUE_MODE must be one of default, async, or sync." - exit 2 - ;; -esac - -typeset -a EXTRA_EMULATOR_FLAGS -EXTRA_EMULATOR_FLAGS=() -case "$GRAPHICS_PROFILE" in - stable) - EXTRA_EMULATOR_FLAGS=(-feature VulkanNativeSwapchain) - ;; - osft) - if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,-GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts' - -append-userspace-opt androidboot.opengles.version=196610 - ) - else - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts' - -append-userspace-opt androidboot.opengles.version=196610 - ) - fi - ;; - osft-no-batching) - if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,-GuestAngle,-GLPipeChecksum,-VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts' - -append-userspace-opt androidboot.opengles.version=196610 - ) - else - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,-VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts' - -append-userspace-opt androidboot.opengles.version=196610 - ) - fi - ;; - osft-no-async-compose) - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,-AsyncComposeSupport,VirtioGpuFenceContexts' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - osft-no-queue-submit-with-commands) - # Diagnostic reproduction only. Real TFT cold boots abort before ADB - # while decoding VK_STRUCTURE_TYPE_APPLICATION_INFO; this profile is - # deliberately absent from the performance-candidate manifest. - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts,-VulkanQueueSubmitWithCommands' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - osft-no-native-swapchain) - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts,-VulkanNativeSwapchain' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - osft-no-fence-contexts) - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,-VirtioGpuFenceContexts' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - osft-no-virtual-queue) - # Diagnostic reproduction only. Emulator 37.1.11 reports that this - # guest feature override is ignored, so it cannot form a valid A/B. - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts,-VulkanVirtualQueue' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - osft-native-swapchain) - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts,VulkanNativeSwapchain' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - osft-low-latency) - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,-AsyncComposeSupport,VirtioGpuFenceContexts,VulkanNativeSwapchain' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - turbo) - EXTRA_EMULATOR_FLAGS=( - -feature 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,VulkanBatchedDescriptorSetUpdate,AsyncComposeSupport,VirtioGpuFenceContexts,VulkanNativeSwapchain' - -append-userspace-opt androidboot.opengles.version=196610 - ) - ;; - *) - print "TFT_GRAPHICS_PROFILE must be one of stable, osft, osft-no-batching, osft-no-async-compose, osft-no-queue-submit-with-commands, osft-no-native-swapchain, osft-no-fence-contexts, osft-no-virtual-queue, osft-native-swapchain, osft-low-latency, or turbo." - exit 2 - ;; -esac - -if [[ "$BATCHED_DESCRIPTORS" == "1" && "$GRAPHICS_PROFILE" == "stable" ]]; then - EXTRA_EMULATOR_FLAGS+=(-feature VulkanBatchedDescriptorSetUpdate) -fi -if [[ "$VIRTIO_GPU_NATIVE_SYNC" == "1" ]]; then - EXTRA_EMULATOR_FLAGS+=(-feature VirtioGpuNativeSync) -fi -if [[ "$VIRTIO_GPU_NEXT" == "1" ]]; then - EXTRA_EMULATOR_FLAGS+=(-feature VirtioGpuNext) -fi - -export ANDROID_SDK_ROOT="$SDK_ROOT" -export ANDROID_AVD_HOME="$AVD_HOME" - -if [[ ! -x "$EMULATOR" ]]; then - print "Android Emulator was not found: $EMULATOR" - exit 1 -fi - -if [[ "$INPUT_BRIDGE_ENABLED" == "1" ]]; then - if [[ ! -f "$INPUT_BRIDGE_SOURCE" ]]; then - print "The input bridge source was not found: $INPUT_BRIDGE_SOURCE" - exit 1 - fi - if [[ ! -x "$INPUT_BRIDGE_BINARY" || "$INPUT_BRIDGE_SOURCE" -nt "$INPUT_BRIDGE_BINARY" ]]; then - readonly INPUT_BRIDGE_NEXT="$INPUT_BRIDGE_BINARY.next" - mkdir -p "${INPUT_BRIDGE_BINARY:h}" "$PROJECT_DIR/runtime/swift-module-cache" - /usr/bin/xcrun swiftc \ - -O \ - -module-cache-path "$PROJECT_DIR/runtime/swift-module-cache" \ - "$INPUT_BRIDGE_SOURCE" \ - -o "$INPUT_BRIDGE_NEXT" - mv -f "$INPUT_BRIDGE_NEXT" "$INPUT_BRIDGE_BINARY" - fi -fi - -if [[ ! -f "$AVD_HOME/$AVD_NAME.ini" ]]; then - print "The rootable AVD was not found: $AVD_HOME/$AVD_NAME.ini" - exit 1 -fi - -EMULATOR_APP="" -if [[ -x "$PACKAGED_EMULATOR_APP/Contents/MacOS/MacticianGameHost" ]]; then - EMULATOR_APP="$PACKAGED_EMULATOR_APP" -elif [[ "$MACOS_GAME_MODE" == "1" && -x "$GAME_MODE_APP/Contents/MacOS/MacticianGameHost" ]]; then - EMULATOR_APP="$GAME_MODE_APP" -fi -readonly EMULATOR_APP - -if [[ "$MACOS_GAME_MODE" == "1" && -z "$EMULATOR_APP" ]]; then - print "The Game Mode app wrapper was not found: $GAME_MODE_APP" - exit 1 -fi - -if [[ -n "$INHERITED_AVD_LOCK_OWNER" ]]; then - typeset inherited_lock_pid="" - if [[ -f "$AVD_LOCK_FILE" ]]; then - IFS= read -r inherited_lock_pid < "$AVD_LOCK_FILE" || true - fi - if [[ "$INHERITED_AVD_LOCK_OWNER" != <-> \ - || "$INHERITED_AVD_LOCK_OWNER" != "$PPID" \ - || "$inherited_lock_pid" != "$INHERITED_AVD_LOCK_OWNER" ]] \ - || ! kill -0 "$INHERITED_AVD_LOCK_OWNER" >/dev/null 2>&1; then - print "The inherited AVD lock does not belong to the parent launcher process." - exit 1 - fi -else - if ! /usr/bin/shlock -f "$AVD_LOCK_FILE" -p "$$"; then - typeset active_lock_pid="unknown" - if [[ -f "$AVD_LOCK_FILE" ]]; then - IFS= read -r active_lock_pid < "$AVD_LOCK_FILE" || true - fi - print "Another TFT launcher already owns the AVD lock (PID $active_lock_pid)." - exit 1 - fi - AVD_LOCK_HELD=1 - trap release_avd_lock EXIT -fi - -if "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; then - print "The rootable TFT AVD is already running on $SERIAL. Close it first." - exit 1 -fi -if [[ "$AUDIO_ENABLED" != "0" && "$AUDIO_ENABLED" != "1" ]]; then - print "TFT_AUDIO_ENABLED must be 0 or 1." - exit 2 -fi - -typeset -a EMULATOR_ARGS -EMULATOR_ARGS=( - "@$AVD_NAME" - -id "TFT-$AVD_NAME" - -port "$EMULATOR_PORT" - -gpu "$HOST_GPU" - "${EXTRA_EMULATOR_FLAGS[@]}" - -append-userspace-opt "androidboot.mactician.graphics_profile=$GRAPHICS_PROFILE" - -skin "$DISPLAY_SIZE" - -vsync-rate 60 - -dns-server 1.1.1.1,8.8.8.8 - -cores "$CPU_CORES" - -memory "$MEMORY_MB" - -no-snapshot - -no-metrics - -no-boot-anim - -crash-report-mode disabled -) -typeset AUDIO_STATUS="enabled" -if [[ "$AUDIO_ENABLED" == "0" ]]; then - EMULATOR_ARGS+=(-no-audio) - AUDIO_STATUS="disabled" -fi -readonly AUDIO_STATUS -if [[ -n "$EMULATOR_APP" ]]; then - typeset -a APP_ENV_ARGS - APP_ENV_ARGS=( - --env "TFT_EMULATOR=$EMULATOR" - --env "TFT_ADB_SERVER_PORT=$ADB_SERVER_PORT" - --env "ANDROID_ADB_SERVER_PORT=$ADB_SERVER_PORT" - --env "ADB_MDNS_AUTO_CONNECT=" - --env "ANDROID_SDK_ROOT=$SDK_ROOT" - --env "ANDROID_AVD_HOME=$AVD_HOME" - ) - for environment_name in \ - ANGLE_FEATURE_OVERRIDES_ENABLED \ - ANGLE_FEATURE_OVERRIDES_DISABLED \ - MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS \ - MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION \ - MVK_CONFIG_ACTIVITY_PERFORMANCE_LOGGING_STYLE \ - MVK_CONFIG_LOG_LEVEL \ - MVK_CONFIG_PERFORMANCE_LOGGING_FRAME_COUNT \ - MVK_CONFIG_PERFORMANCE_TRACKING \ - MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS \ - MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE \ - MVK_CONFIG_SUPPORT_LARGE_QUERY_POOLS \ - MVK_CONFIG_USE_MTLHEAP \ - MVK_CONFIG_FAST_MATH_ENABLED \ - MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS \ - MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE; do - if [[ -n "${(P)environment_name:-}" ]]; then - APP_ENV_ARGS+=(--env "$environment_name=${(P)environment_name}") - fi - done - /usr/bin/open -n -W \ - "${APP_ENV_ARGS[@]}" \ - "$EMULATOR_APP" --args "${EMULATOR_ARGS[@]}" & -else - "$EMULATOR" "${EMULATOR_ARGS[@]}" & -fi -readonly EMULATOR_PID=$! -WATCHER_PID="" -INPUT_BRIDGE_PID="" -UI_SCALE_TEMP_DIR="" - -cleanup() { - if [[ -n "$UI_SCALE_TEMP_DIR" && -d "$UI_SCALE_TEMP_DIR" ]]; then - rm -rf "$UI_SCALE_TEMP_DIR" - UI_SCALE_TEMP_DIR="" - fi - if [[ -n "$INPUT_BRIDGE_PID" ]] && kill -0 "$INPUT_BRIDGE_PID" >/dev/null 2>&1; then - kill "$INPUT_BRIDGE_PID" >/dev/null 2>&1 || true - wait "$INPUT_BRIDGE_PID" >/dev/null 2>&1 || true - fi - if [[ -n "$WATCHER_PID" ]] && kill -0 "$WATCHER_PID" >/dev/null 2>&1; then - kill "$WATCHER_PID" >/dev/null 2>&1 || true - wait "$WATCHER_PID" >/dev/null 2>&1 || true - fi - if [[ ( "$OVERLAY_MOUNTED" == "1" || "$PROFILE_MANAGED" == "1" \ - || "$UNREAL_LIB_MOUNTED" == "1" ) ]] \ - && "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; then - "$ADB" -s "$SERIAL" shell am force-stop "$PACKAGE" >/dev/null 2>&1 || true - integer wait_attempt - for (( wait_attempt = 1; wait_attempt <= 20; wait_attempt++ )); do - if [[ -z "$("$ADB" -s "$SERIAL" shell pidof "$PACKAGE" 2>/dev/null | tr -d '\r')" ]]; then - break - fi - sleep 0.25 - done - if [[ "$PROFILE_MANAGED" == "1" && -n "$PROFILE_DESTINATION" ]]; then - if [[ "$PROFILE_MOUNTED" == "1" ]]; then - if "$ADB" -s "$SERIAL" shell umount "$PROFILE_DESTINATION" >/dev/null 2>&1; then - PROFILE_MOUNTED=0 - print "The persistent guest DeviceProfiles.ini mount was removed." - else - print "Warning: the guest DeviceProfiles.ini mount was not removed; the transaction marker remains for cold-boot recovery." - fi - fi - if [[ "$PROFILE_MOUNTED" == "0" ]]; then - PROFILE_ROLLBACK_APPLIED=0 - if [[ "$PROFILE_WAS_PRESENT" == "1" && -n "$PROFILE_BACKUP" ]]; then - if "$ADB" -s "$SERIAL" shell cp "$PROFILE_BACKUP" "$PROFILE_DESTINATION" \ - && "$ADB" -s "$SERIAL" shell chown "$PROFILE_ORIGINAL_OWNER" "$PROFILE_DESTINATION" \ - && "$ADB" -s "$SERIAL" shell chmod "$PROFILE_ORIGINAL_MODE" "$PROFILE_DESTINATION"; then - PROFILE_ROLLBACK_APPLIED=1 - print "The original guest DeviceProfiles.ini was restored; the journal will be cleared on the next cold boot." - else - print "Warning: guest DeviceProfiles.ini could not be restored before the AVD shut down." - fi - else - if "$ADB" -s "$SERIAL" shell rm -f "$PROFILE_DESTINATION"; then - PROFILE_ROLLBACK_APPLIED=1 - print "The temporary guest DeviceProfiles.ini was removed; the journal will be cleared on the next cold boot." - else - print "Warning: the guest profile marker remains for recovery on the next launch." - fi - fi - if [[ "$PROFILE_ROLLBACK_APPLIED" == "1" ]]; then - PROFILE_MANAGED=0 - fi - fi - fi - if [[ "$OVERLAY_MOUNTED" == "1" && -n "$OVERLAY_BASE_PATH" ]]; then - if "$ADB" -s "$SERIAL" shell umount "$OVERLAY_BASE_PATH" >/dev/null 2>&1; then - OVERLAY_MOUNTED=0 - else - print "Warning: the APK overlay is busy; shutting down the AVD will clear the mount." - fi - fi - if [[ "$UNREAL_LIB_MOUNTED" == "1" && -n "$UNREAL_LIB_TARGET" ]]; then - if "$ADB" -s "$SERIAL" shell umount "$UNREAL_LIB_TARGET" >/dev/null 2>&1; then - UNREAL_LIB_MOUNTED=0 - print "The temporary libUnreal.so overlay was removed." - else - print "Warning: the libUnreal.so overlay is busy; shutting down the AVD will clear the mount." - fi - fi - fi - if [[ "$ANGLE_SETTINGS_MANAGED" == "1" ]] \ - && "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; then - typeset setting_name original_value - for setting_name original_value in \ - angle_gl_driver_selection_pkgs "$ANGLE_ORIGINAL_PACKAGES" \ - angle_gl_driver_selection_values "$ANGLE_ORIGINAL_VALUES" \ - angle_egl_features "$ANGLE_ORIGINAL_FEATURES"; do - if [[ "$original_value" == "null" ]]; then - "$ADB" -s "$SERIAL" shell settings delete global "$setting_name" >/dev/null 2>&1 || true - else - "$ADB" -s "$SERIAL" shell settings put global "$setting_name" "$original_value" >/dev/null 2>&1 || true - fi - done - ANGLE_SETTINGS_MANAGED=0 - print "The original Android graphics-driver selection was restored." - fi - if [[ "$GUEST_WRAP_MANAGED" == "1" ]] \ - && "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; then - if "$ADB" -s "$SERIAL" shell \ - "setprop '$WRAP_PROPERTY' '$GUEST_WRAP_ORIGINAL'" >/dev/null 2>&1; then - typeset restored_guest_wrap - restored_guest_wrap="$("$ADB" -s "$SERIAL" shell getprop "$WRAP_PROPERTY" 2>/dev/null | tr -d '\r')" - if [[ "$restored_guest_wrap" == "$GUEST_WRAP_ORIGINAL" ]]; then - GUEST_WRAP_MANAGED=0 - print "The original Android process wrapper was restored and verified." - else - print "Warning: the Android process wrapper does not match the original after rollback." - fi - else - print "Warning: the Android process wrapper could not be restored." - fi - fi - if [[ "$HWUI_RENDERER_MANAGED" == "1" ]] \ - && "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; then - if "$ADB" -s "$SERIAL" shell \ - "setprop debug.hwui.renderer '$HWUI_RENDERER_ORIGINAL'" >/dev/null 2>&1; then - typeset restored_hwui_renderer - restored_hwui_renderer="$( - "$ADB" -s "$SERIAL" shell getprop debug.hwui.renderer 2>/dev/null \ - | tr -d '\r' - )" - if [[ "$restored_hwui_renderer" == "$HWUI_RENDERER_ORIGINAL" ]]; then - HWUI_RENDERER_MANAGED=0 - print "The original Android HWUI renderer was restored: ${HWUI_RENDERER_ORIGINAL:-unset}." - else - print "Warning: the Android HWUI renderer does not match the original after rollback." - fi - else - print "Warning: the Android HWUI renderer could not be restored." - fi - fi - if kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - "$ADB" -s "$SERIAL" emu kill >/dev/null 2>&1 || true - wait "$EMULATOR_PID" >/dev/null 2>&1 || true - fi - release_avd_lock -} -trap cleanup EXIT -trap 'exit 130' INT TERM - -print "Waiting for the dedicated userdebug/rootable TFT AVD to boot..." -typeset -i adb_waited=0 -until "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; do - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited before ADB connected." - exit 0 - fi - if (( adb_waited >= BOOT_TIMEOUT_SECONDS )); then - print "ADB did not become available within ${BOOT_TIMEOUT_SECONDS} seconds." - exit 1 - fi - sleep 1 - (( adb_waited += 1 )) -done - -typeset -i boot_waited=0 -until [[ "$("$ADB" -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]]; do - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited before Android finished booting." - exit 0 - fi - if (( boot_waited >= BOOT_TIMEOUT_SECONDS )); then - print "Android did not finish booting within ${BOOT_TIMEOUT_SECONDS} seconds." - exit 1 - fi - sleep 1 - (( boot_waited += 1 )) -done - -typeset -i root_attempt -for (( root_attempt = 1; root_attempt <= 10; root_attempt++ )); do - "$ADB" -s "$SERIAL" root >/dev/null 2>&1 || true - typeset -i root_waited=0 - until "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; do - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited while adbd was restarting." - exit 0 - fi - if (( root_waited >= 10 )); then - break - fi - sleep 1 - (( root_waited += 1 )) - done - if [[ "$("$ADB" -s "$SERIAL" shell id -u 2>/dev/null | tr -d '\r')" == "0" ]]; then - break - fi - sleep 1 -done - -if [[ "$("$ADB" -s "$SERIAL" shell id -u 2>/dev/null | tr -d '\r')" != "0" ]]; then - print "Official adbd root could not be enabled." - exit 1 -fi - -if [[ -n "$GL_DRAW_FLUSH_INTERVAL" ]]; then - readonly ACTIVE_DRAW_FLUSH_INTERVAL="$( - "$ADB" -s "$SERIAL" shell getprop ro.boot.qemu.gltransport.drawFlushInterval 2>/dev/null \ - | tr -d '\r' - )" - if [[ "$ACTIVE_DRAW_FLUSH_INTERVAL" != "$GL_DRAW_FLUSH_INTERVAL" ]]; then - print "The draw flush interval was not applied: expected $GL_DRAW_FLUSH_INTERVAL, active ${ACTIVE_DRAW_FLUSH_INTERVAL:-unknown}." - exit 1 - fi -fi - -"$ADB" -s "$SERIAL" shell wm size "$DISPLAY_SIZE" -"$ADB" -s "$SERIAL" shell wm density "$DISPLAY_DENSITY" - -if [[ "$INPUT_BRIDGE_ENABLED" == "1" ]]; then - readonly DISPLAY_WIDTH="${DISPLAY_SIZE%x*}" - readonly DISPLAY_HEIGHT="${DISPLAY_SIZE#*x}" - "$INPUT_BRIDGE_BINARY" \ - --target-pid "$EMULATOR_PID" \ - --target-bundle-id "dev.sergeinaumov.mactician.experiment-emulator" \ - --adb "$ADB" \ - --adb-port "$ADB_SERVER_PORT" \ - --serial "$SERIAL" \ - --display-width "$DISPLAY_WIDTH" \ - --display-height "$DISPLAY_HEIGHT" \ - --diagnostics "$INPUT_DIAGNOSTICS" \ - --diagnostics-log "$INPUT_DIAGNOSTICS_LOG" \ - --shop-point "$INPUT_SHOP_POINT" \ - --reroll-point "$INPUT_REROLL_POINT" \ - --xp-point "$INPUT_XP_POINT" \ - --traits-point "$INPUT_TRAITS_POINT" \ - --items-point "$INPUT_ITEMS_POINT" \ - --damage-point "$INPUT_DAMAGE_POINT" \ - --players-point "$INPUT_PLAYERS_POINT" & - INPUT_BRIDGE_PID=$! - sleep 0.2 - if ! kill -0 "$INPUT_BRIDGE_PID" >/dev/null 2>&1; then - wait "$INPUT_BRIDGE_PID" || true - print "The input bridge exited immediately after launch." - exit 1 - fi -fi - -if ! "$ADB" -s "$SERIAL" shell pm path "$PACKAGE" | grep -q '^package:'; then - print "TFT PBE is not installed in the rootable AVD. The launcher does not copy the APK or private data." - exit 1 -fi - -readonly PROFILE_DIR="/data/user/0/$PACKAGE/files/UnrealGame/TFT/TFT/Saved/Config/Android" -PROFILE_DESTINATION="$PROFILE_DIR/DeviceProfiles.ini" -readonly DATA_OWNER="$( - "$ADB" -s "$SERIAL" shell stat -c '%u:%g' "/data/user/0/$PACKAGE" \ - | tr -d '\r' -)" -if [[ -z "$DATA_OWNER" ]]; then - print "The owner of TFT private data could not be determined." - exit 1 -fi - -# Recover a profile transaction left by SIGKILL/host failure before applying a -# new overlay. This also makes a later stock-root launch self-healing. -typeset RECOVERY_REMOTE_DIR RECOVERY_BACKUP RECOVERY_PRESENT_MARKER RECOVERY_ABSENT_MARKER -for RECOVERY_REMOTE_DIR in "$DIRECT_VULKAN_REMOTE_DIR" "$ANGLE_OPENGL_REMOTE_DIR"; do - RECOVERY_BACKUP="$RECOVERY_REMOTE_DIR/DeviceProfiles.ini.mactician-backup" - RECOVERY_PRESENT_MARKER="$RECOVERY_REMOTE_DIR/DeviceProfiles.ini.mactician-present" - RECOVERY_ABSENT_MARKER="$RECOVERY_REMOTE_DIR/DeviceProfiles.ini.mactician-absent" - if "$ADB" -s "$SERIAL" shell test -f "$RECOVERY_PRESENT_MARKER"; then - if ! "$ADB" -s "$SERIAL" shell test -f "$RECOVERY_BACKUP"; then - print "The rollback guest profile is corrupted: $RECOVERY_BACKUP is missing" - exit 1 - fi - "$ADB" -s "$SERIAL" shell mkdir -p "$PROFILE_DIR" - "$ADB" -s "$SERIAL" shell cp -p "$RECOVERY_BACKUP" "$PROFILE_DESTINATION" - "$ADB" -s "$SERIAL" shell rm -f "$RECOVERY_BACKUP" "$RECOVERY_PRESENT_MARKER" - print "Guest DeviceProfiles.ini was restored after an interrupted launch." - elif "$ADB" -s "$SERIAL" shell test -f "$RECOVERY_ABSENT_MARKER"; then - "$ADB" -s "$SERIAL" shell rm -f "$PROFILE_DESTINATION" "$RECOVERY_ABSENT_MARKER" - print "The temporary guest DeviceProfiles.ini was removed after an interrupted launch." - elif "$ADB" -s "$SERIAL" shell test -f "$RECOVERY_BACKUP"; then - print "A guest profile backup without a transaction marker was found: $RECOVERY_BACKUP" - exit 1 - fi -done - -if [[ -n "$OVERLAY_APK" ]]; then - OVERLAY_BASE_PATH="$( - "$ADB" -s "$SERIAL" shell pm path "$PACKAGE" \ - | tr -d '\r' \ - | sed -n 's/^package:\(.*\/base\.apk\)$/\1/p' \ - | head -n 1 - )" - if [[ -z "$OVERLAY_BASE_PATH" ]]; then - print "The installed TFT base.apk could not be located." - exit 1 - fi - - readonly EXISTING_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat /proc/self/mountinfo \ - | tr -d '\r' \ - | awk -v target="$OVERLAY_BASE_PATH" '$5 == target { count++ } END { print count + 0 }' - )" - if [[ "$EXISTING_MOUNT_COUNT" != "0" ]]; then - print "$OVERLAY_LABEL invariant violation: mounts=$EXISTING_MOUNT_COUNT were found before launch." - print "Shut down the AVD; the launcher will not stack another APK overlay." - exit 1 - fi - - readonly OVERLAY_TARGET_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$OVERLAY_BASE_PATH" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$OVERLAY_TARGET_CONTEXT" != "u:object_r:apk_data_file:s0" ]]; then - print "Unexpected SELinux context for the installed base.apk: ${OVERLAY_TARGET_CONTEXT:-empty}." - exit 1 - fi - - readonly INSTALLED_BASE_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$OVERLAY_BASE_PATH" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$INSTALLED_BASE_SHA256" != "$ORIGINAL_BASE_SHA256" ]]; then - print "The installed base.apk does not match the signed TFT PBE update manifest." - print "Actual SHA-256: $INSTALLED_BASE_SHA256" - exit 42 - fi - - "$ADB" -s "$SERIAL" shell mkdir -p "$OVERLAY_REMOTE_DIR" - "$ADB" -s "$SERIAL" push "$OVERLAY_APK" "$OVERLAY_REMOTE_DIR/base.apk.next" >/dev/null - "$ADB" -s "$SERIAL" shell chmod 644 "$OVERLAY_REMOTE_DIR/base.apk.next" - "$ADB" -s "$SERIAL" shell chcon "$OVERLAY_TARGET_CONTEXT" "$OVERLAY_REMOTE_DIR/base.apk.next" - "$ADB" -s "$SERIAL" shell mv "$OVERLAY_REMOTE_DIR/base.apk.next" "$OVERLAY_REMOTE_DIR/base.apk" - - readonly REMOTE_OVERLAY_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$OVERLAY_REMOTE_DIR/base.apk" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - readonly REMOTE_OVERLAY_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$OVERLAY_REMOTE_DIR/base.apk" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$REMOTE_OVERLAY_SHA256" != "$OVERLAY_SHA256" \ - || "$REMOTE_OVERLAY_CONTEXT" != "$OVERLAY_TARGET_CONTEXT" ]]; then - print "The uploaded $OVERLAY_LABEL APK is corrupted: $REMOTE_OVERLAY_SHA256" - exit 1 - fi - - "$ADB" -s "$SERIAL" shell mount -o bind "$OVERLAY_REMOTE_DIR/base.apk" "$OVERLAY_BASE_PATH" - OVERLAY_MOUNTED=1 - - readonly ACTIVE_BASE_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$OVERLAY_BASE_PATH" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - readonly ACTIVE_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat /proc/self/mountinfo \ - | tr -d '\r' \ - | awk -v target="$OVERLAY_BASE_PATH" '$5 == target { count++ } END { print count + 0 }' - )" - readonly ACTIVE_BASE_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$OVERLAY_BASE_PATH" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$ACTIVE_BASE_SHA256" != "$OVERLAY_SHA256" \ - || "$ACTIVE_MOUNT_COUNT" != "1" \ - || "$ACTIVE_BASE_CONTEXT" != "$OVERLAY_TARGET_CONTEXT" ]]; then - print "A single verified $OVERLAY_LABEL overlay could not be mounted." - exit 1 - fi - - readonly PROFILE_REMOTE="$OVERLAY_REMOTE_DIR/DeviceProfiles.ini" - - "$ADB" -s "$SERIAL" push "$OVERLAY_PROFILE" "$PROFILE_REMOTE" >/dev/null - "$ADB" -s "$SERIAL" shell mkdir -p "$PROFILE_DIR" - readonly PROFILE_TARGET_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$PROFILE_DIR" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ ! "$PROFILE_TARGET_CONTEXT" =~ '^u:object_r:[A-Za-z0-9_]+:s0(:c[0-9]+(,c[0-9]+)*)?$' ]]; then - print "The guest profile SELinux context could not be determined safely: $PROFILE_TARGET_CONTEXT" - exit 1 - fi - "$ADB" -s "$SERIAL" shell chcon "$PROFILE_TARGET_CONTEXT" "$PROFILE_REMOTE" - "$ADB" -s "$SERIAL" shell chown 0:0 "$PROFILE_REMOTE" - "$ADB" -s "$SERIAL" shell chmod 444 "$PROFILE_REMOTE" - readonly EXPECTED_PROFILE_SHA256="$(shasum -a 256 "$OVERLAY_PROFILE" | awk '{ print $1 }')" - readonly REMOTE_PROFILE_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$PROFILE_REMOTE" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$REMOTE_PROFILE_SHA256" != "$EXPECTED_PROFILE_SHA256" ]]; then - print "The uploaded guest DeviceProfiles.ini is corrupted: $REMOTE_PROFILE_SHA256" - exit 1 - fi - PROFILE_BACKUP="$OVERLAY_REMOTE_DIR/DeviceProfiles.ini.mactician-backup" - PROFILE_PRESENT_MARKER="$OVERLAY_REMOTE_DIR/DeviceProfiles.ini.mactician-present" - PROFILE_ABSENT_MARKER="$OVERLAY_REMOTE_DIR/DeviceProfiles.ini.mactician-absent" - if "$ADB" -s "$SERIAL" shell test -f "$PROFILE_DESTINATION"; then - PROFILE_WAS_PRESENT=1 - PROFILE_ORIGINAL_OWNER="$("$ADB" -s "$SERIAL" shell stat -c '%u:%g' "$PROFILE_DESTINATION" | tr -d '\r')" - PROFILE_ORIGINAL_MODE="$("$ADB" -s "$SERIAL" shell stat -c '%a' "$PROFILE_DESTINATION" | tr -d '\r')" - "$ADB" -s "$SERIAL" shell cp -p "$PROFILE_DESTINATION" "$PROFILE_BACKUP" - "$ADB" -s "$SERIAL" shell touch "$PROFILE_PRESENT_MARKER" - else - PROFILE_WAS_PRESENT=0 - "$ADB" -s "$SERIAL" shell touch "$PROFILE_ABSENT_MARKER" - fi - PROFILE_MANAGED=1 - "$ADB" -s "$SERIAL" shell cp "$PROFILE_REMOTE" "$PROFILE_DESTINATION" - "$ADB" -s "$SERIAL" shell chown "$DATA_OWNER" "$PROFILE_DESTINATION" - "$ADB" -s "$SERIAL" shell chmod 600 "$PROFILE_DESTINATION" - readonly EXISTING_PROFILE_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat /proc/self/mountinfo \ - | tr -d '\r' \ - | awk -v target="$PROFILE_DESTINATION" '$5 == target { count++ } END { print count + 0 }' - )" - if [[ "$EXISTING_PROFILE_MOUNT_COUNT" != "0" ]]; then - print "Guest DeviceProfiles.ini invariant violation: mounts=$EXISTING_PROFILE_MOUNT_COUNT were found before the bind mount." - exit 1 - fi - "$ADB" -s "$SERIAL" shell mount -o bind "$PROFILE_REMOTE" "$PROFILE_DESTINATION" - PROFILE_MOUNTED=1 - readonly ACTIVE_PROFILE_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$PROFILE_DESTINATION" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - readonly ACTIVE_PROFILE_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat /proc/self/mountinfo \ - | tr -d '\r' \ - | awk -v target="$PROFILE_DESTINATION" '$5 == target { count++ } END { print count + 0 }' - )" - readonly ACTIVE_PROFILE_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$PROFILE_DESTINATION" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$ACTIVE_PROFILE_SHA256" != "$EXPECTED_PROFILE_SHA256" \ - || "$ACTIVE_PROFILE_MOUNT_COUNT" != "1" \ - || "$ACTIVE_PROFILE_CONTEXT" != "$PROFILE_TARGET_CONTEXT" ]]; then - print "A single guest DeviceProfiles.ini mount could not be established." - exit 1 - fi -fi - -# Unreal exposes ApplicationScale as the global multiplier applied after its -# resolution-dependent DPI rule. Stage it before every game start without -# changing the 3D framebuffer resolution or r.ScreenPercentage. TFT may later -# normalize Saved/Config/Android/Engine.ini, so the launcher preference remains -# the source of truth and is reapplied on every Play. -readonly ENGINE_CONFIG="$PROFILE_DIR/Engine.ini" -UI_SCALE_TEMP_DIR="$(mktemp -d /private/tmp/tft-ui-scale.XXXXXX)" -readonly ENGINE_CONFIG_CURRENT="$UI_SCALE_TEMP_DIR/Engine.ini.current" -readonly ENGINE_CONFIG_NEXT="$UI_SCALE_TEMP_DIR/Engine.ini.next" -"$ADB" -s "$SERIAL" shell mkdir -p "$PROFILE_DIR" -if "$ADB" -s "$SERIAL" shell test -f "$ENGINE_CONFIG"; then - "$ADB" -s "$SERIAL" pull "$ENGINE_CONFIG" "$ENGINE_CONFIG_CURRENT" >/dev/null -else - /usr/bin/touch "$ENGINE_CONFIG_CURRENT" -fi -/usr/bin/awk -v scale="$UI_SCALE" ' - BEGIN { - section = "[/Script/Engine.UserInterfaceSettings]" - found = 0 - in_section = 0 - } - $0 == section { - print - print "ApplicationScale=" scale - found = 1 - in_section = 1 - next - } - /^\[/ { in_section = 0 } - in_section && /^[[:space:]]*ApplicationScale[[:space:]]*=/ { next } - { print } - END { - if (!found) { - print "" - print section - print "ApplicationScale=" scale - } - } -' "$ENGINE_CONFIG_CURRENT" > "$ENGINE_CONFIG_NEXT" -readonly ENGINE_CONFIG_CONTEXT="$( - if "$ADB" -s "$SERIAL" shell test -f "$ENGINE_CONFIG"; then - "$ADB" -s "$SERIAL" shell ls -Zd "$ENGINE_CONFIG" - else - "$ADB" -s "$SERIAL" shell ls -Zd "$PROFILE_DIR" - fi | tr -d '\r' | awk '{ print $1 }' -)" -if [[ ! "$ENGINE_CONFIG_CONTEXT" =~ '^u:object_r:[A-Za-z0-9_]+:s0(:c[0-9]+(,c[0-9]+)*)?$' ]]; then - print "The Engine.ini SELinux context could not be determined safely: $ENGINE_CONFIG_CONTEXT" - exit 1 -fi -readonly ENGINE_CONFIG_REMOTE_NEXT="$PROFILE_DIR/Engine.ini.ui-scale-next" -"$ADB" -s "$SERIAL" push "$ENGINE_CONFIG_NEXT" "$ENGINE_CONFIG_REMOTE_NEXT" >/dev/null -"$ADB" -s "$SERIAL" shell chown "$DATA_OWNER" "$ENGINE_CONFIG_REMOTE_NEXT" -"$ADB" -s "$SERIAL" shell chmod 600 "$ENGINE_CONFIG_REMOTE_NEXT" -"$ADB" -s "$SERIAL" shell chcon "$ENGINE_CONFIG_CONTEXT" "$ENGINE_CONFIG_REMOTE_NEXT" -"$ADB" -s "$SERIAL" shell mv "$ENGINE_CONFIG_REMOTE_NEXT" "$ENGINE_CONFIG" -readonly EXPECTED_ENGINE_CONFIG_SHA256="$(shasum -a 256 "$ENGINE_CONFIG_NEXT" | awk '{ print $1 }')" -readonly ACTIVE_ENGINE_CONFIG_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$ENGINE_CONFIG" \ - | tr -d '\r' \ - | awk '{ print $1 }' -)" -if [[ "$ACTIVE_ENGINE_CONFIG_SHA256" != "$EXPECTED_ENGINE_CONFIG_SHA256" ]]; then - print "Engine.ini with the UI scale failed SHA-256 verification: ${ACTIVE_ENGINE_CONFIG_SHA256:-empty}." - exit 1 -fi -rm -rf "$UI_SCALE_TEMP_DIR" -UI_SCALE_TEMP_DIR="" -print "Unreal UI scale is configured: ${UI_SCALE}x with a ${DISPLAY_SIZE} framebuffer." - -if [[ -n "$UNREAL_LIB_OVERLAY" ]]; then - if [[ -z "$OVERLAY_BASE_PATH" ]]; then - print "The temporary libUnreal.so overlay requires a verified base.apk target." - exit 1 - fi - UNREAL_LIB_TARGET="${OVERLAY_BASE_PATH%/base.apk}/lib/arm64/libUnreal.so" - readonly INSTALLED_UNREAL_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$UNREAL_LIB_TARGET" 2>/dev/null \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$INSTALLED_UNREAL_SHA256" != "$UNREAL_LIB_ORIGINAL_SHA256" ]]; then - print "The installed libUnreal.so does not match the verified original: $INSTALLED_UNREAL_SHA256" - exit 1 - fi - readonly EXISTING_UNREAL_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat /proc/self/mountinfo \ - | tr -d '\r' \ - | awk -v target="$UNREAL_LIB_TARGET" '$5 == target { count++ } END { print count + 0 }' - )" - if [[ "$EXISTING_UNREAL_MOUNT_COUNT" != "0" ]]; then - print "A libUnreal.so mount already exists before launch; shut down the AVD before applying another overlay." - exit 1 - fi - - readonly UNREAL_REMOTE="$UNREAL_LIB_REMOTE_DIR/libUnreal.so" - readonly UNREAL_TARGET_OWNER="$("$ADB" -s "$SERIAL" shell stat -c '%u:%g' "$UNREAL_LIB_TARGET" | tr -d '\r')" - readonly UNREAL_TARGET_MODE="$("$ADB" -s "$SERIAL" shell stat -c '%a' "$UNREAL_LIB_TARGET" | tr -d '\r')" - readonly UNREAL_TARGET_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$UNREAL_LIB_TARGET" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ ! "$UNREAL_TARGET_CONTEXT" =~ '^u:object_r:[A-Za-z0-9_]+:s0$' ]]; then - print "The libUnreal.so SELinux context could not be determined safely: $UNREAL_TARGET_CONTEXT" - exit 1 - fi - "$ADB" -s "$SERIAL" shell mkdir -p "$UNREAL_LIB_REMOTE_DIR" - "$ADB" -s "$SERIAL" push "$UNREAL_LIB_OVERLAY" "$UNREAL_REMOTE.next" >/dev/null - "$ADB" -s "$SERIAL" shell chmod "$UNREAL_TARGET_MODE" "$UNREAL_REMOTE.next" - "$ADB" -s "$SERIAL" shell chown "$UNREAL_TARGET_OWNER" "$UNREAL_REMOTE.next" - "$ADB" -s "$SERIAL" shell chcon "$UNREAL_TARGET_CONTEXT" "$UNREAL_REMOTE.next" - readonly UNREAL_REMOTE_CONTEXT="$( - "$ADB" -s "$SERIAL" shell ls -Zd "$UNREAL_REMOTE.next" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - if [[ "$UNREAL_REMOTE_CONTEXT" != "$UNREAL_TARGET_CONTEXT" ]]; then - print "The staging libUnreal.so SELinux context does not match the target." - exit 1 - fi - "$ADB" -s "$SERIAL" shell mv "$UNREAL_REMOTE.next" "$UNREAL_REMOTE" - "$ADB" -s "$SERIAL" shell mount -o bind "$UNREAL_REMOTE" "$UNREAL_LIB_TARGET" - UNREAL_LIB_MOUNTED=1 - - readonly ACTIVE_UNREAL_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "$UNREAL_LIB_TARGET" \ - | tr -d '\r' \ - | awk '{ print $1 }' - )" - readonly ACTIVE_UNREAL_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat /proc/self/mountinfo \ - | tr -d '\r' \ - | awk -v target="$UNREAL_LIB_TARGET" '$5 == target { count++ } END { print count + 0 }' - )" - if [[ "$ACTIVE_UNREAL_SHA256" != "$UNREAL_LIB_OVERLAY_SHA256" \ - || "$ACTIVE_UNREAL_MOUNT_COUNT" != "1" ]]; then - print "A single verified libUnreal.so overlay could not be mounted." - exit 1 - fi -fi - -if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - ANGLE_ORIGINAL_PACKAGES="$("$ADB" -s "$SERIAL" shell settings get global angle_gl_driver_selection_pkgs | tr -d '\r')" - ANGLE_ORIGINAL_VALUES="$("$ADB" -s "$SERIAL" shell settings get global angle_gl_driver_selection_values | tr -d '\r')" - ANGLE_ORIGINAL_FEATURES="$("$ADB" -s "$SERIAL" shell settings get global angle_egl_features | tr -d '\r')" - ANGLE_SETTINGS_MANAGED=1 -fi - -"$ADB" -s "$SERIAL" shell settings put global angle_gl_driver_selection_pkgs "$PACKAGE" -"$ADB" -s "$SERIAL" shell settings put global angle_gl_driver_selection_values "$GUEST_GL_DRIVER" -if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - "$ADB" -s "$SERIAL" shell settings delete global angle_egl_features >/dev/null - "$ADB" -s "$SERIAL" shell "setprop debug.angle.feature_overrides_enabled ''" - "$ADB" -s "$SERIAL" shell "setprop debug.angle.feature_overrides_disabled ''" -else - "$ADB" -s "$SERIAL" shell settings put global angle_egl_features "$ANGLE_FEATURES" - "$ADB" -s "$SERIAL" shell setprop debug.angle.feature_overrides_enabled "$ANGLE_FEATURES" - if [[ -n "$ANGLE_DISABLED_FEATURES" ]]; then - "$ADB" -s "$SERIAL" shell setprop debug.angle.feature_overrides_disabled "$ANGLE_DISABLED_FEATURES" - else - "$ADB" -s "$SERIAL" shell "setprop debug.angle.feature_overrides_disabled ''" - fi -fi -"$ADB" -s "$SERIAL" shell settings put global show_angle_in_use_dialog_box 0 - -if [[ "$GUEST_SUBMIT_THREAD" != "default" ]]; then - GUEST_WRAP_ORIGINAL="$("$ADB" -s "$SERIAL" shell getprop "$WRAP_PROPERTY" | tr -d '\r')" - if [[ ! "$GUEST_WRAP_ORIGINAL" =~ '^[-A-Za-z0-9_./= ]*$' ]]; then - print "The original Android process wrapper contains unsafe characters; refusing to modify it." - exit 1 - fi - GUEST_WRAP_MANAGED=1 - if [[ "$GUEST_SUBMIT_THREAD" == "on-demand" ]]; then - "$ADB" -s "$SERIAL" shell "setprop '$WRAP_PROPERTY' '/system/bin/env'" - readonly EXPECTED_GUEST_WRAP="/system/bin/env" - else - "$ADB" -s "$SERIAL" shell \ - "setprop '$WRAP_PROPERTY' '/system/bin/env MESA_VK_ENABLE_SUBMIT_THREAD=$GUEST_SUBMIT_THREAD'" - readonly EXPECTED_GUEST_WRAP="/system/bin/env MESA_VK_ENABLE_SUBMIT_THREAD=$GUEST_SUBMIT_THREAD" - fi - readonly ACTIVE_GUEST_WRAP="$("$ADB" -s "$SERIAL" shell getprop "$WRAP_PROPERTY" | tr -d '\r')" - if [[ "$ACTIVE_GUEST_WRAP" != "$EXPECTED_GUEST_WRAP" ]]; then - print "The Android process wrapper was not applied: ${ACTIVE_GUEST_WRAP:-empty}." - exit 1 - fi -fi - -HWUI_RENDERER_ORIGINAL="$( - "$ADB" -s "$SERIAL" shell getprop debug.hwui.renderer \ - | tr -d '\r' -)" -if [[ ! "$HWUI_RENDERER_ORIGINAL" =~ '^[-A-Za-z0-9._]*$' ]]; then - print "The original Android HWUI renderer contains unsafe characters." - exit 1 -fi -if [[ "$HWUI_RENDERER_ORIGINAL" != "$HWUI_RENDERER" ]]; then - "$ADB" -s "$SERIAL" shell setprop debug.hwui.renderer "$HWUI_RENDERER" - HWUI_RENDERER_MANAGED=1 -fi -readonly ACTIVE_HWUI_RENDERER="$( - "$ADB" -s "$SERIAL" shell getprop debug.hwui.renderer \ - | tr -d '\r' -)" -if [[ "$ACTIVE_HWUI_RENDERER" != "$HWUI_RENDERER" ]]; then - print "The Android HWUI renderer was not applied: ${ACTIVE_HWUI_RENDERER:-empty}." - exit 1 -fi - -if ! "$ADB" -s "$SERIAL" shell cmd locale set-app-locales "$PACKAGE" "$GAME_LANGUAGE" >/dev/null 2>&1; then - print "Android app locale could not be applied; Unreal will use the selected culture from UECommandLine." -fi -"$ADB" -s "$SERIAL" shell am force-stop "$PACKAGE" - -# Riot's streaming installer can leave a zero-byte release manifest and sparse -# chunk placeholders behind when its first download is interrupted. On every -# later start the game tries to repair that cache, fails with "Truncated -# manifest header", and leaves the patching screen waiting forever. The cache -# contains only downloadable public game resources, so reset that directory -# when the exact corrupt state is present. Login and other app data stay intact. -readonly STREAMING_INSTALL_DIR="/sdcard/Android/data/$PACKAGE/files/StreamingInstalls" -readonly STREAMING_INSTALL_MANIFEST="$STREAMING_INSTALL_DIR/Metadata.manifest" -if "$ADB" -s "$SERIAL" shell test -e "$STREAMING_INSTALL_MANIFEST" \ - && ! "$ADB" -s "$SERIAL" shell test -s "$STREAMING_INSTALL_MANIFEST"; then - print "Empty TFT streaming manifest detected; resetting the downloadable streaming cache." - "$ADB" -s "$SERIAL" shell rm -rf "$STREAMING_INSTALL_DIR" - "$ADB" -s "$SERIAL" shell sync - if "$ADB" -s "$SERIAL" shell test -e "$STREAMING_INSTALL_DIR"; then - print "Could not remove the corrupt TFT streaming cache." - exit 1 - fi - print "Corrupt TFT streaming cache removed; TFT will download the required resources again." -fi - -"$ADB" -s "$SERIAL" shell am start -n "$PACKAGE/$ACTIVITY" - -if [[ "$PROFILE_MOUNTED" == "1" ]]; then - typeset -i profile_waited=0 - typeset PROFILE_GAME_PID PROFILE_PROCESS_MOUNT_COUNT PROFILE_PROCESS_SHA256 - PROFILE_PROCESS_VERIFIED=0 - while (( profile_waited < 90 )); do - PROFILE_GAME_PID="$("$ADB" -s "$SERIAL" shell pidof "$PACKAGE" 2>/dev/null | tr -d '\r' || true)" - PROFILE_GAME_PID="${PROFILE_GAME_PID%% *}" - if [[ -n "$PROFILE_GAME_PID" ]]; then - PROFILE_PROCESS_MOUNT_COUNT="$( - "$ADB" -s "$SERIAL" shell cat "/proc/$PROFILE_GAME_PID/mountinfo" 2>/dev/null \ - | tr -d '\r' \ - | awk -v target="$PROFILE_DESTINATION" '$5 == target { count++ } END { print count + 0 }' \ - || true - )" - PROFILE_PROCESS_SHA256="$( - "$ADB" -s "$SERIAL" shell sha256sum "/proc/$PROFILE_GAME_PID/root$PROFILE_DESTINATION" 2>/dev/null \ - | tr -d '\r' \ - | awk '{ print $1 }' \ - || true - )" - if [[ "$PROFILE_PROCESS_MOUNT_COUNT" == "1" \ - && "$PROFILE_PROCESS_SHA256" == "$EXPECTED_PROFILE_SHA256" ]]; then - PROFILE_PROCESS_VERIFIED=1 - break - fi - fi - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited while the guest profile namespace was being verified." - exit 0 - fi - sleep 1 - (( profile_waited += 1 )) - done - if [[ "$PROFILE_PROCESS_VERIFIED" != "1" ]]; then - print "Guest DeviceProfiles.ini with the expected SHA-256 is not visible in the TFT mount namespace." - exit 1 - fi - print "Guest DeviceProfiles.ini was verified in the mount namespace of TFT PID $PROFILE_GAME_PID." -fi - -if [[ "$GUEST_SUBMIT_THREAD" == "0" || "$GUEST_SUBMIT_THREAD" == "1" ]]; then - typeset -i submit_waited=0 - typeset SUBMIT_GAME_PID ACTIVE_SUBMIT_ENV - SUBMIT_ENV_VERIFIED=0 - while (( submit_waited < 90 )); do - SUBMIT_GAME_PID="$("$ADB" -s "$SERIAL" shell pidof "$PACKAGE" 2>/dev/null | tr -d '\r' || true)" - if [[ -n "$SUBMIT_GAME_PID" ]]; then - ACTIVE_SUBMIT_ENV="$( - "$ADB" -s "$SERIAL" shell cat "/proc/$SUBMIT_GAME_PID/environ" 2>/dev/null \ - | tr '\0' '\n' \ - | grep -Fx "MESA_VK_ENABLE_SUBMIT_THREAD=$GUEST_SUBMIT_THREAD" || true - )" - if [[ "$ACTIVE_SUBMIT_ENV" == "MESA_VK_ENABLE_SUBMIT_THREAD=$GUEST_SUBMIT_THREAD" ]]; then - SUBMIT_ENV_VERIFIED=1 - break - fi - fi - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited while the guest submit thread was being verified." - exit 0 - fi - sleep 1 - (( submit_waited += 1 )) - done - if [[ "$SUBMIT_ENV_VERIFIED" != "1" ]]; then - print "MESA_VK_ENABLE_SUBMIT_THREAD did not appear in the TFT environment within 90 seconds." - exit 1 - fi - print "The guest Vulkan submit mode was verified through /proc/PID/environ: $GUEST_SUBMIT_THREAD." -elif [[ "$GUEST_SUBMIT_THREAD" == "on-demand" ]]; then - typeset -i control_waited=0 - typeset CONTROL_GAME_PID CONTROL_SUBMIT_ENV - CONTROL_ENV_VERIFIED=0 - while (( control_waited < 90 )); do - CONTROL_GAME_PID="$("$ADB" -s "$SERIAL" shell pidof "$PACKAGE" 2>/dev/null | tr -d '\r' || true)" - CONTROL_GAME_PID="${CONTROL_GAME_PID%% *}" - if [[ -n "$CONTROL_GAME_PID" ]]; then - CONTROL_SUBMIT_ENV="$( - "$ADB" -s "$SERIAL" shell cat "/proc/$CONTROL_GAME_PID/environ" 2>/dev/null \ - | tr '\0' '\n' \ - | grep -E '^MESA_VK_ENABLE_SUBMIT_THREAD=' || true - )" - if [[ -z "$CONTROL_SUBMIT_ENV" ]]; then - CONTROL_ENV_VERIFIED=1 - break - fi - fi - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited while the control submit mode was being verified." - exit 0 - fi - sleep 1 - (( control_waited += 1 )) - done - if [[ "$CONTROL_ENV_VERIFIED" != "1" ]]; then - print "The control mode is contaminated by MESA_VK_ENABLE_SUBMIT_THREAD." - exit 1 - fi - print "The guest Vulkan submit control was verified: MESA_VK_ENABLE_SUBMIT_THREAD is absent." -fi - -if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - typeset -i native_waited=0 - typeset GAME_PID GAME_MAPS - NATIVE_GLES_VERIFIED=0 - while (( native_waited < 90 )); do - GAME_PID="$("$ADB" -s "$SERIAL" shell pidof "$PACKAGE" 2>/dev/null | tr -d '\r' || true)" - if [[ -n "$GAME_PID" ]]; then - GAME_MAPS="$("$ADB" -s "$SERIAL" shell cat "/proc/$GAME_PID/maps" 2>/dev/null | tr -d '\r' || true)" - if [[ "$GAME_MAPS" == *libEGL_emulation.so* \ - && "$GAME_MAPS" == *libGLESv2_emulation.so* \ - && "$GAME_MAPS" == *libGLESv2_enc.so* ]]; then - if [[ "$GAME_MAPS" == *libGLESv2_angle.so* ]]; then - print "Native GLES verification failed: guest ANGLE is still loaded." - exit 1 - fi - NATIVE_GLES_VERIFIED=1 - break - fi - fi - if ! kill -0 "$EMULATOR_PID" >/dev/null 2>&1; then - print "The emulator exited while native GLES was being verified." - exit 0 - fi - sleep 1 - (( native_waited += 1 )) - done - if [[ "$NATIVE_GLES_VERIFIED" != "1" ]]; then - print "Native GLES could not be verified through /proc/PID/maps within 90 seconds." - exit 1 - fi - print "Native GLES was verified: the gfxstream GLES encoder is loaded and guest ANGLE is absent." -fi - -TFT_SERIAL="$SERIAL" \ -TFT_ADB="$ADB" \ -TFT_PSO_CPU_LIST="0-$(( CPU_CORES - 1 ))" \ - "$PROJECT_DIR/scripts/watch-root-pso.command" & -WATCHER_PID=$! - -case "$RENDERER" in - direct-vulkan) - print "TFT is running: Unreal Vulkan -> gfxstream -> MoltenVK -> Metal, ${DISPLAY_SIZE}@${DISPLAY_DENSITY}dpi@60, audio $AUDIO_STATUS." - ;; - angle-opengl) - if [[ "$GUEST_GL_DRIVER" == "native" ]]; then - if [[ "$HOST_GPU" == "swangle" ]]; then - print "TFT is running: Unreal OpenGL ES -> gfxstream GLES encoder -> host ANGLE -> Vulkan -> SwiftShader CPU, ${DISPLAY_SIZE}@${DISPLAY_DENSITY}dpi@60, audio $AUDIO_STATUS." - else - print "TFT is running: Unreal OpenGL ES -> gfxstream GLES encoder -> host ANGLE -> Metal, ${DISPLAY_SIZE}@${DISPLAY_DENSITY}dpi@60, audio $AUDIO_STATUS." - fi - else - print "TFT is running: Unreal OpenGL ES -> guest ANGLE -> Vulkan -> Metal, ${DISPLAY_SIZE}@${DISPLAY_DENSITY}dpi@60, audio $AUDIO_STATUS." - fi - ;; - angle) - print "TFT is running: stock OpenGL ES -> ANGLE -> Vulkan -> Metal, ${DISPLAY_SIZE}@${DISPLAY_DENSITY}dpi@60, audio $AUDIO_STATUS." - ;; -esac -print "Root remains visible; the watcher only changes live scheduling for PSO threads." -if [[ "$INPUT_BRIDGE_ENABLED" == "1" ]]; then - print "GameActivity controls: Space - shop, D - reroll, F - buy XP, Tab - items/traits, V - players/damage; right-click is blocked only in game." - if [[ "$INPUT_DIAGNOSTICS" == "1" ]]; then - print "Input diagnostics: click marker and host timestamps are enabled; log=${INPUT_DIAGNOSTICS_LOG:-stderr}." - fi -fi -print "Graphics profile: $GRAPHICS_PROFILE." -print "TFT renderer: $RENDERER." -print "Host GPU backend: $HOST_GPU." -print "Android HWUI renderer: $HWUI_RENDERER; the Unreal renderer is unchanged." -print "Guest GL driver: $GUEST_GL_DRIVER." -print "Guest Vulkan submit thread: $GUEST_SUBMIT_THREAD." -print "Guest resources: ${CPU_CORES} cores, ${MEMORY_MB} MB RAM." -print "Unreal UI scale: ${UI_SCALE}x." -print "ANGLE enabled features: $ANGLE_FEATURES." -print "ANGLE disabled features: ${ANGLE_DISABLED_FEATURES:-none}." -if [[ -n "$OVERLAY_APK" ]]; then - print "$OVERLAY_LABEL overlay: the APK and DeviceProfile passed SHA-256 verification and bind mounts were established 0->1." -fi -if [[ "$UNREAL_LIB_MOUNTED" == "1" ]]; then - print "libUnreal.so overlay: SHA-256 and the 0->1 mount invariant were verified before process startup." -fi -if [[ "$BATCHED_DESCRIPTORS" == "1" ]]; then - print "Experiment enabled: VulkanBatchedDescriptorSetUpdate." -fi -if [[ "$VIRTIO_GPU_NATIVE_SYNC" == "1" ]]; then - print "Experiment enabled: VirtioGpuNativeSync." -fi -if [[ "$VIRTIO_GPU_NEXT" == "1" ]]; then - print "Experiment enabled: VirtioGpuNext." -fi -if [[ -n "$GL_DRAW_FLUSH_INTERVAL" ]]; then - print "Graphics draw flush interval: $GL_DRAW_FLUSH_INTERVAL microseconds." -fi -if [[ "$MVK_QUEUE_MODE" == "async" ]]; then - print "Experiment enabled: asynchronous MoltenVK queue submissions." -elif [[ "$MVK_QUEUE_MODE" == "sync" ]]; then - print "Control mode: synchronous MoltenVK queue submissions." -fi -if [[ "${MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE:-}" == "8" \ - && "${MVK_CONFIG_FAST_MATH_ENABLED:-}" == "1" ]]; then - print "OSFT MoltenVK tuning: 8 active Metal command buffers, fast math." -fi -if [[ "$MACOS_GAME_MODE" == "1" ]]; then - print "The Game Mode app wrapper is active. Enter fullscreen and verify Cmd-Esc -> Game Mode: On." -fi -print "Closing the Android Emulator window will shut down the experimental AVD." - -wait "$EMULATOR_PID" || true -print "The emulator was closed; this is treated as a normal exit." diff --git a/scripts/audit-native-gles-coverage.command b/scripts/audit-native-gles-coverage.command index b95ee50..9517ca8 100755 --- a/scripts/audit-native-gles-coverage.command +++ b/scripts/audit-native-gles-coverage.command @@ -73,7 +73,7 @@ if [[ -z "$NM_TOOL" || ! -x "$NM_TOOL" ]]; then exit 2 fi -WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/mactician-gles-audit.XXXXXX") +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tftmac-gles-audit.XXXXXX") trap 'rm -rf "$WORK_DIR"' EXIT extract_exports() { diff --git a/scripts/build-android-egl-capability-probe.command b/scripts/build-android-egl-capability-probe.command index e33e523..0c9290e 100755 --- a/scripts/build-android-egl-capability-probe.command +++ b/scripts/build-android-egl-capability-probe.command @@ -5,7 +5,7 @@ readonly PROJECT_DIR="${0:A:h:h}" readonly SOURCE_DIR="$PROJECT_DIR/artifacts/android-egl-capability-probe" readonly OUTPUT="${1:-$PROJECT_DIR/runtime/android-egl-capability-probe}" readonly GO_COMMAND="${TFT_GO_COMMAND:-go1.24.3}" -readonly BUILD_CACHE="$(mktemp -d -t mactician-android-egl-go-cache)" +readonly BUILD_CACHE="$(mktemp -d -t tftmac-android-egl-go-cache)" cleanup() { rm -rf "$BUILD_CACHE" diff --git a/scripts/build-mactician-icns.pl b/scripts/build-mactician-icns.pl deleted file mode 100755 index e30df82..0000000 --- a/scripts/build-mactician-icns.pl +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/perl -use strict; -use warnings; - -my ($iconset, $output) = @ARGV; -die "Usage: $0 ICONSET OUTPUT.icns\n" unless defined $iconset && defined $output; - -my @chunks = ( - ["icp4", "icon_16x16.png"], - ["icp5", "icon_16x16\@2x.png"], - ["icp6", "icon_32x32\@2x.png"], - ["ic07", "icon_128x128.png"], - ["ic08", "icon_128x128\@2x.png"], - ["ic09", "icon_256x256\@2x.png"], - ["ic10", "icon_512x512\@2x.png"], -); - -my @payloads; -my $total_length = 8; -for my $chunk (@chunks) { - my ($type, $filename) = @$chunk; - my $path = "$iconset/$filename"; - open my $input, "<:raw", $path or die "Could not read $path: $!\n"; - local $/; - my $payload = <$input>; - close $input; - push @payloads, [$type, $payload]; - $total_length += 8 + length($payload); -} - -open my $destination, ">:raw", $output or die "Could not write $output: $!\n"; -print {$destination} "icns", pack("N", $total_length); -for my $chunk (@payloads) { - my ($type, $payload) = @$chunk; - print {$destination} $type, pack("N", 8 + length($payload)), $payload; -} -close $destination or die "Could not close $output: $!\n"; diff --git a/scripts/build-mactician.command b/scripts/build-mactician.command deleted file mode 100755 index 3730079..0000000 --- a/scripts/build-mactician.command +++ /dev/null @@ -1,264 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -readonly LAUNCHER_DIR="$PROJECT_DIR/launcher" -readonly BUILD_DIR="$LAUNCHER_DIR/.build" -readonly DIST_DIR="${MACTICIAN_DIST_DIR:-$PROJECT_DIR/dist}" -readonly SPARKLE_ROOT="$("$PROJECT_DIR/scripts/prepare-sparkle.command")" -readonly SPARKLE_FRAMEWORK_SOURCE="$SPARKLE_ROOT/Sparkle.framework" -readonly SPARKLE_LICENSE_SOURCE="$SPARKLE_ROOT/LICENSE" -readonly SIGNING_IDENTITY="${MACTICIAN_CODESIGN_IDENTITY:--}" -readonly NOTARY_PROFILE="${MACTICIAN_NOTARY_PROFILE:-}" -readonly VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$LAUNCHER_DIR/Info.plist")" -typeset -i PUBLIC_RELEASE=0 -if [[ "$SIGNING_IDENTITY" != "-" || -n "$NOTARY_PROFILE" ]]; then - if [[ "$SIGNING_IDENTITY" != 'Developer ID Application: '* || -z "$NOTARY_PROFILE" ]]; then - print -u2 "A public release requires both a Developer ID Application identity and MACTICIAN_NOTARY_PROFILE." - exit 2 - fi - if ! security find-identity -v -p codesigning \ - | grep -Fq -- "\"$SIGNING_IDENTITY\""; then - print -u2 "Developer ID signing identity is not available in the login Keychain: $SIGNING_IDENTITY" - exit 2 - fi - PUBLIC_RELEASE=1 -fi -readonly PUBLIC_RELEASE -readonly APP="$DIST_DIR/Mactician.app" -readonly DMG="$DIST_DIR/Mactician-$VERSION.dmg" -readonly APP_CONTENTS="$APP/Contents" -readonly RESOURCES="$APP_CONTENTS/Resources" -readonly THIRD_PARTY_LICENSES="$RESOURCES/ThirdPartyLicenses" -readonly FRAMEWORKS="$APP_CONTENTS/Frameworks" -readonly GAME_RESOURCES="$RESOURCES/Game" -readonly RUNTIME_TEMPLATE="$RESOURCES/RuntimeTemplate" -readonly EMULATOR_APP="$RUNTIME_TEMPLATE/Mactician Game Host.app" -readonly EMULATOR_APP_CONTENTS="$EMULATOR_APP/Contents" -readonly APK_DIR="${TFT_GAME_APK_DIR:-}" - -if [[ -z "$APK_DIR" ]]; then - print -u2 "TFT_GAME_APK_DIR must point to the directory containing the four pinned, unmodified APK splits." - exit 1 -fi - -typeset -A EXPECTED_APK_HASHES -EXPECTED_APK_HASHES=( - base.apk 2f4996a620623d0b958383bfe58bdec78fb70cca095099ca2474f3d08c62ff18 - split_config.arm64_v8a.apk a7332efdddf070e724f75c41adb1a829bc3c442bd53ae79fe8cc55075e951939 - split_config.en.apk b57210f98779e12d6f6874c378942f67a55459e75549bbf97487a2d6e0084cd5 - split_config.hdpi.apk f3b653ba01a8d352f6a25ad6ecbae8892b13aa44daad6b74aebdb5a5be05d756 -) - -for apk expected_hash in ${(kv)EXPECTED_APK_HASHES}; do - apk_path="$APK_DIR/$apk" - if [[ ! -f "$apk_path" ]]; then - print -u2 "Private build input not found: $apk_path" - exit 1 - fi - actual_hash="$(shasum -a 256 "$apk_path" | awk '{print $1}')" - if [[ "$actual_hash" != "$expected_hash" ]]; then - print -u2 "SHA-256 for $apk does not match the TFT PBE 18.1 manifest." - exit 1 - fi -done - -copy_plain_file() { - local source_path="$1" - local destination_path="$2" - local temporary_copy - temporary_copy="$(mktemp /private/tmp/mactician-copy.XXXXXX)" - if ! cp -X "$source_path" "$temporary_copy"; then - rm -f "$temporary_copy" - return 1 - fi - chmod 644 "$temporary_copy" - if ! mv -f "$temporary_copy" "$destination_path"; then - rm -f "$temporary_copy" - return 1 - fi -} - -rm -rf "$BUILD_DIR/module-cache" "$APP" "$DMG" -mkdir -p "$BUILD_DIR" "$APP_CONTENTS/MacOS" "$RESOURCES" "$FRAMEWORKS" "$GAME_RESOURCES" \ - "$THIRD_PARTY_LICENSES" \ - "$RUNTIME_TEMPLATE/scripts" \ - "$RUNTIME_TEMPLATE/artifacts/tft-pbe-18.1-5212127-angle-opengl" \ - "$EMULATOR_APP_CONTENTS/MacOS" "$EMULATOR_APP_CONTENTS/Resources" \ - "$DIST_DIR" - -mkdir -p "$BUILD_DIR/module-cache" -typeset -a SWIFT_SOURCES -SWIFT_SOURCES=("$LAUNCHER_DIR"/Sources/*.swift) -xcrun swiftc \ - -O \ - -parse-as-library \ - -target arm64-apple-macosx12.0 \ - -module-cache-path "$BUILD_DIR/module-cache" \ - -F "$SPARKLE_ROOT" \ - "${SWIFT_SOURCES[@]}" \ - -framework Sparkle \ - -Xlinker -rpath \ - -Xlinker @executable_path/../Frameworks \ - -o "$APP_CONTENTS/MacOS/Mactician" - -ditto "$SPARKLE_FRAMEWORK_SOURCE" "$FRAMEWORKS/Sparkle.framework" -# The launcher is not App Sandbox-enabled. Sparkle's XPC services are only for -# sandboxed hosts and complicate manual Developer ID signing unnecessarily. -rm -rf "$FRAMEWORKS/Sparkle.framework/Versions/B/XPCServices" -rm -f "$FRAMEWORKS/Sparkle.framework/XPCServices" - -copy_plain_file "$LAUNCHER_DIR/Info.plist" "$APP_CONTENTS/Info.plist" -copy_plain_file "$LAUNCHER_DIR/Resources/Mactician.icns" "$RESOURCES/Mactician.icns" -copy_plain_file "$LAUNCHER_DIR/Resources/release-manifest.json" "$RESOURCES/release-manifest.json" -copy_plain_file "$LAUNCHER_DIR/Resources/MacticianHero.png" "$RESOURCES/MacticianHero.png" -copy_plain_file "$LAUNCHER_DIR/Resources/launcher-runtime.command" "$RESOURCES/launcher-runtime.command" -copy_plain_file "$LAUNCHER_DIR/Resources/QEMU-Hypervisor.entitlements" "$RESOURCES/QEMU-Hypervisor.entitlements" -copy_plain_file "$SPARKLE_LICENSE_SOURCE" "$THIRD_PARTY_LICENSES/Sparkle-LICENSE.txt" -chmod 755 "$RESOURCES/launcher-runtime.command" - -for localization in en ru; do - mkdir -p "$RESOURCES/$localization.lproj" - copy_plain_file "$LAUNCHER_DIR/Resources/$localization.lproj/Localizable.strings" \ - "$RESOURCES/$localization.lproj/Localizable.strings" - plutil -lint "$RESOURCES/$localization.lproj/Localizable.strings" >/dev/null -done - -copy_plain_file "$LAUNCHER_DIR/Resources/EmulatorHost-Info.plist" "$EMULATOR_APP_CONTENTS/Info.plist" -copy_plain_file "$LAUNCHER_DIR/Resources/EmulatorIcon.icns" "$EMULATOR_APP_CONTENTS/Resources/EmulatorIcon.icns" -xcrun clang \ - -O2 \ - -target arm64-apple-macosx12.0 \ - "$LAUNCHER_DIR/EmulatorHost/main.c" \ - -o "$EMULATOR_APP_CONTENTS/MacOS/MacticianGameHost" - -for apk in ${(k)EXPECTED_APK_HASHES}; do - copy_plain_file "$APK_DIR/$apk" "$GAME_RESOURCES/$apk" -done - -copy_plain_file "$PROJECT_DIR/run-tft-root-affinity.command" "$RUNTIME_TEMPLATE/run-tft-root-affinity.command" -copy_plain_file "$PROJECT_DIR/run-tft-angle-opengl.command" "$RUNTIME_TEMPLATE/run-tft-angle-opengl.command" -copy_plain_file "$PROJECT_DIR/scripts/run-asg-experiment.command" "$RUNTIME_TEMPLATE/scripts/run-asg-experiment.command" -copy_plain_file "$PROJECT_DIR/scripts/watch-root-pso.command" "$RUNTIME_TEMPLATE/scripts/watch-root-pso.command" -copy_plain_file "$PROJECT_DIR/scripts/android-environment.sh" "$RUNTIME_TEMPLATE/scripts/android-environment.sh" -copy_plain_file "$PROJECT_DIR/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.shader-prewarm.ini" \ - "$RUNTIME_TEMPLATE/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.shader-prewarm.ini" -copy_plain_file "$PROJECT_DIR/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.performance-max.ini" \ - "$RUNTIME_TEMPLATE/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.performance-max.ini" -copy_plain_file "$PROJECT_DIR/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.effects-high.ini" \ - "$RUNTIME_TEMPLATE/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.effects-high.ini" -copy_plain_file "$PROJECT_DIR/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.effects-performance.ini" \ - "$RUNTIME_TEMPLATE/artifacts/tft-pbe-18.1-5212127-angle-opengl/Android_Codex.DeviceProfiles.effects-performance.ini" -chmod 755 \ - "$RUNTIME_TEMPLATE/run-tft-root-affinity.command" \ - "$RUNTIME_TEMPLATE/run-tft-angle-opengl.command" \ - "$RUNTIME_TEMPLATE/scripts/run-asg-experiment.command" \ - "$RUNTIME_TEMPLATE/scripts/watch-root-pso.command" \ - "$RUNTIME_TEMPLATE/scripts/android-environment.sh" - -if rg -n '/Users/[[:alnum:]_.-]+/' "$APP_CONTENTS"; then - print -u2 "The launcher build contains an absolute developer path." - exit 1 -fi - -plutil -lint "$APP_CONTENTS/Info.plist" >/dev/null -plutil -lint "$EMULATOR_APP_CONTENTS/Info.plist" >/dev/null -plutil -lint "$RESOURCES/QEMU-Hypervisor.entitlements" >/dev/null - -# Some synchronized or sandboxed directories can block codesign while it -# atomically replaces the linker signature. Sign an identical temporary bundle -# on the local filesystem, then copy the verified result back into dist. -readonly SIGNING_ROOT="$(mktemp -d /private/tmp/mactician-sign.XXXXXX)" -readonly SIGNING_APP="$SIGNING_ROOT/Mactician.app" -readonly SIGNING_EMULATOR_APP="$SIGNING_APP/Contents/Resources/RuntimeTemplate/Mactician Game Host.app" -readonly SIGNING_SPARKLE="$SIGNING_APP/Contents/Frameworks/Sparkle.framework" -readonly SIGNING_SPARKLE_VERSION="$SIGNING_SPARKLE/Versions/B" -cleanup_signing_root() { - rm -rf "$SIGNING_ROOT" -} -trap cleanup_signing_root EXIT -cp -R -X "$APP" "$SIGNING_APP" -if (( PUBLIC_RELEASE == 1 )); then - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_SPARKLE_VERSION/Autoupdate" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_SPARKLE_VERSION/Updater.app" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_SPARKLE" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_EMULATOR_APP/Contents/MacOS/MacticianGameHost" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_EMULATOR_APP" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_APP/Contents/MacOS/Mactician" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ - "$SIGNING_APP" -else - codesign --force --sign - --timestamp=none --options runtime \ - "$SIGNING_SPARKLE_VERSION/Autoupdate" - codesign --force --sign - --timestamp=none --options runtime \ - "$SIGNING_SPARKLE_VERSION/Updater.app" - codesign --force --sign - --timestamp=none --options runtime \ - "$SIGNING_SPARKLE" - codesign --force --sign - --timestamp=none "$SIGNING_EMULATOR_APP/Contents/MacOS/MacticianGameHost" - codesign --force --sign - --timestamp=none "$SIGNING_EMULATOR_APP" - codesign --force --sign - --timestamp=none "$SIGNING_APP/Contents/MacOS/Mactician" - codesign --force --sign - --timestamp=none "$SIGNING_APP" -fi -codesign --verify --deep --strict --verbose=2 "$SIGNING_APP" -rm -rf "$APP" -mv "$SIGNING_APP" "$APP" -codesign --verify --deep --strict --verbose=2 "$APP" -if (( PUBLIC_RELEASE == 1 )) && [[ -x /usr/bin/syspolicy_check ]]; then - syspolicy_check notary-submission "$APP" -fi - -readonly DMG_ROOT="$SIGNING_ROOT/dmg-root" -mkdir -p "$DMG_ROOT" -cp -R -X "$APP" "$DMG_ROOT/Mactician.app" -ln -s /Applications "$DMG_ROOT/Applications" -hdiutil create \ - -size 512m \ - -fs HFS+ \ - -volname "Mactician" \ - -srcfolder "$DMG_ROOT" \ - -ov \ - -format UDZO \ - "$DMG" >/dev/null -xattr -c "$DMG" - -if (( PUBLIC_RELEASE == 1 )); then - codesign --force --sign "$SIGNING_IDENTITY" --timestamp "$DMG" - readonly NOTARY_RESULT="$SIGNING_ROOT/notary-result.json" - if ! xcrun notarytool submit "$DMG" \ - --keychain-profile "$NOTARY_PROFILE" \ - --wait \ - --timeout 30m \ - --output-format json >"$NOTARY_RESULT"; then - cat "$NOTARY_RESULT" >&2 - exit 1 - fi - cat "$NOTARY_RESULT" - readonly NOTARY_ID="$(jq -r '.id // empty' "$NOTARY_RESULT")" - readonly NOTARY_STATUS="$(jq -r '.status // empty' "$NOTARY_RESULT")" - if [[ -z "$NOTARY_ID" || "$NOTARY_STATUS" != "Accepted" ]]; then - print -u2 "Apple did not accept the notarization submission." - exit 1 - fi - xcrun notarytool log "$NOTARY_ID" \ - --keychain-profile "$NOTARY_PROFILE" - xcrun stapler staple "$DMG" - xcrun stapler validate "$DMG" - spctl --assess --type open --context context:primary-signature --verbose=2 "$DMG" - if [[ -x /usr/bin/syspolicy_check ]]; then - syspolicy_check distribution "$APP" - fi -fi - -print "App: $APP" -print "DMG: $DMG" -if (( PUBLIC_RELEASE == 1 )); then - print "Developer ID-signed and notarized public release is ready." -else - print "Private ad-hoc build is ready. On another Mac, allow the first launch via System Settings → Privacy & Security → Open Anyway." -fi diff --git a/scripts/build-native-app.command b/scripts/build-native-app.command new file mode 100755 index 0000000..0d4d316 --- /dev/null +++ b/scripts/build-native-app.command @@ -0,0 +1,107 @@ +#!/bin/zsh +set -euo pipefail + +ROOT="${0:A:h:h}" +ICON_SOURCE="${ROOT}/tftmac/Assets/TFTMAC-Official-Icon.png" +ICON_SOURCE_SHA256="d6ba9ceb76c4b1e44e87f059f775a0ed629f9bea29b0dd73245853d7dca3a016" +if [[ -z "${DEVELOPER_DIR:-}" ]]; then + if [[ -d /Applications/Xcode-26.6.0.app/Contents/Developer ]]; then + export DEVELOPER_DIR=/Applications/Xcode-26.6.0.app/Contents/Developer + else + export DEVELOPER_DIR="$(xcode-select -p)" + fi +fi +DERIVED="${ROOT}/.build/native-release" +APP="${DERIVED}/Build/Products/Release/TFTMAC.app" +DIST="${ROOT}/dist/TFTMAC.app" +ICON_WORK="$(mktemp -d /private/tmp/tftmac-native-icon.XXXXXX)" +SIGNING_IDENTITY_NAME="${TFTMAC_CODE_SIGN_IDENTITY_NAME:-TFTMAC Local Code Signing}" +SIGNING_IDENTITY_HASH="$(/usr/bin/security find-identity -v -p codesigning \ + | /usr/bin/awk -v name="${SIGNING_IDENTITY_NAME}" 'index($0, "\"" name "\"") { print $2; exit }')" + +[[ -n "${SIGNING_IDENTITY_HASH}" ]] || { + print -u2 "TFTMAC requires the stable '${SIGNING_IDENTITY_NAME}' identity. Run scripts/ensure-local-signing-identity.command once." + exit 1 +} +[[ -s "${ICON_SOURCE}" ]] || { + print -u2 "The official TFTMAC icon source is missing: ${ICON_SOURCE}" + exit 1 +} +[[ "$(/usr/bin/shasum -a 256 "${ICON_SOURCE}" | /usr/bin/awk '{print $1}')" == "${ICON_SOURCE_SHA256}" ]] || { + print -u2 "The official TFTMAC icon source failed its SHA-256 receipt." + exit 1 +} + +cleanup() { + /bin/rm -rf "${ICON_WORK}" +} +trap cleanup EXIT + +/usr/bin/xcodebuild \ + -quiet \ + -project "${ROOT}/TFTMAC.xcodeproj" \ + -scheme TFTMAC \ + -configuration Release \ + -derivedDataPath "${DERIVED}" \ + CODE_SIGNING_ALLOWED=NO \ + build + +[[ -d "${APP}" ]] || { echo "Native build did not produce ${APP}" >&2; exit 1; } +/bin/rm -rf "${DIST}" +/bin/mkdir -p "${ROOT}/dist" +/usr/bin/ditto "${APP}" "${DIST}" + +# The Info.plist declares TFTMAC.icns. Downsample the official generated +# 1:1 master and embed every required Mac representation before signing. +/usr/bin/sips -z 1024 1024 "${ICON_SOURCE}" --out "${ICON_WORK}/icon_1024x1024.png" >/dev/null +/bin/mkdir -p "${ICON_WORK}/TFTMAC.iconset" "${DIST}/Contents/Resources" +for specification in \ + '16 icon_16x16.png' \ + '32 icon_16x16@2x.png' \ + '32 icon_32x32.png' \ + '64 icon_32x32@2x.png' \ + '128 icon_128x128.png' \ + '256 icon_128x128@2x.png' \ + '256 icon_256x256.png' \ + '512 icon_256x256@2x.png' \ + '512 icon_512x512.png' \ + '1024 icon_512x512@2x.png'; do + pixels="${specification%% *}" + filename="${specification#* }" + /usr/bin/sips -z "${pixels}" "${pixels}" "${ICON_WORK}/icon_1024x1024.png" \ + --out "${ICON_WORK}/TFTMAC.iconset/${filename}" >/dev/null +done +/usr/bin/iconutil -c icns "${ICON_WORK}/TFTMAC.iconset" -o "${DIST}/Contents/Resources/TFTMAC.icns" +/bin/cp "${ICON_WORK}/icon_1024x1024.png" "${DIST}/Contents/Resources/TFTMAC-1024.png" + +# Perfetto v58.2 mac-arm64 is pinned by the official manifest SHA-256. Raw +# combat traces are normalized locally with this exact executable before the +# app records any causal trace conclusion. +TRACE_PROCESSOR="$(/bin/zsh "${ROOT}/scripts/install-trace-processor.command")" +/bin/cp "${TRACE_PROCESSOR}" "${DIST}/Contents/Resources/trace_processor_shell" +/bin/chmod 755 "${DIST}/Contents/Resources/trace_processor_shell" +[[ "$(/usr/bin/shasum -a 256 "${DIST}/Contents/Resources/trace_processor_shell" | /usr/bin/awk '{print $1}')" == "d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa" ]] || { + print -u2 "Packaged Perfetto trace_processor failed its pinned SHA-256 receipt." + exit 1 +} + +# Package a TFTMAC-owned Mac application host for Android Emulator. Launching +# this nested app with /usr/bin/open keeps the emulator and ADB identity inside +# the logged-in user's macOS session, matching the proven donor architecture. +HOST_APP="${DIST}/Contents/Resources/TFTMAC Emulator Host.app" +HOST_MACOS="${HOST_APP}/Contents/MacOS" +/bin/mkdir -p "${HOST_MACOS}" +/usr/bin/xcrun --sdk macosx clang \ + -Os -arch arm64 -mmacosx-version-min=15.0 \ + "${ROOT}/RuntimeHost/main.c" \ + -o "${HOST_MACOS}/TFTMACEmulatorHost" +/bin/cp "${ROOT}/RuntimeHost/Info.plist" "${HOST_APP}/Contents/Info.plist" +/usr/bin/plutil -lint "${HOST_APP}/Contents/Info.plist" >/dev/null +/usr/bin/codesign --force --sign "${SIGNING_IDENTITY_HASH}" --timestamp=none "${HOST_APP}" + +/usr/bin/codesign --force --deep --sign "${SIGNING_IDENTITY_HASH}" --timestamp=none "${DIST}" +/usr/bin/codesign --verify --deep --strict --verbose=2 "${DIST}" +/usr/bin/codesign -dvv "${DIST}" 2>&1 \ + | /usr/bin/grep -F "Authority=${SIGNING_IDENTITY_NAME}" >/dev/null + +echo "Native TFTMAC built: ${DIST}" diff --git a/scripts/build-tftmac-app.command b/scripts/build-tftmac-app.command index 81ddc3e..a415cc0 100644 --- a/scripts/build-tftmac-app.command +++ b/scripts/build-tftmac-app.command @@ -1,120 +1,9 @@ #!/bin/zsh set -euo pipefail -readonly PROJECT_DIR="${0:A:h:h}" -readonly APP="$PROJECT_DIR/dist/TFTMAC.app" -readonly CONTENTS="$APP/Contents" -readonly MACOS="$CONTENTS/MacOS" -readonly RESOURCES="$CONTENTS/Resources" -readonly SOURCES_DIR="$PROJECT_DIR/tftmac/Sources" -readonly INFO_PLIST="$PROJECT_DIR/tftmac/Info.plist" -readonly ICON_SOURCE="$PROJECT_DIR/tftmac/GenerateIcon.swift" -readonly ICON_WORK="$(mktemp -d /private/tmp/tftmac-icon.XXXXXX)" -readonly INSTALL_ROOT="${TFTMAC_INSTALL_ROOT:-/Applications}" -readonly DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode-26.6.0.app/Contents/Developer}" -export DEVELOPER_DIR -readonly INSTALLED_APP="$INSTALL_ROOT/TFTMAC.app" +readonly ROOT="${0:A:h:h}" -cleanup() { - rm -rf "$ICON_WORK" -} -trap cleanup EXIT - -[[ -x "$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc" ]] || { print -u2 "Required Xcode 26.6 toolchain is missing: $DEVELOPER_DIR"; exit 1; } -[[ "$(xcodebuild -version | head -n 1)" == "Xcode 26.6" ]] || { print -u2 "TFTMAC requires Xcode 26.6"; exit 1; } - -rm -rf "$APP" -mkdir -p "$MACOS" "$RESOURCES/Tools" "$RESOURCES/ssot" "$PROJECT_DIR/dist" - -plutil -lint "$INFO_PLIST" >/dev/null - -# Generate a native TFTMAC launcher icon locally. No Android/Emulator branding is used. -xcrun swift "$ICON_SOURCE" "$ICON_WORK/icon_1024x1024.png" -mkdir -p "$ICON_WORK/TFTMAC.iconset" -for spec in \ - '16 icon_16x16.png' \ - '32 icon_16x16@2x.png' \ - '32 icon_32x32.png' \ - '64 icon_32x32@2x.png' \ - '128 icon_128x128.png' \ - '256 icon_128x128@2x.png' \ - '256 icon_256x256.png' \ - '512 icon_256x256@2x.png' \ - '512 icon_512x512.png' \ - '1024 icon_512x512@2x.png'; do - pixels="${spec%% *}" - name="${spec#* }" - sips -z "$pixels" "$pixels" "$ICON_WORK/icon_1024x1024.png" --out "$ICON_WORK/TFTMAC.iconset/$name" >/dev/null - done -iconutil -c icns "$ICON_WORK/TFTMAC.iconset" -o "$RESOURCES/TFTMAC.icns" -cp -f "$ICON_WORK/icon_1024x1024.png" "$RESOURCES/TFTMAC-1024.png" - -# Compile only the new stock-runtime TFTMAC shell. The legacy Mactician/PBE launcher is not linked. -typeset -a SWIFT_SOURCES -SWIFT_SOURCES=("$SOURCES_DIR"/*.swift) -xcrun swiftc \ - -O \ - -swift-version 5 \ - -parse-as-library \ - -target arm64-apple-macosx14.0 \ - "${SWIFT_SOURCES[@]}" \ - -framework AppKit \ - -framework SwiftUI \ - -framework ApplicationServices \ - -o "$MACOS/TFTMAC" - -cp -f "$INFO_PLIST" "$CONTENTS/Info.plist" -cp -f "$PROJECT_DIR/tools/tftmac-direct-control.mjs" "$RESOURCES/Tools/tftmac-direct-control.mjs" -chmod 755 "$RESOURCES/Tools/tftmac-direct-control.mjs" -cp -f "$PROJECT_DIR/ssot/TFTMAC_PERFORMANCE_LAB.sql" "$RESOURCES/ssot/TFTMAC_PERFORMANCE_LAB.sql" -cp -f "$PROJECT_DIR/ssot/TFTMAC_ENGINEERING_MAP.sql" "$RESOURCES/ssot/TFTMAC_ENGINEERING_MAP.sql" -cp -f "$PROJECT_DIR/ssot/STACK.lock.yaml" "$RESOURCES/ssot/STACK.lock.yaml" -git -C "$PROJECT_DIR" rev-parse HEAD > "$RESOURCES/Tools/build-commit.txt" - -plutil -lint "$CONTENTS/Info.plist" >/dev/null -if rg -n 'CFBundle(DisplayName|Name).*Emulator|Emulator|Mactician' "$CONTENTS/Info.plist"; then - print -u2 "TFTMAC app metadata contains legacy Emulator/Mactician branding." - exit 1 -fi - -codesign --force --sign - --timestamp=none --options runtime "$APP" -codesign --verify --deep --strict --verbose=2 "$APP" - -mkdir -p "$INSTALL_ROOT" -rm -rf "$INSTALLED_APP" -ditto "$APP" "$INSTALLED_APP" -codesign --verify --deep --strict --verbose=2 "$INSTALLED_APP" - -# Launcher smoke: the installed Finder/Dock icon must start the native shell. -pkill -f '/Applications/TFTMAC.app/Contents/MacOS/TFTMAC' 2>/dev/null || true -sleep 0.4 -# Remove only TFTMAC's stale private ADB server before app launch. A stale 5040 -# listener is not a valid runtime and must never prevent the launcher from booting. -/opt/homebrew/bin/node "$PROJECT_DIR/tools/tftmac-direct-control.mjs" cleanup-tftmac-adb-residue >/dev/null -open -na "$INSTALLED_APP" -sleep 1.5 -pgrep -f '/Applications/TFTMAC.app/Contents/MacOS/TFTMAC' >/dev/null || { print -u2 "Installed TFTMAC launcher did not stay running."; exit 1; } - -# End-to-end launcher gate: after opening the installed app, the app itself must -# establish exactly one owned emulator/logger/ADB runtime. TFT launch may pause -# at Android secure unlock, which is intentionally a manual security boundary. -typeset runtime_ready=0 runtime_audit="" -for _ in {1..45}; do - runtime_audit="$(/opt/homebrew/bin/node "$PROJECT_DIR/tools/tftmac-direct-control.mjs" runtime-process-audit 2>/dev/null || true)" - if [[ -n "$runtime_audit" ]] \ - && print -r -- "$runtime_audit" | jq -e ' - .duplicateRisk.tftmacApps == 1 - and .duplicateRisk.emulators == 1 - and .duplicateRisk.samplers == 1 - and .duplicateRisk.adbServers == 1 - ' >/dev/null 2>&1; then - runtime_ready=1 - break - fi - sleep 1 -done -(( runtime_ready == 1 )) || { print -u2 "TFTMAC launcher did not establish its emulator/logger/ADB runtime. Last audit: $runtime_audit"; exit 1; } - -print "Built: $APP" -print "Installed: $INSTALLED_APP" -print "Launched: $INSTALLED_APP" +# Compatibility entrypoint only. The old glob-compiled service-context app +# caused the ADB authorization regression and is not a build authority. +print -u2 "build-tftmac-app.command now delegates to the native Xcode build." +exec /bin/zsh "${ROOT}/scripts/build-native-app.command" "$@" diff --git a/scripts/ensure-local-signing-identity.command b/scripts/ensure-local-signing-identity.command new file mode 100755 index 0000000..647ca40 --- /dev/null +++ b/scripts/ensure-local-signing-identity.command @@ -0,0 +1,72 @@ +#!/bin/zsh +set -euo pipefail + +# TFTMAC's Android runtime intentionally lives on a removable volume. macOS +# associates that consent with the app's code-signing identity, so ad-hoc +# signing causes the prompt to return after every changed build. This creates a +# stable, local-only identity in the current user's login keychain. It is not a +# distribution or notarization identity. +readonly IDENTITY_NAME="TFTMAC Local Code Signing" +readonly LOGIN_KEYCHAIN="$(/usr/bin/security default-keychain -d user \ + | /usr/bin/sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//')" + +identity_hash() { + /usr/bin/security find-identity -v -p codesigning "${LOGIN_KEYCHAIN}" \ + | /usr/bin/awk -v name="${IDENTITY_NAME}" 'index($0, "\"" name "\"") { print $2; exit }' +} + +existing_hash="$(identity_hash)" +if [[ -n "${existing_hash}" ]]; then + echo "${existing_hash}" + exit 0 +fi + +if /usr/bin/security find-certificate -c "${IDENTITY_NAME}" "${LOGIN_KEYCHAIN}" >/dev/null 2>&1; then + print -u2 "TFTMAC signing certificate exists but is not a valid code-signing identity. Repair it in Keychain Access before rebuilding." + exit 1 +fi + +readonly OPENSSL="${TFTMAC_OPENSSL:-/opt/homebrew/bin/openssl}" +[[ -x "${OPENSSL}" ]] || { + print -u2 "TFTMAC requires OpenSSL at ${OPENSSL} to create its one-time local signing identity." + exit 1 +} + +work_dir="$(/usr/bin/mktemp -d /private/tmp/tftmac-local-signing.XXXXXX)" +cleanup() { + /bin/rm -rf "${work_dir}" +} +trap cleanup EXIT + +passphrase="$(/usr/bin/uuidgen)$(/usr/bin/uuidgen)" +umask 077 +"${OPENSSL}" req -x509 -newkey rsa:3072 -sha256 -days 3650 -nodes \ + -subj "/CN=${IDENTITY_NAME}/O=TFTMAC Local Development" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "keyUsage=critical,digitalSignature" \ + -addext "extendedKeyUsage=codeSigning" \ + -keyout "${work_dir}/identity.key" \ + -out "${work_dir}/identity.pem" >/dev/null 2>&1 +"${OPENSSL}" pkcs12 -export -legacy \ + -inkey "${work_dir}/identity.key" \ + -in "${work_dir}/identity.pem" \ + -name "${IDENTITY_NAME}" \ + -passout "pass:${passphrase}" \ + -out "${work_dir}/identity.p12" + +/usr/bin/security import "${work_dir}/identity.p12" \ + -k "${LOGIN_KEYCHAIN}" \ + -P "${passphrase}" \ + -T /usr/bin/codesign >/dev/null +/usr/bin/security add-trusted-cert \ + -r trustRoot \ + -p codeSign \ + -k "${LOGIN_KEYCHAIN}" \ + "${work_dir}/identity.pem" + +created_hash="$(identity_hash)" +[[ -n "${created_hash}" ]] || { + print -u2 "TFTMAC created the certificate but macOS did not expose a valid signing identity." + exit 1 +} +echo "${created_hash}" diff --git a/scripts/generate-emulator-proto.command b/scripts/generate-emulator-proto.command new file mode 100644 index 0000000..bb0add9 --- /dev/null +++ b/scripts/generate-emulator-proto.command @@ -0,0 +1,57 @@ +#!/bin/zsh +set -euo pipefail + +ROOT="${0:A:h:h}" +if [[ -z "${DEVELOPER_DIR:-}" ]]; then + if [[ -d /Applications/Xcode-26.6.0.app/Contents/Developer ]]; then + export DEVELOPER_DIR=/Applications/Xcode-26.6.0.app/Contents/Developer + else + export DEVELOPER_DIR="$(xcode-select -p)" + fi +fi +[[ -d "$DEVELOPER_DIR" ]] || { echo "Authoritative Xcode is missing: $DEVELOPER_DIR" >&2; exit 20; } + +PROTO="$ROOT/Vendor/AndroidEmulator/emulator_controller.proto" +[[ -f "$PROTO" ]] || { echo "Vendored EmulatorController proto is missing" >&2; exit 21; } + +PROTOC="" +for candidate in /opt/homebrew/bin/protoc /usr/local/bin/protoc; do + if [[ -x "$candidate" ]]; then PROTOC="$candidate"; break; fi +done +[[ -n "$PROTOC" ]] || { echo "Pinned build cannot generate EmulatorController Swift: protoc is not installed at the standard existing toolchain paths." >&2; exit 22; } + +SWIFT_PROTOBUF="$ROOT/.build/native-release/SourcePackages/checkouts/swift-protobuf" +GRPC_PROTOBUF="$ROOT/.build/native-release/SourcePackages/checkouts/grpc-swift-protobuf" +[[ -d "$SWIFT_PROTOBUF" && -d "$GRPC_PROTOBUF" ]] || { echo "Pinned Swift package checkouts from Gate 1 are missing." >&2; exit 23; } + +/usr/bin/xcrun swift build --package-path "$SWIFT_PROTOBUF" -c release --product protoc-gen-swift >/dev/null +/usr/bin/xcrun swift build --package-path "$GRPC_PROTOBUF" -c release --product protoc-gen-grpc-swift-2 >/dev/null + +SWIFT_PLUGIN="$SWIFT_PROTOBUF/.build/release/protoc-gen-swift" +GRPC_PLUGIN="$GRPC_PROTOBUF/.build/release/protoc-gen-grpc-swift-2" +[[ -x "$SWIFT_PLUGIN" ]] || { echo "Pinned protoc-gen-swift build did not produce an executable." >&2; exit 24; } +[[ -x "$GRPC_PLUGIN" ]] || { echo "Pinned protoc-gen-grpc-swift-2 build did not produce an executable." >&2; exit 25; } + +PROTO_INCLUDE="" +for candidate in /opt/homebrew/include /usr/local/include; do + if [[ -f "$candidate/google/protobuf/empty.proto" ]]; then PROTO_INCLUDE="$candidate"; break; fi +done +[[ -n "$PROTO_INCLUDE" ]] || { echo "google/protobuf/empty.proto is missing from the existing protoc installation." >&2; exit 26; } + +OUT="$ROOT/Generated/EmulatorController" +/bin/rm -rf "$OUT" +/bin/mkdir -p "$OUT" + +"$PROTOC" \ + -I "$ROOT/Vendor/AndroidEmulator" \ + -I "$PROTO_INCLUDE" \ + --plugin="protoc-gen-swift=$SWIFT_PLUGIN" \ + --plugin="protoc-gen-grpc-swift-2=$GRPC_PLUGIN" \ + --swift_out="$OUT" \ + --grpc-swift-2_out="$OUT" \ + "$PROTO" + +[[ -f "$OUT/emulator_controller.pb.swift" ]] || { echo "Swift protobuf output missing" >&2; exit 27; } +[[ -f "$OUT/emulator_controller.grpc.swift" ]] || { echo "Swift gRPC output missing" >&2; exit 28; } + +echo "Generated EmulatorController Swift client from vendored Emulator 37.1.11 protocol." diff --git a/scripts/generate-mactician-assets.command b/scripts/generate-mactician-assets.command deleted file mode 100755 index f0aa447..0000000 --- a/scripts/generate-mactician-assets.command +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -readonly BRAND_DIR="$PROJECT_DIR/branding" -readonly GENERATED_DIR="$BRAND_DIR/generated" -readonly LAUNCHER_ICONSET="$GENERATED_DIR/Mactician.iconset" -readonly GAME_HOST_ICONSET="$GENERATED_DIR/EmulatorIcon.iconset" -readonly APP_ICON_SVG="$BRAND_DIR/mactician-app-icon.svg" -readonly GAME_HOST_ICON_SVG="$BRAND_DIR/mactician-game-host-icon.svg" - -for command_name in rsvg-convert iconutil magick perl; do - command -v "$command_name" >/dev/null 2>&1 || { - print -u2 "Required asset tool is unavailable: $command_name" - exit 1 - } -done - -rm -rf "$GENERATED_DIR" -mkdir -p "$LAUNCHER_ICONSET" "$GAME_HOST_ICONSET" - -render_png() { - local source="$1" - local width="$2" - local height="$3" - local destination="$4" - rsvg-convert --width "$width" --height "$height" "$source" --output "$destination" -} - -render_iconset() { - local source="$1" - local iconset="$2" - render_png "$source" 16 16 "$iconset/icon_16x16.png" - render_png "$source" 32 32 "$iconset/icon_16x16@2x.png" - render_png "$source" 32 32 "$iconset/icon_32x32.png" - render_png "$source" 64 64 "$iconset/icon_32x32@2x.png" - render_png "$source" 128 128 "$iconset/icon_128x128.png" - render_png "$source" 256 256 "$iconset/icon_128x128@2x.png" - render_png "$source" 256 256 "$iconset/icon_256x256.png" - render_png "$source" 512 512 "$iconset/icon_256x256@2x.png" - render_png "$source" 512 512 "$iconset/icon_512x512.png" - render_png "$source" 1024 1024 "$iconset/icon_512x512@2x.png" -} - -render_iconset "$APP_ICON_SVG" "$LAUNCHER_ICONSET" -render_iconset "$GAME_HOST_ICON_SVG" "$GAME_HOST_ICONSET" - -perl "$PROJECT_DIR/scripts/build-mactician-icns.pl" \ - "$LAUNCHER_ICONSET" "$GENERATED_DIR/Mactician.icns" -perl "$PROJECT_DIR/scripts/build-mactician-icns.pl" \ - "$GAME_HOST_ICONSET" "$GENERATED_DIR/EmulatorIcon.icns" -readonly LAUNCHER_VALIDATION_ICONSET="$GENERATED_DIR/.launcher-validation.iconset" -readonly GAME_HOST_VALIDATION_ICONSET="$GENERATED_DIR/.game-host-validation.iconset" -iconutil -c iconset "$GENERATED_DIR/Mactician.icns" -o "$LAUNCHER_VALIDATION_ICONSET" -iconutil -c iconset "$GENERATED_DIR/EmulatorIcon.icns" -o "$GAME_HOST_VALIDATION_ICONSET" -rm -rf "$LAUNCHER_VALIDATION_ICONSET" "$GAME_HOST_VALIDATION_ICONSET" -render_png "$APP_ICON_SVG" 1024 1024 "$GENERATED_DIR/Mactician-1024.png" -render_png "$GAME_HOST_ICON_SVG" 1024 1024 "$GENERATED_DIR/EmulatorIcon-1024.png" -render_png "$BRAND_DIR/mactician-favicon.svg" 64 64 "$GENERATED_DIR/mactician-favicon-64.png" -magick "$GENERATED_DIR/mactician-favicon-64.png" -define icon:auto-resize=64,32,16 \ - "$GENERATED_DIR/mactician-favicon.ico" -render_png "$BRAND_DIR/mactician-social-preview.svg" 1280 640 \ - "$GENERATED_DIR/mactician-social-preview.png" -render_png "$BRAND_DIR/mactician-open-graph.svg" 1200 630 \ - "$GENERATED_DIR/mactician-open-graph.png" -render_png "$BRAND_DIR/mactician-small-size-test.svg" 1120 420 \ - "$GENERATED_DIR/mactician-small-size-test.png" -render_png "$BRAND_DIR/mactician-product-hero.svg" 1600 900 \ - "$GENERATED_DIR/mactician-product-hero.png" - -cp -f "$GENERATED_DIR/Mactician.icns" "$PROJECT_DIR/launcher/Resources/Mactician.icns" -cp -f "$GENERATED_DIR/Mactician-1024.png" "$PROJECT_DIR/launcher/Resources/Mactician-1024.png" -cp -f "$GENERATED_DIR/EmulatorIcon.icns" "$PROJECT_DIR/launcher/Resources/EmulatorIcon.icns" -cp -f "$GENERATED_DIR/EmulatorIcon-1024.png" \ - "$PROJECT_DIR/launcher/Resources/EmulatorIcon-1024.png" -cp -f "$GENERATED_DIR/mactician-product-hero.png" \ - "$PROJECT_DIR/launcher/Resources/MacticianHero.png" - -print "Generated Mactician assets in $GENERATED_DIR" diff --git a/scripts/install-trace-processor.command b/scripts/install-trace-processor.command new file mode 100644 index 0000000..3faee9d --- /dev/null +++ b/scripts/install-trace-processor.command @@ -0,0 +1,40 @@ +#!/bin/zsh +set -euo pipefail + +readonly ROOT="${0:A:h:h}" +readonly VERSION="58.2" +readonly EXPECTED_SHA256="d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa" +readonly EXPECTED_BYTES="13597976" +readonly URL="https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v${VERSION}/mac-arm64/trace_processor_shell" +readonly TOOL_DIRECTORY="${ROOT}/.build/tools/perfetto" +readonly TOOL="${TOOL_DIRECTORY}/trace_processor_shell-v${VERSION}-mac-arm64" + +/bin/mkdir -p "${TOOL_DIRECTORY}" +if [[ -f "${TOOL}" ]]; then + actual_sha="$(/usr/bin/shasum -a 256 "${TOOL}" | /usr/bin/awk '{print $1}')" + actual_bytes="$(/usr/bin/stat -f '%z' "${TOOL}")" + if [[ "${actual_sha}" == "${EXPECTED_SHA256}" && "${actual_bytes}" == "${EXPECTED_BYTES}" ]]; then + /bin/chmod 755 "${TOOL}" + print -r -- "${TOOL}" + exit 0 + fi +fi + +temporary="$(/usr/bin/mktemp "${TOOL_DIRECTORY}/trace_processor.XXXXXX")" +cleanup() { /bin/rm -f "${temporary}"; } +trap cleanup EXIT +/usr/bin/curl -fsSL "${URL}" -o "${temporary}" +actual_sha="$(/usr/bin/shasum -a 256 "${temporary}" | /usr/bin/awk '{print $1}')" +actual_bytes="$(/usr/bin/stat -f '%z' "${temporary}")" +[[ "${actual_sha}" == "${EXPECTED_SHA256}" ]] || { + print -u2 "Perfetto trace_processor SHA-256 mismatch: ${actual_sha}" + exit 1 +} +[[ "${actual_bytes}" == "${EXPECTED_BYTES}" ]] || { + print -u2 "Perfetto trace_processor size mismatch: ${actual_bytes}" + exit 1 +} +/bin/chmod 755 "${temporary}" +/bin/mv -f "${temporary}" "${TOOL}" +trap - EXIT +print -r -- "${TOOL}" diff --git a/scripts/integration-test-mactician.command b/scripts/integration-test-mactician.command deleted file mode 100755 index 0ee19f0..0000000 --- a/scripts/integration-test-mactician.command +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -readonly LAUNCHER_DIR="$PROJECT_DIR/launcher" -readonly APP_RESOURCES="$PROJECT_DIR/dist/Mactician.app/Contents/Resources" -readonly TEST_ROOT="$(mktemp -d /tmp/mactician-integration.XXXXXX)" -readonly TEST_BINARY="$TEST_ROOT/InstallerIntegration" -readonly FIXTURE_DIR="$LAUNCHER_DIR/.build/integration-fixtures" -cleanup() { - rm -rf "$TEST_ROOT" -} -trap cleanup EXIT - -if pgrep -f '[q]emu-system-aarch64' >/dev/null 2>&1; then - print -u2 "Another Android Emulator is running. Close it before the provisioning integration test." - exit 2 -fi - -if [[ ! -f "$APP_RESOURCES/release-manifest.json" ]]; then - print -u2 "Build the app first: ./scripts/build-mactician.command" - exit 1 -fi - -mkdir -p "$FIXTURE_DIR" -download_fixture() { - local -r fixture_name="$1" - local -r fixture_url="$2" - local -r fixture_sha256="$3" - local -r fixture_path="$FIXTURE_DIR/$fixture_name" - if [[ ! -f "$fixture_path" ]] \ - || [[ "$(shasum -a 256 "$fixture_path" | awk '{print $1}')" != "$fixture_sha256" ]]; then - curl -fL --retry 3 --continue-at - "$fixture_url" -o "$fixture_path.partial" - if [[ "$(shasum -a 256 "$fixture_path.partial" | awk '{print $1}')" != "$fixture_sha256" ]]; then - print -u2 "Integration fixture SHA-256 mismatch: $fixture_name" - return 1 - fi - mv -f "$fixture_path.partial" "$fixture_path" - fi -} - -download_fixture \ - platform-tools_r36.0.2-darwin.zip \ - https://dl.google.com/android/repository/platform-tools_r36.0.2-darwin.zip \ - 106a5d31fad8c1c0c5a180d06f5779767d129d7d5edbe629005c11a85eec5b4b -download_fixture \ - emulator-darwin_aarch64-15917651.zip \ - https://dl.google.com/android/repository/emulator-darwin_aarch64-15917651.zip \ - 22530de9363f34ea945ecb5cad74523abd4b615f27f3c1a9899efb183ea9e144 -download_fixture \ - arm64-v8a-36_r07.zip \ - https://dl.google.com/android/repository/sys-img/google_apis/arm64-v8a-36_r07.zip \ - fb47d861d6f87230ee0fe70f610d579935ca77f41a0eefbf391595d3dc4b5ee2 - -mkdir -p "$LAUNCHER_DIR/.build/module-cache" -xcrun swiftc \ - -target arm64-apple-macosx12.0 \ - -module-cache-path "$LAUNCHER_DIR/.build/module-cache" \ - "$LAUNCHER_DIR/Sources/EmulatorBrandingPatch.swift" \ - "$LAUNCHER_DIR/Sources/LauncherPresentation.swift" \ - "$LAUNCHER_DIR/Sources/CoreModels.swift" \ - "$LAUNCHER_DIR/Sources/HostedGameUpdate.swift" \ - "$LAUNCHER_DIR/Sources/LauncherPaths.swift" \ - "$LAUNCHER_DIR/Sources/SystemServices.swift" \ - "$LAUNCHER_DIR/Sources/InstallerService.swift" \ - "$LAUNCHER_DIR/Tests/InstallerIntegration.swift" \ - -o "$TEST_BINARY" - -"$TEST_BINARY" \ - "$TEST_ROOT/data" \ - "$APP_RESOURCES" \ - "$FIXTURE_DIR/platform-tools_r36.0.2-darwin.zip" \ - "$FIXTURE_DIR/emulator-darwin_aarch64-15917651.zip" \ - "$FIXTURE_DIR/arm64-v8a-36_r07.zip" diff --git a/scripts/login-tft-from-keychain.command b/scripts/login-tft-from-keychain.command index 614ed89..b80055a 100755 --- a/scripts/login-tft-from-keychain.command +++ b/scripts/login-tft-from-keychain.command @@ -12,7 +12,7 @@ ADB="$(tft_resolve_adb)" readonly ADB readonly SERIAL="${TFT_SERIAL:-emulator-5582}" readonly PACKAGE="com.riotgames.league.teamfighttactics.pbe" -readonly KEYCHAIN_SERVICE="${MACTICIAN_KEYCHAIN_SERVICE:-dev.sergeinaumov.mactician}" +readonly KEYCHAIN_SERVICE="${TFTMAC_KEYCHAIN_SERVICE:-com.flashls1.tftmac}" readonly CDP_HELPER="$SCRIPT_DIR/login-tft-webview.mjs" if [[ "$ADB_SERVER_PORT" != <-> ]] \ diff --git a/scripts/prepare-sparkle.command b/scripts/prepare-sparkle.command deleted file mode 100755 index bc367c5..0000000 --- a/scripts/prepare-sparkle.command +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -readonly LAUNCHER_DIR="$PROJECT_DIR/launcher" -readonly SPARKLE_VERSION="2.9.4" -readonly SPARKLE_ARCHIVE="Sparkle-$SPARKLE_VERSION.tar.xz" -readonly SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/$SPARKLE_VERSION/$SPARKLE_ARCHIVE" -readonly SPARKLE_SHA256="ce89daf967db1e1893ed3ebd67575ed82d3902563e3191ca92aaec9164fbdef9" -readonly DEPENDENCY_ROOT="$LAUNCHER_DIR/.build/sparkle-$SPARKLE_VERSION" -readonly FRAMEWORK="$DEPENDENCY_ROOT/Sparkle.framework" -readonly TOOLS_DIR="$DEPENDENCY_ROOT/bin" -readonly LICENSE_FILE="$DEPENDENCY_ROOT/LICENSE" -readonly CACHE_DIR="$LAUNCHER_DIR/.build/downloads" -readonly CACHED_ARCHIVE="$CACHE_DIR/$SPARKLE_ARCHIVE" - -framework_is_ready() { - [[ -d "$FRAMEWORK" ]] \ - && [[ -x "$TOOLS_DIR/generate_appcast" ]] \ - && [[ -s "$LICENSE_FILE" ]] \ - && [[ "$(plutil -extract CFBundleShortVersionString raw \ - "$FRAMEWORK/Resources/Info.plist" 2>/dev/null || true)" == "$SPARKLE_VERSION" ]] \ - && codesign --verify --deep --strict "$FRAMEWORK" >/dev/null 2>&1 -} - -if framework_is_ready; then - print "$DEPENDENCY_ROOT" - exit 0 -fi - -mkdir -p "$CACHE_DIR" -if [[ ! -f "$CACHED_ARCHIVE" ]] \ - || [[ "$(shasum -a 256 "$CACHED_ARCHIVE" | awk '{print $1}')" != "$SPARKLE_SHA256" ]]; then - rm -f "$CACHED_ARCHIVE.partial" - curl -fL --retry 3 --retry-delay 2 \ - "$SPARKLE_URL" \ - -o "$CACHED_ARCHIVE.partial" - if [[ "$(shasum -a 256 "$CACHED_ARCHIVE.partial" | awk '{print $1}')" != "$SPARKLE_SHA256" ]]; then - rm -f "$CACHED_ARCHIVE.partial" - print -u2 "Sparkle $SPARKLE_VERSION archive SHA-256 mismatch." - exit 1 - fi - mv -f "$CACHED_ARCHIVE.partial" "$CACHED_ARCHIVE" -fi - -readonly EXTRACT_ROOT="$(mktemp -d /private/tmp/tft-sparkle.XXXXXX)" -cleanup() { - rm -rf "$EXTRACT_ROOT" -} -trap cleanup EXIT - -tar -xJf "$CACHED_ARCHIVE" -C "$EXTRACT_ROOT" -codesign --verify --deep --strict "$EXTRACT_ROOT/Sparkle.framework" - -rm -rf "$DEPENDENCY_ROOT" -mkdir -p "$DEPENDENCY_ROOT" "$TOOLS_DIR" -ditto "$EXTRACT_ROOT/Sparkle.framework" "$FRAMEWORK" -ditto "$EXTRACT_ROOT/LICENSE" "$LICENSE_FILE" -for tool in BinaryDelta generate_appcast generate_keys sign_update; do - ditto "$EXTRACT_ROOT/bin/$tool" "$TOOLS_DIR/$tool" -done - -framework_is_ready || { - print -u2 "Prepared Sparkle dependency failed validation." - exit 1 -} - -print "$DEPENDENCY_ROOT" diff --git a/scripts/publish-game-update.command b/scripts/publish-game-update.command deleted file mode 100755 index d1bea21..0000000 --- a/scripts/publish-game-update.command +++ /dev/null @@ -1,164 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -readonly SPARKLE_ROOT="$("$PROJECT_DIR/scripts/prepare-sparkle.command")" -readonly SIGN_UPDATE="$SPARKLE_ROOT/bin/sign_update" -readonly APK_DIR="${MACTICIAN_GAME_APK_DIR:-${TFT_GAME_APK_DIR:-}}" -readonly VERSION="${MACTICIAN_GAME_VERSION:-}" -readonly VERSION_CODE="${MACTICIAN_GAME_VERSION_CODE:-}" -readonly SIGNING_ACCOUNT="${MACTICIAN_GAME_SIGNING_ACCOUNT:-mactician-game-updates}" -readonly SIGNING_KEY_FILE="${MACTICIAN_GAME_SIGNING_KEY_FILE:-}" -readonly UPDATE_BASE_URL="${MACTICIAN_UPDATE_BASE_URL:-https://sergeinaumov.dev/mactician/updates}" -readonly SSH_TARGET="${MACTICIAN_UPDATE_SSH_TARGET:-}" -readonly SSH_PORT="${MACTICIAN_UPDATE_SSH_PORT:-22}" -readonly REMOTE_ROOT="${MACTICIAN_UPDATE_REMOTE_ROOT:-}" -readonly OUTPUT_ROOT="${MACTICIAN_GAME_UPDATE_WORKDIR:-$PROJECT_DIR/dist/mactician-game-update}" -typeset -i PREPARE_ONLY=0 - -usage() { - print -u2 "Usage: ${0:t} [--prepare-only]" -} - -for argument in "$@"; do - case "$argument" in - --prepare-only) - (( PREPARE_ONLY == 0 )) || { usage; exit 2; } - PREPARE_ONLY=1 - ;; - *) - usage - exit 2 - ;; - esac -done - -[[ -d "$APK_DIR" ]] || { print -u2 "MACTICIAN_GAME_APK_DIR must contain the official split APK files."; exit 2; } -[[ -n "$VERSION" ]] || { print -u2 "MACTICIAN_GAME_VERSION is required."; exit 2; } -[[ "$VERSION_CODE" == <1-> ]] || { print -u2 "MACTICIAN_GAME_VERSION_CODE must be a positive integer."; exit 2; } -[[ -n "$SIGNING_ACCOUNT" ]] || { print -u2 "MACTICIAN_GAME_SIGNING_ACCOUNT is required to sign the game feed."; exit 2; } -command -v jq >/dev/null || { print -u2 "jq is required."; exit 1; } -command -v openssl >/dev/null || { print -u2 "openssl is required."; exit 1; } - -typeset -a APK_FILES -typeset apk name size sha256 url apk_json='[]' -APK_FILES=("$APK_DIR/base.apk") -for apk in "$APK_DIR"/*.apk(N); do - [[ "${apk:t}" == "base.apk" ]] || APK_FILES+=("$apk") -done -(( ${#APK_FILES[@]} >= 1 && ${#APK_FILES[@]} <= 32 )) || { - print -u2 "The game release must contain between 1 and 32 split APK files." - exit 1 -} - -for apk in "${APK_FILES[@]}"; do - [[ -f "$apk" ]] || { print -u2 "APK not found: $apk"; exit 1; } - name="${apk:t}" - [[ "$name" =~ '^[A-Za-z0-9._-]+[.]apk$' ]] || { print -u2 "Unsafe APK filename: $name"; exit 1; } - size="$(stat -f '%z' "$apk")" - sha256="$(shasum -a 256 "$apk" | awk '{print $1}')" - [[ "$sha256" =~ '^[0-9a-f]{64}$' ]] || { - print -u2 "Could not hash $name" - exit 1 - } - if [[ "$name" == "base.apk" ]]; then - readonly BASE_SHA256="$sha256" - fi - url="$UPDATE_BASE_URL/game/releases/$BASE_SHA256/$name" - apk_json="$(jq -c \ - --arg name "$name" \ - --arg url "$url" \ - --arg sha256 "$sha256" \ - --argjson size "$size" \ - '. + [{name: $name, size: $size, sha256: $sha256, url: $url}]' \ - <<<"$apk_json")" -done - -readonly RELEASE_ROOT="$OUTPUT_ROOT/releases/$BASE_SHA256" -readonly PAYLOAD="$OUTPUT_ROOT/payload.json" -readonly MANIFEST="$OUTPUT_ROOT/manifest.json" -case "$OUTPUT_ROOT" in - "/"|"/Users"|"$HOME"|"$PROJECT_DIR") - print -u2 "Refusing unsafe local output root: $OUTPUT_ROOT" - exit 2 - ;; -esac -rm -rf "$OUTPUT_ROOT" -mkdir -p "$RELEASE_ROOT" -for apk in "${APK_FILES[@]}"; do - ditto "$apk" "$RELEASE_ROOT/${apk:t}" -done - -jq -n \ - --arg publishedAt "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ - --arg packageName "com.riotgames.league.teamfighttactics.pbe" \ - --arg version "$VERSION" \ - --argjson versionCode "$VERSION_CODE" \ - --arg baseSHA256 "$BASE_SHA256" \ - --argjson apks "$apk_json" \ - '{ - schemaVersion: 1, - publishedAt: $publishedAt, - release: { - packageName: $packageName, - version: $version, - versionCode: $versionCode, - baseSHA256: $baseSHA256, - apks: $apks - } - }' >"$PAYLOAD" - -if [[ -n "$SIGNING_KEY_FILE" ]]; then - [[ -f "$SIGNING_KEY_FILE" ]] || { print -u2 "MACTICIAN_GAME_SIGNING_KEY_FILE was not found."; exit 2; } - readonly SIGNATURE="$("$SIGN_UPDATE" --ed-key-file "$SIGNING_KEY_FILE" -p "$PAYLOAD")" -else - readonly SIGNATURE="$("$SIGN_UPDATE" --account "$SIGNING_ACCOUNT" -p "$PAYLOAD")" -fi -[[ "$SIGNATURE" =~ '^[A-Za-z0-9+/=]+$' ]] || { print -u2 "Sparkle returned an invalid feed signature."; exit 1; } -readonly PAYLOAD_BASE64="$(openssl base64 -A -in "$PAYLOAD")" -jq -n \ - --arg payload "$PAYLOAD_BASE64" \ - --arg signature "$SIGNATURE" \ - '{schemaVersion: 1, payload: $payload, signature: $signature}' >"$MANIFEST" - -if (( PREPARE_ONLY == 1 )); then - print "Prepared signed TFT PBE $VERSION feed: $MANIFEST" - exit 0 -fi - -[[ -n "$SSH_TARGET" && -n "$REMOTE_ROOT" ]] || { - print -u2 "Publishing requires MACTICIAN_UPDATE_SSH_TARGET and MACTICIAN_UPDATE_REMOTE_ROOT." - exit 2 -} -[[ "$SSH_PORT" == <1-> ]] && (( SSH_PORT >= 1 && SSH_PORT <= 65535 )) || { - print -u2 "MACTICIAN_UPDATE_SSH_PORT must be a TCP port from 1 through 65535." - exit 2 -} -case "$REMOTE_ROOT" in - "/"|"/var"|"/var/www"|"/usr"|"/etc"|"/home"|"/tmp") - print -u2 "Refusing unsafe remote update root: $REMOTE_ROOT" - exit 2 - ;; -esac - -remote_quote() { - print -r -- "'${1//\'/\'\\\'\'}'" -} - -readonly REMOTE_RELEASE="$REMOTE_ROOT/game/releases/$BASE_SHA256" -readonly REMOTE_STAGING="$REMOTE_ROOT/game/.release-$BASE_SHA256-$$" -readonly REMOTE_MANIFEST_NEXT="$REMOTE_ROOT/game/.manifest-$$.json" -ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_TARGET" \ - "mkdir -p -- $(remote_quote "$REMOTE_STAGING") $(remote_quote "$REMOTE_ROOT/game/releases")" -scp -P "$SSH_PORT" -o StrictHostKeyChecking=accept-new \ - "$RELEASE_ROOT"/*.apk \ - "$SSH_TARGET:$REMOTE_STAGING/" -ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_TARGET" \ - "mkdir -p -- $(remote_quote "$REMOTE_RELEASE") && chmod 644 -- $(remote_quote "$REMOTE_STAGING")/*.apk && mv -f -- $(remote_quote "$REMOTE_STAGING")/*.apk $(remote_quote "$REMOTE_RELEASE")/ && rmdir -- $(remote_quote "$REMOTE_STAGING")" -scp -P "$SSH_PORT" -o StrictHostKeyChecking=accept-new \ - "$MANIFEST" \ - "$SSH_TARGET:$REMOTE_MANIFEST_NEXT" -ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_TARGET" \ - "chmod 644 -- $(remote_quote "$REMOTE_MANIFEST_NEXT") && mv -f -- $(remote_quote "$REMOTE_MANIFEST_NEXT") $(remote_quote "$REMOTE_ROOT/game/manifest.json")" - -print "Published TFT PBE $VERSION to $UPDATE_BASE_URL/game/manifest.json" diff --git a/scripts/publish-mactician-update.command b/scripts/publish-mactician-update.command deleted file mode 100755 index 6fb7049..0000000 --- a/scripts/publish-mactician-update.command +++ /dev/null @@ -1,181 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" - -typeset -i PREPARE_ONLY=0 -typeset -i ALLOW_ADHOC=0 -usage() { - print -u2 "Usage: ${0:t} [--prepare-only] [--allow-adhoc]" -} -for argument in "$@"; do - case "$argument" in - --prepare-only) - (( PREPARE_ONLY == 0 )) || { usage; exit 2; } - PREPARE_ONLY=1 - ;; - --allow-adhoc) - (( ALLOW_ADHOC == 0 )) || { usage; exit 2; } - ALLOW_ADHOC=1 - ;; - *) - usage - exit 2 - ;; - esac -done - -readonly SPARKLE_ROOT="$("$PROJECT_DIR/scripts/prepare-sparkle.command")" -readonly GENERATE_APPCAST="$SPARKLE_ROOT/bin/generate_appcast" -readonly SPARKLE_ACCOUNT="${MACTICIAN_SPARKLE_ACCOUNT:-}" -readonly UPDATE_BASE_URL="${MACTICIAN_UPDATE_BASE_URL:-https://sergeinaumov.dev/mactician/updates}" -readonly PRODUCT_URL="${MACTICIAN_UPDATE_PRODUCT_URL:-https://sergeinaumov.dev/mactician}" -readonly SSH_TARGET="${MACTICIAN_UPDATE_SSH_TARGET:-}" -readonly SSH_PORT="${MACTICIAN_UPDATE_SSH_PORT:-}" -readonly REMOTE_ROOT="${MACTICIAN_UPDATE_REMOTE_ROOT:-}" -readonly APP="${MACTICIAN_APP:-$PROJECT_DIR/dist/Mactician.app}" -readonly UPDATE_ROOT="${MACTICIAN_UPDATE_WORKDIR:-$PROJECT_DIR/dist/mactician-updates}" - -if [[ -z "$SPARKLE_ACCOUNT" ]]; then - print -u2 "MACTICIAN_SPARKLE_ACCOUNT must name the Sparkle Ed25519 key stored in Keychain." - exit 2 -fi - -if [[ ! -d "$APP" ]]; then - print -u2 "Build Mactician before preparing an update." - exit 1 -fi - -readonly INFO_PLIST="$APP/Contents/Info.plist" -readonly VERSION="$(plutil -extract CFBundleShortVersionString raw "$INFO_PLIST")" -readonly BUILD="$(plutil -extract CFBundleVersion raw "$INFO_PLIST")" -readonly BUNDLE_ID="$(plutil -extract CFBundleIdentifier raw "$INFO_PLIST")" -readonly PUBLIC_KEY="$(plutil -extract SUPublicEDKey raw "$INFO_PLIST")" -readonly DMG="${MACTICIAN_DMG:-$PROJECT_DIR/dist/Mactician-$VERSION.dmg}" -readonly RELEASE_BASENAME="Mactician-$VERSION" -readonly RELEASE_ARCHIVE="$UPDATE_ROOT/$RELEASE_BASENAME.dmg" -readonly RELEASE_NOTES_SOURCE="${MACTICIAN_RELEASE_NOTES:-$PROJECT_DIR/launcher/Resources/release-notes/$VERSION.md}" -readonly RELEASE_NOTES="$UPDATE_ROOT/$RELEASE_BASENAME.md" -readonly APPCAST="$UPDATE_ROOT/appcast.xml" - -if [[ ! -f "$DMG" ]]; then - print -u2 "Mactician DMG not found: $DMG" - exit 1 -fi - -if [[ "$BUNDLE_ID" != "dev.sergeinaumov.mactician" ]]; then - print -u2 "Unexpected launcher bundle identifier: $BUNDLE_ID" - exit 1 -fi -if [[ "$PUBLIC_KEY" != "77t8YuvP4mvvP/3oMpVR/TqGRMCcUlrpWFIZGcWqokY=" ]]; then - print -u2 "The launcher does not contain the expected Sparkle public key." - exit 1 -fi -if [[ ! -f "$RELEASE_NOTES_SOURCE" ]]; then - print -u2 "Release notes not found: $RELEASE_NOTES_SOURCE" - exit 1 -fi - -if (( PREPARE_ONLY == 0 )); then - readonly CODESIGN_DETAILS="$(codesign -dvv "$APP" 2>&1)" - if [[ "$CODESIGN_DETAILS" == *'Authority=Developer ID Application:'* ]]; then - codesign --verify --deep --strict "$APP" - xcrun stapler validate "$DMG" - spctl --assess --type open --context context:primary-signature "$DMG" - elif (( ALLOW_ADHOC == 1 )); then - if [[ "$CODESIGN_DETAILS" != *'Signature=adhoc'* ]]; then - print -u2 "The temporary release must contain a valid ad-hoc app signature." - exit 1 - fi - codesign --verify --deep --strict "$APP" - hdiutil verify "$DMG" >/dev/null - print -u2 "Warning: publishing an ad-hoc build without Apple notarization." - else - print -u2 "Publishing requires a Developer ID-signed launcher build." - print -u2 "Use --allow-adhoc only for an explicitly approved temporary release." - exit 1 - fi -fi - -mkdir -p "$UPDATE_ROOT" -ditto "$DMG" "$RELEASE_ARCHIVE" -ditto "$RELEASE_NOTES_SOURCE" "$RELEASE_NOTES" - -"$GENERATE_APPCAST" \ - --account "$SPARKLE_ACCOUNT" \ - --download-url-prefix "$UPDATE_BASE_URL/releases/" \ - --release-notes-url-prefix "$UPDATE_BASE_URL/releases/" \ - --link "$PRODUCT_URL" \ - --maximum-versions 3 \ - --maximum-deltas 5 \ - "$UPDATE_ROOT" - -MACTICIAN_RELEASE_TITLE="Mactician $VERSION" \ - perl -0pi -e ' - s{(.*?).*?()}{$1Mactician Updates$2}s; - s{(.*?).*?()}{$1$ENV{MACTICIAN_RELEASE_TITLE}$2}s; - ' "$APPCAST" - -xmllint --noout "$APPCAST" -if ! grep -Fq 'Mactician Updates' "$APPCAST" \ - || ! grep -Fq "Mactician $VERSION" "$APPCAST"; then - print -u2 "Generated appcast titles do not match the Mactician release metadata." - exit 1 -fi -if ! grep -Fq 'sparkle:edSignature=' "$APPCAST"; then - print -u2 "Generated appcast does not contain an Ed25519 archive signature." - exit 1 -fi - -if (( PREPARE_ONLY == 1 )); then - print "Prepared Sparkle feed with signed update archives: $APPCAST" - exit 0 -fi - -if [[ -z "$SSH_TARGET" || -z "$SSH_PORT" || -z "$REMOTE_ROOT" ]]; then - print -u2 "Publishing requires MACTICIAN_UPDATE_SSH_TARGET, MACTICIAN_UPDATE_SSH_PORT, and MACTICIAN_UPDATE_REMOTE_ROOT." - exit 2 -fi -if [[ "$SSH_PORT" != <-> ]] || (( SSH_PORT < 1 || SSH_PORT > 65535 )); then - print -u2 "MACTICIAN_UPDATE_SSH_PORT must be a TCP port from 1 through 65535." - exit 2 -fi - -case "$REMOTE_ROOT" in - "/"|"/var"|"/var/www"|"/usr"|"/etc"|"/home"|"/tmp") - print -u2 "Refusing unsafe remote update root: $REMOTE_ROOT" - exit 2 - ;; -esac - -remote_quote() { - print -r -- "'${1//\'/\'\\\'\'}'" -} - -ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_TARGET" \ - "mkdir -p -- $(remote_quote "$REMOTE_ROOT/releases")" - -typeset -a RELEASE_FILES -RELEASE_FILES=() -for release_file in "$UPDATE_ROOT"/*(.N); do - if [[ "${release_file:t}" != "appcast.xml" ]]; then - RELEASE_FILES+=("$release_file") - fi -done -if (( ${#RELEASE_FILES[@]} == 0 )); then - print -u2 "No Sparkle release files were generated." - exit 1 -fi - -scp -P "$SSH_PORT" -o StrictHostKeyChecking=accept-new \ - "${RELEASE_FILES[@]}" \ - "$SSH_TARGET:$REMOTE_ROOT/releases/" - -readonly REMOTE_APPCAST_NEXT="$REMOTE_ROOT/.appcast-$BUILD.xml" -scp -P "$SSH_PORT" -o StrictHostKeyChecking=accept-new \ - "$APPCAST" \ - "$SSH_TARGET:$REMOTE_APPCAST_NEXT" -ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_TARGET" \ - "chmod 644 -- $(remote_quote "$REMOTE_APPCAST_NEXT") && mv -f -- $(remote_quote "$REMOTE_APPCAST_NEXT") $(remote_quote "$REMOTE_ROOT/appcast.xml")" - -print "Published Mactician $VERSION (build $BUILD) to $UPDATE_BASE_URL" diff --git a/scripts/restore-emulator-graphics.command b/scripts/restore-emulator-graphics.command index 744d98f..3df64fb 100755 --- a/scripts/restore-emulator-graphics.command +++ b/scripts/restore-emulator-graphics.command @@ -10,7 +10,7 @@ readonly ANGLE_DIR="$LIB_DIR/gles_angle" restore_library() { local active="$1" - local backup="$active.mactician-original" + local backup="$active.tftmac-original" if [[ -L "$active" && -f "$backup" ]]; then unlink "$active" diff --git a/scripts/run-android-ui-transport-probe.command b/scripts/run-android-ui-transport-probe.command deleted file mode 100755 index 5a12ffe..0000000 --- a/scripts/run-android-ui-transport-probe.command +++ /dev/null @@ -1,250 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly SCRIPT_DIR="${0:A:h}" -readonly PROJECT_DIR="${SCRIPT_DIR:h}" -source "$PROJECT_DIR/scripts/android-environment.sh" - -readonly LABEL="${1:-}" -readonly ROUNDS="${2:-6}" -readonly SWIPE_PAIRS="${TFT_UI_TRANSPORT_SWIPE_PAIRS:-15}" -readonly SWIPE_DURATION_MS="${TFT_UI_TRANSPORT_SWIPE_DURATION_MS:-180}" -readonly MINIMUM_FRAMES_PER_ROUND="${TFT_UI_TRANSPORT_MINIMUM_FRAMES_PER_ROUND:-$(( SWIPE_PAIRS * 8 ))}" -readonly ADB_SERVER_PORT="${TFT_ADB_SERVER_PORT:-5038}" -readonly SERIAL="${TFT_SERIAL:-emulator-5582}" -readonly EXPECTED_GRAPHICS_PROFILE="${TFT_UI_TRANSPORT_EXPECTED_GRAPHICS_PROFILE:-}" -readonly TFT_PACKAGE="com.riotgames.league.teamfighttactics.pbe" -readonly SETTINGS_PACKAGE="com.android.settings" -readonly ADB="$(tft_resolve_adb)" -readonly JQ="$(command -v jq || true)" - -if [[ ! "$LABEL" =~ '^[A-Za-z0-9][A-Za-z0-9._-]*$' ]]; then - print -u2 "Usage: ${0:t} [rounds]" - print -u2 "The profile label may contain letters, digits, dots, underscores, and dashes." - exit 2 -fi -for numeric_name numeric_value in \ - rounds "$ROUNDS" \ - swipe-pairs "$SWIPE_PAIRS" \ - swipe-duration-ms "$SWIPE_DURATION_MS" \ - minimum-frames-per-round "$MINIMUM_FRAMES_PER_ROUND"; do - if [[ "$numeric_value" != <-> ]] || (( numeric_value < 1 )); then - print -u2 "$numeric_name must be a positive integer." - exit 2 - fi -done -if [[ -z "$JQ" ]]; then - print -u2 "jq is required to write the transport-probe summary." - exit 1 -fi -if [[ ! "$EXPECTED_GRAPHICS_PROFILE" =~ '^[a-z0-9][a-z0-9-]*$' ]]; then - print -u2 "TFT_UI_TRANSPORT_EXPECTED_GRAPHICS_PROFILE is required and must use lowercase letters, digits, and dashes." - exit 2 -fi -if [[ "$ADB_SERVER_PORT" != <-> ]] \ - || (( ADB_SERVER_PORT < 1024 || ADB_SERVER_PORT > 65534 )); then - print -u2 "TFT_ADB_SERVER_PORT must be a TCP port from 1024 through 65534." - exit 2 -fi - -unset ADB_SERVER_SOCKET ANDROID_ADB_SERVER_ADDRESS -export ANDROID_ADB_SERVER_PORT="$ADB_SERVER_PORT" -"$ADB" -P "$ADB_SERVER_PORT" start-server >/dev/null -if ! "$ADB" -s "$SERIAL" get-state >/dev/null 2>&1; then - print -u2 "The transport probe requires an already running AVD on $SERIAL." - exit 1 -fi -readonly ACTIVE_GRAPHICS_PROFILE="$( - "$ADB" -s "$SERIAL" shell getprop ro.boot.mactician.graphics_profile 2>/dev/null \ - | tr -d '\r' -)" -if [[ "$ACTIVE_GRAPHICS_PROFILE" != "$EXPECTED_GRAPHICS_PROFILE" ]]; then - print -u2 "The running AVD graphics profile is '${ACTIVE_GRAPHICS_PROFILE:-}', expected '$EXPECTED_GRAPHICS_PROFILE'." - print -u2 "Cold-boot the requested profile before collecting transport evidence." - exit 1 -fi -readonly TRANSPORT="$( - "$ADB" -s "$SERIAL" shell getprop ro.boot.hardware.gltransport 2>/dev/null \ - | tr -d '\r' -)" -readonly HWUI_RENDERER="$( - "$ADB" -s "$SERIAL" shell getprop debug.hwui.renderer 2>/dev/null \ - | tr -d '\r' -)" - -readonly DISPLAY_SIZE="$( - "$ADB" -s "$SERIAL" shell wm size 2>/dev/null \ - | tr -d '\r' \ - | sed -n \ - -e 's/^Physical size: \([0-9][0-9]*x[0-9][0-9]*\)$/\1/p' \ - -e 's/^Override size: \([0-9][0-9]*x[0-9][0-9]*\)$/\1/p' \ - | tail -n 1 -)" -if [[ ! "$DISPLAY_SIZE" =~ '^([0-9]+)x([0-9]+)$' ]]; then - print -u2 "Could not determine the active AVD display size." - exit 1 -fi -readonly DISPLAY_WIDTH="${match[1]}" -readonly DISPLAY_HEIGHT="${match[2]}" -readonly DISPLAY_DENSITY="$( - "$ADB" -s "$SERIAL" shell wm density 2>/dev/null \ - | tr -d '\r' \ - | sed -n \ - -e 's/^Physical density: \([0-9][0-9]*\)$/\1/p' \ - -e 's/^Override density: \([0-9][0-9]*\)$/\1/p' \ - | tail -n 1 -)" -if [[ "$DISPLAY_DENSITY" != <-> ]]; then - print -u2 "Could not determine the active AVD display density." - exit 1 -fi -readonly SWIPE_X=$(( DISPLAY_WIDTH / 2 )) -readonly SWIPE_LOW_Y=$(( DISPLAY_HEIGHT * 3 / 4 )) -readonly SWIPE_HIGH_Y=$(( DISPLAY_HEIGHT / 4 )) - -readonly UTC="$(date -u +%Y%m%dT%H%M%SZ)" -readonly RUN_DIR="$PROJECT_DIR/runtime/measurements/android-ui-transport/${UTC}__${LABEL}" -readonly ROUNDS_JSONL="$RUN_DIR/rounds.jsonl" -mkdir -p "${RUN_DIR:h}" -if ! mkdir "$RUN_DIR" 2>/dev/null; then - if [[ -e "$RUN_DIR" ]]; then - print -u2 "Transport-probe evidence directory already exists: $RUN_DIR" - print -u2 "Use a distinct label; existing evidence will not be overwritten." - else - print -u2 "Could not create the transport-probe evidence directory: $RUN_DIR" - fi - exit 1 -fi -: > "$ROUNDS_JSONL" - -write_rejected_summary() { - local reason="$1" - "$JQ" -s \ - --arg utc "$UTC" \ - --arg profile_label "$LABEL" \ - --arg graphics_profile "$ACTIVE_GRAPHICS_PROFILE" \ - --arg serial "$SERIAL" \ - --arg display "$DISPLAY_SIZE" \ - --argjson display_density "$DISPLAY_DENSITY" \ - --arg transport "$TRANSPORT" \ - --arg hwui_renderer "$HWUI_RENDERER" \ - --arg rejected_reason "$reason" \ - --argjson swipe_pairs "$SWIPE_PAIRS" \ - --argjson swipe_duration_ms "$SWIPE_DURATION_MS" \ - --argjson minimum_frames_per_round "$MINIMUM_FRAMES_PER_ROUND" \ - '{schema_version: 5, utc: $utc, "label": $profile_label, serial: $serial, - graphics_profile: $graphics_profile, - display: $display, display_density: $display_density, - transport: $transport, hwui_renderer: $hwui_renderer, - swipe_pairs: $swipe_pairs, swipe_duration_ms: $swipe_duration_ms, - minimum_frames_per_round: $minimum_frames_per_round, - rejected_reason: $rejected_reason, rounds: .}' \ - "$ROUNDS_JSONL" > "$RUN_DIR/summary.json" -} - -"$ADB" -s "$SERIAL" shell am force-stop "$TFT_PACKAGE" -"$ADB" -s "$SERIAL" shell am start -W -a android.settings.SETTINGS > "$RUN_DIR/start.txt" -if ! grep -Fqx 'Status: ok' "$RUN_DIR/start.txt" \ - || ! grep -Eq '^Activity: com[.]android[.]settings/' "$RUN_DIR/start.txt"; then - write_rejected_summary settings_not_foreground - print -u2 "Android Settings did not become the measured foreground activity." - print -u2 "Partial evidence is retained: $RUN_DIR" - exit 1 -fi -sleep 2 - -integer round pair -for (( round = 1; round <= ROUNDS; round++ )); do - round_gfxinfo="$RUN_DIR/round-$round-gfxinfo.txt" - "$ADB" -s "$SERIAL" shell dumpsys gfxinfo "$SETTINGS_PACKAGE" reset >/dev/null - start_ns="$(date +%s%N)" - for (( pair = 1; pair <= SWIPE_PAIRS; pair++ )); do - "$ADB" -s "$SERIAL" shell input swipe \ - "$SWIPE_X" "$SWIPE_LOW_Y" "$SWIPE_X" "$SWIPE_HIGH_Y" \ - "$SWIPE_DURATION_MS" >/dev/null - "$ADB" -s "$SERIAL" shell input swipe \ - "$SWIPE_X" "$SWIPE_HIGH_Y" "$SWIPE_X" "$SWIPE_LOW_Y" \ - "$SWIPE_DURATION_MS" >/dev/null - done - end_ns="$(date +%s%N)" - elapsed_ns=$(( end_ns - start_ns )) - "$ADB" -s "$SERIAL" shell dumpsys gfxinfo "$SETTINGS_PACKAGE" > "$round_gfxinfo" - - total_frames="$(sed -n 's/^Total frames rendered: \([0-9][0-9]*\)$/\1/p' "$round_gfxinfo" | head -n 1)" - janky_frames="$(sed -n 's/^Janky frames: \([0-9][0-9]*\) .*/\1/p' "$round_gfxinfo" | head -n 1)" - p50_ms="$(sed -n 's/^50th percentile: \([0-9][0-9]*\)ms$/\1/p' "$round_gfxinfo" | head -n 1)" - p90_ms="$(sed -n 's/^90th percentile: \([0-9][0-9]*\)ms$/\1/p' "$round_gfxinfo" | head -n 1)" - p95_ms="$(sed -n 's/^95th percentile: \([0-9][0-9]*\)ms$/\1/p' "$round_gfxinfo" | head -n 1)" - p99_ms="$(sed -n 's/^99th percentile: \([0-9][0-9]*\)ms$/\1/p' "$round_gfxinfo" | head -n 1)" - if [[ -z "$total_frames" || -z "$janky_frames" || -z "$p50_ms" \ - || -z "$p90_ms" || -z "$p95_ms" || -z "$p99_ms" ]]; then - write_rejected_summary "gfxinfo_parse_round_$round" - print -u2 "Could not parse Android gfxinfo for round $round." - exit 1 - fi - - "$JQ" -n \ - --argjson round "$round" \ - --argjson elapsed_ns "$elapsed_ns" \ - --argjson total_frames "$total_frames" \ - --argjson janky_frames "$janky_frames" \ - --argjson p50_ms "$p50_ms" \ - --argjson p90_ms "$p90_ms" \ - --argjson p95_ms "$p95_ms" \ - --argjson p99_ms "$p99_ms" \ - '{round: $round, elapsed_ns: $elapsed_ns, total_frames: $total_frames, - janky_frames: $janky_frames, p50_ms: $p50_ms, p90_ms: $p90_ms, - p95_ms: $p95_ms, p99_ms: $p99_ms}' >> "$ROUNDS_JSONL" - if (( total_frames < MINIMUM_FRAMES_PER_ROUND )); then - write_rejected_summary "non_rendering_round_$round" - print -u2 "Transport probe became non-rendering in round $round: $total_frames frames, expected at least $MINIMUM_FRAMES_PER_ROUND." - print -u2 "Partial evidence is retained: $RUN_DIR" - exit 1 - fi -done - -"$JQ" -s \ - --arg utc "$UTC" \ - --arg profile_label "$LABEL" \ - --arg graphics_profile "$ACTIVE_GRAPHICS_PROFILE" \ - --arg serial "$SERIAL" \ - --arg display "$DISPLAY_SIZE" \ - --argjson display_density "$DISPLAY_DENSITY" \ - --arg transport "$TRANSPORT" \ - --arg hwui_renderer "$HWUI_RENDERER" \ - --argjson swipe_pairs "$SWIPE_PAIRS" \ - --argjson swipe_duration_ms "$SWIPE_DURATION_MS" \ - --argjson minimum_frames_per_round "$MINIMUM_FRAMES_PER_ROUND" \ - 'def median: - sort as $sorted - | ($sorted | length) as $count - | if ($count % 2) == 1 then $sorted[($count / 2 | floor)] - else (($sorted[$count / 2 - 1] + $sorted[$count / 2]) / 2) - end; - . as $all - | ($all | if length > 3 then .[3:] else . end) as $warm - | {schema_version: 5, utc: $utc, "label": $profile_label, serial: $serial, - graphics_profile: $graphics_profile, - display: $display, display_density: $display_density, - transport: $transport, hwui_renderer: $hwui_renderer, - swipe_pairs: $swipe_pairs, - swipe_duration_ms: $swipe_duration_ms, - minimum_frames_per_round: $minimum_frames_per_round, rounds: $all, - warmup_rounds_discarded: (($all | length) - ($warm | length)), - mean_elapsed_ms: (($all | map(.elapsed_ns) | add) / ($all | length) / 1000000), - median_elapsed_ms: (($all | map(.elapsed_ns) | median) / 1000000), - max_p95_ms: ($all | map(.p95_ms) | max), - max_p99_ms: ($all | map(.p99_ms) | max), - total_janky_frames: ($all | map(.janky_frames) | add), - warm_mean_elapsed_ms: (($warm | map(.elapsed_ns) | add) / ($warm | length) / 1000000), - warm_median_elapsed_ms: (($warm | map(.elapsed_ns) | median) / 1000000), - warm_max_p95_ms: ($warm | map(.p95_ms) | max), - warm_max_p99_ms: ($warm | map(.p99_ms) | max), - warm_total_janky_frames: ($warm | map(.janky_frames) | add)}' \ - "$ROUNDS_JSONL" > "$RUN_DIR/summary.json" - -print "Android UI transport probe complete: $RUN_DIR" -"$JQ" '{"label": .label, graphics_profile, transport, hwui_renderer, warm_mean_elapsed_ms, - warm_median_elapsed_ms, warm_max_p95_ms, warm_max_p99_ms, - warm_total_janky_frames}' "$RUN_DIR/summary.json" -print "TFT remains stopped in the running experimental AVD." diff --git a/scripts/run-asg-experiment.command b/scripts/run-asg-experiment.command index 7700196..09334d7 100755 --- a/scripts/run-asg-experiment.command +++ b/scripts/run-asg-experiment.command @@ -17,9 +17,9 @@ readonly LAUNCHER="${TFT_LAUNCHER:-$PROJECT_DIR/run-tft-gles32.command}" readonly PACKAGE="com.riotgames.league.teamfighttactics.pbe" readonly CONFIG="$AVD_HOME/$AVD_NAME.avd/config.ini" readonly HARDWARE_CONFIG="$AVD_HOME/$AVD_NAME.avd/hardware-qemu.ini" -readonly CONFIG_BACKUP="$CONFIG.mactician-asg-backup" -readonly HARDWARE_CONFIG_BACKUP="$HARDWARE_CONFIG.mactician-asg-backup" -readonly LOCK_FILE="$AVD_HOME/$AVD_NAME.avd/.mactician-avd.lock" +readonly CONFIG_BACKUP="$CONFIG.tftmac-asg-backup" +readonly HARDWARE_CONFIG_BACKUP="$HARDWARE_CONFIG.tftmac-asg-backup" +readonly LOCK_FILE="$AVD_HOME/$AVD_NAME.avd/.tftmac-avd.lock" readonly TRANSPORT="${TFT_GLTRANSPORT:-virtio-gpu-asg}" readonly EXPECTED_BASELINE_TRANSPORT="${TFT_EXPECTED_GLTRANSPORT_BASELINE:-pipe}" readonly DRAW_FLUSH_INTERVAL="${TFT_GL_DRAW_FLUSH_INTERVAL:-}" diff --git a/scripts/run-autonomous-trial-benchmark.command b/scripts/run-autonomous-trial-benchmark.command index d025e07..84fd9c9 100755 --- a/scripts/run-autonomous-trial-benchmark.command +++ b/scripts/run-autonomous-trial-benchmark.command @@ -32,9 +32,9 @@ readonly AVD_NAME="${TFT_AVD_NAME:-TftRootAffinity}" readonly AVD_DIR="$AVD_HOME/$AVD_NAME.avd" readonly CONFIG="$AVD_DIR/config.ini" readonly HARDWARE_CONFIG="$AVD_DIR/hardware-qemu.ini" -readonly ASG_CONFIG_BACKUP="$CONFIG.mactician-asg-backup" -readonly ASG_HARDWARE_BACKUP="$HARDWARE_CONFIG.mactician-asg-backup" -readonly AVD_LOCK="$AVD_DIR/.mactician-avd.lock" +readonly ASG_CONFIG_BACKUP="$CONFIG.tftmac-asg-backup" +readonly ASG_HARDWARE_BACKUP="$HARDWARE_CONFIG.tftmac-asg-backup" +readonly AVD_LOCK="$AVD_DIR/.tftmac-avd.lock" readonly WRAP_PROPERTY="wrap.$PACKAGE" if [[ -z "$CANDIDATE_ID" ]] \ diff --git a/scripts/run-host-angle-capability-probe.command b/scripts/run-host-angle-capability-probe.command index ae06c36..1d052c4 100755 --- a/scripts/run-host-angle-capability-probe.command +++ b/scripts/run-host-angle-capability-probe.command @@ -3,12 +3,12 @@ set -euo pipefail readonly PROJECT_DIR="${0:A:h:h}" readonly MODE="${1:-}" -readonly SDK_ROOT="${TFT_ROOT_SDK:-${TFT_ANDROID_SDK_ROOT:-$HOME/Library/Application Support/Mactician/sdk}}" +readonly SDK_ROOT="${TFT_ROOT_SDK:-${TFT_ANDROID_SDK_ROOT:-/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk}}" readonly ANGLE_DIR="$SDK_ROOT/emulator/lib64/gles_angle" readonly EGL_LIBRARY="$ANGLE_DIR/libEGL.dylib" readonly GLES_LIBRARY="$ANGLE_DIR/libGLESv2.dylib" readonly SWIFTSHADER_ICD="$ANGLE_DIR/vk_swiftshader_icd.json" -readonly BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mactician-angle-probe.XXXXXX")" +readonly BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/tftmac-angle-probe.XXXXXX")" readonly PROBE="$BUILD_DIR/angle-egl-capability-probe" cleanup() { diff --git a/scripts/run-performance-campaign.command b/scripts/run-performance-campaign.command index 405a445..d37a1b7 100755 --- a/scripts/run-performance-campaign.command +++ b/scripts/run-performance-campaign.command @@ -806,9 +806,9 @@ if "$ADB" -P "$ADB_SERVER_PORT" -s "$SERIAL" get-state >/dev/null 2>&1; then exit 4 fi typeset AVD_DIR="${TFT_ROOT_AVD_HOME:-$(tft_resolve_avd_home)}/${TFT_AVD_NAME:-TftRootAffinity}.avd" -if [[ -e "$AVD_DIR/.mactician-avd.lock" \ - || -e "$AVD_DIR/config.ini.mactician-asg-backup" \ - || -e "$AVD_DIR/hardware-qemu.ini.mactician-asg-backup" ]]; then +if [[ -e "$AVD_DIR/.tftmac-avd.lock" \ + || -e "$AVD_DIR/config.ini.tftmac-asg-backup" \ + || -e "$AVD_DIR/hardware-qemu.ini.tftmac-asg-backup" ]]; then print "Final check failed: AVD rollback markers were not removed." exit 4 fi diff --git a/scripts/summarize-native-session.command b/scripts/summarize-native-session.command new file mode 100755 index 0000000..4506cd2 --- /dev/null +++ b/scripts/summarize-native-session.command @@ -0,0 +1,236 @@ +#!/bin/zsh +# Summarize one native TFTMAC capture without exposing raw guest/emulator logs. +set -euo pipefail + +readonly SQLITE3_BIN="${TFT_SQLITE3:-$(command -v sqlite3 2>/dev/null || true)}" +readonly CAPTURE_ROOT_INPUT="${TFTMAC_CAPTURE_ROOT:-$HOME/Library/Application Support/TFTMAC/Captures}" + +fail() { + print -u2 -- "$*" + exit 1 +} + +canonical_directory() { + [[ -d "$1" ]] || return 1 + (cd "$1" && pwd -P) +} + +[[ -n "$SQLITE3_BIN" && -x "$SQLITE3_BIN" ]] || fail "sqlite3 is required to summarize a native TFTMAC session." +readonly CAPTURE_ROOT="$(canonical_directory "$CAPTURE_ROOT_INPUT")" \ + || fail "Capture root does not exist: $CAPTURE_ROOT_INPUT" + +typeset capture_input +if (( $# > 1 )); then + fail "Usage: ${0:t} [capture-directory]" +elif (( $# == 1 )); then + capture_input="$1" +else + typeset -a captures + captures=("$CAPTURE_ROOT"/*(N/om[1])) + (( ${#captures} > 0 )) || fail "No capture directories were found under $CAPTURE_ROOT" + capture_input="$captures[1]" +fi + +readonly CAPTURE_DIR="$(canonical_directory "$capture_input")" \ + || fail "Capture directory does not exist: $capture_input" +if [[ "$CAPTURE_DIR" != "$CAPTURE_ROOT" && "$CAPTURE_DIR" != "$CAPTURE_ROOT"/* ]]; then + fail "Capture directory must be inside $CAPTURE_ROOT" +fi + +readonly DATABASE="$CAPTURE_DIR/TFTMAC_NATIVE_RUNTIME.sqlite" +[[ -f "$DATABASE" ]] || fail "Missing TFTMAC_NATIVE_RUNTIME.sqlite in $CAPTURE_DIR" + +sql() { + "$SQLITE3_BIN" -readonly -noheader -batch "$DATABASE" "$1" +} + +table_exists() { + [[ "$(sql "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=$(sql_quote "$1");")" == "1" ]] +} + +sql_quote() { + local value="$1" + value="${value//\'/\'\'}" + print -r -- "'$value'" +} + +has_column() { + local table="$1" column="$2" + [[ "$(sql "SELECT count(*) FROM pragma_table_info($(sql_quote "$table")) WHERE name=$(sql_quote "$column");")" == "1" ]] +} + +first_column() { + local table="$1" + shift + local candidate + for candidate in "$@"; do + if has_column "$table" "$candidate"; then + print -r -- "$candidate" + return 0 + fi + done + return 1 +} + +column_or_null() { + local table="$1" + shift + local column + column="$(first_column "$table" "$@" 2>/dev/null || true)" + [[ -n "$column" ]] && print -r -- "\"$column\"" || print -r -- "NULL" +} + +print_table_coverage() { + local table="$1" time_column rows earliest latest + if ! table_exists "$table"; then + print " $table: UNAVAILABLE (this app version did not create the table)" + return + fi + rows="$(sql "SELECT count(*) FROM \"$table\";")" + time_column="$(first_column "$table" observed_utc started_utc ended_utc sampled_utc created_utc timestamp_utc started_monotonic_ns sampled_monotonic_ns monotonic_ns 2>/dev/null || true)" + if [[ -z "$time_column" ]]; then + print " $table: rows=$rows coverage=timestamp-column-unavailable" + return + fi + earliest="$(sql "SELECT COALESCE(min(\"$time_column\"), '—') FROM \"$table\";")" + latest="$(sql "SELECT COALESCE(max(\"$time_column\"), '—') FROM \"$table\";")" + print " $table: rows=$rows coverage=$earliest .. $latest" +} + +print_section() { + print "" + print "$1" +} + +print "TFTMAC native-session report" +print "capture=$CAPTURE_DIR" +print "database=$DATABASE" +print "generated_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + +readonly INTEGRITY="$(sql 'PRAGMA integrity_check;' | tr '\n' ' ')" +readonly QUICK="$(sql 'PRAGMA quick_check;' | tr '\n' ' ')" +print "integrity_check=$INTEGRITY" +print "quick_check=$QUICK" +[[ "$INTEGRITY" == "ok " && "$QUICK" == "ok " ]] || fail "Database integrity check failed; report stopped." + +print_section "Coverage" +for table in sessions runtime_receipts events presentation_samples frame_interval_windows \ + game_frame_windows game_frame_intervals stream_freshness_windows \ + host_presentation_windows resource_samples guest_memory_samples \ + surfaceflinger_samples audio_samples logcat_aggregates pipeline_log_aggregates \ + graphics_pipeline_snapshots diagnostic_artifacts; do + print_table_coverage "$table" +done + +print_section "Guest TFT frame truth (SurfaceFlinger timing)" +if table_exists game_frame_windows; then + readonly GUEST_LOW="$(column_or_null game_frame_windows rolling_1pct_low_fps one_percent_low_fps one_percent_low)" + readonly GUEST_P95="$(column_or_null game_frame_windows p95_interval_ms p95_ms)" + readonly GUEST_P99="$(column_or_null game_frame_windows p99_interval_ms p99_ms)" + readonly GUEST_MAX="$(column_or_null game_frame_windows maximum_interval_ms max_interval_ms max_ms)" + readonly GUEST_JANK="$(column_or_null game_frame_windows jank_count janky_frames)" + readonly GUEST_SEVERE="$(column_or_null game_frame_windows severe_count severe_frames)" + readonly GUEST_MISSED="$(column_or_null game_frame_windows missed_vsync_equivalents missed_vsync_count missed_vsyncs)" + readonly GUEST_ORDER="$(first_column game_frame_windows rolling_1pct_low_fps one_percent_low_fps p99_interval_ms maximum_interval_ms started_monotonic_ns 2>/dev/null || true)" + if [[ -n "$GUEST_ORDER" ]]; then + sql "SELECT ' worst window: 1%-low=' || COALESCE(CAST($GUEST_LOW AS TEXT), '—') || + ' p95ms=' || COALESCE(CAST($GUEST_P95 AS TEXT), '—') || + ' p99ms=' || COALESCE(CAST($GUEST_P99 AS TEXT), '—') || + ' maxms=' || COALESCE(CAST($GUEST_MAX AS TEXT), '—') || + ' jank=' || COALESCE(CAST($GUEST_JANK AS TEXT), '—') || + ' severe=' || COALESCE(CAST($GUEST_SEVERE AS TEXT), '—') || + ' missed-vsync=' || COALESCE(CAST($GUEST_MISSED AS TEXT), '—') + FROM game_frame_windows + WHERE status = 'AVAILABLE' + ORDER BY \"$GUEST_ORDER\" ASC LIMIT 1;" || true + else + print " telemetry rows exist, but the expected frame-window fields are unavailable" + fi +else + print " UNAVAILABLE: no game_frame_windows table; SRC/OUT must not be treated as TFT FPS." +fi + +print_section "Transport freshness and Mac presentation" +if table_exists stream_freshness_windows; then + readonly FREEZE_COL="$(column_or_null stream_freshness_windows longest_identical_run_ms longest_static_run_ms freeze_ms)" + readonly IDENTICAL_COL="$(column_or_null stream_freshness_windows identical_frames repeated_frames)" + print " worst stream freeze_ms=$(sql "SELECT COALESCE(max($FREEZE_COL), '—') FROM stream_freshness_windows;") identical_frames=$(sql "SELECT COALESCE(sum($IDENTICAL_COL), '—') FROM stream_freshness_windows;")" +else + print " stream freshness: UNAVAILABLE" +fi +if table_exists host_presentation_windows; then + readonly HOST_GPU_P95="$(column_or_null host_presentation_windows p95_gpu_time_ms gpu_p95_ms p95_gpu_ms)" + readonly HOST_ERRORS="$(column_or_null host_presentation_windows command_errors command_error_count errors)" + readonly HOST_MISSES="$(column_or_null host_presentation_windows drawable_miss_count drawable_misses)" + print " host Metal: gpu_p95ms=$(sql "SELECT COALESCE(max($HOST_GPU_P95), '—') FROM host_presentation_windows;") errors=$(sql "SELECT COALESCE(sum($HOST_ERRORS), '—') FROM host_presentation_windows;") drawable_misses=$(sql "SELECT COALESCE(sum($HOST_MISSES), '—') FROM host_presentation_windows;")" +else + print " host Metal timing: UNAVAILABLE" +fi + +print_section "Resources and graphics-pipeline signals" +if table_exists resource_samples; then + readonly CPU_COL="$(column_or_null resource_samples emulator_cpu_percent cpu_percent)" + readonly RSS_COL="$(column_or_null resource_samples emulator_rss_kib rss_kib)" + print " emulator CPU peak=$(sql "SELECT COALESCE(max($CPU_COL), '—') FROM resource_samples;")% RSS peak KiB=$(sql "SELECT COALESCE(max($RSS_COL), '—') FROM resource_samples;")" +else + print " resources: UNAVAILABLE" +fi +if table_exists guest_memory_samples; then + readonly AVAILABLE_MEM_COL="$(column_or_null guest_memory_samples available_kib mem_available_kib)" + readonly SWAP_FREE_COL="$(column_or_null guest_memory_samples swap_free_kib)" + print " guest memory: minimum_available_kib=$(sql "SELECT COALESCE(min($AVAILABLE_MEM_COL), '—') FROM guest_memory_samples;") minimum_swap_free_kib=$(sql "SELECT COALESCE(min($SWAP_FREE_COL), '—') FROM guest_memory_samples;")" +else + print " guest memory: UNAVAILABLE" +fi +if table_exists pipeline_log_aggregates; then + readonly GFX_COL="$(column_or_null pipeline_log_aggregates gfxstream_warning_count gfxstream_warnings)" + readonly ASG_COL="$(column_or_null pipeline_log_aggregates asg_stall_count asg_stalls)" + readonly VK_COL="$(column_or_null pipeline_log_aggregates vulkan_error_count vulkan_errors)" + readonly MVK_COL="$(column_or_null pipeline_log_aggregates moltenvk_warning_count moltenvk_warnings)" + readonly SHADER_COL="$(column_or_null pipeline_log_aggregates shader_error_count shader_errors)" + readonly FENCE_COL="$(column_or_null pipeline_log_aggregates fence_timeout_count fence_timeouts)" + print " pipeline aggregate: gfxstream_warn=$(sql "SELECT COALESCE(sum($GFX_COL), '—') FROM pipeline_log_aggregates;") asg_stall=$(sql "SELECT COALESCE(sum($ASG_COL), '—') FROM pipeline_log_aggregates;") vulkan_error=$(sql "SELECT COALESCE(sum($VK_COL), '—') FROM pipeline_log_aggregates;") mvk_warn=$(sql "SELECT COALESCE(sum($MVK_COL), '—') FROM pipeline_log_aggregates;") shader_error=$(sql "SELECT COALESCE(sum($SHADER_COL), '—') FROM pipeline_log_aggregates;") fence_timeout=$(sql "SELECT COALESCE(sum($FENCE_COL), '—') FROM pipeline_log_aggregates;")" +else + print " pipeline aggregates: UNAVAILABLE (raw logs are intentionally not printed)" +fi +if table_exists graphics_pipeline_snapshots; then + sql "SELECT ' latest stack evidence: TFT-surface=' || tft_surface_state || + ' ANGLE=' || angle_state || ' gfxstream=' || gfxstream_state || + ' MoltenVK=' || moltenvk_state || ' host-device=' || COALESCE(host_vulkan_device, '—') + FROM graphics_pipeline_snapshots ORDER BY monotonic_ns DESC LIMIT 1;" || true +else + print " graphics pipeline identity: UNAVAILABLE" +fi + +print_section "Markers, incidents, and diagnostic artifacts" +if table_exists events; then + readonly EVENT_KIND="$(first_column events event_type kind name 2>/dev/null || true)" + if [[ -n "$EVENT_KIND" ]]; then + print " events_total=$(sql 'SELECT count(*) FROM events;') incident_like=$(sql "SELECT count(*) FROM events WHERE upper(\"$EVENT_KIND\") LIKE '%INCIDENT%' OR upper(\"$EVENT_KIND\") LIKE '%STUTTER%' OR upper(\"$EVENT_KIND\") LIKE '%DEGRAD%';")" + else + print " events_total=$(sql 'SELECT count(*) FROM events;')" + fi +else + print " events: UNAVAILABLE" +fi +if table_exists diagnostic_artifacts; then + print " diagnostic_artifacts=$(sql 'SELECT count(*) FROM diagnostic_artifacts;') (artifact paths and trace contents withheld)" +else + print " diagnostic artifacts: UNAVAILABLE" +fi + +print_section "Explicit telemetry gaps" +typeset -a gaps +gaps=() +for table in game_frame_windows game_frame_intervals stream_freshness_windows host_presentation_windows pipeline_log_aggregates graphics_pipeline_snapshots diagnostic_artifacts; do + table_exists "$table" || gaps+=("$table") +done +if (( ${#gaps} == 0 )); then + print " No required telemetry table is missing. Empty rows still mean no sampled evidence for that layer." +else + print " Missing tables: ${(j:, :)gaps}" +fi +if table_exists game_frame_windows && [[ "$(sql 'SELECT count(*) FROM game_frame_windows;')" == "0" ]]; then + print " Guest frame table has no rows: no evidence yet for real TFT combat FPS." +fi +print " This report deliberately excludes raw logcat/emulator text, credentials, and tokens." diff --git a/scripts/test-mactician.command b/scripts/test-mactician.command deleted file mode 100755 index 94487a2..0000000 --- a/scripts/test-mactician.command +++ /dev/null @@ -1,595 +0,0 @@ -#!/bin/zsh -set -euo pipefail -unsetopt BG_NICE - -readonly PROJECT_DIR="${0:A:h:h}" -readonly LAUNCHER_DIR="$PROJECT_DIR/launcher" -readonly SPARKLE_ROOT="$("$PROJECT_DIR/scripts/prepare-sparkle.command")" -readonly TEST_BINARY="$(mktemp -t mactician-tests)" -readonly LIFECYCLE_ROOT="$(mktemp -d -t mactician-lifecycle)" -readonly HOST_ARCH="$(uname -m)" -case "$HOST_ARCH" in - arm64|x86_64) ;; - *) - print -u2 "Unsupported unit-test host architecture: $HOST_ARCH" - exit 2 - ;; -esac -readonly TEST_TARGET="$HOST_ARCH-apple-macosx12.0" -cleanup() { - local exit_code=$? - rm -f "$TEST_BINARY" - rm -rf "$LIFECYCLE_ROOT" - return "$exit_code" -} -trap cleanup EXIT - -jq -e '.schemaVersion == 1 and (.components | length) == 3 and (.game.apks | length) == 4' \ - "$LAUNCHER_DIR/Resources/release-manifest.json" >/dev/null -plutil -lint "$LAUNCHER_DIR/Info.plist" >/dev/null -plutil -lint "$LAUNCHER_DIR/Resources/EmulatorHost-Info.plist" >/dev/null -plutil -lint "$LAUNCHER_DIR/Resources/QEMU-Hypervisor.entitlements" >/dev/null -plutil -lint "$LAUNCHER_DIR/Resources/en.lproj/Localizable.strings" >/dev/null -plutil -lint "$LAUNCHER_DIR/Resources/ru.lproj/Localizable.strings" >/dev/null -typeset -a launcher_localizations -launcher_localizations=("$LAUNCHER_DIR"/Resources/*.lproj(N:t)) -if (( ${#launcher_localizations} != 2 )) \ - || [[ "$launcher_localizations[1]" != "en.lproj" ]] \ - || [[ "$launcher_localizations[2]" != "ru.lproj" ]]; then - print -u2 "Mactician must ship the English and Russian localization resources." - exit 1 -fi -typeset syntax_script -for syntax_script in \ - "$LAUNCHER_DIR/Resources/launcher-runtime.command" \ - "$LAUNCHER_DIR/Resources/emulator-host.command" \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - "$PROJECT_DIR/run-tft-angle-opengl.command" \ - "$PROJECT_DIR/scripts/run-asg-experiment.command" \ - "$PROJECT_DIR/scripts/run-autonomous-trial-benchmark.command" \ - "$PROJECT_DIR/scripts/run-performance-campaign.command" \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - "$PROJECT_DIR/scripts/run-host-angle-capability-probe.command" \ - "$PROJECT_DIR/scripts/summarize-android-ui-transport.command" \ - "$PROJECT_DIR/scripts/audit-native-gles-coverage.command" \ - "$PROJECT_DIR/scripts/build-android-egl-capability-probe.command" \ - "$PROJECT_DIR/scripts/watch-root-pso.command" \ - "$PROJECT_DIR/scripts/android-environment.sh" \ - "$PROJECT_DIR/scripts/prepare-sparkle.command" \ - "$PROJECT_DIR/scripts/publish-mactician-update.command" \ - "$PROJECT_DIR/scripts/publish-game-update.command" \ - "$PROJECT_DIR/scripts/build-mactician.command" \ - "$PROJECT_DIR/scripts/integration-test-mactician.command"; do - zsh -o NO_BG_NICE -n "$syntax_script" -done - -if rg -n '[А-Яа-яЁё]' \ - "$LAUNCHER_DIR/Resources/launcher-runtime.command" \ - "$LAUNCHER_DIR/Resources/emulator-host.command" \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - "$PROJECT_DIR/run-tft-angle-opengl.command" \ - "$PROJECT_DIR/scripts/run-asg-experiment.command" \ - "$PROJECT_DIR/scripts/watch-root-pso.command"; then - print -u2 "Bundled launcher logs must be in English." - exit 1 -fi -xcrun clang \ - -target arm64-apple-macosx12.0 \ - -fsyntax-only \ - "$LAUNCHER_DIR/EmulatorHost/main.c" - -# The current product is the native TFTMAC shell around the stock Play runtime. -# Build it during production validation so Swift/AppKit/window integration cannot drift. -/bin/zsh "$PROJECT_DIR/scripts/build-tftmac-app.command" >/dev/null - -"$PROJECT_DIR/scripts/build-tft-screen-classifier.command" >/dev/null -"$PROJECT_DIR/runtime/tft-screen-classifier" --self-test >/dev/null - -if ! grep -Fq -- '--options runtime' "$PROJECT_DIR/scripts/build-mactician.command" \ - || ! grep -Fq 'notarytool submit' "$PROJECT_DIR/scripts/build-mactician.command" \ - || ! grep -Fq 'stapler staple' "$PROJECT_DIR/scripts/build-mactician.command" \ - || ! grep -Fq 'Sparkle.framework' "$PROJECT_DIR/scripts/build-mactician.command"; then - print -u2 "Public release signing and notarization workflow is incomplete." - exit 1 -fi - -if ! grep -Fq 'MVK_CONFIG_SUPPORT_LARGE_QUERY_POOLS' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'MVK_CONFIG_USE_MTLHEAP' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'MVK_CONFIG_ACTIVITY_PERFORMANCE_LOGGING_STYLE' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'MVK_CONFIG_LOG_LEVEL' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'MVK_CONFIG_PERFORMANCE_LOGGING_FRAME_COUNT' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'MVK_CONFIG_PERFORMANCE_TRACKING' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! rg -Uq 'for environment_name in \\\n+[[:space:]]+ANGLE_FEATURE_OVERRIDES_ENABLED \\\n+[[:space:]]+ANGLE_FEATURE_OVERRIDES_DISABLED \\\n+[[:space:]]+MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! rg -Uq 'MVK_CONFIG_FAST_MATH_ENABLED \\\n+[[:space:]]+MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS \\\n+[[:space:]]+MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE; do' \ - "$PROJECT_DIR/run-tft-root-affinity.command"; then - print -u2 "The Game Mode app wrapper must preserve experimental ANGLE and MoltenVK settings." - exit 1 -fi - -if ! jq -e ' - def safe_relative_path: - type == "string" - and test("^[A-Za-z0-9._/-]+$") - and (startswith("/") | not) - and (contains("..") | not); - .schemaVersion == 1 - and (.candidates | type == "array" and length > 0) - and ([.candidates[].id] | length == (unique | length)) - and ([.candidates[].variant] | length == (unique | length)) - and all(.candidates[]; - (.id | type == "string" and test("^[a-z0-9][a-z0-9-]*$")) - and (.launcher | safe_relative_path) - and ((.profile // "placeholder") | safe_relative_path) - and (.variant | type == "string" and test("^[a-z0-9][a-z0-9_-]*$")) - and (.display | type == "string" - and test("^(2560x1440|2880x1620|3200x1800|3840x2160)$")) - and (.density | type == "number" and floor == . and . >= 120 and . <= 640) - and (.stages | type == "array" and length > 0 - and all(.[]; type == "string" and test("^[1-9]-(1[0-9]|[1-9])$"))) - and ((.profileStage // "1-1") - | type == "string" and test("^[1-9]-(1[0-9]|[1-9])$")) - and ((.minimumTrialSeconds // 0) - | type == "number" and floor == . and . >= 0 and . <= 3600) - and ((.env // {}) | type == "object" - and all(to_entries[]; - (.key | test("^[A-Z][A-Z0-9_]*$")) - and (.value | type == "string" - and test("^[-A-Za-z0-9_./:]+$"))))) - ' "$PROJECT_DIR/scripts/performance-candidates.json" >/dev/null; then - print -u2 "The performance candidate manifest contract is invalid." - exit 1 -fi -typeset candidate_launcher candidate_profile -while IFS=$'\t' read -r candidate_launcher candidate_profile; do - if [[ ! -x "$PROJECT_DIR/$candidate_launcher" \ - || ( -n "$candidate_profile" && ! -f "$PROJECT_DIR/$candidate_profile" ) ]]; then - print -u2 "A performance candidate references a missing launcher or profile: $candidate_launcher ${candidate_profile:-}" - exit 1 - fi -done < <(jq -r '.candidates[] | [.launcher, (.profile // "")] | @tsv' \ - "$PROJECT_DIR/scripts/performance-candidates.json") - -if ! grep -Fq 'osft-no-fence-contexts' "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq -- '-VirtioGpuFenceContexts' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! jq -e ' - [.candidates[].id] as $ids - | ($ids | index("performance-max-no-fence-contexts-screen")) != null - and ($ids | index("performance-max-no-virtual-queue-screen")) == null - and ($ids | index("performance-max-no-queue-submit-with-commands-screen")) == null - and ($ids | index("performance-max-argument-buffers-off-screen")) == null - and ($ids | index("performance-max-single-queue-semaphores-screen")) == null - ' "$PROJECT_DIR/scripts/performance-candidates.json" >/dev/null; then - print -u2 "The rejected/no-op performance candidate isolation is incomplete." - exit 1 -fi - -if ! grep -Fq 'readonly HOST_GPU="${TFT_HOST_GPU:-host}"' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq -- '-gpu "$HOST_GPU"' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'TFT_HOST_GPU must be either host or swangle.' \ - "$PROJECT_DIR/run-tft-root-affinity.command" \ - || ! grep -Fq 'host ANGLE -> Vulkan -> SwiftShader CPU' \ - "$PROJECT_DIR/run-tft-root-affinity.command"; then - print -u2 "The bounded host swangle GLES control is incomplete." - exit 1 -fi - -if ! jq -e ' - .schemaVersion == 1 - and (.runs | length) == 10 - and (.aggregates | length) == 5 - and ([.runs[].label] | unique | length) == 10 - and ([.runs[].summarySha256] | unique | length) == 10 - and all(.runs[]; .summarySha256 | test("^[0-9a-f]{64}$")) - and ([.runs[].graphicsProfile] | unique | length) == 5 - and (.invariants.display == "2560x1440") - and (.invariants.displayDensityDpi == 320) - and (.invariants.transport == "virtio-gpu-asg") - and (.invariants.hwuiRenderer == "skiavk") - and (.invariants.roundsPerRun == 12) - and (.invariants.warmupRoundsDiscarded == 3) - and ([.aggregates[] | select(.graphicsProfile == "osft")][0].warmRounds == 36) - and ([.aggregates[] | select(.decision == "keep")] | length) == 1 - and (. as $document - | all($document.aggregates[]; - . as $aggregate - | [$document.runs[] - | select(.graphicsProfile == $aggregate.graphicsProfile)] as $runs - | ($runs | length) == $aggregate.validRuns - and (($runs | length) * 9) == $aggregate.warmRounds - and (((($runs | map(.warmMeanElapsedMs) | add) / ($runs | length)) - - $aggregate.warmMeanElapsedMs) | fabs) < 0.000001 - and ($runs | map(.warmMaxP95Ms) | max) == $aggregate.warmMaxP95Ms - and ($runs | map(.warmMaxP99Ms) | max) == $aggregate.warmMaxP99Ms - and ($runs | map(.warmTotalJankyFrames) | add) - == $aggregate.warmTotalJankyFrames)) - ' "$PROJECT_DIR/artifacts/android-ui-transport-attested-20260811.json" \ - >/dev/null; then - print -u2 "The attested Android UI transport result artifact is incomplete." - exit 1 -fi -typeset source_summary source_label source_utc source_expected_sha source_actual_sha -for source_summary in \ - "$PROJECT_DIR"/runtime/measurements/android-ui-transport/*/summary.json(N); do - source_label="$(jq -r '.label // ""' "$source_summary")" - source_utc="$(jq -r '.utc // ""' "$source_summary")" - source_expected_sha="$( - jq -r \ - --arg target_label "$source_label" \ - --arg target_utc "$source_utc" \ - '.runs[] - | select(.label == $target_label and .utc == $target_utc) - | .summarySha256' \ - "$PROJECT_DIR/artifacts/android-ui-transport-attested-20260811.json" - )" - [[ -n "$source_expected_sha" ]] || continue - source_actual_sha="$(shasum -a 256 "$source_summary" | awk '{ print $1 }')" - if [[ "$source_actual_sha" != "$source_expected_sha" ]]; then - print -u2 "An attested Android UI source summary no longer matches its recorded SHA: $source_label" - exit 1 - fi -done - -xcrun clang++ \ - -std=c++17 \ - -Wall \ - -Wextra \ - -Werror \ - -fsyntax-only \ - "$PROJECT_DIR/artifacts/angle-egl-probe.cpp" - -if ! grep -Fq 'functional_es32_geometry_pipeline' \ - "$PROJECT_DIR/artifacts/angle-egl-probe.cpp" \ - || ! grep -Fq 'functional_es32_tessellation_pipeline' \ - "$PROJECT_DIR/artifacts/angle-egl-probe.cpp" \ - || ! grep -Fq 'exposeNonConformantExtensionsAndVersions' \ - "$PROJECT_DIR/scripts/run-host-angle-capability-probe.command" \ - || ! grep -Fq 'ANDROID_EMU_gles_max_version_3_2' \ - "$PROJECT_DIR/artifacts/gfxstream-gles32-host-capability-prototype.patch" \ - || ! grep -Fq 'kGles32Aliases' \ - "$PROJECT_DIR/artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch" \ - || ! grep -Fq '{"glTexBuffer", (void*)_egl_glTexBufferEXT}' \ - "$PROJECT_DIR/artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch" \ - || ! grep -Fq 'dynamic_alias_resolution_required' \ - "$PROJECT_DIR/scripts/audit-native-gles-coverage.command" \ - || ! grep -Fq 'runtime.LockOSThread()' \ - "$PROJECT_DIR/artifacts/android-egl-capability-probe/main.go" \ - || ! grep -Fq 'runtime.KeepAlive(highAttrs)' \ - "$PROJECT_DIR/artifacts/android-egl-capability-probe/main.go" \ - || ! grep -Fq 'hwui_renderer: $hwui_renderer' \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - || ! grep -Fq 'display_density: $display_density' \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - || ! grep -Fq 'ro.boot.mactician.graphics_profile' \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - || ! grep -Fq "grep -Fqx 'Status: ok'" \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - || ! grep -Fq 'write_rejected_summary "non_rendering_round_$round"' \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - || ! grep -Fq 'existing evidence will not be overwritten' \ - "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - || ! grep -Fq 'androidboot.mactician.graphics_profile=$GRAPHICS_PROFILE' \ - "$PROJECT_DIR/run-tft-root-affinity.command"; then - print -u2 "The native GLES capability experiment artifacts are incomplete." - exit 1 -fi -if [[ "$(grep -c '^proc ' \ - "$PROJECT_DIR/artifacts/android-egl-capability-probe/native-clean-boot-output.txt")" != "59" ]] \ - || [[ "$(grep -c '^proc .* false$' \ - "$PROJECT_DIR/artifacts/android-egl-capability-probe/native-clean-boot-output.txt")" != "3" ]]; then - print -u2 "The recorded native guest proc-address matrix is incomplete." - exit 1 -fi - -# The transport summarizer must retain short valid runs, reject empty and -# explicitly failed runs without dividing by zero, and keep display/renderer -# configurations separate. -if "$PROJECT_DIR/scripts/run-android-ui-transport-probe.command" \ - attestation-required 1 \ - >"$LIFECYCLE_ROOT/missing-profile-attestation.out" 2>&1 \ - || ! grep -Fq 'TFT_UI_TRANSPORT_EXPECTED_GRAPHICS_PROFILE is required' \ - "$LIFECYCLE_ROOT/missing-profile-attestation.out"; then - print -u2 "The Android UI transport probe accepted an unattested graphics profile." - exit 1 -fi -readonly TRANSPORT_FIXTURE_ROOT="$LIFECYCLE_ROOT/android-ui-transport" -mkdir -p "$TRANSPORT_FIXTURE_ROOT/rejected" \ - "$TRANSPORT_FIXTURE_ROOT/empty" \ - "$TRANSPORT_FIXTURE_ROOT/short-osft" \ - "$TRANSPORT_FIXTURE_ROOT/short-stable" \ - "$TRANSPORT_FIXTURE_ROOT/short-skiagl" \ - "$TRANSPORT_FIXTURE_ROOT/short-density" -jq -n '{schema_version: 5, label: "edge-rejected-A", graphics_profile: "osft", - display: "2560x1440", display_density: 320, - transport: "virtio-gpu-asg", hwui_renderer: "skiavk", - rejected_reason: "fixture_failure", - swipe_pairs: 15, - minimum_frames_per_round: 120, - rounds: [range(1; 5) | {round: ., elapsed_ns: 6000000000, - total_frames: 120, janky_frames: 0, p95_ms: 0, p99_ms: 0}]}' \ - > "$TRANSPORT_FIXTURE_ROOT/rejected/summary.json" -jq -n '{schema_version: 5, label: "edge-rejected-Z", graphics_profile: "osft", - display: "2560x1440", display_density: 320, - transport: "virtio-gpu-asg", hwui_renderer: "skiavk", - swipe_pairs: 15, minimum_frames_per_round: 120, rounds: []}' \ - > "$TRANSPORT_FIXTURE_ROOT/empty/summary.json" -jq -n '{schema_version: 5, label: "edge-short-B", graphics_profile: "osft", - display: "2560x1440", display_density: 320, - transport: "virtio-gpu-asg", hwui_renderer: "skiavk", - swipe_pairs: 15, - minimum_frames_per_round: 120, - rounds: [ - {round: 1, elapsed_ns: 6000000000, total_frames: 120, - janky_frames: 1, p95_ms: 10, p99_ms: 12}, - {round: 2, elapsed_ns: 6200000000, total_frames: 121, - janky_frames: 2, p95_ms: 11, p99_ms: 13}, - {round: 3, elapsed_ns: 6400000000, total_frames: 122, - janky_frames: 3, p95_ms: 12, p99_ms: 14}]}' \ - > "$TRANSPORT_FIXTURE_ROOT/short-osft/summary.json" -jq -n '{schema_version: 5, label: "edge-short-C", graphics_profile: "stable", - display: "2560x1440", display_density: 320, - transport: "virtio-gpu-asg", hwui_renderer: "skiavk", - swipe_pairs: 15, minimum_frames_per_round: 120, - rounds: [ - {round: 1, elapsed_ns: 7000000000, total_frames: 120, - janky_frames: 0, p95_ms: 9, p99_ms: 11}]}' \ - > "$TRANSPORT_FIXTURE_ROOT/short-stable/summary.json" -jq -n '{schema_version: 5, label: "edge-short-D", graphics_profile: "osft", - display: "2560x1440", display_density: 320, - transport: "virtio-gpu-asg", hwui_renderer: "skiagl", - swipe_pairs: 15, minimum_frames_per_round: 120, - rounds: [ - {round: 1, elapsed_ns: 7100000000, total_frames: 120, - janky_frames: 4, p95_ms: 13, p99_ms: 15}]}' \ - > "$TRANSPORT_FIXTURE_ROOT/short-skiagl/summary.json" -jq -n '{schema_version: 5, label: "edge-short-E", graphics_profile: "osft", - display: "2560x1440", display_density: 416, - transport: "virtio-gpu-asg", hwui_renderer: "skiavk", - swipe_pairs: 15, minimum_frames_per_round: 120, - rounds: [ - {round: 1, elapsed_ns: 7200000000, total_frames: 120, - janky_frames: 5, p95_ms: 14, p99_ms: 16}]}' \ - > "$TRANSPORT_FIXTURE_ROOT/short-density/summary.json" -TFT_UI_TRANSPORT_ROOT="$TRANSPORT_FIXTURE_ROOT" \ - "$PROJECT_DIR/scripts/summarize-android-ui-transport.command" \ - '^edge-(rejected|short)-[A-Z]$' \ - > "$TRANSPORT_FIXTURE_ROOT/result.json" -if ! jq -e ' - length == 5 - and .[0].group == "edge-rejected" - and .[0].graphics_profile == "osft" - and .[0].display_density == 320 - and .[0].hwui_renderer == "skiavk" - and .[0].valid_runs == 0 - and .[0].rejected_runs == 2 - and .[0].warm_rounds == 0 - and .[0].warm_mean_elapsed_ms == null - and .[0].warm_total_janky_frames == null - and .[1].group == "edge-short" - and .[1].graphics_profile == "osft" - and .[1].display == "2560x1440" - and .[1].display_density == 320 - and .[1].transport == "virtio-gpu-asg" - and .[1].hwui_renderer == "skiagl" - and .[1].valid_runs == 1 - and .[1].rejected_runs == 0 - and .[1].warm_rounds == 1 - and .[1].warm_mean_elapsed_ms == 7100 - and .[1].warm_max_p95_ms == 13 - and .[1].warm_max_p99_ms == 15 - and .[1].warm_total_janky_frames == 4 - and .[2].group == "edge-short" - and .[2].graphics_profile == "osft" - and .[2].display_density == 320 - and .[2].hwui_renderer == "skiavk" - and .[2].valid_runs == 1 - and .[2].rejected_runs == 0 - and .[2].warm_rounds == 3 - and .[2].warm_mean_elapsed_ms == 6200 - and .[2].warm_median_elapsed_ms == 6200 - and .[2].warm_max_p95_ms == 12 - and .[2].warm_max_p99_ms == 14 - and .[2].warm_total_janky_frames == 6 - and .[3].group == "edge-short" - and .[3].graphics_profile == "osft" - and .[3].display_density == 416 - and .[3].hwui_renderer == "skiavk" - and .[3].valid_runs == 1 - and .[3].warm_rounds == 1 - and .[3].warm_mean_elapsed_ms == 7200 - and .[3].warm_max_p95_ms == 14 - and .[3].warm_max_p99_ms == 16 - and .[4].group == "edge-short" - and .[4].graphics_profile == "stable" - and .[4].display_density == 320 - and .[4].hwui_renderer == "skiavk" - and .[4].valid_runs == 1 - and .[4].warm_rounds == 1 - and .[4].warm_mean_elapsed_ms == 7000 - and .[4].warm_max_p95_ms == 9 - and .[4].warm_max_p99_ms == 11 -' "$TRANSPORT_FIXTURE_ROOT/result.json" >/dev/null; then - print -u2 "Android UI transport summary edge cases regressed." - cat "$TRANSPORT_FIXTURE_ROOT/result.json" >&2 - exit 1 -fi - -if ! grep -Fq -- '--allow-adhoc' "$PROJECT_DIR/scripts/publish-mactician-update.command" \ - || ! grep -Fq 'Signature=adhoc' "$PROJECT_DIR/scripts/publish-mactician-update.command" \ - || ! grep -Fq 'hdiutil verify' "$PROJECT_DIR/scripts/publish-mactician-update.command"; then - print -u2 "Ad-hoc publication safeguards are incomplete." - exit 1 -fi - -if ! grep -Fq 'field__input--animate' \ - "$LAUNCHER_DIR/Sources/RiotLoginAnimationRepairService.swift" \ - || ! grep -Fq 'animation: none !important' \ - "$LAUNCHER_DIR/Sources/RiotLoginAnimationRepairService.swift" \ - || ! grep -Fq 'remainingAnimations == 0' \ - "$LAUNCHER_DIR/Sources/RiotLoginAnimationRepairService.swift" \ - || rg -n 'shell input (tap|keyevent)|KEYCODE_TAB|becomeFirstResponder' \ - "$LAUNCHER_DIR/Sources/RiotLoginAnimationRepairService.swift"; then - print -u2 "Scoped Riot login animation repair is incomplete or uses synthetic focus." - exit 1 -fi - -# A user-requested STOP sends TERM while the runtime child is still alive. -# Reproduce that lifecycle with caffeinate as a harmless long-running child and -# verify that it produces a normal stopped event, not a Repair error. -mkdir -p "$LIFECYCLE_ROOT/runtime/scripts" -ln -s /usr/bin/caffeinate "$LIFECYCLE_ROOT/runtime/scripts/run-asg-experiment.command" -env \ - TFT_RUNTIME_PROJECT="$LIFECYCLE_ROOT/runtime" \ - TFT_LAUNCH_LOG="$LIFECYCLE_ROOT/runtime.log" \ - TFT_ADB=/usr/bin/false \ - TFT_AVD_HOME="$LIFECYCLE_ROOT/avd" \ - TFT_AVD_NAME=TftPBE \ - TFT_SERIAL=emulator-5582 \ - TFT_DISPLAY_SIZE=1920x1080 \ - TFT_DISPLAY_DENSITY=320 \ - TFT_GAME_LANGUAGE=en-US \ - TFT_CPU_CORES=6 \ - TFT_MEMORY_MB=6144 \ - TFT_UI_SCALE=1.0 \ - "$LAUNCHER_DIR/Resources/launcher-runtime.command" \ - >"$LIFECYCLE_ROOT/events.jsonl" & -readonly LIFECYCLE_PID=$! -typeset lifecycle_ready=0 -for lifecycle_attempt in {1..100}; do - if grep -q '"event":"booting"' "$LIFECYCLE_ROOT/events.jsonl" 2>/dev/null; then - lifecycle_ready=1 - break - fi - if ! kill -0 "$LIFECYCLE_PID" >/dev/null 2>&1; then - break - fi - sleep 0.05 -done -if (( lifecycle_ready == 0 )); then - print -u2 "Launcher runtime did not reach the booting state." - cat "$LIFECYCLE_ROOT/events.jsonl" >&2 2>/dev/null || true - exit 1 -fi -kill -TERM "$LIFECYCLE_PID" -if wait "$LIFECYCLE_PID"; then - readonly LIFECYCLE_STATUS=0 -else - readonly LIFECYCLE_STATUS=$? -fi -if (( LIFECYCLE_STATUS != 0 )) \ - || ! grep -q '"event":"stopped"' "$LIFECYCLE_ROOT/events.jsonl" \ - || grep -q '"event":"error"' "$LIFECYCLE_ROOT/events.jsonl"; then - print -u2 "Launcher runtime STOP was not classified as a normal shutdown." - cat "$LIFECYCLE_ROOT/events.jsonl" >&2 - exit 1 -fi - -# After TFT has started, three consecutive missing package-PID checks represent -# a real game close. Verify that the runtime emits game_stopped before cleanup. -readonly GAME_EXIT_ADB="$LIFECYCLE_ROOT/fake-adb.command" -readonly GAME_EXIT_STATE="$LIFECYCLE_ROOT/fake-adb-state" -cat >"$GAME_EXIT_ADB" <<'FAKE_ADB_EOF' -#!/bin/zsh -set -eu -if [[ "$*" == *" get-state" ]]; then - exit 0 -fi -if [[ "$*" == *" getprop sys.boot_completed" ]]; then - print 1 - exit 0 -fi -if [[ "$*" == *" cmd locale set-app-locales"* ]]; then - exit 0 -fi -if [[ "$*" == *" pidof com.riotgames.league.teamfighttactics.pbe" ]]; then - typeset -i count=0 - [[ -f "$TFT_FAKE_ADB_STATE" ]] && count="$(<"$TFT_FAKE_ADB_STATE")" - (( count += 1 )) - print "$count" >"$TFT_FAKE_ADB_STATE" - (( count <= 2 )) && print 4242 - exit 0 -fi -exit 0 -FAKE_ADB_EOF -chmod 755 "$GAME_EXIT_ADB" -env \ - TFT_RUNTIME_PROJECT="$LIFECYCLE_ROOT/runtime" \ - TFT_LAUNCH_LOG="$LIFECYCLE_ROOT/game-exit-runtime.log" \ - TFT_ADB="$GAME_EXIT_ADB" \ - TFT_FAKE_ADB_STATE="$GAME_EXIT_STATE" \ - TFT_AVD_HOME="$LIFECYCLE_ROOT/avd" \ - TFT_AVD_NAME=TftPBE \ - TFT_SERIAL=emulator-5582 \ - TFT_DISPLAY_SIZE=1920x1080 \ - TFT_DISPLAY_DENSITY=320 \ - TFT_GAME_LANGUAGE=en-US \ - TFT_CPU_CORES=6 \ - TFT_MEMORY_MB=6144 \ - TFT_UI_SCALE=1.0 \ - "$LAUNCHER_DIR/Resources/launcher-runtime.command" \ - >"$LIFECYCLE_ROOT/game-exit-events.jsonl" & -readonly GAME_EXIT_PID=$! -typeset game_exit_detected=0 -for game_exit_attempt in {1..200}; do - if grep -q '"event":"game_stopped"' \ - "$LIFECYCLE_ROOT/game-exit-events.jsonl" 2>/dev/null; then - game_exit_detected=1 - break - fi - if ! kill -0 "$GAME_EXIT_PID" >/dev/null 2>&1; then - break - fi - sleep 0.05 -done -kill -TERM "$GAME_EXIT_PID" >/dev/null 2>&1 || true -wait "$GAME_EXIT_PID" || true -if (( game_exit_detected == 0 )) \ - || [[ "$(grep -c '"event":"game_stopped"' "$LIFECYCLE_ROOT/game-exit-events.jsonl")" != 1 ]] \ - || grep -q '"event":"error"' "$LIFECYCLE_ROOT/game-exit-events.jsonl"; then - print -u2 "Launcher runtime did not classify a closed TFT process." - cat "$LIFECYCLE_ROOT/game-exit-events.jsonl" >&2 - exit 1 -fi - -mkdir -p "$LAUNCHER_DIR/.build/module-cache" -xcrun swiftc \ - -target "$TEST_TARGET" \ - -module-cache-path "$LAUNCHER_DIR/.build/module-cache" \ - "$LAUNCHER_DIR/Sources/CoreModels.swift" \ - "$LAUNCHER_DIR/Sources/HostedGameUpdate.swift" \ - "$LAUNCHER_DIR/Sources/LauncherPresentation.swift" \ - "$LAUNCHER_DIR/Sources/LauncherTelemetryService.swift" \ - "$LAUNCHER_DIR/Sources/LauncherPaths.swift" \ - "$LAUNCHER_DIR/Sources/SystemServices.swift" \ - "$LAUNCHER_DIR/Sources/EmulatorBrandingPatch.swift" \ - "$LAUNCHER_DIR/Sources/EmulatorAudioRecoveryService.swift" \ - "$LAUNCHER_DIR/Sources/FPSOverlayService.swift" \ - "$LAUNCHER_DIR/Sources/InputBridgeService.swift" \ - "$LAUNCHER_DIR/Sources/InstallerService.swift" \ - "$LAUNCHER_DIR/Tests/LauncherTests.swift" \ - -o "$TEST_BINARY" - -"$TEST_BINARY" \ - "$LAUNCHER_DIR/Resources/release-manifest.json" \ - "$LAUNCHER_DIR" - -typeset -a ALL_SOURCES -ALL_SOURCES=("$LAUNCHER_DIR"/Sources/*.swift) -xcrun swiftc \ - -typecheck \ - -parse-as-library \ - -target arm64-apple-macosx12.0 \ - -module-cache-path "$LAUNCHER_DIR/.build/module-cache" \ - -F "$SPARKLE_ROOT" \ - "${ALL_SOURCES[@]}" - -print "Mactician typecheck: OK" diff --git a/scripts/test-native-app.command b/scripts/test-native-app.command new file mode 100644 index 0000000..685b568 --- /dev/null +++ b/scripts/test-native-app.command @@ -0,0 +1,28 @@ +#!/bin/zsh +set -euo pipefail + +ROOT="${0:A:h:h}" +if [[ -z "${DEVELOPER_DIR:-}" ]]; then + if [[ -d /Applications/Xcode-26.6.0.app/Contents/Developer ]]; then + export DEVELOPER_DIR=/Applications/Xcode-26.6.0.app/Contents/Developer + else + export DEVELOPER_DIR="$(xcode-select -p)" + fi +fi +DERIVED="${ROOT}/.build/native-tests" + +/usr/bin/xcodebuild \ + -quiet \ + -project "${ROOT}/TFTMAC.xcodeproj" \ + -scheme TFTMAC \ + -configuration Debug \ + -destination 'platform=macOS,arch=arm64' \ + -derivedDataPath "${DERIVED}" \ + CODE_SIGNING_ALLOWED=NO \ + build-for-testing + +readonly TEST_BUNDLE="${DERIVED}/Build/Products/Debug/TFTMACTests.xctest" +readonly XCTEST="${DEVELOPER_DIR}/usr/bin/xctest" +[[ -x "${XCTEST}" ]] || { echo "XCTest runner is unavailable: ${XCTEST}" >&2; exit 1; } +[[ -x "${TEST_BUNDLE}/Contents/MacOS/TFTMACTests" ]] || { echo "Test bundle is incomplete: ${TEST_BUNDLE}" >&2; exit 1; } +"${XCTEST}" "${TEST_BUNDLE}" diff --git a/scripts/verify-branding.command b/scripts/verify-branding.command deleted file mode 100755 index 0ebe7bf..0000000 --- a/scripts/verify-branding.command +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -cd "$PROJECT_DIR" - -fail() { - print -u2 "Brand verification failed: $*" - exit 1 -} - -readonly launcher_word="Launcher" -readonly launcher_word_lower="launcher" -readonly tactician_tail="Tician" -readonly old_product_pattern="TFT-PBE-$launcher_word|TFTPBE $launcher_word|tft-pbe-$launcher_word_lower|tft_pbe_$launcher_word_lower|TFTPBE$launcher_word|TFT$launcher_word" -readonly old_titled_product="TFT PBE $launcher_word" -readonly wrong_casing="Mac$tactician_tail" -readonly forbidden_suffix="Mactician $launcher_word" - -typeset content_matches -content_matches="$(rg -n -i --hidden \ - --glob '!.git/**' \ - --glob '!dist/**' \ - --glob '!launcher/.build/**' \ - --glob '!build/**' \ - --glob '!DerivedData/**' \ - "$old_product_pattern|$forbidden_suffix" . || true)" -[[ -z "$content_matches" ]] || { - print -r -- "$content_matches" >&2 - fail "obsolete or forbidden product naming remains in repository content" -} - -content_matches="$(rg -n --hidden \ - --glob '!.git/**' \ - --glob '!dist/**' \ - --glob '!launcher/.build/**' \ - --glob '!build/**' \ - --glob '!DerivedData/**' \ - "$old_titled_product|$wrong_casing" . || true)" -[[ -z "$content_matches" ]] || { - print -r -- "$content_matches" >&2 - fail "incorrect Mactician casing remains in repository content" -} - -typeset filename_matches -filename_matches="$(find . \ - -path './.git' -prune -o \ - -path './dist' -prune -o \ - -path './launcher/.build' -prune -o \ - -path './build' -prune -o \ - -path './DerivedData' -prune -o \ - -print | LC_ALL=C grep -Ei "$old_product_pattern|$forbidden_suffix" || true)" -[[ -z "$filename_matches" ]] || { - print -r -- "$filename_matches" >&2 - fail "obsolete or forbidden product naming remains in repository paths" -} - -filename_matches="$(find . \ - -path './.git' -prune -o \ - -path './dist' -prune -o \ - -path './launcher/.build' -prune -o \ - -path './build' -prune -o \ - -path './DerivedData' -prune -o \ - -print | LC_ALL=C grep -E "$old_titled_product|$wrong_casing" || true)" -[[ -z "$filename_matches" ]] || { - print -r -- "$filename_matches" >&2 - fail "incorrect Mactician casing remains in repository paths" -} - -print "Brand verification: OK" diff --git a/scripts/verify-installed-runtime.command b/scripts/verify-installed-runtime.command new file mode 100755 index 0000000..1afb3d3 --- /dev/null +++ b/scripts/verify-installed-runtime.command @@ -0,0 +1,82 @@ +#!/bin/zsh +set -euo pipefail +unsetopt BG_NICE + +readonly ROOT="${0:A:h:h}" +cd "$ROOT" + +fail() { + print -u2 "TFTMAC installed-runtime verification failed: $*" + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "required command is unavailable: $1" +} + +# Local-host contract only. This script intentionally examines machine state +# that repository verification and GitHub CI must never require. +for tool in codesign jq plutil security shasum; do + require_command "$tool" +done + +readonly AUTHORITY="ssot/runtime-authority.json" +[[ -f "$AUTHORITY" ]] || fail "runtime authority is missing" +jq -e '.finalInstalledRelease and .currentHostAudit' "$AUTHORITY" >/dev/null \ + || fail "release or current-host audit authority is missing" + +readonly INSTALLED_APP="$(jq -r '.finalInstalledRelease.path' "$AUTHORITY")" +readonly RUNTIME_ROOT="$(jq -r '.runtimeRoot' "$AUTHORITY")" +readonly SDK_ROOT="$(jq -r '.sdkRoot' "$AUTHORITY")" +readonly INFO="${INSTALLED_APP}/Contents/Info.plist" +readonly EXECUTABLE="${INSTALLED_APP}/Contents/MacOS/TFTMAC" +readonly HOST_EXECUTABLE="${INSTALLED_APP}/Contents/Resources/TFTMAC Emulator Host.app/Contents/MacOS/TFTMACEmulatorHost" +readonly ICON_1024="${INSTALLED_APP}/Contents/Resources/TFTMAC-1024.png" +readonly ICON_ICNS="${INSTALLED_APP}/Contents/Resources/TFTMAC.icns" +readonly EMULATOR="${SDK_ROOT}/emulator/emulator" +readonly ADB="${SDK_ROOT}/platform-tools/adb" +readonly INSTALLED_PROTO="$(jq -r '.emulatorController.installedPath' "$AUTHORITY")" + +[[ -d "$INSTALLED_APP" ]] || fail "released native app is not installed: $INSTALLED_APP" +[[ -d "$RUNTIME_ROOT" && -d "$SDK_ROOT" ]] || fail "external Android runtime is unavailable: $RUNTIME_ROOT" +for artifact in "$INFO" "$EXECUTABLE" "$HOST_EXECUTABLE" "$ICON_1024" "$ICON_ICNS" "$EMULATOR" "$ADB" "$INSTALLED_PROTO"; do + [[ -s "$artifact" ]] || fail "required installed artifact is missing or empty: $artifact" +done + +[[ "$(plutil -extract CFBundleShortVersionString raw "$INFO")" == "$(jq -r '.finalInstalledRelease.version' "$AUTHORITY")" ]] \ + || fail "installed app version differs from release authority" +[[ "$(plutil -extract CFBundleVersion raw "$INFO")" == "$(jq -r '.finalInstalledRelease.build' "$AUTHORITY")" ]] \ + || fail "installed app build differs from release authority" +[[ "$(shasum -a 256 "$EXECUTABLE" | awk '{print $1}')" == "$(jq -r '.finalInstalledRelease.executableSHA256' "$AUTHORITY")" ]] \ + || fail "installed executable hash differs from historical release authority" +[[ "$(shasum -a 256 "$HOST_EXECUTABLE" | awk '{print $1}')" == "$(jq -r '.finalInstalledRelease.emulatorHostExecutableSHA256' "$AUTHORITY")" ]] \ + || fail "installed emulator-host hash differs from historical release authority" +[[ "$(shasum -a 256 "$ICON_1024" | awk '{print $1}')" == "$(jq -r '.finalInstalledRelease.officialIcon1024SHA256' "$AUTHORITY")" ]] \ + || fail "installed 1024px icon differs from release authority" +[[ "$(shasum -a 256 "$ICON_ICNS" | awk '{print $1}')" == "$(jq -r '.finalInstalledRelease.embeddedIconICNSSHA256' "$AUTHORITY")" ]] \ + || fail "installed ICNS differs from release authority" +[[ "$(shasum -a 256 "$INSTALLED_PROTO" | awk '{print $1}')" == "$(jq -r '.emulatorController.sha256' "$AUTHORITY")" ]] \ + || fail "installed EmulatorController protocol differs from runtime authority" +[[ "$(jq -r '.currentHostAudit.releaseIdentityHashesMatch' "$AUTHORITY")" == "true" ]] \ + || fail "current-host audit does not confirm the release identity hashes" + +readonly SIGNING_IDENTITY="$(jq -r '.finalInstalledRelease.signingIdentity' "$AUTHORITY")" +readonly IDENTITY_COUNT="$(security find-identity -v -p codesigning | awk -v name="$SIGNING_IDENTITY" 'index($0, "\"" name "\"") { count += 1 } END { print count + 0 }')" +integer CURRENT_HOST_BLOCKED=0 +if [[ "$IDENTITY_COUNT" -eq 0 ]]; then + print -u2 "Current-host signing blocker: zero available '${SIGNING_IDENTITY}' code-signing identities." + CURRENT_HOST_BLOCKED=1 +fi + +SIGNATURE_OUTPUT="" +if ! SIGNATURE_OUTPUT="$(codesign --verify --deep --strict --verbose=2 "$INSTALLED_APP" 2>&1)"; then + print -u2 -- "$SIGNATURE_OUTPUT" + CURRENT_HOST_BLOCKED=1 +fi + +if [[ "$CURRENT_HOST_BLOCKED" -ne 0 ]]; then + print -u2 "Recorded audit: $(jq -r '.currentHostAudit.observedAt + " — " + .currentHostAudit.cssmError' "$AUTHORITY")" + fail "installed release hashes match, but current-host signing trust is not passing" +fi + +print "TFTMAC installed-runtime verification: OK" diff --git a/scripts/verify-repository.command b/scripts/verify-repository.command deleted file mode 100755 index 3709a4d..0000000 --- a/scripts/verify-repository.command +++ /dev/null @@ -1,303 +0,0 @@ -#!/bin/zsh -set -euo pipefail -unsetopt BG_NICE - -readonly PROJECT_DIR="${0:A:h:h}" -cd "$PROJECT_DIR" - -fail() { - print -u2 "Repository validation failed: $*" - exit 1 -} - -require_command() { - command -v "$1" >/dev/null 2>&1 || fail "required command is unavailable: $1" -} - -require_command find -require_command jq -require_command perl -require_command plutil -require_command rg -require_command xattr -require_command xcrun -require_command zsh - -typeset -a REQUIRED_FILES -REQUIRED_FILES=( - .editorconfig - .gitattributes - .gitignore - .github/workflows/ci.yml - .github/ISSUE_TEMPLATE/bug_report.yml - .github/ISSUE_TEMPLATE/feature_request.yml - .github/ISSUE_TEMPLATE/config.yml - .github/pull_request_template.md - .github/repository-metadata.yml - CHANGELOG.md - CONTRIBUTING.md - LICENSE - NOTICE.md - README.md - SECURITY.md - SUPPORT.md - docs/architecture.md - docs/benchmarks.md - docs/building.md - docs/launch-profiles.md - docs/releasing.md - docs/reproducibility.md - docs/research-log.md - docs/troubleshooting.md - docs/telemetry-contract/SHA256SUMS - docs/telemetry-contract/first-game-session-v2.json - docs/telemetry-contract/game-session-diagnostics-v2.json - launcher/Info.plist - launcher/Resources/EmulatorHost-Info.plist - launcher/Resources/EmulatorIcon.icns - launcher/Resources/EmulatorIcon-1024.png - launcher/Resources/Mactician.icns - launcher/Resources/Mactician-1024.png - launcher/Resources/MacticianHero.png - launcher/Resources/release-manifest.json - scripts/android-environment.sh - scripts/build-mactician.command - scripts/build-mactician-icns.pl - scripts/generate-mactician-assets.command - scripts/integration-test-mactician.command - scripts/publish-mactician-update.command - scripts/test-mactician.command - scripts/verify-branding.command - scripts/verify-telemetry-contract.command - scripts/verify-repository.command - run-tft-best-verified.command - branding/mactician-app-icon.svg - branding/mactician-game-host-icon.svg - branding/mactician-favicon.svg - branding/mactician-mark.svg - branding/mactician-mark-dark.svg - branding/mactician-mark-light.svg - branding/mactician-mark-monochrome.svg - branding/mactician-open-graph.svg - branding/mactician-product-hero.svg - branding/mactician-small-size-test.svg - branding/mactician-social-preview.svg - branding/mactician-wordmark.svg - branding/generated/Mactician.icns - branding/generated/EmulatorIcon.icns - branding/generated/mactician-favicon.ico - branding/generated/mactician-open-graph.png - branding/generated/mactician-small-size-test.png - branding/generated/mactician-social-preview.png -) -for required_file in "${REQUIRED_FILES[@]}"; do - [[ -f "$required_file" ]] || fail "required file is missing: $required_file" -done -[[ -x scripts/build-mactician-icns.pl ]] || fail "ICNS builder is not executable" - -typeset script_file -while IFS= read -r script_file; do - [[ -x "$script_file" ]] || fail "script is not executable: $script_file" - zsh -o NO_BG_NICE -n "$script_file" || fail "zsh syntax check failed: $script_file" -done < <(find . -type f \( -name '*.command' -o -name '*.sh' \) \ - -not -path './.git/*' -not -path './launcher/.build/*' \ - -not -path './dist/*' -not -path './dist-build*/*' \ - -not -path './runtime/*' | LC_ALL=C sort) - -plutil -lint launcher/Info.plist >/dev/null || fail "launcher Info.plist is invalid" -plutil -lint launcher/Resources/EmulatorHost-Info.plist >/dev/null \ - || fail "emulator host Info.plist is invalid" -plutil -lint launcher/Resources/QEMU-Hypervisor.entitlements >/dev/null \ - || fail "hypervisor entitlements are invalid" -typeset strings_file -for strings_file in launcher/Resources/*.lproj/Localizable.strings; do - plutil -lint "$strings_file" >/dev/null || fail "strings file is invalid: $strings_file" -done - -typeset json_file -while IFS= read -r json_file; do - jq -e . "$json_file" >/dev/null || fail "JSON file is invalid: $json_file" -done < <(find . -type f -name '*.json' -not -path './.git/*' \ - -not -path './launcher/.build/*' -not -path './dist/*' \ - -not -path './dist-build*/*' -not -path './runtime/*' | LC_ALL=C sort) - -xcrun clang -target arm64-apple-macosx12.0 -fsyntax-only launcher/EmulatorHost/main.c - -typeset developer_root="/""Users/" -typeset local_user="sb""naumov" -typeset path_match -while IFS= read -r path_match; do - case "$path_match" in - ./scripts/build-mactician.command:*|./launcher/Tests/LauncherTests.swift:*) ;; - *) fail "developer-specific absolute path remains: $path_match" ;; - esac -done < <(rg -n --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!scripts/verify-repository.command' "$developer_root" . || true) - -typeset identity_match -while IFS= read -r identity_match; do - [[ "$identity_match" == *"local.${local_user}.tft-pbe-"* ]] \ - || fail "personal identifier remains outside the documented compatibility IDs: $identity_match" -done < <(rg -n --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!scripts/verify-repository.command' "$local_user" . || true) - -typeset codex_match -while IFS= read -r codex_match; do - [[ "$codex_match" == *'Android_Codex'* ]] \ - || fail "workspace-only Codex label remains: $codex_match" -done < <(rg -ni --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!scripts/verify-repository.command' 'codex' . || true) -while IFS= read -r codex_match; do - [[ "${codex_match:t}" == Android_Codex.DeviceProfiles*.ini ]] \ - || fail "workspace-only Codex filename remains: $codex_match" -done < <(find . -iname '*codex*' -not -path './.git/*' \ - -not -path './launcher/.build/*' -not -path './dist/*' \ - -not -path './dist-build*/*' -not -path './runtime/*' | LC_ALL=C sort) - -if rg -ni --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!scripts/verify-repository.command' \ - 'next chat|handoff for the next|managed workspace|documents/codex|files-mentioned-by-the-user' .; then - fail "internal handoff or managed-workspace language remains" -fi - -if rg -n '[А-Яа-яЁё]' --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!launcher/Resources/ru.lproj/**' \ - --glob '!scripts/test-mactician.command' \ - --glob '!scripts/verify-repository.command' .; then - fail "repository text is not in English" -fi - -typeset junk_path -while IFS= read -r junk_path; do - fail "macOS metadata or AppleDouble file remains: $junk_path" -done < <(find . \( -name '.DS_Store' -o -name '._*' \) -not -path './.git/*' \ - -not -path './launcher/.build/*' -not -path './dist/*' \ - -not -path './dist-build*/*' -not -path './runtime/*') - -typeset attr_path -while IFS= read -r attr_path; do - if [[ -n "$(xattr "$attr_path" 2>/dev/null)" ]]; then - fail "extended attributes remain on: $attr_path" - fi -done < <(find . -not -path './.git' -not -path './.git/*' \ - -not -path './launcher/.build' -not -path './launcher/.build/*' \ - -not -path './dist' -not -path './dist/*' \ - -not -path './dist-build*' -not -path './dist-build*/*' \ - -not -path './runtime' -not -path './runtime/*') - -typeset forbidden_path -while IFS= read -r forbidden_path; do - if git check-ignore -q "$forbidden_path" 2>/dev/null; then - continue - fi - fail "private or generated artifact is present and not ignored: $forbidden_path" -done < <(find . -type f \( -name '*.apk' -o -name '*.apks' -o -name '*.xapk' \ - -o -name '*.aab' -o -name '*.obb' -o -name '*.qcow2' -o -name 'userdata*.img' \ - -o -name 'encryptionkey*.img' -o -name '*.p12' -o -name '*.pfx' -o -name '*.jks' \ - -o -name '*.keystore' -o -name '*.key' -o -name '*.pem' -o -name '*.crt' \ - -o -name '*.cer' -o -name '*.mobileprovision' -o -name '*.token' \ - -o -name '*.secret' -o -name '.DS_Store' \) \ - -not -path './.git/*' -not -path './dist-build*/*') - -if rg -n --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!scripts/verify-repository.command' \ - -- '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}' .; then - fail "text resembling a private key or access token remains" -fi - -typeset owner_placeholder='" -typeset repository_placeholder='" -if rg -n --hidden --glob '!.git/**' --glob '!launcher/.build/**' \ - --glob '!scripts/verify-repository.command' \ - "$owner_placeholder|$repository_placeholder|NAME \(TEAM_ID\)|TODO_PUBLIC" .; then - fail "publication placeholder remains" -fi - -typeset markdown_file link target -while IFS= read -r markdown_file; do - while IFS=$'\t' read -r link; do - [[ -n "$link" ]] || continue - case "$link" in - \#*|http://*|https://*|mailto:*|../../releases/*) continue ;; - esac - link="${link%%#*}" - link="${link%%\?*}" - [[ -n "$link" ]] || continue - [[ "$link" != /* ]] || fail "absolute local Markdown link in $markdown_file: $link" - target="${markdown_file:h}/$link" - [[ -e "$target" ]] || fail "broken Markdown link in $markdown_file: $link" - done < <(perl -ne 'while (/\[[^]]*\]\(([^)]+)\)/g) { print "$1\n" }' "$markdown_file") -done < <(find . -type f -name '*.md' -not -path './.git/*' \ - -not -path './launcher/.build/*' -not -path './dist/*' \ - -not -path './dist-build*/*' -not -path './runtime/*' | LC_ALL=C sort) - -typeset -a DOCUMENTED_LAUNCHERS -DOCUMENTED_LAUNCHERS=( - run-tft-angle-opengl.command - run-tft-best-verified.command - run-tft-mvk128-experimental.command -) -typeset documented_launcher -for documented_launcher in "${DOCUMENTED_LAUNCHERS[@]}"; do - [[ -x "$documented_launcher" ]] \ - || fail "documented launcher is absent or not executable: $documented_launcher" -done -while IFS= read -r documented_launcher; do - [[ -f "$documented_launcher" || -f "scripts/$documented_launcher" ]] \ - || fail "documentation names a missing launcher: $documented_launcher" -done < <(rg -o --no-filename 'run-tft-[A-Za-z0-9-]+[.]command' \ - README.md launcher/README.md docs | LC_ALL=C sort -u) - -typeset version build_number host_version host_build -version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' launcher/Info.plist)" -build_number="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' launcher/Info.plist)" -host_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' launcher/Resources/EmulatorHost-Info.plist)" -host_build="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' launcher/Resources/EmulatorHost-Info.plist)" -[[ "$version" == "$host_version" ]] || fail "launcher and emulator host versions differ" -[[ "$build_number" == "$host_build" ]] || fail "launcher and emulator host build numbers differ" -grep -Fq "Version: **$version** (build $build_number)" README.md \ - || fail "README version/build does not match Info.plist" -grep -Fq "version $version, build $build_number" CHANGELOG.md \ - || fail "CHANGELOG version/build does not match Info.plist" -grep -Fq "## Unreleased" CHANGELOG.md || fail "CHANGELOG has no Unreleased section" -grep -Fq "version $version, build $build_number" docs/releasing.md \ - || fail "release guide version/build does not match Info.plist" - -[[ "$(plutil -extract CFBundleDisplayName raw launcher/Info.plist)" == "Mactician" ]] \ - || fail "unexpected app display name" -[[ "$(plutil -extract CFBundleExecutable raw launcher/Info.plist)" == "Mactician" ]] \ - || fail "unexpected app executable" -[[ "$(plutil -extract CFBundleIdentifier raw launcher/Info.plist)" \ - == "dev.sergeinaumov.mactician" ]] || fail "unexpected bundle identifier" -grep -Fq 'Mactician-$VERSION.dmg' scripts/build-mactician.command \ - || fail "versioned DMG naming is missing" -grep -Fq -- '-volname "Mactician"' scripts/build-mactician.command \ - || fail "DMG volume name is missing" -grep -Fq 'Sparkle-LICENSE.txt' scripts/build-mactician.command \ - || fail "Sparkle license is not packaged in the application" -grep -Fq 'readonly LICENSE_FILE=' scripts/prepare-sparkle.command \ - || fail "prepared Sparkle dependency does not retain its license" - -jq -e '.schemaVersion == 1 - and (.components | length == 3) - and (.game.apks | length == 4) - and (.components[] | .sha256 | test("^[0-9a-f]{64}$")) - and (.game.apks[] | .sha256 | test("^[0-9a-f]{64}$"))' \ - launcher/Resources/release-manifest.json >/dev/null \ - || fail "release manifest structure or hashes are invalid" - -typeset required_ignore -for required_ignore in '/runtime/' '/dist/' '/dist-build*/' '/launcher/.build/' '/private/' '*.apk' '*.qcow2' '*.p12' '*.pem' '*.mobileprovision' '.DS_Store'; do - grep -Fqx "$required_ignore" .gitignore \ - || fail ".gitignore lacks required rule: $required_ignore" -done - -if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - git diff --check - git diff --cached --check -fi - -./scripts/verify-branding.command -./scripts/verify-telemetry-contract.command - -print "Repository validation: OK ($version build $build_number)" diff --git a/scripts/verify-telemetry-contract.command b/scripts/verify-telemetry-contract.command deleted file mode 100755 index 711f696..0000000 --- a/scripts/verify-telemetry-contract.command +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h}" -readonly CONTRACT_DIR="$PROJECT_DIR/docs/telemetry-contract" -readonly API_CONTRACT_DIR="${MACTICIAN_API_CONTRACT_DIR:-$PROJECT_DIR/../sergeinaumov.dev/server/mactician-api/testdata/contract}" - -( - cd "$CONTRACT_DIR" - shasum -a 256 -c SHA256SUMS -) - -if [[ -d "$API_CONTRACT_DIR" ]]; then - for fixture in first-game-session-v2.json game-session-diagnostics-v2.json SHA256SUMS; do - cmp -s "$CONTRACT_DIR/$fixture" "$API_CONTRACT_DIR/$fixture" || { - print -u2 "Telemetry contract drift: $fixture differs from $API_CONTRACT_DIR/$fixture" - exit 1 - } - done - print "Launcher and API telemetry contracts match." -else - print "API contract directory not present; canonical launcher fixture hashes are valid." -fi diff --git a/scripts/verify-tftmac.command b/scripts/verify-tftmac.command new file mode 100755 index 0000000..d8abe33 --- /dev/null +++ b/scripts/verify-tftmac.command @@ -0,0 +1,296 @@ +#!/bin/zsh +set -euo pipefail +unsetopt BG_NICE + +readonly ROOT="${0:A:h:h}" +cd "$ROOT" + +fail() { + print -u2 "TFTMAC source validation failed: $*" + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "required command is unavailable: $1" +} + +# Repository/CI contract. It deliberately has no dependency on +# /Applications/TFTMAC.app, the external emulator runtime, a signing identity, +# user credentials, or a private capture. Those checks belong to the separate +# local-only verify-installed-runtime.command contract. +for tool in git jq node plutil rg shasum xcodebuild zsh; do + require_command "$tool" +done + +readonly INFO="tftmac/Info.plist" +readonly PACKAGE_RESOLVED="TFTMAC.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" +readonly PROTO="Vendor/AndroidEmulator/emulator_controller.proto" +readonly PROTO_SOURCE="Vendor/AndroidEmulator/SOURCE.json" + +for required in \ + TFTMAC.xcodeproj/project.pbxproj \ + TFTMAC.xcodeproj/xcshareddata/xcschemes/TFTMAC.xcscheme \ + "$INFO" \ + "$PACKAGE_RESOLVED" \ + "$PROTO" \ + "$PROTO_SOURCE" \ + tftmac/Assets/TFTMAC-Official-Icon.png \ + Generated/EmulatorController/emulator_controller.pb.swift \ + Generated/EmulatorController/emulator_controller.grpc.swift \ + scripts/generate-emulator-proto.command \ + scripts/build-tftmac-app.command \ + scripts/build-native-app.command \ + scripts/install-trace-processor.command \ + scripts/ensure-local-signing-identity.command \ + scripts/test-native-app.command \ + scripts/verify-installed-runtime.command \ + ssot/AUTHORITY_INPUTS.sha256 \ + ssot/STACK.lock.yaml \ + ssot/runtime-authority.json \ + ssot/retained-evidence-index.json \ + ssot/TFTMAC_ENGINEERING_MAP.sql \ + ssot/TFTMAC_PERFORMANCE_LAB.sql; do + [[ -f "$required" ]] || fail "required file is missing: $required" +done + +plutil -lint "$INFO" >/dev/null || fail "Info.plist is invalid" +[[ "$(plutil -extract CFBundleDisplayName raw "$INFO")" == "TFTMAC" ]] || fail "unexpected app display name" +[[ "$(plutil -extract CFBundleExecutable raw "$INFO")" == "TFTMAC" ]] || fail "unexpected app executable" +[[ "$(plutil -extract CFBundleIdentifier raw "$INFO")" == "com.flashls1.tftmac" ]] || fail "unexpected bundle identifier" +readonly INFO_VERSION="$(plutil -extract CFBundleShortVersionString raw "$INFO")" +readonly INFO_BUILD="$(plutil -extract CFBundleVersion raw "$INFO")" +readonly AUTHORITY_VERSION="$(jq -r '.finalInstalledRelease.version' ssot/runtime-authority.json)" +readonly AUTHORITY_BUILD="$(jq -r '.finalInstalledRelease.build' ssot/runtime-authority.json)" +[[ "$INFO_VERSION" == "$AUTHORITY_VERSION" ]] || fail "source and release-authority versions differ" +[[ "$INFO_BUILD" == "$AUTHORITY_BUILD" ]] || fail "source and release-authority builds differ" +[[ "$(plutil -extract NSRemovableVolumesUsageDescription raw "$INFO")" == *"Android emulator runtime"* ]] \ + || fail "removable-volume purpose string is missing" + +# There is one executable build authority. The compatibility entrypoint may +# delegate to it, but it may never rebuild or install the retired Node shell. +rg -q -F 'scripts/build-native-app.command' scripts/build-tftmac-app.command \ + || fail "compatibility build entrypoint does not delegate to the native build" +if rg -n 'tftmac/Sources|tftmac-direct-control|5040|5592|swiftc' scripts/build-tftmac-app.command; then + fail "compatibility build entrypoint still contains a retired build/runtime path" +fi +if rg -n 'TFTMACRuntimeBridge|TFTMACViews|TFTMACWindowCoordinator' TFTMAC.xcodeproj/project.pbxproj; then + fail "native Xcode target references the retired Node shell" +fi + +jq -e ' + .schema == 3 and .result == "PASS" and + .resultScope == "HISTORICAL_RELEASE_AND_LIVE_RUNTIME_ACCEPTANCE; SEE currentHostAudit FOR CURRENT SIGNING TRUST" and + .emulator.avd == "TFT_Ultra_Tablet" and + .emulator.adbSerial == "emulator-5582" and + .emulator.adbServerPort == 5038 and + .emulator.consolePort == 5582 and + .emulator.adbVendorKeysInjected == false and + .runtimeProfile.vcpu == 6 and + .runtimeProfile.ramMiB == 5120 and + .runtimeProfile.display == "1920x1080" and + .runtimeProfile.densityDpi == 320 and + .runtimeProfile.refreshHz == 60 and + .runtimeProfile.tftGraphicsQuality == "High" and + .runtimeProfile.tftFrameRateCap == 60 and + .runtimeProfile.tftPerformanceModeBeta == false and + .runtimeProfile.activeExperiment == "combat_latency_a" and + .finalInstalledRelease.version == "2.3.0" and + .finalInstalledRelease.build == "8" and + .finalInstalledRelease.receiptScope == "HISTORICAL_BUILD8_RELEASE_ACCEPTANCE" and + .finalInstalledRelease.deepCodeSignatureValidScope == "AT_RELEASE_ACCEPTANCE" and + .finalInstalledRelease.nativeVerifier == "PASS_AT_RELEASE_ACCEPTANCE" and + .finalInstalledRelease.executableSHA256 == "d3bf7c249a3e5f11b81f778b063e1a8cfe2e7fdeec0537ee6bd8447b1c2268d2" and + .finalInstalledRelease.emulatorHostExecutableSHA256 == "ea028ec1d74cc025638c2a0e5f8c783748803c1b0ba9012962c038251fb3eb63" and + .finalInstalledRelease.officialIconSourceSHA256 == "d6ba9ceb76c4b1e44e87f059f775a0ed629f9bea29b0dd73245853d7dca3a016" and + .finalInstalledRelease.officialIcon1024SHA256 == "ed5fd83efa6e04599e82ca00f897b09813cdeac007850f6993efd601a730345f" and + .finalInstalledRelease.embeddedIconICNSSHA256 == "010729a19f165b68edeb1fb44e8c31e450f79b3a988c95b8de090373378f6f06" and + .finalInstalledRelease.signingIdentity == "TFTMAC Local Code Signing" and + .finalInstalledRelease.adHocSigned == false and + .finalInstalledRelease.removableVolumePermissionRetainedAcrossRelaunch == true and + .finalInstalledRelease.nonErrorUnlockOverlayVisible == false and + .finalInstalledRelease.unitTestsPassed == 43 and + .finalInstalledRelease.gameModeEligible == true and + .finalInstalledRelease.guestGameplayPowerGate == true and + .finalInstalledRelease.primaryInputTransport == "EmulatorController.sendTouch" and + .androidWebView.currentVersion == "151.0.7922.199" +' ssot/runtime-authority.json >/dev/null || fail "native runtime authority drifted" + +jq -e ' + .finalInstalledRelease.currentReleaseGameplayBenchmark == "VERIFIED_CAPTURE_ROOT_ATTRIBUTION_UNKNOWN" and + .currentHostAudit.releaseIdentityHashesMatch == true and + .currentHostAudit.zeroIdentityFindings == true and + .currentHostAudit.trustEvaluation == "NOT_TRUSTED_BY_CURRENT_HOST_POLICY" and + .currentHostAudit.cssmError == "CSSMERR_TP_NOT_TRUSTED" and + .currentHostAudit.installedRuntimeVerifier == "BLOCKED_SIGNING_IDENTITY" and + .currentGameplayCapture.captureId == "2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200" and + .currentGameplayCapture.storage == "PRIVATE_LOCAL_ONLY" and + .currentGameplayCapture.database.byteCount == 63897600 and + .currentGameplayCapture.database.sha256 == "c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77" and + .currentGameplayCapture.graphicsRun.duration == "42m27s" and + .currentGameplayCapture.graphicsRun.exactLayerCoveragePercent == 99.629 and + .currentGameplayCapture.graphicsRun.frameIntervalCount == 144364 and + .currentGameplayCapture.graphicsRun.degradationIncidentCount == 189 and + .currentGameplayCapture.graphicsRun.weightedFps == 56.98 and + .currentGameplayCapture.graphicsRun.fpsOnePercentLow == 21.49 and + .currentGameplayCapture.graphicsRun.p95FrameIntervalMs == 21.51 and + .currentGameplayCapture.graphicsRun.p99FrameIntervalMs == 33.434 and + .currentGameplayCapture.effectiveStackReceipt == "UNREAL_DIRECT_VULKAN -> GFXSTREAM_ASG -> HOST_VULKAN -> MOLTENVK -> METAL" and + .currentGameplayCapture.angleStatus == "CONDITIONAL_NOT_ASSUMED_FOR_TFT_MAIN_RENDERING_PATH" and + .currentGameplayCapture.macPresenter == "EXCLUDED_FROM_CAUSAL_CANDIDATES_CONTEXT_ONLY" and + .currentGameplayCapture.markersAndBattles == "OPTIONAL_ANNOTATIONS_NOT_VALIDITY_OR_CAUSAL_GATES" and + .currentGameplayCapture.automaticLogging == "VERIFIED_PID_LAYER_LIFETIME" and + .currentGameplayCapture.rootAttribution == "UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE" and + .currentGameplayCapture.nextLayer == "ADVANCED_SOURCE_CAUSAL_LOGGER_PLANNED" and + .diagnosticRuntimeEligibility.repository == "flashls1/tftmac-runtime" and + .diagnosticRuntimeEligibility.commit == "c8aa26ebaa5b977965eb165ad8aac5c98408469f" and + .diagnosticRuntimeEligibility.normalPlayAuthority == "STOCK_BUILD8" +' ssot/runtime-authority.json >/dev/null || fail "current Build 8 capture or diagnostic-runtime authority drifted" + +jq -e ' + .schema == 2 and + any(.externalPrivateEvidence[]; + .id == "capture-2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200" and + .classification == "PRIVATE_LOCAL_CAPTURE_METADATA_ONLY" and + .byteCount == 63897600 and + .sha256 == "c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77" and + .rawDatabaseCommitted == false) +' ssot/retained-evidence-index.json >/dev/null || fail "retained private-evidence receipt drifted" + +for locked in \ + 'mode: "released_native_runtime"' \ + 'avd_name: "TFT_Ultra_Tablet"' \ + 'emulator_console_port: 5582' \ + 'adb_serial: "emulator-5582"' \ + 'adb_server_port: 5038' \ + 'ram_mb: 5120' \ + 'selected: A' \ + 'version: "2.3.0"' \ + 'build: "8"' \ + 'official_icon_source_sha256: "d6ba9ceb76c4b1e44e87f059f775a0ed629f9bea29b0dd73245853d7dca3a016"' \ + 'official_icon_1024_sha256: "ed5fd83efa6e04599e82ca00f897b09813cdeac007850f6993efd601a730345f"' \ + 'embedded_icon_icns_sha256: "010729a19f165b68edeb1fb44e8c31e450f79b3a988c95b8de090373378f6f06"' \ + 'mac_icon_embedded: true' \ + 'removable_volume_permission_relaunch: PASS' \ + 'non_error_unlock_overlay: ABSENT' \ + 'unit_tests_passed: 43' \ + 'webview_version: "151.0.7922.199"' \ + 'build8_gameplay_benchmark: "VERIFIED_CAPTURE_ROOT_ATTRIBUTION_UNKNOWN"' \ + 'receipt_scope: "historical_build8_release_acceptance"' \ + 'native_verifier_at_release_acceptance: PASS' \ + 'release_identity_hashes_match: true' \ + 'zero_identity_findings: true' \ + 'cssm_error: "CSSMERR_TP_NOT_TRUSTED"' \ + 'installed_runtime_verifier: "BLOCKED_SIGNING_IDENTITY"' \ + 'id: "2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200"' \ + 'advanced_source_causal_logger: "planned"'; do + rg -q -F -- "$locked" ssot/STACK.lock.yaml || fail "active stack lock drifted: $locked" +done + +readonly STACK_RUNTIME_AUTHORITY_SHA="$(awk -F'"' '/^[[:space:]]*runtime_authority_sha256:/ {print $2}' ssot/STACK.lock.yaml)" +readonly STACK_AUTHORITY_INPUTS_SHA="$(awk -F'"' '/^[[:space:]]*authority_inputs_sha256:/ {print $2}' ssot/STACK.lock.yaml)" +readonly ACTUAL_RUNTIME_AUTHORITY_SHA="$(shasum -a 256 ssot/runtime-authority.json | awk '{print $1}')" +readonly ACTUAL_AUTHORITY_INPUTS_SHA="$(shasum -a 256 ssot/AUTHORITY_INPUTS.sha256 | awk '{print $1}')" +[[ "$STACK_RUNTIME_AUTHORITY_SHA" == "$ACTUAL_RUNTIME_AUTHORITY_SHA" ]] || fail "STACK runtime-authority hash drifted" +[[ "$STACK_AUTHORITY_INPUTS_SHA" == "$ACTUAL_AUTHORITY_INPUTS_SHA" ]] || fail "STACK authority-input manifest hash drifted" + +while read -r expected_hash authority_path; do + [[ -z "${expected_hash:-}" || "$expected_hash" == \#* ]] && continue + [[ -f "$authority_path" ]] || fail "authority input is missing: $authority_path" + actual_hash="$(shasum -a 256 "$authority_path" | awk '{print $1}')" + [[ "$actual_hash" == "$expected_hash" ]] || fail "authority input hash drifted: $authority_path" +done < ssot/AUTHORITY_INPUTS.sha256 + +readonly TEST_FUNCTION_COUNT="$(rg -n '^[[:space:]]*func test' Tests/TFTMACTests --glob '*.swift' | wc -l | tr -d '[:space:]')" +[[ "$TEST_FUNCTION_COUNT" == "43" ]] || fail "native test inventory drifted: expected 43, found $TEST_FUNCTION_COUNT" +[[ "$(plutil -extract LSSupportsGameMode raw "$INFO")" == "true" ]] \ + || fail "native app is not eligible for macOS Game Mode" +[[ "$(shasum -a 256 tftmac/Assets/TFTMAC-Official-Icon.png | awk '{print $1}')" == "d6ba9ceb76c4b1e44e87f059f775a0ed629f9bea29b0dd73245853d7dca3a016" ]] \ + || fail "official TFTMAC icon source hash drifted" + +readonly PROTO_SHA="$(shasum -a 256 "$PROTO" | awk '{print $1}')" +readonly RECORDED_PROTO_SHA="$(jq -r '.vendoredProtoSHA256' "$PROTO_SOURCE")" +readonly RECORDED_INSTALLED_SHA="$(jq -r '.installedProtoSHA256' "$PROTO_SOURCE")" +[[ "$PROTO_SHA" == "$RECORDED_PROTO_SHA" ]] || fail "vendored EmulatorController proto hash drift" +[[ "$PROTO_SHA" == "$RECORDED_INSTALLED_SHA" ]] || fail "vendored proto no longer matches frozen installed-runtime authority" +jq -e '.schema == 1 and .authority == "INSTALLED_ANDROID_EMULATOR" and .vendoredProtoPath == "Vendor/AndroidEmulator/emulator_controller.proto" and .generator.protocVersion == "36.0" and .generator.swiftProtobufVersion == "1.38.1" and .generator.grpcSwiftProtobufVersion == "2.4.1"' "$PROTO_SOURCE" >/dev/null \ + || fail "EmulatorController provenance is invalid" + +readonly GENERATED_PB_SHA="$(shasum -a 256 Generated/EmulatorController/emulator_controller.pb.swift | awk '{print $1}')" +readonly GENERATED_GRPC_SHA="$(shasum -a 256 Generated/EmulatorController/emulator_controller.grpc.swift | awk '{print $1}')" +[[ "$GENERATED_PB_SHA" == "$(jq -r '.generatedSources["Generated/EmulatorController/emulator_controller.pb.swift"]' "$PROTO_SOURCE")" ]] || fail "generated Swift protobuf source drift" +[[ "$GENERATED_GRPC_SHA" == "$(jq -r '.generatedSources["Generated/EmulatorController/emulator_controller.grpc.swift"]' "$PROTO_SOURCE")" ]] || fail "generated Swift gRPC source drift" + +jq -e ' + .version == 3 and + any(.pins[]; .identity == "grpc-swift-2" and .state.version == "2.4.2") and + any(.pins[]; .identity == "grpc-swift-nio-transport" and .state.version == "2.9.1") and + any(.pins[]; .identity == "grpc-swift-protobuf" and .state.version == "2.4.1") and + any(.pins[]; .identity == "swift-protobuf" and .state.version == "1.38.1") +' "$PACKAGE_RESOLVED" >/dev/null || fail "SwiftPM authority pins drifted" + +while IFS= read -r script; do + zsh -o NO_BG_NICE -n "$script" || fail "zsh syntax failed: $script" +done < <(find scripts -type f \( -name '*.command' -o -name '*.sh' \) | LC_ALL=C sort) + +node --check tools/tftmac-direct-control.mjs >/dev/null +[[ ! -f tools/tftmac-v2.mjs ]] || node --check tools/tftmac-v2.mjs >/dev/null +node tools/tftmac-direct-control.mjs engineering-map-selftest >/dev/null +node tools/tftmac-direct-control.mjs lab-selftest >/dev/null + +if [[ -n "${TFTMAC_FORBIDDEN_TOKEN:-}" ]]; then + if git ls-files | rg -i -F -- "$TFTMAC_FORBIDDEN_TOKEN"; then + fail "forbidden retired-product token remains in a tracked path" + fi + if git grep -n -i -F -- "$TFTMAC_FORBIDDEN_TOKEN"; then + fail "forbidden retired-product token remains in tracked content" + fi +fi + +if git ls-files | rg -i '\.(apk|apks|xapk|aab|obb|qcow2|p12|pfx|jks|keystore|mobileprovision|sqlite|sqlite3|db|perfetto-trace|trace|pcap|dmp)$'; then + fail "private, generated, credential, or raw runtime artifact is tracked" +fi +if git ls-files | rg -i '(^|/)(captures?|screenshots?|login[-_]?probes?|ocr[-_]?probes?|recovery[-_]?apps?|avd[-_]?data)(/|$)'; then + fail "private capture, screenshot, login/OCR probe, recovery app, or AVD data is tracked" +fi + +git diff --check +git diff --cached --check + +readonly STATE_DIR="$(mktemp -d /private/tmp/tftmac-source-verify.XXXXXX)" +readonly STATE_BEFORE="${STATE_DIR}/before" +readonly STATE_AFTER="${STATE_DIR}/after" +cleanup() { + /bin/rm -rf "$STATE_DIR" +} +trap cleanup EXIT +git status --porcelain=v1 --untracked-files=all > "$STATE_BEFORE" + +if [[ -z "${DEVELOPER_DIR:-}" ]]; then + if [[ -d /Applications/Xcode-26.6.0.app/Contents/Developer ]]; then + export DEVELOPER_DIR=/Applications/Xcode-26.6.0.app/Contents/Developer + else + export DEVELOPER_DIR="$(xcode-select -p)" + fi +fi +readonly RELEASE_DERIVED="${ROOT}/.build/native-ci-release" +/usr/bin/xcodebuild \ + -quiet \ + -project "${ROOT}/TFTMAC.xcodeproj" \ + -scheme TFTMAC \ + -configuration Release \ + -destination 'platform=macOS,arch=arm64' \ + -derivedDataPath "$RELEASE_DERIVED" \ + CODE_SIGNING_ALLOWED=NO \ + build +[[ -x "${RELEASE_DERIVED}/Build/Products/Release/TFTMAC.app/Contents/MacOS/TFTMAC" ]] \ + || fail "unsigned Release build did not produce the TFTMAC executable" + +/bin/zsh scripts/test-native-app.command + +git status --porcelain=v1 --untracked-files=all > "$STATE_AFTER" +cmp -s "$STATE_BEFORE" "$STATE_AFTER" || { + diff -u "$STATE_BEFORE" "$STATE_AFTER" || true + fail "source verification changed tracked or visible generated state" +} + +print "TFTMAC source validation: OK (unsigned Release build; 43 native tests)" diff --git a/ssot/AUTHORITY_INPUTS.sha256 b/ssot/AUTHORITY_INPUTS.sha256 index c9ccd4c..5cdc7b1 100644 --- a/ssot/AUTHORITY_INPUTS.sha256 +++ b/ssot/AUTHORITY_INPUTS.sha256 @@ -1,6 +1,11 @@ -# TFTMAC GPU Runtime v2.0 authority inputs -# Exact bytes uploaded and approved on 2026-08-26. -# Canonical repository names are fixed; future revisions must update both documents together. +# TFTMAC Build 8 documentation and machine-readable authority inputs +# Regenerate only after reviewing the complete authority change set. -81bd386c1d9d47f26659c93af914ec9a92568ee689e7bed47eb0f2afd6dd5f1a TFTMAC_FULL_IMPLEMENTATION_PLAN.md -4c905ad35a676aa8f4ad0a22416e73d640120851ce9f41e742349ac61ceab1ea TFTMAC_GPU_RUNTIME_SSOT.md +78016e5e3c7c08e263e2d8aba435f8519ae199cadc18ea74d6c39634cd0fff18 facts.md +273307f655f0749a12a0a3e48f421f5e61bc6bd40f00b1afb7040c0b2fd15adb project.md +a40e186e4d37a4e7d6024bd069a69509729eb9e6efda36a0fab12162ecf60f7e dev.md +15a33754add669bdfea2001053cbfb47df1120658be7f10022a50656fb4069c6 benchmark.md +bad70917a4e3230646748c33a171aeba0f0b1f4ff9b93fdfaa7b04788b84c532 TFTMAC_GPU_RUNTIME_SSOT.md +2af6196e08b3f81032b8226fa3a9c25a3b0d99537f0872b7eb21a96aaf2b5d2f ssot/runtime-authority.json +9f5d87a2ae5a34eb8f89691ec1756ad1c7152fc94706aa8de0a111363623dd24 ssot/TFTMAC_ENGINEERING_MAP.sql +e2b495770c13abe3f65779027656a699a9dec200278a9260e7de767516ffa0c1 ssot/TFTMAC_PERFORMANCE_LAB.sql diff --git a/ssot/STACK.lock.yaml b/ssot/STACK.lock.yaml index 09036e7..a425daf 100644 --- a/ssot/STACK.lock.yaml +++ b/ssot/STACK.lock.yaml @@ -1,17 +1,20 @@ -schema: 2 -frozen_at: "2026-08-28T08:46:32.848Z" +schema: 3 +frozen_at: "2026-08-31T23:13:30Z" phase0_status: PASS authority: - ssot_version: "2.0" - implementation_plan_version: "2.0" - ssot_sha256: "4c905ad35a676aa8f4ad0a22416e73d640120851ce9f41e742349ac61ceab1ea" - implementation_plan_sha256: "81bd386c1d9d47f26659c93af914ec9a92568ee689e7bed47eb0f2afd6dd5f1a" + mode: "released_native_runtime" + runtime_authority: "ssot/runtime-authority.json" + ssot_version: "4.0-build8-reconciliation" + documentation_authority: "facts.md + project.md + dev.md + benchmark.md" + authority_inputs: "ssot/AUTHORITY_INPUTS.sha256" + runtime_authority_sha256: "2af6196e08b3f81032b8226fa3a9c25a3b0d99537f0872b7eb21a96aaf2b5d2f" + authority_inputs_sha256: "1d85f4f2f64f3b6089c79385b329dfe42bb479b8c65877c39bf01bdef13703ac" zengate_version: "2.3" - zengate_score: 93 + zengate_score: 95.8 zengate_result: "PASS" - aemu_branch: "emu-master-dev" - production_guest: "official Android 17 Google Play ARM64" + source_build_inputs_status: "historical_reference_only_for_normal_play; isolated_diagnostic_eligibility_only" + production_guest: "official Android 16 Google Play ARM64" shipping_render_target: "1920x1080@60" host: @@ -23,6 +26,54 @@ host: macos_sdk_path: "/Applications/Xcode-26.6.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.5.sdk" hardware_model: "Mac16,10" +release: + receipt_scope: "historical_build8_release_acceptance" + accepted_at: "2026-08-31T21:41:12Z" + version: "2.3.0" + build: "8" + installed_path: "/Applications/TFTMAC.app" + executable_sha256: "d3bf7c249a3e5f11b81f778b063e1a8cfe2e7fdeec0537ee6bd8447b1c2268d2" + emulator_host_executable_sha256: "ea028ec1d74cc025638c2a0e5f8c783748803c1b0ba9012962c038251fb3eb63" + signing_at_release_acceptance: "stable_local_identity_valid" + signing_identity: "TFTMAC Local Code Signing" + notarized_for_public_distribution: false + mac_icon_embedded: true + official_icon_source_sha256: "d6ba9ceb76c4b1e44e87f059f775a0ed629f9bea29b0dd73245853d7dca3a016" + official_icon_1024_sha256: "ed5fd83efa6e04599e82ca00f897b09813cdeac007850f6993efd601a730345f" + embedded_icon_icns_sha256: "010729a19f165b68edeb1fb44e8c31e450f79b3a988c95b8de090373378f6f06" + removable_volume_permission_relaunch: PASS + non_error_unlock_overlay: ABSENT + native_verifier_at_release_acceptance: PASS + unit_tests_passed: 43 + rapid_combat_ab: "AUTOMATIC_FULL_RUN_VERIFIED_ROOT_ATTRIBUTION_UNKNOWN" + current_release_launch_acceptance: "LIVE_BUILD8_PID_LAYER_COMPLETE_RECEIPT_PASS" + build7_launch_capture: "2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494" + build8_clean_shutdown_capture: "2026-08-31T21-30-37.047Z-f77d727b-a5ad-4418-8af7-2e352cdbdbbb" + build8_launch_capture: "2026-08-31T21-39-18.396Z-fe34e3a1-fb91-44eb-804f-4ca8519dfc31" + build8_automatic_graphics_logger: "PASS_PID_LAYER_COMPLETE_RECEIPT_FRAME_LINKS" + build8_gameplay_benchmark: "VERIFIED_CAPTURE_ROOT_ATTRIBUTION_UNKNOWN" + host_qos_pre_exec: "USER_INTERACTIVE_SET_RESULT_0" + qemu_worker_qos_inheritance: "NOT_CLAIMED_WITHOUT_COMBAT_EVIDENCE" + game_mode_eligible: true + guest_gameplay_power_gate: true + active_experiment: "combat_latency_a" + tft_graphics: "High / 60 FPS / Performance Mode OFF" + perfetto_trace_processor_version: "58.2" + perfetto_trace_processor_sha256: "d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa" + current_host_audit: + observed_at: "2026-08-31T23:13:30Z" + release_identity_hashes_match: true + zero_identity_findings: true + trust_evaluation: "NOT_TRUSTED_BY_CURRENT_HOST_POLICY" + cssm_error: "CSSMERR_TP_NOT_TRUSTED" + installed_runtime_verifier: "BLOCKED_SIGNING_IDENTITY" + conclusion: "Historical signing acceptance remains distinct from this timestamped host trust audit." + +verification_contracts: + repository_source: "scripts/verify-tftmac.command; no installed app, external runtime, signing identity, credentials, or private capture" + installed_runtime: "scripts/verify-installed-runtime.command; local-only and currently blocked on signing identity" + github_ci: "repository_source only" + android_command_line_tools: archive: "commandlinetools-mac_arm64-15859902_latest.zip" sha256: "835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e" @@ -30,19 +81,22 @@ android_command_line_tools: installed_revision: "22.0" android: - api: 37 + api: 36 play_image_package: "system-images;android-36;google_apis_playstore;arm64-v8a" play_image_revision: 7 - avd_name: "TFTMAC_Live_API37" - emulator_console_port: 5592 - adb_serial: "emulator-5592" - adb_server_port: 5040 + avd_name: "TFT_Ultra_Tablet" + emulator_console_port: 5582 + adb_serial: "emulator-5582" + adb_server_port: 5038 platform_tools_revision: "37.0.1" emulator_revision: "37.1.11" + webview_provider: "com.google.android.webview" + webview_version: "151.0.7922.199" platform_revision: "1" build_tools_revision: "37.0.0" aemu: + status: "historical_reference_only_not_required_by_live_runtime" authority_branch: "emu-master-dev" resolved_manifest_sha256: "28865cd8a162178ba462b296f5714b6b7b6916e0cafcddfc0c5e44aa03f8e8d3" qemu_commit: "ae9d18d2b6261179fbd57fffec720a04f7bfb053" @@ -53,17 +107,55 @@ aemu: guestangle_authority: "PASS" vulkan_sdk: + status: "historical_reference_only_not_required_by_live_runtime" version: "1.4.357.0" sha256: "539433589c83522e6f31b1c7b418a4167e21597a4a361ab119e1dc0760cf3865" vulkaninfo_version: "vulkaninfo - Summarize Vulkan information in relation to the current environment.\n\nUSAGE: \n vulkaninfo --summary\n vulkaninfo -o | --output \n vulkaninfo -j | -j= | --json | --json=\n vulkaninfo --text\n vulkaninfo --html\n vulkaninfo --show-all\n vulkaninfo --show-formats\n vulkaninfo --show-tool-props\n vulkaninfo --show-promoted-structs\n vulkaninfo --show-video-props\n\nOPTIONS:\n[-h, --help] Print this help.\n[--summary] Show a summary of the instance and GPU's on a system.\n[-o , --output ]\n Print output to a new file whose name is specified by filename.\n File will be written to the current working directory.\n[--text] Produce a text version of vulkaninfo output to stdout. This is\n the default output.\n[--html] Produce an html version of vulkaninfo output, saved as\n \"vulkaninfo.html\" in the directory in which the command\n is run.\n[-j, --json] Produce a json version of vulkaninfo output conforming to the Vulkan\n Profiles schema, saved as \n \"VP_VULKANINFO_[DEVICE_NAME]_[DRIVER_VERSION].json\"\n of the first gpu in the system.\n[-j=, --json=]\n For a multi-gpu system, a single gpu can be targeted by\n specifying the gpu-number associated with the gpu of \n interest. This number can be determined by running\n vulkaninfo without any options specified.\n[--show-all] Show everything (includes all the below options)\n[--show-tool-props] Show the active VkPhysicalDeviceToolPropertiesEXT that vulkaninfo finds.\n[--show-formats] Display the format properties of each physical device.\n Note: This only affects text output.\n[--show-promoted-structs] Include structs promoted to core in pNext Chains.\n[--show-video-props]\n Display the video profile info, video capabilities and\n video format properties of each video profile supported\n by each physical device.\n Note: This only affects text output which by default\n only contains the list of supported video profile names." moltenvk: + status: "emulator_bundled_runtime_active_reference_source_historical" reference_tag: "v1.4.2" reference_commit: "db66022459ffb663aa2b50f6b018bc2e124f5edf" selected_commit: null patch_series_sha256: null +diagnostic_runtime: + repository: "flashls1/tftmac-runtime" + commit: "c8aa26ebaa5b977965eb165ad8aac5c98408469f" + status: "isolated_non_comparable_diagnostic_eligibility_only" + normal_play_authority: "stock_build8" + +current_gameplay_capture: + id: "2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200" + storage: "private_local_only" + database_byte_count: 63897600 + database_sha256: "c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77" + graphics_run_duration: "42m27s" + active_experiment: "combat_latency_a" + tft_graphics: "High / 60 FPS / Performance Mode OFF" + automatic_logging: "verified_pid_layer_lifetime" + exact_layer_coverage_percent: 99.629 + frame_interval_count: 144364 + weighted_fps: 56.98 + fps_one_percent_low: 21.49 + p50_frame_interval_ms: 16.707 + p95_frame_interval_ms: 21.51 + p99_frame_interval_ms: 33.434 + max_frame_interval_ms: 2233.611 + jank_count: 6544 + jank_percent: 4.53 + severe_stall_count: 144 + missed_vsync_equivalent_count: 7644 + degradation_incident_count: 189 + effective_stack_receipt: "Unreal direct Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal" + angle_status: "conditional_not_assumed_for_TFT_main_rendering_path" + mac_presenter: "excluded_from_causal_candidates_context_only" + markers_and_battles: "optional_annotations_not_validity_or_causal_gates" + root_attribution: "UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE" + advanced_source_causal_logger: "planned" + generality: + status: "historical_reference_only_not_release_acceptance" gles_cts_tag: "opengl-es-cts-3.2.14.1" gles_cts_commit: "067e8832315e79817ede1c4863804e440f5d1c80" vulkan_cts_tag: "vulkan-cts-1.4.6.1" @@ -74,13 +166,14 @@ generality: resource_profiles: A: vcpu: 6 - ram_mb: 6144 + ram_mb: 5120 B: vcpu: 8 - ram_mb: 8192 - selected: null + ram_mb: 6144 + selected: A phase0_evidence: + status: "historical_source_research_only" authority_hashes: "ssot/AUTHORITY_INPUTS.sha256" policy: "ssot/phase0-policy.json" remediation_inventory: "ssot/phase0-remediation-inventory.md" diff --git a/ssot/TFTMAC_DIRECT_PLAY_CONTROL_BUILD.md b/ssot/TFTMAC_DIRECT_PLAY_CONTROL_BUILD.md index 1dfe8f4..7b2f5f9 100644 --- a/ssot/TFTMAC_DIRECT_PLAY_CONTROL_BUILD.md +++ b/ssot/TFTMAC_DIRECT_PLAY_CONTROL_BUILD.md @@ -1,11 +1,13 @@ # TFTMAC Direct-Play Control Build -**Status:** Execution authority for the fast control build +**Status:** SUPERSEDED HISTORICAL CONTROL-BUILD RECEIPT **Date:** 2026-08-28 **Project:** `TFTMAC` **Repository:** `flashls1/TFTMAC` **Purpose:** Build the smallest trustworthy TFT-on-Mac control product, launch the current official TFT client through Google Play/Riot authority, and capture enough synchronized telemetry to make subsequent performance work causal rather than speculative. +> This dated direct-play/Node control plan is preserved as evidence. It is not current launch authority. Current native authority is `ssot/runtime-authority.json` and `TFTMAC_GPU_RUNTIME_SSOT.md`; in particular, do not reuse this receipt's `5040/5592` service-context identity or 6144-MiB control value. + --- # 0. Fresh-chat execution directive @@ -183,8 +185,8 @@ Riot's own in-game content/update services Explicitly forbidden as production authority: -- Mactician's hosted TFT feed; -- Mactician's bundled TFT package; +- the retired donor's hosted TFT feed; +- the retired donor's bundled TFT package; - stale PBE package pins; - community APK mirrors; - public APK download sites; @@ -192,7 +194,7 @@ Explicitly forbidden as production authority: - re-signed Riot binaries; - patched Riot gameplay binaries. -Mactician is a donor/reference implementation only. +The retired implementation is donor/reference evidence only. ## 5.1 PackageStateManager @@ -338,7 +340,7 @@ The first official-package control should be as close to stock Google behavior a Use the known donor graphics configuration only to the minimum extent required to cross a real compatibility boundary. Every non-default graphics control must be captured in `runtime-state.json` and `renderer-state.json`. -Do not inherit Mactician's nonconformant GLES-version exposure into the product silently. If TFT requires it for this control, label it explicitly as a temporary compatibility adapter and keep the existing genuine-capability laboratory as the truth test. +Do not inherit the retired donor's nonconformant GLES-version exposure into the product silently. If TFT requires it for this control, label it explicitly as a temporary compatibility adapter and keep the existing genuine-capability laboratory as the truth test. --- @@ -372,7 +374,7 @@ No presentation system is allowed to reintroduce guest software video encoding. Use the strongest reusable open-source/donor product patterns already in the repository instead of re-inventing basic launcher UX. -Allowed Mactician donor concepts/code patterns under its applicable license include: +Allowed retired-donor concepts/code patterns under their applicable license include: - SwiftUI launcher structure; - install/readiness/running/failure states; @@ -992,7 +994,7 @@ The direct-play control is **PASS** only when all of these are true: 12. The logger starts before emulator/game launch and continues through shutdown. 13. Required runtime, package, renderer, clock, and frame-timing artifacts are captured. 14. A session artifact manifest is hashed. -15. No Mactician server/feed/bundled Riot package is required. +15. No retired-donor server/feed/bundled Riot package is required. 16. No giant AEMU source checkout is required for ordinary launch. 17. Poor FPS is allowed at this gate if the workload is playable enough to produce valid measurements. diff --git a/ssot/TFTMAC_ENGINEERING_MAP.sql b/ssot/TFTMAC_ENGINEERING_MAP.sql index 573a86f..49fcf45 100644 --- a/ssot/TFTMAC_ENGINEERING_MAP.sql +++ b/ssot/TFTMAC_ENGINEERING_MAP.sql @@ -263,7 +263,7 @@ INSERT OR REPLACE INTO components(id,name,kind,layer,ownership,status,purpose,no ('xcode','Xcode 26.6','toolchain','host_build','APPLE','PROVEN','Native compiler/toolchain authority','Build 17F113; selected explicitly.'), ('macos_sdk','macOS SDK 26.5','sdk','host_build','APPLE','PROVEN','Host SDK used for AEMU compilation','Located inside Xcode 26.6.'), ('tftmac_harness','TFTMAC build/runtime harness','owned_code','orchestration','TFTMAC','ACTIVE','Own compatibility, evidence, storage, build and runtime orchestration','This is the correct place to absorb stale upstream build-system assumptions.'), -('tftmac_shell','TFTMAC native macOS shell','owned_code','presentation','TFTMAC','AVAILABLE','Native single-window launcher/runtime UI','Existing SwiftUI/Mactician-derived foundation exists.'), +('tftmac_shell','TFTMAC native macOS shell','owned_code','presentation','TFTMAC','AVAILABLE','Native single-window launcher/runtime UI','Existing SwiftUI donor-derived foundation exists.'), ('android_guest','Android 17 Google Play ARM64 guest','guest_os','guest','GOOGLE','PROVEN','Official production Android guest','API 37; Google Play ARM64 ps16k image revision 6.'), ('google_play','Google Play','package_authority','guest','GOOGLE','PROVEN','Authoritative TFT acquisition/update path','Production app authority; avoid third-party APK mirrors.'), ('tft_android','Teamfight Tactics Android client','workload','application','RIOT','AVAILABLE','Primary production workload','Current live package is com.riotgames.league.teamfighttactics. Current evidence says Riot native Android runtime, not Unreal.'), @@ -659,17 +659,17 @@ INSERT OR REPLACE INTO map_meta(key, value) VALUES ('document_policy','Current authority, historical donor evidence, stale reports, and legacy claims must be distinguishable in queries; no whole-document trust by filename alone.'); INSERT OR REPLACE INTO components(id,name,kind,layer,ownership,status,purpose,notes) VALUES -('mactician_legacy','Mactician 1.x launcher/runtime','legacy_product','donor','UPSTREAM','AVAILABLE','Known working Apple-Silicon TFT PBE donor implementation','Useful compatibility evidence must be harvested field-by-field; legacy architecture is not v2 authority.'), -('android36_guest','Android 16 / API 36 ARM64 guests','guest_os','guest','GOOGLE','AVAILABLE','Historical working guest family for Mactician/TFTMAC experiments','Includes Google APIs userdebug and Google Play variants; not current v2 production authority.'), +('legacy_launcher_donor','Retired launcher/runtime donor','legacy_product','donor','UPSTREAM','AVAILABLE','Known working Apple-Silicon TFT PBE donor implementation','Useful compatibility evidence must be harvested field-by-field; legacy architecture is not v2 authority.'), +('android36_guest','Android 16 / API 36 ARM64 guests','guest_os','guest','GOOGLE','AVAILABLE','Historical working guest family for retired-donor/TFTMAC experiments','Includes Google APIs userdebug and Google Play variants; not current v2 production authority.'), ('gfxstream_main_dev_donor','emu-main-dev gfxstream donor build','gpu_transport','donor','UPSTREAM','AVAILABLE','Historical source-build evidence for native GLES/gfxstream experiments','Revision a9184fd was built successfully with local warning-compatibility allowances; not co-authoritative with locked emu-master-dev.'), ('deployment_target','macOS deployment target','build_contract','host_build','TFTMAC','ACTIVE','Defines minimum macOS API availability for produced native binaries','Must be selected independently from host OS and SDK version.'); INSERT OR REPLACE INTO evidence(id,observed_at,kind,source_path,source_sha256,statement,confidence,notes) VALUES ('ev_host_26_6_2','2026-08-28T04:07:47.479Z','host_preflight','ssot/host-preflight.json','c32ab07bdc18afa5ea261c4ab702f75a087319bce6f8cfe0efff9a3f898e5b97','Current host is Apple M4 Mac mini Mac16,10, arm64, 16 GB RAM, macOS 26.6.2 build 25G83; selected Xcode is 26.6 build 17F113 with macOS SDK 26.5.','DIRECT','This is current machine truth and must qualify every host-build compatibility claim.'), -('ev_mactician_target12','2026-08-27T01:46:26.991Z','source_build_contract','scripts/build-mactician.command','21932700a90aecf88a80e4d68fe0cdc748050aa39c0571bf2673a91228ff9c74','Mactician production Swift and emulator-host binaries explicitly target arm64-apple-macosx12.0; launcher Info.plist also declares minimum macOS 12.0.','DIRECT','This is strong project-local evidence that 12.0 is a real compatibility target, not an invented number.'), +('ev_legacy_target12','2026-08-27T01:46:26.991Z','source_build_contract','historical:removed-build-script','21932700a90aecf88a80e4d68fe0cdc748050aa39c0571bf2673a91228ff9c74','Retired donor production Swift and emulator-host binaries explicitly targeted arm64-apple-macosx12.0; historical launcher metadata also declared minimum macOS 12.0.','DIRECT','This is strong project-local evidence that 12.0 is a real compatibility target, not an invented number.'), ('ev_phase1_target1014_fail','2026-08-28T05:10:20.931Z','compiler_failure','/Volumes/MAC MINI M4/TFTMAC/Build/logs/phase1-build.stderr.log',NULL,'Phase 1 advanced through Ninja step 885/9854 and failed because AEMU host tooling set macOS deployment target 10.14 while source uses std::filesystem APIs marked available from macOS 10.15.','DIRECT','The current build worker is FAILED, not RUNNING. The failure is host build-contract compatibility, not a graphics capability failure.'), ('ev_aemu_legacy_target1014','2026-08-28T05:19:00Z','source_audit','external/qemu/android/scripts/unix/gen-android-sdk-toolchain.sh',NULL,'Locked AEMU helper carries OSX_DEPLOYMENT_TARGET=10.14 / OSX_REQUIRED=10.14 despite current upstream macOS development guidance requiring SDK 10.15 or later.','DIRECT','Treat this helper value as stale upstream build plumbing.'), -('ev_mactician_stack','2026-08-27T01:46:26.986Z','release_manifest','launcher/Resources/release-manifest.json','ec5b31e1d0fc6c05de087ea6bcd6dc4c9acfebb575b2d4fdcfe45d8a98de892c','Mactician 1.0.4 pins Platform Tools 36.0.2, Android Emulator 37.1.11 build 15917651, Android 36 Google APIs ARM64 system image r07, and TFT PBE 18.1-5212127.','DIRECT','Exact archived donor component set.'), +('ev_legacy_stack','2026-08-27T01:46:26.986Z','release_manifest','launcher/Resources/release-manifest.json','ec5b31e1d0fc6c05de087ea6bcd6dc4c9acfebb575b2d4fdcfe45d8a98de892c','Retired donor release evidence pins Platform Tools 36.0.2, Android Emulator 37.1.11 build 15917651, Android 36 Google APIs ARM64 system image r07, and TFT PBE 18.1-5212127.','DIRECT','Exact archived donor component set.'), ('ev_benchmark_m1max_stack','2026-08-27T01:46:26.981Z','benchmark_document','docs/benchmarks.md','526507229e293e92155fc3ba588bc48d6a149cc7c164d24e5d10ac33755d7965','Historical benchmark environment used M1 Max, macOS 26.6 build 25G72, Emulator 37.1.11 build 15917651, Android 36 ARM64 Google APIs userdebug, with ANGLE->Vulkan->gfxstream/MoltenVK->Metal.','DIRECT','This proves a specific older guest/runtime family ran extensively on macOS 26.x, but not on this exact M4 host.'), ('ev_native_gles_main_dev','2026-08-27T01:46:26.982Z','historical_source_build','docs/native-gles-transport-experiment.md','9b92313317a5eebd1900f5008c9db7018f467b20b6a6029dfc33523f240c650c','A historical emu-main-dev gfxstream host backend at revision a9184fd built successfully on the Mac after two AppleClang warning-compatibility adjustments; standalone backend was not a production drop-in.','DIRECT','Useful donor evidence only; do not mix branch authority.'), ('ev_live_tft_native','2026-08-27T01:46:35.222Z','package_architecture','docs/TFTMAC_GRAPHICS_ARCHITECTURE.md','06b7f5bf6ed6a84e7be2f3f296563a0fa9b436ec462eca0700d5bd8483490c59','Live TFT 16.16.8042660 base APK SHA 9ed691... launches RiotNativeActivity, contains libleagueoflegends.so, and showed no Unreal runtime markers.','DIRECT','Current workload engine evidence; separate from legacy PBE Unreal experiments.'); @@ -694,9 +694,9 @@ INSERT OR REPLACE INTO source_documents(id,path,sha256,role,authority_rank,tempo ('doc_phase0_remediation','ssot/phase0-remediation-inventory.md','608c258b56a57ab4775100cd2e7b333db183df86464e09c9c4419892623b2622','historical remediation log',30,'STALE','Pre-Phase-0 blocker inventory','2026-08-28T05:19:03.867Z',1,'Still lists blockers already resolved; never use as current status.'), ('doc_tftmac_legacy','TFTMAC.md','a93a4c96a37b89033b627520bdbe848cb39b68a12d257caec31ca77ea703e37c','legacy live-profile claim',35,'CURRENT_WITH_LEGACY_CONTENT','Pre-v2 live TFT architecture claims','2026-08-28T05:19:03.867Z',1,'Contains Android 16/API36 and ES3.2 property-era claims; candidate evidence only.'), ('doc_graphics_legacy','docs/TFTMAC_GRAPHICS_ARCHITECTURE.md','06b7f5bf6ed6a84e7be2f3f296563a0fa9b436ec462eca0700d5bd8483490c59','live engine evidence + legacy adapter design',45,'CURRENT_WITH_LEGACY_CONTENT','Live TFT engine identification and pre-v2 adapter architecture','2026-08-28T05:19:03.867Z',1,'Live package inspection remains useful; architecture is not v2 authority.'), -('doc_readme_mactician','README.md','810e1d7117f4e9f6253d680bef270958f18645d7e275dcf9384c7db1a8cd6a13','Mactician donor overview',30,'DONOR','Legacy product requirements/version pins/performance summary','2026-08-28T05:19:03.867Z',1,'Do not treat nonconformant GLES3.2-era behavior as v2 acceptance.'), -('doc_mactician_build','scripts/build-mactician.command','21932700a90aecf88a80e4d68fe0cdc748050aa39c0571bf2673a91228ff9c74','legacy build implementation evidence',50,'DONOR','macOS 12 deployment, signing, packaging','2026-08-28T05:19:03.867Z',0,'Strong donor for native macOS compatibility target.'), -('doc_mactician_manifest','launcher/Resources/release-manifest.json','ec5b31e1d0fc6c05de087ea6bcd6dc4c9acfebb575b2d4fdcfe45d8a98de892c','legacy exact version manifest',55,'DONOR','Mactician Android/emulator/game component pins','2026-08-28T05:19:03.867Z',0,'Exact archived component versions.'), +('doc_readme_legacy','README.md','810e1d7117f4e9f6253d680bef270958f18645d7e275dcf9384c7db1a8cd6a13','Retired donor overview',30,'DONOR','Legacy product requirements/version pins/performance summary','2026-08-28T05:19:03.867Z',1,'Do not treat nonconformant GLES3.2-era behavior as v2 acceptance.'), +('doc_legacy_build','historical:removed-build-script','21932700a90aecf88a80e4d68fe0cdc748050aa39c0571bf2673a91228ff9c74','legacy build implementation evidence',50,'DONOR','macOS 12 deployment, signing, packaging','2026-08-28T05:19:03.867Z',0,'Strong donor for native macOS compatibility target.'), +('doc_legacy_manifest','launcher/Resources/release-manifest.json','ec5b31e1d0fc6c05de087ea6bcd6dc4c9acfebb575b2d4fdcfe45d8a98de892c','legacy exact version manifest',55,'DONOR','Retired donor Android/emulator/game component pins','2026-08-28T05:19:03.867Z',0,'Exact archived component versions.'), ('doc_benchmarks','docs/benchmarks.md','526507229e293e92155fc3ba588bc48d6a149cc7c164d24e5d10ac33755d7965','historical performance evidence',55,'HISTORICAL','M1 Max / Android36 / Emulator37.1.11 performance experiments','2026-08-28T05:19:03.867Z',0,'Scene-specific results; not current acceptance thresholds.'), ('doc_native_gles','docs/native-gles-transport-experiment.md','9b92313317a5eebd1900f5008c9db7018f467b20b6a6029dfc33523f240c650c','historical graphics capability research',65,'HISTORICAL','ANGLE/gfxstream/native GLES source and runtime experiments','2026-08-28T05:19:03.867Z',0,'Contains valuable negative evidence and emu-main-dev donor build results.'), ('doc_research_log','docs/research-log.md','e24b935ebcb6235db53130357121a26e724da0eec9186cbc90b6a0a1a6967dce','historical experiment chronology',50,'HISTORICAL','Compatibility/performance/rejected experiments','2026-08-28T05:19:03.867Z',0,'Useful for avoiding repeated failed experiments.'); @@ -709,16 +709,16 @@ INSERT OR REPLACE INTO version_catalog(id,component_id,product,version_label,rel ('vc_xcode164','xcode','Xcode','16.4',NULL,'stable','arm64','15.3','26.1.x',NULL,NULL,'15.5','10.13','15','Apple support matrix','ext_apple_xcode_matrix','OFFICIAL_DOCUMENTED','CANDIDATE','INCOMPATIBLE','2026-08-28T05:20:00Z','Ships SDK 15.5 and has older deployment range.','Apple support matrix stops host support at Tahoe 26.1.x; current host is 26.6.2. SDK 15.5 also exceeds locked helper allowlist ending at 15.2.','Rejected on current host unless isolated older build environment'), ('vc_xcode162','xcode','Xcode','16.2',NULL,'stable','arm64','14.5','15.x',NULL,NULL,'15.2','10.13','15','Apple support matrix','ext_apple_xcode_matrix','OFFICIAL_DOCUMENTED','CANDIDATE','INCOMPATIBLE','2026-08-28T05:20:00Z','Ships SDK 15.2, which matches the locked helper allowlist.','Apple does not support Xcode 16.2 on macOS 26.6.2. Could be useful only on a separate older macOS build environment.','Historical helper-compatible compiler candidate'), ('vc_xcode134','xcode','Xcode','13.4 / 13F17a',NULL,'historical','arm64',NULL,NULL,NULL,NULL,'12.3',NULL,NULL,'Google AEMU guide','ext_aemu_darwin_dev','OFFICIAL_DOCUMENTED','HISTORICAL','CLAIMED','2026-08-28T05:20:00Z','Current AEMU macOS guide names Xcode 13.4 with SDK 12.3 as a recommended historical build stack.','Not supported as a current-host installation claim; would require a compatible separate build OS.','Historical upstream reference'), -('vc_emulator37111','aemu','Android Emulator','37.1.11 / build 15917651','2026-07-30','stable','darwin-aarch64',NULL,NULL,36,37,NULL,NULL,NULL,'Google release notes + project manifest','ext_emulator_releases','PROJECT_ATTESTED','CURRENT_CONTROL','PROVEN','2026-08-28T05:20:00Z','Pinned by Mactician; benchmarked on macOS 26.6 M1 Max; Google 37.1.11 release adds Vulkan extensions required for API 37.','Stock graphics capability for genuine conformant ES3.2 on current M4/API37 is not yet proven.','High-value stock-control candidate'), +('vc_emulator37111','aemu','Android Emulator','37.1.11 / build 15917651','2026-07-30','stable','darwin-aarch64',NULL,NULL,36,37,NULL,NULL,NULL,'Google release notes + project manifest','ext_emulator_releases','PROJECT_ATTESTED','CURRENT_CONTROL','PROVEN','2026-08-28T05:20:00Z','Pinned by retired donor; benchmarked on macOS 26.6 M1 Max; Google 37.1.11 release adds Vulkan extensions required for API 37.','Stock graphics capability for genuine conformant ES3.2 on current M4/API37 is not yet proven.','High-value stock-control candidate'), ('vc_emulator36611','aemu','Android Emulator','36.6.11','2026-06-02','stable','darwin-aarch64',NULL,NULL,37,37,NULL,NULL,NULL,'Google release notes','ext_emulator_releases','OFFICIAL_DOCUMENTED','CANDIDATE','CLAIMED','2026-08-28T05:20:00Z','Explicitly supports API37 memory requirements and includes a macOS 26.3 Hypervisor cleanup fix.','No project-local TFT run yet; exact API37 Vulkan extension coverage versus 37.1.11 must be tested.','Older stable emulator candidate'), ('vc_emulator36510','aemu','Android Emulator','36.5.10','2026-04-02','stable','darwin-aarch64',NULL,NULL,NULL,NULL,NULL,NULL,NULL,'Google release notes','ext_emulator_releases','OFFICIAL_DOCUMENTED','CANDIDATE','CLAIMED','2026-08-28T05:35:30Z','Introduces the newer multi-device networking stack and includes a macOS crashpad high-CPU fix.','No official release-note evidence that this release adds the Vulkan improvements previously attributed to it; it also predates 37.1.11 API37 Vulkan-extension additions.','Older general-runtime control candidate'), ('vc_emulator3649','aemu','Android Emulator','36.4.9','2026-02-10','stable','darwin-aarch64',NULL,NULL,NULL,NULL,NULL,NULL,NULL,'Google release notes','ext_emulator_releases','OFFICIAL_DOCUMENTED','CANDIDATE','CLAIMED','2026-08-28T05:35:30Z','Updates the bundled Vulkan loader, fixes Vulkan backend invalid-use cases, adds SkiaVk graphics-queue-emulation support and VulkanNativeSwapchain composition support; release notes describe macOS Vulkan support as experimental.','Older than API37-specific 36.6/37.1 changes; must be tested with the exact current guest and workload before any promotion.','Older Vulkan-focused emulator candidate'), -('vc_platformtools3602','android36_guest','Android Platform Tools','36.0.2',NULL,'stable','darwin',NULL,NULL,NULL,NULL,NULL,NULL,NULL,'Mactician manifest','launcher/Resources/release-manifest.json','PROJECT_ATTESTED','KNOWN_GOOD_DONOR','PROVEN','2026-08-27T01:46:26.986Z','Exact Mactician 1.0.4 platform-tools pin.','Not current v2 control revision.','Legacy donor'), -('vc_android36_r07','android36_guest','Android system image','Android 36 Google APIs ARM64 r07',NULL,'stable','arm64',NULL,NULL,36,36,NULL,NULL,NULL,'Mactician manifest','launcher/Resources/release-manifest.json','PROJECT_ATTESTED','KNOWN_GOOD_DONOR','PROVEN','2026-08-27T01:46:26.986Z','Exact Mactician system-image archive pin.','No Google Play in this exact manifest entry; rootable/Google Play variants were separate.','Legacy guest candidate'), +('vc_platformtools3602','android36_guest','Android Platform Tools','36.0.2',NULL,'stable','darwin',NULL,NULL,NULL,NULL,NULL,NULL,NULL,'retired-donor manifest','launcher/Resources/release-manifest.json','PROJECT_ATTESTED','KNOWN_GOOD_DONOR','PROVEN','2026-08-27T01:46:26.986Z','Exact retired-donor platform-tools pin.','Not current v2 control revision.','Legacy donor'), +('vc_android36_r07','android36_guest','Android system image','Android 36 Google APIs ARM64 r07',NULL,'stable','arm64',NULL,NULL,36,36,NULL,NULL,NULL,'retired-donor manifest','launcher/Resources/release-manifest.json','PROJECT_ATTESTED','KNOWN_GOOD_DONOR','PROVEN','2026-08-27T01:46:26.986Z','Exact retired-donor system-image archive pin.','No Google Play in this exact manifest entry; rootable/Google Play variants were separate.','Legacy guest candidate'), ('vc_android37_r6','android_guest','Android system image','Android 17 / API37 Google Play ps16k ARM64 rev6',NULL,'stable','arm64',NULL,NULL,37,37,NULL,NULL,NULL,'STACK.lock','ssot/STACK.lock.yaml','DIRECT_OBSERVED','CURRENT_AUTHORITY','PROVEN','2026-08-28T04:32:56.368Z','Current frozen production guest image.','Runtime graphics acceptance not yet completed.','Current guest'), -('vc_mactician104','mactician_legacy','Mactician','1.0.4 build 40',NULL,'release','arm64','12.0',NULL,36,36,NULL,'12.0',NULL,'project source','README.md + launcher/Info.plist','PROJECT_ATTESTED','KNOWN_GOOD_DONOR','PROVEN','2026-08-27T01:46:26.986Z','Legacy Apple-Silicon launcher/runtime with minimum macOS 12.0 and exact Emulator37.1.11/Android36 pins.','Experimental/best-effort; PBE workload; historical nonconformant ES3.2 exposure was used and cannot satisfy v2 graphics truth.','Native shell/build/runtime donor'), +('vc_legacy_launcher104','legacy_launcher_donor','Retired donor launcher','1.0.4 build 40',NULL,'release','arm64','12.0',NULL,36,36,NULL,'12.0',NULL,'project source','README.md + launcher/Info.plist','PROJECT_ATTESTED','KNOWN_GOOD_DONOR','PROVEN','2026-08-27T01:46:26.986Z','Legacy Apple-Silicon launcher/runtime with minimum macOS 12.0 and exact Emulator37.1.11/Android36 pins.','Experimental/best-effort; PBE workload; historical nonconformant ES3.2 exposure was used and cannot satisfy v2 graphics truth.','Native shell/build/runtime donor'), ('vc_tft_live1616','tft_android','TFT Android live','16.16.8042660',NULL,'Google Play','arm64',NULL,NULL,NULL,NULL,NULL,NULL,NULL,'package inspection','docs/TFTMAC_GRAPHICS_ARCHITECTURE.md','PROJECT_ATTESTED','CURRENT_CONTROL','PROVEN','2026-08-27T01:46:35.222Z','Current inspected live package uses RiotNativeActivity/libleagueoflegends.so with no Unreal markers.','Compatibility with v2 API37 source-built runtime still requires vertical-slice proof.','Current workload'), -('vc_pbe1815212127','tft_android','TFT PBE','18.1-5212127',NULL,'PBE','arm64',NULL,NULL,36,36,NULL,NULL,NULL,'Mactician manifest','launcher/Resources/release-manifest.json','PROJECT_ATTESTED','HISTORICAL','PROVEN','2026-08-27T01:46:26.986Z','Exact historical PBE workload used for Mactician 1.0.4 and benchmarks.','Not the current live production client.','Historical graphics/workload donor'), +('vc_pbe1815212127','tft_android','TFT PBE','18.1-5212127',NULL,'PBE','arm64',NULL,NULL,36,36,NULL,NULL,NULL,'retired-donor manifest','launcher/Resources/release-manifest.json','PROJECT_ATTESTED','HISTORICAL','PROVEN','2026-08-27T01:46:26.986Z','Exact historical PBE workload used for retired-donor benchmarks.','Not the current live production client.','Historical graphics/workload donor'), ('vc_gfx_main_a9184fd','gfxstream_main_dev_donor','gfxstream host backend','emu-main-dev a9184fd',NULL,'development','darwin-aarch64',NULL,NULL,NULL,NULL,NULL,NULL,NULL,'historical project source build','docs/native-gles-transport-experiment.md','HISTORICAL_PROJECT','KNOWN_GOOD_DONOR','PARTIAL','2026-08-27T01:46:26.982Z','Standalone gfxstream_backend built successfully after two AppleClang warning allowances.','Export surface was not production drop-in; branch is not current authority.','Source-build donor'), ('vc_aemu_locked','aemu','AEMU/QEMU source','emu-master-dev qemu ae9d18d2',NULL,'development','darwin-aarch64',NULL,NULL,37,37,NULL,'10.14',NULL,'resolved manifest + source','ssot/STACK.lock.yaml','DIRECT_OBSERVED','CURRENT_AUTHORITY','PARTIAL','2026-08-28T05:19:03.867Z','Locked source configured fully and compiled through Ninja step 885/9854 with TFTMAC host adapters.','Upstream helper deployment target 10.14 is stale/incompatible with current source+Xcode; build not complete.','Current source authority'); @@ -726,27 +726,27 @@ INSERT OR REPLACE INTO compatibility_claims(id,version_id,environment_id,subject ('cc_current_host','vc_xcode266','env_current_m4','Xcode 26.6','runs on','macOS 26.6.2','OBSERVED','SUPPORTS','ev_host_26_6_2','ext_apple_xcode_matrix','doc_stack','2026-08-28T04:07:47.479Z','2026-08-28T05:20:00Z',NULL,'Installed and selected successfully.'), ('cc_xcode266_deployment','vc_xcode266','env_current_m4','Xcode 26.6','supports macOS deployment target','11 through 26.5','OFFICIAL_DOCUMENTED','SUPPORTS',NULL,'ext_apple_xcode_matrix',NULL,NULL,'2026-08-28T05:20:00Z',NULL,'Therefore 10.14 is outside the officially supported deployment-target range.'), ('cc_aemu_target1014','vc_aemu_locked','env_current_m4','Locked AEMU helper','forces deployment target','10.14','OBSERVED','BLOCKS','ev_phase1_target1014_fail','ext_aemu_helper',NULL,'2026-08-28T05:10:20.931Z','2026-08-28T05:19:03.867Z',NULL,'Directly caused std::filesystem availability compile errors under SDK26.5.'), -('cc_mactician_target12','vc_mactician104','env_current_m4','Mactician donor build','targets minimum macOS','12.0','OBSERVED','SUPPORTS','ev_mactician_target12',NULL,'doc_mactician_build','2026-08-27T01:46:26.991Z','2026-08-28T05:19:03.867Z',NULL,'12.0 is inside Xcode26.6 supported deployment range and satisfies std::filesystem availability.'), +('cc_legacy_target12','vc_legacy_launcher104','env_current_m4','Retired donor build','targets minimum macOS','12.0','OBSERVED','SUPPORTS','ev_legacy_target12',NULL,'doc_legacy_build','2026-08-27T01:46:26.991Z','2026-08-28T05:19:03.867Z',NULL,'12.0 is inside Xcode26.6 supported deployment range and satisfies std::filesystem availability.'), ('cc_xcode162_helper','vc_xcode162','env_current_m4','Xcode 16.2 SDK15.2','matches locked AEMU helper SDK allowlist','yes','INFERRED','BLOCKS',NULL,'ext_apple_xcode_matrix',NULL,NULL,'2026-08-28T05:20:00Z',NULL,'SDK matches helper but Apple does not support Xcode16.2 on current macOS26.6.2, so it is not a direct current-host solution.'), ('cc_xcode263_helper','vc_xcode263','env_current_m4','Xcode 26.3 SDK26.2','matches locked AEMU helper SDK allowlist','no','INFERRED','CONDITIONAL',NULL,'ext_apple_xcode_matrix',NULL,NULL,'2026-08-28T05:20:00Z',NULL,'Can run on current host but still needs adapter for stale AEMU helper.'), ('cc_emulator37111_mac26','vc_emulator37111','env_benchmark_m1max','Emulator 37.1.11','runs extensively on','macOS 26.6 M1 Max + Android36 userdebug','OBSERVED','SUPPORTS','ev_benchmark_m1max_stack',NULL,'doc_benchmarks','2026-08-27T01:46:26.981Z','2026-08-28T05:19:03.867Z',NULL,'Historical project benchmark evidence, not a guarantee for every guest or M4.'), ('cc_emulator37111_api37','vc_emulator37111',NULL,'Emulator 37.1.11','adds Vulkan extensions required for','API37 system images','OFFICIAL_DOCUMENTED','SUPPORTS',NULL,'ext_emulator_releases',NULL,'2026-07-30','2026-08-28T05:20:00Z',NULL,'High-value reason to keep 37.1.11 as stock control.'), ('cc_emulator36611_api37','vc_emulator36611',NULL,'Emulator 36.6.11','supports minimum VM memory behavior for','API37','OFFICIAL_DOCUMENTED','SUPPORTS',NULL,'ext_emulator_releases',NULL,'2026-06-02','2026-08-28T05:20:00Z',NULL,'Candidate older stable control; graphics extension parity not assumed.'), ('cc_emulator3649_vulkan','vc_emulator3649',NULL,'Emulator 36.4.9','documents Vulkan improvements including','loader/backend/SkiaVk/VulkanNativeSwapchain','OFFICIAL_DOCUMENTED','SUPPORTS',NULL,'ext_emulator_releases',NULL,'2026-02-10','2026-08-28T05:35:30Z',NULL,'Useful older graphics-focused control candidate, but macOS Vulkan was still described as experimental and API37 parity is not assumed.'), -('cc_legacy_es32','vc_mactician104','env_benchmark_m1max','Historical Mactician graphics path','claimed ES3.2 using','exposeNonConformantExtensionsAndVersions','PROJECT_DOCUMENTED','BLOCKS',NULL,NULL,'doc_research_log',NULL,'2026-08-28T05:19:03.867Z',NULL,'Worked as historical workload-enablement evidence but is forbidden as v2 conformance proof.'), +('cc_legacy_es32','vc_legacy_launcher104','env_benchmark_m1max','Historical retired-donor graphics path','claimed ES3.2 using','exposeNonConformantExtensionsAndVersions','PROJECT_DOCUMENTED','BLOCKS',NULL,NULL,'doc_research_log',NULL,'2026-08-28T05:19:03.867Z',NULL,'Worked as historical workload-enablement evidence but is forbidden as v2 conformance proof.'), ('cc_live_api36_claim','vc_tft_live1616',NULL,'Legacy TFTMAC live profile','claims live TFT runs on','Android16/API36 stock/high-end tablet runtime','PROJECT_DOCUMENTED','CONDITIONAL','ev_live_tft_native',NULL,'doc_tftmac_legacy','2026-08-27T01:46:35.222Z','2026-08-28T05:19:03.867Z',NULL,'Useful older-stack candidate claim, but current v2 preflight explicitly quarantines legacy claims until revalidated.'); INSERT OR REPLACE INTO deployment_target_candidates(target_version,candidate_state,xcode26_6_supported,satisfies_std_filesystem,matches_existing_product_minimum,compatibility_score,source_basis,rationale) VALUES ('10.14','REJECTED',0,0,0,0,'AEMU stale helper + Apple Xcode26.6 matrix + direct compile failure','Outside Xcode26.6 supported deployment range and directly fails current AEMU std::filesystem compilation.'), ('10.15','REJECTED',0,1,0,25,'std::filesystem availability + Apple Xcode26.6 matrix','Meets std::filesystem introduction point but is still below Xcode26.6 official minimum deployment target 11.'), ('11.0','VIABLE',1,1,0,75,'Apple Xcode26.6 support matrix','Technically supported by current Xcode and source APIs, but lower than the project’s already-proven macOS12 minimum and therefore adds compatibility surface without proven product value.'), -('12.0','RECOMMENDED',1,1,1,95,'Apple Xcode26.6 support matrix + Mactician build/Info.plist','Supported by current Xcode, satisfies source APIs, and matches a proven Apple-Silicon product deployment target already used throughout this repository.'), +('12.0','RECOMMENDED',1,1,1,95,'Apple Xcode26.6 support matrix + retired-donor build metadata','Supported by current Xcode, satisfies source APIs, and matches a proven Apple-Silicon product deployment target already used throughout this repository.'), ('15.0','VIABLE',1,1,0,70,'Apple Xcode26.6 support matrix','Modern and supported but unnecessarily narrows product compatibility relative to proven macOS12 target.'), ('26.5','CONDITIONAL',1,1,0,45,'Apple Xcode26.6 SDK/deployment matrix','Would minimize availability ambiguity but would restrict produced binaries to the newest OS family and discard useful Apple-Silicon compatibility for no current evidence-based benefit.'); INSERT OR REPLACE INTO stack_profiles(id,name,purpose,status,confidence,environment_id,workload,exact_result,limitations,next_use) VALUES ('stack_v2_current','TFTMAC v2 frozen API37 source stack','Current architecture authority','CURRENT_AUTHORITY','DIRECT','env_current_m4','Current live TFT','Phase0 PASS; locked AEMU fully configures and compiles to step 885/9854 before host deployment-target mismatch.','Phase1 build not complete; graphics/runtime probes not yet complete.','Test deployment target 12.0 through existing detached build harness; then resume smallest Phase1 proof.'), -('stack_mactician104','Mactician 1.0.4 Android36 / Emulator37.1.11','Legacy known-good donor','KNOWN_GOOD_DONOR','DIRECT',NULL,'TFT PBE 18.1-5212127','Native SwiftUI launcher, exact pinned SDK/emulator/system image, extensive game/runtime experiments.','Historical PBE and nonconformant ES32 workaround; not v2 conformance.','Harvest packaging, macOS12 target, runtime state machine, Emulator37.1.11 control behavior.'), +('stack_legacy_launcher104','Retired donor Android36 / Emulator37.1.11','Legacy known-good donor','KNOWN_GOOD_DONOR','DIRECT',NULL,'TFT PBE 18.1-5212127','Native SwiftUI launcher, exact pinned SDK/emulator/system image, extensive game/runtime experiments.','Historical PBE and nonconformant ES32 workaround; not v2 conformance.','Harvest packaging, macOS12 target, runtime state machine, Emulator37.1.11 control behavior.'), ('stack_m1max_benchmark','M1 Max Android36 userdebug performance stack','Historical performance donor','HISTORICAL','DIRECT','env_benchmark_m1max','TFT PBE','Extensive fixed-stage benchmark evidence on Emulator37.1.11 / Android36 / ANGLE-Vulkan-gfxstream-MoltenVK-Metal.','Different hardware and PBE workload; many results are scene-specific.','Use only for causal/relative graphics insights and older-version compatibility candidates.'), ('stack_stock37111_api37','Stock Emulator37.1.11 + current API37 Play image','Low-maintenance control candidate','CANDIDATE','STRONG','env_current_m4','Current live TFT','Official release explicitly adds Vulkan extensions required for API37; current guest is already frozen.','Genuine ES3.2/GuestAngle capability on current M4 not yet measured.','Run permanent host/guest Vulkan and GLES probes before source-patching AEMU graphics.'), ('stack_stock36611_api37','Stock Emulator36.6.11 + API37','Older stable control candidate','CANDIDATE','STRONG','env_current_m4','Capability probes first','Official API37 memory behavior; macOS26.3 Hypervisor fix indicates active macOS26 support work.','May lack Vulkan additions explicitly delivered in 37.1.11.','Only test if 37.1.11 behavior regresses or source build remains disproportionately costly.'), @@ -758,11 +758,11 @@ INSERT OR REPLACE INTO stack_profile_members(stack_id,version_id,role,required) ('stack_v2_current','vc_android37_r6','guest',1), ('stack_v2_current','vc_aemu_locked','source runtime',1), ('stack_v2_current','vc_tft_live1616','workload',1), -('stack_mactician104','vc_mactician104','launcher/runtime product',1), -('stack_mactician104','vc_emulator37111','emulator',1), -('stack_mactician104','vc_platformtools3602','platform tools',1), -('stack_mactician104','vc_android36_r07','guest image',1), -('stack_mactician104','vc_pbe1815212127','workload',1), +('stack_legacy_launcher104','vc_legacy_launcher104','launcher/runtime product',1), +('stack_legacy_launcher104','vc_emulator37111','emulator',1), +('stack_legacy_launcher104','vc_platformtools3602','platform tools',1), +('stack_legacy_launcher104','vc_android36_r07','guest image',1), +('stack_legacy_launcher104','vc_pbe1815212127','workload',1), ('stack_m1max_benchmark','vc_emulator37111','emulator',1), ('stack_m1max_benchmark','vc_pbe1815212127','workload',1), ('stack_stock37111_api37','vc_emulator37111','stock emulator',1), @@ -789,7 +789,7 @@ INSERT OR REPLACE INTO unknowns(id,question,owning_layer,blocking,next_probe,sta INSERT OR REPLACE INTO decisions(id,decided_at,decision,rationale,state,evidence_id,supersedes) VALUES ('dec_separate_host_sdk_target','2026-08-28T05:19:03.867Z','Model host macOS, Xcode, SDK, and deployment target as four independent compatibility dimensions.','The current host was known but stale AEMU deployment-target assumptions still caused a real compile failure.','ACTIVE','ev_phase1_target1014_fail',NULL), -('dec_target12_first','2026-08-28T05:20:00Z','Use macOS 12.0 as the first deployment-target candidate for Phase1 parity testing; do not set target to current host version by default.','12.0 is inside Xcode26.6 supported range, satisfies std::filesystem availability, and is already proven throughout the existing Apple-Silicon Mactician product build.','ACTIVE','ev_mactician_target12',NULL), +('dec_target12_first','2026-08-28T05:20:00Z','Use macOS 12.0 as the first deployment-target candidate for Phase1 parity testing; do not set target to current host version by default.','12.0 is inside Xcode26.6 supported range, satisfies std::filesystem availability, and is already proven throughout the historical Apple-Silicon donor build.','ACTIVE','ev_legacy_target12',NULL), ('dec_version_matrix','2026-08-28T05:20:00Z','Maintain older software versions as explicit candidates with provenance, limitations, and revalidation state.','Older stable combinations may be simpler and already compatible; newest-version bias is not an engineering requirement.','ACTIVE',NULL,NULL); INSERT OR REPLACE INTO constraints(id,category,statement,severity,mutable,rationale) VALUES @@ -885,9 +885,9 @@ UPDATE field_metadata SET unit='0-100 score', semantic_role='compatibility prior INSERT INTO update_log(observed_at,subject,change_summary,evidence_id) VALUES ('2026-08-28T05:19:03.867Z','Phase 1 truth correction','Reconciled detached build: no longer running; failed at Ninja step 885/9854 on macOS deployment target 10.14 versus std::filesystem availability.','ev_phase1_target1014_fail'), ('2026-08-28T05:20:00Z','Host compatibility model','Separated current host macOS 26.6.2, Xcode 26.6, SDK 26.5 and deployment target into independent compatibility dimensions.','ev_host_26_6_2'), -('2026-08-28T05:20:00Z','Version strategy','Added current/older Xcode, Emulator, Android guest, Mactician and historical source-build versions with explicit provenance and limitations.',NULL), +('2026-08-28T05:20:00Z','Version strategy','Added current/older Xcode, Emulator, Android guest, retired-donor and historical source-build versions with explicit provenance and limitations.',NULL), ('2026-08-28T05:20:00Z','Document truth model','Classified current authority, legacy donor, historical evidence and stale documents so obsolete claims cannot masquerade as current state.',NULL), -('2026-08-28T05:20:00Z','Deployment target','Promoted macOS 12.0 as first Phase1 parity candidate; rejected inherited 10.14 and unsupported 10.15 targets.','ev_mactician_target12'); +('2026-08-28T05:20:00Z','Deployment target','Promoted macOS 12.0 as first Phase1 parity candidate; rejected inherited 10.14 and unsupported 10.15 targets.','ev_legacy_target12'); -- --------------------------------------------------------------------------- -- Historical runtime/profile/benchmark coverage @@ -960,8 +960,8 @@ ORDER BY CASE classification WHEN 'CONFIRMED' THEN 0 WHEN 'REJECTED' THEN 1 WHEN INSERT OR REPLACE INTO source_documents(id,path,sha256,role,authority_rank,temporal_status,scope,last_verified_at,conflicts_with_current_authority,notes) VALUES ('doc_launch_profiles','docs/launch-profiles.md','b3a53fff5c5e8ce37e5f7c0b1bf714e37d8ccead76e09981898934b8baa03524','historical runtime-profile index',55,'HISTORICAL','Explicit promoted/provisional/rejected launcher profiles','2026-08-28T05:31:00Z',0,'Primary source for not repeating rejected graphics profiles.'), -('doc_legacy_architecture','docs/architecture.md','7132bf652fc1d851cf5479f49d9258f9b6e789c6f299db3a868f657515994c2e','Mactician donor architecture',45,'DONOR','Legacy launcher/runtime state machine, host/guest boundary, graphics and rollback','2026-08-28T05:31:00Z',1,'Architecture is donor-only; lifecycle/rollback patterns remain useful.'), -('doc_building','docs/building.md','53252cc1b4b364d69e2f0d6f6061d3c90ef246353fa6e4f3e60048e38f977e73','Mactician donor build contract',45,'DONOR','macOS12 target, tools, signing, environment variables','2026-08-28T05:31:00Z',0,NULL), +('doc_legacy_architecture','docs/architecture.md','7132bf652fc1d851cf5479f49d9258f9b6e789c6f299db3a868f657515994c2e','Retired donor architecture',45,'DONOR','Legacy launcher/runtime state machine, host/guest boundary, graphics and rollback','2026-08-28T05:31:00Z',1,'Architecture is donor-only; lifecycle/rollback patterns remain useful.'), +('doc_building','docs/building.md','53252cc1b4b364d69e2f0d6f6061d3c90ef246353fa6e4f3e60048e38f977e73','Retired donor build contract',45,'DONOR','macOS12 target, tools, signing, environment variables','2026-08-28T05:31:00Z',0,NULL), ('doc_tftmac_app','tftmac/Sources/TFTMACApp.swift','b92fc0b8b525fca9e77e57633ab7c1939e87c113c7dbaf4aadfa00888ca15a33','pre-v2 TFTMAC app implementation',35,'CURRENT_WITH_LEGACY_CONTENT','Native UI and legacy live-runtime orchestration','2026-08-28T05:31:00Z',1,'Hardcodes old internal SDK/AVD roots and legacy ES3.2 property injection; must not be promoted unchanged.'), ('doc_tftmac_info','tftmac/Info.plist','e1a46f86f6aa748f884314e84ff40518e1ce3b4841708fb7083b62d6cc6b6005','current native-app metadata donor',50,'CURRENT_WITH_LEGACY_CONTENT','Bundle identity/minimum macOS version','2026-08-28T05:31:00Z',0,'LSMinimumSystemVersion 12.0 supports deployment-target candidate.'), ('doc_v2_tool','tools/tftmac-v2.mjs',NULL,'current v2 execution harness',85,'CURRENT','External storage, Phase0, source/build workers, host compatibility and map validation','2026-08-28T05:31:00Z',0,'Self-hash changes during active implementation.'); @@ -970,34 +970,34 @@ INSERT OR REPLACE INTO evidence(id,observed_at,kind,source_path,source_sha256,st ('ev_tftmac_shell_legacy_paths','2026-08-27T01:46:35.223Z','source_audit','tftmac/Sources/TFTMACApp.swift','b92fc0b8b525fca9e77e57633ab7c1939e87c113c7dbaf4aadfa00888ca15a33','Pre-v2 TFTMAC shell hardcodes ~/Library/Application Support/TFTMAC/sdk and avd, AVD TftHighEndTablet, and enhanced renderer property androidboot.opengles.version=196610.','DIRECT','This shell is a presentation/orchestration donor, not compatible unchanged with v2 external Runtime root or no-spoof acceptance.'), ('ev_reference_android36','2026-08-27T01:46:26.989Z','reference_config','reference/avd/config.ini','4432ae12207175e046e943ec60ffd97e1a427dd3c9142a7d1b9495f3ed623d92','Reference TftPBE AVD is Android36 ARM64 Google Play-class configuration with host GPU; generated hardware evidence shows 7 vCPU/6144MB and historical pipe transport.','DIRECT','Reference files capture donor state, not current production v2 state.'), ('ev_reference_rootable36','2026-08-27T01:46:26.989Z','reference_config','reference/rootable-avd/config.ini','095305826c0efe3c8bec12f9926e81672773ceb9deda3594894fb103fafb6977','Reference TftRootAffinity AVD is Android36 ARM64 Google APIs, rootable/no Play Store, 7 vCPU/6144MB, host GPU, 1600x900.','DIRECT','Useful proof of package-authority/execution-guest split patterns.'), -('ev_mactician_min12_plist','2026-08-27T01:46:26.983Z','bundle_metadata','launcher/Info.plist','33f27c2f2b1ee80e0b1d56035fb83002c862d4f6c003083b098303e2eec264ae','Mactician 1.0.4 bundle declares LSMinimumSystemVersion 12.0.','DIRECT',NULL), +('ev_legacy_min12_plist','2026-08-27T01:46:26.983Z','bundle_metadata','launcher/Info.plist','33f27c2f2b1ee80e0b1d56035fb83002c862d4f6c003083b098303e2eec264ae','Retired donor bundle declared LSMinimumSystemVersion 12.0.','DIRECT',NULL), ('ev_tftmac_min12_plist','2026-08-27T01:46:35.222Z','bundle_metadata','tftmac/Info.plist','e1a46f86f6aa748f884314e84ff40518e1ce3b4841708fb7083b62d6cc6b6005','TFTMAC 1.0.0 native shell also declares LSMinimumSystemVersion 12.0.','DIRECT','Independent second project-local signal supporting macOS12 floor.'); INSERT OR REPLACE INTO compatibility_claims(id,version_id,environment_id,subject,predicate,object,claim_kind,result,evidence_id,external_source_id,source_document_id,observed_at,last_revalidated_at,stale_after,notes) VALUES ('cc_tftmac_shell_paths',NULL,'env_current_m4','Pre-v2 TFTMAC shell','uses runtime storage','internal Application Support sdk/avd paths','OBSERVED','BLOCKS','ev_tftmac_shell_legacy_paths',NULL,'doc_tftmac_app','2026-08-27T01:46:35.223Z','2026-08-28T05:31:00Z',NULL,'Must be refactored to consume v2 external Runtime/SDK/AVD roots before native-shell promotion.'), ('cc_tftmac_shell_spoof',NULL,'env_current_m4','Pre-v2 TFTMAC enhanced renderer','injects','androidboot.opengles.version=196610','OBSERVED','BLOCKS','ev_tftmac_shell_legacy_paths',NULL,'doc_tftmac_app','2026-08-27T01:46:35.223Z','2026-08-28T05:31:00Z',NULL,'Forbidden as v2 capability proof; source remains useful for UI/runtime control structure only.'), -('cc_two_min12_signals','vc_mactician104','env_current_m4','Mactician and TFTMAC native bundles','share minimum macOS target','12.0','OBSERVED','SUPPORTS','ev_tftmac_min12_plist',NULL,'doc_tftmac_info','2026-08-27T01:46:35.222Z','2026-08-28T05:31:00Z',NULL,'Two independent project bundles plus build scripts use 12.0.'), +('cc_two_min12_signals','vc_legacy_launcher104','env_current_m4','Retired donor and TFTMAC native bundles','share minimum macOS target','12.0','OBSERVED','SUPPORTS','ev_tftmac_min12_plist',NULL,'doc_tftmac_info','2026-08-27T01:46:35.222Z','2026-08-28T05:31:00Z',NULL,'Two independent project bundles plus build scripts use 12.0.'), ('cc_rootable36_split','vc_android36_r07',NULL,'Android36 userdebug/rootable donor','supports architecture pattern','separate execution guest without Play Store','PROJECT_DOCUMENTED','SUPPORTS','ev_reference_rootable36',NULL,'doc_legacy_architecture','2026-08-27T01:46:26.989Z','2026-08-28T05:31:00Z',NULL,'Useful only if conditional custom-driver execution guest becomes necessary.'); INSERT OR REPLACE INTO runtime_variants(id,entrypoint,classification,runtime_family,graphics_path,display_profile,resource_profile,exact_result,promotion_state,source_document_id,current_relevance,notes) VALUES -('rv_best_verified','run-tft-best-verified.command','RECOMMENDED','Mactician/PBE Android36','ANGLE/OpenGL -> Vulkan -> gfxstream/MoltenVK -> Metal','1440p historical default',NULL,'Canonical audited legacy source launch; pins selected ASG, ANGLE/OpenGL and MoltenVK settings.','PROMOTED','doc_launch_profiles','Donor baseline only; v2 workload/guest differs.','Historical recommendation does not override v2 no-spoof policy.'), -('rv_fast_quality','run-tft-fast-quality.command','STABLE_FALLBACK','Mactician/PBE Android36','Same selected legacy graphics stack','configurable',NULL,'Stable fallback without canonical override reset.','RETAINED','doc_launch_profiles','Useful rollback/donor structure.',NULL), -('rv_performance_max','run-tft-performance-max.command','HISTORICAL','Mactician/PBE Android36','ANGLE/Vulkan/gfxstream/MoltenVK','2560x1440; 67% 3D scale','selected 16KiB ASG write step','Two full transport confirmations retained 34.1-35.1 FPS at stage1-8; app render-base.','PROMOTED','doc_launch_profiles','Performance donor only; not v2 acceptance.',NULL), -('rv_angle_opengl','run-tft-angle-opengl.command','DIAGNOSTIC','Mactician/PBE Android36','Verified ANGLE/OpenGL overlay',NULL,NULL,'Required lower-level renderer delegate; not complete safety wrapper.','RETAINED','doc_launch_profiles','Useful graphics-control donor.',NULL), +('rv_best_verified','run-tft-best-verified.command','RECOMMENDED','retired-donor/PBE Android36','ANGLE/OpenGL -> Vulkan -> gfxstream/MoltenVK -> Metal','1440p historical default',NULL,'Canonical audited legacy source launch; pins selected ASG, ANGLE/OpenGL and MoltenVK settings.','PROMOTED','doc_launch_profiles','Donor baseline only; v2 workload/guest differs.','Historical recommendation does not override v2 no-spoof policy.'), +('rv_fast_quality','run-tft-fast-quality.command','STABLE_FALLBACK','retired-donor/PBE Android36','Same selected legacy graphics stack','configurable',NULL,'Stable fallback without canonical override reset.','RETAINED','doc_launch_profiles','Useful rollback/donor structure.',NULL), +('rv_performance_max','run-tft-performance-max.command','HISTORICAL','retired-donor/PBE Android36','ANGLE/Vulkan/gfxstream/MoltenVK','2560x1440; 67% 3D scale','selected 16KiB ASG write step','Two full transport confirmations retained 34.1-35.1 FPS at stage1-8; app render-base.','PROMOTED','doc_launch_profiles','Performance donor only; not v2 acceptance.',NULL), +('rv_angle_opengl','run-tft-angle-opengl.command','DIAGNOSTIC','retired-donor/PBE Android36','Verified ANGLE/OpenGL overlay',NULL,NULL,'Required lower-level renderer delegate; not complete safety wrapper.','RETAINED','doc_launch_profiles','Useful graphics-control donor.',NULL), ('rv_root_affinity','run-tft-root-affinity.command','DIAGNOSTIC','Android36 rootable execution guest','ANGLE/gfxstream with root scheduling/overlays','1600x900 historical','7 vCPU / 6144MB donor','Owns rootable AVD, overlays, PSO scheduling, HWUI repair and cleanup.','RETAINED','doc_launch_profiles','Important conditional execution-guest donor.',NULL), ('rv_gles32_legacy','run-tft-gles32.command','HISTORICAL','older non-root Android36','legacy pipe-era GLES path','1600x900','6GB','Legacy stable fallback before selected ASG stack.','RETAINED','doc_launch_profiles','Do not infer genuine ES3.2 from name.',NULL), -('rv_mvk128','run-tft-mvk128-experimental.command','EXPERIMENTAL','Mactician/PBE Android36','MoltenVK 128-buffer candidate',NULL,NULL,'One strong run failed cold and sustained reproducibility.','NOT_PROMOTED','doc_launch_profiles','Do not repeat as default.',NULL), -('rv_no_fbo_submit','run-tft-fast-quality-angle-no-fbo-submit.command','PROVISIONAL','Mactician/PBE Android36','ANGLE preferSubmitAtFBOBoundary disabled',NULL,NULL,'Promising first run; lacked required cold confirmation.','NOT_PROMOTED','doc_launch_profiles','May be retested only under comparable modern stack evidence.',NULL), -('rv_shader_prewarm','run-tft-fast-quality-shader-prewarm.command','REJECTED','Mactician/PBE Android36','shader preload candidate',NULL,NULL,'Neutral/rejected by fixed-stage campaign.','REJECTED','doc_launch_profiles','Do not repeat without new causal evidence.',NULL), -('rv_submit_thread','run-tft-fast-quality-submit-thread.command','REJECTED','Mactician/PBE Android36','guest Vulkan submit/marshalling thread',NULL,NULL,'Regressed to 37.40/32.60/25.80 FPS at fixed stages.','REJECTED','doc_launch_profiles','Negative evidence.',NULL), -('rv_upstream_asg','run-tft-fast-quality-upstream-asg.command','REJECTED','Mactician/PBE Android36','upstream ASG feature candidate',NULL,NULL,'Failed campaign promotion gates.','REJECTED','doc_launch_profiles','Negative evidence.',NULL), -('rv_asg_active_consumer','run-tft-fast-quality-asg-active-consumer.command','REJECTED','Mactician/PBE Android36','isolated host active-consumer patch',NULL,NULL,'11.2 FPS / 334ms p95 versus 60 FPS / 18.44ms same-scene control.','REJECTED','doc_launch_profiles','Explicit forensic opt-in only.',NULL), -('rv_native_gles','run-tft-fast-quality-native-gles.command','DIAGNOSTIC','Mactician/PBE Android36','native gfxstream GLES without guest ANGLE',NULL,NULL,'High-risk diagnostic; no accepted production result.','NOT_PROMOTED','doc_launch_profiles','Protocol/API capability gap proven in native GLES research.',NULL), -('rv_native_gles30','run-tft-fast-quality-native-gles30.command','REJECTED','Mactician/PBE Android36','native GLES3.0',NULL,NULL,'TFT crashed before first frame because required GLES APIs absent.','REJECTED','doc_launch_profiles','Strong evidence against GLES3.0 shortcut.',NULL), -('rv_native_gles31','run-tft-fast-quality-native-gles31.command','REJECTED','Mactician/PBE Android36','native GLES3.1 attempt',NULL,NULL,'Host native path exposed only GLES3.0; strict gate failed.','REJECTED','doc_launch_profiles','Strong evidence against shallow native-GLES shortcut.',NULL), -('rv_ubo_direct','run-tft-fast-quality-ubo-direct-write.command','EXPERIMENTAL','Mactician/PBE Android36','direct UBO writes',NULL,NULL,'Isolated risky profile; no promotion evidence.','NOT_PROMOTED','doc_launch_profiles','Keep only for reproducibility.',NULL), -('rv_ubo_pool','run-tft-fast-quality-ubo-pool.command','EXPERIMENTAL','Mactician/PBE Android36','larger UBO pool',NULL,NULL,'Isolated risky profile; no promotion evidence.','NOT_PROMOTED','doc_launch_profiles','Keep only for reproducibility.',NULL), -('rv_direct_vulkan','run-tft-direct-vulkan.command','REJECTED','Mactician/PBE Android36','direct Unreal Vulkan RHI',NULL,NULL,'Selected Shipping device profile disabled direct Vulkan; Vulkan remained below ANGLE.','REJECTED','doc_launch_profiles','Current live client is not proven Unreal anyway.',NULL); +('rv_mvk128','run-tft-mvk128-experimental.command','EXPERIMENTAL','retired-donor/PBE Android36','MoltenVK 128-buffer candidate',NULL,NULL,'One strong run failed cold and sustained reproducibility.','NOT_PROMOTED','doc_launch_profiles','Do not repeat as default.',NULL), +('rv_no_fbo_submit','run-tft-fast-quality-angle-no-fbo-submit.command','PROVISIONAL','retired-donor/PBE Android36','ANGLE preferSubmitAtFBOBoundary disabled',NULL,NULL,'Promising first run; lacked required cold confirmation.','NOT_PROMOTED','doc_launch_profiles','May be retested only under comparable modern stack evidence.',NULL), +('rv_shader_prewarm','run-tft-fast-quality-shader-prewarm.command','REJECTED','retired-donor/PBE Android36','shader preload candidate',NULL,NULL,'Neutral/rejected by fixed-stage campaign.','REJECTED','doc_launch_profiles','Do not repeat without new causal evidence.',NULL), +('rv_submit_thread','run-tft-fast-quality-submit-thread.command','REJECTED','retired-donor/PBE Android36','guest Vulkan submit/marshalling thread',NULL,NULL,'Regressed to 37.40/32.60/25.80 FPS at fixed stages.','REJECTED','doc_launch_profiles','Negative evidence.',NULL), +('rv_upstream_asg','run-tft-fast-quality-upstream-asg.command','REJECTED','retired-donor/PBE Android36','upstream ASG feature candidate',NULL,NULL,'Failed campaign promotion gates.','REJECTED','doc_launch_profiles','Negative evidence.',NULL), +('rv_asg_active_consumer','run-tft-fast-quality-asg-active-consumer.command','REJECTED','retired-donor/PBE Android36','isolated host active-consumer patch',NULL,NULL,'11.2 FPS / 334ms p95 versus 60 FPS / 18.44ms same-scene control.','REJECTED','doc_launch_profiles','Explicit forensic opt-in only.',NULL), +('rv_native_gles','run-tft-fast-quality-native-gles.command','DIAGNOSTIC','retired-donor/PBE Android36','native gfxstream GLES without guest ANGLE',NULL,NULL,'High-risk diagnostic; no accepted production result.','NOT_PROMOTED','doc_launch_profiles','Protocol/API capability gap proven in native GLES research.',NULL), +('rv_native_gles30','run-tft-fast-quality-native-gles30.command','REJECTED','retired-donor/PBE Android36','native GLES3.0',NULL,NULL,'TFT crashed before first frame because required GLES APIs absent.','REJECTED','doc_launch_profiles','Strong evidence against GLES3.0 shortcut.',NULL), +('rv_native_gles31','run-tft-fast-quality-native-gles31.command','REJECTED','retired-donor/PBE Android36','native GLES3.1 attempt',NULL,NULL,'Host native path exposed only GLES3.0; strict gate failed.','REJECTED','doc_launch_profiles','Strong evidence against shallow native-GLES shortcut.',NULL), +('rv_ubo_direct','run-tft-fast-quality-ubo-direct-write.command','EXPERIMENTAL','retired-donor/PBE Android36','direct UBO writes',NULL,NULL,'Isolated risky profile; no promotion evidence.','NOT_PROMOTED','doc_launch_profiles','Keep only for reproducibility.',NULL), +('rv_ubo_pool','run-tft-fast-quality-ubo-pool.command','EXPERIMENTAL','retired-donor/PBE Android36','larger UBO pool',NULL,NULL,'Isolated risky profile; no promotion evidence.','NOT_PROMOTED','doc_launch_profiles','Keep only for reproducibility.',NULL), +('rv_direct_vulkan','run-tft-direct-vulkan.command','REJECTED','retired-donor/PBE Android36','direct Unreal Vulkan RHI',NULL,NULL,'Selected Shipping device profile disabled direct Vulkan; Vulkan remained below ANGLE.','REJECTED','doc_launch_profiles','Current live client is not proven Unreal anyway.',NULL); INSERT OR REPLACE INTO benchmark_findings(id,classification,environment_id,stack_id,workload,metric_summary,causal_interpretation,reproducibility,source_document_id,current_relevance,notes) VALUES ('bf_asg_vs_pipe','CONFIRMED','env_benchmark_m1max','stack_m1max_benchmark','Exact stage1-1 battle','ASG 40.1 FPS / 34.85ms p95 vs pipe 29.6 FPS / 49.75ms p95','ASG transport materially outperformed old pipe transport in controlled scene.','Accepted exact-scene A/B','doc_benchmarks','Transport-design donor; do not assume identical delta on API37.',NULL), @@ -1018,11 +1018,11 @@ INSERT OR REPLACE INTO artifact_registry(id,path,artifact_kind,layer,role,status ('ar_map','ssot/TFTMAC_ENGINEERING_MAP.sql','knowledge_graph','reasoning','Persistent dependency/version/evidence map','CURRENT_AUTHORITY','Update before architecture-changing action','doc_map',NULL), ('ar_v2tool','tools/tftmac-v2.mjs','orchestration_code','host_build','Phase0/Phase1/external storage/build compatibility/map validation','CURRENT_IMPLEMENTATION','Changes require validation and map evidence','doc_v2_tool',NULL), ('ar_tftmac_shell','tftmac/Sources/TFTMACApp.swift','native_app_source','presentation','Pre-v2 native TFTMAC UI/runtime controller','DONOR','Do not promote unchanged; remove old storage and spoof assumptions first','doc_tftmac_app',NULL), -('ar_mactician_build','scripts/build-mactician.command','build_script','donor','Proven macOS12 native build/sign/package path','DONOR','Harvest compatibility patterns only','doc_mactician_build',NULL), -('ar_mactician_runtime','launcher/Sources/RuntimeController.swift','runtime_controller','donor','Legacy process/runtime orchestration and environment injection','DONOR','Harvest state/lifecycle patterns; paths/profiles are legacy','doc_legacy_architecture',NULL), -('ar_mactician_paths','launcher/Sources/LauncherPaths.swift','path_model','donor','Legacy Application Support SDK/AVD layout','HISTORICAL','Do not use for v2 bulk runtime path','doc_legacy_architecture',NULL), -('ar_ref_play36','reference/avd/config.ini','avd_reference','guest','Android36 Play-class AVD donor','HISTORICAL','Read-only evidence','doc_readme_mactician',NULL), -('ar_ref_root36','reference/rootable-avd/config.ini','avd_reference','guest','Android36 rootable execution donor','HISTORICAL','Read-only evidence','doc_readme_mactician',NULL), +('ar_legacy_build','historical:removed-build-script','build_script','donor','Proven macOS12 native build/sign/package path','DONOR','Harvest compatibility patterns only','doc_legacy_build',NULL), +('ar_legacy_runtime','historical:removed-runtime-controller','runtime_controller','donor','Legacy process/runtime orchestration and environment injection','DONOR','Harvest state/lifecycle patterns; paths/profiles are legacy','doc_legacy_architecture',NULL), +('ar_legacy_paths','historical:removed-path-model','path_model','donor','Legacy Application Support SDK/AVD layout','HISTORICAL','Do not use for v2 bulk runtime path','doc_legacy_architecture',NULL), +('ar_ref_play36','reference/avd/config.ini','avd_reference','guest','Android36 Play-class AVD donor','HISTORICAL','Read-only evidence','doc_readme_legacy',NULL), +('ar_ref_root36','reference/rootable-avd/config.ini','avd_reference','guest','Android36 rootable execution donor','HISTORICAL','Read-only evidence','doc_readme_legacy',NULL), ('ar_native_gles_host_patch','artifacts/gfxstream-gles32-host-capability-prototype.patch','prototype_patch','graphics_transport','Historical GLES3.2 host capability prototype','EXPERIMENTAL','Do not apply to locked source without fresh causal evidence','doc_native_gles',NULL), ('ar_native_gles_alias_patch','artifacts/gfxstream-gles32-guest-proc-alias-prototype.patch','prototype_patch','guest_graphics','Historical proc-alias prototype','REJECTED','Retain for evidence only; runtime loader already resolved aliases','doc_native_gles',NULL), ('ar_performance_candidates','scripts/performance-candidates.json','experiment_manifest','performance','Historical reproducible performance candidates','HISTORICAL','Do not infer promotion from file presence','doc_benchmarks',NULL); @@ -1341,7 +1341,7 @@ INSERT OR REPLACE INTO map_meta(key,value) VALUES ('latest_5gb_trend','Latest closed 5 GiB run split into 10-minute windows: heavy-gameplay CPU ~245-277%, emulator RSS ~6.1-6.5 GiB, host available ~5.1-5.5 GiB, guest available ~1.64-1.75 GiB. Across the full ~106-minute run compressed memory rose ~2.72 GiB and host available fell ~2.62 GiB. Pressure accumulates over time, but disconnect did not coincide with guest-memory collapse.'), ('latest_5gb_vs_6gb','Directional whole-run comparison: pageout rate 6 GiB ~207.18/min vs 5 GiB ~133.54/min (-35.55%); compressed mean ~0.45 GiB lower and compressed p95 ~1.65 GiB lower on 5 GiB. Workload mixes differ, so 5 GiB is retained but not promoted as a final causal fact.'), ('current_ram_decision','KEEP 5 GiB for continued development; DO NOT cut to 4 GiB now. Heavy-gameplay guest available memory already reaches ~1.64 GiB, so another 1 GiB cut has insufficient safety margin without stronger evidence.'), -('current_presentation_candidate','Stage one-factor gfxstream/ASG candidate mactician_compatible_5gb_flush400_v1: drawFlushInterval 800 -> 400 only. Motivation: broad prior run added +524 HWC misses with +0 GPU misses, and AOSP defines this parameter as balancing host-GPU starvation against notification overhead. Do not combine with RAM, graphics preset, FPS, Performance Mode or other transport changes.'), +('current_presentation_candidate','Stage one-factor gfxstream/ASG candidate tftmac_5gb_flush400_exp_v1: drawFlushInterval 800 -> 400 only. Motivation: broad prior run added +524 HWC misses with +0 GPU misses, and AOSP defines this parameter as balancing host-GPU starvation against notification overhead. Do not combine with RAM, graphics preset, FPS, Performance Mode or other transport changes.'), ('current_preplay_pressure_thresholds','Between-game TFT app refresh threshold tightened to host compressed >=3.75 GiB or host available <=4.75 GiB based on the 5 GiB end-of-run trend; refresh preserves emulator and logger.'); INSERT OR REPLACE INTO evidence(id,observed_at,kind,source_path,source_sha256,statement,confidence,notes) VALUES @@ -1355,13 +1355,13 @@ INSERT INTO update_log(observed_at,subject,change_summary,evidence_id) VALUES ('2026-08-28T23:47:00Z','Logger reliability hardening','Decoupled raw capture sealing and cleanup from SQLite normalization so post-processing cannot strand or invalidate a long run.','ev_logger_raw_seal'); INSERT OR REPLACE INTO map_meta(key,value) VALUES -('latest_closed_run_session','2026-08-28T23-31-16-637Z-df54ebaa-561a-4567-ab20-d94baf0a3619; 3860.24s; mactician_compatible_5gb_v1; raw capture SEALED; SQLite COMPLETE; 18/18 required artifacts present; manifest f17014ce217742682898a4562aa873a58d8d3fed09b765603c6d4052ea49aa6b.'), +('latest_closed_run_session','2026-08-28T23-31-16-637Z-df54ebaa-561a-4567-ab20-d94baf0a3619; 3860.24s; tftmac_5gb_baseline_v1; raw capture SEALED; SQLite COMPLETE; 18/18 required artifacts present; manifest f17014ce217742682898a4562aa873a58d8d3fed09b765603c6d4052ea49aa6b.'), ('latest_closed_run_memory','Second sustained 5 GiB run reproduced the operating envelope. Full run weighted means: host available 5.425 GiB, host compressed 2.793 GiB, guest available 2.224 GiB, emulator RSS 5829 MiB. Heavy 10-minute windows reached CPU 229-288%, guest available about 1.70-1.90 GiB, host available about 5.01-5.26 GiB, compressed about 2.97-3.40 GiB. Keep 5120 MB; do not reduce to 4096 MB.'), ('latest_closed_run_pageouts','Current 5 GiB run: 10711 pageouts over 3860.24s = 166.48/min including cold boot. Excluding the first 10-minute cold-boot/startup window, sustained rate is about 127.94/min, close to the prior 5 GiB run about 133.54/min. Old 6 GiB baseline was about 207.18/min. Directional evidence continues to favor 5 GiB.'), ('latest_closed_run_audio','Explicit CoreAudio held through the full run: TFT OpenSL ES 44.1 kHz stereo playback active, ranchu pcm_write I/O failures=0, mixer underruns partial=0 empty=0.'), ('latest_closed_run_network','TFT stayed PID 5276 and Android network 101 remained assigned. End-of-game TFT connectivity callback requests at about 19:30 were immediately assigned to network 101; no Android LOST/UNAVAIL sequence or process restart was observed.'), ('latest_closed_run_presentation','Post-run SurfaceFlinger state: 60 Hz, 1280x720 TFT SurfaceView scaled 1.5x to 1920x1080, total missed=562 and HWC missed=562. The old parser converted an actual zero counter to NULL; parser has been corrected to preserve zero. Current evidence continues to prioritize presentation/HWC pacing over host GPU saturation.'), -('current_graphics_next_action','Proceed with one-factor mactician_compatible_5gb_flush400_v1 experiment: ASG drawFlushInterval 800 -> 400 only, after validating the corrected zero-counter parser and sealed-run comparison. Keep RAM=5120, Medium/60/Performance OFF, CoreAudio, ANGLE/Vulkan/gfxstream/MoltenVK, resolution and all other transport fields fixed.'); +('current_graphics_next_action','Proceed with one-factor tftmac_5gb_flush400_exp_v1 experiment: ASG drawFlushInterval 800 -> 400 only, after validating the corrected zero-counter parser and sealed-run comparison. Keep RAM=5120, Medium/60/Performance OFF, CoreAudio, ANGLE/Vulkan/gfxstream/MoltenVK, resolution and all other transport fields fixed.'); COMMIT; @@ -1379,3 +1379,104 @@ COMMIT; -- 3. Resolve or create the corresponding unknown. -- 4. Re-query v_architecture_options and v_open_questions. -- 5. Only then choose the next mutation. + +-- --------------------------------------------------------------------------- +-- 2026-08-30 native full-screen runtime authority +-- --------------------------------------------------------------------------- +UPDATE runtime_variants +SET current_relevance='Historical first-win control receipt; superseded as current launcher identity by rv_current_native_5gb.' +WHERE id='rv_current_first_win'; + +INSERT OR REPLACE INTO runtime_variants(id,entrypoint,classification,runtime_family,graphics_path,display_profile,resource_profile,exact_result,promotion_state,source_document_id,current_relevance,notes) VALUES +('rv_current_native_5gb','TFTMAC.app','RECOMMENDED','Released Emulator37.1.11 / Android16 API36 Play through packaged macOS app host','TFT Unreal -> ANGLE -> Vulkan/ranchu -> virtio-gpu-asg/gfxstream -> host Vulkan -> MoltenVK -> Metal -> raw gRPC RGBA -> AppKit Metal','1920x1080 / 320 DPI / 60Hz target / native full screen','6 vCPU / 5120 MB','Authenticated controller, ADB5038/emulator-5582 device, official TFT18.1 SplashActivity -> Unreal GameActivity, correctly oriented native frames and input all passed in one live session.','PROMOTED',NULL,'Current native product/runtime authority','Source/presentation FPS are transport metrics, not Unreal FPS. CoreAudio software path passed; user audible confirmation and a full native match remain acceptance gaps.'); + +INSERT OR REPLACE INTO evidence(id,observed_at,kind,source_path,source_sha256,statement,confidence,notes) VALUES +('ev_native_fullscreen_20260830','2026-08-30T08:41:35Z','native_runtime_acceptance','~/Library/Application Support/TFTMAC/Captures/2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94/TFTMAC_NATIVE_RUNTIME.sqlite',NULL,'Native AppKit TFTMAC authenticated Emulator37.1.11, reached ADB5038/emulator-5582 device, rendered exact 1920x1080 raw RGBA through Metal, accepted mouse input, and launched official TFT18.1 into com.epicgames.unreal.GameActivity.','DIRECT','Live lobby acceptance. Does not claim a completed native match, user-audible sound, or causal graphics-boundary attribution.'); + +INSERT OR REPLACE INTO map_meta(key,value) VALUES +('current_native_runtime','TFTMAC.app; packaged TFTMAC Emulator Host.app via /usr/bin/open -n -W; ADB5038; emulator-5582; no injected ADB_VENDOR_KEYS; 6 vCPU; 5120 MiB; 1920x1080/320dpi/60Hz; CoreAudio; authenticated gRPC raw RGBA -> AppKit Metal full screen.'), +('current_native_telemetry','Private local SQL plus JSONL/raw sidecars: configuration receipts, events, one-second native-frame interval windows, output presentation, qemu CPU/RSS, guest memory, clock sync, SurfaceFlinger deltas, AudioFlinger health, logcat fault counts, game-process sessions, input counts and manual match/combat/stutter/end markers.'), +('current_native_metric_boundary','gRPC source-window and Metal output rates are transport/presentation observations, not Unreal engine FPS. Use bounded SurfaceFlinger/Perfetto evidence for guest/pipeline attribution.'); + +INSERT INTO update_log(observed_at,subject,change_summary,evidence_id) VALUES +('2026-08-30T08:41:35Z','Native runtime promotion','Promoted the logged-in-session packaged-host 5038/5582/5120-MiB AppKit/Metal path after live TFT GameActivity acceptance; preserved 5040/5592/6144 evidence as historical only.','ev_native_fullscreen_20260830'); + +-- --------------------------------------------------------------------------- +-- 2026-08-31 Build 8 current graphics-run authority +-- Historical rows remain evidence of their own timestamps and are not deleted. +-- --------------------------------------------------------------------------- +INSERT OR REPLACE INTO evidence(id,observed_at,kind,source_path,source_sha256,statement,confidence,notes) VALUES +('ev_build8_current_capture_20260831','2026-08-31T23:13:30Z','private_capture_metadata','PRIVATE_LOCAL_ONLY/TFTMAC_NATIVE_RUNTIME.sqlite','c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77','Build 8 capture 2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200 is a complete 42m27s automatic TFT PID/layer-lifetime run. Its private SQLite database is 63897600 bytes; it contains 144364 exact-layer intervals, 99.629% exact-layer coverage, weighted FPS 56.98, 1% low 21.49 FPS, p95 21.51 ms, p99 33.434 ms, 6544 jank frames, 144 severe stalls, 7644 missed-vsync equivalents, and 189 degradation incidents.','DIRECT','Raw database excluded from Git; hash and byte count are retained for integrity.'), +('ev_build8_current_stack_20260831','2026-08-31T23:13:30Z','runtime_stack_receipt','PRIVATE_LOCAL_ONLY/TFTMAC_NATIVE_RUNTIME.sqlite','c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77','The active TFT path is direct Unreal Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal. ANGLE is conditional and is not assumed to be the TFT main rendering path. Final Mac presentation is excluded from causal candidates.','DIRECT','Receipt is runtime-specific; it does not expose signed Riot Unreal source lines.'), +('ev_build8_current_root_unknown_20260831','2026-08-31T23:13:30Z','causal_limit','PRIVATE_LOCAL_ONLY/TFTMAC_NATIVE_RUNTIME.sqlite','c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77','Current automatic logging verifies degradation but cannot name one internal root component: a shared causal work ID and owned-stage timing are absent. Root attribution is UNKNOWN upstream of or at the guest TFT surface.','DIRECT','No ASG, gfxstream, MoltenVK, Metal, ANGLE, or Unreal cause is inferred from the current capture.'), +('ev_current_host_audit_20260831','2026-08-31T23:13:30Z','host_trust_audit','ssot/runtime-authority.json',NULL,'The current-host audit found release identity hashes matching the historical Build 8 receipt with zero identity findings, while the host trust policy reported CSSMERR_TP_NOT_TRUSTED. This audit is timestamped separately and does not invalidate or rewrite historical signing acceptance.','DIRECT','Trust-policy result is not a release-identity mismatch.'); + +INSERT OR REPLACE INTO runtime_variants(id,entrypoint,classification,runtime_family,graphics_path,display_profile,resource_profile,exact_result,promotion_state,source_document_id,current_relevance,notes) VALUES +('rv_stock_build8_normal_play','TFTMAC.app','RECOMMENDED','Stock released Emulator37.1.11 / Android16 API36 Play through packaged macOS app host','Unreal direct Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal','1920x1080 / 320 DPI / 60Hz target / native full screen','6 vCPU / 5120 MiB / CoreAudio / host GPU','Build 8 automatic 42m27s graphics run complete with exact-layer coverage and stack receipts.','PROMOTED',NULL,'Current normal-play authority','Active observed combat_latency_a uses High/60/Performance Mode OFF and is not promoted as a performance result. Markers and battle classification are optional annotations.'), +('rv_tftmac_runtime_diagnostic','isolated source-built emulator','DIAGNOSTIC','tftmac-runtime @ c8aa26ebaa5b977965eb165ad8aac5c98408469f','Planned source-instrumented gfxstream/MoltenVK causal trace','Not applicable until isolated diagnostic acceptance','Not comparable to stock Build 8','Eligible only for the planned advanced causal logger.','NOT_PROMOTED',NULL,'Diagnostic eligibility only','Requires separate parity, correctness, clock, stream-seal, and observer-overhead gates. Never replace normal-play stock authority by implication.'); + +INSERT OR REPLACE INTO unknowns(id,question,owning_layer,blocking,next_probe,status,resolution) VALUES +('unk_current_internal_root_attribution','Which owned component and source site first diverges before the exact TFT SurfaceFlinger actual-present degradation?','cross_stack_causal_logger',1,'Build the planned isolated source causal logger with explicit work IDs across owned boundaries; require complete joins, clock validity, sealed streams, and observer-overhead <=5% before naming a cause.','OPEN',NULL); + +INSERT OR REPLACE INTO decisions(id,decided_at,decision,rationale,state,evidence_id,supersedes) VALUES +('dec_stock_build8_normal_play_authority','2026-08-31T23:13:30Z','Retain stock Build 8 as normal-play authority.','It is the verified automatic logging/control runtime. The advanced source-build path is diagnostic-only and non-comparable until a separate parity gate passes.','ACTIVE','ev_build8_current_capture_20260831',NULL), +('dec_mac_presenter_context_only','2026-08-31T23:13:30Z','Exclude final Mac presenter from causal candidates.','The current capture and user-approved fact establish it as context/correctness telemetry, not the graphics optimization target.','ACTIVE','ev_build8_current_stack_20260831',NULL), +('dec_markers_optional_annotations','2026-08-31T23:13:30Z','Treat match and battle markers as optional annotations.','Automatic TFT process/layer lifetime determines raw capture validity; marker absence cannot invalidate causal analysis.','ACTIVE','ev_build8_current_capture_20260831',NULL); + +INSERT OR REPLACE INTO map_meta(key,value) VALUES +('current_phase','Advanced causal graphics logging planned; Build 8 automatic full-run baseline verified.'), +('current_release_gameplay_benchmark','VERIFIED_CAPTURE_ROOT_ATTRIBUTION_UNKNOWN; capture 2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200.'), +('current_runtime_authority','Stock Build 8 packaged macOS-host launcher is normal-play authority. tftmac-runtime@c8aa26e is isolated, diagnostic-only, and non-comparable.'), +('current_observed_configuration','combat_latency_a; High graphics / 60 FPS / Performance Mode OFF; observed active, not performance-promoted.'), +('current_pipeline_receipt','Unreal direct Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal; ANGLE conditional; final Mac presenter context-only.'), +('current_capture_validity','Automatic PID/layer lifetime logging is verified. Match, battle, combat, and result markers are optional annotations and are not validity or causal gates.'), +('current_root_attribution','UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE. Existing telemetry measures degradation but lacks shared owned-stage work IDs for causal naming.'), +('current_advanced_logger_status','PLANNED: isolated source-built runtime instrumentation; do not claim a root component or source line until all causal evidence gates pass.'), +('current_host_audit','Timestamped current-host audit: matching release identity hashes, zero identity findings, CSSMERR_TP_NOT_TRUSTED under current host trust policy; historical release signing acceptance remains distinct.'); + +INSERT OR REPLACE INTO map_meta(key,value) VALUES +('current_presentation_candidate','RETIRED_AS_CURRENT_ACTION: final Mac presentation is context-only; earlier broad HWC/ASG directional candidates remain historical and cannot name a root cause.'), +('current_graphics_next_action','Plan the isolated source causal logger first. No graphics tuning intervention is current until it identifies an owned first divergent boundary or reports an explicit unowned/missing boundary.'); + +INSERT INTO update_log(observed_at,subject,change_summary,evidence_id) VALUES +('2026-08-31T23:13:30Z','Build 8 full-run baseline','Recorded the verified automatic 42m27s capture, private integrity metadata, direct Vulkan stack receipt, and explicit UNKNOWN root-attribution boundary.','ev_build8_current_capture_20260831'), +('2026-08-31T23:13:30Z','Causal ownership boundary','Excluded final Mac presentation from causal candidates; made markers/battles optional annotations; retained source-instrumented runtime only as isolated diagnostic eligibility.','ev_build8_current_root_unknown_20260831'), +('2026-08-31T23:13:30Z','Current-host trust audit','Recorded matching release identities and current host CSSMERR_TP_NOT_TRUSTED separately from historical signing acceptance.','ev_current_host_audit_20260831'); + +-- Reclassify superseded rows without deleting their historical evidence. These +-- exact-key overrides prevent an older INSERT OR REPLACE from acting as current +-- authority when this map is rebuilt from scratch. +UPDATE runtime_variants +SET classification='HISTORICAL', + promotion_state='RETAINED', + current_relevance='Historical runtime receipt; superseded by rv_stock_build8_normal_play.', + notes=COALESCE(notes,'') || ' Reclassified on 2026-08-31; not current renderer or normal-play authority.' +WHERE id IN ('rv_current_first_win','rv_current_native_5gb'); + +UPDATE stack_profiles +SET status='HISTORICAL', + next_use='Historical comparison only; Build 8 stock normal-play authority is recorded by rv_stock_build8_normal_play.' +WHERE id='stack_first_win'; + +UPDATE source_documents +SET path='docs/history/2026-08-31-pre-build8/TFTMAC.md', + temporal_status='HISTORICAL', + conflicts_with_current_authority=0, + notes='Archived pre-Build-8 document. The concise TFTMAC.md replacement points to current authorities.' +WHERE id='doc_tftmac_legacy'; + +INSERT OR REPLACE INTO source_documents(id,path,sha256,role,authority_rank,temporal_status,scope,last_verified_at,conflicts_with_current_authority,notes) VALUES +('doc_tftmac_build8_current','TFTMAC.md',NULL,'current Build 8 authority pointer',90,'CURRENT','Build 8 normal-play and causal-logging handoff','2026-08-31T23:13:30Z',0,'Defers detailed facts, chronology, engineering status, and formulas to facts.md, project.md, dev.md, and benchmark.md.'); + +INSERT OR REPLACE INTO map_meta(key,value) VALUES +('current_renderer','Unreal direct Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal. ANGLE is conditional and is not claimed as the TFT main renderer without a new runtime receipt.'), +('current_logger_guard','Automatic logging follows the TFT process and exact layer from game start until process/app close. Match, combat, battle, quality, and result markers are optional annotations only.'), +('current_multi_match_capture','One uninterrupted automatic capture may contain any number of games. Process/layer lifetime and timestamped samples are authoritative; marker pairing is never required for validity.'), +('current_performance_truth','Build 8 automatic capture verifies sustained frame degradation and exact-layer coverage; the first failing internal component remains UNKNOWN until owned-stage work IDs exist.'), +('current_graphics_pipeline','TFT Unreal direct Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal. Final Mac presentation is hidden correctness context and excluded from causal ranking.'), +('current_connector_state','The direct-Vulkan stack is receipt-verified. Per-frame ownership below the guest TFT surface remains unmeasured; no internal component is a proven live root cause.'), +('current_native_telemetry','Private local SQL and sealed sidecars are captured automatically across the TFT PID/layer lifetime. Match, combat, battle, stutter, quality, and result markers are optional annotations.'), +('current_native_metric_boundary','Exact TFT SurfaceFlinger actual-present timestamps are FPS authority. Final Mac presentation is hidden correctness context only and cannot be selected as the root cause.'), +('current_game_settings','Observed Build 8 capture: combat_latency_a; High graphics; 60 FPS; Performance Mode OFF. This is an observation, not a promoted tuning result.'), +('current_pipeline_next_probe','Build the isolated source causal logger with owned-stage work IDs, valid clock joins, sealed streams, and <=5% observer-overhead gate before any causal patch.'), +('current_optimization_priority','Use all automatic full-run graphics data. Name or patch a component only after the source causal logger proves the first divergent owned boundary; otherwise report UNKNOWN.'); diff --git a/ssot/TFTMAC_NATIVE_APP_PREFLIGHT.md b/ssot/TFTMAC_NATIVE_APP_PREFLIGHT.md index 215453c..e95df59 100644 --- a/ssot/TFTMAC_NATIVE_APP_PREFLIGHT.md +++ b/ssot/TFTMAC_NATIVE_APP_PREFLIGHT.md @@ -76,8 +76,8 @@ Runtime root: /Volumes/MAC MINI M4/TFTMAC/Runtime Android Emulator: 37.1.11.0 / build 15917651 ADB: 1.0.41 AVD: TFT_Ultra_Tablet -Console port: 5592 -ADB server: isolated port 5040 +Console port: 5582 +ADB server: donor port 5038 Google Play ARM64 API36 image ``` diff --git a/ssot/TFTMAC_PERFORMANCE_LAB.sql b/ssot/TFTMAC_PERFORMANCE_LAB.sql index 6a2d943..c90115b 100644 --- a/ssot/TFTMAC_PERFORMANCE_LAB.sql +++ b/ssot/TFTMAC_PERFORMANCE_LAB.sql @@ -524,7 +524,7 @@ INSERT INTO unknowns VALUES -- preserving those older rows as historical context elsewhere in the project. INSERT OR REPLACE INTO lab_meta(key,value) VALUES -('current_playable_baseline','mactician_compatible_official_v0'), +('current_playable_baseline','tftmac_official_baseline_v1'), ('current_playable_baseline_session','2026-08-28T11-06-18-553Z-e6d3204f-17c2-4b80-9084-e76642089da2'), ('current_guest','Android 16 / API 36 Google Play ARM64 revision >=7'), ('current_emulator','Google Android Emulator 37.1.11'), @@ -556,9 +556,9 @@ INSERT OR REPLACE INTO runtime_configs ( display_width,display_height,density_dpi,refresh_hz,gpu_mode,audio_enabled, graphics_transport,angle_mode,vulkan_mode,moltenvk_mode,presentation_mode,state,created_at,notes ) VALUES -('mactician_compatible_official_v0',NULL,'Mactician-compatible official TFT control v0','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5592',5040,5592,6,6144,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','GuestAngle + explicit exposeNonConformantExtensionsAndVersions/exposeES32ForTesting compatibility adapter','ranchu guest Vulkan','gfxstream host Vulkan -> MoltenVK/Metal','direct emulator window','CONTROL','2026-08-28T11:06:18Z','Completed a full first-place official TFT match. Compatibility exposure is a truthful workload adapter, not GLES conformance proof.'), -('mactician_compatible_5gb_v1','mactician_compatible_official_v0','RAM 5 GiB candidate','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5592',5040,5592,6,5120,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','same as baseline','same as baseline','same as baseline','same as baseline','CANDIDATE','2026-08-28T21:31:30Z','One-factor candidate: only guest RAM changes 6144 -> 5120 MB. Direct 4096 MB cut deferred because observed heavy-gameplay guest headroom reaches about 1.64-1.75 GiB.'), -('mactician_compatible_5gb_flush400_v1','mactician_compatible_5gb_v1','5 GiB + lower draw flush interval candidate','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5592',5040,5592,6,5120,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','same as 5 GiB baseline','same as 5 GiB baseline','same as 5 GiB baseline','same as 5 GiB baseline','CANDIDATE','2026-08-28T23:47:00Z','One-factor graphics-transport candidate: hw.gltransport.drawFlushInterval 800 -> 400 only. AOSP defines this interval as the balance between host-GPU starvation and pipe-notification overhead; test only against SurfaceFlinger miss-rate and resource telemetry.'); +('tftmac_official_baseline_v1',NULL,'TFTMAC official playable baseline official TFT control v0','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5592',5040,5592,6,6144,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','GuestAngle + explicit exposeNonConformantExtensionsAndVersions/exposeES32ForTesting compatibility adapter','ranchu guest Vulkan','gfxstream host Vulkan -> MoltenVK/Metal','direct emulator window','CONTROL','2026-08-28T11:06:18Z','Completed a full first-place official TFT match. Compatibility exposure is a truthful workload adapter, not GLES conformance proof.'), +('tftmac_5gb_baseline_v1','tftmac_official_baseline_v1','RAM 5 GiB candidate','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5592',5040,5592,6,5120,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','same as baseline','same as baseline','same as baseline','same as baseline','CANDIDATE','2026-08-28T21:31:30Z','One-factor candidate: only guest RAM changes 6144 -> 5120 MB. Direct 4096 MB cut deferred because observed heavy-gameplay guest headroom reaches about 1.64-1.75 GiB.'), +('tftmac_5gb_flush400_exp_v1','tftmac_5gb_baseline_v1','5 GiB + lower draw flush interval candidate','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5592',5040,5592,6,5120,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','same as 5 GiB baseline','same as 5 GiB baseline','same as 5 GiB baseline','same as 5 GiB baseline','CANDIDATE','2026-08-28T23:47:00Z','One-factor graphics-transport candidate: hw.gltransport.drawFlushInterval 800 -> 400 only. AOSP defines this interval as the balance between host-GPU starvation and pipe-notification overhead; test only against SurfaceFlinger miss-rate and resource telemetry.'); UPDATE hypotheses SET status='TESTING', confidence=0.60, @@ -571,18 +571,18 @@ INSERT OR REPLACE INTO hypotheses VALUES ('h_gpu_frame_miss','GPU-side presentation misses contribute to visible lag','SURFACEFLINGER','A meaningful portion of visible gameplay lag is caused by GPU/presentation misses somewhere in the Unreal -> ANGLE -> Vulkan -> gfxstream -> MoltenVK -> Metal path.','TFT-specific SurfaceFlinger frametimeline traces show GPU-missed or late frames correlated with visible stalls while memory state is controlled.','TFT-specific frametimeline remains healthy during visible stalls, or misses disappear without improving perceived performance.','QUEUED',0.35,NULL,'2026-08-28T18:42:01Z','Nominated from cumulative SurfaceFlinger counters only: 2709 total missed, 2163 GPU missed, 546 HWC missed at the latest post-match read. These counters are since boot and are not match-scoped.'); INSERT OR REPLACE INTO experiments VALUES -('exp_ram_5gb_ab','h_guest_ram_host_pressure','Guest RAM 6144 -> 5120 MB A/B','INTERVENTION','mactician_compatible_official_v0','mactician_compatible_5gb_v1','HEAVY',1,'PLANNED',1,'Same official TFT version, renderer path, display, transport and vCPU; compare continuous-run pressure and native frame timing when available.','2026-08-28T21:31:30Z',NULL,'First RAM intervention. One GiB reduction only; 4096 MB is deferred because current guest headroom is insufficient for a safe first cut.'), -('exp_native_frame_trace',NULL,'Native Unreal/Vulkan frame-timing capture','OBSERVATION','mactician_compatible_official_v0',NULL,'HEAVY',0,'PLANNED',0,'Capture android.surfaceflinger.frame + android.surfaceflinger.frametimeline + android.surfaceflinger.layers + android.gpu.memory during real TFT combat; align to the existing host monotonic clock. Add linux.ftrace only in the heavier validation run.','2026-08-28T18:30:59Z',NULL,'Blocking measurement experiment before renderer/transport tuning. All required Perfetto data sources are directly proven available on the current guest.'); +('exp_ram_5gb_ab','h_guest_ram_host_pressure','Guest RAM 6144 -> 5120 MB A/B','INTERVENTION','tftmac_official_baseline_v1','tftmac_5gb_baseline_v1','HEAVY',1,'PLANNED',1,'Same official TFT version, renderer path, display, transport and vCPU; compare continuous-run pressure and native frame timing when available.','2026-08-28T21:31:30Z',NULL,'First RAM intervention. One GiB reduction only; 4096 MB is deferred because current guest headroom is insufficient for a safe first cut.'), +('exp_native_frame_trace',NULL,'Native Unreal/Vulkan frame-timing capture','OBSERVATION','tftmac_official_baseline_v1',NULL,'HEAVY',0,'PLANNED',0,'Capture android.surfaceflinger.frame + android.surfaceflinger.frametimeline + android.surfaceflinger.layers + android.gpu.memory during real TFT combat; align to the existing host monotonic clock. Add linux.ftrace only in the heavier validation run.','2026-08-28T18:30:59Z',NULL,'Blocking measurement experiment before renderer/transport tuning. All required Perfetto data sources are directly proven available on the current guest.'); UPDATE experiments -SET baseline_config_id='mactician_compatible_official_v0', +SET baseline_config_id='tftmac_official_baseline_v1', state='COMPLETE', notes=COALESCE(notes,'') || ' Superseded runtime identity corrected to the proven playable baseline.' WHERE id='exp_control_direct_play'; UPDATE experiments SET state='CANCELLED', - notes=COALESCE(notes,'') || ' Superseded by mactician_compatible_official_v0 and exp_native_frame_trace.' + notes=COALESCE(notes,'') || ' Superseded by tftmac_official_baseline_v1 and exp_native_frame_trace.' WHERE id IN ('exp_control_repeat_warm','exp_transition_capture','exp_heavy_capture') AND baseline_config_id='control_stock_direct_v0'; @@ -594,10 +594,10 @@ INSERT OR REPLACE INTO hypotheses VALUES ('h_asg_flush_latency','ASG draw flush cadence contributes to presentation lateness','GFXSTREAM_ASG','The current draw-flush cadence allows guest graphics work to reach the host late enough to contribute to SurfaceFlinger/HWC presentation misses even when GPU-miss counters remain flat.','Reducing drawFlushInterval 800 -> 400 lowers HWC/total missed-frame rate in comparable gameplay without a disproportionate CPU or notification-overhead penalty.','HWC/total miss rate does not improve, or host CPU/overhead rises materially enough to offset frame-pacing gains.','QUEUED',0.45,NULL,'2026-08-28T23:47:00Z','Nominated from broad 5 GiB run delta: +524 total misses, +524 HWC misses, +0 GPU misses, plus available host CPU headroom. Requires 10-second SurfaceFlinger counter stream for a clean A/B.'); INSERT OR REPLACE INTO experiments VALUES -('exp_asg_flush400_ab','h_asg_flush_latency','ASG draw flush interval 800 -> 400 A/B','INTERVENTION','mactician_compatible_5gb_v1','mactician_compatible_5gb_flush400_v1','HEAVY',1,'PLANNED',1,'Keep 5 GiB RAM, 6 vCPU, 1920x1080, Medium/60/Performance OFF, ANGLE/Vulkan/MoltenVK, ASG sizes and TFT package unchanged; compare 10-second SurfaceFlinger HWC/GPU/total miss rate plus CPU/memory.','2026-08-28T23:47:00Z',NULL,'Do not activate until a stable 5 GiB/CoreAudio control run is available with the new SurfaceFlinger counter stream.'), -('exp_tft_performance_mode_ab',NULL,'TFT Performance Mode (Beta) A/B','INTERVENTION','mactician_compatible_official_v0',NULL,'HEAVY',1,'PLANNED',0,'Keep Medium graphics, 60 FPS cap, emulator/runtime/package identical; change only Performance Mode (Beta) ON/OFF and compare native frame timing + memory/GPU pressure.','2026-08-28T20:11:18Z',NULL,'First game-level intervention after the fully labeled Medium/60 baseline.'), -('exp_tft_fps_cap_ab',NULL,'TFT FPS cap A/B','INTERVENTION','mactician_compatible_official_v0',NULL,'HEAVY',1,'PLANNED',0,'Hold graphics preset and Performance Mode constant; compare 60 versus None first, with 30 retained as a diagnostic lower-load control if needed.','2026-08-28T20:11:18Z',NULL,'Do not combine FPS-cap changes with graphics-preset changes.'), -('exp_tft_graphics_preset_ab',NULL,'TFT graphics preset A/B','INTERVENTION','mactician_compatible_official_v0',NULL,'HEAVY',1,'PLANNED',0,'Hold FPS cap and Performance Mode constant; test Low, Medium, High, Ultra High one preset at a time with native frame timing.','2026-08-28T20:11:18Z',NULL,'Target is the highest preset that preserves stable 60-Hz frame pacing and acceptable memory/GPU pressure.'); +('exp_asg_flush400_ab','h_asg_flush_latency','ASG draw flush interval 800 -> 400 A/B','INTERVENTION','tftmac_5gb_baseline_v1','tftmac_5gb_flush400_exp_v1','HEAVY',1,'PLANNED',1,'Keep 5 GiB RAM, 6 vCPU, 1920x1080, Medium/60/Performance OFF, ANGLE/Vulkan/MoltenVK, ASG sizes and TFT package unchanged; compare 10-second SurfaceFlinger HWC/GPU/total miss rate plus CPU/memory.','2026-08-28T23:47:00Z',NULL,'Do not activate until a stable 5 GiB/CoreAudio control run is available with the new SurfaceFlinger counter stream.'), +('exp_tft_performance_mode_ab',NULL,'TFT Performance Mode (Beta) A/B','INTERVENTION','tftmac_official_baseline_v1',NULL,'HEAVY',1,'PLANNED',0,'Keep Medium graphics, 60 FPS cap, emulator/runtime/package identical; change only Performance Mode (Beta) ON/OFF and compare native frame timing + memory/GPU pressure.','2026-08-28T20:11:18Z',NULL,'First game-level intervention after the fully labeled Medium/60 baseline.'), +('exp_tft_fps_cap_ab',NULL,'TFT FPS cap A/B','INTERVENTION','tftmac_official_baseline_v1',NULL,'HEAVY',1,'PLANNED',0,'Hold graphics preset and Performance Mode constant; compare 60 versus None first, with 30 retained as a diagnostic lower-load control if needed.','2026-08-28T20:11:18Z',NULL,'Do not combine FPS-cap changes with graphics-preset changes.'), +('exp_tft_graphics_preset_ab',NULL,'TFT graphics preset A/B','INTERVENTION','tftmac_official_baseline_v1',NULL,'HEAVY',1,'PLANNED',0,'Hold FPS cap and Performance Mode constant; test Low, Medium, High, Ultra High one preset at a time with native frame timing.','2026-08-28T20:11:18Z',NULL,'Target is the highest preset that preserves stable 60-Hz frame pacing and acceptable memory/GPU pressure.'); INSERT OR REPLACE INTO lab_meta(key,value) VALUES ('latest_observed_match_2','placement=1; result=WIN; exact result at 2026-08-28T20:57:14.054Z'), @@ -637,14 +637,14 @@ INSERT OR REPLACE INTO decisions(id,experiment_id,decision,rationale,cold_confir ('decision_ram5_operational_keep','exp_ram_5gb_ab','KEEP','Retain 5120 MB guest RAM for continued development: host-pressure indicators improved materially and the guest retained usable headroom without OOM/ANR. Do not cut to 4096 MB now.','0',NULL,'2026-08-28T23:49:00Z','KEEP means current development default, not a permanent promoted performance fact.'); INSERT OR REPLACE INTO lab_meta(key,value) VALUES -('latest_closed_run_session','2026-08-28T23-31-16-637Z-df54ebaa-561a-4567-ab20-d94baf0a3619; 3860.24s; mactician_compatible_5gb_v1; raw SEALED; normalized COMPLETE; 18/18 required artifacts present.'), +('latest_closed_run_session','2026-08-28T23-31-16-637Z-df54ebaa-561a-4567-ab20-d94baf0a3619; 3860.24s; tftmac_5gb_baseline_v1; raw SEALED; normalized COMPLETE; 18/18 required artifacts present.'), ('latest_closed_run_memory','Full-run weighted means: emulator CPU 171.87%, RSS 5829 MiB, host available 5.425 GiB, host compressed 2.793 GiB, guest available 2.224 GiB. Heavy windows reproduced about 1.70-1.90 GiB guest headroom. Keep 5120 MB and defer/reject 4096 MB for now.'), ('latest_closed_run_pageouts','10711 pageouts / 3860.24s = 166.48 per minute including cold boot. Excluding first 10 minutes gives about 127.94 per minute, close to prior 5 GiB run 133.54 per minute and below old 6 GiB baseline 207.18 per minute. Treat as repeated directional support, not perfectly matched workload proof.'), ('latest_closed_run_audio','CoreAudio sustained: explicit backend, 0 ranchu pcm_write I/O errors, active TFT 44.1 kHz stereo OpenSL ES track, 0 mixer underruns.'), ('latest_closed_run_network','No emulator/Android network disconnect reproduced: TFT PID stayed 5276; network 101 stayed assigned; end-of-game callback requests were immediately reassigned to network 101.'), ('latest_closed_run_presentation','Surface remains 1280x720 scaled to 1920x1080; post-run total missed=562 and HWC missed=562 at 60 Hz. Zero-valued GPU counter parsing was fixed so zero no longer becomes NULL. Next graphics intervention remains ASG drawFlushInterval 800 -> 400 only.'), ('current_5gb_operational_decision','KEEP 5120 MB as development runtime. Two long runs reproduce lower pressure/pageout direction versus 6144 MB while maintaining adequate heavy-game guest headroom; 4096 MB remains deferred.'), -('current_graphics_experiment','Next one-factor candidate: mactician_compatible_5gb_flush400_v1, drawFlushInterval 800 -> 400 only. Hold RAM, in-game settings, CoreAudio, resolution, vCPU, ANGLE, Vulkan, gfxstream and MoltenVK constant.'), +('current_graphics_experiment','Next one-factor candidate: tftmac_5gb_flush400_exp_v1, drawFlushInterval 800 -> 400 only. Hold RAM, in-game settings, CoreAudio, resolution, vCPU, ANGLE, Vulkan, gfxstream and MoltenVK constant.'), ('latest_graphics_quality_observation','2026-08-28 late game: user changed TFT from Medium to Ultra High, observed Ultra High as unplayable due to severe lag, then returned to Medium and playability recovered. Exact switch timestamps were not instrumented in this legacy run, so do not assign telemetry bins or inferred FPS values to the Ultra High interval.'), ('current_tft_graphics_observed','Medium'), ('ultra_high_current_usability','REJECT for current runtime usability until new native-app FPS/config instrumentation can quantify the boundary; direct user observation is decisive for usability but not causal pipeline attribution.'); @@ -664,3 +664,135 @@ COMMIT; -- SELECT * FROM v_open_hypotheses; -- SELECT * FROM v_experiment_scorecard; -- SELECT * FROM v_promoted_facts; + +-- --------------------------------------------------------------------------- +-- 2026-08-30 native AppKit/Metal runtime authority +-- Historical 5040/5592/6144 rows above remain immutable experiment receipts. +-- --------------------------------------------------------------------------- +INSERT OR REPLACE INTO runtime_configs ( + id,parent_config_id,name,emulator_version,platform_tools_version,system_image_package,system_image_revision, + avd_name,adb_serial,adb_server_port,emulator_console_port,vcpu,ram_mb, + display_width,display_height,density_dpi,refresh_hz,gpu_mode,audio_enabled, + graphics_transport,angle_mode,vulkan_mode,moltenvk_mode,presentation_mode,state,created_at,notes +) VALUES +('tftmac_5gb_native_v1','tftmac_5gb_baseline_v1','TFTMAC native full-screen 5 GiB baseline','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7,'TFT_Ultra_Tablet','emulator-5582',5038,5582,6,5120,1920,1080,320,60.0,'host',1,'virtio-gpu-asg','GuestAngle + explicit exposeNonConformantExtensionsAndVersions/exposeES32ForTesting compatibility adapter','ranchu guest Vulkan','gfxstream host Vulkan -> MoltenVK/Metal','authenticated raw gRPC -> AppKit Metal full screen','CONTROL','2026-08-30T08:40:36Z','Donor-compatible logged-in-session launcher: /usr/bin/open -n -W through packaged TFTMAC Emulator Host.app; ADB_VENDOR_KEYS absent. Native lobby acceptance passed; source/presentation counters are transport metrics, not Unreal engine FPS.'); + +INSERT OR REPLACE INTO sessions( + id,runtime_config_id,started_utc,ended_utc,host_start_mono_ns,host_end_mono_ns, + boot_class,workload_class,package_name,package_version_name,package_version_code, + package_state_sha256,renderer_state_sha256,session_manifest_sha256, + package_updated_during_session,capture_state,semantic_valid,invalid_reason,notes +) VALUES +('2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94','tftmac_5gb_native_v1','2026-08-30T08:40:36Z',NULL,NULL,NULL,'COLD','LOBBY','com.riotgames.league.teamfighttactics','18.1-5402721','8402721',NULL,NULL,NULL,0,'PARTIAL',1,NULL,'Native full-screen lobby acceptance; capture was still open when this SSOT receipt was written.'); + +INSERT OR REPLACE INTO lab_meta(key,value) VALUES +('current_playable_baseline','tftmac_5gb_native_v1'), +('current_native_runtime_config','tftmac_5gb_native_v1'), +('current_native_acceptance_session','2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94'), +('current_runtime_identity','ADB 5038; console 5582; serial emulator-5582; packaged app host via /usr/bin/open; no injected ADB_VENDOR_KEYS'), +('current_native_frame_truth','Raw gRPC source-window rate and Metal presentation rate are transport/output metrics only. Do not label either as Unreal engine FPS.'), +('current_native_logger','Private local capture: SQL receipts/events, one-second frame interval windows, presentation, qemu CPU/RSS, guest memory, clock sync, SurfaceFlinger deltas, AudioFlinger health, aggregate logcat signals, game PID sessions, input counts and explicit match/stutter markers.'), +('current_surfaceflinger_counter_policy','Sample the live dumpsys SurfaceFlinger counters every 30 seconds during gameplay and at session boundaries. Compare deltas inside marked windows; absolute counters remain cumulative since boot.'), +('current_optimization_priority','Hold 1920x1080/320 dpi, 5038/5582, 5120 MiB, 6 vCPU, CoreAudio, host GPU and the renderer chain. Run one restart-bound variable A/B at a time and require cold confirmation before promotion.'); + +INSERT OR REPLACE INTO evidence(id,hypothesis_id,session_id,experiment_id,evidence_type,claim,relation,strength,source_artifact_id,created_at,notes) VALUES +('ev_native_fullscreen_acceptance_20260830',NULL,'2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94',NULL,'DIRECT_MEASUREMENT','Native TFTMAC filled the 1920x1080 display in an AppKit full-screen window, authenticated Emulator37.1.11 gRPC, authorized emulator-5582 on ADB5038, rendered 1920x1080 RGBA through Metal at about 60 output presentations/s, and launched official TFT18.1 into Unreal GameActivity.','NEUTRAL','DECISIVE',NULL,'2026-08-30T08:41:35Z','Capture 2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94. Lobby/input/software-audio path proven; full-match and user-audible acceptance remain separate.'); + +-- --------------------------------------------------------------------------- +-- 2026-08-31 Build 8 automatic full-run capture: current causal baseline +-- This preserves older measurements while replacing neither their rows nor +-- their historical conclusions. The raw SQLite capture remains private. +-- --------------------------------------------------------------------------- +INSERT OR REPLACE INTO runtime_configs ( + id,parent_config_id,name,emulator_version,platform_tools_version,system_image_package,system_image_revision, + avd_name,adb_serial,adb_server_port,emulator_console_port,vcpu,ram_mb, + display_width,display_height,density_dpi,refresh_hz,gpu_mode,audio_enabled, + graphics_transport,angle_mode,vulkan_mode,moltenvk_mode,presentation_mode,state,created_at,notes +) VALUES ( + 'tftmac_stock_build8_high60_control','tftmac_5gb_native_v1','Stock Build 8 High/60 normal-play control','37.1.11','37.0.1','system-images;android-36;google_apis_playstore;arm64-v8a',7, + 'TFT_Ultra_Tablet','emulator-5582',5038,5582,6,5120,1920,1080,320,60.0,'host',1, + 'virtio-gpu-asg','Conditional: do not assume ANGLE is TFT main rendering path','Unreal direct Vulkan observed','gfxstream host Vulkan -> MoltenVK/Metal','authenticated raw gRPC -> AppKit Metal full screen','CONTROL','2026-08-31T22:30:26Z', + 'Normal-play authority. Active observed experiment combat_latency_a uses High/60/Performance Mode OFF and is not a promoted performance intervention.' +); + +INSERT OR REPLACE INTO sessions( + id,runtime_config_id,started_utc,ended_utc,host_start_mono_ns,host_end_mono_ns, + boot_class,workload_class,package_name,package_version_name,package_version_code, + package_state_sha256,renderer_state_sha256,session_manifest_sha256, + package_updated_during_session,capture_state,semantic_valid,invalid_reason,notes +) VALUES ( + '2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','tftmac_stock_build8_high60_control','2026-08-31T22:31:03Z','2026-08-31T23:13:30Z',NULL,NULL, + 'UNKNOWN','MIXED','com.riotgames.league.teamfighttactics','18.1-5402721','8402721',NULL,NULL,NULL, + 0,'COMPLETE',1,NULL,'42m27s automatic PID/layer-lifetime run. Markers and battle labels are optional annotations; no marker is required for validity or causal analysis.' +); + +INSERT OR REPLACE INTO artifacts(id,session_id,experiment_id,artifact_kind,path,sha256,byte_count,required,state,created_at,notes) VALUES +('artifact_build8_current_private_db','2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200',NULL,'PRIVATE_SQLITE_CAPTURE','PRIVATE_LOCAL_ONLY/TFTMAC_NATIVE_RUNTIME.sqlite','c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77',63897600,1,'PRESENT','2026-08-31T23:13:30Z','Metadata and integrity hash only; raw database is intentionally excluded from Git.'); + +INSERT OR REPLACE INTO experiments VALUES +('exp_build8_automatic_full_run',NULL,'Build 8 automatic full-run graphics observation','OBSERVATION','tftmac_stock_build8_high60_control',NULL,'MIXED',0,'COMPLETE',0,'Automatic PID/layer lifetime, exact TFT layer and receipt linkage; markers/battle labels are optional and have no validity role.','2026-08-31T22:30:26Z','2026-08-31T23:13:30Z','Diagnostic observation only; establishes current performance facts and preserves root attribution as UNKNOWN.'); + +INSERT OR REPLACE INTO experiment_sessions(experiment_id,session_id,role) VALUES +('exp_build8_automatic_full_run','2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','DIAGNOSTIC'); + +INSERT INTO metrics(session_id,experiment_id,metric_scope,metric_name,metric_value,unit,source_artifact_id,semantic_valid,notes) VALUES +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','weighted_fps',56.98,'fps','artifact_build8_current_private_db',1,'Exact TFT SurfaceFlinger actual-present authority.'), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','fps_1_percent_low',21.49,'fps','artifact_build8_current_private_db',1,'Exact TFT SurfaceFlinger actual-present authority.'), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','p50_frame_interval',16.707,'ms','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','p95_frame_interval',21.51,'ms','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','p99_frame_interval',33.434,'ms','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','max_frame_interval',2233.611,'ms','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','jank_count',6544,'frames','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','jank_percent',4.53,'percent','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','severe_stall_count',144,'windows','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','missed_vsync_equivalent_count',7644,'frames','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','exact_layer_coverage',99.629,'percent','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','frame_interval_count',144364,'intervals','artifact_build8_current_private_db',1,NULL), +('2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','GRAPHICS_RUN','degradation_incident_count',189,'incidents','artifact_build8_current_private_db',1,'Incident count is aggregate; no battle classifier is used as a causal gate.'); + +INSERT OR REPLACE INTO evidence(id,hypothesis_id,session_id,experiment_id,evidence_type,claim,relation,strength,source_artifact_id,created_at,notes) VALUES +('ev_build8_current_full_run',NULL,'2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','DIRECT_MEASUREMENT','Build 8 automatically captured a complete 42m27s TFT graphics run with 144364 exact-layer frame intervals and 99.629% exact-layer coverage. The observed direct path is Unreal Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal; ANGLE is conditional, final Mac presentation is context-only, and the first causal boundary remains UNKNOWN.','NEUTRAL','DECISIVE','artifact_build8_current_private_db','2026-08-31T23:13:30Z','No raw database, screenshots, credentials, or frame payloads are committed.'), +('ev_build8_current_root_unknown',NULL,'2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200','exp_build8_automatic_full_run','TRACE_CORRELATION','Current automatic logging proves degradation but cannot identify an internal owned root component because no shared guest-to-host causal work ID exists.','NEUTRAL','DECISIVE','artifact_build8_current_private_db','2026-08-31T23:13:30Z','Do not infer ASG, gfxstream, MoltenVK, Metal, ANGLE, or Unreal ownership from the present capture.'); + +INSERT OR REPLACE INTO decisions(id,experiment_id,decision,rationale,cold_confirmation_complete,promoted_config_id,decided_at,notes) VALUES +('decision_build8_current_full_run','exp_build8_automatic_full_run','DIAGNOSTIC_ONLY','The capture verifies automatic logging and current full-run degradation metrics, but it is not a controlled promotion and root attribution is UNKNOWN.',0,NULL,'2026-08-31T23:13:30Z','Keep stock Build 8 as normal-play authority; advanced source causal logging is planned.'); + +INSERT OR REPLACE INTO unknowns(id,question,boundary,status,blocking,resolution_evidence_id,opened_at,resolved_at,notes) VALUES +('u_current_internal_root_cause','Which owned graphics component first diverges before TFT SurfaceFlinger actual-present degradation?','CROSS_STACK_CAUSAL','OPEN',1,NULL,'2026-08-31T23:13:30Z',NULL,'Requires the planned source-instrumented causal logger and complete work-ID joins.'); + +INSERT OR REPLACE INTO lab_meta(key,value) VALUES +('current_playable_baseline','tftmac_stock_build8_high60_control'), +('current_playable_baseline_session','2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200'), +('current_release_gameplay_benchmark','VERIFIED_CAPTURE_ROOT_ATTRIBUTION_UNKNOWN'), +('current_runtime_mode','STOCK_BUILD8_NORMAL_PLAY_AUTHORITY'), +('current_active_experiment','combat_latency_a observed High/60/Performance Mode OFF; not promoted'), +('current_renderer_path','Unreal direct Vulkan -> gfxstream/ASG -> host Vulkan -> MoltenVK -> Metal; ANGLE conditional only'), +('current_mac_presenter_policy','Final Mac presenter is excluded from causal candidates and retained only as context/correctness telemetry.'), +('current_marker_policy','MATCH_ENTRY, MATCH_END, combat, battle, and quality markers are optional annotations only; automatic process/layer lifetime defines capture validity.'), +('current_root_attribution','UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE'), +('advanced_source_causal_logger','PLANNED: isolated non-comparable tftmac-runtime diagnostic build; no causal code-site claim until complete work-ID joins, valid clocks, sealed streams, and overhead gate pass.'); + +UPDATE experiments +SET state='CANCELLED', + notes=COALESCE(notes,'') || ' Historical candidate retained; superseded as current work by the verified Build 8 full-run baseline and planned source causal logger. Do not execute as an active tuning recommendation.' +WHERE id IN ('exp_asg_flush400_ab','exp_native_frame_trace','exp_tft_fps_cap_ab','exp_tft_graphics_preset_ab','exp_tft_performance_mode_ab') + AND state='PLANNED'; + +INSERT OR REPLACE INTO lab_meta(key,value) VALUES +('current_presentation_candidate','RETIRED_AS_CURRENT_ACTION: final Mac presentation and broad SurfaceFlinger/HWC directional candidates are context only; causal changes wait for the planned source causal logger.'), +('current_graphics_next_action','Build and validate the isolated source causal logger. Do not run a tuning A/B until it identifies an owned first divergent boundary or explicitly reports that the first missing boundary is unowned.'); + +-- Exact-key overrides for the Build 8 authority. Earlier values remain in the +-- SQL history but cannot survive as the effective current policy. +INSERT OR REPLACE INTO lab_meta(key,value) VALUES +('logger_guard_policy','Automatic logging follows the TFT process and exact layer from game start until process/app close. MATCH_ENTRY, MATCH_END, combat, battle, quality, and result markers are optional annotations only.'), +('multi_match_policy','Keep raw telemetry continuous across every game. Process/layer lifetime and timestamps define validity; marker pairing and battle classification are never required.'), +('current_measurement_gap','Exact TFT SurfaceFlinger actual-present timing is implemented and verified. Internal causal attribution below the guest TFT surface remains unimplemented because owned-stage work IDs are absent.'), +('current_tft_graphics_observed','High'), +('current_tft_fps_cap_observed','60'), +('current_tft_performance_mode_beta_observed','OFF'), +('current_native_frame_truth','Exact TFT SurfaceFlinger actual-present timestamps are FPS authority. Final Mac presentation is hidden correctness context only and is excluded from causal ranking.'), +('current_native_logger','Automatic private SQL capture covers the TFT PID/layer lifetime, exact intervals/windows, receipts, incidents, and bounded supporting telemetry. Markers are optional annotations.'), +('current_surfaceflinger_counter_policy','Exact TFT actual-present intervals and one-second windows are primary. Cumulative counters are supporting context only and do not require marked windows.'), +('current_optimization_priority','Analyze all automatic full-run graphics data. Do not tune or name a root component until the planned source causal logger proves the first divergent owned boundary or explicitly reports UNKNOWN.'), +('current_graphics_experiment','No tuning A/B is active. The next development phase is the PLANNED isolated source causal logger; combat_latency_a is an observed High/60/Performance Mode OFF preset, not a promoted result.'); diff --git a/ssot/TFTMAC_REBUILD_PREFLIGHT.md b/ssot/TFTMAC_REBUILD_PREFLIGHT.md deleted file mode 100644 index c132664..0000000 --- a/ssot/TFTMAC_REBUILD_PREFLIGHT.md +++ /dev/null @@ -1,922 +0,0 @@ -# TFTMAC Rebuild Preflight — Fact Base Before Specification - -**Status:** ACTIVE PREFLIGHT — specification is intentionally not frozen yet -**Date:** 2026-08-28 -**Purpose:** Replace assumption-driven implementation with a fact-driven product specification using current TFTMAC evidence, the successful Mactician run on the target Mac, current Mactician open-source code, and explicit product requirements. - ---- - -## 0. Governing rule - -This document is the decision boundary between research and the next TFTMAC specification. - -The next spec may contain only: - -1. facts already proven by current TFTMAC evidence; -2. facts reproduced on the target Mac; -3. source-verified donor behavior that we deliberately choose to adopt; -4. explicit product requirements from the user; -5. clearly labeled unresolved decisions with a bounded preflight probe. - -The existing `TFTMAC_GPU_RUNTIME_SSOT.md` and `TFTMAC_FULL_IMPLEMENTATION_PLAN.md` remain historical planning inputs, but their source-built-AEMU critical path is **not automatically authoritative for the rebuild**. No new source-AEMU work should resume merely because the old plan says to do so. - -Donor research remains quarantined in: - -`ssot/donors/TFTMAC_DONOR_RESEARCH.sql` - -Nothing enters production truth merely because Mactician or another donor did it. - ---- - -# 1. Evidence classes - -Every fact in the rebuild must be attributable to one of these classes. - -| Class | Meaning | -|---|---| -| `PROJECT_PROVEN` | Proven by current TFTMAC project artifacts or probes. | -| `TARGET_HOST_PROVEN` | Observed directly on the target Mac Mini M4. | -| `DONOR_SOURCE_VERIFIED` | Present in current open-source donor code/manifests. | -| `DONOR_AUTHOR_MEASURED` | Published by the donor author with a described controlled measurement. | -| `TARGET_RUNTIME_OBSERVED` | Observed in the actual Mactician runtime/log on this Mac. | -| `USER_OBSERVED` | Direct user observation during actual use; requires instrumentation before becoming a quantitative engineering claim. | -| `PRODUCT_REQUIREMENT` | Required behavior regardless of donor implementation. | -| `UNKNOWN` | Must be resolved before the corresponding spec section is frozen. | - ---- - -# 2. Target Mac — proven facts - -Source: `ssot/host-preflight.json`, `ssot/STACK.lock.yaml`. - -| Fact | Value | Class | -|---|---|---| -| Host | Mac mini | `PROJECT_PROVEN` | -| Model | Mac16,10 | `PROJECT_PROVEN` | -| Chip | Apple M4 | `PROJECT_PROVEN` | -| RAM | 16 GB | `PROJECT_PROVEN` | -| Architecture | arm64 | `PROJECT_PROVEN` | -| macOS | 26.6.2 build 25G83 | `PROJECT_PROVEN` | -| Installed Xcode | 26.6 / 17F113 | `PROJECT_PROVEN` | -| Additional Xcode | 26.4 / 17E192 | `PROJECT_PROVEN` | -| Current macOS SDK | 26.5 | `PROJECT_PROVEN` | -| Google Emulator control version already known | 37.1.11 | `PROJECT_PROVEN` | - -### Rebuild consequence - -The target host is already proven capable of running Google Android Emulator 37.1.11 with hardware acceleration and the Mactician graphics path. The rebuild does not need to prove basic Apple-Silicon virtualization feasibility again. - ---- - -# 3. Storage — new hard direction - -## 3.1 Proven/current state - -The old TFTMAC path created a very large AEMU source/build workspace on `/Volumes/MAC MINI M4/TFTMAC` and made the external volume mandatory for Build and Runtime roots. - -Mactician demonstrates a much thinner product model: - -- it ships a native Mac launcher rather than an AEMU source tree; -- it downloads verified released Android components; -- its current README requires 25 GiB of free disk headroom for downloads, extraction, the AVD, game assets, and updates; -- its documented initial download is about 2.3 GB before extraction and provisioning. - -Sources: -- https://github.com/tweet9ra/mactician -- https://sergeinaumov.dev/writing/how-i-built-mactician - -## 3.2 Product requirements - -1. **No 100+ GB or ~200 GB source/build tree is a production dependency.** -2. Source-built AEMU becomes optional research material only unless a measured blocker requires a custom emulator binary. -3. TFTMAC runtime storage must be a bounded installed runtime, not a development checkout. -4. Runtime location must not be hardcoded to one external volume. -5. Default runtime may live in normal macOS Application Support when space permits. -6. A user-selectable external runtime location may be supported for large game/runtime state. -7. The installer must show required and expected disk space before downloading. -8. A cleanup inventory must distinguish: - - shippable runtime; - - caches safe to delete; - - user game/AVD state; - - optional development source; - - diagnostic artifacts. -9. Stateful AVD images must never be placed in File Provider/cloud-offloaded storage. - -## 3.3 Preflight unknown - -`UNKNOWN-STORAGE-01`: Measure a clean direct-Google TFTMAC runtime after install and after one fully patched live TFT session. Freeze exact installed-size budget in the spec only after measurement. - ---- - -# 4. Mactician current upstream — source facts we can legally use - -Current public repository: - -`https://github.com/tweet9ra/mactician` - -License: MIT. The license explicitly allows use, copy, modification, merge, publication, distribution, sublicensing, and sale, provided the copyright/permission notice is retained in copies or substantial portions. - -Current repository facts observed 2026-08-28: - -| Area | Current donor fact | Class | -|---|---|---| -| App | Native SwiftUI launcher | `DONOR_SOURCE_VERIFIED` | -| Version | 1.1.0 build 45 | `DONOR_SOURCE_VERIFIED` | -| Minimum macOS | 12.0 | `DONOR_SOURCE_VERIFIED` | -| Emulator | Google Android Emulator 37.1.11 | `DONOR_SOURCE_VERIFIED` | -| Guest generation | Android 36 ARM64 | `DONOR_SOURCE_VERIFIED` | -| Installer | Direct verified Android component downloads | `DONOR_SOURCE_VERIFIED` | -| Integrity | SHA-256 verification of runtime components/game splits | `DONOR_SOURCE_VERIFIED` | -| Lifecycle | `needsInstall -> installing -> ready -> launching -> playing -> stopping`, plus failed/recovery states | `DONOR_SOURCE_VERIFIED` | -| AVD ownership | Dedicated AVD create/provision/start/stop/repair/reset | `DONOR_SOURCE_VERIFIED` | -| Settings | Resolution, UI scale, Android RAM, vCPU, language, graphics controls | `DONOR_SOURCE_VERIFIED` | -| FPS | Native FPS overlay service exists | `DONOR_SOURCE_VERIFIED` | -| Audio | Emulator audio recovery service exists | `DONOR_SOURCE_VERIFIED` | -| Input | Native macOS input/hotkey bridge exists | `DONOR_SOURCE_VERIFIED` | -| Recovery | Repair and destructive Reset are distinct | `DONOR_SOURCE_VERIFIED` | -| Updates | Sparkle update mechanism for Mac app | `DONOR_SOURCE_VERIFIED` | -| Diagnostics | Optional extended diagnostics + telemetry notice/service | `DONOR_SOURCE_VERIFIED` | -| Release | Developer ID signing, hardened runtime, notarization | `DONOR_SOURCE_VERIFIED` / author-documented | - -Relevant source layout includes: - -- `launcher/Sources/CoreModels.swift` -- `launcher/Sources/InstallerService.swift` -- `launcher/Sources/LauncherModel.swift` -- `launcher/Sources/LauncherSettingsView.swift` -- `launcher/Sources/LauncherStateViews.swift` -- `launcher/Sources/LauncherTheme.swift` -- `launcher/Sources/LauncherView.swift` -- `launcher/Sources/RuntimeController.swift` -- `launcher/Sources/FPSOverlayService.swift` -- `launcher/Sources/EmulatorAudioRecoveryService.swift` -- `launcher/Sources/InputBridgeService.swift` -- `launcher/Sources/HostedGameUpdate.swift` -- `launcher/Sources/LauncherTelemetryService.swift` -- `launcher/Sources/LauncherUpdateController.swift` - -### Rebuild consequence - -We do not need to reinvent the launcher/state-management/product-shell patterns. We may fork or adapt the MIT implementation and then replace the game-delivery, fullscreen/presentation, performance, branding, and runtime policies that do not meet TFTMAC requirements. - ---- - -# 5. Proven working graphics recipe from Mactician - -Author engineering source: - -`https://sergeinaumov.dev/writing/how-i-built-mactician` - -The donor's working conceptual path is: - -```text -TFT OpenGL ES - -> Android ANGLE - -> Vulkan - -> Android Emulator gfxstream / MoltenVK - -> Apple Metal -``` - -The donor proved that forcing the game itself to direct Vulkan was not the correct boundary. The working route kept the game's GLES renderer and translated beneath it. - -The donor also proved: - -- native Android Emulator presentation was substantially better than scrcpy because scrcpy added guest CPU video encoding and incorrect input semantics; -- zsh `BG_NICE` could silently reduce QEMU priority to nice=5; normal priority mattered; -- simply adding more vCPU/RAM did not cure the active bottleneck; -- DeviceProfiles/runtime behavior could matter more than visible in-game graphics settings; -- ASG transport materially outperformed legacy `pipe` in a controlled scene; -- cold repeated measurements are mandatory because one-run wins frequently disappeared. - -These are engineering priors, not automatic current settings. - ---- - -# 6. Actual successful Mactician run on this exact Mac - -Source: copied live runtime log at `artifacts/mactician-live.log` plus direct user observation. - -## 6.1 Host/guest runtime facts observed - -Mactician successfully launched a runtime on the Apple M4 with: - -- Android Emulator 37.1.11; -- Android 36 ARM64 Google APIs image; -- gfxstream; -- Apple M4 selected through MoltenVK; -- Vulkan 1.4.x host path reported by MoltenVK; -- guest ANGLE; -- `virtio-gpu-asg` graphics transport; -- ASG write buffer 1,048,576 bytes; -- ASG write step 16,384 bytes; -- ASG data ring 32,768 bytes; -- 1920x1080 framebuffer; -- 320 DPI; -- 60 Hz; -- 6 guest cores; -- 6144 MB guest RAM; -- audio enabled; -- asynchronous MoltenVK queue submission; -- 64 active Metal command buffers; -- fast math; -- PSO watcher/scheduling adjustments; -- donor graphics profile identified in the log as `osft`; -- `preferSubmitAtFBOBoundary` disabled in ANGLE. - -The successful log explicitly reported: - -```text -TFT is running: Unreal OpenGL ES -> guest ANGLE -> Vulkan -> Metal -``` - -and a real TFT process was launched. - -## 6.2 Important conformance fact - -The successful donor runtime also reported: - -```text -ANGLE enabled features: exposeNonConformantExtensionsAndVersions:exposeES32ForTesting -``` - -and exposed GLES 3.2 to the guest. - -This is a **donor compatibility technique**, not automatically a TFTMAC shipping requirement. The old TFTMAC SSOT forbids claiming genuine GLES 3.2 merely through nonconformant version exposure. - -The rebuild spec must explicitly choose between: - -A. compatibility-first parity with the donor exposure mechanism; or -B. genuine feature-complete ES 3.2 without the exposure shortcut. - -The decision must be based on whether current live TFT actually executes features that the host/guest path cannot provide, not on ideology or version strings. - ---- - -# 7. Actual game/update failure we observed - -## 7.1 Successful user-visible sequence - -The actual user experience was: - -1. Mactician installed successfully after one reinstall retry. -2. TFT launched. -3. TFT downloaded a patch. -4. The user logged in. -5. The user joined a party with another current TFT player. -6. When the party attempted to start a game, Riot reported that the game versions did not match. -7. The user restarted Mactician to allow the patch/update state to settle. -8. Subsequent launches failed. - -## 7.2 Log evidence - -Mactician repeatedly reported: - -```text -Hosted TFT feed unavailable, using bundled fallback: Invalid TFT release -``` - -or: - -```text -Game update availability check failed: Invalid TFT release -``` - -After the failed restart, Android itself still booted successfully, but Mactician then encountered: - -```text -stat: '/data/user/0/com.riotgames.league.teamfighttactics': No such file or directory -The owner of TFT private data could not be determined. -``` - -Mactician then intentionally shut down the emulator. The visible `Snapshots have been disabled by the user` message occurred during shutdown and was not the root cause. - -## 7.3 Product consequence - -**TFTMAC must not depend on a third-party hosted game-package feed for current live TFT.** - -The next architecture uses authoritative upstream delivery directly: - -```text -Google Android repository -> Android runtime components -Google Play -> TFT application install/update authority -Riot services -> in-game patch/content/authentication/matchmaking -``` - -No Sergei/Mactician game feed is a production dependency. - -No public APK mirror is a production dependency. - -No Riot APK is committed to the repository. - ---- - -# 8. Direct Google/Riot delivery — required architecture - -## 8.1 Android runtime components - -TFTMAC downloads required Google Android components directly from official Google endpoints and verifies them locally. - -Mactician already source-verifies its Android SDK component URLs to `https://dl.google.com`, proving this distribution pattern is simple and productized. - -## 8.2 TFT package authority - -Primary requirement: - -- use a Google Play-enabled guest where practical; -- user signs into Google Play inside Android; -- install official `com.riotgames.league.teamfighttactics` through Google Play; -- Google Play handles APK/split delivery and application updates; -- Riot handles its own in-game content patching; -- TFTMAC records installed package name, versionName, versionCode, signature/certificate identity, and split inventory for diagnostics; -- TFTMAC never invents a current version number from a third-party feed. - -## 8.3 Update UX - -The launcher needs explicit states: - -```text -Game missing -Game installed -Google Play update required/check requested -Riot content patching/incomplete -Ready to play -Version mismatch/recovery guidance -``` - -The launcher must not treat a missing private-data directory as an opaque fatal crash. It must identify whether the package is missing, not initialized, mid-update, or damaged and present a targeted repair path. - ---- - -# 9. Guest choice — not yet frozen - -There are two materially different donor/current paths: - -### Candidate A — single official Google Play guest - -- official Play-enabled ARM64 Android image; -- Google Play directly owns TFT install/update; -- no root dependency; -- simplest user product; -- preferred architecture if graphics/performance requirements can be met. - -### Candidate B — authority/execution split, only if required - -- Play-enabled guest is package/update authority; -- rootable execution guest is used only if required for runtime-only graphics/profile controls; -- exact official package splits may be transferred locally only after identity/signature/hash verification; -- no third-party package feed. - -### Current rule - -Do **not** build Candidate B speculatively. - -`UNKNOWN-GUEST-01`: Prove whether Candidate A can launch current live TFT with the required graphics path and acceptable performance. Only if it fails for a root-owned requirement may Candidate B enter the spec. - -Android 36 and the existing API 37 Play image are both evidence-bearing candidates. Do not select based on newest/oldest labels alone. - ---- - -# 10. Source-built AEMU — demoted from critical path - -## Proven facts - -The old plan required building AEMU from `emu-master-dev`. Phase 1 encountered a sequence of host-build compatibility problems and most recently failed because the build adapter forced a macOS 10.14 deployment target while source required `std::filesystem` availability from 10.15+. - -Separately, Mactician proves that stock Google Emulator 37.1.11 can execute the relevant graphics stack on Apple Silicon. - -## New rule - -The normal rebuild starts with the stock released Google emulator. - -Source-built AEMU is justified only by a specific measured requirement such as: - -- a missing emulator feature that cannot be enabled/configured in stock; -- a confirmed gfxstream defect requiring a patch; -- native presentation integration impossible through supported stock boundaries; -- a performance bottleneck proven inside AEMU/gfxstream source rather than elsewhere. - -If none of those occurs, no source AEMU build is part of production. - ---- - -# 11. Performance — current facts and new acceptance behavior - -## 11.1 Donor measured facts - -Mactician's author reports: - -- ASG vs legacy pipe in the same early combat scene: about **40.1 FPS vs 29.6 FPS**; -- reproducible control series around **40.60 / 36.03 / 27.83 FPS** at progressively heavier stages; -- the heavy-stage target of 57 FPS was **not reached**; -- 128 Metal command buffers looked good once but failed cold confirmation; -- forced submit thread regressed performance; -- aggressive PSO prewarm caused crashes; -- higher resolution was not always materially slower because tested scenes were often CPU/RHI/transport bound. - -This donor evidence means a lobby overlay showing 60 FPS is not sufficient evidence of a smooth product. - -## 11.2 User-observed target-Mac behavior - -The user observed: - -- approximately 60 FPS in light conditions; -- dramatic transient drops, including a reported drop to roughly 6 FPS while entering/loading activity; -- experience felt glitchy despite the 60 FPS overlay. - -This is `USER_OBSERVED`, not yet a controlled benchmark. - -## 11.3 Product requirement - -TFTMAC performance acceptance must include: - -- lobby/light scene; -- game transition/load into match; -- early combat; -- middle combat; -- late/heavy combat; -- shop/board rapid interaction; -- a second warm match after caches are populated. - -Metrics: - -- presented FPS; -- p50/p95/p99 frame interval; -- >33.3 ms jank percentage; -- >100 ms stalls; -- host CPU; -- host GPU where measurable; -- guest CPU saturation; -- memory pressure; -- shader/PSO worker activity; -- input-to-visible-response latency. - -The existing TFTMAC 60 Hz contract remains a candidate target: - -```text -average >= 58 FPS -p95 <= 20 ms -p99 <= 33.334 ms -jank <= 1% -``` - -but the spec must pair these with heavy-scene evidence rather than averages that can hide 6–20 FPS collapses. - -`UNKNOWN-PERF-01`: Establish the target-M4 stock-emulator baseline with the donor-compatible graphics path using one fixed heavy scene and one transition measurement before selecting optimization work. - ---- - -# 12. Resource allocation - -## Facts - -- target Mac has 16 GB total RAM; -- actual Mactician run used 6 vCPU / 6144 MB; -- donor author found that blindly adding CPU/RAM was not causal on his M1 Max; -- TFTMAC historical profiles already considered 6/6144 and 8/8192. - -## Requirement - -Do not hardcode “more is better.” - -Initial target-M4 control: - -```text -6 vCPU / 6144 MB -``` - -Only one bounded comparison is justified initially: - -```text -6/6144 control -vs -one alternate profile chosen from measured host pressure -``` - -No broad CPU/RAM matrix. - ---- - -# 13. Native macOS UI — required product direction - -## Donor facts - -Mactician already provides a solid native SwiftUI product shell with: - -- install/progress UI; -- Play/Stop lifecycle; -- settings menus; -- resolution; -- UI scale; -- CPU/RAM controls; -- graphics controls; -- language; -- hotkeys; -- Repair; -- Reset; -- app update controls; -- diagnostics/feedback concepts; -- clear state-specific UI. - -Mactician intentionally keeps the launcher and the running Android Emulator window side by side and provides a macOS **window-fill** shortcut. This is not the same as one native fullscreen game application. - -## New hard requirements - -TFTMAC must: - -1. be a native macOS application; -2. have normal macOS menu-bar behavior; -3. support true macOS fullscreen using the green-window/fullscreen convention and keyboard shortcut; -4. present the game without visible Android Emulator chrome in normal play; -5. make the game visually feel like the application content, not a second utility window the user has to resize; -6. preserve direct/high-performance presentation and must not regress to a scrcpy-like encode/stream path; -7. preserve correct mouse, keyboard, scroll, right-click and focus semantics; -8. provide seamless exit from fullscreen; -9. handle display changes and Retina scaling without making the guest framebuffer lie about its real render size; -10. support 1920x1080 as the initial guaranteed profile and adapt the native window to the physical display. - -`UNKNOWN-UI-01`: Select and prove the presentation boundary. The prototype must compare direct emulator presentation against the chosen native fullscreen integration for FPS, frame-time, input latency and window behavior. The implementation may use a custom host/helper only if it preserves the direct-rendering characteristics. - -This unknown must be resolved **before** the final UI architecture is frozen. - ---- - -# 14. Display and render profiles - -Initial guaranteed product target: - -```text -1920x1080 @ 60 Hz -``` - -Requirements: - -- fullscreen window size and actual guest render size are separately measurable; -- no stretched 1280x720 pretending to be 1920x1080; -- UI scaling is independently configurable from actual render resolution; -- dynamic resolution must be observable and not silently defeat quality settings; -- higher-resolution profiles may exist later but do not ship until 1080p heavy-match performance is acceptable. - -Do not prioritize 1440p/4K while late-game 1080p performance is unstable. - ---- - -# 15. Graphics tuning — what we inherit and what we do not - -## Strong donor priors to reproduce first - -- stock Emulator 37.1.11; -- host GPU acceleration; -- ANGLE below TFT GLES; -- Vulkan beneath ANGLE; -- gfxstream; -- MoltenVK -> Metal; -- ASG instead of legacy pipe; -- 16 KiB ASG write step as a control candidate; -- normal host process scheduling priority; -- no scrcpy rendering path; -- async MoltenVK / bounded command buffers as later performance candidates; -- persistent and reversible graphics/profile changes; -- cold-run confirmation before promotion. - -## Explicitly not inherited blindly - -- nonconformant ES 3.2 exposure as final proof; -- donor `osft` profile name/assumptions; -- stale PBE package hashes; -- donor hosted TFT feed; -- private/bundled Riot APK delivery; -- root requirement unless Candidate A proves insufficient; -- 1440p as primary profile; -- 128/256 Metal command buffer experiments; -- rejected prewarm/submit-thread experiments; -- any tuning whose exact current ANGLE/gfxstream/MoltenVK version differs without remeasurement. - ---- - -# 16. Audio, microphone and voice - -## Proven - -The target runtime successfully produced game audio. Mactician includes explicit emulator audio recovery logic. - -## Requirements - -TFTMAC must support: - -- stable output audio; -- host microphone transport into Android when enabled; -- macOS microphone permission UX; -- an explicit microphone on/off control; -- no silent microphone activation; -- diagnostics that distinguish “host mic unavailable” from “Android mic unavailable” from “game does not implement voice.” - -`UNKNOWN-AUDIO-01`: Determine whether the current live TFT Android package declares/uses microphone/voice functionality. Emulator microphone transport is a product capability regardless, but the spec must not promise Riot in-game voice unless the current client supports it. - ---- - -# 17. Installer and recovery requirements - -Borrow the successful product pattern from Mactician, but remove its package-feed weakness. - -Required states: - -```text -Needs Install -Downloading Android Runtime -Creating Device -Google Play Setup Required -TFT Missing -TFT Updating -Riot Content Updating -Ready -Launching -Playing -Stopping -Repairing -Failed with specific recovery action -``` - -Required actions: - -- Install; -- Play; -- Stop; -- Check/Refresh game state; -- Open Google Play/TFT update path; -- Repair runtime without deleting game state when possible; -- Recreate AVD when runtime corruption is proven; -- Reset only as destructive last resort; -- Export diagnostics; -- optional send-feedback/send-diagnostics flow. - -Recovery must be state-aware. A missing package or private-data directory must trigger package recovery, not a generic emulator shutdown. - ---- - -# 18. Diagnostics requirements - -The launcher should expose useful diagnostics without requiring Terminal. - -Minimum UI-visible diagnostics: - -- installed TFT versionName/versionCode; -- Android version/image; -- Emulator version; -- graphics path summary; -- current framebuffer/density/refresh; -- CPU/RAM allocation; -- current FPS; -- p95/p99 frame time over recent window; -- severe-stall counter; -- audio state; -- microphone state; -- Google Play/package state; -- last recovery action; -- log export. - -Optional extended diagnostics may include host model/macOS/memory/CPU count and applied runtime profile, with explicit user consent. - -No Riot password, session token, Google credential, serial number, or unfiltered private game state should be sent by diagnostics. - ---- - -# 19. App updates vs game updates - -These are separate systems. - -### TFTMAC application update - -- signed/notarized native Mac application; -- Sparkle or equivalent signed update mechanism is acceptable; -- app bundle is replaceable without deleting runtime/user state. - -### Android runtime update - -- update only when current compatibility testing approves a new Google Emulator/system image; -- do not auto-upgrade a known-good emulator merely because Google published a newer one. - -### TFT update - -- Google Play is application package authority; -- Riot in-game updater is content authority; -- launcher observes current installed state and guides official update flow; -- no third-party hosted TFT package feed. - ---- - -# 20. Security / trust boundaries - -Preserve: - -- Riot login occurs inside official TFT client; -- Google login occurs inside Google Play/Android; -- TFTMAC does not request or proxy Riot credentials; -- no Riot APKs or Android userdata committed to Git; -- no third-party APK mirrors as production authority; -- runtime component hashes/integrity are recorded; -- native Mac release is signed/notarized; -- diagnostic upload is explicit and sanitized; -- destructive Reset requires confirmation. - ---- - -# 21. Existing TFTMAC work to retain - -The rebuild does **not** mean throwing away everything already learned. - -Retain/reuse: - -- host preflight and machine discovery; -- Google Android component/version discovery; -- current external/internal storage migration knowledge; -- package inspection tools; -- ADB isolation and serial management; -- capability probes where they answer a current question; -- performance/frame-time methodology; -- historical benchmark/rejected-experiment database; -- known ASG findings; -- MoltenVK/ANGLE/gfxstream research; -- Apple signing/notarization work; -- native shell code where useful; -- update/rollback/diagnostic design; -- engineering map and donor research database. - -Demote/archive from critical path: - -- giant AEMU source checkout/build requirement; -- custom-AEMU toolchain adaptation unless later evidence requires it; -- stale Android/PBE package pins; -- experimental graphics variants already rejected; -- assumptions that newest Android/Xcode is automatically best. - ---- - -# 22. Immediate spec blockers — bounded and small - -The next product specification does **not** require another massive research campaign. It requires only these bounded preflight answers: - -### P1 — Direct official game delivery - -Prove one clean Google Play-enabled ARM64 guest can install/update current live TFT through official Google Play and reach Riot login without any third-party package feed. - -**Pass:** package installed from Play, package metadata captured, Riot login opens. -**Fail owner:** guest/Play compatibility only. - -### P2 — Working graphics parity on the Play guest - -Apply the minimum donor-compatible runtime controls that do not require root and attempt current TFT launch. - -**Pass:** real TFT UI launches and stays alive. -**Fail owner:** identify exact root-owned/profile/graphics requirement before considering a second execution guest. - -### P3 — Performance baseline on target M4 - -Measure one transition and one fixed heavy scene using stock Emulator 37.1.11 + chosen guest. - -**Pass:** produces trustworthy FPS/frame-time/stall evidence. -**Purpose:** determine the actual bottleneck before optimization. - -### P4 — Native fullscreen presentation prototype - -Demonstrate a fullscreen native macOS presentation route that hides emulator chrome and preserves direct-render performance/input characteristics. - -**Pass:** native fullscreen works and does not materially regress direct emulator control. -**Fail:** keep direct emulator window as temporary control and investigate presentation boundary only. - -### P5 — Storage bill of materials - -Measure clean runtime + AVD + fully patched TFT + realistic update headroom. - -**Pass:** exact product storage budget frozen; no 100+ GB source tree required. - -These five answers are enough to write the new implementation spec. - ---- - -# 23. Proposed rebuilt product architecture — provisional until P1–P5 - -```text -TFTMAC.app (native SwiftUI) -| -|-- Installer / state machine -|-- Settings / profiles -|-- Fullscreen presentation controller -|-- Input bridge -|-- Audio + microphone controller -|-- FPS/frame-time diagnostics -|-- Repair/reset/diagnostics -|-- Signed app updater -| -+--> Official Google Android components - | - +--> Stock Android Emulator 37.1.11 control - | - +--> Play-enabled ARM64 Android guest - | - +--> Google Play - | +--> official live TFT package - | - +--> Riot patch/auth/game services - | - +--> TFT GLES - -> ANGLE - -> Vulkan - -> gfxstream - -> MoltenVK - -> Metal - -> Apple M4 -``` - -No third-party TFT package feed appears in this architecture. - -Source-built AEMU is outside the normal box and enters only as a measured repair adapter. - ---- - -# 24. Product win condition for the upcoming spec - -The next spec should define “done” as a user product, not a graphics experiment. - -At minimum: - -1. Install TFTMAC like a normal signed/notarized Mac app. -2. Install the Android runtime without Android Studio or Terminal. -3. Obtain current live TFT from official Google Play/Riot paths. -4. Launch, authenticate, join a party, enter a current live match, and complete it without version mismatch. -5. Present TFT as a native-feeling fullscreen Mac application with emulator chrome hidden. -6. Keep mouse/keyboard/audio stable. -7. Support host microphone transport and truthfully report whether the current game implements voice. -8. Maintain acceptable late-game frame pacing, not just 60 FPS in the lobby. -9. Repair package/runtime damage without unnecessary full resets. -10. Show useful FPS/version/runtime diagnostics in the UI. -11. Update the Mac app independently of Android/game state. -12. Keep product storage bounded and remove giant source-build trees from the normal install. -13. Depend on Google/Riot for authoritative game delivery, not a community-maintained TFT package feed. -14. Preserve user state across app updates. -15. Fail with a specific recovery action rather than an unexplained emulator shutdown. - ---- - -# 25. Preflight decision summary - -### Already decided - -- Rebuild around the working donor recipe and our existing TFTMAC knowledge. -- Mactician is a donor/reference, not a production dependency. -- MIT launcher/product-shell code may be forked/adapted with required license notice retained. -- Use stock Google Emulator first. -- Direct Google/Riot package/update authority. -- No third-party hosted TFT feed. -- No giant AEMU source tree in the normal product. -- Native SwiftUI Mac application. -- True fullscreen/native-feeling game presentation is required. -- 1080p/60 is the first guaranteed target. -- Heavy-match frame pacing matters more than lobby FPS. -- Installer/Repair/Reset/diagnostics/FPS concepts are required product features. - -### Must be proven before final spec freeze - -- exact Play-enabled Android guest/version; -- whether root is avoidable for current live TFT; -- exact compatibility mechanism needed for current live renderer; -- target-M4 heavy-scene baseline and bottleneck; -- native fullscreen presentation boundary; -- clean installed storage footprint; -- current TFT microphone/voice capability. - -### Explicitly not a blocker anymore - -- rebuilding AEMU from source; -- reproducing every historical graphics experiment; -- maintaining a private/community TFT package feed; -- solving every Vulkan/GLES theoretical capability before proving the current game path; -- downloading every Xcode/Android/emulator version. - ---- - -# 26. Source index - -Current Mactician source/research used as donor evidence: - -- https://github.com/tweet9ra/mactician -- https://github.com/tweet9ra/mactician/blob/master/LICENSE -- https://github.com/tweet9ra/mactician/tree/master/launcher/Sources -- https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/CoreModels.swift -- https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/LauncherModel.swift -- https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/InstallerService.swift -- https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/LauncherSettingsView.swift -- https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/RuntimeController.swift -- https://sergeinaumov.dev/writing/how-i-built-mactician - -Current TFTMAC evidence: - -- `ssot/host-preflight.json` -- `ssot/STACK.lock.yaml` -- `ssot/TFTMAC_ENGINEERING_MAP.sql` -- `ssot/donors/TFTMAC_DONOR_RESEARCH.sql` -- `docs/TFTMAC_GRAPHICS_ARCHITECTURE.md` -- `artifacts/mactician-live.log` - ---- - -**Preflight state:** READY FOR P1–P5 EXECUTION. The next implementation specification should be written only after these bounded proofs are recorded. diff --git a/ssot/donors/TFTMAC_DONOR_RESEARCH.sql b/ssot/donors/TFTMAC_DONOR_RESEARCH.sql deleted file mode 100644 index f9829c3..0000000 --- a/ssot/donors/TFTMAC_DONOR_RESEARCH.sql +++ /dev/null @@ -1,452 +0,0 @@ --- TFTMAC Donor Research Database --- --- QUARANTINE CONTRACT --- =================== --- This file is deliberately separate from ssot/TFTMAC_ENGINEERING_MAP.sql. --- Nothing in this database is TFTMAC production authority merely because it --- appears here. Donor facts, public claims, historical measurements, inferred --- relationships, and black-box observations are isolated so they cannot poison --- the production SSOT. --- --- Promotion rule: --- A donor finding may be copied into TFTMAC_ENGINEERING_MAP.sql only after an --- explicit TFTMAC reproduction/probe produces direct current evidence on the --- current host/workload or after the governing SSOT is deliberately revised. --- --- Reverse-engineering boundary: --- Open-source code may be inspected, compared, built, forked, and adapted --- within its license. Proprietary applications such as OS FIGHT TACTICS are --- treated as black-box/public-evidence donors only. Do not decompile, bypass --- licensing, or copy proprietary implementation details. --- --- SQLite compatible. - -PRAGMA foreign_keys = ON; -BEGIN; - -CREATE TABLE donor_meta ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL -); - -CREATE TABLE donor_projects ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - project_type TEXT NOT NULL CHECK (project_type IN ('OPEN_SOURCE','PUBLIC_BLACK_BOX','INTERNAL_DONOR','UPSTREAM_COMPONENT')), - source_url TEXT, - website_url TEXT, - license TEXT, - source_access TEXT NOT NULL CHECK (source_access IN ('FULL','PARTIAL','NONE','LOCAL_SNAPSHOT')), - current_research_role TEXT NOT NULL, - trust_boundary TEXT NOT NULL, - observed_at TEXT NOT NULL, - notes TEXT -); - -CREATE TABLE donor_snapshots ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - snapshot_kind TEXT NOT NULL CHECK (snapshot_kind IN ('REPOSITORY_BRANCH','LOCAL_TREE','PUBLIC_WEBSITE','CASE_STUDY','SOURCE_FILE','RELEASE_BINARY','HISTORICAL_DOC')), - ref TEXT, - observed_at TEXT NOT NULL, - source_url TEXT, - source_path TEXT, - content_sha256 TEXT, - evidence_class TEXT NOT NULL CHECK (evidence_class IN ('SOURCE_VERIFIED','PROJECT_LOCAL','AUTHOR_TECHNICAL_WRITEUP','PUBLIC_CLAIM','HISTORICAL','INFERRED','UNVERIFIED')), - freshness TEXT NOT NULL CHECK (freshness IN ('CURRENT','CURRENT_BUT_VERSION_VOLATILE','HISTORICAL','UNKNOWN')), - notes TEXT -); - -CREATE TABLE donor_components ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - name TEXT NOT NULL, - component_type TEXT NOT NULL, - version_or_ref TEXT, - architecture TEXT, - host_requirement TEXT, - guest_requirement TEXT, - role TEXT NOT NULL, - evidence_class TEXT NOT NULL, - source_snapshot_id TEXT REFERENCES donor_snapshots(id), - notes TEXT -); - -CREATE TABLE donor_architecture_edges ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - from_component TEXT NOT NULL, - to_component TEXT NOT NULL, - interface_type TEXT NOT NULL, - relationship TEXT NOT NULL, - state TEXT NOT NULL CHECK (state IN ('SOURCE_VERIFIED','AUTHOR_VERIFIED','PUBLIC_CLAIM','INFERRED','UNKNOWN')), - evidence_snapshot_id TEXT REFERENCES donor_snapshots(id), - notes TEXT -); - -CREATE TABLE donor_capabilities ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - capability TEXT NOT NULL, - layer TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('PROVEN_BY_SOURCE','PROVEN_BY_MEASUREMENT','AUTHOR_CLAIM','PUBLIC_CLAIM','PARTIAL','FAILED','UNKNOWN')), - exact_result TEXT NOT NULL, - workload_scope TEXT, - evidence_snapshot_id TEXT REFERENCES donor_snapshots(id), - reproducibility TEXT, - limitations TEXT -); - -CREATE TABLE donor_techniques ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - technique TEXT NOT NULL, - layer TEXT NOT NULL, - problem_solved TEXT NOT NULL, - implementation_summary TEXT NOT NULL, - state TEXT NOT NULL CHECK (state IN ('PROMOTED_BY_DONOR','RETAINED','PROVISIONAL','REJECTED','HISTORICAL','UNKNOWN')), - source_snapshot_id TEXT REFERENCES donor_snapshots(id), - tftmac_relevance TEXT NOT NULL, - contamination_risk TEXT NOT NULL CHECK (contamination_risk IN ('LOW','MEDIUM','HIGH')), - notes TEXT -); - -CREATE TABLE donor_failures ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - layer TEXT NOT NULL, - attempted_path TEXT NOT NULL, - observed_failure TEXT NOT NULL, - causal_interpretation TEXT NOT NULL, - donor_decision TEXT NOT NULL, - source_snapshot_id TEXT REFERENCES donor_snapshots(id), - tftmac_rule TEXT NOT NULL -); - -CREATE TABLE donor_runtime_profiles ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - profile_name TEXT NOT NULL, - emulator_version TEXT, - android_version TEXT, - system_image TEXT, - cpu_config TEXT, - memory_config TEXT, - display_config TEXT, - gpu_mode TEXT, - graphics_transport TEXT, - angle_mode TEXT, - vulkan_mode TEXT, - moltenvk_mode TEXT, - package_authority TEXT, - status TEXT NOT NULL CHECK (status IN ('WORKING_DONOR','HISTORICAL_WORKING','PUBLIC_CLAIM','CONTROL','EXPERIMENTAL','REJECTED','UNKNOWN')), - evidence_snapshot_id TEXT REFERENCES donor_snapshots(id), - notes TEXT -); - -CREATE TABLE donor_build_system ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - area TEXT NOT NULL, - implementation TEXT NOT NULL, - exact_tool_or_target TEXT, - status TEXT NOT NULL CHECK (status IN ('SOURCE_VERIFIED','AUTHOR_DOCUMENTED','PUBLIC_CLAIM','INFERRED','UNKNOWN')), - source_snapshot_id TEXT REFERENCES donor_snapshots(id), - tftmac_relevance TEXT NOT NULL, - notes TEXT -); - -CREATE TABLE donor_measurements ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - workload TEXT NOT NULL, - comparison TEXT NOT NULL, - result TEXT NOT NULL, - classification TEXT NOT NULL CHECK (classification IN ('CONFIRMED','PROVISIONAL','REJECTED','DIAGNOSTIC','PUBLIC_CLAIM')), - environment TEXT, - source_snapshot_id TEXT REFERENCES donor_snapshots(id), - transferable_lesson TEXT NOT NULL, - transfer_limit TEXT NOT NULL -); - -CREATE TABLE donor_public_claims ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - claim TEXT NOT NULL, - claim_scope TEXT NOT NULL, - observed_at TEXT NOT NULL, - evidence_snapshot_id TEXT REFERENCES donor_snapshots(id), - corroboration_state TEXT NOT NULL CHECK (corroboration_state IN ('SOURCE_CORROBORATED','MEASUREMENT_CORROBORATED','PARTIALLY_CORROBORATED','UNVERIFIED','CONTRADICTED')), - notes TEXT -); - -CREATE TABLE donor_deltas ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - tftmac_area TEXT NOT NULL, - donor_state TEXT NOT NULL, - current_tftmac_state TEXT NOT NULL, - delta_type TEXT NOT NULL CHECK (delta_type IN ('DONOR_SIMPLER','TFTMAC_STRONGER','INCOMPATIBLE_ASSUMPTION','SAME_APPROACH','UNKNOWN')), - significance INTEGER NOT NULL CHECK (significance BETWEEN 1 AND 10), - recommended_action TEXT NOT NULL, - promotion_test TEXT NOT NULL, - notes TEXT -); - -CREATE TABLE donor_promotion_queue ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - finding TEXT NOT NULL, - target_main_map_area TEXT NOT NULL, - priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 10), - status TEXT NOT NULL CHECK (status IN ('QUEUED','TESTING','PROMOTED','REJECTED','DEFERRED')), - required_tftmac_evidence TEXT NOT NULL, - reason TEXT NOT NULL -); - -CREATE TABLE donor_checkout_registry ( - id TEXT PRIMARY KEY, - donor_id TEXT NOT NULL REFERENCES donor_projects(id), - repository_url TEXT, - desired_ref TEXT, - local_target TEXT, - license_ok INTEGER NOT NULL CHECK (license_ok IN (0,1)), - checkout_state TEXT NOT NULL CHECK (checkout_state IN ('NOT_NEEDED','PLANNED','PRESENT','STALE','BLOCKED')), - reason TEXT NOT NULL, - notes TEXT -); - -CREATE VIEW v_donor_priority AS -SELECT p.name, - q.finding, - q.priority, - q.status, - q.required_tftmac_evidence, - q.reason -FROM donor_promotion_queue q -JOIN donor_projects p ON p.id=q.donor_id -ORDER BY q.status='QUEUED' DESC, q.priority DESC, p.name; - -CREATE VIEW v_working_donor_profiles AS -SELECT p.name AS donor, - r.profile_name, - r.emulator_version, - r.android_version, - r.cpu_config, - r.memory_config, - r.display_config, - r.graphics_transport, - r.angle_mode, - r.vulkan_mode, - r.moltenvk_mode, - r.package_authority, - r.status -FROM donor_runtime_profiles r -JOIN donor_projects p ON p.id=r.donor_id -WHERE r.status IN ('WORKING_DONOR','HISTORICAL_WORKING','PUBLIC_CLAIM','CONTROL') -ORDER BY p.name,r.profile_name; - -CREATE VIEW v_source_verified_techniques AS -SELECT p.name AS donor, - t.technique, - t.layer, - t.problem_solved, - t.implementation_summary, - t.state, - t.tftmac_relevance -FROM donor_techniques t -JOIN donor_projects p ON p.id=t.donor_id -WHERE t.source_snapshot_id IN ( - SELECT id FROM donor_snapshots - WHERE evidence_class IN ('SOURCE_VERIFIED','PROJECT_LOCAL','AUTHOR_TECHNICAL_WRITEUP') -) -ORDER BY p.name,t.layer,t.id; - -CREATE VIEW v_black_box_claims AS -SELECT p.name AS donor, - c.claim, - c.claim_scope, - c.corroboration_state, - c.notes -FROM donor_public_claims c -JOIN donor_projects p ON p.id=c.donor_id -WHERE p.project_type='PUBLIC_BLACK_BOX' -ORDER BY p.name,c.id; - -INSERT INTO donor_meta(key,value) VALUES -('schema_version','1'), -('created_at','2026-08-28T06:00:00Z'), -('project','TFTMAC'), -('quarantine','TRUE'), -('authority','NON_AUTHORITATIVE_DONOR_RESEARCH'), -('promotion_rule','No donor finding enters TFTMAC production truth without independent current reproduction or explicit coupled SSOT/plan revision.'), -('version_policy','Patch/game version strings are volatile. Architecture, component relationships, exact build mechanics, and reproducible capability evidence matter more than README freshness.'), -('source_policy','Prefer source code, exact manifests, runtime scripts and measurements over README text. Public proprietary claims remain black-box claims.'), -('research_goal','Identify the minimum already-working architecture and harvest open-source implementation patterns before inventing replacements.'); - -INSERT INTO donor_projects VALUES -('mactician_upstream','Mactician current upstream','OPEN_SOURCE','https://github.com/tweet9ra/mactician','https://sergeinaumov.dev/mactician','MIT','FULL','Primary open-source working/reference implementation for TFT on Apple Silicon.','Open-source source may be inspected/adapted under MIT; volatile game pins are not authority.','2026-08-28T06:00:00Z','Current public repository and author technical case study sampled.'), -('mactician_local','Mactician donor snapshot embedded in TFTMAC','INTERNAL_DONOR',NULL,NULL,'MIT','LOCAL_SNAPSHOT','Local reproducible donor snapshot for code-level comparison without network drift.','Useful historical/source evidence only; predates current TFTMAC v2 authority.','2026-08-28T06:00:00Z','launcher/, scripts/, artifacts/, docs/ and run-tft-* files in TFTMAC repository.'), -('osft','OS FIGHT TACTICS','PUBLIC_BLACK_BOX',NULL,'https://macosfighttactics.com/en',NULL,'NONE','Independent contemporary implementation proving another design can run TFT locally on Apple Silicon.','Public capability/architecture claims only. No source repository found in current search; application is proprietary.','2026-08-28T06:00:00Z','Do not decompile or bypass license; use only public technical claims and observable behavior.'), -('utm_mvk','UTM/CrossOver MoltenVK donor','UPSTREAM_COMPONENT','https://github.com/utmapp/MoltenVK',NULL,'Apache-2.0','FULL','Lower-layer donor for Metal/Vulkan fixes if a measured MoltenVK gap exists.','Not a TFT implementation; only causal patches may be harvested.','2026-08-28T06:00:00Z','Already referenced by TFTMAC SSOT as conditional donor.'); - -INSERT INTO donor_snapshots VALUES -('snap_mactician_master','mactician_upstream','REPOSITORY_BRANCH','master','2026-08-28T06:00:00Z','https://github.com/tweet9ra/mactician',NULL,NULL,'SOURCE_VERIFIED','CURRENT_BUT_VERSION_VOLATILE','Repository has SwiftUI launcher, installer, runtime scripts, benchmark tooling, profiles, tests and docs.'), -('snap_mactician_runtime_controller','mactician_upstream','SOURCE_FILE','master','2026-08-28T06:00:00Z','https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/RuntimeController.swift','launcher/Sources/RuntimeController.swift',NULL,'SOURCE_VERIFIED','CURRENT','Source shows runtime environment, ASG, ANGLE, MoltenVK and profile controls.'), -('snap_mactician_installer','mactician_upstream','SOURCE_FILE','master','2026-08-28T06:00:00Z','https://github.com/tweet9ra/mactician/blob/master/launcher/Sources/InstallerService.swift','launcher/Sources/InstallerService.swift',NULL,'SOURCE_VERIFIED','CURRENT','Source shows AVD creation, package installation, overlay creation, state persistence and integrity verification.'), -('snap_mactician_manifest','mactician_upstream','SOURCE_FILE','master','2026-08-28T06:00:00Z','https://github.com/tweet9ra/mactician/blob/master/launcher/Resources/release-manifest.json','launcher/Resources/release-manifest.json',NULL,'SOURCE_VERIFIED','CURRENT_BUT_VERSION_VOLATILE','Pins platform-tools 36.0.2, Emulator37.1.11 and Android36 Google APIs ARM64 r07; bundled game fields may lag hosted feed.'), -('snap_mactician_case','mactician_upstream','CASE_STUDY','2026-08-09','2026-08-28T06:00:00Z','https://sergeinaumov.dev/writing/how-i-built-mactician',NULL,NULL,'AUTHOR_TECHNICAL_WRITEUP','HISTORICAL','Author documents actual causal experiments, rejected paths and measured working graphics chain.'), -('snap_mactician_local_core','mactician_local','LOCAL_TREE','TFTMAC embedded donor','2026-08-28T06:00:00Z',NULL,'launcher/Sources/CoreModels.swift','9e2c332811b38c3d5f1195a3fb10b49b5adc64c3f588d929e9bb7fe72a4d9fa4','PROJECT_LOCAL','HISTORICAL','Contains signed hosted-game feed URL/public key and legacy PBE package validation.'), -('snap_mactician_local_hosted','mactician_local','LOCAL_TREE','TFTMAC embedded donor','2026-08-28T06:00:00Z',NULL,'launcher/Sources/HostedGameUpdate.swift','ce8d437e7a687c0cc8bcc05c5be9be5f7bf26ac64ded561f5e7d61ae38835d5b','PROJECT_LOCAL','HISTORICAL','Ed25519 signed remote game feed, same-origin HTTPS APK restriction, versionCode update logic.'), -('snap_osft_site','osft','PUBLIC_WEBSITE','current public product site','2026-08-28T06:00:00Z','https://macosfighttactics.com/en',NULL,NULL,'PUBLIC_CLAIM','CURRENT_BUT_VERSION_VOLATILE','Claims Apple Hypervisor, local Android, real Google Play, GPU-accelerated OpenGL, macOS12+, Apple Silicon and current TFT/PBE support.'), -('snap_osft_legal','osft','PUBLIC_WEBSITE','legal notice 2026-08-11','2026-08-28T06:00:00Z','https://macosfighttactics.com/en/legal/mentions-legales',NULL,NULL,'PUBLIC_CLAIM','CURRENT','Confirms proprietary OSFT Launcher/IP status; source must not be assumed public.'), -('snap_mactician_arch_local','mactician_local','LOCAL_TREE','TFTMAC embedded donor','2026-08-28T06:00:00Z',NULL,'docs/architecture.md','7132bf652fc1d851cf5479f49d9258f9b6e789c6f299db3a868f657515994c2e','PROJECT_LOCAL','HISTORICAL','Complete donor architecture/state-machine description.'), -('snap_mactician_bench_local','mactician_local','LOCAL_TREE','TFTMAC embedded donor','2026-08-28T06:00:00Z',NULL,'docs/benchmarks.md','526507229e293e92155fc3ba588bc48d6a149cc7c164d24e5d10ac33755d7965','PROJECT_LOCAL','HISTORICAL','Fixed-scene performance evidence and rejected candidate record.'); - -INSERT INTO donor_components VALUES -('mac_comp_host','mactician_upstream','Apple Silicon Mac','host',NULL,'arm64','macOS12+',NULL,'Host hardware','AUTHOR_TECHNICAL_WRITEUP','snap_mactician_case',NULL), -('mac_comp_emulator','mactician_upstream','Google Android Emulator','emulator','37.1.11','darwin-aarch64','Apple Silicon','Android36 ARM64','Virtual device host','SOURCE_VERIFIED','snap_mactician_manifest','Stock released Google emulator, not a source-built custom AEMU requirement.'), -('mac_comp_guest','mactician_upstream','Android 36 Google APIs ARM64','guest_os','r07','arm64',NULL,NULL,'Guest OS','SOURCE_VERIFIED','snap_mactician_manifest','Source AVD config explicitly uses Google APIs image with PlayStore.enabled=false in the provisioning guest.'), -('mac_comp_angle','mactician_upstream','Android/system ANGLE','graphics_translation',NULL,'arm64',NULL,NULL,'Package GLES->Vulkan translation','AUTHOR_TECHNICAL_WRITEUP','snap_mactician_case','Source runtime sets package-specific ANGLE controls.'), -('mac_comp_gfxstream','mactician_upstream','gfxstream','graphics_transport',NULL,NULL,NULL,NULL,'Guest/host graphics transport','AUTHOR_TECHNICAL_WRITEUP','snap_mactician_case',NULL), -('mac_comp_mvk','mactician_upstream','MoltenVK','graphics_translation',NULL,'darwin-aarch64',NULL,NULL,'Host Vulkan->Metal','AUTHOR_TECHNICAL_WRITEUP','snap_mactician_case',NULL), -('mac_comp_metal','mactician_upstream','Apple Metal','host_graphics',NULL,'arm64',NULL,NULL,'Final GPU API','AUTHOR_TECHNICAL_WRITEUP','snap_mactician_case',NULL), -('mac_comp_swiftui','mactician_upstream','Native SwiftUI launcher','native_ui',NULL,'arm64','macOS12+',NULL,'Install/state/runtime orchestration UI','SOURCE_VERIFIED','snap_mactician_master',NULL), -('mac_comp_gamefeed','mactician_upstream','Signed hosted game update feed','package_update',NULL,NULL,NULL,NULL,'Move fast-moving game payload/version outside static app release','PROJECT_LOCAL','snap_mactician_local_hosted','Ed25519 envelope validation and same-origin HTTPS APK URLs.'), -('osft_comp_hypervisor','osft','Apple Hypervisor local Android runtime','emulator','unknown','arm64','macOS12+',NULL,'Local Android execution','PUBLIC_CLAIM','snap_osft_site','Exact emulator implementation/version not public.'), -('osft_comp_opengl','osft','GPU-accelerated OpenGL path','graphics','unknown','arm64','macOS12+',NULL,'TFT renderer path','PUBLIC_CLAIM','snap_osft_site','Claims guaranteed OpenGL3.2; exact ANGLE/gfxstream/MoltenVK implementation not published.'), -('osft_comp_play','osft','Google Play','package_authority','current',NULL,NULL,NULL,'Official TFT install/update path','PUBLIC_CLAIM','snap_osft_site',NULL); - -INSERT INTO donor_architecture_edges VALUES -('edge_mac_tft_angle','mactician_upstream','TFT GLES','Android/system ANGLE','GLES','TFT remains a GLES-rendered application while ANGLE translates below it.','AUTHOR_VERIFIED','snap_mactician_case',NULL), -('edge_mac_angle_vk','mactician_upstream','Android/system ANGLE','Vulkan','graphics API translation','ANGLE emits Vulkan for emulator graphics stack.','AUTHOR_VERIFIED','snap_mactician_case',NULL), -('edge_mac_vk_gfx','mactician_upstream','Vulkan','gfxstream','guest-host transport','Vulkan commands cross emulator graphics transport.','AUTHOR_VERIFIED','snap_mactician_case',NULL), -('edge_mac_gfx_mvk','mactician_upstream','gfxstream','MoltenVK','host Vulkan backend','gfxstream host path reaches MoltenVK.','AUTHOR_VERIFIED','snap_mactician_case',NULL), -('edge_mac_mvk_metal','mactician_upstream','MoltenVK','Metal','Vulkan-to-Metal','MoltenVK targets Apple Metal.','AUTHOR_VERIFIED','snap_mactician_case',NULL), -('edge_mac_launcher_runtime','mactician_upstream','SwiftUI launcher','runtime project','process/state control','Launcher refreshes owned runtime scripts, validates hashes and starts runtime helper.','SOURCE_VERIFIED','snap_mactician_runtime_controller',NULL), -('edge_osft_hv_android','osft','OSFT Launcher','local Android device','Hypervisor','Public site says Android runs locally on Apple Hypervisor.','PUBLIC_CLAIM','snap_osft_site',NULL), -('edge_osft_play_game','osft','Google Play','TFT','package install','User installs official TFT from Google Play.','PUBLIC_CLAIM','snap_osft_site',NULL), -('edge_osft_game_gpu','osft','TFT','GPU-accelerated OpenGL','graphics','Public site claims OpenGL direct GPU rendering.','PUBLIC_CLAIM','snap_osft_site','Do not infer exact lower translation layers without evidence.'); - -INSERT INTO donor_runtime_profiles VALUES -('profile_mac_source','mactician_upstream','Source-verified runtime baseline','37.1.11','Android36','Google APIs ARM64 r07','6 logical vCPU baseline; host sizing can vary','6144MB baseline; user-selectable validated values','1920x1080/320 baseline with higher profiles','host','virtio-gpu-asg at runtime despite AVD baseline pipe','package/system ANGLE with donor controls','Vulkan under ANGLE','async; 64 active Metal command buffers; fast math enabled','bundled/signed hosted game feed in source design','WORKING_DONOR','snap_mactician_runtime_controller','RuntimeController sets TFT_GLTRANSPORT=virtio-gpu-asg, write step 16384, MVK async/64, fast math and disables ANGLE preferSubmitAtFBOBoundary.'), -('profile_mac_case','mactician_upstream','Author measured working graphics path','37.1.11','Android16/36 family','ARM64','7 vCPU measured M1 Max','guest had ample free RAM; more RAM not causal','2560x1440 historical quality profile','host','ASG','ANGLE controlled ES3.2 exposure','Vulkan','MoltenVK->Metal','historical pinned TFT PBE payload','HISTORICAL_WORKING','snap_mactician_case','Author entered real matches and ran fixed-stage performance experiments.'), -('profile_osft_public','osft','OSFT public working profile','unknown','local Android','unknown','tuned per machine','tuned per machine','fullscreen/user-facing','GPU accelerated','unknown','OpenGL 3.2 claimed','unknown','unknown','real Google Play','PUBLIC_CLAIM','snap_osft_site','Useful proof of product feasibility; internal implementation remains unknown.'); - -INSERT INTO donor_capabilities VALUES -('cap_mac_real_match','mactician_upstream','Game reaches UI/content/authentication/match','workload','PROVEN_BY_MEASUREMENT','Author defines working result as UI + content + auth + entered match, not process existence.','Historical TFT PBE research','snap_mactician_case','Author technical case study with benchmark work.','Historical workload/version; architecture lessons transfer, current live compatibility must be separately verified.'), -('cap_mac_stock_emulator','mactician_upstream','Stock Emulator37.1.11 sufficient for donor runtime','emulator','PROVEN_BY_SOURCE','Donor ships/pins Google Emulator37.1.11 binary rather than requiring source-built AEMU.','Mactician release architecture','snap_mactician_manifest','Source and author documentation agree on stock emulator dependency.','Does not prove TFTMAC v2 genuine-conformance requirement.'), -('cap_mac_asg','mactician_upstream','ASG transport improves controlled match performance','graphics_transport','PROVEN_BY_MEASUREMENT','40.1 FPS / 34.85ms p95 ASG vs 29.6 FPS / 49.75ms p95 old pipe in exact stage1-1 A/B.','Historical fixed TFT scene','snap_mactician_bench_local','Controlled A/B.','Different host/workload; direction is stronger evidence than absolute numbers.'), -('cap_mac_native_window','mactician_upstream','Native emulator window removes scrcpy CPU/video path','presentation','PROVEN_BY_MEASUREMENT','Moving off scrcpy removed software guest video encode path and intermittent freezes.','Historical runtime','snap_mactician_case','Author causal diagnosis.','Native single-window TFTMAC UX may later use a different presentation transport.'), -('cap_mac_bg_nice','mactician_upstream','QEMU process priority correction','host_runtime','PROVEN_BY_MEASUREMENT','unsetopt BG_NICE prevents zsh background QEMU from inheriting nice=5; QEMU runs nice=0.','Historical runtime','snap_mactician_case','Author causal diagnosis.','TFTMAC should preserve this launch invariant if shell backgrounding is used.'), -('cap_mac_update_feed','mactician_upstream','Fast-moving game payload decoupled from static app manifest','package_update','PROVEN_BY_SOURCE','Hosted signed feed supports versionCode comparison, per-APK hashes and same-origin HTTPS downloads.','Mactician app update flow','snap_mactician_local_hosted','Local source direct.','Legacy validation hardcodes PBE package; concept remains valuable even if exact package logic changes.'), -('cap_osft_local','osft','Runs TFT locally on Apple Silicon','runtime','PUBLIC_CLAIM','Public product site says local Android on Apple Hypervisor, no cloud streaming.','Current product','snap_osft_site','Public commercial claim.','No source or independent TFTMAC measurement in this database.'), -('cap_osft_play','osft','Real Google Play acquisition','package_authority','PUBLIC_CLAIM','Public site instructs user to sign into Google Play and install TFT officially.','Current product','snap_osft_site','Public commercial claim.','Exact image/Play Services architecture unknown.'), -('cap_osft_gl32','osft','OpenGL 3.2 support','graphics','PUBLIC_CLAIM','Product site advertises guaranteed OpenGL3.2 support and GPU-accelerated OpenGL.','Current product','snap_osft_site','Public claim only.','No source evidence of whether this is conformant, spoofed, ANGLE-backed, or another method.'); - -INSERT INTO donor_techniques VALUES -('tech_mac_stock_pin','mactician_upstream','Pin a released stock emulator instead of source-building AEMU','emulator','Reduce build-system/toolchain surface while retaining known runtime behavior.','Manifest pins Emulator37.1.11 download/hash; launcher installs it as a verified runtime component.','PROMOTED_BY_DONOR','snap_mactician_manifest','Highest-value TFTMAC comparison: prove whether stock37.1.11 + our guest/capability probes makes source AEMU unnecessary.','LOW','This directly challenges our source-first assumption.'), -('tech_mac_asg','mactician_upstream','Promote ASG transport over legacy pipe','graphics_transport','Reduce guest-host transport overhead.','Runtime sets virtio-gpu-asg; measured exact-scene A/B selected ASG.','PROMOTED_BY_DONOR','snap_mactician_case','Use as prior for current control profile, but remeasure current stack.','LOW',NULL), -('tech_mac_write_step','mactician_upstream','Use 16KiB ASG write step','graphics_transport','Reduce transport syscall/MMIO/ring overhead.','RuntimeController exports TFT_ASG_WRITE_STEP_SIZE=16384.','PROMOTED_BY_DONOR','snap_mactician_runtime_controller','Candidate current control parameter; already represented in historical TFTMAC donor.','MEDIUM','Revalidate because source/guest changed.'), -('tech_mac_mvk_async','mactician_upstream','MoltenVK async queue + bounded active command buffers','host_graphics','Improve host submission throughput without pathological buffer growth.','Runtime exports TFT_MVK_QUEUE_MODE=async and MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE=64.','PROMOTED_BY_DONOR','snap_mactician_runtime_controller','Strong donor default for host-side performance experiments after capability correctness.','MEDIUM','Performance setting, not correctness proof.'), -('tech_mac_no_fbo_submit','mactician_upstream','Disable ANGLE preferSubmitAtFBOBoundary','guest_graphics','Reduce unnecessary submit boundaries.','Runtime exports TFT_ANGLE_DISABLED_FEATURES=preferSubmitAtFBOBoundary.','PROMOTED_BY_DONOR','snap_mactician_runtime_controller','Potential current candidate only after matching ANGLE version/behavior.','MEDIUM','Historical evidence originally provisional; current source promotion indicates donor accepted it later.'), -('tech_mac_device_profile','mactician_upstream','Transactional DeviceProfiles/overlay with hash + rollback','workload_profile','Prevent game restart from reverting framebuffer/profile and preserve exact rendering assumptions.','Launcher verifies source/hash, applies profile for AVD session, stores sidecar backups and restores on cleanup.','PROMOTED_BY_DONOR','snap_mactician_case','Strong implementation pattern if current live client still consumes equivalent profile/overlay mechanism.','HIGH','Engine/layout specific; do not apply to Riot-native client without evidence.'), -('tech_mac_native_window','mactician_upstream','Use native Emulator window rather than scrcpy','presentation','Avoid guest software video encode and wrong right-click semantics.','Removed scrcpy display loop after causal failures.','PROMOTED_BY_DONOR','snap_mactician_case','Keep native emulator as control even if TFTMAC later wraps presentation.','LOW',NULL), -('tech_mac_bg_nice','mactician_upstream','Disable zsh BG_NICE for background emulator launch','host_runtime','Avoid QEMU silently running at nice=5.','unsetopt BG_NICE before orchestration.','PROMOTED_BY_DONOR','snap_mactician_case','Add as explicit TFTMAC launch invariant if relevant.','LOW',NULL), -('tech_mac_signed_feed','mactician_upstream','Signed remote game-update feed','package_update','Riot patches quickly; avoid shipping a new launcher binary for every game payload update.','Ed25519 envelope + same-host HTTPS + per-APK size/hash + monotonic versionCode handling.','PROMOTED_BY_DONOR','snap_mactician_local_hosted','Very useful architectural idea independent of game version.','LOW','TFTMAC prefers Google Play production authority; feed pattern may instead apply to compatibility metadata, not distributing Riot binaries.'), -('tech_osft_googleplay','osft','User installs TFT through real Google Play','package_authority','Avoid stale bundled game payloads and mirror/update burden.','Public product flow: login to Google Play once; install/update TFT there.','PROMOTED_BY_DONOR','snap_osft_site','Matches TFTMAC v2 package-authority goal exactly.','LOW','Public black-box claim, but architecture is independently reasonable.'), -('tech_osft_machine_tuning','osft','Tune RAM/cores by host model','resource_management','Avoid one-size-fits-all guest sizing.','Public site says RAM and cores tuned to machine; implementation unpublished.','UNKNOWN','snap_osft_site','We can implement independently using our own measured resource profiles.','LOW','No proprietary details needed.'); - -INSERT INTO donor_failures VALUES -('fail_mac_generic_gles','mactician_upstream','guest_graphics','Plain emulator GLES2/3.0/3.1','TFT PBE would not complete startup because workload required ES3.2.','Installed APK != graphics compatibility; game requirement lives above emulator package installation.','Select package ANGLE + Vulkan-backed path.','snap_mactician_case','Never use install/process existence as graphics acceptance.'), -('fail_mac_direct_vk','mactician_upstream','workload_graphics','Force Unreal direct Vulkan RHI','Did not solve compatibility; device profile kept game on OpenGL.','Changing game RHI was not the correct boundary.','Keep game GLES; use Vulkan beneath ANGLE.','snap_mactician_case','Do not force a different workload API without proof the exact game build supports it.'), -('fail_mac_scrcpy','mactician_upstream','presentation','scrcpy display path','Guest software video encoding caused stalls and right-click mapped to Android Back.','Presentation layer introduced its own CPU/input failure modes.','Use native emulator window.','snap_mactician_case','Keep direct emulator presentation as baseline/control.'), -('fail_mac_more_resources','mactician_upstream','resource_management','Increase guest CPU/RAM blindly','Measurements showed CPUs idle and RAM available while active bottleneck remained elsewhere.','Resource quantity was not the constrained boundary.','Measure before increasing resources.','snap_mactician_case','Do not use more vCPU/RAM as generic performance cure.'), -('fail_mac_prewarm','mactician_upstream','shader_pipeline','Aggressive PSO prewarming','Reproducible crash in OpenGL program-binary cache.','Broad shader warmup disturbed a fragile cache path.','Reject broad prewarm; preserve persistent cache.','snap_mactician_case','Do not revive broad prewarm without new causal evidence.'), -('fail_mac_submit_thread','mactician_upstream','graphics_transport','Forced submit thread','Heavy-stage performance regressed to about25.8 FPS in donor campaign.','Threading change increased submission/marshalling overhead or synchronization cost.','Reject default submit-thread path.','snap_mactician_bench_local','Historical negative prior; only retest if current traces identify same boundary differently.'), -('fail_mac_mvk128','mactician_upstream','host_graphics','128 active Metal command buffers','Strong first result failed cold repeat; heavy stage fell to23.3 FPS.','Single-run improvement was not reproducible.','Keep experimental only.','snap_mactician_case','Never promote isolated FPS win without cold confirmation.'), -('fail_mac_fileprovider','mactician_upstream','storage','Keep stateful AVD under File Provider-managed directory','Large qcow2 files became compressed/dataless placeholders and AVD became unrecoverable.','Stateful virtual disks cannot tolerate cloud/offload semantics.','Dedicated local Application Support runtime.','snap_mactician_case','Our external M4 runtime must remain ordinary local filesystem storage, not File Provider/cloud-synced.'); - -INSERT INTO donor_build_system VALUES -('build_mac_native','mactician_upstream','macOS app','SwiftUI app compiled as native arm64 macOS app','arm64-apple-macosx12.0','SOURCE_VERIFIED','snap_mactician_master','Confirms no requirement for Xcode26.6 specifically to build donor application; minimum deployment target12.0 is deliberate.',NULL), -('build_mac_emulator_delivery','mactician_upstream','Android emulator','Download verified Google Emulator37.1.11 archive','emulator-darwin_aarch64-15917651.zip','SOURCE_VERIFIED','snap_mactician_manifest','Avoids AEMU source compile entirely for donor shipping runtime.',NULL), -('build_mac_guest_delivery','mactician_upstream','Android guest','Download verified Android36 ARM64 system image','android-36 Google APIs r07','SOURCE_VERIFIED','snap_mactician_manifest','Guest image is treated as immutable verified dependency.',NULL), -('build_mac_state_machine','mactician_upstream','installation','Durable install state stages empty->downloading->sdk_installed->avd_created->ready','InstallState','SOURCE_VERIFIED','snap_mactician_installer','Useful production pattern; separates resumable setup from runtime launch.',NULL), -('build_mac_avd_manual','mactician_upstream','AVD construction','Create config.ini + qcow2 userdata + encryption key directly','qemu-img create 12G','SOURCE_VERIFIED','snap_mactician_installer','Avoids Android Studio; exact AVD configuration is deterministic.',NULL), -('build_mac_integrity','mactician_upstream','integrity','SHA256 every downloaded component/game split; transactional staging','SHA-256','SOURCE_VERIFIED','snap_mactician_installer','Useful unchanged principle for TFTMAC runtime components.',NULL), -('build_mac_release','mactician_upstream','distribution','Native app bundle, hardened runtime/DeveloperID/notarization, Sparkle updates','macOS release pipeline','AUTHOR_DOCUMENTED','snap_mactician_case','Donor proves normal signed/notarized macOS packaging is independent of Android runtime complexity.',NULL), -('build_osft_engine','osft','engine install','One-time engine installation then launcher controls local Android','unknown','PUBLIC_CLAIM','snap_osft_site','Evidence that user-facing runtime can be packaged as an installed engine rather than a developer build tree.',NULL); - -INSERT INTO donor_measurements VALUES -('measure_mac_asg','mactician_upstream','Exact TFT stage1-1 combat','virtio-gpu ASG vs old pipe','40.1 FPS /34.85ms p95 vs29.6 FPS /49.75ms p95','CONFIRMED','M1 Max donor environment','snap_mactician_case','Prefer ASG as the stock donor transport baseline.','Absolute FPS does not transfer to M4/API37/current client.'), -('measure_mac_resolution','mactician_upstream','TFT stage1-5','1600x900 vs2560x1440','30.5 vs31.3 FPS despite2.56x pixels','CONFIRMED','M1 Max donor environment','snap_mactician_case','The tested scene was CPU/RHI/transport-bound; lowering resolution was not the causal fix.','Scene-specific; no general resolution-free claim.'), -('measure_mac_control','mactician_upstream','Tocker Trial fixed stages','reproducible donor baseline','40.60 /36.03 /27.83 FPS stages1-2/1-5/1-8','CONFIRMED','M1 Max donor environment','snap_mactician_case','Use fixed-scene repeatability instead of lobby FPS.','Historical PBE workload.'), -('measure_mac_mvk128','mactician_upstream','Heavy TFT scene','MVK128 first vs cold repeat','32.4 FPS first; cold repeat23.3 FPS heavy stage','REJECTED','M1 Max donor environment','snap_mactician_case','Cold repeat is required before promotion.','Historical setting/driver behavior.'), -('measure_osft_ranked','osft','Live ranked TFT','Public site points to Master-ranked users playing on OSFT','Product feasibility claim','PUBLIC_CLAIM','Current commercial product','snap_osft_site','Treat as feasibility signal only.','No controlled technical measurement or source evidence.'); - -INSERT INTO donor_public_claims VALUES -('claim_osft_mac12','osft','Supports Apple Silicon M1+ on macOS12+','host compatibility','2026-08-28T06:00:00Z','snap_osft_site','UNVERIFIED','Public product requirement.'), -('claim_osft_local_android','osft','Runs a real Android device locally using Apple Hypervisor','virtualization','2026-08-28T06:00:00Z','snap_osft_site','UNVERIFIED','Exact virtual-machine/emulator stack is not public.'), -('claim_osft_gl32','osft','Guarantees OpenGL3.2 support with GPU-accelerated OpenGL','graphics','2026-08-28T06:00:00Z','snap_osft_site','UNVERIFIED','Important feasibility claim; exact conformance/translation technique unknown.'), -('claim_osft_play','osft','Uses real Google Play for user-installed TFT','package authority','2026-08-28T06:00:00Z','snap_osft_site','PARTIALLY_CORROBORATED','Architecture independently matches TFTMAC goal; implementation is proprietary.'), -('claim_osft_disk','osft','Requires about10GB disk space','resource footprint','2026-08-28T06:00:00Z','snap_osft_site','UNVERIFIED','Potentially much smaller than our 131GB AEMU source workspace because it ships/installs runtime artifacts rather than development source.'), -('claim_mactician_current','mactician_upstream','Current public project positions Mactician as a native TFT launcher for Apple Silicon','product scope','2026-08-28T06:00:00Z','snap_mactician_master','SOURCE_CORROBORATED','Do not use patch string as blocker; source architecture is the relevant evidence.'); - -INSERT INTO donor_deltas VALUES -('delta_stock_vs_source','mactician_upstream','emulator acquisition','Ships verified stock Emulator37.1.11 binary.','TFTMAC v2 currently mandates source-built emu-master-dev.','DONOR_SIMPLER',10,'Run TFTMAC capability probes against stock37.1.11 before continuing source-build critical path.','Stock37.1.11 + current guest must pass host/guest Vulkan, genuine GLES3.2 and live TFT smoke without prohibited spoofing.','Potentially removes almost the entire Phase1 source-build burden.'), -('delta_guest36_37','mactician_upstream','guest OS','Android36 ARM64 known donor stack.','TFTMAC v2 freezes Android17/API37 Google Play.','INCOMPATIBLE_ASSUMPTION',8,'Keep Android36 as a control candidate, not authority.','Measure current live TFT install/launch and graphics capability on a clean donor-compatible Android36 guest.','Older guest may be simpler and sufficiently compatible.'), -('delta_package_authority','osft','package acquisition','Real Google Play user install/update.','TFTMAC v2 also requires Google Play authority.','SAME_APPROACH',9,'Preserve Google Play authority; do not distribute Riot binaries as production mechanism.','Current Play guest can install/update official live TFT and package identity/signature can be recorded.','Strong convergence between independent implementations.'), -('delta_storage_size','osft','runtime footprint','Public product claims ~10GB installed space.','TFTMAC development source tree alone is ~131GB.','DONOR_SIMPLER',8,'Separate development-source workspace from shippable runtime footprint; stop assuming source checkout is a production dependency.','Build a runtime bill-of-materials and measure only required shipping artifacts.','This may expose huge simplification.'), -('delta_native_window','mactician_upstream','presentation','Native Emulator window is known stable donor path.','TFTMAC SSOT ultimately wants a single native shell with hidden emulator chrome.','TFTMAC_STRONGER',5,'Keep native emulator window as control; native wrapping is a later UX optimization.','Do not hide emulator chrome until latency/control acceptance proves wrapper parity.','Avoid reintroducing scrcpy-like cost.'), -('delta_gles_truth','mactician_upstream','GLES3.2 acceptance','Historical donor used controlled ES3.2 exposure to make PBE workload run.','TFTMAC v2 forbids nonconformant version exposure as final proof.','TFTMAC_STRONGER',10,'Harvest transport/performance/runtime mechanics but do not inherit donor ES3.2 acceptance shortcut.','TFTMAC guest probe must create real3.2 context and execute required features without nonconformant exposure.','This is exactly why donor success is not automatically production truth.'), -('delta_dynamic_updates','mactician_upstream','fast game patch handling','Signed hosted feed decouples fast Riot payload updates from app binary updates.','TFTMAC v2 prefers Google Play for game delivery but has no equivalent rapidly-updated compatibility metadata channel yet.','DONOR_SIMPLER',6,'Borrow the signed-manifest concept for compatibility metadata/runtime profiles, not necessarily Riot APK distribution.','Implement signed compatibility manifest and prove downgrade/replay resistance.','Solves user point that Riot patch strings should not block architecture progress.'); - -INSERT INTO donor_promotion_queue VALUES -('promote_stock37111','mactician_upstream','Stock Google Emulator37.1.11 may make source-built AEMU unnecessary for production.','architecture_candidates',10,'QUEUED','Run the same current host Vulkan, guest Vulkan, genuine GLES3.2 and live TFT probes against stock37.1.11.','Largest potential reduction in complexity and build time.'), -('promote_android36_control','mactician_upstream','Android36 ARM64 donor guest may be simpler than API37 for current workload.','version_catalog/stack_profiles',9,'QUEUED','Create clean isolated Android36 control with official package authority where possible; run live package compatibility + graphics probes.','Older working guest may satisfy requirements with fewer preview/current API complications.'), -('promote_asg','mactician_upstream','ASG should be current graphics transport control baseline.','runtime_profile',8,'QUEUED','Verify active transport and run one current capability/frame smoke comparison against pipe only if pipe control is needed.','Strongest measured donor transport improvement.'), -('promote_mvk_async64','mactician_upstream','Async MoltenVK +64 active command buffers is a high-value donor default.','performance_candidates',5,'DEFERRED','Only test after current host/guest capability correctness is green and profiling indicates host submission relevance.','Performance tuning should not precede correctness.'), -('promote_signed_compat_feed','mactician_upstream','Use signed rapidly-updated compatibility metadata so Riot patch strings do not require hardcoded app releases.','update_architecture',6,'QUEUED','Design a signed non-Riot-binary compatibility manifest with exact runtime/profile/package metadata and rollback rules.','Makes fast game patches non-blocking while preserving integrity.'), -('promote_native_control','mactician_upstream','Native emulator window should remain the presentation latency/control baseline.','phase8_control',6,'QUEUED','Boot current working runtime in direct native emulator window and capture control latency before any custom native presentation.','Avoids unnecessary presentation invention before runtime works.'), -('promote_osft_feasibility','osft','Independent product claims Google Play + local Android + OpenGL3.2 on macOS12+ with ~10GB footprint.','architecture_research',4,'DEFERRED','Only compare behavior through lawful public/demo/user-owned execution; do not infer private implementation.','Useful independent feasibility signal but not source authority.'); - -INSERT INTO donor_checkout_registry VALUES -('checkout_mactician','mactician_upstream','https://github.com/tweet9ra/mactician','master','/Volumes/MAC MINI M4/TFTMAC/Donors/mactician-upstream',1,'PLANNED','A clean external checkout can enable exact source diff against embedded donor without polluting repository history.','Do not vendor the entire donor into TFTMAC Git unless a specific source subset is adopted; keep checkout under external build/research storage.'), -('checkout_utm_mvk','utm_mvk','https://github.com/utmapp/MoltenVK','crossovers/v25.1.0','/Volumes/MAC MINI M4/TFTMAC/Donors/utm-moltenvk',1,'NOT_NEEDED','Only needed if current host Vulkan probe proves a MoltenVK-owned missing feature.','Avoid speculative donor checkout.'), -('checkout_osft','osft',NULL,NULL,NULL,0,'BLOCKED','OSFT is proprietary/public black-box; no open-source repository was found in current search.','Do not attempt to decompile or bypass licensing.'); - --- Target-M4 real-run evidence captured after installing current Mactician. -INSERT INTO donor_snapshots VALUES -('snap_mactician_target_m4_live','mactician_upstream','LOCAL_TREE','target M4 live runtime log','2026-08-28T06:39:17Z',NULL,'artifacts/mactician-live.log','8042a71ff6b22dc1e0791653fde5068b269c84352b78ccf738e54436c2dacc75','PROJECT_LOCAL','CURRENT','Actual Mactician session log from Mac16,10 Apple M4. Contains successful TFT launch, exact active graphics/runtime profile, later hosted-feed failures, post-patch version-state failure, and repeated controlled shutdowns.'); - -INSERT INTO donor_runtime_profiles VALUES -('profile_mac_target_m4_observed','mactician_upstream','Target Mac M4 observed gameplay profile','37.1.11','Android36','Google APIs ARM64','6 vCPU','6144MB','1920x1080@320dpi@60','host','virtio-gpu-asg','guest ANGLE; exposeNonConformantExtensionsAndVersions/exposeES32ForTesting observed','Vulkan beneath ANGLE','async queue; 64 active Metal command buffers; fast math','Mactician hosted/fallback package mechanism; later invalid feed','CONTROL','snap_mactician_target_m4_live','Log reports ASG writeBufferSize1048576, writeStepSize16384, dataRingSize32768, graphics profile osft, audio enabled, PSO watcher active, and TFT Unreal OpenGL ES -> guest ANGLE -> Vulkan -> Metal.'); - -INSERT INTO donor_capabilities VALUES -('cap_mac_target_m4_launch','mactician_upstream','Current donor runtime launches TFT on target Apple M4','workload','PROVEN_BY_MEASUREMENT','TFT launched as com.riotgames.league.teamfighttactics, real process PID observed, login path operated, game patched, and user reached party/lobby before Riot rejected match start for version mismatch.','Target Mac M4 actual session','snap_mactician_target_m4_live','Direct runtime log plus user-observed party/version-mismatch sequence.','Does not prove current package version parity or complete live-match stability.'), -('cap_mac_target_m4_graphics','mactician_upstream','Target M4 executes donor ANGLE/Vulkan/MoltenVK/Metal path','graphics','PROVEN_BY_MEASUREMENT','Runtime log reports TFT Unreal OpenGL ES -> guest ANGLE -> Vulkan -> Metal, Apple M4 MoltenVK device, gfxstream initialized, ASG transport active.','Target Mac M4 actual session','snap_mactician_target_m4_live','Direct runtime log.','Donor also enables nonconformant ES3.2 exposure; do not treat as genuine conformance proof.'); - -INSERT INTO donor_failures VALUES -('fail_mac_target_feed','mactician_upstream','package_update','Hosted TFT feed during target-M4 session','Hosted feed repeatedly returned Invalid TFT release; launcher fell back to bundled metadata. User could patch/login/join party but Riot then reported game versions did not match.','Third-party package metadata/feed was not synchronized with current Riot live client state.','Remove third-party TFT package feed from TFTMAC production architecture; use official Google Play/Riot authority.','snap_mactician_target_m4_live','A community feed must never be the authoritative current-live package source for TFTMAC.'), -('fail_mac_target_postpatch','mactician_upstream','package_state','Restart after in-game patch/version mismatch','Android booted normally, then launcher found /data/user/0/com.riotgames.league.teamfighttactics missing, could not determine owner, and intentionally shut emulator down.','Package/private-data assumptions were invalid after the patch/update sequence; snapshot warning was only shutdown noise.','TFTMAC recovery must classify package missing/not initialized/updating/damaged and route to official package repair instead of generic shutdown.','snap_mactician_target_m4_live','Repeated across multiple launches in the copied log.'); - -INSERT INTO donor_measurements VALUES -('measure_mac_target_user_drop','mactician_upstream','Target M4 real use during patch/loading','FPS overlay light-state vs user-observed transition drop','Approximately 60 FPS in light state with a reported transient drop to roughly 6 FPS while entering/loading activity','DIAGNOSTIC','Mac16,10 Apple M4, 16GB; session included patching and cache activity','snap_mactician_target_m4_live','Do not accept lobby/light-scene FPS as product performance; measure transitions and heavy combat explicitly.','User observation is not a controlled benchmark; exact frame-time trace still required.'); - -INSERT INTO donor_deltas VALUES -('delta_target_direct_delivery','mactician_upstream','game delivery','Current donor depends on hosted/bundled package metadata and exact APKs; target session demonstrated feed/version drift.','Rebuild requirement is Google Play/Riot direct authority with no third-party TFT feed.','TFTMAC_STRONGER',10,'Use Play-enabled guest first; inspect installed package/version/signature locally and let Google Play/Riot own update state.','Clean Play-enabled ARM64 guest installs current live TFT, reaches Riot login, and after update can enter a party/match without version mismatch.','Tonight provided direct target-host evidence that feed drift is a user-visible production failure.'), -('delta_target_fullscreen','mactician_upstream','presentation','Donor uses launcher plus separate resizable/window-fill emulator window.','Rebuild requires one native-feeling macOS fullscreen product surface with emulator chrome hidden.','TFTMAC_STRONGER',9,'Prototype native fullscreen presentation while retaining direct-emulator frame/input path as the control.','Fullscreen prototype must preserve FPS/frame time/input latency within the product acceptance envelope.','Do not reintroduce scrcpy/software encode path.'); - -INSERT INTO donor_promotion_queue VALUES -('promote_direct_google_target','mactician_upstream','Replace donor hosted/bundled TFT delivery with direct Google Play/Riot authority.','package_update_architecture',10,'QUEUED','Prove a clean Play-enabled ARM64 guest installs and updates current live TFT and reaches a match without version mismatch.','Direct target-M4 failure demonstrated the third-party feed is a reliability dependency we do not need.'), -('promote_target_profile_control','mactician_upstream','Use the exact successful M4 donor runtime profile as a comparison control, not as final product truth.','performance_control',9,'QUEUED','Reproduce stock37.1.11 + ASG16KiB + 6/6144 + 1080p on official package-authority guest and collect transition/heavy-scene frame-time data.','Gives TFTMAC a proven starting control while allowing measured performance improvement.'); - -COMMIT; - --- High-value queries --- SELECT * FROM v_donor_priority; --- SELECT * FROM v_working_donor_profiles; --- SELECT * FROM v_source_verified_techniques; --- SELECT * FROM donor_deltas ORDER BY significance DESC; --- SELECT * FROM donor_failures ORDER BY donor_id,layer; --- SELECT * FROM v_black_box_claims; diff --git a/ssot/history/runtime-authority-gate4-2026-08-30.json b/ssot/history/runtime-authority-gate4-2026-08-30.json new file mode 100644 index 0000000..235d59a --- /dev/null +++ b/ssot/history/runtime-authority-gate4-2026-08-30.json @@ -0,0 +1,49 @@ +{ + "schema": 1, + "observedAt": "2026-08-30T03:59:46Z", + "gate": "Gate 4 stock-runtime independence", + "result": "PASS", + "runtimeRoot": "/Volumes/MAC MINI M4/TFTMAC/Runtime", + "sdkRoot": "/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk", + "emulator": { + "version": "Android emulator version 37.1.11.0", + "buildId": "15917651", + "avd": "TFT_Ultra_Tablet", + "adbSerial": "emulator-5592", + "adbServerPort": 5040, + "consolePort": 5592 + }, + "package": { + "name": "com.riotgames.league.teamfighttactics", + "versionName": "18.1-5402721", + "versionCode": "8402721", + "installer": "com.android.vending" + }, + "emulatorController": { + "installedPath": "/Volumes/MAC MINI M4/TFTMAC/Runtime/sdk/emulator/lib/emulator_controller.proto", + "vendoredPath": "Vendor/AndroidEmulator/emulator_controller.proto", + "sha256": "1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f", + "byteEquality": true + }, + "buildTreeRequiredByLiveRuntime": false, + "retiredApplicationRequired": false, + "processAudit": { + "operation": "tftmac-gate4-process-audit-20260829", + "duplicateRisk": { + "tftmacApps": 1, + "emulators": 1, + "samplers": 1, + "adbServers": 1 + }, + "buildTreeProcessReferences": 0 + }, + "evidenceOperations": [ + "tftmac-gate4-runtime-inventory-20260829", + "tftmac-gate4-start-baseline-20260829", + "tftmac-gate4-package-state-20260829", + "tftmac-gate4-proto-equality-20260829", + "tftmac-gate4-process-audit-20260829", + "tftmac-gate4-clean-stop-20260829" + ], + "historicalNotice": "Exact archived receipt. Its 5040/5592 service-context identity is superseded and must not be used as current launch authority." +} diff --git a/ssot/phase0-remediation-inventory.md b/ssot/phase0-remediation-inventory.md index 7fc90e9..b7d4a64 100644 --- a/ssot/phase0-remediation-inventory.md +++ b/ssot/phase0-remediation-inventory.md @@ -1,75 +1,17 @@ -# TFTMAC GPU Runtime v2.0 — Phase 0 Remediation Inventory +# Historical Phase 0 Source-Build Evidence -This file is evidence and execution guidance only. It does not supersede `TFTMAC_GPU_RUNTIME_SSOT.md`, `TFTMAC_FULL_IMPLEMENTATION_PLAN.md`, or `STACK.lock.yaml`. +**Status:** RETIRED EVIDENCE — NOT EXECUTION GUIDANCE -## Authority state +The former GPU-runtime program attempted a source-built Android Emulator path. It established useful historical facts, including an `emu-master-dev` source freeze, host/toolchain compatibility findings, and a real multi-thousand-step AEMU compile before that architecture was retired. -- Revised SSOT and implementation plan were reviewed together as one coupled authority set. -- ZenGate v2.3 hard gates are clear; calculated result: Q=98, R=5, Z=93 => PASS. -- Exact approved input hashes are frozen in `ssot/AUTHORITY_INPUTS.sha256`. -- Canonical authority files must be present in the repository under the names in that hash file before Phase 0 can PASS. +Current TFTMAC does not require that source checkout or its build outputs. The released Google Android Emulator 37.1.11 is the verified runtime authority, and native TFTMAC freezes the installed EmulatorController protocol directly from that runtime. -## Current base +Preserved historical conclusions: -- GitHub authority base: `1d8f681336a5d4f2f5b93fddacb8ce822cc099c5`. -- Managed implementation change was rebased onto that exact `origin/master` before Phase 0 repository writes. +- the source family was `emu-master-dev`; +- the source experiment reached a real AEMU/gfxstream compile; +- one observed failure involved an obsolete macOS deployment-target/toolchain mismatch; +- large source/build artifacts were moved to external storage because they were inappropriate for normal internal-disk operation; +- later measured work proved the released stock emulator is the correct product control/runtime path. -## Confirmed Phase 0 observations - -### Host - -- Architecture: arm64 — PASS. -- Hardware: Apple M4 Mac mini — PASS. -- macOS: 26.6.2 (25G83) — observed. -- Active Xcode: 26.4 / 17E192 — FAIL against frozen Xcode 26.6 / 17F113 requirement. -- `/Applications` discovery found no Xcode 26.6 installation at observation time. - -### Android command-line tools - -- `commandlinetools-mac_arm64-15859902_latest.zip` downloaded to the canonical TFTMAC Application Support root. -- SHA-256 verified: `835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e` — PASS. -- Android SDK package installation is blocked pending explicit Android SDK license acceptance evidence; bootstrap intentionally refuses to auto-accept terms. - -### Vulkan SDK - -- `vulkaninfo` not found in active SDK or standard paths. -- Frozen required SDK remains Vulkan SDK 1.4.357.0 with SHA-256 `539433589c83522e6f31b1c7b418a4167e21597a4a361ab119e1dc0760cf3865`. - -### AEMU source freeze - -- `emu-master-dev` repo initialization began successfully. -- First `repo sync -c -j8` failed because the internal user volume ran out of free space while packing AEMU prebuilts. -- Failure was storage-only; no source-authority contradiction was observed. -- The v2 tooling now uses `/Volumes/MAC MINI M4/TFTMAC/Build` as the default build/source root instead of the internal Application Support `Build` directory. -- `TFTMAC_BUILD_ROOT` may override that path explicitly for portability. -- When the default external volume is unavailable, the tooling fails closed instead of silently recreating the large build tree on the internal disk. -- Migrate the existing partial internal `Build` tree to the external build root intact, verify it, then resume the same frozen sync. Do not use an Xcode/Developer-directory symlink, do not discard the partial AEMU checkout, do not switch branches, and do not substitute `emu-main-dev`. - -## Legacy implementation quarantine - -Current `master` predates the v2.0 authority and is donor/legacy evidence, not acceptance proof. - -Observed conflicts that must be removed or replaced only after Phase 0 PASS: - -- production path is Android 16 / API 36; -- AVD names include `TftLiveStore` and `TftHighEndTablet` instead of `TFTMAC_Live_API37`; -- Swift launcher "Enhanced" mode injects `androidboot.opengles.version=196610`; -- documentation labels `196610` as OpenGL ES 3.2 capability; -- legacy architecture centers `LiveNativeAdapter` / future `UnrealEnhancedAdapter`, while v2.0 freezes one Android 17 built-in-ANGLE/gfxstream/AEMU/MoltenVK architecture with custom ANGLE only as a conditional Phase 5 repair adapter. - -None of those legacy claims may satisfy a v2.0 gate. - -## Phase 0 open loops - -1. Install/select Xcode 26.6 / 17F113 and rerun host preflight. -2. Migrate the existing internal TFTMAC `Build` tree to `/Volumes/MAC MINI M4/TFTMAC/Build`, verify it, remove the verified internal duplicate, and resume the same `emu-master-dev` resolved-manifest sync from the external build root. -3. Complete Android SDK license acceptance, then install/freeze exact API 37 package revisions and create `TFTMAC_Live_API37`. -4. Install/verify Vulkan SDK 1.4.357.0 and capture `vulkaninfo --summary`. -5. Complete resolved AEMU manifest; freeze qemu/aemu/gfxstream/ANGLE/MoltenVK commits and GuestAngle source audit. -6. Freeze MoltenVK v1.4.2 reference commit. -7. Resolve full `opengl-es-cts-3.2.14.1` commit. -8. Generate exact `ssot/vulkan-required-cases.txt` from the pinned CTS tree and dependency map; hash it. Do not remove failing cases later. -9. Materialize the two approved authority documents under canonical repository names and verify their SHA-256 values. -10. Generate `ssot/preflight-report.md`; Phase 0 can pass only when every critical null is resolved. - -No Phase 1+ graphics mutation is authorized while any item above remains unresolved. +Do not restart repository synchronization, source compilation, CTS/reference downloads, or the former source-build workflow from this record. Any future source-runtime effort requires a separately approved measured blocker. diff --git a/ssot/phase0-source.json b/ssot/phase0-source.json index c94715f..9b6d671 100644 --- a/ssot/phase0-source.json +++ b/ssot/phase0-source.json @@ -1,5 +1,6 @@ { - "schema": 1, + "schema": 2, + "status": "RETIRED_SOURCE_BUILD_EVIDENCE", "observedAt": "2026-08-28T04:03:27.389Z", "aemuBranch": "emu-master-dev", "manifestSHA256": "28865cd8a162178ba462b296f5714b6b7b6916e0cafcddfc0c5e44aa03f8e8d3", @@ -13,8 +14,7 @@ "guestAnglePass": true, "moltenVKIntegration": { "authority": "prebuilts/android-emulator", - "commit": "fb26612eb84576adb974fe7f18a49d263072116f", - "discovery": "ssot/moltenvk-integration-discovery.json" + "commit": "fb26612eb84576adb974fe7f18a49d263072116f" }, "moltenVKReference": { "tag": "v1.4.2", @@ -27,9 +27,5 @@ "vulkanCTSCommit": "5c8aae22885448d70a2873e94a93b24b49505c32", "vulkanSamplesCommit": "89dd3af22d41f9244eeab6e0650460112285c0e1" }, - "repoTool": { - "path": "/Volumes/MAC MINI M4/TFTMAC/Build/bin/repo", - "sha256": "1211b57b57e4122a9c546295a59b37d24068f1164d0e87bef096d5323c413e4f", - "version": "repo version v2.66.1\n (from https://gerrit.googlesource.com/git-repo)\n (tracking refs/heads/stable)\n (Mon, 10 Aug 2026 13:37:04 -0700)\nrepo launcher version 2.65\n (from /Volumes/MAC MINI M4/TFTMAC/Build/bin/repo)\n (currently at 2.66.1)\nrepo User-Agent git-repo/2.66.1 (Darwin) git/2.50.1 (Apple Git-155) Python/3.14.6\ngit 2.50.1 (Apple Git-155)\ngit User-Agent git/2.50.1 (Apple Git-155) (Darwin) git-repo/2.66.1\nPython 3.14.6 (main, Jun 10 2026, 10:03:53) [Clang 21.0.0 (clang-2100.0.123.102)]\nOS Darwin 25.6.0 (Darwin Kernel Version 25.6.0: Fri Jul 31 19:11:03 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T8132)\nCPU arm64 (arm)\nBug reports: https://issues.gerritcodereview.com/issues/new?component=1370071" - } + "executionPolicy": "Historical source authority only. Normal TFTMAC operation must not recreate or execute the retired source-build laboratory." } diff --git a/ssot/project-retirement.json b/ssot/project-retirement.json new file mode 100644 index 0000000..2b5a2d9 --- /dev/null +++ b/ssot/project-retirement.json @@ -0,0 +1,23 @@ +{ + "schema": 1, + "authoritativeProject": "tftmac", + "completedAt": "2026-08-30T04:22:00Z", + "projects": { + "tftmac2": { + "state": "RETIRED", + "catalogName": "RETIRED - TFTMAC2", + "uniqueContentCheck": "PASS", + "classification": "ALREADY_SUPERSEDED", + "evidence": "Only Clara foundation files existed; no active changes, operations, release, or unique implementation." + }, + "tftmac-runtime": { + "state": "ISOLATED_DIAGNOSTIC_ELIGIBILITY", + "catalogName": "TFTMAC Runtime - isolated diagnostic source authority", + "publishedHeadReviewed": "c8aa26ebaa5b977965eb165ad8aac5c98408469f", + "publishedChangeClosed": true, + "classification": "NON_COMPARABLE_DIAGNOSTIC_ONLY", + "evidence": "Stock Build 8 remains the only normal-play authority. The separate runtime repository is eligible only for the planned source-instrumented causal logger; any diagnostic result is non-comparable to stock performance until an explicit parity gate passes." + } + }, + "claraCatalogResult": "TFTMAC is the only non-retired TFT project identity." +} diff --git a/ssot/retained-evidence-index.json b/ssot/retained-evidence-index.json new file mode 100644 index 0000000..a87985e --- /dev/null +++ b/ssot/retained-evidence-index.json @@ -0,0 +1,24 @@ +{ + "schema": 2, + "policy": "Decision relevance, not age. Preserve current playable baseline, package/runtime authority, promoted configuration evidence, current unresolved-failure evidence, and final native acceptance evidence. Superseded raw runs may be compacted only after their decision is represented durably.", + "protectedEvidence": [ + "ssot/runtime-authority.json", + "ssot/storage-reclamation.json", + "Vendor/AndroidEmulator/SOURCE.json", + "ssot/TFTMAC_PERFORMANCE_LAB.sql", + "ssot/TFTMAC_ENGINEERING_MAP.sql" + ], + "rawPruningThisGate": "NONE_REQUIRED", + "reason": "The approved first cleanup does not require age-based deletion. The dominant abandoned storage target has been reclaimed; raw telemetry remains bounded by decision relevance and may be pruned later only when superseded evidence is compacted.", + "externalPrivateEvidence": [ + { + "id": "capture-2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200", + "classification": "PRIVATE_LOCAL_CAPTURE_METADATA_ONLY", + "databaseFileName": "TFTMAC_NATIVE_RUNTIME.sqlite", + "byteCount": 63897600, + "sha256": "c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77", + "retentionReason": "Current Build 8 automatic graphics-run evidence; raw database remains private and is not added to this repository.", + "rawDatabaseCommitted": false + } + ] +} diff --git a/ssot/runtime-authority.json b/ssot/runtime-authority.json new file mode 100644 index 0000000..f94c844 --- /dev/null +++ b/ssot/runtime-authority.json @@ -0,0 +1,182 @@ +{ + "schema": 3, + "observedAt": "2026-08-31T23:13:30Z", + "gate": "Native full-screen playable runtime acceptance", + "result": "PASS", + "resultScope": "HISTORICAL_RELEASE_AND_LIVE_RUNTIME_ACCEPTANCE; SEE currentHostAudit FOR CURRENT SIGNING TRUST", + "runtimeRoot": "/Volumes/MAC MINI M4/TFTMAC/Runtime", + "sdkRoot": "/Volumes/MAC MINI M4/TFTMAC/Runtime/SDK", + "emulator": { + "version": "Android emulator version 37.1.11.0", + "buildId": "15917651", + "avd": "TFT_Ultra_Tablet", + "adbSerial": "emulator-5582", + "adbServerPort": 5038, + "consolePort": 5582, + "launcher": "/usr/bin/open -n -W --env ... --args ...", + "launcherHost": "TFTMAC Emulator Host.app", + "adbVendorKeysInjected": false + }, + "runtimeProfile": { + "id": "tftmac_5gb_native_v1", + "vcpu": 6, + "ramMiB": 5120, + "display": "1920x1080", + "densityDpi": 320, + "refreshHz": 60, + "gpu": "host", + "audio": "coreaudio", + "graphicsTransport": "virtio-gpu-asg", + "asgDrawFlushInterval": 800, + "tftGraphicsQuality": "High", + "tftFrameRateCap": 60, + "tftPerformanceModeBeta": false, + "activeExperiment": "combat_latency_a" + }, + "androidWebView": { + "package": "com.google.android.webview", + "preRepairVersion": "133.0.6943.137", + "currentVersion": "151.0.7922.199", + "updateSource": "com.android.vending", + "riotLoginActivity": "com.riotgames.platformui.mobilefre.MobileFREWebViewActivity" + }, + "package": { + "name": "com.riotgames.league.teamfighttactics", + "versionName": "18.1-5402721", + "versionCode": "8402721", + "installer": "com.android.vending" + }, + "emulatorController": { + "installedPath": "/Volumes/MAC MINI M4/TFTMAC/Runtime/SDK/emulator/lib/emulator_controller.proto", + "vendoredPath": "Vendor/AndroidEmulator/emulator_controller.proto", + "sha256": "1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f", + "byteEquality": true + }, + "buildTreeRequiredByLiveRuntime": false, + "retiredApplicationRequired": false, + "nativeAcceptance": { + "captureSession": "2026-08-30T08-40-36.792Z-5637b7cf-0c8b-435e-adbb-8f4c0e18de94", + "controllerAuthenticated": true, + "frameTransport": "raw_grpc_rgba8888", + "frameSize": "1920x1080", + "metalPresentationHzObserved": 60.5, + "sourceStreamHzObserved": 61.1, + "windowNativeFullscreen": true, + "tftActivity": "com.epicgames.unreal.GameActivity", + "audioSoftwarePathVerified": true, + "userAudibleConfirmation": false + }, + "finalInstalledRelease": { + "receiptScope": "HISTORICAL_BUILD8_RELEASE_ACCEPTANCE", + "acceptedAt": "2026-08-31T21:41:12Z", + "path": "/Applications/TFTMAC.app", + "version": "2.3.0", + "build": "8", + "executableSHA256": "d3bf7c249a3e5f11b81f778b063e1a8cfe2e7fdeec0537ee6bd8447b1c2268d2", + "emulatorHostExecutableSHA256": "ea028ec1d74cc025638c2a0e5f8c783748803c1b0ba9012962c038251fb3eb63", + "deepCodeSignatureValid": true, + "deepCodeSignatureValidScope": "AT_RELEASE_ACCEPTANCE", + "signingIdentity": "TFTMAC Local Code Signing", + "adHocSigned": false, + "designatedRequirement": "identifier com.flashls1.tftmac and certificate root AD42350FDE69323BA63D0E0C6604AC347C506F0C", + "macIconEmbedded": true, + "officialIconSourceSHA256": "d6ba9ceb76c4b1e44e87f059f775a0ed629f9bea29b0dd73245853d7dca3a016", + "officialIcon1024SHA256": "ed5fd83efa6e04599e82ca00f897b09813cdeac007850f6993efd601a730345f", + "embeddedIconICNSSHA256": "010729a19f165b68edeb1fb44e8c31e450f79b3a988c95b8de090373378f6f06", + "nativeVerifier": "PASS_AT_RELEASE_ACCEPTANCE", + "unitTestsPassed": 43, + "rapidCombatABImplementation": "AUTOMATIC_FULL_RUN_VERIFIED_ROOT_ATTRIBUTION_UNKNOWN", + "gameModeEligible": true, + "guestGameplayPowerGate": true, + "hostSchedulingReceiptProbe": "USER_INTERACTIVE_DIRECT_PRE_EXEC_PASS", + "retiredHomeRunA": "REJECTED_USER_EXPERIENCE_NOT_SELECTABLE", + "perfettoTraceProcessorVersion": "58.2", + "perfettoTraceProcessorSHA256": "d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa", + "liveMatchAndCleanShutdownCapture": "2026-08-30T09-25-17.519Z-1a9d0227-3cf8-4a19-b353-c0f135ccf31c", + "priorSignedReleaseCleanShutdownCapture": "2026-08-30T20-24-24.969Z-998c4e53-ff91-4cf3-8002-21543dc5d46f", + "priorVersionLaunchCapture": "2026-08-30T20-41-22.662Z-4bdb8a3f-813d-4e70-b8bd-67c0b6b5766f", + "build7LaunchCapture": "2026-08-31T02-54-28.329Z-14000b50-bf29-44c6-a963-9203d5313494", + "build8CleanShutdownCapture": "2026-08-31T21-30-37.047Z-f77d727b-a5ad-4418-8af7-2e352cdbdbbb", + "build8LaunchCapture": "2026-08-31T21-39-18.396Z-fe34e3a1-fb91-44eb-804f-4ca8519dfc31", + "build8AutomaticGraphicsLogger": "PASS_PID_LAYER_COMPLETE_RECEIPT_FRAME_LINKS", + "build8ReceiptCompleteness": "COMPLETE_WITH_EXPLICIT_INTERNAL_FRAME_ID_UNKNOWNS", + "build8FrameFactsAtAcceptance": ">=2378_ALL_RUN_HASH_WINDOW_RECEIPT_LINKED", + "currentReleaseLaunchAcceptance": "LIVE_BUILD8_PID_LAYER_COMPLETE_RECEIPT_PASS", + "currentReleaseGameplayBenchmark": "VERIFIED_CAPTURE_ROOT_ATTRIBUTION_UNKNOWN", + "build7HostQoSPreExec": "USER_INTERACTIVE_SET_RESULT_0", + "build7GuestPowerState": "POWERED_STAY_ON_AWAKE", + "build7LoggerHealth": "PASS", + "build7RiotLoginRecovery": "RIOT_PROCESS_ONLY_AFTER_WEBVIEW_INPUT_ANR", + "removableVolumePermissionRetainedAcrossRelaunch": true, + "nonErrorUnlockOverlayVisible": false, + "confirmedMemoryKills": 0, + "secureAndroidUnlock": "MANUAL_ONLY", + "primaryInputTransport": "EmulatorController.sendTouch", + "primaryTouchReceipt": "PRIMARY_TOUCH_INPUT_ACTIVE" + }, + "currentHostAudit": { + "observedAt": "2026-08-31T23:13:30Z", + "source": "timestamped current-host audit; distinct from finalInstalledRelease historical signing acceptance", + "releaseIdentityHashesMatch": true, + "zeroIdentityFindings": true, + "trustEvaluation": "NOT_TRUSTED_BY_CURRENT_HOST_POLICY", + "cssmError": "CSSMERR_TP_NOT_TRUSTED", + "installedRuntimeVerifier": "BLOCKED_SIGNING_IDENTITY", + "conclusion": "Release identity hashes match the historical signed Build 8 receipt; current-host trust evaluation is a separate timestamped audit and does not rewrite historical release acceptance." + }, + "currentGameplayCapture": { + "captureId": "2026-08-31T22-30-26.086Z-8df607d7-a34a-4e2a-b00d-739aa3143200", + "storage": "PRIVATE_LOCAL_ONLY", + "database": { + "fileName": "TFTMAC_NATIVE_RUNTIME.sqlite", + "byteCount": 63897600, + "sha256": "c1ef9c9ffe591a297cb86660e3ccfea7e9aeb593f22100e4e732b2fc77d4ee77" + }, + "graphicsRun": { + "duration": "42m27s", + "exactLayerCoveragePercent": 99.629, + "frameIntervalCount": 144364, + "degradationIncidentCount": 189, + "weightedFps": 56.98, + "fpsOnePercentLow": 21.49, + "p50FrameIntervalMs": 16.707, + "p95FrameIntervalMs": 21.51, + "p99FrameIntervalMs": 33.434, + "maxFrameIntervalMs": 2233.611, + "jankCount": 6544, + "jankPercent": 4.53, + "severeStallCount": 144, + "missedVsyncEquivalentCount": 7644 + }, + "effectiveStackReceipt": "UNREAL_DIRECT_VULKAN -> GFXSTREAM_ASG -> HOST_VULKAN -> MOLTENVK -> METAL", + "angleStatus": "CONDITIONAL_NOT_ASSUMED_FOR_TFT_MAIN_RENDERING_PATH", + "macPresenter": "EXCLUDED_FROM_CAUSAL_CANDIDATES_CONTEXT_ONLY", + "markersAndBattles": "OPTIONAL_ANNOTATIONS_NOT_VALIDITY_OR_CAUSAL_GATES", + "automaticLogging": "VERIFIED_PID_LAYER_LIFETIME", + "rootAttribution": "UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE", + "nextLayer": "ADVANCED_SOURCE_CAUSAL_LOGGER_PLANNED" + }, + "diagnosticRuntimeEligibility": { + "repository": "flashls1/tftmac-runtime", + "commit": "c8aa26ebaa5b977965eb165ad8aac5c98408469f", + "status": "ISOLATED_NON_COMPARABLE_DIAGNOSTIC_ELIGIBILITY_ONLY", + "normalPlayAuthority": "STOCK_BUILD8" + }, + "evidenceOperations": [ + "native-events:CONTROLLER_AUTHENTICATED", + "native-events:ADB_DEVICE_AUTHORIZED", + "native-events:FIRST_NATIVE_FRAME", + "native-events:HOST_SCHEDULING_RECEIPT", + "native-events:GUEST_GAMEPLAY_POWER_READY", + "native-events:LOGGER_HEALTH_GATE_PASSED", + "native-events:PRIMARY_TOUCH_INPUT_ACTIVE", + "native-events:OFFICIAL_TFT_PACKAGE_RECEIPT", + "native-events:TFT_LAUNCH_REQUESTED", + "native-events:TFT_PROCESS_STARTED", + "native-events:TFT_GRAPHICS_RUN_STARTED", + "sql:graphics_pipeline_snapshots:COMPLETE", + "sql:graphics_frame_facts:FULLY_LINKED", + "appkit-accessibility:AXFullScreen=true" + ], + "supersedes": "ssot/history/runtime-authority-gate4-2026-08-30.json" +} diff --git a/ssot/storage-reclamation.json b/ssot/storage-reclamation.json new file mode 100644 index 0000000..e4299a0 --- /dev/null +++ b/ssot/storage-reclamation.json @@ -0,0 +1,28 @@ +{ + "schema": 1, + "observedAt": "2026-08-30T04:06:34.933Z", + "buildRoot": "/Volumes/MAC MINI M4/TFTMAC/Build", + "reclaimRoot": "/Volumes/MAC MINI M4/TFTMAC/Build.reclaiming", + "runtimeRoot": "/Volumes/MAC MINI M4/TFTMAC/Runtime", + "quiescence": { + "liveBuildUsers": [] + }, + "buildBytesBefore": 141198893056, + "runtimeBytesBefore": 31927107584, + "dfBefore": "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/disk7s1 999678976 796053660 203420880 80% /Volumes/MAC MINI M4", + "state": "COMPLETE", + "completedAt": "2026-08-30T04:08:07.738Z", + "buildBytesAfter": 0, + "reclaimedBytes": 141198893056, + "runtimeBytesAfter": 31926767616, + "runtimeAllocatedByteDelta": -339968, + "runtimeIntegrity": { + "emulatorPresent": true, + "adbPresent": true, + "avdPresent": true, + "installedProtoSHA256": "1d62c6bcad5f06621f90ec2bf26c661ba769ccd0f1416b5314d25a68e04eee5f", + "frozenProtoMatch": true + }, + "dfAfter": "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/disk7s1 999678976 658048080 341426460 66% /Volumes/MAC MINI M4", + "result": "PASS" +} diff --git a/tftmac/App/AppCoordinator.swift b/tftmac/App/AppCoordinator.swift new file mode 100644 index 0000000..8fe73c4 --- /dev/null +++ b/tftmac/App/AppCoordinator.swift @@ -0,0 +1,109 @@ +import AppKit + +@MainActor +final class AppCoordinator: NSObject, NSApplicationDelegate { + private let mailbox = LatestFrameMailbox() + private var mainWindowController: MainWindowController? + private var runtimeController: TFTMACRuntimeController? + private var settingsWindowController: RuntimeSettingsWindowController? + private var activeProfile: TFTMACRuntimeProfile = .playable + private var terminationInProgress = false + + func applicationDidFinishLaunching(_ notification: Notification) { + let controller = MainWindowController(mailbox: mailbox) + mainWindowController = controller + let activeProfile = TFTMACRuntimeProfile.load() + self.activeProfile = activeProfile + let runtime = TFTMACRuntimeController( + profile: activeProfile, + mailbox: mailbox, + status: { [weak controller] text, isError in + controller?.emulatorView.setStatus(text, isError: isError) + }, + gameFrame: { [weak controller] window in + controller?.emulatorView.setGameFrameWindow(window) + } + ) + runtimeController = runtime + controller.emulatorView.onTouchInput = { [weak runtime] input in + runtime?.sendTouch(input) + } + controller.emulatorView.onMouseInput = { [weak runtime] x, y, buttons in + runtime?.sendMouse(x: x, y: y, buttons: buttons) + } + controller.emulatorView.onKeyboardInput = { [weak runtime] text, key in + runtime?.sendKeyboard(text: text, key: key) + } + controller.emulatorView.onPresentationSample = { [weak runtime] sample in + runtime?.recordPresentation(sample) + } + controller.emulatorView.onHostPresentationWindow = { [weak runtime] sample in + runtime?.recordHostPresentation(sample) + } + controller.showWindow(nil) + controller.window?.makeFirstResponder(controller.emulatorView) + NSApp.activate(ignoringOtherApps: true) + runtime.start() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak controller] in + controller?.enterNativeFullscreen() + } + } + + @objc func showSettings(_ sender: Any?) { + let settings = settingsWindowController ?? RuntimeSettingsWindowController(profile: TFTMACRuntimeProfile.load()) + settings.onSave = { [weak self] previous, next in + self?.runtimeController?.recordSettingsChange(previous: previous, next: next) + } + settingsWindowController = settings + settings.refreshFromSavedProfile() + settings.showWindow(sender) + settings.window?.makeKeyAndOrderFront(sender) + NSApp.activate(ignoringOtherApps: true) + } + + @objc func markMatchEntry(_ sender: Any?) { recordMarker("MATCH_ENTRY") } + @objc func startCombatBenchmark(_ sender: Any?) { + runtimeController?.startCombatBenchmark(performanceModeConfirmed: false) + } + @objc func markVisibleStutter(_ sender: Any?) { runtimeController?.markVisibleStutter() } + @objc func endCombatBenchmark(_ sender: Any?) { + var correctnessPassed = true + if activeProfile.experimentPreset.isActiveCandidate { + let alert = NSAlert() + alert.messageText = "Did Combat Latency A preserve correctness?" + alert.informativeText = "Reject the run if boot, graphics, input, audio, login, or gameplay correctness regressed. TFTMAC will restore Control for the next launch." + alert.alertStyle = .informational + alert.addButton(withTitle: "All Correct — End") + alert.addButton(withTitle: "Reject: Correctness Problem") + correctnessPassed = alert.runModal() == .alertFirstButtonReturn + } + runtimeController?.endCombatBenchmark(correctnessPassed: correctnessPassed) + } + @objc func markMatchEnd(_ sender: Any?) { recordMarker("MATCH_END") } + + @objc func revealCaptureFolder(_ sender: Any?) { + let captures = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support/TFTMAC/Captures", isDirectory: true) + try? FileManager.default.createDirectory(at: captures, withIntermediateDirectories: true) + NSWorkspace.shared.open(captures) + } + + private func recordMarker(_ marker: String) { + runtimeController?.recordMarker(marker) + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + true + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + if terminationInProgress { return .terminateLater } + guard let runtimeController else { return .terminateNow } + terminationInProgress = true + Task { @MainActor in + await runtimeController.stop() + sender.reply(toApplicationShouldTerminate: true) + } + return .terminateLater + } +} diff --git a/tftmac/App/MainWindowController.swift b/tftmac/App/MainWindowController.swift new file mode 100644 index 0000000..15ae39b --- /dev/null +++ b/tftmac/App/MainWindowController.swift @@ -0,0 +1,36 @@ +import AppKit + +@MainActor +final class MainWindowController: NSWindowController { + let emulatorView: EmbeddedEmulatorView + + init(mailbox: LatestFrameMailbox) { + emulatorView = EmbeddedEmulatorView( + frame: NSRect(x: 0, y: 0, width: 1920, height: 1080), + mailbox: mailbox + ) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1920, height: 1080), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.title = "TFTMAC" + window.titleVisibility = .visible + window.collectionBehavior.insert(.fullScreenPrimary) + window.minSize = NSSize(width: 960, height: 540) + window.center() + window.contentView = emulatorView + super.init(window: window) + shouldCascadeWindows = false + } + + required init?(coder: NSCoder) { + nil + } + + func enterNativeFullscreen() { + guard let window, !window.styleMask.contains(.fullScreen) else { return } + window.toggleFullScreen(nil) + } +} diff --git a/tftmac/App/RuntimeSettingsWindowController.swift b/tftmac/App/RuntimeSettingsWindowController.swift new file mode 100644 index 0000000..50d8db7 --- /dev/null +++ b/tftmac/App/RuntimeSettingsWindowController.swift @@ -0,0 +1,164 @@ +import AppKit + +@MainActor +final class RuntimeSettingsWindowController: NSWindowController { + private let experimentButton = NSPopUpButton() + private let vCPUButton = NSPopUpButton() + private let ramButton = NSPopUpButton() + private let refreshButton = NSPopUpButton() + private let flushButton = NSPopUpButton() + private let resultLabel = NSTextField(labelWithString: "") + private var originalProfile: TFTMACRuntimeProfile + var onSave: ((TFTMACRuntimeProfile, TFTMACRuntimeProfile) -> Void)? + + init(profile: TFTMACRuntimeProfile) { + originalProfile = profile + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 600, height: 510), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + window.title = "TFTMAC Performance Lab" + window.isReleasedWhenClosed = false + window.center() + super.init(window: window) + configureContent(profile: profile) + } + + required init?(coder: NSCoder) { nil } + + func refreshFromSavedProfile() { + originalProfile = TFTMACRuntimeProfile.load() + select(originalProfile.experimentPreset, in: experimentButton) + select(originalProfile.vCPU, in: vCPUButton) + select(originalProfile.ramMiB, in: ramButton) + select(originalProfile.refreshHz, in: refreshButton) + select(originalProfile.asgDrawFlushInterval, in: flushButton) + resultLabel.stringValue = "Changes are validated, logged, and applied on the next app launch." + } + + private func configureContent(profile: TFTMACRuntimeProfile) { + guard let window else { return } + let effect = NSVisualEffectView() + effect.material = .windowBackground + effect.blendingMode = .behindWindow + effect.state = .active + effect.translatesAutoresizingMaskIntoConstraints = false + window.contentView = effect + + let title = NSTextField(labelWithString: "Performance Lab") + title.font = .systemFont(ofSize: 24, weight: .semibold) + let subtitle = NSTextField(wrappingLabelWithString: "The proven 1920 × 1080, 320-dpi, host-GPU, CoreAudio and ANGLE/Vulkan/MoltenVK pipeline stays fixed. These bounded variables apply after restart so every run remains attributable and recoverable.") + subtitle.textColor = .secondaryLabelColor + subtitle.maximumNumberOfLines = 3 + + experimentButton.addItems(withTitles: RuntimeExperimentPreset.selectableCases.map(\.displayName)) + vCPUButton.addItems(withTitles: TFTMACRuntimeProfile.supportedVCPU.map(String.init)) + ramButton.addItems(withTitles: TFTMACRuntimeProfile.supportedRAMMiB.map { "\($0) MiB" }) + refreshButton.addItems(withTitles: TFTMACRuntimeProfile.supportedRefreshHz.map { "\($0) Hz" }) + flushButton.addItems(withTitles: TFTMACRuntimeProfile.supportedASGDrawFlushIntervals.map { "\($0) µs" }) + for button in [vCPUButton, ramButton, refreshButton, flushButton] { + button.isEnabled = false + button.toolTip = "Locked for the controlled Combat Benchmark so the preset is the only changed variable." + } + select(profile.experimentPreset, in: experimentButton) + select(profile.vCPU, in: vCPUButton) + select(profile.ramMiB, in: ramButton) + select(profile.refreshHz, in: refreshButton) + select(profile.asgDrawFlushInterval, in: flushButton) + + let grid = NSGridView(views: [ + [fieldLabel("Launch experiment"), experimentButton], + [fieldLabel("Virtual CPUs"), vCPUButton], + [fieldLabel("Android RAM"), ramButton], + [fieldLabel("Guest refresh target"), refreshButton], + [fieldLabel("ASG draw flush interval"), flushButton], + [fieldLabel("Play surface"), fixedValue("1920 × 1080 @ 320 dpi")], + [fieldLabel("Graphics / audio"), fixedValue("Host GPU · CoreAudio")] + ]) + grid.rowSpacing = 13 + grid.columnSpacing = 24 + grid.column(at: 0).xPlacement = .trailing + grid.column(at: 1).xPlacement = .fill + grid.translatesAutoresizingMaskIntoConstraints = false + + resultLabel.stringValue = "Changes are validated, logged, and applied on the next app launch." + resultLabel.textColor = .secondaryLabelColor + resultLabel.font = .systemFont(ofSize: 12) + + let baseline = NSButton(title: "Restore Proven Baseline", target: self, action: #selector(restoreBaseline(_:))) + let save = NSButton(title: "Save for Next Launch", target: self, action: #selector(saveSettings(_:))) + save.keyEquivalent = "\r" + save.bezelStyle = .rounded + let buttons = NSStackView(views: [baseline, NSView(), save]) + buttons.orientation = .horizontal + buttons.spacing = 10 + buttons.distribution = .fill + + let stack = NSStackView(views: [title, subtitle, grid, resultLabel, buttons]) + stack.orientation = .vertical + stack.alignment = .leading + stack.spacing = 18 + stack.translatesAutoresizingMaskIntoConstraints = false + effect.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: effect.leadingAnchor, constant: 30), + stack.trailingAnchor.constraint(equalTo: effect.trailingAnchor, constant: -30), + stack.topAnchor.constraint(equalTo: effect.topAnchor, constant: 28), + stack.bottomAnchor.constraint(lessThanOrEqualTo: effect.bottomAnchor, constant: -24), + grid.widthAnchor.constraint(equalTo: stack.widthAnchor), + buttons.widthAnchor.constraint(equalTo: stack.widthAnchor), + resultLabel.widthAnchor.constraint(equalTo: stack.widthAnchor) + ]) + } + + private func fieldLabel(_ text: String) -> NSTextField { + let label = NSTextField(labelWithString: text) + label.font = .systemFont(ofSize: 13, weight: .medium) + return label + } + + private func fixedValue(_ text: String) -> NSTextField { + let value = NSTextField(labelWithString: text) + value.textColor = .secondaryLabelColor + return value + } + + private func select(_ value: Int, in button: NSPopUpButton) { + let candidate = button.itemTitles.first(where: { $0.split(separator: " ").first == "\(value)" }) + if let candidate { button.selectItem(withTitle: candidate) } + } + + private func select(_ preset: RuntimeExperimentPreset, in button: NSPopUpButton) { + button.selectItem(withTitle: preset.displayName) + } + + @objc private func restoreBaseline(_ sender: Any?) { + let baseline = TFTMACRuntimeProfile.playable + select(.control, in: experimentButton) + select(baseline.vCPU, in: vCPUButton) + select(baseline.ramMiB, in: ramButton) + select(baseline.refreshHz, in: refreshButton) + select(baseline.asgDrawFlushInterval, in: flushButton) + resultLabel.stringValue = "Proven 6-CPU / 5120-MiB / 60-Hz / 800-µs values selected. Save to keep them." + } + + @objc private func saveSettings(_ sender: Any?) { + guard let preset = selectedExperimentPreset() else { return } + let next = TFTMACRuntimeProfile.playable.with(experimentPreset: preset) + next.save() + onSave?(originalProfile, next) + originalProfile = next + resultLabel.stringValue = "Saved as \(next.identifier). Quit and relaunch TFTMAC to apply it." + } + + private func selectedInteger(_ button: NSPopUpButton) -> Int? { + button.titleOfSelectedItem?.split(separator: " ").first.flatMap { Int($0) } + } + + private func selectedExperimentPreset() -> RuntimeExperimentPreset? { + RuntimeExperimentPreset.selectableCases.first { $0.displayName == experimentButton.titleOfSelectedItem } + } +} diff --git a/tftmac/App/TFTMACApplication.swift b/tftmac/App/TFTMACApplication.swift new file mode 100644 index 0000000..0f5831a --- /dev/null +++ b/tftmac/App/TFTMACApplication.swift @@ -0,0 +1,95 @@ +import AppKit + +@main +enum TFTMACApplication { + @MainActor private static var coordinator: AppCoordinator? + + @MainActor + static func main() { + let application = NSApplication.shared + let coordinator = AppCoordinator() + Self.coordinator = coordinator + application.delegate = coordinator + application.setActivationPolicy(.regular) + installMainMenu(on: application, coordinator: coordinator) + application.run() + } + + @MainActor + private static func installMainMenu(on application: NSApplication, coordinator: AppCoordinator) { + let mainMenu = NSMenu(title: "TFTMAC") + + let appItem = NSMenuItem() + let appMenu = NSMenu(title: "TFTMAC") + appItem.submenu = appMenu + appMenu.addItem(withTitle: "About TFTMAC", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") + let settings = appMenu.addItem(withTitle: "Performance Lab…", action: #selector(AppCoordinator.showSettings(_:)), keyEquivalent: ",") + settings.target = coordinator + appMenu.addItem(.separator()) + let quit = appMenu.addItem(withTitle: "Quit TFTMAC", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") + quit.target = application + mainMenu.addItem(appItem) + + let editItem = NSMenuItem() + let editMenu = NSMenu(title: "Edit") + editItem.submenu = editMenu + editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") + mainMenu.addItem(editItem) + + let viewItem = NSMenuItem() + let viewMenu = NSMenu(title: "View") + viewItem.submenu = viewMenu + let fullscreen = viewMenu.addItem( + withTitle: "Toggle Full Screen", + action: #selector(NSWindow.toggleFullScreen(_:)), + keyEquivalent: "f" + ) + fullscreen.keyEquivalentModifierMask = [.command, .control] + mainMenu.addItem(viewItem) + + let performanceItem = NSMenuItem() + let performanceMenu = NSMenu(title: "Telemetry") + performanceItem.submenu = performanceMenu + let automaticLogger = NSMenuItem( + title: "Graphics Logger: Automatic While TFT Runs", + action: nil, + keyEquivalent: "" + ) + automaticLogger.state = .on + automaticLogger.isEnabled = false + performanceMenu.addItem(automaticLogger) + performanceMenu.addItem(.separator()) + addTelemetryItem("Mark Match Entry", action: #selector(AppCoordinator.markMatchEntry(_:)), key: "1", to: performanceMenu, coordinator: coordinator) + addTelemetryItem("Start Controlled A/B Window (Optional)", action: #selector(AppCoordinator.startCombatBenchmark(_:)), key: "2", to: performanceMenu, coordinator: coordinator) + addTelemetryItem("Mark Visible Stutter", action: #selector(AppCoordinator.markVisibleStutter(_:)), key: "3", to: performanceMenu, coordinator: coordinator) + addTelemetryItem("End Controlled A/B Window (Optional)", action: #selector(AppCoordinator.endCombatBenchmark(_:)), key: "4", to: performanceMenu, coordinator: coordinator) + addTelemetryItem("Mark Match End", action: #selector(AppCoordinator.markMatchEnd(_:)), key: "5", to: performanceMenu, coordinator: coordinator) + performanceMenu.addItem(.separator()) + let reveal = performanceMenu.addItem(withTitle: "Reveal Local Capture Folder", action: #selector(AppCoordinator.revealCaptureFolder(_:)), keyEquivalent: "l") + reveal.keyEquivalentModifierMask = [.command, .shift] + reveal.target = coordinator + mainMenu.addItem(performanceItem) + + let windowItem = NSMenuItem() + let windowMenu = NSMenu(title: "Window") + windowItem.submenu = windowMenu + windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m") + windowMenu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") + mainMenu.addItem(windowItem) + + application.mainMenu = mainMenu + } + + @MainActor + private static func addTelemetryItem( + _ title: String, + action: Selector, + key: String, + to menu: NSMenu, + coordinator: AppCoordinator + ) { + let item = menu.addItem(withTitle: title, action: action, keyEquivalent: key) + item.keyEquivalentModifierMask = [.command, .shift] + item.target = coordinator + } +} diff --git a/tftmac/Assets/TFTMAC-Official-Icon.png b/tftmac/Assets/TFTMAC-Official-Icon.png new file mode 100644 index 0000000..86a1fe5 Binary files /dev/null and b/tftmac/Assets/TFTMAC-Official-Icon.png differ diff --git a/tftmac/Info.plist b/tftmac/Info.plist index a68ebaa..ce9ac35 100644 --- a/tftmac/Info.plist +++ b/tftmac/Info.plist @@ -19,15 +19,19 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.0 + 2.3.0 CFBundleVersion - 1 + 8 LSApplicationCategoryType public.app-category.games LSMinimumSystemVersion - 14.0 + 15.0 + LSSupportsGameMode + NSHighResolutionCapable + NSRemovableVolumesUsageDescription + TFTMAC uses the Android emulator runtime stored on MAC MINI M4. NSHumanReadableCopyright TFTMAC. Teamfight Tactics and Riot Games are property of their respective owners. diff --git a/tftmac/Presentation/EmbeddedEmulatorView.swift b/tftmac/Presentation/EmbeddedEmulatorView.swift new file mode 100644 index 0000000..08139a0 --- /dev/null +++ b/tftmac/Presentation/EmbeddedEmulatorView.swift @@ -0,0 +1,514 @@ +import AppKit +import MetalKit + +private final class PresenterGPUState: @unchecked Sendable { + private let lock = NSLock() + private var inFlight = [0, 0, 0] + private var completedPresentations: UInt64 = 0 + + func availableUploadSlot(excluding current: Int?) -> Int? { + lock.lock() + defer { lock.unlock() } + return inFlight.indices.first(where: { inFlight[$0] == 0 && $0 != current }) + ?? inFlight.indices.first(where: { inFlight[$0] == 0 }) + } + + func beginPresentation(slot: Int) { + lock.lock() + inFlight[slot] += 1 + lock.unlock() + } + + func completePresentation(slot: Int) { + lock.lock() + inFlight[slot] = max(0, inFlight[slot] - 1) + completedPresentations &+= 1 + lock.unlock() + } + + func completedCount() -> UInt64 { + lock.lock() + defer { lock.unlock() } + return completedPresentations + } +} + +private final class HostPresentationTelemetry: @unchecked Sendable { + private struct WindowState { + var startedMonotonicNS: UInt64 + var submittedFrames = 0 + var completedFrames = 0 + var uniqueSourceUploads = 0 + var repeatedSourcePresents = 0 + var drawableMisses = 0 + var encoderMisses = 0 + var commandBufferMisses = 0 + var commandErrors = 0 + var completionLatenciesMS = [Double]() + var gpuTimesMS = [Double]() + } + + private let lock = NSLock() + private var window = WindowState(startedMonotonicNS: DispatchTime.now().uptimeNanoseconds) + // A 60 Hz presenter needs only about 60 entries/window; this cap protects telemetry itself + // from becoming a source of memory pressure if the display rate changes. + private let maximumSamples = 256 + + func recordSubmitted(uniqueSourceUpload: Bool) { + lock.lock() + window.submittedFrames += 1 + if uniqueSourceUpload { + window.uniqueSourceUploads += 1 + } else { + window.repeatedSourcePresents += 1 + } + lock.unlock() + } + + func recordDrawableMiss() { + lock.lock() + window.drawableMisses += 1 + lock.unlock() + } + + func recordEncoderMiss() { + lock.lock() + window.encoderMisses += 1 + lock.unlock() + } + + func recordCommandBufferMiss() { + lock.lock() + window.commandBufferMisses += 1 + lock.unlock() + } + + func recordCompletion(submittedMonotonicNS: UInt64, commandBuffer: MTLCommandBuffer) { + let completedMonotonicNS = DispatchTime.now().uptimeNanoseconds + let completionMS = Double(completedMonotonicNS &- submittedMonotonicNS) / 1_000_000 + let gpuStart = commandBuffer.gpuStartTime + let gpuEnd = commandBuffer.gpuEndTime + let gpuMS: Double? = gpuStart > 0 && gpuEnd >= gpuStart ? (gpuEnd - gpuStart) * 1_000 : nil + // A completed-handler normally sees `.completed` or `.error`; treat any other terminal + // outcome as failed so the persisted count does not hide cancelled/abnormal work. + let wasError = commandBuffer.status != .completed || commandBuffer.error != nil + + lock.lock() + window.completedFrames += 1 + if wasError { window.commandErrors += 1 } + if window.completionLatenciesMS.count < maximumSamples { + window.completionLatenciesMS.append(completionMS) + } + if let gpuMS, window.gpuTimesMS.count < maximumSamples { + window.gpuTimesMS.append(gpuMS) + } + lock.unlock() + } + + /// Drains a bounded approximately-one-second host window. Completion callbacks may arrive on + /// Metal worker threads, so the whole snapshot/reset operation is lock-protected. + func drainIfNeeded(nowMonotonicNS: UInt64) -> HostPresentationWindow? { + lock.lock() + defer { lock.unlock() } + let elapsedNS = nowMonotonicNS &- window.startedMonotonicNS + guard elapsedNS >= 1_000_000_000 else { return nil } + let snapshot = window + window = WindowState(startedMonotonicNS: nowMonotonicNS) + return HostPresentationWindow( + startedMonotonicNS: snapshot.startedMonotonicNS, + endedMonotonicNS: nowMonotonicNS, + submittedFrames: snapshot.submittedFrames, + completedFrames: snapshot.completedFrames, + uniqueSourceUploads: snapshot.uniqueSourceUploads, + repeatedSourcePresents: snapshot.repeatedSourcePresents, + // The shared schema exposes one presentation-miss field. Encoder and command-buffer + // misses cannot produce a drawable either, so include them while retaining separate + // in-memory counters above for their distinct collection paths. + drawableMisses: snapshot.drawableMisses + snapshot.encoderMisses + snapshot.commandBufferMisses, + commandErrors: snapshot.commandErrors, + meanCompletionLatencyMS: Self.mean(snapshot.completionLatenciesMS), + p95CompletionLatencyMS: Self.percentile(snapshot.completionLatenciesMS, percentile: 0.95), + p99CompletionLatencyMS: Self.percentile(snapshot.completionLatenciesMS, percentile: 0.99), + maximumCompletionLatencyMS: snapshot.completionLatenciesMS.max(), + meanGPUTimeMS: Self.mean(snapshot.gpuTimesMS), + p95GPUTimeMS: Self.percentile(snapshot.gpuTimesMS, percentile: 0.95), + maximumGPUTimeMS: snapshot.gpuTimesMS.max() + ) + } + + private static func mean(_ values: [Double]) -> Double? { + guard !values.isEmpty else { return nil } + return values.reduce(0, +) / Double(values.count) + } + + private static func percentile(_ values: [Double], percentile: Double) -> Double? { + guard !values.isEmpty else { return nil } + let sorted = values.sorted() + let index = min(sorted.count - 1, max(0, Int(ceil(Double(sorted.count) * percentile)) - 1)) + return sorted[index] + } +} + +@MainActor +final class EmbeddedEmulatorView: MTKView, MTKViewDelegate { + var onTouchInput: ((TouchInput) -> Void)? + var onMouseInput: ((Int32, Int32, Int32) -> Void)? + var onKeyboardInput: ((String?, String?) -> Void)? + var onPresentationSample: ((PresentationSample) -> Void)? + var onHostPresentationWindow: ((HostPresentationWindow) -> Void)? + + private let mailbox: LatestFrameMailbox + private let commandQueue: MTLCommandQueue + private let pipeline: MTLRenderPipelineState + private let gpuState = PresenterGPUState() + private let hostPresentationTelemetry = HostPresentationTelemetry() + private var textures: [MTLTexture?] = [nil, nil, nil] + private var currentTextureSlot: Int? + private var lastPresentedSequence: UInt32? + private var lastSampleTime = CACurrentMediaTime() + private var lastSamplePresentationCount: UInt64 = 0 + private var lastSampleReceivedCount: UInt64 = 0 + private var lastSourceFPS: Double = 0 + private var lastPresentationFPS: Double = 0 + private var lastHostGPUTimeP95MS: Double? + private var gameFrameWindow: GameFrameTelemetryWindow? + private var primaryTouchSequence = PrimaryTouchSequence() + private let statusLabel = NSTextField(labelWithString: "Preparing native Android runtime…") + private let fpsLabel = NSTextField(labelWithString: "0 FPS") + + init(frame: NSRect, mailbox: LatestFrameMailbox) { + guard let device = MTLCreateSystemDefaultDevice() else { + fatalError("TFTMAC requires Metal on Apple Silicon") + } + guard let commandQueue = device.makeCommandQueue() else { + fatalError("TFTMAC could not create its persistent Metal command queue") + } + self.mailbox = mailbox + self.commandQueue = commandQueue + do { + pipeline = try Self.makePipeline(device: device) + } catch { + fatalError("TFTMAC could not create its native frame pipeline: \(error.localizedDescription)") + } + super.init(frame: frame, device: device) + framebufferOnly = true + colorPixelFormat = .bgra8Unorm_srgb + preferredFramesPerSecond = 60 + enableSetNeedsDisplay = false + isPaused = false + clearColor = MTLClearColorMake(0.015, 0.018, 0.025, 1.0) + delegate = self + configureOverlays() + updatePerformanceOverlay() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var acceptsFirstResponder: Bool { true } + + func setStatus(_ text: String, isError: Bool) { + // Informational runtime state belongs in SQL telemetry, not over the + // Android display. Only a terminal error may interrupt the game view. + guard isError, !text.isEmpty else { + statusLabel.isHidden = true + return + } + statusLabel.stringValue = text + statusLabel.textColor = .systemRed + statusLabel.isHidden = false + } + + /// The runtime collector owns Android SurfaceFlinger truth. This presenter never substitutes + /// ingress or Metal presentation rates for actual guest frame production. + func setGameFrameWindow(_ window: GameFrameTelemetryWindow?) { + gameFrameWindow = window + updatePerformanceOverlay() + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} + + func draw(in view: MTKView) { + let uploadedNewSource = uploadNewestFrameIfPossible() + guard let slot = currentTextureSlot, let texture = textures[slot] else { + updatePresentationSampleIfNeeded() + return + } + guard let drawable = currentDrawable, let descriptor = currentRenderPassDescriptor else { + hostPresentationTelemetry.recordDrawableMiss() + updatePresentationSampleIfNeeded() + return + } + guard let buffer = commandQueue.makeCommandBuffer() else { + hostPresentationTelemetry.recordCommandBufferMiss() + updatePresentationSampleIfNeeded() + return + } + guard let encoder = buffer.makeRenderCommandEncoder(descriptor: descriptor) else { + hostPresentationTelemetry.recordEncoderMiss() + updatePresentationSampleIfNeeded() + return + } + + let source = CGSize(width: texture.width, height: texture.height) + let target = drawableSize + let scale = min(target.width / source.width, target.height / source.height) + let renderWidth = source.width * scale + let renderHeight = source.height * scale + encoder.setViewport(MTLViewport( + originX: (target.width - renderWidth) / 2, + originY: (target.height - renderHeight) / 2, + width: renderWidth, + height: renderHeight, + znear: 0, + zfar: 1 + )) + encoder.setRenderPipelineState(pipeline) + encoder.setFragmentTexture(texture, index: 0) + encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) + encoder.endEncoding() + + let submittedMonotonicNS = DispatchTime.now().uptimeNanoseconds + hostPresentationTelemetry.recordSubmitted(uniqueSourceUpload: uploadedNewSource) + gpuState.beginPresentation(slot: slot) + let state = gpuState + let telemetry = hostPresentationTelemetry + buffer.addCompletedHandler { commandBuffer in + state.completePresentation(slot: slot) + telemetry.recordCompletion(submittedMonotonicNS: submittedMonotonicNS, commandBuffer: commandBuffer) + } + buffer.present(drawable) + buffer.commit() + updatePresentationSampleIfNeeded() + } + + override func mouseDown(with event: NSEvent) { + window?.makeFirstResponder(self) + sendTouch(event, isContact: true) + } + override func mouseDragged(with event: NSEvent) { sendTouch(event, isContact: true) } + override func mouseUp(with event: NSEvent) { sendTouch(event, isContact: false) } + override func rightMouseDown(with event: NSEvent) { sendMouse(event, buttons: 2) } + override func rightMouseDragged(with event: NSEvent) { sendMouse(event, buttons: 2) } + override func rightMouseUp(with event: NSEvent) { sendMouse(event, buttons: 0) } + + override func keyDown(with event: NSEvent) { + if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command) { + super.keyDown(with: event) + return + } + if let key = Self.specialKey(for: event) { + onKeyboardInput?(nil, key) + } else if let text = event.characters, !text.isEmpty { + onKeyboardInput?(text, nil) + } + } + + override func performKeyEquivalent(with event: NSEvent) -> Bool { + let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + if modifiers == .command, event.charactersIgnoringModifiers?.lowercased() == "v" { + paste(nil) + return true + } + return super.performKeyEquivalent(with: event) + } + + @objc func paste(_ sender: Any?) { + guard let text = NSPasteboard.general.string(forType: .string), !text.isEmpty else { return } + onKeyboardInput?(String(text.prefix(1024)), nil) + } + + @discardableResult + private func uploadNewestFrameIfPossible() -> Bool { + guard let frame = mailbox.takeLatest() else { return false } + guard let slot = gpuState.availableUploadSlot(excluding: currentTextureSlot) else { return false } + if textures[slot] == nil { + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: .rgba8Unorm_srgb, + width: frame.width, + height: frame.height, + mipmapped: false + ) + descriptor.usage = [.shaderRead] + descriptor.storageMode = .shared + textures[slot] = device?.makeTexture(descriptor: descriptor) + textures[slot]?.label = "TFTMAC Android frame \(slot)" + } + guard let texture = textures[slot] else { return false } + frame.pixels.withUnsafeBytes { bytes in + guard let baseAddress = bytes.baseAddress else { return } + texture.replace( + region: MTLRegionMake2D(0, 0, frame.width, frame.height), + mipmapLevel: 0, + withBytes: baseAddress, + bytesPerRow: frame.width * FrameContract.bytesPerPixel + ) + } + currentTextureSlot = slot + lastPresentedSequence = frame.sequence + return true + } + + private func androidPoint(for event: NSEvent) -> TouchPoint? { + let location = convert(event.locationInWindow, from: nil) + let mapper = ViewportMapper( + sourceSize: CGSize(width: FrameContract.width, height: FrameContract.height), + viewportSize: bounds.size + ) + guard let source = mapper.sourcePoint(for: location) else { return nil } + let x = Int32(max(0, min(FrameContract.width - 1, Int(source.x.rounded())))) + let topOriginY = FrameContract.height - 1 - Int(source.y.rounded()) + let y = Int32(max(0, min(FrameContract.height - 1, topOriginY))) + return TouchPoint(x: x, y: y) + } + + private func sendTouch(_ event: NSEvent, isContact: Bool) { + let point = androidPoint(for: event) + let input = isContact + ? primaryTouchSequence.contact(at: point) + : primaryTouchSequence.release(at: point) + guard let input else { return } + onTouchInput?(input) + } + + private func sendMouse(_ event: NSEvent, buttons: Int32) { + guard let point = androidPoint(for: event) else { return } + onMouseInput?(point.x, point.y, buttons) + } + + private func updatePresentationSampleIfNeeded() { + let now = CACurrentMediaTime() + let elapsed = now - lastSampleTime + guard elapsed >= 1 else { return } + let total = gpuState.completedCount() + let delta = total - lastSamplePresentationCount + let presentationFPS = Double(delta) / elapsed + let mailboxSnapshot = mailbox.snapshot() + let receivedDelta = mailboxSnapshot.receivedFrames - lastSampleReceivedCount + let sourceFPS = Double(receivedDelta) / elapsed + lastSourceFPS = sourceFPS + lastPresentationFPS = presentationFPS + let sample = PresentationSample( + presentedFrames: total, + presentationFPS: presentationFPS, + sourceFPS: sourceFPS, + mailbox: mailboxSnapshot, + lastPresentedSequence: lastPresentedSequence, + sampledMonotonicNanoseconds: DispatchTime.now().uptimeNanoseconds + ) + onPresentationSample?(sample) + lastSamplePresentationCount = total + lastSampleReceivedCount = mailboxSnapshot.receivedFrames + lastSampleTime = now + if let hostWindow = hostPresentationTelemetry.drainIfNeeded(nowMonotonicNS: DispatchTime.now().uptimeNanoseconds) { + lastHostGPUTimeP95MS = hostWindow.p95GPUTimeMS + onHostPresentationWindow?(hostWindow) + } + updatePerformanceOverlay() + } + + private func updatePerformanceOverlay() { + let guestLine: String + if let gameFrameWindow, case .available = gameFrameWindow.status { + let low = gameFrameWindow.onePercentLowFPS.map { String(format: "%.0f", $0) } ?? "—" + let p99 = gameFrameWindow.p99MS.map { String(format: "%.1f", $0) } ?? "—" + guestLine = String(format: "TFT %.0f · 1%% %@ · P99 %@ms", gameFrameWindow.effectiveFPS, low, p99) + } else { + guestLine = "TFT —" + } + let gpu = lastHostGPUTimeP95MS.map { String(format: "%.1f", $0) } ?? "—" + fpsLabel.stringValue = String( + format: "%@\nPIPE %.0f · MAC %.0f · GPU %@ms", + guestLine, + lastSourceFPS, + lastPresentationFPS, + gpu + ) + } + + private func configureOverlays() { + statusLabel.font = .systemFont(ofSize: 16, weight: .semibold) + statusLabel.alignment = .center + statusLabel.maximumNumberOfLines = 3 + statusLabel.translatesAutoresizingMaskIntoConstraints = false + statusLabel.wantsLayer = true + statusLabel.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.72).cgColor + statusLabel.layer?.cornerRadius = 10 + statusLabel.isHidden = true + addSubview(statusLabel) + + fpsLabel.font = .monospacedDigitSystemFont(ofSize: 13, weight: .bold) + fpsLabel.textColor = .white + fpsLabel.alignment = .right + fpsLabel.maximumNumberOfLines = 2 + fpsLabel.translatesAutoresizingMaskIntoConstraints = false + fpsLabel.wantsLayer = true + fpsLabel.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.62).cgColor + fpsLabel.layer?.cornerRadius = 6 + addSubview(fpsLabel) + + NSLayoutConstraint.activate([ + statusLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + statusLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + statusLabel.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.72), + statusLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 52), + fpsLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12), + fpsLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), + fpsLabel.widthAnchor.constraint(equalToConstant: 300), + fpsLabel.heightAnchor.constraint(equalToConstant: 46) + ]) + } + + private static func makePipeline(device: MTLDevice) throws -> MTLRenderPipelineState { + let source = """ + #include + using namespace metal; + + struct RasterData { + float4 position [[position]]; + float2 textureCoordinate; + }; + + vertex RasterData tftmac_vertex(uint vertexID [[vertex_id]]) { + const float2 positions[3] = { float2(-1.0, -1.0), float2(3.0, -1.0), float2(-1.0, 3.0) }; + // Metal's bottom screen edge must sample the bottom RGBA row. + // The oversized triangle therefore maps bottom vertices to v=1. + const float2 coordinates[3] = { float2(0.0, 1.0), float2(2.0, 1.0), float2(0.0, -1.0) }; + RasterData output; + output.position = float4(positions[vertexID], 0.0, 1.0); + output.textureCoordinate = coordinates[vertexID]; + return output; + } + + fragment float4 tftmac_fragment(RasterData input [[stage_in]], texture2d frame [[texture(0)]]) { + constexpr sampler sampleState(coord::normalized, address::clamp_to_edge, filter::linear); + return frame.sample(sampleState, input.textureCoordinate); + } + """ + let library = try device.makeLibrary(source: source, options: nil) + let descriptor = MTLRenderPipelineDescriptor() + descriptor.label = "TFTMAC RGBA presenter" + descriptor.vertexFunction = library.makeFunction(name: "tftmac_vertex") + descriptor.fragmentFunction = library.makeFunction(name: "tftmac_fragment") + descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm_srgb + return try device.makeRenderPipelineState(descriptor: descriptor) + } + + private static func specialKey(for event: NSEvent) -> String? { + switch event.keyCode { + case 36, 76: return "Enter" + case 48: return "Tab" + case 51, 117: return "Backspace" + case 53: return "GoBack" + case 111: return "Power" + case 115: return "GoHome" + case 119: return "End" + case 123: return "ArrowLeft" + case 124: return "ArrowRight" + case 125: return "ArrowDown" + case 126: return "ArrowUp" + default: return nil + } + } +} diff --git a/tftmac/Presentation/FrameContract.swift b/tftmac/Presentation/FrameContract.swift new file mode 100644 index 0000000..fb513b1 --- /dev/null +++ b/tftmac/Presentation/FrameContract.swift @@ -0,0 +1,95 @@ +import Foundation + +struct EmulatorFrame: @unchecked Sendable { + let pixels: Data + let width: Int + let height: Int + let sequence: UInt32 + let emulatorTimestampMicroseconds: UInt64 + let receivedMonotonicNanoseconds: UInt64 +} + +enum FrameContractError: LocalizedError, Equatable { + case inactiveDisplay + case wrongDimensions(width: Int, height: Int) + case wrongByteCount(expected: Int, actual: Int) + + var errorDescription: String? { + switch self { + case .inactiveDisplay: + return "The Android display is inactive." + case .wrongDimensions(let width, let height): + return "Expected a 1920×1080 Android frame, received \(width)×\(height)." + case .wrongByteCount(let expected, let actual): + return "Expected \(expected) RGBA bytes, received \(actual)." + } + } +} + +enum FrameContract { + static let width = 1920 + static let height = 1080 + static let bytesPerPixel = 4 + static let expectedByteCount = width * height * bytesPerPixel + + static func validate(width: Int, height: Int, byteCount: Int) throws { + guard width > 0, height > 0 else { throw FrameContractError.inactiveDisplay } + guard width == Self.width, height == Self.height else { + throw FrameContractError.wrongDimensions(width: width, height: height) + } + guard byteCount == expectedByteCount else { + throw FrameContractError.wrongByteCount(expected: expectedByteCount, actual: byteCount) + } + } +} + +struct FrameMailboxSnapshot: Sendable { + let receivedFrames: UInt64 + let replacedBeforePresentation: UInt64 + let sequenceDrops: UInt64 + let latestSequence: UInt32? + let latestReceiveMonotonicNanoseconds: UInt64? +} + +final class LatestFrameMailbox: @unchecked Sendable { + private let lock = NSLock() + private var latest: EmulatorFrame? + private var receivedFrames: UInt64 = 0 + private var replacedBeforePresentation: UInt64 = 0 + private var sequenceDrops: UInt64 = 0 + private var previousSequence: UInt32? + private var latestReceiveMonotonicNanoseconds: UInt64? + + func publish(_ frame: EmulatorFrame) { + lock.lock() + defer { lock.unlock() } + receivedFrames &+= 1 + if latest != nil { replacedBeforePresentation &+= 1 } + if let previousSequence, frame.sequence > previousSequence &+ 1 { + sequenceDrops &+= UInt64(frame.sequence - previousSequence - 1) + } + previousSequence = frame.sequence + latestReceiveMonotonicNanoseconds = frame.receivedMonotonicNanoseconds + latest = frame + } + + func takeLatest() -> EmulatorFrame? { + lock.lock() + defer { lock.unlock() } + let frame = latest + latest = nil + return frame + } + + func snapshot() -> FrameMailboxSnapshot { + lock.lock() + defer { lock.unlock() } + return FrameMailboxSnapshot( + receivedFrames: receivedFrames, + replacedBeforePresentation: replacedBeforePresentation, + sequenceDrops: sequenceDrops, + latestSequence: previousSequence, + latestReceiveMonotonicNanoseconds: latestReceiveMonotonicNanoseconds + ) + } +} diff --git a/tftmac/Presentation/ViewportMapper.swift b/tftmac/Presentation/ViewportMapper.swift new file mode 100644 index 0000000..7a466a8 --- /dev/null +++ b/tftmac/Presentation/ViewportMapper.swift @@ -0,0 +1,30 @@ +import CoreGraphics + +struct ViewportMapper: Sendable { + let sourceSize: CGSize + let viewportSize: CGSize + + var displayedRect: CGRect { + guard sourceSize.width > 0, sourceSize.height > 0, + viewportSize.width > 0, viewportSize.height > 0 else { + return .zero + } + let scale = min(viewportSize.width / sourceSize.width, viewportSize.height / sourceSize.height) + let size = CGSize(width: sourceSize.width * scale, height: sourceSize.height * scale) + return CGRect( + x: (viewportSize.width - size.width) / 2, + y: (viewportSize.height - size.height) / 2, + width: size.width, + height: size.height + ) + } + + func sourcePoint(for viewportPoint: CGPoint) -> CGPoint? { + let rect = displayedRect + guard rect.width > 0, rect.height > 0, rect.contains(viewportPoint) else { return nil } + return CGPoint( + x: (viewportPoint.x - rect.minX) / rect.width * sourceSize.width, + y: (viewportPoint.y - rect.minY) / rect.height * sourceSize.height + ) + } +} diff --git a/tftmac/Runtime/AVDTransactionGuard.swift b/tftmac/Runtime/AVDTransactionGuard.swift new file mode 100644 index 0000000..94516c3 --- /dev/null +++ b/tftmac/Runtime/AVDTransactionGuard.swift @@ -0,0 +1,52 @@ +import Foundation + +enum AVDTransactionRestoreDecision: Equatable, Sendable { + case alreadyOriginal + case restoreBackup +} + +enum AVDTransactionGuardError: LocalizedError, Equatable, Sendable { + case conflictingCurrentConfiguration + case unexpectedRecoveryPath + + var errorDescription: String? { + switch self { + case .conflictingCurrentConfiguration: + return "The AVD config changed after TFTMAC applied its profile; automatic restore stopped without overwriting it." + case .unexpectedRecoveryPath: + return "The interrupted AVD transaction names a path outside TFTMAC's exact config and capture roots; recovery stopped safely." + } + } +} + +enum AVDTransactionGuard { + static func restoreDecision( + currentSHA256: String, + originalSHA256: String, + appliedSHA256: String + ) throws -> AVDTransactionRestoreDecision { + if currentSHA256 == originalSHA256 { return .alreadyOriginal } + guard currentSHA256 == appliedSHA256 else { + throw AVDTransactionGuardError.conflictingCurrentConfiguration + } + return .restoreBackup + } + + static func validateRecoveryPaths( + markerConfigURL: URL, + expectedConfigURL: URL, + backupURL: URL, + captureRoot: URL + ) throws { + let markerConfig = markerConfigURL.standardizedFileURL.resolvingSymlinksInPath() + let expectedConfig = expectedConfigURL.standardizedFileURL.resolvingSymlinksInPath() + let backup = backupURL.standardizedFileURL.resolvingSymlinksInPath() + let captures = captureRoot.standardizedFileURL.resolvingSymlinksInPath() + let capturesPrefix = captures.path.hasSuffix("/") ? captures.path : captures.path + "/" + guard markerConfig.path == expectedConfig.path, + backup.path.hasPrefix(capturesPrefix), + backup.lastPathComponent == "avd-config.before.ini" else { + throw AVDTransactionGuardError.unexpectedRecoveryPath + } + } +} diff --git a/tftmac/Runtime/CombatBenchmarkAnalysis.swift b/tftmac/Runtime/CombatBenchmarkAnalysis.swift new file mode 100644 index 0000000..4284a44 --- /dev/null +++ b/tftmac/Runtime/CombatBenchmarkAnalysis.swift @@ -0,0 +1,210 @@ +import Foundation + +enum CombatLayerIdentity { + static func comparable(_ identity: String) -> String? { + let stable = "SurfaceView[com.riotgames.league.teamfighttactics/com.epicgames.unreal.GameActivity]" + return identity.contains(stable) ? stable : nil + } +} + +enum CombatBenchmarkDecision: String, Equatable, Sendable { + case homeRun = "HOME_RUN" + case promising = "PROMISING" + case reject = "REJECT" + case inconclusive = "INCONCLUSIVE" +} + +enum CombatBenchmarkValidityFailure: String, Equatable, Sendable { + case combatDurationTooShort + case insufficientSurfaceAvailability + case insufficientClockCoverage + case clockRoundTripTooHigh + case frameHistoryTruncated + case exactLayerChangedOrAmbiguous +} + +struct CombatBenchmarkValidity: Equatable, Sendable { + static let minimumCombatDurationSeconds = 300.0 + static let minimumSurfaceAvailability = 0.95 + static let minimumClockCoverage = 0.95 + static let maximumP95ClockRoundTripMilliseconds = 10.0 + + let failures: [CombatBenchmarkValidityFailure] + + var isValid: Bool { failures.isEmpty } + + static func evaluate(_ metrics: CombatBenchmarkMetrics) -> CombatBenchmarkValidity { + var failures = [CombatBenchmarkValidityFailure]() + if metrics.combatDurationSeconds < minimumCombatDurationSeconds { + failures.append(.combatDurationTooShort) + } + if metrics.surfaceAvailability < minimumSurfaceAvailability { + failures.append(.insufficientSurfaceAvailability) + } + if metrics.clockCoverage < minimumClockCoverage { + failures.append(.insufficientClockCoverage) + } + if metrics.p95ClockRoundTripMilliseconds > maximumP95ClockRoundTripMilliseconds { + failures.append(.clockRoundTripTooHigh) + } + if metrics.frameHistoryTruncated { + failures.append(.frameHistoryTruncated) + } + if !metrics.exactLayerStable { + failures.append(.exactLayerChangedOrAmbiguous) + } + return CombatBenchmarkValidity(failures: failures) + } +} + +struct CombatBenchmarkMetrics: Equatable, Sendable { + let combatDurationSeconds: Double + let surfaceAvailability: Double + let clockCoverage: Double + let p95ClockRoundTripMilliseconds: Double + let frameHistoryTruncated: Bool + let exactLayerStable: Bool + let correctnessPassed: Bool + let weightedFPS: Double + let onePercentLowFPS: Double + let p95IntervalMilliseconds: Double + let p99IntervalMilliseconds: Double + let jankRate: Double + let severeRate: Double + let missedVsyncRate: Double + + init( + combatDurationSeconds: Double, + surfaceAvailability: Double, + clockCoverage: Double, + p95ClockRoundTripMilliseconds: Double, + frameHistoryTruncated: Bool, + exactLayerStable: Bool, + correctnessPassed: Bool, + weightedFPS: Double, + onePercentLowFPS: Double, + p95IntervalMilliseconds: Double, + p99IntervalMilliseconds: Double, + jankRate: Double, + severeRate: Double, + missedVsyncRate: Double + ) { + self.combatDurationSeconds = combatDurationSeconds + self.surfaceAvailability = surfaceAvailability + self.clockCoverage = clockCoverage + self.p95ClockRoundTripMilliseconds = p95ClockRoundTripMilliseconds + self.frameHistoryTruncated = frameHistoryTruncated + self.exactLayerStable = exactLayerStable + self.correctnessPassed = correctnessPassed + self.weightedFPS = weightedFPS + self.onePercentLowFPS = onePercentLowFPS + self.p95IntervalMilliseconds = p95IntervalMilliseconds + self.p99IntervalMilliseconds = p99IntervalMilliseconds + self.jankRate = jankRate + self.severeRate = severeRate + self.missedVsyncRate = missedVsyncRate + } + + static func onePercentLowFPS(from intervalMilliseconds: [Double]) -> Double { + guard !intervalMilliseconds.isEmpty else { return 0 } + let slowFrameCount = max(1, Int(ceil(Double(intervalMilliseconds.count) * 0.01))) + let slowest = intervalMilliseconds.sorted().suffix(slowFrameCount) + let meanSlowInterval = slowest.reduce(0, +) / Double(slowest.count) + return meanSlowInterval > 0 ? 1_000 / meanSlowInterval : 0 + } +} + +struct CombatBenchmarkDeltas: Equatable, Sendable { + /// Positive values mean the candidate has more FPS than the baseline. + let weightedFPSPercent: Double + /// Positive values mean the candidate has more 1%-low FPS than the baseline. + let onePercentLowFPSPercent: Double + /// Positive values mean the candidate has a longer (worse) frame interval. + let p95IntervalPercent: Double + /// Positive values mean the candidate has a longer (worse) frame interval. + let p99IntervalPercent: Double + /// Positive values mean more affected frames than the baseline. + let jankRatePercentagePoints: Double + /// Positive values mean more severe frames than the baseline. + let severeRatePercentagePoints: Double + /// Positive values mean more missed-vsync equivalents than the baseline. + let missedVsyncRatePercentagePoints: Double + + init(baseline: CombatBenchmarkMetrics, candidate: CombatBenchmarkMetrics) { + weightedFPSPercent = Self.percentChange(from: baseline.weightedFPS, to: candidate.weightedFPS) + onePercentLowFPSPercent = Self.percentChange(from: baseline.onePercentLowFPS, to: candidate.onePercentLowFPS) + p95IntervalPercent = Self.percentChange(from: baseline.p95IntervalMilliseconds, to: candidate.p95IntervalMilliseconds) + p99IntervalPercent = Self.percentChange(from: baseline.p99IntervalMilliseconds, to: candidate.p99IntervalMilliseconds) + jankRatePercentagePoints = candidate.jankRate - baseline.jankRate + severeRatePercentagePoints = candidate.severeRate - baseline.severeRate + missedVsyncRatePercentagePoints = candidate.missedVsyncRate - baseline.missedVsyncRate + } + + private static func percentChange(from baseline: Double, to candidate: Double) -> Double { + guard baseline > 0 else { return candidate == 0 ? 0 : .infinity } + return ((candidate - baseline) / baseline) * 100 + } +} + +struct CombatBenchmarkAnalysis: Equatable, Sendable { + let baselineValidity: CombatBenchmarkValidity + let candidateValidity: CombatBenchmarkValidity + let deltas: CombatBenchmarkDeltas + let decision: CombatBenchmarkDecision + + init(baseline: CombatBenchmarkMetrics, candidate: CombatBenchmarkMetrics) { + let baselineValidity = CombatBenchmarkValidity.evaluate(baseline) + let candidateValidity = CombatBenchmarkValidity.evaluate(candidate) + self.baselineValidity = baselineValidity + self.candidateValidity = candidateValidity + let deltas = CombatBenchmarkDeltas(baseline: baseline, candidate: candidate) + self.deltas = deltas + decision = Self.makeDecision( + baseline: baseline, + candidate: candidate, + baselineValidity: baselineValidity, + candidateValidity: candidateValidity, + deltas: deltas + ) + } + + private static func makeDecision( + baseline: CombatBenchmarkMetrics, + candidate: CombatBenchmarkMetrics, + baselineValidity: CombatBenchmarkValidity, + candidateValidity: CombatBenchmarkValidity, + deltas: CombatBenchmarkDeltas + ) -> CombatBenchmarkDecision { + guard baselineValidity.isValid, candidateValidity.isValid else { return .inconclusive } + guard baseline.correctnessPassed else { return .inconclusive } + guard candidate.correctnessPassed else { return .reject } + if deltas.p95IntervalPercent >= 10 || deltas.p99IntervalPercent >= 10 { return .reject } + if deltas.weightedFPSPercent < 5 { return .reject } + if isHomeRun(baseline: baseline, candidate: candidate, deltas: deltas) { return .homeRun } + if isPromising(deltas) { return .promising } + return .inconclusive + } + + private static func isHomeRun( + baseline: CombatBenchmarkMetrics, + candidate: CombatBenchmarkMetrics, + deltas: CombatBenchmarkDeltas + ) -> Bool { + deltas.onePercentLowFPSPercent >= 20 + && relativeReductionIsAtLeast30Percent(from: baseline.jankRate, to: candidate.jankRate) + && relativeReductionIsAtLeast30Percent(from: baseline.severeRate, to: candidate.severeRate) + && (deltas.weightedFPSPercent >= 10 || deltas.p95IntervalPercent <= -15) + } + + private static func isPromising(_ deltas: CombatBenchmarkDeltas) -> Bool { + deltas.weightedFPSPercent >= 5 + && deltas.onePercentLowFPSPercent >= 10 + && deltas.p95IntervalPercent <= 0 + && deltas.p99IntervalPercent <= 0 + } + + private static func relativeReductionIsAtLeast30Percent(from baseline: Double, to candidate: Double) -> Bool { + guard baseline > 0 else { return false } + return (baseline - candidate) / baseline >= 0.30 + } +} diff --git a/tftmac/Runtime/CombatBenchmarkStore.swift b/tftmac/Runtime/CombatBenchmarkStore.swift new file mode 100644 index 0000000..f6a5266 --- /dev/null +++ b/tftmac/Runtime/CombatBenchmarkStore.swift @@ -0,0 +1,488 @@ +import Foundation +import SQLite3 + +struct CombatBenchmarkRun: Sendable { + let benchmarkID: String + let sessionID: String + let presetID: RuntimeExperimentPreset + let configurationSHA256: String + let comparisonIdentitySHA256: String + let configurationJSON: String + let tftPackageVersion: String + let performanceModeConfirmed: Bool + let startedUTC: String + let endedUTC: String + let startedMonotonicNS: UInt64 + let endedMonotonicNS: UInt64 + let exactLayerIdentity: String? + let metrics: CombatBenchmarkMetrics + let p50IntervalMilliseconds: Double + let maximumIntervalMilliseconds: Double + let observerOverheadInvalid: Bool + + var validity: CombatBenchmarkValidity { .evaluate(metrics) } + var isValid: Bool { metrics.correctnessPassed && validity.isValid } + var invalidReason: String? { + if !metrics.correctnessPassed { return "REJECTED_CORRECTNESS" } + let reasons = validity.failures.map(\.rawValue) + return reasons.isEmpty ? nil : reasons.joined(separator: ",") + } +} + +struct CombatIncidentRecord: Sendable { + let incidentID: String + let benchmarkID: String + let sessionID: String + let presetID: RuntimeExperimentPreset + let trigger: String + let observedMonotonicNS: UInt64 + let effectiveFPS: Double? + let onePercentLowFPS: Double? + let p99IntervalMilliseconds: Double? + let severeCount: Int + let traceSequence: Int? + let firstDivergentBoundary: String + let confidence: String + let explicitUnknowns: String +} + +struct CombatComparisonRecord: Sendable { + let comparisonID: String + let controlBenchmarkID: String + let candidateBenchmarkID: String + let analysis: CombatBenchmarkAnalysis + let correctnessStatus: String + let observerOverheadInvalid: Bool + let createdUTC: String +} + +struct ActiveCombatBenchmark: Sendable { + let benchmarkID: String + let sessionID: String + let presetID: RuntimeExperimentPreset + let configurationSHA256: String + let comparisonIdentitySHA256: String + let configurationJSON: String + let tftPackageVersion: String + let performanceModeConfirmed: Bool + let startedUTC: String + let startedMonotonicNS: UInt64 + + private(set) var intervalMilliseconds = [Double]() + private(set) var traceActiveIntervals = [Double]() + private(set) var traceInactiveIntervals = [Double]() + private(set) var jankCount = 0 + private(set) var severeCount = 0 + private(set) var missedVsyncEquivalents = 0 + private(set) var measuredWindowNS: UInt64 = 0 + private(set) var availableWindowNS: UInt64 = 0 + private(set) var historyTruncated = false + private(set) var layerNames = Set() + private(set) var clockSamples = [(hostMidpointNS: UInt64, roundTripNS: UInt64)]() + + mutating func ingest( + update: GameFrameTelemetryUpdate?, + window: GameFrameTelemetryWindow, + traceActive: Bool + ) { + let duration = window.endedMonotonicNS >= window.startedMonotonicNS + ? window.endedMonotonicNS - window.startedMonotonicNS + : 0 + measuredWindowNS &+= duration + historyTruncated = historyTruncated || window.historyTruncated + if case .available = window.status { + availableWindowNS &+= duration + if let layerName = window.layerName { layerNames.insert(layerName) } + } + guard let update else { return } + let values = update.intervals.map(\.intervalMS) + intervalMilliseconds.append(contentsOf: values) + if traceActive { + traceActiveIntervals.append(contentsOf: values) + } else { + traceInactiveIntervals.append(contentsOf: values) + } + jankCount += update.intervals.lazy.filter(\.isJanky).count + severeCount += update.intervals.lazy.filter(\.isSevere).count + missedVsyncEquivalents += update.intervals.reduce(0) { $0 + $1.missedVsyncEquivalents } + } + + mutating func recordClock(hostMidpointNS: UInt64, roundTripNS: UInt64) { + clockSamples.append((hostMidpointNS, roundTripNS)) + } + + func finish( + endedUTC: String, + endedMonotonicNS: UInt64, + correctnessPassed: Bool = true + ) -> CombatBenchmarkRun { + let durationNS = endedMonotonicNS >= startedMonotonicNS + ? endedMonotonicNS - startedMonotonicNS + : 0 + let durationSeconds = Double(durationNS) / 1_000_000_000 + let surfaceAvailability = measuredWindowNS == 0 + ? 0 + : min(1, Double(availableWindowNS) / Double(measuredWindowNS)) + let orderedClock = clockSamples.sorted { $0.hostMidpointNS < $1.hostMidpointNS } + let clockCoverage: Double + if let first = orderedClock.first?.hostMidpointNS, let last = orderedClock.last?.hostMidpointNS, + durationNS > 0, last >= first { + let bracketStart = max(startedMonotonicNS, first) + let bracketEnd = min(endedMonotonicNS, last) + clockCoverage = bracketEnd > bracketStart + ? min(1, Double(bracketEnd - bracketStart) / Double(durationNS)) + : 0 + } else { + clockCoverage = 0 + } + let orderedRTTMS = orderedClock.map { Double($0.roundTripNS) / 1_000_000 }.sorted() + let p95RTTMS = orderedRTTMS.isEmpty ? 1_000_000_000 : Self.percentile(orderedRTTMS, 0.95) + let ordered = intervalMilliseconds.sorted() + let intervalTotalMS = ordered.reduce(0, +) + let weightedFPS = intervalTotalMS > 0 ? Double(ordered.count) / (intervalTotalMS / 1_000) : 0 + let p50 = Self.percentile(ordered, 0.50) + let p95 = Self.percentile(ordered, 0.95) + let p99 = Self.percentile(ordered, 0.99) + let maximum = ordered.last ?? 0 + let count = max(1, ordered.count) + let metrics = CombatBenchmarkMetrics( + combatDurationSeconds: durationSeconds, + surfaceAvailability: surfaceAvailability, + clockCoverage: clockCoverage, + p95ClockRoundTripMilliseconds: p95RTTMS, + frameHistoryTruncated: historyTruncated, + exactLayerStable: layerNames.count == 1, + correctnessPassed: correctnessPassed, + weightedFPS: weightedFPS, + onePercentLowFPS: CombatBenchmarkMetrics.onePercentLowFPS(from: ordered), + p95IntervalMilliseconds: p95, + p99IntervalMilliseconds: p99, + jankRate: Double(jankCount) / Double(count), + severeRate: Double(severeCount) / Double(count), + missedVsyncRate: Double(missedVsyncEquivalents) / Double(count) + ) + return CombatBenchmarkRun( + benchmarkID: benchmarkID, + sessionID: sessionID, + presetID: presetID, + configurationSHA256: configurationSHA256, + comparisonIdentitySHA256: comparisonIdentitySHA256, + configurationJSON: configurationJSON, + tftPackageVersion: tftPackageVersion, + performanceModeConfirmed: performanceModeConfirmed, + startedUTC: startedUTC, + endedUTC: endedUTC, + startedMonotonicNS: startedMonotonicNS, + endedMonotonicNS: endedMonotonicNS, + exactLayerIdentity: layerNames.count == 1 ? layerNames.first : nil, + metrics: metrics, + p50IntervalMilliseconds: p50, + maximumIntervalMilliseconds: maximum, + observerOverheadInvalid: Self.observerOverheadInvalid( + traceActive: traceActiveIntervals, + traceInactive: traceInactiveIntervals + ) + ) + } + + private static func percentile(_ ordered: [Double], _ quantile: Double) -> Double { + guard !ordered.isEmpty else { return 0 } + let index = min(ordered.count - 1, max(0, Int(ceil(Double(ordered.count) * quantile)) - 1)) + return ordered[index] + } + + private static func observerOverheadInvalid(traceActive: [Double], traceInactive: [Double]) -> Bool { + guard traceActive.count >= 10, traceInactive.count >= 10 else { return false } + let activeTotal = traceActive.reduce(0, +) + let inactiveTotal = traceInactive.reduce(0, +) + guard activeTotal > 0, inactiveTotal > 0 else { return false } + let activeFPS = Double(traceActive.count) / (activeTotal / 1_000) + let inactiveFPS = Double(traceInactive.count) / (inactiveTotal / 1_000) + let fpsDelta = abs((activeFPS - inactiveFPS) / inactiveFPS) * 100 + let activeP95 = percentile(traceActive.sorted(), 0.95) + let inactiveP95 = percentile(traceInactive.sorted(), 0.95) + let p95Delta = inactiveP95 > 0 ? abs((activeP95 - inactiveP95) / inactiveP95) * 100 : 0 + return fpsDelta > 5 || p95Delta > 5 + } +} + +final class CombatBenchmarkLabStore: @unchecked Sendable { + private enum Value { + case integer(Int64) + case real(Double) + case text(String) + case null + } + + private var database: OpaquePointer? + private let transientDestructor = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + + init(applicationSupport: URL) throws { + try FileManager.default.createDirectory(at: applicationSupport, withIntermediateDirectories: true) + let databaseURL = applicationSupport.appendingPathComponent("TFTMAC_LAB.sqlite") + guard sqlite3_open_v2( + databaseURL.path, + &database, + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, + nil + ) == SQLITE_OK else { + throw TFTMACRuntimeError("The persistent TFTMAC lab database could not be opened.") + } + sqlite3_busy_timeout(database, 5_000) + try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: databaseURL.path) + try createSchema() + } + + deinit { + if let database { sqlite3_close(database) } + } + + func record(_ run: CombatBenchmarkRun) throws { + try execute(Self.insertBenchmarkSQL, Self.benchmarkValues(run)) + } + + func record(_ incident: CombatIncidentRecord) throws { + try execute(Self.insertIncidentSQL, Self.incidentValues(incident)) + } + + func comparisonForCandidate(_ candidate: CombatBenchmarkRun) throws -> CombatComparisonRecord? { + guard candidate.presetID.isActiveCandidate, + candidate.isValid, + candidate.exactLayerIdentity != nil, + let control = try latestValidControl(matching: candidate) else { return nil } + let analysis = CombatBenchmarkAnalysis(baseline: control.metrics, candidate: candidate.metrics) + let comparison = CombatComparisonRecord( + comparisonID: UUID().uuidString.lowercased(), + controlBenchmarkID: control.benchmarkID, + candidateBenchmarkID: candidate.benchmarkID, + analysis: analysis, + correctnessStatus: candidate.metrics.correctnessPassed ? "PASSED" : "REJECTED_CORRECTNESS", + observerOverheadInvalid: control.observerOverheadInvalid || candidate.observerOverheadInvalid, + createdUTC: Self.utcNow() + ) + try record(comparison) + return comparison + } + + func record(_ comparison: CombatComparisonRecord) throws { + let deltas = comparison.analysis.deltas + try execute( + """ + INSERT INTO combat_comparisons( + comparison_id, control_benchmark_id, candidate_benchmark_id, + weighted_fps_delta_percent, one_percent_low_delta_percent, + p95_delta_percent, p99_delta_percent, jank_delta_points, + severe_delta_points, missed_vsync_delta_points, correctness_status, + observer_overhead_invalid, decision, created_utc + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [ + .text(comparison.comparisonID), .text(comparison.controlBenchmarkID), + .text(comparison.candidateBenchmarkID), .real(deltas.weightedFPSPercent), + .real(deltas.onePercentLowFPSPercent), .real(deltas.p95IntervalPercent), + .real(deltas.p99IntervalPercent), .real(deltas.jankRatePercentagePoints), + .real(deltas.severeRatePercentagePoints), .real(deltas.missedVsyncRatePercentagePoints), + .text(comparison.correctnessStatus), .integer(comparison.observerOverheadInvalid ? 1 : 0), + .text(comparison.analysis.decision.rawValue), .text(comparison.createdUTC) + ] + ) + } + + private func latestValidControl(matching candidate: CombatBenchmarkRun) throws -> CombatBenchmarkRun? { + guard let database else { return nil } + let sql = """ + SELECT benchmark_id, session_id, configuration_sha256, comparison_identity_sha256, + configuration_json, tft_package_version, performance_mode_confirmed, started_utc, ended_utc, + started_monotonic_ns, ended_monotonic_ns, exact_layer_identity, + duration_seconds, surface_availability, clock_coverage, p95_clock_rtt_ms, + history_truncated, correctness_passed, weighted_fps, one_percent_low_fps, + p50_interval_ms, p95_interval_ms, p99_interval_ms, max_interval_ms, + jank_rate, severe_rate, missed_vsync_rate, observer_overhead_invalid + FROM combat_benchmarks + WHERE preset_id = 'control' AND valid = 1 AND correctness_passed = 1 + AND comparison_identity_sha256 = ? AND tft_package_version = ? + AND exact_layer_identity IS NOT NULL AND ended_utc <= ? + ORDER BY ended_utc DESC LIMIT 20 + """ + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK, + let statement else { throw TFTMACRuntimeError("SQLite could not prepare the control benchmark query.") } + defer { sqlite3_finalize(statement) } + sqlite3_bind_text(statement, 1, candidate.comparisonIdentitySHA256, -1, transientDestructor) + sqlite3_bind_text(statement, 2, candidate.tftPackageVersion, -1, transientDestructor) + sqlite3_bind_text(statement, 3, candidate.endedUTC, -1, transientDestructor) + func text(_ index: Int32) -> String { + guard let value = sqlite3_column_text(statement, index) else { return "" } + return String(cString: value) + } + guard let candidateLayer = CombatLayerIdentity.comparable(candidate.exactLayerIdentity!) else { return nil } + while sqlite3_step(statement) == SQLITE_ROW { + guard CombatLayerIdentity.comparable(text(11)) == candidateLayer else { continue } + let metrics = CombatBenchmarkMetrics( + combatDurationSeconds: sqlite3_column_double(statement, 12), + surfaceAvailability: sqlite3_column_double(statement, 13), + clockCoverage: sqlite3_column_double(statement, 14), + p95ClockRoundTripMilliseconds: sqlite3_column_double(statement, 15), + frameHistoryTruncated: sqlite3_column_int(statement, 16) != 0, + exactLayerStable: sqlite3_column_type(statement, 11) != SQLITE_NULL, + correctnessPassed: sqlite3_column_int(statement, 17) != 0, + weightedFPS: sqlite3_column_double(statement, 18), + onePercentLowFPS: sqlite3_column_double(statement, 19), + p95IntervalMilliseconds: sqlite3_column_double(statement, 21), + p99IntervalMilliseconds: sqlite3_column_double(statement, 22), + jankRate: sqlite3_column_double(statement, 24), + severeRate: sqlite3_column_double(statement, 25), + missedVsyncRate: sqlite3_column_double(statement, 26) + ) + return CombatBenchmarkRun( + benchmarkID: text(0), sessionID: text(1), presetID: .control, + configurationSHA256: text(2), comparisonIdentitySHA256: text(3), configurationJSON: text(4), + tftPackageVersion: text(5), performanceModeConfirmed: sqlite3_column_int(statement, 6) != 0, + startedUTC: text(7), endedUTC: text(8), + startedMonotonicNS: UInt64(bitPattern: sqlite3_column_int64(statement, 9)), + endedMonotonicNS: UInt64(bitPattern: sqlite3_column_int64(statement, 10)), + exactLayerIdentity: text(11), metrics: metrics, + p50IntervalMilliseconds: sqlite3_column_double(statement, 20), + maximumIntervalMilliseconds: sqlite3_column_double(statement, 23), + observerOverheadInvalid: sqlite3_column_int(statement, 27) != 0 + ) + } + return nil + } + + private func createSchema() throws { + guard let database else { throw TFTMACRuntimeError("The persistent lab database is closed.") } + guard sqlite3_exec(database, Self.schemaSQL, nil, nil, nil) == SQLITE_OK else { + throw TFTMACRuntimeError("The persistent combat benchmark schema could not be created.") + } + // Existing lab databases predate the comparison identity. Old rows stay + // nullable and cannot be paired with a new candidate. + sqlite3_exec( + database, + "ALTER TABLE combat_benchmarks ADD COLUMN comparison_identity_sha256 TEXT", + nil, + nil, + nil + ) + } + + private func execute(_ sql: String, _ values: [Value]) throws { + guard let database else { throw TFTMACRuntimeError("The persistent lab database is closed.") } + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK, + let statement else { throw TFTMACRuntimeError("SQLite could not prepare a combat benchmark statement.") } + defer { sqlite3_finalize(statement) } + for (offset, value) in values.enumerated() { + let index = Int32(offset + 1) + switch value { + case .integer(let value): sqlite3_bind_int64(statement, index, value) + case .real(let value): sqlite3_bind_double(statement, index, value) + case .text(let value): sqlite3_bind_text(statement, index, value, -1, transientDestructor) + case .null: sqlite3_bind_null(statement, index) + } + } + guard sqlite3_step(statement) == SQLITE_DONE else { + throw TFTMACRuntimeError("SQLite could not write a combat benchmark statement.") + } + } + + static let insertBenchmarkSQL = """ + INSERT OR REPLACE INTO combat_benchmarks( + benchmark_id, session_id, preset_id, configuration_sha256, comparison_identity_sha256, configuration_json, + tft_package_version, performance_mode_confirmed, started_utc, ended_utc, + started_monotonic_ns, ended_monotonic_ns, exact_layer_identity, duration_seconds, + surface_availability, clock_coverage, p95_clock_rtt_ms, history_truncated, + correctness_passed, weighted_fps, one_percent_low_fps, p50_interval_ms, + p95_interval_ms, p99_interval_ms, max_interval_ms, jank_rate, severe_rate, + missed_vsync_rate, observer_overhead_invalid, valid, invalid_reason + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + + private static func benchmarkValues(_ run: CombatBenchmarkRun) -> [Value] { + let metrics = run.metrics + return [ + .text(run.benchmarkID), .text(run.sessionID), .text(run.presetID.rawValue), + .text(run.configurationSHA256), .text(run.comparisonIdentitySHA256), + .text(run.configurationJSON), .text(run.tftPackageVersion), + .integer(run.performanceModeConfirmed ? 1 : 0), .text(run.startedUTC), .text(run.endedUTC), + .integer(Int64(bitPattern: run.startedMonotonicNS)), .integer(Int64(bitPattern: run.endedMonotonicNS)), + run.exactLayerIdentity.map(Value.text) ?? .null, .real(metrics.combatDurationSeconds), + .real(metrics.surfaceAvailability), .real(metrics.clockCoverage), + .real(metrics.p95ClockRoundTripMilliseconds), .integer(metrics.frameHistoryTruncated ? 1 : 0), + .integer(metrics.correctnessPassed ? 1 : 0), .real(metrics.weightedFPS), + .real(metrics.onePercentLowFPS), .real(run.p50IntervalMilliseconds), + .real(metrics.p95IntervalMilliseconds), .real(metrics.p99IntervalMilliseconds), + .real(run.maximumIntervalMilliseconds), .real(metrics.jankRate), .real(metrics.severeRate), + .real(metrics.missedVsyncRate), .integer(run.observerOverheadInvalid ? 1 : 0), + .integer(run.isValid ? 1 : 0), run.invalidReason.map(Value.text) ?? .null + ] + } + + static let insertIncidentSQL = """ + INSERT INTO combat_incidents( + incident_id, benchmark_id, session_id, preset_id, trigger, observed_monotonic_ns, + effective_fps, one_percent_low_fps, p99_interval_ms, severe_count, trace_sequence, + first_divergent_boundary, confidence, explicit_unknowns + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + + private static func incidentValues(_ incident: CombatIncidentRecord) -> [Value] { + [ + .text(incident.incidentID), .text(incident.benchmarkID), .text(incident.sessionID), + .text(incident.presetID.rawValue), .text(incident.trigger), + .integer(Int64(bitPattern: incident.observedMonotonicNS)), + incident.effectiveFPS.map(Value.real) ?? .null, + incident.onePercentLowFPS.map(Value.real) ?? .null, + incident.p99IntervalMilliseconds.map(Value.real) ?? .null, + .integer(Int64(incident.severeCount)), + incident.traceSequence.map { .integer(Int64($0)) } ?? .null, + .text(incident.firstDivergentBoundary), .text(incident.confidence), + .text(incident.explicitUnknowns) + ] + } + + static let schemaSQL = """ + CREATE TABLE IF NOT EXISTS combat_benchmarks( + benchmark_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, preset_id TEXT NOT NULL, + configuration_sha256 TEXT NOT NULL, comparison_identity_sha256 TEXT NOT NULL, + configuration_json TEXT NOT NULL, + tft_package_version TEXT NOT NULL, performance_mode_confirmed INTEGER NOT NULL, + started_utc TEXT NOT NULL, ended_utc TEXT NOT NULL, + started_monotonic_ns INTEGER NOT NULL, ended_monotonic_ns INTEGER NOT NULL, + exact_layer_identity TEXT, duration_seconds REAL NOT NULL, + surface_availability REAL NOT NULL, clock_coverage REAL NOT NULL, + p95_clock_rtt_ms REAL NOT NULL, history_truncated INTEGER NOT NULL, + correctness_passed INTEGER NOT NULL, weighted_fps REAL NOT NULL, + one_percent_low_fps REAL NOT NULL, p50_interval_ms REAL NOT NULL, + p95_interval_ms REAL NOT NULL, p99_interval_ms REAL NOT NULL, + max_interval_ms REAL NOT NULL, jank_rate REAL NOT NULL, severe_rate REAL NOT NULL, + missed_vsync_rate REAL NOT NULL, observer_overhead_invalid INTEGER NOT NULL, + valid INTEGER NOT NULL, invalid_reason TEXT + ); + CREATE TABLE IF NOT EXISTS combat_incidents( + incident_id TEXT PRIMARY KEY, benchmark_id TEXT NOT NULL, session_id TEXT NOT NULL, + preset_id TEXT NOT NULL, trigger TEXT NOT NULL, observed_monotonic_ns INTEGER NOT NULL, + effective_fps REAL, one_percent_low_fps REAL, p99_interval_ms REAL, + severe_count INTEGER NOT NULL, trace_sequence INTEGER, + first_divergent_boundary TEXT NOT NULL, confidence TEXT NOT NULL, + explicit_unknowns TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS combat_comparisons( + comparison_id TEXT PRIMARY KEY, control_benchmark_id TEXT NOT NULL, + candidate_benchmark_id TEXT NOT NULL, weighted_fps_delta_percent REAL NOT NULL, + one_percent_low_delta_percent REAL NOT NULL, p95_delta_percent REAL NOT NULL, + p99_delta_percent REAL NOT NULL, jank_delta_points REAL NOT NULL, + severe_delta_points REAL NOT NULL, missed_vsync_delta_points REAL NOT NULL, + correctness_status TEXT NOT NULL, observer_overhead_invalid INTEGER NOT NULL, + decision TEXT NOT NULL, created_utc TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_combat_benchmarks_preset_end ON combat_benchmarks(preset_id, ended_utc); + CREATE INDEX IF NOT EXISTS idx_combat_incidents_benchmark_time ON combat_incidents(benchmark_id, observed_monotonic_ns); + """ + + private static func utcNow() -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: Date()) + } +} diff --git a/tftmac/Runtime/GameFrameTelemetry.swift b/tftmac/Runtime/GameFrameTelemetry.swift new file mode 100644 index 0000000..cb21d7d --- /dev/null +++ b/tftmac/Runtime/GameFrameTelemetry.swift @@ -0,0 +1,300 @@ +import Foundation + +enum GameFrameTelemetryUnavailable: Sendable, Equatable { + case noTFTSurfaceView + case multipleTFTSurfaceViews + case noTimestamps + case malformedLatency + case adbError +} + +enum GameFrameTelemetryStatus: Sendable, Equatable { + case available + case unavailable(GameFrameTelemetryUnavailable) +} + +enum GameFrameLayerSelection: Sendable, Equatable { + case selected(String) + case unavailable(GameFrameTelemetryUnavailable) +} + +struct GameFrameLatencySample: Sendable, Equatable { + let desiredPresentNS: UInt64 + let actualPresentNS: UInt64 + let frameReadyNS: UInt64 +} + +struct GameFrameLatencyPoll: Sendable, Equatable { + let refreshPeriodNS: UInt64 + let samples: [GameFrameLatencySample] + let historyTruncated: Bool +} + +struct GameFramePresentInterval: Sendable, Equatable { + let actualPresentNS: UInt64 + let intervalNS: UInt64 + let intervalMS: Double + let missedVsyncEquivalents: Int + let isJanky: Bool + let isSevere: Bool +} + +struct GameFrameTelemetryWindow: Sendable, Equatable { + let status: GameFrameTelemetryStatus + let layerName: String? + let refreshPeriodNS: UInt64? + let startedMonotonicNS: UInt64 + let endedMonotonicNS: UInt64 + let frameCount: Int + let effectiveFPS: Double + let p50MS: Double? + let p95MS: Double? + let p99MS: Double? + let maximumMS: Double? + let onePercentLowFPS: Double? + let jankCount: Int + let severeCount: Int + let missedVsyncEquivalents: Int + let historyTruncated: Bool +} + +struct GameFrameTelemetryUpdate: Sendable, Equatable { + let status: GameFrameTelemetryStatus + let layerName: String? + let refreshPeriodNS: UInt64? + let intervals: [GameFramePresentInterval] + let window: GameFrameTelemetryWindow? +} + +enum GameFrameTelemetry { + static let tftGameActivitySurface = "SurfaceView[com.riotgames.league.teamfighttactics/com.epicgames.unreal.GameActivity](BLAST)" + static let maxLatencyHistoryFrames = 128 + + static func selectTFTSurfaceViewLayer(from output: String) -> GameFrameLayerSelection { + let matches = output.split(whereSeparator: \.isNewline).compactMap { rawLine -> String? in + let line = normalizedLayerLine(String(rawLine)) + guard line.contains(tftGameActivitySurface) else { return nil } + return line + } + switch matches.count { + case 0: return .unavailable(.noTFTSurfaceView) + case 1: return .selected(matches[0]) + default: return .unavailable(.multipleTFTSurfaceViews) + } + } + + static func parseSurfaceFlingerLatency(_ output: String) -> GameFrameLatencyPoll? { + let lines = output.split(whereSeparator: \.isNewline) + guard let first = lines.first, + let refresh = UInt64(first.trimmingCharacters(in: .whitespacesAndNewlines)), refresh > 0 else { + return nil + } + let samples = lines.dropFirst().compactMap(parseLatencyLine).sorted { $0.actualPresentNS < $1.actualPresentNS } + return GameFrameLatencyPoll( + refreshPeriodNS: refresh, + samples: samples, + historyTruncated: samples.count >= maxLatencyHistoryFrames + ) + } + + static func surfaceFlingerLatencyShellCommand(layerName: String) -> String { + "dumpsys SurfaceFlinger --latency \(androidShellQuote(layerName))" + } + + private static func normalizedLayerLine(_ line: String) -> String { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + guard trimmed.hasPrefix("RequestedLayerState{") else { return trimmed } + let start = trimmed.index(trimmed.startIndex, offsetBy: "RequestedLayerState{".count) + let remainder = String(trimmed[start...]) + guard let parent = remainder.range(of: " parentId=") else { return remainder } + return String(remainder[.. GameFrameLatencySample? { + let fields = line.split(whereSeparator: \.isWhitespace) + guard fields.count == 3, + let desired = UInt64(fields[0]), let actual = UInt64(fields[1]), let ready = UInt64(fields[2]), + desired > 0, actual > 0, ready > 0, + !isSentinel(desired), !isSentinel(actual), !isSentinel(ready) else { return nil } + return GameFrameLatencySample(desiredPresentNS: desired, actualPresentNS: actual, frameReadyNS: ready) + } + + private static func isSentinel(_ value: UInt64) -> Bool { + value == UInt64.max || value == UInt64(Int64.max) + } + + private static func androidShellQuote(_ value: String) -> String { + "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'" + } +} + +struct GameFrameTelemetrySampler: Sendable { + private(set) var status: GameFrameTelemetryStatus = .unavailable(.noTFTSurfaceView) + private(set) var selectedLayer: String? + private let windowDurationNS: UInt64 + private var previousActualPresentNS: UInt64? + private var recentActualPresentNS = [UInt64]() + private var windowStartedNS: UInt64? + private var windowIntervals = [GameFramePresentInterval]() + private var windowHistoryTruncated = false + private var windowRefreshPeriodNS: UInt64? + + init(windowDurationNS: UInt64 = 1_000_000_000) { + self.windowDurationNS = windowDurationNS + } + + mutating func updateLayerList(_ output: String) -> GameFrameTelemetryStatus { + switch GameFrameTelemetry.selectTFTSurfaceViewLayer(from: output) { + case .selected(let layer): + if selectedLayer != layer { reset(layer: layer) } + status = .available + case .unavailable(let reason): + selectedLayer = nil + status = .unavailable(reason) + resetTimingState() + } + return status + } + + mutating func ingestLatency(_ output: String, observedMonotonicNS: UInt64) -> GameFrameTelemetryUpdate { + guard case .available = status else { + return GameFrameTelemetryUpdate( + status: status, layerName: selectedLayer, refreshPeriodNS: nil, + intervals: [], window: nil + ) + } + guard let poll = GameFrameTelemetry.parseSurfaceFlingerLatency(output) else { + status = .unavailable(.malformedLatency) + resetTimingState() + return GameFrameTelemetryUpdate( + status: status, layerName: selectedLayer, refreshPeriodNS: nil, + intervals: [], window: nil + ) + } + + guard !poll.samples.isEmpty || previousActualPresentNS != nil else { + status = .unavailable(.noTimestamps) + return GameFrameTelemetryUpdate( + status: status, layerName: selectedLayer, refreshPeriodNS: poll.refreshPeriodNS, + intervals: [], window: nil + ) + } + + // The first successful poll establishes a boundary. SurfaceFlinger + // returns a rolling history, so treating that backlog as frames seen + // during this session would inflate the first measurement window. + if previousActualPresentNS == nil, let latest = poll.samples.last { + previousActualPresentNS = latest.actualPresentNS + recentActualPresentNS = poll.samples.map(\.actualPresentNS) + windowStartedNS = observedMonotonicNS + windowRefreshPeriodNS = poll.refreshPeriodNS + return GameFrameTelemetryUpdate( + status: status, layerName: selectedLayer, refreshPeriodNS: poll.refreshPeriodNS, + intervals: [], window: nil + ) + } + + if windowStartedNS == nil { windowStartedNS = observedMonotonicNS } + windowRefreshPeriodNS = poll.refreshPeriodNS + let previousWasRetained = previousActualPresentNS.map { previous in + poll.samples.contains(where: { $0.actualPresentNS == previous }) + } ?? true + let newSamples = poll.samples.filter { !recentActualPresentNS.contains($0.actualPresentNS) } + recentActualPresentNS.append(contentsOf: newSamples.map(\.actualPresentNS)) + if recentActualPresentNS.count > GameFrameTelemetry.maxLatencyHistoryFrames { + recentActualPresentNS.removeFirst(recentActualPresentNS.count - GameFrameTelemetry.maxLatencyHistoryFrames) + } + + var intervals = [GameFramePresentInterval]() + if poll.historyTruncated, !previousWasRetained, !newSamples.isEmpty { + // More frames arrived than the SurfaceFlinger ring retained. Do + // not collapse the missing history into one fabricated interval. + previousActualPresentNS = newSamples.first?.actualPresentNS + windowHistoryTruncated = true + } + for sample in newSamples { + defer { previousActualPresentNS = sample.actualPresentNS } + guard let previous = previousActualPresentNS, sample.actualPresentNS > previous else { continue } + let intervalNS = sample.actualPresentNS - previous + let interval = makeInterval(actualPresentNS: sample.actualPresentNS, intervalNS: intervalNS, refreshPeriodNS: poll.refreshPeriodNS) + intervals.append(interval) + windowIntervals.append(interval) + } + let completedWindow: GameFrameTelemetryWindow? + if let started = windowStartedNS, observedMonotonicNS >= started &+ windowDurationNS { + completedWindow = makeWindow(startedNS: started, endedNS: observedMonotonicNS) + windowStartedNS = observedMonotonicNS + windowIntervals.removeAll(keepingCapacity: true) + windowHistoryTruncated = false + windowRefreshPeriodNS = poll.refreshPeriodNS + } else { + completedWindow = nil + } + return GameFrameTelemetryUpdate( + status: status, layerName: selectedLayer, refreshPeriodNS: poll.refreshPeriodNS, + intervals: intervals, window: completedWindow + ) + } + + private mutating func reset(layer: String?) { + selectedLayer = layer + resetTimingState() + } + + private mutating func resetTimingState() { + previousActualPresentNS = nil + recentActualPresentNS.removeAll(keepingCapacity: true) + windowStartedNS = nil + windowIntervals.removeAll(keepingCapacity: true) + windowHistoryTruncated = false + windowRefreshPeriodNS = nil + } + + private func makeInterval(actualPresentNS: UInt64, intervalNS: UInt64, refreshPeriodNS: UInt64) -> GameFramePresentInterval { + let vsyncs = max(1, Int((Double(intervalNS) / Double(refreshPeriodNS)).rounded())) + return GameFramePresentInterval( + actualPresentNS: actualPresentNS, + intervalNS: intervalNS, + intervalMS: Double(intervalNS) / 1_000_000, + missedVsyncEquivalents: max(0, vsyncs - 1), + isJanky: Double(intervalNS) > Double(refreshPeriodNS) * 1.5, + isSevere: Double(intervalNS) >= Double(refreshPeriodNS) * 3 + ) + } + + private func makeWindow(startedNS: UInt64, endedNS: UInt64) -> GameFrameTelemetryWindow { + let sorted = windowIntervals.map(\.intervalMS).sorted() + let elapsed = max(1, endedNS - startedNS) + let fps = Double(windowIntervals.count) * 1_000_000_000 / Double(elapsed) + let slowFrameCount = max(1, Int(ceil(Double(sorted.count) * 0.01))) + let slowMeanMS = sorted.isEmpty ? nil : sorted.suffix(slowFrameCount).reduce(0, +) / Double(slowFrameCount) + let onePercentLow = slowMeanMS.map { $0 > 0 ? 1000 / $0 : 0 } + return GameFrameTelemetryWindow( + status: status, + layerName: selectedLayer, + refreshPeriodNS: windowRefreshPeriodNS, + startedMonotonicNS: startedNS, + endedMonotonicNS: endedNS, + frameCount: windowIntervals.count, + effectiveFPS: fps, + p50MS: percentile(sorted, 0.50), + p95MS: percentile(sorted, 0.95), + p99MS: percentile(sorted, 0.99), + maximumMS: sorted.last, + onePercentLowFPS: onePercentLow, + jankCount: windowIntervals.filter(\.isJanky).count, + severeCount: windowIntervals.filter(\.isSevere).count, + missedVsyncEquivalents: windowIntervals.reduce(0) { $0 + $1.missedVsyncEquivalents }, + historyTruncated: windowHistoryTruncated + ) + } + + private func percentile(_ sorted: [Double], _ percentile: Double) -> Double? { + guard !sorted.isEmpty else { return nil } + return sorted[percentileIndex(count: sorted.count, percentile: percentile)] + } + + private func percentileIndex(count: Int, percentile: Double) -> Int { + min(count - 1, max(0, Int(ceil(Double(count) * percentile)) - 1)) + } +} diff --git a/tftmac/Runtime/GraphicsStackReceipt.swift b/tftmac/Runtime/GraphicsStackReceipt.swift new file mode 100644 index 0000000..a06284f --- /dev/null +++ b/tftmac/Runtime/GraphicsStackReceipt.swift @@ -0,0 +1,59 @@ +import CryptoKit +import Foundation + +struct GraphicsStackReceiptField: Sendable, Equatable { + let value: String + let source: String + let confidence: String + + var isExplicitlyUnknown: Bool { + confidence.trimmingCharacters(in: .whitespacesAndNewlines) + .uppercased() == "UNKNOWN" + } +} + +enum GraphicsStackReceiptCompleteness: String, Sendable, Equatable { + case complete = "COMPLETE" + case partial = "PARTIAL" + case unknown = "UNKNOWN" +} + +struct GraphicsStackReceipt: Sendable, Equatable { + let fields: [String: GraphicsStackReceiptField] + + var canonicalJSON: String { + let object = Dictionary(uniqueKeysWithValues: fields.map { key, field in + ( + key, + [ + "value": field.value, + "source": field.source, + "confidence": field.confidence + ] + ) + }) + let data = (try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])) ?? Data() + return String(decoding: data, as: UTF8.self) + } + + var sha256: String { + SHA256.hash(data: Data(canonicalJSON.utf8)).map { String(format: "%02x", $0) }.joined() + } + + func explicitUnknownKeys() -> [String] { + fields.compactMap { key, field in field.isExplicitlyUnknown ? key : nil }.sorted() + } + + func unresolvedRequiredKeys(requiredKeys: Set) -> [String] { + requiredKeys.filter { key in + guard let field = fields[key] else { return true } + return field.isExplicitlyUnknown + }.sorted() + } + + func completeness(requiredKeys: Set) -> GraphicsStackReceiptCompleteness { + let unresolved = unresolvedRequiredKeys(requiredKeys: requiredKeys) + if unresolved.isEmpty { return .complete } + return unresolved.count == requiredKeys.count ? .unknown : .partial + } +} diff --git a/tftmac/Runtime/RuntimeLease.swift b/tftmac/Runtime/RuntimeLease.swift new file mode 100644 index 0000000..a64f86c --- /dev/null +++ b/tftmac/Runtime/RuntimeLease.swift @@ -0,0 +1,105 @@ +import Darwin +import Foundation + +enum RuntimeLeaseError: LocalizedError, Sendable { + case alreadyOwned(processIdentifier: Int32) + case cannotCreate(String) + + var errorDescription: String? { + switch self { + case .alreadyOwned(let processIdentifier): + return "TFTMAC runtime is already owned by process \(processIdentifier). Close the other TFTMAC session first." + case .cannotCreate(let reason): + return "TFTMAC could not acquire its runtime lease: \(reason)" + } + } +} + +final class TFTMACRuntimeLease: @unchecked Sendable { + let processIdentifier: Int32 + let token: String + let url: URL + private var released = false + + private init(url: URL, processIdentifier: Int32, token: String) { + self.url = url + self.processIdentifier = processIdentifier + self.token = token + } + + static func acquire(stateRoot: URL, processIdentifier: Int32 = ProcessInfo.processInfo.processIdentifier) throws -> Self { + try FileManager.default.createDirectory( + at: stateRoot, + withIntermediateDirectories: true, + attributes: [.posixPermissions: 0o700] + ) + let url = stateRoot.appendingPathComponent("native-runtime.lease") + let token = UUID().uuidString.lowercased() + for _ in 0..<2 { + let descriptor = Darwin.open(url.path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR) + if descriptor >= 0 { + let payload: [String: Any] = [ + "schema": 1, + "pid": processIdentifier, + "token": token, + "created_utc": ISO8601DateFormatter().string(from: Date()) + ] + do { + let data = try JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) + try writeAll(data, to: descriptor) + _ = Darwin.fsync(descriptor) + Darwin.close(descriptor) + return Self(url: url, processIdentifier: processIdentifier, token: token) + } catch { + Darwin.close(descriptor) + try? FileManager.default.removeItem(at: url) + throw RuntimeLeaseError.cannotCreate(error.localizedDescription) + } + } + guard errno == EEXIST else { + throw RuntimeLeaseError.cannotCreate(String(cString: strerror(errno))) + } + if let owner = leaseOwner(at: url), processExists(owner) { + throw RuntimeLeaseError.alreadyOwned(processIdentifier: owner) + } + do { try FileManager.default.removeItem(at: url) } + catch { throw RuntimeLeaseError.cannotCreate("stale lease could not be removed") } + } + throw RuntimeLeaseError.cannotCreate("another launch won the lease race") + } + + func release() { + guard !released else { return } + released = true + guard let data = try? Data(contentsOf: url), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + object["token"] as? String == token else { return } + try? FileManager.default.removeItem(at: url) + } + + deinit { release() } + + private static func leaseOwner(at url: URL) -> Int32? { + guard let data = try? Data(contentsOf: url), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let number = object["pid"] as? NSNumber else { return nil } + return number.int32Value + } + + private static func processExists(_ processIdentifier: Int32) -> Bool { + if Darwin.kill(processIdentifier, 0) == 0 { return true } + return errno != ESRCH + } + + private static func writeAll(_ data: Data, to descriptor: Int32) throws { + try data.withUnsafeBytes { rawBuffer in + guard let baseAddress = rawBuffer.baseAddress else { return } + var written = 0 + while written < rawBuffer.count { + let result = Darwin.write(descriptor, baseAddress.advanced(by: written), rawBuffer.count - written) + guard result > 0 else { throw RuntimeLeaseError.cannotCreate(String(cString: strerror(errno))) } + written += result + } + } + } +} diff --git a/tftmac/Runtime/RuntimeProfile.swift b/tftmac/Runtime/RuntimeProfile.swift new file mode 100644 index 0000000..86101f5 --- /dev/null +++ b/tftmac/Runtime/RuntimeProfile.swift @@ -0,0 +1,350 @@ +import CryptoKit +import Foundation + +/// A named, reversible experiment selection. The runtime treats `control` as +/// the normal launch contract; a non-control value must be explicitly applied +/// and recorded by the launch transaction. +enum RuntimeExperimentPreset: String, CaseIterable, Codable, Sendable { + case control + case combatLatencyA = "combat_latency_a" + case retiredHomeRunA = "home_run_a" + + private static let preferenceKey = "runtime.experimentPreset" + static let baselineEmulatorFeatures = [ + "GLESDynamicVersion", + "Vulkan", + "GuestAngle", + "-GLPipeChecksum", + "VulkanBatchedDescriptorSetUpdate", + "AsyncComposeSupport", + "VirtioGpuFenceContexts" + ] + static let selectableCases: [Self] = [.control, .combatLatencyA] + + var displayName: String { + switch self { + case .control: "Control (Proven Baseline)" + case .combatLatencyA: "Combat Latency A" + case .retiredHomeRunA: "Retired — Performance Mode Beta" + } + } + + var detail: String { + switch self { + case .control: + "Uses High / 60 FPS / Performance Mode OFF and the proven emulator settings." + case .combatLatencyA: + "Keeps the complete Control graphics stack and requests user-interactive macOS scheduling for the emulator launch." + case .retiredHomeRunA: + "Historical receipt only. Riot Performance Mode Beta is rejected and cannot be selected for a new launch." + } + } + + var requiresManualPerformanceModeBetaConfirmation: Bool { + self == .retiredHomeRunA + } + + var isActiveCandidate: Bool { + self == .combatLatencyA + } + + var requestsHostLatencyQoS: Bool { + self == .combatLatencyA + } + + var emulatorFeatureAdditions: [String] { + switch self { + case .control, .combatLatencyA: [] + case .retiredHomeRunA: ["NativeTextureDecompression", "NoDelayCloseColorBuffer"] + } + } + + func effectiveEmulatorFeatures( + baseline: [String] = RuntimeExperimentPreset.baselineEmulatorFeatures + ) -> [String] { + baseline + emulatorFeatureAdditions.filter { !baseline.contains($0) } + } + + func configurationReceipt( + baselineFeatures: [String] = RuntimeExperimentPreset.baselineEmulatorFeatures + ) -> RuntimeExperimentConfigurationReceipt { + let configuration: [String: Any] = [ + "emulator_features": effectiveEmulatorFeatures(baseline: baselineFeatures), + "host_qos_requested": requestsHostLatencyQoS ? "user_interactive" : "default", + "preset": rawValue, + "requires_manual_performance_mode_beta_confirmation": requiresManualPerformanceModeBetaConfirmation, + "schema": 2 + ] + let data = (try? JSONSerialization.data(withJSONObject: configuration, options: [.sortedKeys])) ?? Data() + return RuntimeExperimentConfigurationReceipt( + canonicalJSON: String(decoding: data, as: UTF8.self), + sha256: SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + ) + } + + static func load(from defaults: UserDefaults = .standard) -> Self { + guard let rawValue = defaults.string(forKey: preferenceKey), + let preset = Self(rawValue: rawValue), + Self.selectableCases.contains(preset) else { + return .control + } + return preset + } + + func save(to defaults: UserDefaults = .standard) { + defaults.set(rawValue, forKey: Self.preferenceKey) + } +} + +struct RuntimeExperimentConfigurationReceipt: Sendable, Equatable { + let canonicalJSON: String + let sha256: String +} + +struct TFTMACRuntimeProfile: Codable, Equatable, Sendable { + static let supportedVCPU = [4, 6, 8] + static let supportedRAMMiB = [4096, 5120, 6144] + static let supportedRefreshHz = [30, 60] + static let supportedASGDrawFlushIntervals = [400, 800] + + static let playable = TFTMACRuntimeProfile( + identifier: "tftmac_5gb_native_v1", + width: 1920, + height: 1080, + densityDPI: 320, + refreshHz: 60, + vCPU: 6, + ramMiB: 5120, + gpuMode: "host", + audioBackend: "coreaudio", + graphicsTransport: "virtio-gpu-asg", + asgWriteBufferSize: 1_048_576, + asgWriteStepSize: 16_384, + asgDataRingSize: 32_768, + asgDrawFlushInterval: 800, + controllerPort: 8554, + angleEnabledFeatures: "exposeNonConformantExtensionsAndVersions:exposeES32ForTesting", + angleDisabledFeatures: "preferSubmitAtFBOBoundary", + experimentPreset: .control + ) + + private enum PreferenceKey { + static let vCPU = "runtime.vcpu" + static let ramMiB = "runtime.ramMiB" + static let refreshHz = "runtime.refreshHz" + static let asgDrawFlushInterval = "runtime.asgDrawFlushInterval" + } + + let identifier: String + let width: Int + let height: Int + let densityDPI: Int + let refreshHz: Int + let vCPU: Int + let ramMiB: Int + let gpuMode: String + let audioBackend: String + let graphicsTransport: String + let asgWriteBufferSize: Int + let asgWriteStepSize: Int + let asgDataRingSize: Int + let asgDrawFlushInterval: Int + let controllerPort: Int + let angleEnabledFeatures: String + let angleDisabledFeatures: String + let experimentPreset: RuntimeExperimentPreset + + var effectiveEmulatorFeatures: [String] { + experimentPreset.effectiveEmulatorFeatures() + } + + var experimentConfigurationReceipt: RuntimeExperimentConfigurationReceipt { + let configuration: [String: Any] = [ + "angle_disabled_features": angleDisabledFeatures, + "angle_enabled_features": angleEnabledFeatures, + "asg_data_ring_size": asgDataRingSize, + "asg_draw_flush_interval_us": asgDrawFlushInterval, + "asg_write_buffer_size": asgWriteBufferSize, + "asg_write_step_size": asgWriteStepSize, + "audio_backend": audioBackend, + "controller_port": controllerPort, + "density_dpi": densityDPI, + "emulator_features": effectiveEmulatorFeatures, + "game_mode_eligible": true, + "gpu_mode": gpuMode, + "graphics_transport": graphicsTransport, + "height": height, + "host_qos_requested": experimentPreset.requestsHostLatencyQoS ? "user_interactive" : "default", + "moltenvk_fast_math": true, + "moltenvk_max_active_command_buffers": 64, + "moltenvk_synchronous_queue_submits": false, + "preset": experimentPreset.rawValue, + "ram_mib": ramMiB, + "refresh_hz": refreshHz, + "requires_manual_performance_mode_beta_confirmation": experimentPreset.requiresManualPerformanceModeBetaConfirmation, + "schema": 2, + "tft_frame_rate_cap": 60, + "tft_graphics_quality": "high", + "tft_performance_mode_beta_expected": false, + "vcpu": vCPU, + "width": width + ] + let data = (try? JSONSerialization.data(withJSONObject: configuration, options: [.sortedKeys])) ?? Data() + return RuntimeExperimentConfigurationReceipt( + canonicalJSON: String(decoding: data, as: UTF8.self), + sha256: SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + ) + } + + /// Hash of every locked comparison value with the experiment intervention + /// normalized to Control. Candidate runs can only pair with a Control run + /// carrying this same identity. + var comparisonConfigurationSHA256: String { + with(experimentPreset: .control).experimentConfigurationReceipt.sha256 + } + + static func load(from defaults: UserDefaults = .standard) -> Self { + Self.playable.with(experimentPreset: RuntimeExperimentPreset.load(from: defaults)) + } + + func save(to defaults: UserDefaults = .standard) { + defaults.set(vCPU, forKey: PreferenceKey.vCPU) + defaults.set(ramMiB, forKey: PreferenceKey.ramMiB) + defaults.set(refreshHz, forKey: PreferenceKey.refreshHz) + defaults.set(asgDrawFlushInterval, forKey: PreferenceKey.asgDrawFlushInterval) + experimentPreset.save(to: defaults) + } + + func with(vCPU: Int, ramMiB: Int, refreshHz: Int, asgDrawFlushInterval: Int) -> Self { + let safeVCPU = Self.supportedValue(vCPU, in: Self.supportedVCPU) ?? self.vCPU + let safeRAM = Self.supportedValue(ramMiB, in: Self.supportedRAMMiB) ?? self.ramMiB + let safeRefresh = Self.supportedValue(refreshHz, in: Self.supportedRefreshHz) ?? self.refreshHz + let safeFlush = Self.supportedValue( + asgDrawFlushInterval, + in: Self.supportedASGDrawFlushIntervals + ) ?? self.asgDrawFlushInterval + let identifier = "tftmac_native_\(safeRAM)m_\(safeVCPU)c_\(safeRefresh)hz_flush\(safeFlush)" + return Self( + identifier: identifier, + width: width, + height: height, + densityDPI: densityDPI, + refreshHz: safeRefresh, + vCPU: safeVCPU, + ramMiB: safeRAM, + gpuMode: gpuMode, + audioBackend: audioBackend, + graphicsTransport: graphicsTransport, + asgWriteBufferSize: asgWriteBufferSize, + asgWriteStepSize: asgWriteStepSize, + asgDataRingSize: asgDataRingSize, + asgDrawFlushInterval: safeFlush, + controllerPort: controllerPort, + angleEnabledFeatures: angleEnabledFeatures, + angleDisabledFeatures: angleDisabledFeatures, + experimentPreset: experimentPreset + ) + } + + func with(experimentPreset: RuntimeExperimentPreset) -> Self { + let baseIdentifier = identifier.components(separatedBy: "_preset_").first ?? identifier + let identifier = experimentPreset == .control + ? baseIdentifier + : "\(baseIdentifier)_preset_\(experimentPreset.rawValue)" + return Self( + identifier: identifier, + width: width, + height: height, + densityDPI: densityDPI, + refreshHz: refreshHz, + vCPU: vCPU, + ramMiB: ramMiB, + gpuMode: gpuMode, + audioBackend: audioBackend, + graphicsTransport: graphicsTransport, + asgWriteBufferSize: asgWriteBufferSize, + asgWriteStepSize: asgWriteStepSize, + asgDataRingSize: asgDataRingSize, + asgDrawFlushInterval: asgDrawFlushInterval, + controllerPort: controllerPort, + angleEnabledFeatures: angleEnabledFeatures, + angleDisabledFeatures: angleDisabledFeatures, + experimentPreset: experimentPreset + ) + } + + private static func supportedValue(_ candidate: Int, in allowed: [Int]) -> Int? { + allowed.contains(candidate) ? candidate : nil + } +} + +struct GuestPowerState: Equatable, Sendable { + let isPowered: Bool + let stayOn: Bool + let wakefulness: String + + var isGameplayReady: Bool { + isPowered && stayOn && wakefulness.caseInsensitiveCompare("Awake") == .orderedSame + } + + static func parse(_ dumpsysPower: String) -> Self? { + guard let powered = boolean(named: "mIsPowered", in: dumpsysPower), + let stayOn = boolean(named: "mStayOn", in: dumpsysPower), + let wakefulness = value(named: "mWakefulness", in: dumpsysPower) else { + return nil + } + return Self(isPowered: powered, stayOn: stayOn, wakefulness: wakefulness) + } + + private static func boolean(named key: String, in text: String) -> Bool? { + guard let raw = value(named: key, in: text) else { return nil } + switch raw.lowercased() { + case "true": return true + case "false": return false + default: return nil + } + } + + private static func value(named key: String, in text: String) -> String? { + text.split(whereSeparator: \.isNewline) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .first { $0.hasPrefix("\(key)=") }? + .dropFirst(key.count + 1) + .split(whereSeparator: \.isWhitespace) + .first + .map(String.init) + } +} + +struct HostSchedulingReceipt: Equatable, Sendable { + let requested: String + let setResult: Int + let effective: String + let relativePriority: Int + + var userInteractiveVerified: Bool { + requested == "user_interactive" && setResult == 0 && effective == "user_interactive" + } + + static func parse(_ hostOutput: String) -> Self? { + let fields = Dictionary(uniqueKeysWithValues: hostOutput.split(whereSeparator: \.isNewline).compactMap { line -> (String, String)? in + let pair = line.split(separator: "=", maxSplits: 1).map(String.init) + guard pair.count == 2, pair[0].hasPrefix("TFTMAC_HOST_QOS_") else { return nil } + return (pair[0], pair[1]) + }) + guard let requested = fields["TFTMAC_HOST_QOS_REQUESTED"], + let setResultText = fields["TFTMAC_HOST_QOS_SET_RESULT"], + let setResult = Int(setResultText), + let effective = fields["TFTMAC_HOST_QOS_EFFECTIVE"], + let priorityText = fields["TFTMAC_HOST_QOS_RELATIVE_PRIORITY"], + let relativePriority = Int(priorityText) else { + return nil + } + return Self( + requested: requested, + setResult: setResult, + effective: effective, + relativePriority: relativePriority + ) + } +} diff --git a/tftmac/Runtime/TFTMACRuntime.swift b/tftmac/Runtime/TFTMACRuntime.swift new file mode 100644 index 0000000..4ef9581 --- /dev/null +++ b/tftmac/Runtime/TFTMACRuntime.swift @@ -0,0 +1,4916 @@ +import AppKit +import CryptoKit +import Darwin +import Foundation +import GRPCCore +import GRPCNIOTransportHTTP2 +import GRPCProtobuf +import Metal +import SQLite3 +import SwiftProtobuf + +struct TFTMACRuntimePaths: Sendable { + let sdkRoot: URL + let emulator: URL + let adb: URL + let avdHome: URL + let avdDirectory: URL + let avdConfig: URL + let hostApplication: URL + let applicationSupport: URL + + static func discover() throws -> Self { + let manager = FileManager.default + let runtimeRoot = URL(fileURLWithPath: "/Volumes/MAC MINI M4/TFTMAC/Runtime", isDirectory: true) + let sdkCandidates = ["SDK", "sdk"].map { runtimeRoot.appendingPathComponent($0, isDirectory: true) } + guard let sdkRoot = sdkCandidates.first(where: { + manager.isExecutableFile(atPath: $0.appendingPathComponent("emulator/emulator").path) + && manager.isExecutableFile(atPath: $0.appendingPathComponent("platform-tools/adb").path) + }) else { + throw TFTMACRuntimeError("The proven Android runtime is not mounted at /Volumes/MAC MINI M4/TFTMAC/Runtime.") + } + + let avdCandidates = ["AVD", "avd"].map { runtimeRoot.appendingPathComponent($0, isDirectory: true) } + guard let avdHome = avdCandidates.first(where: { + manager.fileExists(atPath: $0.appendingPathComponent("TFT_Ultra_Tablet.ini").path) + }) else { + throw TFTMACRuntimeError("The TFT_Ultra_Tablet AVD is missing from the proven runtime.") + } + let avdINI = avdHome.appendingPathComponent("TFT_Ultra_Tablet.ini") + let iniText = try String(contentsOf: avdINI, encoding: .utf8) + guard let avdPath = iniText.split(whereSeparator: \.isNewline) + .first(where: { $0.hasPrefix("path=") })? + .dropFirst("path=".count), !avdPath.isEmpty else { + throw TFTMACRuntimeError("TFT_Ultra_Tablet.ini does not identify its AVD directory.") + } + let avdDirectory = URL(fileURLWithPath: String(avdPath), isDirectory: true) + let avdConfig = avdDirectory.appendingPathComponent("config.ini") + guard manager.fileExists(atPath: avdConfig.path) else { + throw TFTMACRuntimeError("The TFT_Ultra_Tablet config.ini is missing.") + } + + guard let resourceURL = Bundle.main.resourceURL else { + throw TFTMACRuntimeError("TFTMAC.app has no Resources directory.") + } + let hostApplication = resourceURL.appendingPathComponent("TFTMAC Emulator Host.app", isDirectory: true) + guard manager.fileExists(atPath: hostApplication.path) else { + throw TFTMACRuntimeError("TFTMAC Emulator Host.app is missing from the application bundle.") + } + let applicationSupport = manager.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support/TFTMAC", isDirectory: true) + return Self( + sdkRoot: sdkRoot, + emulator: sdkRoot.appendingPathComponent("emulator/emulator"), + adb: sdkRoot.appendingPathComponent("platform-tools/adb"), + avdHome: avdHome, + avdDirectory: avdDirectory, + avdConfig: avdConfig, + hostApplication: hostApplication, + applicationSupport: applicationSupport + ) + } +} + +struct EmulatorControllerDiscovery: Sendable { + let processIdentifier: Int32 + let port: Int + let token: String + let recordPath: String +} + +struct MouseInput: Sendable { + let x: Int32 + let y: Int32 + let buttons: Int32 +} + +struct KeyboardInput: Sendable { + let text: String? + let key: String? +} + +private enum EmulatorInput: Sendable { + case touch(TouchInput) + case mouse(MouseInput) + case keyboard(KeyboardInput) +} + +struct PresentationSample: Sendable { + let presentedFrames: UInt64 + let presentationFPS: Double + let sourceFPS: Double + let mailbox: FrameMailboxSnapshot + let lastPresentedSequence: UInt32? + let sampledMonotonicNanoseconds: UInt64 +} + +private struct FrameVisualSample: Sendable { + let sampleCount: Int + let meanLuma: Double + let nonBlackFraction: Double + let minimumRGB: Int + let maximumRGB: Int + let minimumAlpha: Int + let maximumAlpha: Int + let contentSHA256: String? +} + +private struct FrameIntervalWindow: Sendable { + let startedMonotonicNS: UInt64 + let endedMonotonicNS: UInt64 + let frameCount: Int + let sequenceDropCount: UInt64 + let meanIntervalMS: Double? + let p95IntervalMS: Double? + let maximumIntervalMS: Double? +} + +private struct GuestMemorySample: Sendable { + let totalKiB: Int64 + let availableKiB: Int64 + let swapTotalKiB: Int64? + let swapFreeKiB: Int64? +} + +private struct HostResourceSample: Sendable { + let availableKiB: Int64? + let compressedKiB: Int64? + let swapUsedKiB: Int64? + let pageouts: Int64? + let thermalState: String + let powerSource: String +} + +private struct SurfaceFlingerSample: Sendable { + let renderRateHz: Double? + let totalMissedFrames: Int64? + let hwcMissedFrames: Int64? + let gpuMissedFrames: Int64? + let tftRequestedRateHz: Double? +} + +private struct AudioFlingerSample: Sendable { + let activeOutput: Bool + let sampleRateHz: Int? + let stereoOutput: Bool + let activeTracks: Int? + let partialUnderruns: Int64? + let emptyUnderruns: Int64? +} + +private struct LogcatAggregate: Sendable { + let byteStart: UInt64 + let byteEnd: UInt64 + let skippedBytes: UInt64 + let lineCount: Int + let anrCount: Int + let inputTimeoutCount: Int + let fatalCount: Int + let memoryKillCount: Int + let choreographerSkipCount: Int + let angleWarningCount: Int + let vulkanWarningCount: Int + let audioErrorCount: Int +} + +private struct PipelineLogAggregate: Sendable { + let sourceStream: String + let byteStart: UInt64 + let byteEnd: UInt64 + let skippedBytes: UInt64 + let lineCount: Int + let signals: PipelineLogSignals +} + +private struct GraphicsPipelineSnapshot: Sendable { + static let requiredReceiptKeys: Set = [ + "tft_package_version", "tft_surface", "unreal_engine", "game_graphics_api", "angle", + "gfxstream", "moltenvk", "host_vulkan_device", "metal_device", + "native_presenter", "configuration_sha256" + ] + + let label: String + let gamePID: Int32? + let exactLayerName: String? + let tftSurfaceState: String + let gameGraphicsAPI: String + let gameGraphicsAPIConfidence: String + let angleState: String + let gfxstreamState: String + let moltenVKState: String + let emulatorVersion: String? + let emulatorBuildID: String? + let emulatorGPUSelection: String? + let gfxstreamFeatureReceipt: String? + let gfxstreamTracingState: String + let moltenVKVersion: String? + let moltenVKConfiguration: String + let hostVulkanDevice: String? + let vulkanComposition: Bool? + let nativeSwapchain: Bool? + let guestEGLImplementation: String? + let guestVulkanImplementation: String? + let globalAngleSelection: String? + let packageAngleSelection: String? + let metalDeviceName: String? + let metalRegistryID: String? + let receipt: GraphicsStackReceipt +} + +struct StreamFreshnessWindow: Sendable { + let startedMonotonicNS: UInt64 + let endedMonotonicNS: UInt64 + let receivedFrames: Int + let contentChanges: Int + let identicalFrames: Int + let longestIdenticalRunFrames: Int + let longestIdenticalRunMS: Double + let sequenceDrops: UInt64 + let sampledPixelsPerFrame: Int +} + +struct HostPresentationWindow: Sendable { + let startedMonotonicNS: UInt64 + let endedMonotonicNS: UInt64 + let submittedFrames: Int + let completedFrames: Int + let uniqueSourceUploads: Int + let repeatedSourcePresents: Int + let drawableMisses: Int + let commandErrors: Int + let meanCompletionLatencyMS: Double? + let p95CompletionLatencyMS: Double? + let p99CompletionLatencyMS: Double? + let maximumCompletionLatencyMS: Double? + let meanGPUTimeMS: Double? + let p95GPUTimeMS: Double? + let maximumGPUTimeMS: Double? +} + +private struct DiagnosticArtifact: Sendable { + let graphicsRunID: String? + let graphicsStackSHA256: String? + let captureScope: String + let createdUTC: String + let createdMonotonicNS: UInt64 + let kind: String + let trigger: String + let relativePath: String + let byteCount: Int64 + let sha256: String + let analysisState: String + let normalizedRelativePath: String + let normalizedSHA256: String + let normalizedSummaryCSV: String + let traceProcessorSHA256: String +} + +private struct GraphicsPipelineIncident: Sendable { + let incidentID: String + let trigger: String + let observedMonotonicNS: UInt64 + let window: GameFrameTelemetryWindow + let traceSequence: Int? + let firstObservedDivergentBoundary: String + let causalOwner: String + let causalConfidence: String + let explicitUnknowns: [String] +} + +private enum DiagnosticTraceScope: String, Sendable { + case combatBenchmark = "COMBAT_BENCHMARK" + case automaticGraphics = "AUTOMATIC_GRAPHICS" +} + +struct TFTMACRuntimeError: LocalizedError, Sendable { + let message: String + init(_ message: String) { self.message = message } + var errorDescription: String? { message } +} + +private enum SQLiteValue: Sendable { + case integer(Int64) + case real(Double) + case text(String) + case null +} + +final class TFTMACNativeTelemetry: @unchecked Sendable { + let sessionIdentifier: String + let captureDirectory: URL + + private let queue = DispatchQueue(label: "com.flashls1.tftmac.telemetry") + private let configurationSHA256: String + private let targetFPS: Int + private var database: OpaquePointer? + private var eventLog: FileHandle? + private var activeGraphicsRunID: String? + private var activeGraphicsRunPID: Int32? + private var activeGraphicsStackSHA256: String? + private let transientDestructor = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + + init(profile: TFTMACRuntimeProfile, applicationSupport: URL) throws { + configurationSHA256 = profile.experimentConfigurationReceipt.sha256 + targetFPS = profile.refreshHz + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + sessionIdentifier = formatter.string(from: Date()) + .replacingOccurrences(of: ":", with: "-") + "-" + UUID().uuidString.lowercased() + captureDirectory = applicationSupport.appendingPathComponent("Captures/\(sessionIdentifier)", isDirectory: true) + try FileManager.default.createDirectory( + at: captureDirectory, + withIntermediateDirectories: true, + attributes: [.posixPermissions: 0o700] + ) + + let logURL = captureDirectory.appendingPathComponent("native-events.jsonl") + FileManager.default.createFile(atPath: logURL.path, contents: nil, attributes: [.posixPermissions: 0o600]) + eventLog = try FileHandle(forWritingTo: logURL) + + // Each capture owns a new UUID-scoped database. Historical per-session + // databases are immutable evidence and are never reopened or migrated. + let databaseURL = captureDirectory.appendingPathComponent("TFTMAC_NATIVE_RUNTIME.sqlite") + guard !FileManager.default.fileExists(atPath: databaseURL.path) else { + throw TFTMACRuntimeError("A new TFTMAC capture unexpectedly collided with an existing SQL database.") + } + guard sqlite3_open_v2( + databaseURL.path, + &database, + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, + nil + ) == SQLITE_OK else { + throw TFTMACRuntimeError("The native SQL telemetry database could not be opened.") + } + try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: databaseURL.path) + try executeSchema() + try execute( + "INSERT INTO sessions(session_id, started_utc, status, profile_id) VALUES(?, ?, 'STARTING', ?)", + [.text(sessionIdentifier), .text(Self.utcNow()), .text(profile.identifier)] + ) + recordEvent("LOGGER_INITIALIZED", payload: [ + "database": databaseURL.lastPathComponent, + "rawEventLog": logURL.lastPathComponent, + "loggerStartsBeforeEmulator": true, + "graphics_logger_mode": "AUTOMATIC_TFT_PROCESS_LIFETIME", + "graphics_schema_version": 3 + ]) + } + + deinit { + queue.sync { + try? eventLog?.close() + if let database { sqlite3_close(database) } + database = nil + } + } + + func recordReceipt(key: String, value: String, source: String, confidence: String) { + enqueue { + try self.execute( + "INSERT INTO runtime_receipts(session_id, receipt_key, receipt_value, source, confidence, observed_utc) VALUES(?, ?, ?, ?, ?, ?)", + [.text(self.sessionIdentifier), .text(key), .text(value), .text(source), .text(confidence), .text(Self.utcNow())] + ) + } + } + + func markRunning() { + enqueue { + try self.execute( + "UPDATE sessions SET status = 'RUNNING' WHERE session_id = ? AND ended_utc IS NULL", + [.text(self.sessionIdentifier)] + ) + } + } + + /// Opens the single graphics lifecycle owned by the observed TFT process/layer. + /// The serial SQL queue is the semantic owner, so all previously submitted rows + /// remain outside the new run and every later row resolves to it automatically. + func beginOrUpdateGraphicsRun(gamePID: Int32?, exactLayerName: String?, reason: String) { + queue.sync { + let nowUTC = Self.utcNow() + let nowNS = DispatchTime.now().uptimeNanoseconds + if let activeGraphicsRunID { + if let activeGraphicsRunPID, let gamePID, activeGraphicsRunPID != gamePID { + try? self.closeGraphicsRun( + activeGraphicsRunID, + endedUTC: nowUTC, + endedMonotonicNS: nowNS, + reason: "TFT_PROCESS_REPLACED" + ) + self.activeGraphicsRunID = nil + self.activeGraphicsRunPID = nil + self.activeGraphicsStackSHA256 = nil + } else { + try? self.execute( + "UPDATE graphics_runs SET game_pid = COALESCE(?, game_pid), exact_layer_name = COALESCE(?, exact_layer_name), last_observed_utc = ?, last_observed_monotonic_ns = ? WHERE graphics_run_id = ? AND ended_utc IS NULL", + [ + gamePID.map { .integer(Int64($0)) } ?? .null, + exactLayerName.map(SQLiteValue.text) ?? .null, + .text(nowUTC), .integer(Int64(bitPattern: nowNS)), + .text(activeGraphicsRunID) + ] + ) + if let gamePID { self.activeGraphicsRunPID = gamePID } + return + } + } + + let runID = UUID().uuidString.lowercased() + do { + try self.execute( + "INSERT INTO graphics_runs(graphics_run_id, session_id, game_pid, started_utc, started_monotonic_ns, last_observed_utc, last_observed_monotonic_ns, start_reason, configuration_sha256, target_fps, exact_layer_name) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(runID), .text(self.sessionIdentifier), + gamePID.map { .integer(Int64($0)) } ?? .null, + .text(nowUTC), .integer(Int64(bitPattern: nowNS)), + .text(nowUTC), .integer(Int64(bitPattern: nowNS)), + .text(reason), .text(self.configurationSHA256), + .integer(Int64(self.targetFPS)), + exactLayerName.map(SQLiteValue.text) ?? .null + ] + ) + self.activeGraphicsRunID = runID + self.activeGraphicsRunPID = gamePID + self.activeGraphicsStackSHA256 = nil + } catch { + fputs("TFTMAC graphics-run error: \(error.localizedDescription)\n", stderr) + } + } + } + + func updateGraphicsRunLayer(_ exactLayerName: String?) { + queue.sync { + guard let activeGraphicsRunID else { return } + try? self.execute( + "UPDATE graphics_runs SET exact_layer_name = ?, last_observed_utc = ?, last_observed_monotonic_ns = ? WHERE graphics_run_id = ? AND ended_utc IS NULL", + [ + exactLayerName.map(SQLiteValue.text) ?? .null, + .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .text(activeGraphicsRunID) + ] + ) + } + } + + func endGraphicsRun(reason: String) { + queue.sync { + guard let activeGraphicsRunID else { return } + try? self.closeGraphicsRun( + activeGraphicsRunID, + endedUTC: Self.utcNow(), + endedMonotonicNS: DispatchTime.now().uptimeNanoseconds, + reason: reason + ) + self.activeGraphicsRunID = nil + self.activeGraphicsRunPID = nil + self.activeGraphicsStackSHA256 = nil + } + } + + fileprivate func currentGraphicsContext() -> (runID: String?, stackSHA256: String?) { + queue.sync { (activeGraphicsRunID, activeGraphicsStackSHA256) } + } + + func recordEvent(_ kind: String, payload: [String: Any] = [:]) { + let payloadText = Self.json(payload) + let utc = Self.utcNow() + let monotonic = DispatchTime.now().uptimeNanoseconds + let lineData = Data((Self.json([ + "session_id": sessionIdentifier, + "observed_utc": utc, + "monotonic_ns": String(monotonic), + "kind": kind, + "payload": payload + ]) + "\n").utf8) + enqueue { + try self.execute( + "INSERT INTO events(session_id, observed_utc, monotonic_ns, kind, payload_json) VALUES(?, ?, ?, ?, ?)", + [.text(self.sessionIdentifier), .text(utc), .integer(Int64(bitPattern: monotonic)), .text(kind), .text(payloadText)] + ) + try self.eventLog?.write(contentsOf: lineData) + } + } + + fileprivate func recordFrameReceived( + _ frame: EmulatorFrame, + transport: String, + sequenceDropCount: UInt64, + visual: FrameVisualSample + ) { + enqueue { + try self.execute( + "INSERT INTO frame_samples(session_id, graphics_run_id, sequence, emulator_timestamp_us, received_monotonic_ns, width, height, byte_count, transport, sequence_drop_count, visual_sample_count, mean_luma, nonblack_fraction, minimum_rgb, maximum_rgb, minimum_alpha, maximum_alpha, content_sha256) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), + .integer(Int64(frame.sequence)), + .integer(Int64(bitPattern: frame.emulatorTimestampMicroseconds)), + .integer(Int64(bitPattern: frame.receivedMonotonicNanoseconds)), + .integer(Int64(frame.width)), .integer(Int64(frame.height)), + .integer(Int64(frame.pixels.count)), .text(transport), + .integer(Int64(bitPattern: sequenceDropCount)), + .integer(Int64(visual.sampleCount)), .real(visual.meanLuma), + .real(visual.nonBlackFraction), .integer(Int64(visual.minimumRGB)), + .integer(Int64(visual.maximumRGB)), .integer(Int64(visual.minimumAlpha)), + .integer(Int64(visual.maximumAlpha)), visual.contentSHA256.map(SQLiteValue.text) ?? .null + ] + ) + } + } + + fileprivate func recordFrameIntervalWindow(_ sample: FrameIntervalWindow) { + enqueue { + try self.execute( + "INSERT INTO frame_interval_windows(session_id, graphics_run_id, started_monotonic_ns, ended_monotonic_ns, frame_count, sequence_drop_count, mean_interval_ms, p95_interval_ms, maximum_interval_ms) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), + .integer(Int64(bitPattern: sample.startedMonotonicNS)), + .integer(Int64(bitPattern: sample.endedMonotonicNS)), + .integer(Int64(sample.frameCount)), + .integer(Int64(bitPattern: sample.sequenceDropCount)), + sample.meanIntervalMS.map(SQLiteValue.real) ?? .null, + sample.p95IntervalMS.map(SQLiteValue.real) ?? .null, + sample.maximumIntervalMS.map(SQLiteValue.real) ?? .null + ] + ) + } + } + + func recordPresentation(_ sample: PresentationSample) { + enqueue { + try self.execute( + "INSERT INTO presentation_samples(session_id, graphics_run_id, sampled_monotonic_ns, presented_frames, presentation_fps, source_fps, received_frames, mailbox_replacements, sequence_drops, last_sequence) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), + .integer(Int64(bitPattern: sample.sampledMonotonicNanoseconds)), + .integer(Int64(bitPattern: sample.presentedFrames)), .real(sample.presentationFPS), .real(sample.sourceFPS), + .integer(Int64(bitPattern: sample.mailbox.receivedFrames)), + .integer(Int64(bitPattern: sample.mailbox.replacedBeforePresentation)), + .integer(Int64(bitPattern: sample.mailbox.sequenceDrops)), + sample.lastPresentedSequence.map { .integer(Int64($0)) } ?? .null + ] + ) + } + } + + func recordResourceSample(emulatorPID: Int32, emulatorCPU: Double?, emulatorRSSKiB: Int64?, gamePID: Int32?, topActivity: String) { + enqueue { + try self.execute( + "INSERT INTO resource_samples(session_id, observed_utc, monotonic_ns, emulator_pid, emulator_cpu_percent, emulator_rss_kib, game_pid, top_activity) VALUES(?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .integer(Int64(emulatorPID)), emulatorCPU.map(SQLiteValue.real) ?? .null, + emulatorRSSKiB.map(SQLiteValue.integer) ?? .null, + gamePID.map { .integer(Int64($0)) } ?? .null, .text(topActivity) + ] + ) + } + } + + fileprivate func recordGuestMemory(_ sample: GuestMemorySample) { + enqueue { + try self.execute( + "INSERT INTO guest_memory_samples(session_id, observed_utc, monotonic_ns, total_kib, available_kib, swap_total_kib, swap_free_kib) VALUES(?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .integer(sample.totalKiB), .integer(sample.availableKiB), + sample.swapTotalKiB.map(SQLiteValue.integer) ?? .null, + sample.swapFreeKiB.map(SQLiteValue.integer) ?? .null + ] + ) + } + } + + fileprivate func recordHostResource(_ sample: HostResourceSample) { + enqueue { + try self.execute( + "INSERT INTO host_resource_samples(session_id, observed_utc, monotonic_ns, available_kib, compressed_kib, swap_used_kib, pageouts, thermal_state, power_source) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + sample.availableKiB.map(SQLiteValue.integer) ?? .null, + sample.compressedKiB.map(SQLiteValue.integer) ?? .null, + sample.swapUsedKiB.map(SQLiteValue.integer) ?? .null, + sample.pageouts.map(SQLiteValue.integer) ?? .null, + .text(sample.thermalState), .text(sample.powerSource) + ] + ) + } + } + + fileprivate func recordClockSync(hostT0NS: UInt64, guestUptimeNS: UInt64, hostT1NS: UInt64) { + let midpoint = hostT0NS &+ ((hostT1NS &- hostT0NS) / 2) + enqueue { + try self.execute( + "INSERT INTO clock_sync_samples(session_id, observed_utc, host_t0_ns, guest_uptime_ns, host_t1_ns, host_midpoint_ns, round_trip_ns, host_minus_guest_ns) VALUES(?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), .text(Self.utcNow()), + .integer(Int64(bitPattern: hostT0NS)), .integer(Int64(bitPattern: guestUptimeNS)), + .integer(Int64(bitPattern: hostT1NS)), .integer(Int64(bitPattern: midpoint)), + .integer(Int64(bitPattern: hostT1NS &- hostT0NS)), + .integer(Int64(bitPattern: midpoint &- guestUptimeNS)) + ] + ) + } + } + + fileprivate func recordSurfaceFlinger(_ sample: SurfaceFlingerSample, label: String) { + enqueue { + try self.execute( + "INSERT INTO surfaceflinger_samples(session_id, graphics_run_id, observed_utc, monotonic_ns, sample_label, render_rate_hz, total_missed_frames, hwc_missed_frames, gpu_missed_frames, tft_requested_rate_hz) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), .text(label), + sample.renderRateHz.map(SQLiteValue.real) ?? .null, + sample.totalMissedFrames.map(SQLiteValue.integer) ?? .null, + sample.hwcMissedFrames.map(SQLiteValue.integer) ?? .null, + sample.gpuMissedFrames.map(SQLiteValue.integer) ?? .null, + sample.tftRequestedRateHz.map(SQLiteValue.real) ?? .null + ] + ) + } + } + + fileprivate func recordAudioFlinger(_ sample: AudioFlingerSample, label: String) { + enqueue { + try self.execute( + "INSERT INTO audio_samples(session_id, observed_utc, monotonic_ns, sample_label, backend, active_output, sample_rate_hz, stereo_output, active_tracks, partial_underruns, empty_underruns) VALUES(?, ?, ?, ?, 'coreaudio', ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), .text(label), + .integer(sample.activeOutput ? 1 : 0), + sample.sampleRateHz.map { .integer(Int64($0)) } ?? .null, + .integer(sample.stereoOutput ? 1 : 0), + sample.activeTracks.map { .integer(Int64($0)) } ?? .null, + sample.partialUnderruns.map(SQLiteValue.integer) ?? .null, + sample.emptyUnderruns.map(SQLiteValue.integer) ?? .null + ] + ) + } + } + + fileprivate func recordLogcatAggregate(_ sample: LogcatAggregate) { + enqueue { + try self.execute( + "INSERT INTO logcat_aggregates(session_id, observed_utc, monotonic_ns, byte_start, byte_end, skipped_bytes, line_count, anr_count, input_timeout_count, fatal_count, memory_kill_count, choreographer_skip_count, angle_warning_count, vulkan_warning_count, audio_error_count) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .integer(Int64(bitPattern: sample.byteStart)), .integer(Int64(bitPattern: sample.byteEnd)), + .integer(Int64(bitPattern: sample.skippedBytes)), .integer(Int64(sample.lineCount)), + .integer(Int64(sample.anrCount)), .integer(Int64(sample.inputTimeoutCount)), + .integer(Int64(sample.fatalCount)), .integer(Int64(sample.memoryKillCount)), + .integer(Int64(sample.choreographerSkipCount)), .integer(Int64(sample.angleWarningCount)), + .integer(Int64(sample.vulkanWarningCount)), .integer(Int64(sample.audioErrorCount)) + ] + ) + } + } + + fileprivate func recordPipelineLogAggregate(_ sample: PipelineLogAggregate) { + enqueue { + try self.execute( + "INSERT INTO pipeline_log_aggregates(session_id, graphics_run_id, observed_utc, monotonic_ns, source_stream, byte_start, byte_end, skipped_bytes, line_count, gfxstream_warning_count, asg_stall_count, vulkan_error_count, moltenvk_warning_count, shader_error_count, fence_timeout_count) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .text(sample.sourceStream), + .integer(Int64(bitPattern: sample.byteStart)), + .integer(Int64(bitPattern: sample.byteEnd)), + .integer(Int64(bitPattern: sample.skippedBytes)), + .integer(Int64(sample.lineCount)), + .integer(Int64(sample.signals.gfxstreamWarningCount)), + .integer(Int64(sample.signals.asgStallCount)), + .integer(Int64(sample.signals.vulkanErrorCount)), + .integer(Int64(sample.signals.moltenVKWarningCount)), + .integer(Int64(sample.signals.shaderErrorCount)), + .integer(Int64(sample.signals.fenceTimeoutCount)) + ] + ) + } + } + + fileprivate func recordGraphicsPipelineSnapshot(_ sample: GraphicsPipelineSnapshot) { + enqueue { + let unknowns = sample.receipt.explicitUnknownKeys() + let completeness = sample.receipt.completeness( + requiredKeys: GraphicsPipelineSnapshot.requiredReceiptKeys + ).rawValue + try self.execute( + """ + INSERT INTO graphics_pipeline_snapshots( + session_id, graphics_run_id, observed_utc, monotonic_ns, sample_label, + stack_sha256, stack_receipt_json, receipt_completeness, explicit_unknowns_json, + game_pid, exact_layer_name, tft_surface_state, game_graphics_api, + game_graphics_api_confidence, angle_state, gfxstream_state, moltenvk_state, + emulator_version, emulator_build_id, emulator_gpu_selection, + gfxstream_feature_receipt, gfxstream_tracing_state, moltenvk_version, + moltenvk_configuration_json, host_vulkan_device, vulkan_composition, + native_swapchain, guest_egl_implementation, guest_vulkan_implementation, + global_angle_selection, package_angle_selection, metal_device_name, + metal_registry_id + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), .text(Self.utcNow()), + .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .text(sample.label), .text(sample.receipt.sha256), + .text(sample.receipt.canonicalJSON), .text(completeness), + .text(Self.jsonArray(unknowns)), + sample.gamePID.map { .integer(Int64($0)) } ?? .null, + sample.exactLayerName.map(SQLiteValue.text) ?? .null, + .text(sample.tftSurfaceState), .text(sample.gameGraphicsAPI), + .text(sample.gameGraphicsAPIConfidence), .text(sample.angleState), + .text(sample.gfxstreamState), .text(sample.moltenVKState), + sample.emulatorVersion.map(SQLiteValue.text) ?? .null, + sample.emulatorBuildID.map(SQLiteValue.text) ?? .null, + sample.emulatorGPUSelection.map(SQLiteValue.text) ?? .null, + sample.gfxstreamFeatureReceipt.map(SQLiteValue.text) ?? .null, + .text(sample.gfxstreamTracingState), + sample.moltenVKVersion.map(SQLiteValue.text) ?? .null, + .text(sample.moltenVKConfiguration), + sample.hostVulkanDevice.map(SQLiteValue.text) ?? .null, + sample.vulkanComposition.map { .integer($0 ? 1 : 0) } ?? .null, + sample.nativeSwapchain.map { .integer($0 ? 1 : 0) } ?? .null, + sample.guestEGLImplementation.map(SQLiteValue.text) ?? .null, + sample.guestVulkanImplementation.map(SQLiteValue.text) ?? .null, + sample.globalAngleSelection.map(SQLiteValue.text) ?? .null, + sample.packageAngleSelection.map(SQLiteValue.text) ?? .null, + sample.metalDeviceName.map(SQLiteValue.text) ?? .null, + sample.metalRegistryID.map(SQLiteValue.text) ?? .null + ] + ) + self.activeGraphicsStackSHA256 = sample.receipt.sha256 + } + } + + func recordStreamFreshness(_ sample: StreamFreshnessWindow) { + enqueue { + try self.execute( + "INSERT INTO stream_freshness_windows(session_id, graphics_run_id, started_monotonic_ns, ended_monotonic_ns, received_frames, content_changes, identical_frames, longest_identical_run_frames, longest_identical_run_ms, sequence_drops, sampled_pixels_per_frame) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), + .integer(Int64(bitPattern: sample.startedMonotonicNS)), + .integer(Int64(bitPattern: sample.endedMonotonicNS)), + .integer(Int64(sample.receivedFrames)), + .integer(Int64(sample.contentChanges)), + .integer(Int64(sample.identicalFrames)), + .integer(Int64(sample.longestIdenticalRunFrames)), + .real(sample.longestIdenticalRunMS), + .integer(Int64(bitPattern: sample.sequenceDrops)), + .integer(Int64(sample.sampledPixelsPerFrame)) + ] + ) + } + } + + func recordHostPresentation(_ sample: HostPresentationWindow) { + enqueue { + try self.execute( + "INSERT INTO host_presentation_windows(session_id, graphics_run_id, started_monotonic_ns, ended_monotonic_ns, submitted_frames, completed_frames, unique_source_uploads, repeated_source_presents, drawable_misses, command_errors, mean_completion_latency_ms, p95_completion_latency_ms, p99_completion_latency_ms, maximum_completion_latency_ms, mean_gpu_time_ms, p95_gpu_time_ms, maximum_gpu_time_ms) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), + .integer(Int64(bitPattern: sample.startedMonotonicNS)), + .integer(Int64(bitPattern: sample.endedMonotonicNS)), + .integer(Int64(sample.submittedFrames)), + .integer(Int64(sample.completedFrames)), + .integer(Int64(sample.uniqueSourceUploads)), + .integer(Int64(sample.repeatedSourcePresents)), + .integer(Int64(sample.drawableMisses)), + .integer(Int64(sample.commandErrors)), + sample.meanCompletionLatencyMS.map(SQLiteValue.real) ?? .null, + sample.p95CompletionLatencyMS.map(SQLiteValue.real) ?? .null, + sample.p99CompletionLatencyMS.map(SQLiteValue.real) ?? .null, + sample.maximumCompletionLatencyMS.map(SQLiteValue.real) ?? .null, + sample.meanGPUTimeMS.map(SQLiteValue.real) ?? .null, + sample.p95GPUTimeMS.map(SQLiteValue.real) ?? .null, + sample.maximumGPUTimeMS.map(SQLiteValue.real) ?? .null + ] + ) + } + } + + func recordGameFrameUpdate( + _ update: GameFrameTelemetryUpdate, + layerName: String?, + refreshPeriodNS: UInt64? + ) { + enqueue { + try self.transaction { + let observedNS = DispatchTime.now().uptimeNanoseconds + let windowID = try update.window.map { try self.insertGameFrameWindow($0) } + if let windowID, let window = update.window { + try self.execute( + "UPDATE game_frame_intervals SET game_frame_window_id = ? WHERE session_id = ? AND graphics_run_id IS ? AND game_frame_window_id IS NULL AND observed_monotonic_ns >= ? AND observed_monotonic_ns <= ?", + [ + .integer(windowID), .text(self.sessionIdentifier), self.graphicsRunValue(), + .integer(Int64(bitPattern: window.startedMonotonicNS)), + .integer(Int64(bitPattern: window.endedMonotonicNS)) + ] + ) + } + for interval in update.intervals { + try self.execute( + "INSERT INTO game_frame_intervals(session_id, graphics_run_id, stack_sha256, game_frame_window_id, observed_monotonic_ns, layer_name, refresh_period_ns, actual_present_ns, interval_ns, interval_ms, missed_vsync_equivalents, is_janky, is_severe) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), self.graphicsRunValue(), + self.graphicsStackValue(), + windowID.map(SQLiteValue.integer) ?? .null, + .integer(Int64(bitPattern: observedNS)), + layerName.map(SQLiteValue.text) ?? .null, + refreshPeriodNS.map { .integer(Int64(bitPattern: $0)) } ?? .null, + .integer(Int64(bitPattern: interval.actualPresentNS)), + .integer(Int64(bitPattern: interval.intervalNS)), + .real(interval.intervalMS), + .integer(Int64(interval.missedVsyncEquivalents)), + .integer(interval.isJanky ? 1 : 0), + .integer(interval.isSevere ? 1 : 0) + ] + ) + } + } + } + } + + func recordGameFrameWindow(_ window: GameFrameTelemetryWindow) { + enqueue { try self.insertGameFrameWindow(window) } + } + + fileprivate func recordDiagnosticArtifact(_ artifact: DiagnosticArtifact) { + enqueue { + try self.execute( + "INSERT INTO diagnostic_artifacts(session_id, graphics_run_id, stack_sha256, capture_scope, created_utc, created_monotonic_ns, artifact_kind, trigger, relative_path, byte_count, sha256, analysis_state, normalized_relative_path, normalized_sha256, normalized_summary_csv, trace_processor_sha256) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(self.sessionIdentifier), artifact.graphicsRunID.map(SQLiteValue.text) ?? .null, + artifact.graphicsStackSHA256.map(SQLiteValue.text) ?? .null, + .text(artifact.captureScope), .text(artifact.createdUTC), + .integer(Int64(bitPattern: artifact.createdMonotonicNS)), + .text(artifact.kind), .text(artifact.trigger), .text(artifact.relativePath), + .integer(artifact.byteCount), .text(artifact.sha256), .text(artifact.analysisState), + .text(artifact.normalizedRelativePath), .text(artifact.normalizedSHA256), + .text(artifact.normalizedSummaryCSV), .text(artifact.traceProcessorSHA256) + ] + ) + } + } + + fileprivate func recordGraphicsPipelineIncident(_ incident: GraphicsPipelineIncident) { + enqueue { + let window = incident.window + try self.execute( + """ + INSERT INTO graphics_pipeline_incidents( + incident_id, session_id, graphics_run_id, trigger, observed_monotonic_ns, + stack_sha256, + window_started_monotonic_ns, window_ended_monotonic_ns, layer_name, + effective_fps, one_percent_low_fps, p95_interval_ms, p99_interval_ms, + maximum_interval_ms, jank_count, severe_count, missed_vsync_equivalents, + trace_sequence, first_observed_divergent_boundary, causal_owner, + causal_confidence, explicit_unknowns_json + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [ + .text(incident.incidentID), .text(self.sessionIdentifier), self.graphicsRunValue(), + .text(incident.trigger), .integer(Int64(bitPattern: incident.observedMonotonicNS)), + self.graphicsStackValue(), + .integer(Int64(bitPattern: window.startedMonotonicNS)), + .integer(Int64(bitPattern: window.endedMonotonicNS)), + window.layerName.map(SQLiteValue.text) ?? .null, + .real(window.effectiveFPS), + window.onePercentLowFPS.map(SQLiteValue.real) ?? .null, + window.p95MS.map(SQLiteValue.real) ?? .null, + window.p99MS.map(SQLiteValue.real) ?? .null, + window.maximumMS.map(SQLiteValue.real) ?? .null, + .integer(Int64(window.jankCount)), .integer(Int64(window.severeCount)), + .integer(Int64(window.missedVsyncEquivalents)), + incident.traceSequence.map { .integer(Int64($0)) } ?? .null, + .text(incident.firstObservedDivergentBoundary), .text(incident.causalOwner), + .text(incident.causalConfidence), .text(Self.jsonArray(incident.explicitUnknowns)) + ] + ) + } + } + + func recordCombatBenchmark(_ run: CombatBenchmarkRun) { + enqueue { + let metrics = run.metrics + try self.execute( + """ + INSERT OR REPLACE INTO combat_benchmarks( + benchmark_id, session_id, preset_id, configuration_sha256, comparison_identity_sha256, configuration_json, + tft_package_version, performance_mode_confirmed, started_utc, ended_utc, + started_monotonic_ns, ended_monotonic_ns, exact_layer_identity, duration_seconds, + surface_availability, clock_coverage, p95_clock_rtt_ms, history_truncated, + correctness_passed, weighted_fps, one_percent_low_fps, p50_interval_ms, + p95_interval_ms, p99_interval_ms, max_interval_ms, jank_rate, severe_rate, + missed_vsync_rate, observer_overhead_invalid, valid, invalid_reason + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [ + .text(run.benchmarkID), .text(run.sessionID), .text(run.presetID.rawValue), + .text(run.configurationSHA256), .text(run.comparisonIdentitySHA256), + .text(run.configurationJSON), .text(run.tftPackageVersion), + .integer(run.performanceModeConfirmed ? 1 : 0), .text(run.startedUTC), .text(run.endedUTC), + .integer(Int64(bitPattern: run.startedMonotonicNS)), + .integer(Int64(bitPattern: run.endedMonotonicNS)), + run.exactLayerIdentity.map(SQLiteValue.text) ?? .null, + .real(metrics.combatDurationSeconds), .real(metrics.surfaceAvailability), + .real(metrics.clockCoverage), .real(metrics.p95ClockRoundTripMilliseconds), + .integer(metrics.frameHistoryTruncated ? 1 : 0), + .integer(metrics.correctnessPassed ? 1 : 0), .real(metrics.weightedFPS), + .real(metrics.onePercentLowFPS), .real(run.p50IntervalMilliseconds), + .real(metrics.p95IntervalMilliseconds), .real(metrics.p99IntervalMilliseconds), + .real(run.maximumIntervalMilliseconds), .real(metrics.jankRate), + .real(metrics.severeRate), .real(metrics.missedVsyncRate), + .integer(run.observerOverheadInvalid ? 1 : 0), + .integer(run.isValid ? 1 : 0), + run.invalidReason.map(SQLiteValue.text) ?? .null + ] + ) + } + } + + func recordCombatIncident(_ incident: CombatIncidentRecord) { + enqueue { + try self.execute( + """ + INSERT INTO combat_incidents( + incident_id, benchmark_id, session_id, preset_id, trigger, observed_monotonic_ns, + effective_fps, one_percent_low_fps, p99_interval_ms, severe_count, trace_sequence, + first_divergent_boundary, confidence, explicit_unknowns + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [ + .text(incident.incidentID), .text(incident.benchmarkID), .text(incident.sessionID), + .text(incident.presetID.rawValue), .text(incident.trigger), + .integer(Int64(bitPattern: incident.observedMonotonicNS)), + incident.effectiveFPS.map(SQLiteValue.real) ?? .null, + incident.onePercentLowFPS.map(SQLiteValue.real) ?? .null, + incident.p99IntervalMilliseconds.map(SQLiteValue.real) ?? .null, + .integer(Int64(incident.severeCount)), + incident.traceSequence.map { .integer(Int64($0)) } ?? .null, + .text(incident.firstDivergentBoundary), .text(incident.confidence), + .text(incident.explicitUnknowns) + ] + ) + } + } + + func recordCombatComparison(_ comparison: CombatComparisonRecord) { + enqueue { + let deltas = comparison.analysis.deltas + try self.execute( + """ + INSERT INTO combat_comparisons( + comparison_id, control_benchmark_id, candidate_benchmark_id, + weighted_fps_delta_percent, one_percent_low_delta_percent, + p95_delta_percent, p99_delta_percent, jank_delta_points, + severe_delta_points, missed_vsync_delta_points, correctness_status, + observer_overhead_invalid, decision, created_utc + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [ + .text(comparison.comparisonID), .text(comparison.controlBenchmarkID), + .text(comparison.candidateBenchmarkID), .real(deltas.weightedFPSPercent), + .real(deltas.onePercentLowFPSPercent), .real(deltas.p95IntervalPercent), + .real(deltas.p99IntervalPercent), .real(deltas.jankRatePercentagePoints), + .real(deltas.severeRatePercentagePoints), .real(deltas.missedVsyncRatePercentagePoints), + .text(comparison.correctnessStatus), + .integer(comparison.observerOverheadInvalid ? 1 : 0), + .text(comparison.analysis.decision.rawValue), .text(comparison.createdUTC) + ] + ) + } + } + + func recordGameProcessTransition(previousPID: Int32?, currentPID: Int32?) { + enqueue { + let now = Self.utcNow() + let monotonic = Int64(bitPattern: DispatchTime.now().uptimeNanoseconds) + if let previousPID { + try self.execute( + "UPDATE game_process_sessions SET ended_utc = ?, ended_monotonic_ns = ? WHERE session_id = ? AND game_pid = ? AND ended_utc IS NULL", + [.text(now), .integer(monotonic), .text(self.sessionIdentifier), .integer(Int64(previousPID))] + ) + } + if let currentPID { + try self.execute( + "INSERT INTO game_process_sessions(session_id, game_pid, started_utc, started_monotonic_ns) VALUES(?, ?, ?, ?)", + [.text(self.sessionIdentifier), .integer(Int64(currentPID)), .text(now), .integer(monotonic)] + ) + } + } + } + + fileprivate func recordInput(_ input: EmulatorInput) { + let values: [SQLiteValue] + switch input { + case .touch(let touch): + values = [ + .text(sessionIdentifier), .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .text("touch"), .integer(Int64(touch.x)), .integer(Int64(touch.y)), + .null, .integer(Int64(touch.pressure)), .null, .null + ] + case .mouse(let mouse): + values = [ + .text(sessionIdentifier), .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .text("mouse"), .integer(Int64(mouse.x)), .integer(Int64(mouse.y)), + .integer(Int64(mouse.buttons)), .null, .null, .null + ] + case .keyboard(let keyboard): + values = [ + .text(sessionIdentifier), .integer(Int64(bitPattern: DispatchTime.now().uptimeNanoseconds)), + .text("keyboard"), .null, .null, .null, .null, + keyboard.text.map { .integer(Int64($0.count)) } ?? .null, + keyboard.key.map(SQLiteValue.text) ?? .null + ] + } + enqueue { + try self.execute( + "INSERT INTO input_samples(session_id, monotonic_ns, input_kind, x, y, buttons, pressure, character_count, special_key) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", + values + ) + } + } + + func finish(status: String) { + queue.sync { + let now = Self.utcNow() + let monotonic = Int64(bitPattern: DispatchTime.now().uptimeNanoseconds) + if let activeGraphicsRunID { + try? self.closeGraphicsRun( + activeGraphicsRunID, + endedUTC: now, + endedMonotonicNS: UInt64(bitPattern: monotonic), + reason: "SESSION_SEALED" + ) + self.activeGraphicsRunID = nil + self.activeGraphicsRunPID = nil + self.activeGraphicsStackSHA256 = nil + } + try? self.execute( + "UPDATE game_process_sessions SET ended_utc = ?, ended_monotonic_ns = ? WHERE session_id = ? AND ended_utc IS NULL", + [.text(now), .integer(monotonic), .text(self.sessionIdentifier)] + ) + try? self.execute( + "UPDATE sessions SET ended_utc = ?, status = ? WHERE session_id = ?", + [.text(now), .text(status), .text(self.sessionIdentifier)] + ) + try? self.eventLog?.synchronize() + } + } + + private func executeSchema() throws { + let schema = """ + PRAGMA journal_mode=WAL; + PRAGMA synchronous=NORMAL; + PRAGMA foreign_keys=ON; + CREATE TABLE IF NOT EXISTS sessions( + session_id TEXT PRIMARY KEY, + started_utc TEXT NOT NULL, + ended_utc TEXT, + status TEXT NOT NULL, + profile_id TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS runtime_receipts( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + receipt_key TEXT NOT NULL, + receipt_value TEXT NOT NULL, + source TEXT NOT NULL, + confidence TEXT NOT NULL, + observed_utc TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS events( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + kind TEXT NOT NULL, + payload_json TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS graphics_runs( + graphics_run_id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + game_pid INTEGER, + started_utc TEXT NOT NULL, + started_monotonic_ns INTEGER NOT NULL, + last_observed_utc TEXT NOT NULL, + last_observed_monotonic_ns INTEGER NOT NULL, + ended_utc TEXT, + ended_monotonic_ns INTEGER, + start_reason TEXT NOT NULL, + end_reason TEXT, + configuration_sha256 TEXT NOT NULL, + target_fps INTEGER NOT NULL, + exact_layer_name TEXT + ); + CREATE TABLE IF NOT EXISTS frame_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + sequence INTEGER NOT NULL, + emulator_timestamp_us INTEGER NOT NULL, + received_monotonic_ns INTEGER NOT NULL, + width INTEGER NOT NULL, + height INTEGER NOT NULL, + byte_count INTEGER NOT NULL, + transport TEXT NOT NULL, + sequence_drop_count INTEGER NOT NULL, + visual_sample_count INTEGER NOT NULL, + mean_luma REAL NOT NULL, + nonblack_fraction REAL NOT NULL, + minimum_rgb INTEGER NOT NULL, + maximum_rgb INTEGER NOT NULL, + minimum_alpha INTEGER NOT NULL, + maximum_alpha INTEGER NOT NULL, + content_sha256 TEXT + ); + CREATE TABLE IF NOT EXISTS presentation_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + sampled_monotonic_ns INTEGER NOT NULL, + presented_frames INTEGER NOT NULL, + presentation_fps REAL NOT NULL, + source_fps REAL NOT NULL, + received_frames INTEGER NOT NULL, + mailbox_replacements INTEGER NOT NULL, + sequence_drops INTEGER NOT NULL, + last_sequence INTEGER + ); + CREATE TABLE IF NOT EXISTS frame_interval_windows( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + started_monotonic_ns INTEGER NOT NULL, + ended_monotonic_ns INTEGER NOT NULL, + frame_count INTEGER NOT NULL, + sequence_drop_count INTEGER NOT NULL, + mean_interval_ms REAL, + p95_interval_ms REAL, + maximum_interval_ms REAL + ); + CREATE TABLE IF NOT EXISTS game_frame_intervals( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + stack_sha256 TEXT, + game_frame_window_id INTEGER, + observed_monotonic_ns INTEGER NOT NULL, + layer_name TEXT, + refresh_period_ns INTEGER, + actual_present_ns INTEGER NOT NULL, + interval_ns INTEGER NOT NULL, + interval_ms REAL NOT NULL, + missed_vsync_equivalents INTEGER NOT NULL, + is_janky INTEGER NOT NULL, + is_severe INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS game_frame_windows( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + stack_sha256 TEXT, + started_monotonic_ns INTEGER NOT NULL, + ended_monotonic_ns INTEGER NOT NULL, + status TEXT NOT NULL, + unavailable_reason TEXT, + layer_name TEXT, + refresh_period_ns INTEGER, + frame_count INTEGER NOT NULL, + effective_fps REAL, + one_percent_low_fps REAL, + p50_interval_ms REAL, + p95_interval_ms REAL, + p99_interval_ms REAL, + maximum_interval_ms REAL, + jank_count INTEGER NOT NULL, + severe_count INTEGER NOT NULL, + missed_vsync_equivalents INTEGER NOT NULL, + history_truncated INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS stream_freshness_windows( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + started_monotonic_ns INTEGER NOT NULL, + ended_monotonic_ns INTEGER NOT NULL, + received_frames INTEGER NOT NULL, + content_changes INTEGER NOT NULL, + identical_frames INTEGER NOT NULL, + longest_identical_run_frames INTEGER NOT NULL, + longest_identical_run_ms REAL NOT NULL, + sequence_drops INTEGER NOT NULL, + sampled_pixels_per_frame INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS host_presentation_windows( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + started_monotonic_ns INTEGER NOT NULL, + ended_monotonic_ns INTEGER NOT NULL, + submitted_frames INTEGER NOT NULL, + completed_frames INTEGER NOT NULL, + unique_source_uploads INTEGER NOT NULL, + repeated_source_presents INTEGER NOT NULL, + drawable_misses INTEGER NOT NULL, + command_errors INTEGER NOT NULL, + mean_completion_latency_ms REAL, + p95_completion_latency_ms REAL, + p99_completion_latency_ms REAL, + maximum_completion_latency_ms REAL, + mean_gpu_time_ms REAL, + p95_gpu_time_ms REAL, + maximum_gpu_time_ms REAL + ); + CREATE TABLE IF NOT EXISTS resource_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + emulator_pid INTEGER NOT NULL, + emulator_cpu_percent REAL, + emulator_rss_kib INTEGER, + game_pid INTEGER, + top_activity TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS guest_memory_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + total_kib INTEGER NOT NULL, + available_kib INTEGER NOT NULL, + swap_total_kib INTEGER, + swap_free_kib INTEGER + ); + CREATE TABLE IF NOT EXISTS host_resource_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + available_kib INTEGER, + compressed_kib INTEGER, + swap_used_kib INTEGER, + pageouts INTEGER, + thermal_state TEXT NOT NULL, + power_source TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS clock_sync_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + host_t0_ns INTEGER NOT NULL, + guest_uptime_ns INTEGER NOT NULL, + host_t1_ns INTEGER NOT NULL, + host_midpoint_ns INTEGER NOT NULL, + round_trip_ns INTEGER NOT NULL, + host_minus_guest_ns INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS surfaceflinger_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + sample_label TEXT NOT NULL, + render_rate_hz REAL, + total_missed_frames INTEGER, + hwc_missed_frames INTEGER, + gpu_missed_frames INTEGER, + tft_requested_rate_hz REAL + ); + CREATE TABLE IF NOT EXISTS audio_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + sample_label TEXT NOT NULL, + backend TEXT NOT NULL, + active_output INTEGER NOT NULL, + sample_rate_hz INTEGER, + stereo_output INTEGER NOT NULL, + active_tracks INTEGER, + partial_underruns INTEGER, + empty_underruns INTEGER + ); + CREATE TABLE IF NOT EXISTS logcat_aggregates( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + byte_start INTEGER NOT NULL, + byte_end INTEGER NOT NULL, + skipped_bytes INTEGER NOT NULL, + line_count INTEGER NOT NULL, + anr_count INTEGER NOT NULL, + input_timeout_count INTEGER NOT NULL, + fatal_count INTEGER NOT NULL, + memory_kill_count INTEGER NOT NULL, + choreographer_skip_count INTEGER NOT NULL, + angle_warning_count INTEGER NOT NULL, + vulkan_warning_count INTEGER NOT NULL, + audio_error_count INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS pipeline_log_aggregates( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + source_stream TEXT NOT NULL, + byte_start INTEGER NOT NULL, + byte_end INTEGER NOT NULL, + skipped_bytes INTEGER NOT NULL, + line_count INTEGER NOT NULL, + gfxstream_warning_count INTEGER NOT NULL, + asg_stall_count INTEGER NOT NULL, + vulkan_error_count INTEGER NOT NULL, + moltenvk_warning_count INTEGER NOT NULL, + shader_error_count INTEGER NOT NULL, + fence_timeout_count INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS graphics_pipeline_snapshots( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + observed_utc TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + sample_label TEXT NOT NULL, + stack_sha256 TEXT NOT NULL, + stack_receipt_json TEXT NOT NULL, + receipt_completeness TEXT NOT NULL, + explicit_unknowns_json TEXT NOT NULL, + game_pid INTEGER, + exact_layer_name TEXT, + tft_surface_state TEXT NOT NULL, + game_graphics_api TEXT NOT NULL, + game_graphics_api_confidence TEXT NOT NULL, + angle_state TEXT NOT NULL, + gfxstream_state TEXT NOT NULL, + moltenvk_state TEXT NOT NULL, + emulator_version TEXT, + emulator_build_id TEXT, + emulator_gpu_selection TEXT, + gfxstream_feature_receipt TEXT, + gfxstream_tracing_state TEXT NOT NULL, + moltenvk_version TEXT, + moltenvk_configuration_json TEXT NOT NULL, + host_vulkan_device TEXT, + vulkan_composition INTEGER, + native_swapchain INTEGER, + guest_egl_implementation TEXT, + guest_vulkan_implementation TEXT, + global_angle_selection TEXT, + package_angle_selection TEXT, + metal_device_name TEXT, + metal_registry_id TEXT + ); + CREATE TABLE IF NOT EXISTS diagnostic_artifacts( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + stack_sha256 TEXT, + capture_scope TEXT NOT NULL, + created_utc TEXT NOT NULL, + created_monotonic_ns INTEGER NOT NULL, + artifact_kind TEXT NOT NULL, + trigger TEXT NOT NULL, + relative_path TEXT NOT NULL, + byte_count INTEGER NOT NULL, + sha256 TEXT NOT NULL, + analysis_state TEXT NOT NULL, + normalized_relative_path TEXT NOT NULL, + normalized_sha256 TEXT NOT NULL, + normalized_summary_csv TEXT NOT NULL, + trace_processor_sha256 TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS graphics_pipeline_incidents( + incident_id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + graphics_run_id TEXT, + trigger TEXT NOT NULL, + observed_monotonic_ns INTEGER NOT NULL, + stack_sha256 TEXT, + window_started_monotonic_ns INTEGER NOT NULL, + window_ended_monotonic_ns INTEGER NOT NULL, + layer_name TEXT, + effective_fps REAL NOT NULL, + one_percent_low_fps REAL, + p95_interval_ms REAL, + p99_interval_ms REAL, + maximum_interval_ms REAL, + jank_count INTEGER NOT NULL, + severe_count INTEGER NOT NULL, + missed_vsync_equivalents INTEGER NOT NULL, + trace_sequence INTEGER, + first_observed_divergent_boundary TEXT NOT NULL, + causal_owner TEXT NOT NULL, + causal_confidence TEXT NOT NULL, + explicit_unknowns_json TEXT NOT NULL + ); + \(CombatBenchmarkLabStore.schemaSQL) + CREATE TABLE IF NOT EXISTS game_process_sessions( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + game_pid INTEGER NOT NULL, + started_utc TEXT NOT NULL, + started_monotonic_ns INTEGER NOT NULL, + ended_utc TEXT, + ended_monotonic_ns INTEGER + ); + CREATE TABLE IF NOT EXISTS input_samples( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + monotonic_ns INTEGER NOT NULL, + input_kind TEXT NOT NULL, + x INTEGER, + y INTEGER, + buttons INTEGER, + pressure INTEGER, + character_count INTEGER, + special_key TEXT + ); + CREATE INDEX IF NOT EXISTS idx_events_kind_time ON events(kind, monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_graphics_runs_session_time ON graphics_runs(session_id, started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_frames_time ON frame_samples(received_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_frames_graphics_run ON frame_samples(graphics_run_id, received_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_frame_windows_time ON frame_interval_windows(started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_game_frame_intervals_time ON game_frame_intervals(observed_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_game_frame_windows_time ON game_frame_windows(started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_game_frame_windows_run ON game_frame_windows(graphics_run_id, started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_game_frame_intervals_window ON game_frame_intervals(game_frame_window_id, actual_present_ns); + CREATE INDEX IF NOT EXISTS idx_stream_freshness_time ON stream_freshness_windows(started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_host_presentation_time ON host_presentation_windows(started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_resources_time ON resource_samples(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_guest_memory_time ON guest_memory_samples(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_host_resources_time ON host_resource_samples(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_surfaceflinger_time ON surfaceflinger_samples(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_audio_time ON audio_samples(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_logcat_time ON logcat_aggregates(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_pipeline_log_time ON pipeline_log_aggregates(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_graphics_pipeline_time ON graphics_pipeline_snapshots(monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_graphics_pipeline_run_hash ON graphics_pipeline_snapshots(graphics_run_id, monotonic_ns, stack_sha256); + CREATE INDEX IF NOT EXISTS idx_diagnostic_artifacts_time ON diagnostic_artifacts(created_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_graphics_incidents_run_time ON graphics_pipeline_incidents(graphics_run_id, observed_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_combat_benchmarks_session ON combat_benchmarks(session_id, started_monotonic_ns); + CREATE INDEX IF NOT EXISTS idx_inputs_time ON input_samples(monotonic_ns); + DROP VIEW IF EXISTS graphics_frame_facts; + DROP VIEW IF EXISTS graphics_pipeline_windows; + DROP VIEW IF EXISTS graphics_window_context; + CREATE VIEW graphics_window_context AS + SELECT + w.id AS game_frame_window_id, + w.graphics_run_id, + w.session_id, + w.started_monotonic_ns, + w.ended_monotonic_ns, + (SELECT p.id + FROM graphics_pipeline_snapshots p + WHERE p.graphics_run_id = w.graphics_run_id + AND p.stack_sha256 = w.stack_sha256 + AND p.monotonic_ns <= w.ended_monotonic_ns + ORDER BY p.monotonic_ns DESC + LIMIT 1) AS pipeline_snapshot_id, + (SELECT s.id + FROM stream_freshness_windows s + WHERE s.graphics_run_id = w.graphics_run_id + AND s.started_monotonic_ns < w.ended_monotonic_ns + AND s.ended_monotonic_ns > w.started_monotonic_ns + ORDER BY s.started_monotonic_ns DESC + LIMIT 1) AS stream_window_id, + (SELECT h.id + FROM host_presentation_windows h + WHERE h.graphics_run_id = w.graphics_run_id + AND h.started_monotonic_ns < w.ended_monotonic_ns + AND h.ended_monotonic_ns > w.started_monotonic_ns + ORDER BY h.started_monotonic_ns DESC + LIMIT 1) AS host_presentation_window_id, + (SELECT l.id + FROM pipeline_log_aggregates l + WHERE l.graphics_run_id = w.graphics_run_id + AND l.monotonic_ns BETWEEN w.started_monotonic_ns - 5000000000 + AND w.ended_monotonic_ns + 5000000000 + ORDER BY l.monotonic_ns DESC + LIMIT 1) AS pipeline_log_aggregate_id + FROM game_frame_windows w; + CREATE VIEW graphics_pipeline_windows AS + SELECT + w.id AS game_frame_window_id, + w.session_id, + w.graphics_run_id, + w.started_monotonic_ns, + w.ended_monotonic_ns, + w.status AS surface_status, + w.unavailable_reason, + w.layer_name, + w.refresh_period_ns, + w.frame_count AS surface_frame_count, + w.effective_fps AS surface_effective_fps, + w.one_percent_low_fps, + w.p50_interval_ms, + w.p95_interval_ms, + w.p99_interval_ms, + w.maximum_interval_ms, + w.jank_count, + w.severe_count, + w.missed_vsync_equivalents, + p.stack_sha256, + p.stack_receipt_json, + p.receipt_completeness, + p.explicit_unknowns_json, + p.game_graphics_api, + p.game_graphics_api_confidence, + p.angle_state, + p.gfxstream_state, + p.moltenvk_state, + p.host_vulkan_device, + p.metal_device_name, + s.received_frames AS stream_received_frames, + CASE WHEN s.ended_monotonic_ns > s.started_monotonic_ns + THEN s.received_frames * 1000000000.0 / (s.ended_monotonic_ns - s.started_monotonic_ns) + ELSE NULL END AS stream_received_fps, + s.content_changes AS stream_content_changes, + s.identical_frames AS stream_identical_frames, + s.longest_identical_run_ms, + s.sequence_drops AS stream_sequence_drops, + h.submitted_frames AS presenter_submitted_frames, + h.completed_frames AS presenter_completed_frames, + CASE WHEN h.ended_monotonic_ns > h.started_monotonic_ns + THEN h.completed_frames * 1000000000.0 / (h.ended_monotonic_ns - h.started_monotonic_ns) + ELSE NULL END AS presenter_completed_fps, + h.repeated_source_presents, + h.drawable_misses, + h.command_errors, + h.p95_completion_latency_ms, + h.p95_gpu_time_ms, + l.gfxstream_warning_count, + l.asg_stall_count, + l.vulkan_error_count, + l.moltenvk_warning_count, + l.shader_error_count, + l.fence_timeout_count, + l.skipped_bytes AS pipeline_log_skipped_bytes, + CASE + WHEN l.id IS NULL THEN 'UNKNOWN_NO_CORRELATED_LOG_WINDOW' + WHEN l.skipped_bytes > 0 THEN 'UNKNOWN_TRUNCATED_LOG_WINDOW' + ELSE 'COMPLETE_CORRELATED_LOG_WINDOW' + END AS pipeline_log_coverage, + CASE + WHEN l.id IS NULL THEN 'NONE_OBSERVED' + WHEN l.skipped_bytes > 0 THEN 'UNKNOWN_TRUNCATED_LOG_WINDOW' + WHEN l.gfxstream_warning_count > 0 OR l.asg_stall_count > 0 + OR l.vulkan_error_count > 0 OR l.moltenvk_warning_count > 0 + OR l.shader_error_count > 0 OR l.fence_timeout_count > 0 + THEN 'LOG_CORRELATED_UNATTRIBUTED' + ELSE 'NONE_OBSERVED' + END AS concurrent_pipeline_signal, + CASE + WHEN w.status <> 'AVAILABLE' THEN 'UNKNOWN_NO_EXACT_SURFACE_SAMPLE' + WHEN w.effective_fps < 59.0 OR w.jank_count > 0 OR w.severe_count > 0 + OR w.missed_vsync_equivalents > 0 THEN 'TFT_SURFACE_ACTUAL_PRESENT' + WHEN s.id IS NOT NULL AND ( + s.received_frames * 1000000000.0 / MAX(1, s.ended_monotonic_ns - s.started_monotonic_ns) < 59.0 + OR s.sequence_drops > 0) THEN 'EMULATOR_IMAGE_STREAM' + WHEN h.id IS NOT NULL AND ( + h.completed_frames * 1000000000.0 / MAX(1, h.ended_monotonic_ns - h.started_monotonic_ns) < 59.0 + OR h.drawable_misses > 0 OR h.command_errors > 0 + OR COALESCE(h.p95_completion_latency_ms, 0) > 16.667 + OR COALESCE(h.p95_gpu_time_ms, 0) > 16.667) THEN 'TFTMAC_NATIVE_PRESENTER' + ELSE 'NO_OBSERVED_DIVERGENCE' + END AS first_observed_divergent_boundary, + CASE + WHEN w.status <> 'AVAILABLE' THEN 'UNKNOWN' + WHEN w.effective_fps < 59.0 OR w.jank_count > 0 OR w.severe_count > 0 + OR w.missed_vsync_equivalents > 0 THEN 'UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE' + WHEN s.id IS NOT NULL AND ( + s.received_frames * 1000000000.0 / MAX(1, s.ended_monotonic_ns - s.started_monotonic_ns) < 59.0 + OR s.sequence_drops > 0) THEN 'EMULATOR_OUTPUT_OR_CONTROLLER' + WHEN h.id IS NOT NULL AND ( + h.completed_frames * 1000000000.0 / MAX(1, h.ended_monotonic_ns - h.started_monotonic_ns) < 59.0 + OR h.drawable_misses > 0 OR h.command_errors > 0 + OR COALESCE(h.p95_completion_latency_ms, 0) > 16.667 + OR COALESCE(h.p95_gpu_time_ms, 0) > 16.667) THEN 'TFTMAC_NATIVE_PRESENTER' + ELSE 'NONE_OBSERVED' + END AS causal_owner, + CASE + WHEN w.status <> 'AVAILABLE' THEN 'UNKNOWN' + WHEN w.effective_fps < 59.0 OR w.jank_count > 0 OR w.severe_count > 0 + OR w.missed_vsync_equivalents > 0 THEN 'UNKNOWN' + WHEN s.id IS NOT NULL AND ( + s.received_frames * 1000000000.0 / MAX(1, s.ended_monotonic_ns - s.started_monotonic_ns) < 59.0 + OR s.sequence_drops > 0) THEN + CASE WHEN p.receipt_completeness = 'COMPLETE' THEN 'MEDIUM' ELSE 'LOW' END + WHEN h.id IS NOT NULL AND ( + h.completed_frames * 1000000000.0 / MAX(1, h.ended_monotonic_ns - h.started_monotonic_ns) < 59.0 + OR h.drawable_misses > 0 OR h.command_errors > 0 + OR COALESCE(h.p95_completion_latency_ms, 0) > 16.667 + OR COALESCE(h.p95_gpu_time_ms, 0) > 16.667) THEN + CASE WHEN p.receipt_completeness = 'COMPLETE' THEN 'MEDIUM' ELSE 'LOW' END + ELSE 'NOT_APPLICABLE' + END AS causal_confidence, + 'Internal Unreal, ANGLE, ASG, gfxstream, Vulkan-submit, MoltenVK, and Metal ownership remains UNKNOWN without a shared cross-stack frame ID.' AS attribution_limit + FROM graphics_window_context c + JOIN game_frame_windows w ON w.id = c.game_frame_window_id + LEFT JOIN graphics_pipeline_snapshots p ON p.id = c.pipeline_snapshot_id + LEFT JOIN stream_freshness_windows s ON s.id = c.stream_window_id + LEFT JOIN host_presentation_windows h ON h.id = c.host_presentation_window_id + LEFT JOIN pipeline_log_aggregates l ON l.id = c.pipeline_log_aggregate_id; + CREATE VIEW graphics_frame_facts AS + SELECT + i.id AS game_frame_interval_id, + i.session_id, + i.graphics_run_id, + i.game_frame_window_id, + i.observed_monotonic_ns, + i.actual_present_ns, + i.interval_ns, + i.interval_ms, + i.missed_vsync_equivalents, + i.is_janky, + i.is_severe, + i.stack_sha256, + s.stack_receipt_json, + s.game_graphics_api, + s.angle_state, + s.gfxstream_state, + s.moltenvk_state, + s.host_vulkan_device, + s.metal_device_name, + p.first_observed_divergent_boundary, + p.causal_owner, + p.causal_confidence, + p.attribution_limit + FROM game_frame_intervals i + LEFT JOIN graphics_pipeline_windows p ON p.game_frame_window_id = i.game_frame_window_id + LEFT JOIN graphics_pipeline_snapshots s ON s.id = ( + SELECT s2.id + FROM graphics_pipeline_snapshots s2 + WHERE s2.graphics_run_id = i.graphics_run_id + AND s2.stack_sha256 = i.stack_sha256 + AND s2.monotonic_ns <= i.observed_monotonic_ns + ORDER BY s2.monotonic_ns DESC + LIMIT 1 + ); + PRAGMA user_version=3; + """ + guard sqlite3_exec(database, schema, nil, nil, nil) == SQLITE_OK else { + throw TFTMACRuntimeError("The native SQL telemetry schema could not be created.") + } + } + + private func enqueue(_ operation: @escaping @Sendable () throws -> Void) { + queue.async { + do { try operation() } + catch { fputs("TFTMAC telemetry error: \(error.localizedDescription)\n", stderr) } + } + } + + @discardableResult + private func insertGameFrameWindow(_ window: GameFrameTelemetryWindow) throws -> Int64 { + let state = Self.gameFrameStatus(window.status) + let available: Bool + if case .available = window.status { available = true } else { available = false } + try execute( + "INSERT INTO game_frame_windows(session_id, graphics_run_id, stack_sha256, started_monotonic_ns, ended_monotonic_ns, status, unavailable_reason, layer_name, refresh_period_ns, frame_count, effective_fps, one_percent_low_fps, p50_interval_ms, p95_interval_ms, p99_interval_ms, maximum_interval_ms, jank_count, severe_count, missed_vsync_equivalents, history_truncated) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + .text(sessionIdentifier), graphicsRunValue(), graphicsStackValue(), + .integer(Int64(bitPattern: window.startedMonotonicNS)), + .integer(Int64(bitPattern: window.endedMonotonicNS)), + .text(state.status), state.reason.map(SQLiteValue.text) ?? .null, + window.layerName.map(SQLiteValue.text) ?? .null, + window.refreshPeriodNS.map { .integer(Int64(bitPattern: $0)) } ?? .null, + .integer(Int64(window.frameCount)), available ? .real(window.effectiveFPS) : .null, + window.onePercentLowFPS.map(SQLiteValue.real) ?? .null, + window.p50MS.map(SQLiteValue.real) ?? .null, + window.p95MS.map(SQLiteValue.real) ?? .null, + window.p99MS.map(SQLiteValue.real) ?? .null, + window.maximumMS.map(SQLiteValue.real) ?? .null, + .integer(Int64(window.jankCount)), .integer(Int64(window.severeCount)), + .integer(Int64(window.missedVsyncEquivalents)), + .integer(window.historyTruncated ? 1 : 0) + ] + ) + guard let database else { throw TFTMACRuntimeError("The telemetry database is closed.") } + return sqlite3_last_insert_rowid(database) + } + + private func graphicsRunValue() -> SQLiteValue { + activeGraphicsRunID.map(SQLiteValue.text) ?? .null + } + + private func graphicsStackValue() -> SQLiteValue { + activeGraphicsStackSHA256.map(SQLiteValue.text) ?? .null + } + + private func closeGraphicsRun( + _ graphicsRunID: String, + endedUTC: String, + endedMonotonicNS: UInt64, + reason: String + ) throws { + try execute( + "UPDATE graphics_runs SET ended_utc = ?, ended_monotonic_ns = ?, last_observed_utc = ?, last_observed_monotonic_ns = ?, end_reason = ? WHERE graphics_run_id = ? AND ended_utc IS NULL", + [ + .text(endedUTC), .integer(Int64(bitPattern: endedMonotonicNS)), + .text(endedUTC), .integer(Int64(bitPattern: endedMonotonicNS)), + .text(reason), .text(graphicsRunID) + ] + ) + } + + private static func gameFrameStatus(_ status: GameFrameTelemetryStatus) -> (status: String, reason: String?) { + switch status { + case .available: + return ("AVAILABLE", nil) + case .unavailable(.noTFTSurfaceView): + return ("UNAVAILABLE", "NO_TFT_SURFACE_VIEW") + case .unavailable(.multipleTFTSurfaceViews): + return ("UNAVAILABLE", "MULTIPLE_TFT_SURFACE_VIEWS") + case .unavailable(.noTimestamps): + return ("UNAVAILABLE", "NO_TIMESTAMPS") + case .unavailable(.malformedLatency): + return ("UNAVAILABLE", "MALFORMED_LATENCY") + case .unavailable(.adbError): + return ("UNAVAILABLE", "ADB_ERROR") + } + } + + private func execute(_ sql: String, _ values: [SQLiteValue]) throws { + guard let database else { throw TFTMACRuntimeError("The telemetry database is closed.") } + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK, let statement else { + throw TFTMACRuntimeError("SQLite could not prepare a telemetry statement.") + } + defer { sqlite3_finalize(statement) } + for (offset, value) in values.enumerated() { + let index = Int32(offset + 1) + switch value { + case .integer(let value): sqlite3_bind_int64(statement, index, value) + case .real(let value): sqlite3_bind_double(statement, index, value) + case .text(let value): sqlite3_bind_text(statement, index, value, -1, transientDestructor) + case .null: sqlite3_bind_null(statement, index) + } + } + guard sqlite3_step(statement) == SQLITE_DONE else { + throw TFTMACRuntimeError("SQLite could not write a telemetry record.") + } + } + + private func transaction(_ operation: () throws -> Void) throws { + guard let database else { throw TFTMACRuntimeError("The telemetry database is closed.") } + guard sqlite3_exec(database, "BEGIN IMMEDIATE", nil, nil, nil) == SQLITE_OK else { + throw TFTMACRuntimeError("SQLite could not begin a telemetry transaction.") + } + do { + try operation() + guard sqlite3_exec(database, "COMMIT", nil, nil, nil) == SQLITE_OK else { + throw TFTMACRuntimeError("SQLite could not commit a telemetry transaction.") + } + } catch { + sqlite3_exec(database, "ROLLBACK", nil, nil, nil) + throw error + } + } + + private static func utcNow() -> String { + ISO8601DateFormatter().string(from: Date()) + } + + private static func json(_ object: [String: Any]) -> String { + guard JSONSerialization.isValidJSONObject(object), + let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys, .withoutEscapingSlashes]) else { + return "{}" + } + return String(data: data, encoding: .utf8) ?? "{}" + } + + private static func jsonArray(_ values: [String]) -> String { + guard let data = try? JSONSerialization.data( + withJSONObject: values, + options: [.sortedKeys, .withoutEscapingSlashes] + ) else { return "[]" } + return String(data: data, encoding: .utf8) ?? "[]" + } +} + +private final class NativeFrameAdmissionState: @unchecked Sendable { + private let lock = NSLock() + private let mailbox: LatestFrameMailbox + private let telemetry: TFTMACNativeTelemetry + private var previousSequence: UInt32? + private var admittedFirstFrame = false + private var windowStartedNS: UInt64? + private var previousReceivedNS: UInt64? + private var windowFrameCount = 0 + private var windowSequenceDrops: UInt64 = 0 + private var windowIntervalsNS = [UInt64]() + private var previousContentFingerprint: UInt64? + private var freshnessContentChanges = 0 + private var freshnessIdenticalFrames = 0 + private var identicalRunFrames = 0 + private var identicalRunStartedNS: UInt64? + private var longestIdenticalRunFrames = 0 + private var longestIdenticalRunMS = 0.0 + + init(mailbox: LatestFrameMailbox, telemetry: TFTMACNativeTelemetry) { + self.mailbox = mailbox + self.telemetry = telemetry + } + + func admit(_ image: Android_Emulation_Control_Image) throws -> Bool { + let width = Int(image.format.width == 0 ? image.width : image.format.width) + let height = Int(image.format.height == 0 ? image.height : image.format.height) + guard width > 0, height > 0 else { return false } + guard image.format.format == .rgba8888 else { + throw TFTMACRuntimeError("The emulator returned a non-RGBA8888 frame.") + } + try FrameContract.validate(width: width, height: height, byteCount: image.image.count) + + let receivedNS = DispatchTime.now().uptimeNanoseconds + let fingerprint = Self.sampledContentFingerprint(image.image) + lock.lock() + let sequenceDrops: UInt64 + if let previousSequence, image.seq > previousSequence &+ 1 { + sequenceDrops = UInt64(image.seq - previousSequence - 1) + } else { + sequenceDrops = 0 + } + previousSequence = image.seq + let isFirstFrame = !admittedFirstFrame + admittedFirstFrame = true + if windowStartedNS == nil { windowStartedNS = receivedNS } + let priorReceivedNS = previousReceivedNS + if let priorReceivedNS { windowIntervalsNS.append(receivedNS &- priorReceivedNS) } + previousReceivedNS = receivedNS + windowFrameCount += 1 + windowSequenceDrops &+= sequenceDrops + if let previousContentFingerprint { + if previousContentFingerprint == fingerprint.value { + freshnessIdenticalFrames += 1 + identicalRunFrames += 1 + if identicalRunStartedNS == nil { identicalRunStartedNS = priorReceivedNS ?? receivedNS } + longestIdenticalRunFrames = max(longestIdenticalRunFrames, identicalRunFrames) + if let runStarted = identicalRunStartedNS { + longestIdenticalRunMS = max(longestIdenticalRunMS, Double(receivedNS &- runStarted) / 1_000_000) + } + } else { + freshnessContentChanges += 1 + identicalRunFrames = 1 + identicalRunStartedNS = receivedNS + } + } else { + identicalRunFrames = 1 + identicalRunStartedNS = receivedNS + } + previousContentFingerprint = fingerprint.value + var completedWindow: FrameIntervalWindow? + var completedFreshnessWindow: StreamFreshnessWindow? + if let started = windowStartedNS, receivedNS &- started >= 1_000_000_000 { + let sorted = windowIntervalsNS.sorted() + let mean = sorted.isEmpty ? nil : Double(sorted.reduce(0, &+)) / Double(sorted.count) / 1_000_000 + let p95Index = sorted.isEmpty ? 0 : min(sorted.count - 1, Int(ceil(Double(sorted.count) * 0.95)) - 1) + completedWindow = FrameIntervalWindow( + startedMonotonicNS: started, + endedMonotonicNS: receivedNS, + frameCount: windowFrameCount, + sequenceDropCount: windowSequenceDrops, + meanIntervalMS: mean, + p95IntervalMS: sorted.isEmpty ? nil : Double(sorted[p95Index]) / 1_000_000, + maximumIntervalMS: sorted.last.map { Double($0) / 1_000_000 } + ) + completedFreshnessWindow = StreamFreshnessWindow( + startedMonotonicNS: started, + endedMonotonicNS: receivedNS, + receivedFrames: windowFrameCount, + contentChanges: freshnessContentChanges, + identicalFrames: freshnessIdenticalFrames, + longestIdenticalRunFrames: longestIdenticalRunFrames, + longestIdenticalRunMS: longestIdenticalRunMS, + sequenceDrops: windowSequenceDrops, + sampledPixelsPerFrame: fingerprint.sampleCount + ) + windowStartedNS = receivedNS + windowFrameCount = 0 + windowSequenceDrops = 0 + windowIntervalsNS.removeAll(keepingCapacity: true) + freshnessContentChanges = 0 + freshnessIdenticalFrames = 0 + longestIdenticalRunFrames = identicalRunFrames + longestIdenticalRunMS = 0 + } + lock.unlock() + + let checkpoint = isFirstFrame || image.seq.isMultiple(of: 60) + + let frame = EmulatorFrame( + pixels: image.image, + width: width, + height: height, + sequence: image.seq, + emulatorTimestampMicroseconds: image.timestampUs, + receivedMonotonicNanoseconds: receivedNS + ) + mailbox.publish(frame) + if let completedWindow { telemetry.recordFrameIntervalWindow(completedWindow) } + if let completedFreshnessWindow { telemetry.recordStreamFreshness(completedFreshnessWindow) } + guard checkpoint else { return isFirstFrame } + let visual = Self.sampleVisualContent(image.image, includeHash: true) + telemetry.recordFrameReceived(frame, transport: "raw_grpc_rgba8888", sequenceDropCount: sequenceDrops, visual: visual) + let visualPayload: [String: Any] = [ + "visual_sample_count": visual.sampleCount, + "mean_luma": visual.meanLuma, + "nonblack_fraction": visual.nonBlackFraction, + "minimum_rgb": visual.minimumRGB, + "maximum_rgb": visual.maximumRGB, + "minimum_alpha": visual.minimumAlpha, + "maximum_alpha": visual.maximumAlpha, + "content_sha256": visual.contentSHA256 ?? NSNull() + ] + if isFirstFrame { + var payload: [String: Any] = [ + "width": width, "height": height, "bytes": image.image.count, + "sequence": image.seq, "bottom_up": true, + "pixel_format": "RGBA8888", + "rotation": image.format.rotation.rotation.rawValue + ] + payload.merge(visualPayload) { current, _ in current } + telemetry.recordEvent("FIRST_NATIVE_FRAME", payload: payload) + } else if checkpoint { + var payload = visualPayload + payload["sequence"] = image.seq + payload["emulator_timestamp_us"] = String(image.timestampUs) + telemetry.recordEvent("FRAME_VISUAL_CHECKPOINT", payload: payload) + } + return isFirstFrame + } + + private static func sampleVisualContent(_ data: Data, includeHash: Bool) -> FrameVisualSample { + let pixelCount = data.count / FrameContract.bytesPerPixel + let step = max(1, pixelCount / 4096) + var sampleCount = 0 + var lumaSum = 0.0 + var nonBlackCount = 0 + var minimumRGB = 255 + var maximumRGB = 0 + var minimumAlpha = 255 + var maximumAlpha = 0 + + data.withUnsafeBytes { raw in + let bytes = raw.bindMemory(to: UInt8.self) + var pixel = 0 + while pixel < pixelCount { + let offset = pixel * FrameContract.bytesPerPixel + let red = Int(bytes[offset]) + let green = Int(bytes[offset + 1]) + let blue = Int(bytes[offset + 2]) + let alpha = Int(bytes[offset + 3]) + minimumRGB = Swift.min(minimumRGB, Swift.min(red, Swift.min(green, blue))) + maximumRGB = Swift.max(maximumRGB, Swift.max(red, Swift.max(green, blue))) + minimumAlpha = min(minimumAlpha, alpha) + maximumAlpha = max(maximumAlpha, alpha) + lumaSum += (0.2126 * Double(red) + 0.7152 * Double(green) + 0.0722 * Double(blue)) / 255.0 + if max(red, green, blue) > 4 { nonBlackCount += 1 } + sampleCount += 1 + pixel += step + } + } + + let divisor = Double(max(sampleCount, 1)) + let digest = includeHash + ? SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + : nil + return FrameVisualSample( + sampleCount: sampleCount, + meanLuma: lumaSum / divisor, + nonBlackFraction: Double(nonBlackCount) / divisor, + minimumRGB: minimumRGB, + maximumRGB: maximumRGB, + minimumAlpha: minimumAlpha, + maximumAlpha: maximumAlpha, + contentSHA256: digest + ) + } + + private static func sampledContentFingerprint(_ data: Data) -> (value: UInt64, sampleCount: Int) { + let pixelCount = data.count / FrameContract.bytesPerPixel + let step = max(1, pixelCount / 4096) + var hash: UInt64 = 1_469_598_103_934_665_603 + var sampleCount = 0 + data.withUnsafeBytes { raw in + let bytes = raw.bindMemory(to: UInt8.self) + var pixel = 0 + while pixel < pixelCount { + let offset = pixel * FrameContract.bytesPerPixel + hash ^= UInt64(bytes[offset]) + hash &*= 1_099_511_628_211 + hash ^= UInt64(bytes[offset + 1]) << 8 + hash &*= 1_099_511_628_211 + hash ^= UInt64(bytes[offset + 2]) << 16 + hash &*= 1_099_511_628_211 + hash ^= UInt64(bytes[offset + 3]) << 24 + hash &*= 1_099_511_628_211 + sampleCount += 1 + pixel += step + } + } + return (hash, sampleCount) + } +} + +actor TFTMACRuntimeService { + typealias StatusHandler = @MainActor @Sendable (String, Bool) -> Void + typealias GameFrameHandler = @MainActor @Sendable (GameFrameTelemetryWindow?) -> Void + + private let profile: TFTMACRuntimeProfile + private let mailbox: LatestFrameMailbox + private let status: StatusHandler + private let gameFrame: GameFrameHandler + private var telemetry: TFTMACNativeTelemetry? + private var labStore: CombatBenchmarkLabStore? + private var paths: TFTMACRuntimePaths? + private var openProcess: Process? + private var logcatProcess: Process? + private var logcatOutputHandle: FileHandle? + private var logcatErrorHandle: FileHandle? + private var logcatReadOffset: UInt64 = 0 + private var emulatorStdoutReadOffset: UInt64 = 0 + private var emulatorStderrReadOffset: UInt64 = 0 + private var runtimeLease: TFTMACRuntimeLease? + private var expectedSessionMarker: String? + private var discovery: EmulatorControllerDiscovery? + private var inputContinuation: AsyncStream.Continuation? + private var avdTransaction: AVDConfigurationTransaction? + private var traceCaptureInProgress = false + private var traceCaptureTask: Task? + private var traceCaptureMeasurementStartNS: UInt64? + private var traceCaptureMeasurementEndNS: UInt64? + private var traceCaptureCount = 0 + private var automaticTraceCount = 0 + private var incidentTraceCount = 0 + private var lastAutomaticTraceNS: UInt64 = 0 + private var currentGamePID: Int32? + private var currentExactLayerName: String? + private var consecutiveBadGraphicsWindows = 0 + private var graphicsAutomaticTraceCount = 0 + private var graphicsIncidentTraceCount = 0 + private var lastGraphicsAutomaticTraceNS: UInt64 = 0 + private var activeCombatBenchmark: ActiveCombatBenchmark? + private var benchmarkDeadlineTask: Task? + private var latestGameFrameWindow: GameFrameTelemetryWindow? + private var tftPackageVersion = "unknown" + private var stopping = false + + init( + profile: TFTMACRuntimeProfile, + mailbox: LatestFrameMailbox, + status: @escaping StatusHandler, + gameFrame: @escaping GameFrameHandler + ) { + self.profile = profile + self.mailbox = mailbox + self.status = status + self.gameFrame = gameFrame + } + + func run() async throws { + let applicationSupport = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support/TFTMAC", isDirectory: true) + let telemetry = try TFTMACNativeTelemetry(profile: profile, applicationSupport: applicationSupport) + self.telemetry = telemetry + labStore = try CombatBenchmarkLabStore(applicationSupport: applicationSupport) + await status("Starting Android through the native Mac app host…", false) + + do { + let paths = try TFTMACRuntimePaths.discover() + self.paths = paths + let stateRoot = paths.applicationSupport.appendingPathComponent("State", isDirectory: true) + runtimeLease = try TFTMACRuntimeLease.acquire(stateRoot: stateRoot) + telemetry.recordEvent("RUNTIME_LEASE_ACQUIRED", payload: [ + "lease": "State/native-runtime.lease", + "pid": ProcessInfo.processInfo.processIdentifier, + "exclusive": true + ]) + try assertRuntimeUnoccupied(telemetry: telemetry) + try recoverInterruptedAVDTransaction(paths: paths) + recordFrozenReceipts(telemetry: telemetry, paths: paths) + avdTransaction = try prepareAVD(paths: paths, telemetry: telemetry) + try startADBServer(paths: paths, telemetry: telemetry) + let launchStarted = Date() + try launchEmulatorHost(paths: paths, telemetry: telemetry) + let discovery = try await waitForDiscovery(paths: paths, captureDirectory: telemetry.captureDirectory, after: launchStarted) + self.discovery = discovery + telemetry.recordEvent("EMULATOR_CONTROLLER_DISCOVERED", payload: [ + "pid": discovery.processIdentifier, + "grpc_port": discovery.port, + "record": discovery.recordPath, + "token_persisted": false + ]) + try recordHostSchedulingReceipt(telemetry: telemetry) + + let (inputStream, continuation) = AsyncStream.makeStream(of: EmulatorInput.self, bufferingPolicy: .bufferingNewest(256)) + inputContinuation = continuation + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [profile, mailbox] in + try await Self.runController( + discovery: discovery, + profile: profile, + mailbox: mailbox, + telemetry: telemetry, + inputStream: inputStream, + status: self.status + ) + } + group.addTask { + try await self.waitForBootAndLaunchGame(paths: paths, telemetry: telemetry) + } + group.addTask { + try await self.sampleRuntime(paths: paths, telemetry: telemetry, emulatorPID: discovery.processIdentifier) + } + group.addTask { + try await self.sampleGameFrames(paths: paths, telemetry: telemetry) + } + _ = try await group.next() + group.cancelAll() + } + if !stopping { throw TFTMACRuntimeError("The native emulator session ended unexpectedly.") } + } catch { + if stopping || error is CancellationError { + telemetry.recordEvent("RUNTIME_STOP_REQUESTED", payload: ["reason": "application_termination"]) + await cleanup(status: "STOPPED") + return + } + telemetry.recordEvent("RUNTIME_FAILED", payload: [ + "error": error.localizedDescription, + "diagnostic": String(describing: error), + "type": String(reflecting: type(of: error)) + ]) + if profile.experimentPreset.isActiveCandidate { + recordCorrectnessRejection(reason: error.localizedDescription) + TFTMACRuntimeProfile.playable.with(experimentPreset: .control).save() + telemetry.recordEvent("EXPERIMENT_AUTO_ROLLBACK", payload: [ + "failed_preset": profile.experimentPreset.rawValue, + "restored_preset": RuntimeExperimentPreset.control.rawValue, + "classification": "REJECTED_CORRECTNESS", + "applies_after_restart": true + ]) + } + await cleanup(status: stopping ? "STOPPED" : "FAILED") + if !stopping { await status(error.localizedDescription, true) } + throw error + } + await cleanup(status: "STOPPED") + } + + func sendMouse(_ input: MouseInput) { + telemetry?.recordInput(.mouse(input)) + inputContinuation?.yield(.mouse(input)) + } + + func sendTouch(_ input: TouchInput) { + telemetry?.recordInput(.touch(input)) + inputContinuation?.yield(.touch(input)) + } + + func sendKeyboard(_ input: KeyboardInput) { + telemetry?.recordInput(.keyboard(input)) + inputContinuation?.yield(.keyboard(input)) + } + + func recordPresentation(_ sample: PresentationSample) { + telemetry?.recordPresentation(sample) + } + + func recordHostPresentation(_ sample: HostPresentationWindow) { + telemetry?.recordHostPresentation(sample) + } + + func startCombatBenchmark(performanceModeConfirmed: Bool) { + guard activeCombatBenchmark == nil else { + telemetry?.recordEvent("COMBAT_BENCHMARK_START_REJECTED", payload: ["reason": "ALREADY_RUNNING"]) + return + } + guard let telemetry, let paths, discovery != nil else { return } + if profile.experimentPreset.requiresManualPerformanceModeBetaConfirmation, + !performanceModeConfirmed { + telemetry.recordEvent("COMBAT_BENCHMARK_START_REJECTED", payload: [ + "reason": "PERFORMANCE_MODE_BETA_NOT_CONFIRMED", + "preset_id": profile.experimentPreset.rawValue + ]) + return + } + + let nowNS = DispatchTime.now().uptimeNanoseconds + let receipt = profile.experimentConfigurationReceipt + activeCombatBenchmark = ActiveCombatBenchmark( + benchmarkID: UUID().uuidString.lowercased(), + sessionID: telemetry.sessionIdentifier, + presetID: profile.experimentPreset, + configurationSHA256: receipt.sha256, + comparisonIdentitySHA256: profile.comparisonConfigurationSHA256, + configurationJSON: receipt.canonicalJSON, + tftPackageVersion: tftPackageVersion, + performanceModeConfirmed: performanceModeConfirmed, + startedUTC: Self.utcNow(), + startedMonotonicNS: nowNS + ) + if let active = activeCombatBenchmark { + let placeholder = active.finish(endedUTC: active.startedUTC, endedMonotonicNS: nowNS) + telemetry.recordCombatBenchmark(placeholder) + try? labStore?.record(placeholder) + } + automaticTraceCount = 0 + incidentTraceCount = 0 + lastAutomaticTraceNS = 0 + recordClockSync(paths: paths, telemetry: telemetry) + recordDiagnosticSnapshot(paths: paths, telemetry: telemetry, label: "combat_benchmark_start") + recordGraphicsPipelineSnapshot(paths: paths, telemetry: telemetry, label: "combat_benchmark_start") + telemetry.recordEvent("COMBAT_BENCHMARK_STARTED", payload: [ + "benchmark_id": activeCombatBenchmark?.benchmarkID ?? "unknown", + "preset_id": profile.experimentPreset.rawValue, + "configuration_sha256": receipt.sha256, + "performance_mode_beta_confirmed": performanceModeConfirmed, + "minimum_valid_seconds": 300, + "automatic_close_seconds": 480, + "combat_only_trace_budget": 3 + ]) + requestDiagnosticTrace( + scope: .combatBenchmark, + trigger: "COMBAT_BENCHMARK_START", + automatic: false, + durationSeconds: 20, + bufferMiB: 32, + benchmarkStartTrace: true + ) + benchmarkDeadlineTask?.cancel() + benchmarkDeadlineTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(480)) + guard !Task.isCancelled else { return } + await self?.endCombatBenchmark(reason: "AUTOMATIC_EIGHT_MINUTE_CLOSE") + } + } + + func markVisibleStutter() { + guard let telemetry else { return } + guard currentGamePID != nil, currentExactLayerName != nil else { + telemetry.recordEvent("VISIBLE_STUTTER_IGNORED", payload: ["reason": "NO_ACTIVE_TFT_GRAPHICS_RUN"]) + return + } + let active = activeCombatBenchmark + telemetry.recordEvent("VISIBLE_STUTTER", payload: [ + "benchmark_id": active?.benchmarkID ?? NSNull(), + "preset_id": active?.presetID.rawValue ?? profile.experimentPreset.rawValue, + "graphics_logger_automatic": true, + "host_monotonic_timestamp": true + ]) + let traceSequence = requestDiagnosticTrace( + scope: active == nil ? .automaticGraphics : .combatBenchmark, + trigger: "VISIBLE_STUTTER", + automatic: false, + durationSeconds: 15, + bufferMiB: 32, + benchmarkStartTrace: false + ) + recordGraphicsPipelineIncident( + trigger: "VISIBLE_STUTTER", + window: latestGameFrameWindow, + traceSequence: traceSequence + ) + recordCombatIncident( + trigger: "VISIBLE_STUTTER", + window: latestGameFrameWindow, + traceSequence: traceSequence + ) + } + + func endCombatBenchmark( + reason: String = "USER_ENDED", + correctnessPassed: Bool = true + ) { + guard var active = activeCombatBenchmark, let telemetry else { return } + benchmarkDeadlineTask?.cancel() + benchmarkDeadlineTask = nil + if let paths { + recordClockSync(paths: paths, telemetry: telemetry) + recordDiagnosticSnapshot(paths: paths, telemetry: telemetry, label: "combat_benchmark_end") + recordGraphicsPipelineSnapshot(paths: paths, telemetry: telemetry, label: "combat_benchmark_end") + } + if let refreshed = activeCombatBenchmark { active = refreshed } + let endedNS = DispatchTime.now().uptimeNanoseconds + let run = active.finish( + endedUTC: Self.utcNow(), + endedMonotonicNS: endedNS, + correctnessPassed: correctnessPassed + ) + activeCombatBenchmark = nil + telemetry.recordCombatBenchmark(run) + telemetry.recordEvent("COMBAT_BENCHMARK_ENDED", payload: [ + "benchmark_id": run.benchmarkID, + "reason": reason, + "preset_id": run.presetID.rawValue, + "duration_seconds": run.metrics.combatDurationSeconds, + "surface_availability": run.metrics.surfaceAvailability, + "clock_coverage": run.metrics.clockCoverage, + "weighted_fps": run.metrics.weightedFPS, + "one_percent_low_fps": run.metrics.onePercentLowFPS, + "p95_interval_ms": run.metrics.p95IntervalMilliseconds, + "p99_interval_ms": run.metrics.p99IntervalMilliseconds, + "correctness_passed": run.metrics.correctnessPassed, + "valid": run.isValid, + "invalid_reason": run.invalidReason ?? NSNull(), + "observer_overhead_invalid": run.observerOverheadInvalid + ]) + var comparisonDecision: CombatBenchmarkDecision? + do { + try labStore?.record(run) + if let comparison = try labStore?.comparisonForCandidate(run) { + comparisonDecision = comparison.analysis.decision + telemetry.recordCombatComparison(comparison) + telemetry.recordEvent("COMBAT_COMPARISON_READY", payload: [ + "comparison_id": comparison.comparisonID, + "control_benchmark_id": comparison.controlBenchmarkID, + "candidate_benchmark_id": comparison.candidateBenchmarkID, + "decision": comparison.analysis.decision.rawValue, + "weighted_fps_delta_percent": comparison.analysis.deltas.weightedFPSPercent, + "one_percent_low_delta_percent": comparison.analysis.deltas.onePercentLowFPSPercent, + "p95_delta_percent": comparison.analysis.deltas.p95IntervalPercent, + "p99_delta_percent": comparison.analysis.deltas.p99IntervalPercent, + "observer_overhead_invalid": comparison.observerOverheadInvalid + ]) + } + } catch { + telemetry.recordEvent("COMBAT_LAB_PERSISTENCE_FAILED", payload: ["error": error.localizedDescription]) + } + if run.presetID.isActiveCandidate, + comparisonDecision != .homeRun, + comparisonDecision != .promising { + TFTMACRuntimeProfile.playable.with(experimentPreset: .control).save() + let classification: String + if !run.metrics.correctnessPassed { + classification = "REJECTED_CORRECTNESS" + } else { + classification = comparisonDecision?.rawValue ?? "INCONCLUSIVE_NO_MATCHING_CONTROL" + } + telemetry.recordEvent("EXPERIMENT_AUTO_ROLLBACK", payload: [ + "failed_preset": run.presetID.rawValue, + "restored_preset": RuntimeExperimentPreset.control.rawValue, + "classification": classification, + "applies_after_restart": true + ]) + } + } + + func recordMarker(_ marker: String) { + let allowed = ["MATCH_ENTRY", "MATCH_END"] + guard allowed.contains(marker) else { return } + telemetry?.recordEvent(marker, payload: [ + "source": "native_menu", + "profile_id": profile.identifier, + "host_monotonic_timestamp": true + ]) + } + + func recordSettingsChange(previous: TFTMACRuntimeProfile, next: TFTMACRuntimeProfile) { + telemetry?.recordEvent("NEXT_LAUNCH_PROFILE_SAVED", payload: [ + "active_profile_id": profile.identifier, + "previous_saved_profile_id": previous.identifier, + "next_profile_id": next.identifier, + "next_vcpu": next.vCPU, + "next_ram_mib": next.ramMiB, + "next_refresh_hz": next.refreshHz, + "next_asg_draw_flush_interval": next.asgDrawFlushInterval, + "applies_after_restart": true + ]) + } + + func stop() async { + guard !stopping else { return } + stopping = true + if activeCombatBenchmark != nil { endCombatBenchmark(reason: "APPLICATION_STOP") } + await status("Sealing SQL telemetry and stopping Android…", false) + inputContinuation?.finish() + if let paths, + let ownedPID = discovery?.processIdentifier, + Self.processMatchesLaunchedIdentity(ownedPID, sessionMarker: expectedSessionMarker) { + telemetry?.recordEvent("EMULATOR_STOP_SIGNAL_SENT", payload: [ + "pid": ownedPID, + "serial": "emulator-5582", + "method": "adb emu kill", + "ownership_verified": true + ]) + _ = try? Self.runCommand( + paths.adb, + ["-P", "5038", "-s", "emulator-5582", "emu", "kill"], + environment: Self.adbEnvironment(paths: paths), + timeout: 15 + ) + } + } + + private func cleanup(status finalStatus: String) async { + inputContinuation?.finish() + inputContinuation = nil + var emulatorExitConfirmed = true + if let paths { + let ownedPID = discovery?.processIdentifier ?? expectedSessionMarker.flatMap(Self.findOwnedEmulatorPID) + let ownedProcessExists = ownedPID.map(Self.processExists) ?? false + let ownsRunningEmulator = ownedPID.map { + ownedProcessExists && Self.processMatchesLaunchedIdentity($0, sessionMarker: expectedSessionMarker) + } ?? false + if ownsRunningEmulator { + recordDiagnosticSnapshot(paths: paths, telemetry: telemetry, label: "session_end") + if let telemetry { + recordGraphicsPipelineSnapshot(paths: paths, telemetry: telemetry, label: "session_end") + } + } + telemetry?.endGraphicsRun(reason: "APPLICATION_STOP") + currentGamePID = nil + currentExactLayerName = nil + stopLogcatCapture() + if let ownedPID, ownsRunningEmulator { + _ = try? Self.runCommand( + paths.adb, + ["-P", "5038", "-s", "emulator-5582", "emu", "kill"], + environment: Self.adbEnvironment(paths: paths), + timeout: 15 + ) + emulatorExitConfirmed = await waitForOwnedEmulatorExit(ownedPID) + } else if let ownedPID, ownedProcessExists { + emulatorExitConfirmed = false + telemetry?.recordEvent("EMULATOR_KILL_WITHHELD", payload: [ + "pid": ownedPID, + "reason": "process no longer matches this session ownership marker" + ]) + } else if ownedPID != nil { + emulatorExitConfirmed = true + } + telemetry?.recordEvent( + emulatorExitConfirmed ? "EMULATOR_EXIT_CONFIRMED" : "EMULATOR_EXIT_TIMEOUT", + payload: ["pid": ownedPID ?? 0, "serial": "emulator-5582", "owned": ownsRunningEmulator] + ) + } + if openProcess?.isRunning == true { openProcess?.terminate() } + openProcess = nil + var avdRestoreConfirmed = avdTransaction == nil + if let transaction = avdTransaction { + do { + guard emulatorExitConfirmed else { + throw TFTMACRuntimeError("AVD restore was withheld because the emulator exit was not confirmed.") + } + guard !Self.anyEmulatorUsingSharedAVD() else { + throw TFTMACRuntimeError("AVD restore was withheld because another TFT_Ultra_Tablet process is active.") + } + try transaction.restore() + avdRestoreConfirmed = true + telemetry?.recordEvent("AVD_CONFIG_RESTORED", payload: ["sha256": transaction.originalSHA256]) + } catch { + telemetry?.recordEvent("AVD_CONFIG_RESTORE_FAILED", payload: ["error": error.localizedDescription]) + } + } + avdTransaction = nil + if let traceCaptureTask { + await traceCaptureTask.value + self.traceCaptureTask = nil + } + let sealedStatus = finalStatus == "STOPPED" && emulatorExitConfirmed && avdRestoreConfirmed ? "STOPPED" : "FAILED" + telemetry?.finish(status: sealedStatus) + runtimeLease?.release() + runtimeLease = nil + await status(sealedStatus == "STOPPED" ? "TFTMAC stopped cleanly." : "TFTMAC needs attention.", sealedStatus == "FAILED") + } + + private func startLogcatCapture(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) throws { + guard logcatProcess == nil else { return } + let outputURL = telemetry.captureDirectory.appendingPathComponent("logcat.raw.txt") + let errorURL = telemetry.captureDirectory.appendingPathComponent("logcat.stderr.log") + let sessionStartSelector = try Self.adb( + paths: paths, + ["shell", "date '+%m-%d %H:%M:%S.%3N'"], + timeout: 10 + ).output.trimmingCharacters(in: .whitespacesAndNewlines) + guard Self.firstRegexText("^([0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3})$", in: sessionStartSelector) != nil else { + throw TFTMACRuntimeError("Android did not provide a valid session boundary for local logcat capture.") + } + FileManager.default.createFile(atPath: outputURL.path, contents: nil, attributes: [.posixPermissions: 0o600]) + FileManager.default.createFile(atPath: errorURL.path, contents: nil, attributes: [.posixPermissions: 0o600]) + let outputHandle = try FileHandle(forWritingTo: outputURL) + let errorHandle = try FileHandle(forWritingTo: errorURL) + let process = Process() + process.executableURL = paths.adb + process.arguments = ["-P", "5038", "-s", "emulator-5582", "logcat", "-v", "threadtime", "-T", sessionStartSelector] + process.environment = Self.adbEnvironment(paths: paths) + process.standardOutput = outputHandle + process.standardError = errorHandle + do { + try process.run() + } catch { + try? outputHandle.close() + try? errorHandle.close() + throw error + } + logcatProcess = process + logcatOutputHandle = outputHandle + logcatErrorHandle = errorHandle + logcatReadOffset = 0 + telemetry.recordEvent("LOGCAT_CAPTURE_STARTED", payload: [ + "format": "threadtime", + "raw_file": outputURL.lastPathComponent, + "stderr_file": errorURL.lastPathComponent, + "raw_content_local_only": true, + "raw_content_sensitive": true, + "raw_content_excluded_from_sql": true, + "session_start_selector": sessionStartSelector, + "initial_backlog_lines_requested": 0, + "process_pid": process.processIdentifier + ]) + } + + private func stopLogcatCapture() { + guard let process = logcatProcess else { return } + if process.isRunning { + process.terminate() + let deadline = Date().addingTimeInterval(2) + while process.isRunning && Date() < deadline { Thread.sleep(forTimeInterval: 0.02) } + if process.isRunning { Darwin.kill(process.processIdentifier, SIGKILL) } + process.waitUntilExit() + } + try? logcatOutputHandle?.synchronize() + try? logcatErrorHandle?.synchronize() + try? logcatOutputHandle?.close() + try? logcatErrorHandle?.close() + telemetry?.recordEvent("LOGCAT_CAPTURE_STOPPED", payload: [ + "termination_status": process.terminationStatus, + "final_read_offset": String(logcatReadOffset) + ]) + logcatProcess = nil + logcatOutputHandle = nil + logcatErrorHandle = nil + } + + private func waitForOwnedEmulatorExit(_ processIdentifier: Int32) async -> Bool { + let deadline = Date().addingTimeInterval(30) + while Date() < deadline { + if !Self.processExists(processIdentifier) { return true } + try? await Task.sleep(for: .milliseconds(250)) + } + return false + } + + private func assertRuntimeUnoccupied(telemetry: TFTMACNativeTelemetry) throws { + let processOutput = (try? Self.runCommand( + URL(fileURLWithPath: "/bin/ps"), + ["-axo", "pid=,command="], + timeout: 10 + ).output) ?? "" + let emulatorConflicts = processOutput.split(whereSeparator: \.isNewline).filter { line in + line.contains("qemu-system-aarch64") + && (line.contains("@TFT_Ultra_Tablet") || line.contains("-port 5582") || line.contains("-grpc 8554")) + } + let listenerOutput = (try? Self.runCommand( + URL(fileURLWithPath: "/usr/sbin/lsof"), + ["-nP", "-iTCP:5582", "-iTCP:8554", "-sTCP:LISTEN"], + timeout: 10 + ).output) ?? "" + let listeners = listenerOutput.split(whereSeparator: \.isNewline).dropFirst() + guard emulatorConflicts.isEmpty && listeners.isEmpty else { + throw TFTMACRuntimeError("The shared TFT_Ultra_Tablet runtime or ports 5582/8554 are already in use. Close the existing emulator before launching TFTMAC.") + } + telemetry.recordEvent("RUNTIME_OWNERSHIP_PREFLIGHT_PASSED", payload: [ + "avd": "TFT_Ultra_Tablet", + "console_port": 5582, + "controller_port": 8554, + "existing_emulator_count": 0, + "existing_listener_count": 0 + ]) + } + + private func recordFrozenReceipts(telemetry: TFTMACNativeTelemetry, paths: TFTMACRuntimePaths) { + let experimentReceipt = profile.experimentConfigurationReceipt + let receipts: [(String, String, String, String)] = [ + ("engine", "Unreal Engine", "user_locked_fact", "LOCKED"), + ("runtime_profile_id", profile.identifier, "validated native preferences", "DIRECT"), + ("runtime_experiment_preset", profile.experimentPreset.rawValue, "named launch experiment", "DIRECT"), + ("runtime_configuration_sha256", experimentReceipt.sha256, "canonical effective configuration", "DIRECT"), + ("runtime_configuration_json", experimentReceipt.canonicalJSON, "canonical effective configuration", "DIRECT"), + ("launcher_method", "/usr/bin/open -n -W --env ... --args ...", "Mactician donor architecture", "DIRECT"), + ("macos_game_mode_eligible", "true", "LSSupportsGameMode bundle contract", "DIRECT"), + ("host_qos_requested", profile.experimentPreset.requestsHostLatencyQoS ? "user_interactive" : "default", "named launch experiment", "REQUESTED"), + ("adb_server_port", "5038", "known-good donor", "DIRECT"), + ("emulator_console_port", "5582", "known-good donor", "DIRECT"), + ("adb_serial", "emulator-5582", "known-good donor", "DIRECT"), + ("controller_port", "\(profile.controllerPort)", "native authenticated controller", "REQUESTED"), + ("adb_vendor_keys", "ABSENT", "launch environment contract", "DIRECT"), + ("avd", "TFT_Ultra_Tablet", "installed runtime", "DIRECT"), + ("resolution", "\(profile.width)x\(profile.height)", "5 GiB gameplay evidence", "DIRECT"), + ("density_dpi", "\(profile.densityDPI)", "5 GiB gameplay evidence", "DIRECT"), + ("refresh_hz", "\(profile.refreshHz)", "5 GiB gameplay evidence", "DIRECT"), + ("vcpu", "\(profile.vCPU)", "5 GiB gameplay evidence", "DIRECT"), + ("ram_mib", "\(profile.ramMiB)", "latest retained 5 GiB runs", "STRONG"), + ("gpu_mode", profile.gpuMode, "playable baseline", "DIRECT"), + ("audio_backend", profile.audioBackend, "audio health receipt", "DIRECT"), + ("graphics_transport_requested", profile.graphicsTransport, "playable baseline", "REQUESTED"), + ("emulator_features_requested", profile.effectiveEmulatorFeatures.joined(separator: ","), "named launch experiment", "REQUESTED"), + ("asg_write_buffer_size", "\(profile.asgWriteBufferSize)", "playable baseline", "REQUESTED"), + ("asg_write_step_size", "\(profile.asgWriteStepSize)", "playable baseline", "REQUESTED"), + ("asg_data_ring_size", "\(profile.asgDataRingSize)", "playable baseline", "REQUESTED"), + ("asg_draw_flush_interval_us", "\(profile.asgDrawFlushInterval)", "playable baseline", "REQUESTED"), + ("angle_enabled_requested", profile.angleEnabledFeatures, "playable baseline", "REQUESTED"), + ("angle_disabled_requested", profile.angleDisabledFeatures, "playable baseline", "REQUESTED"), + ("frame_transport", "raw_grpc_rgba8888", "native admission path", "DIRECT"), + ("raw_logcat_policy", "LOCAL_SENSITIVE_SESSION_SCOPED_NOT_FOR_SHARING", "privacy contract", "LOCKED"), + ("sdk_root", paths.sdkRoot.path, "filesystem discovery", "DIRECT") + ] + for receipt in receipts { + telemetry.recordReceipt(key: receipt.0, value: receipt.1, source: receipt.2, confidence: receipt.3) + } + } + + private func startADBServer(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) throws { + let result = try Self.runCommand( + paths.adb, + ["-P", "5038", "start-server"], + environment: Self.adbEnvironment(paths: paths), + timeout: 30 + ) + guard result.status == 0 else { + throw TFTMACRuntimeError("ADB server 5038 could not start: \(result.output.suffix(1200))") + } + telemetry.recordEvent("ADB_SERVER_STARTED", payload: [ + "port": 5038, + "serial": "emulator-5582", + "adb_vendor_keys_present": false, + "output": result.output.suffix(2000).description + ]) + } + + private func launchEmulatorHost(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) throws { + let stdout = telemetry.captureDirectory.appendingPathComponent("emulator.stdout.log") + let stderr = telemetry.captureDirectory.appendingPathComponent("emulator.stderr.log") + FileManager.default.createFile(atPath: stdout.path, contents: nil) + FileManager.default.createFile(atPath: stderr.path, contents: nil) + for root in Self.controllerDiscoveryRoots(paths: paths) { + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + } + expectedSessionMarker = "androidboot.tftmac.session=\(telemetry.sessionIdentifier)" + var arguments = [ + "-n", "-W", + "--env", "TFT_EMULATOR=\(paths.emulator.path)", + "--env", "TFT_ADB_SERVER_PORT=5038", + "--env", "ANDROID_ADB_SERVER_PORT=5038", + "--env", "ADB_MDNS_AUTO_CONNECT=", + "--env", "ADB_SERVER_SOCKET=", + "--env", "ANDROID_ADB_SERVER_ADDRESS=", + "--env", "ANDROID_SDK_ROOT=\(paths.sdkRoot.path)", + "--env", "ANDROID_AVD_HOME=\(paths.avdHome.path)", + "--env", "ANDROID_EMULATOR_USE_SYSTEM_LIBS=0", + "--env", "ANGLE_FEATURE_OVERRIDES_ENABLED=\(profile.angleEnabledFeatures)", + "--env", "ANGLE_FEATURE_OVERRIDES_DISABLED=\(profile.angleDisabledFeatures)", + "--env", "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS=0", + "--env", "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE=64", + "--env", "MVK_CONFIG_FAST_MATH_ENABLED=1", + "--env", "TFT_HOST_LATENCY_QOS=\(profile.experimentPreset.requestsHostLatencyQoS ? "user_interactive" : "default")", + "--env", "TFT_HOST_STDOUT=\(stdout.path)", + "--env", "TFT_HOST_STDERR=\(stderr.path)", + paths.hostApplication.path, + "--args", + "@TFT_Ultra_Tablet", "-id", "TFTMAC", "-port", "5582", + "-gpu", profile.gpuMode, "-audio", profile.audioBackend, + "-feature", profile.effectiveEmulatorFeatures.joined(separator: ","), + "-append-userspace-opt", "androidboot.opengles.version=196610", + "-append-userspace-opt", "androidboot.tftmac.graphics_profile=tftmac", + "-append-userspace-opt", expectedSessionMarker!, + "-skin", "\(profile.width)x\(profile.height)", + "-vsync-rate", "\(profile.refreshHz)", + "-dns-server", "1.1.1.1,8.8.8.8", + "-cores", "\(profile.vCPU)", "-memory", "\(profile.ramMiB)", + "-no-hidpi-scaling", "-no-snapshot", "-no-metrics", "-no-boot-anim", + "-crash-report-mode", "disabled", "-qt-hide-window", + "-grpc", "\(profile.controllerPort)", "-grpc-use-token", + "-idle-grpc-timeout", "300" + ] + if let zone = TimeZone.current.identifier.addingPercentEncoding(withAllowedCharacters: .alphanumerics), !zone.isEmpty { + arguments += ["-timezone", TimeZone.current.identifier] + } + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/open") + process.arguments = arguments + var environment = ProcessInfo.processInfo.environment + environment.removeValue(forKey: "ADB_VENDOR_KEYS") + process.environment = environment + try process.run() + openProcess = process + telemetry.recordEvent("EMULATOR_HOST_LAUNCHED", payload: [ + "method": "/usr/bin/open", + "flags": ["-n", "-W", "--env", "--args"], + "host_application": paths.hostApplication.path, + "open_pid": process.processIdentifier, + "adb_vendor_keys_present": false, + "game_mode_eligible": true, + "host_qos_requested": profile.experimentPreset.requestsHostLatencyQoS ? "user_interactive" : "default", + "controller_discovery_roots": Self.controllerDiscoveryRoots(paths: paths).map(\.path), + "emulator_arguments": Array(arguments.suffix(from: arguments.firstIndex(of: "--args") ?? arguments.startIndex).dropFirst()) + ]) + } + + private func recordHostSchedulingReceipt(telemetry: TFTMACNativeTelemetry) throws { + let outputURL = telemetry.captureDirectory.appendingPathComponent("emulator.stdout.log") + guard let output = try? String(contentsOf: outputURL, encoding: .utf8), + let receipt = HostSchedulingReceipt.parse(output) else { + throw TFTMACRuntimeError("The emulator host did not publish its macOS scheduling receipt.") + } + telemetry.recordReceipt( + key: "host_qos_pre_exec_effective", + value: receipt.effective, + source: "pthread_get_qos_class_np before emulator exec", + confidence: "DIRECT" + ) + telemetry.recordReceipt( + key: "host_qos_set_result", + value: "\(receipt.setResult)", + source: "pthread_set_qos_class_self_np", + confidence: "DIRECT" + ) + telemetry.recordEvent("HOST_SCHEDULING_RECEIPT", payload: [ + "requested": receipt.requested, + "set_result": receipt.setResult, + "pre_exec_effective": receipt.effective, + "relative_priority": receipt.relativePriority, + "qemu_child_thread_inheritance": "NOT_CLAIMED_WITHOUT_COMBAT_EVIDENCE" + ]) + if profile.experimentPreset.requestsHostLatencyQoS, !receipt.userInteractiveVerified { + throw TFTMACRuntimeError( + "Combat Latency A could not establish user-interactive scheduling at the emulator launch boundary." + ) + } + } + + private func waitForDiscovery(paths: TFTMACRuntimePaths, captureDirectory: URL, after launchStarted: Date) async throws -> EmulatorControllerDiscovery { + let logURL = captureDirectory.appendingPathComponent("emulator.stdout.log") + let deadline = Date().addingTimeInterval(90) + var candidates = [URL]() + while Date() < deadline { + try Task.checkCancellation() + if let log = try? String(contentsOf: logURL, encoding: .utf8) { + for line in log.split(whereSeparator: \.isNewline) { + guard let range = line.range(of: "Advertising in:") else { continue } + let path = line[range.upperBound...].trimmingCharacters(in: .whitespaces) + if !path.isEmpty { + let candidate = URL(fileURLWithPath: path) + if !candidates.contains(candidate) { candidates.append(candidate) } + } + } + } + for root in Self.controllerDiscoveryRoots(paths: paths) { + if let files = try? FileManager.default.contentsOfDirectory( + at: root, + includingPropertiesForKeys: [.contentModificationDateKey], + options: [.skipsHiddenFiles] + ) { + for file in files where file.lastPathComponent.hasPrefix("pid_") && file.pathExtension == "ini" { + if !candidates.contains(file) { candidates.append(file) } + } + } + } + for candidate in candidates where FileManager.default.fileExists(atPath: candidate.path) { + let modified = try? candidate.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate + guard modified == nil || modified! >= launchStarted.addingTimeInterval(-2) else { continue } + guard let text = try? String(contentsOf: candidate, encoding: .utf8) else { continue } + let values = Self.parseINI(text) + guard let rawPort = values["grpc.port"], let port = Int(rawPort), port > 0, + let token = values["grpc.token"], !token.isEmpty else { continue } + let name = candidate.deletingPathExtension().lastPathComponent + let pidText = name.dropFirst("pid_".count).prefix(while: \.isNumber) + guard let pid = Int32(pidText), Self.processMatchesLaunchedIdentity(pid, sessionMarker: expectedSessionMarker) else { continue } + return EmulatorControllerDiscovery(processIdentifier: pid, port: port, token: token, recordPath: candidate.path) + } + try await Task.sleep(for: .milliseconds(200)) + } + throw TFTMACRuntimeError("The emulator did not publish its authenticated controller endpoint.") + } + + private func waitForBootAndLaunchGame(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) async throws { + await status("Waiting for the proven ADB identity on emulator-5582…", false) + var lastState = "missing" + var previouslyLoggedState: String? + while !stopping { + try Task.checkCancellation() + let result = try? Self.runCommand( + paths.adb, + ["-P", "5038", "-s", "emulator-5582", "get-state"], + environment: Self.adbEnvironment(paths: paths), + timeout: 10 + ) + let diagnostic = result?.output.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if result?.status == 0 && diagnostic == "device" { + lastState = "device" + } else if diagnostic.localizedCaseInsensitiveContains("unauthorized") { + lastState = "unauthorized" + } else if diagnostic.localizedCaseInsensitiveContains("offline") { + lastState = "offline" + } else { + lastState = "missing" + } + if lastState != previouslyLoggedState { + telemetry.recordEvent("ADB_STATE_CHANGED", payload: [ + "previous_state": previouslyLoggedState ?? "none", + "current_state": lastState, + "serial": "emulator-5582", + "diagnostic": String(diagnostic.prefix(800)) + ]) + if lastState == "unauthorized" { + telemetry.recordEvent("ADB_UNAUTHORIZED_OBSERVED", payload: [ + "serial": "emulator-5582", + "authorization_is_user_controlled": true + ]) + } else if lastState == "offline" { + await status("Android is booting; ADB is temporarily offline…", false) + } else if lastState == "missing" { + await status("Waiting for emulator-5582 to appear on ADB 5038…", false) + } + previouslyLoggedState = lastState + } + if lastState == "device" { break } + try await Task.sleep(for: .seconds(1)) + } + try Task.checkCancellation() + guard lastState == "device" else { + throw TFTMACRuntimeError("ADB emulator-5582 did not authorize in the logged-in Mac session (last state: \(lastState)).") + } + telemetry.recordEvent("ADB_DEVICE_AUTHORIZED", payload: ["port": 5038, "serial": "emulator-5582"]) + try startLogcatCapture(paths: paths, telemetry: telemetry) + + let bootDeadline = Date().addingTimeInterval(300) + var bootCompleted = false + while Date() < bootDeadline { + try Task.checkCancellation() + let booted = try Self.adb(paths: paths, ["shell", "getprop", "sys.boot_completed"], timeout: 10).output + .trimmingCharacters(in: .whitespacesAndNewlines) + if booted == "1" { + bootCompleted = true + break + } + try await Task.sleep(for: .seconds(1)) + } + guard bootCompleted else { + throw TFTMACRuntimeError("Android did not finish booting before the five-minute deadline.") + } + try await establishGuestGameplayPower(paths: paths, telemetry: telemetry) + var manualUnlockRequired = false + while !stopping { + try Task.checkCancellation() + let user = try Self.adb(paths: paths, ["shell", "dumpsys", "user"], timeout: 15).output + if user.contains("RUNNING_UNLOCKED") { break } + if !manualUnlockRequired { + manualUnlockRequired = true + telemetry.recordEvent("GUEST_SECURE_UNLOCK_REQUIRED", payload: [ + "user": 0, + "pin_entry": "manual_only", + "credential_logged": false + ]) + } + try await Task.sleep(for: .seconds(1)) + } + try Task.checkCancellation() + telemetry.recordEvent("GUEST_UNLOCKED", payload: [ + "user": 0, + "manual_unlock_was_required": manualUnlockRequired + ]) + let package = "com.riotgames.league.teamfighttactics" + let packageDump = try Self.adb(paths: paths, ["shell", "dumpsys", "package", package], timeout: 30).output + guard packageDump.contains("Package [\(package)]") || packageDump.contains("versionName=") else { + throw TFTMACRuntimeError("Official TFT is not installed. Open Google Play in Android and install Teamfight Tactics.") + } + let installer = try? Self.adb(paths: paths, ["shell", "cmd", "package", "get-install-source", package], timeout: 15).output + tftPackageVersion = packageDump.split(whereSeparator: \.isNewline) + .first(where: { $0.contains("versionName=") }) + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + ?? "unknown" + let versionCodeLine = packageDump.split(whereSeparator: \.isNewline) + .first(where: { $0.contains("versionCode=") }) + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + ?? "unknown" + let signingLine = packageDump.split(whereSeparator: \.isNewline) + .first(where: { $0.contains("signatures=PackageSignatures") }) + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + ?? "unknown" + telemetry.recordReceipt(key: "official_tft_version", value: tftPackageVersion, source: "dumpsys package", confidence: "DIRECT") + telemetry.recordReceipt(key: "official_tft_version_code", value: versionCodeLine, source: "dumpsys package", confidence: "DIRECT") + telemetry.recordReceipt(key: "official_tft_installer", value: installer?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "unknown", source: "cmd package get-install-source", confidence: "DIRECT") + telemetry.recordReceipt(key: "official_tft_signing_receipt", value: signingLine, source: "dumpsys package", confidence: signingLine == "unknown" ? "UNKNOWN" : "DIRECT") + telemetry.recordEvent("OFFICIAL_TFT_PACKAGE_RECEIPT", payload: [ + "package": package, + "installer_output": installer?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "unknown", + "version_line": tftPackageVersion, + "version_code_line": versionCodeLine, + "signing_line": signingLine + ]) + try await Task.sleep(for: .milliseconds(750)) + guard logcatProcess?.isRunning == true, + Self.fileSize(telemetry.captureDirectory.appendingPathComponent("logcat.raw.txt")) > 0 else { + throw TFTMACRuntimeError("The required local logcat recorder did not become healthy before TFT launch.") + } + telemetry.recordEvent("LOGGER_HEALTH_GATE_PASSED", payload: [ + "logcat_growing": true, + "resource_sampler_active": true, + "sql_database": "TFTMAC_NATIVE_RUNTIME.sqlite" + ]) + recordDiagnosticSnapshot(paths: paths, telemetry: telemetry, label: "before_tft_launch") + + let resolved = try? Self.adb( + paths: paths, + ["shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER", package], + timeout: 20 + ).output.split(whereSeparator: \.isNewline).last.map(String.init) + var launched = false + for component in [resolved, "\(package)/com.epicgames.unreal.SplashActivity", "\(package)/com.epicgames.unreal.GameActivity"].compactMap({ $0 }) { + let result = try? Self.adb(paths: paths, ["shell", "am", "start", "-W", "-n", component], timeout: 45) + if result?.status == 0 { + launched = true + telemetry.recordEvent("TFT_LAUNCH_REQUESTED", payload: ["component": component]) + break + } + } + guard launched else { throw TFTMACRuntimeError("Android could not launch the official TFT activity.") } + telemetry.recordEvent("TFT_READY_FOR_USER", payload: [ + "engine": "Unreal Engine", + "resolution": "1920x1080", + "refresh_hz": profile.refreshHz, + "audio_backend": "coreaudio", + "profile_id": profile.identifier + ]) + telemetry.markRunning() + await status("", false) + while !stopping { + try Task.checkCancellation() + try await Task.sleep(for: .seconds(1)) + } + } + + private func establishGuestGameplayPower( + paths: TFTMACRuntimePaths, + telemetry: TFTMACNativeTelemetry + ) async throws { + _ = try Self.adb(paths: paths, ["shell", "dumpsys", "battery", "set", "ac", "1"], timeout: 10) + _ = try Self.adb( + paths: paths, + ["shell", "settings", "put", "global", "stay_on_while_plugged_in", "7"], + timeout: 10 + ) + _ = try Self.adb(paths: paths, ["shell", "input", "keyevent", "KEYCODE_WAKEUP"], timeout: 10) + + var lastState: GuestPowerState? + for _ in 0..<12 { + try Task.checkCancellation() + let output = try Self.adb(paths: paths, ["shell", "dumpsys", "power"], timeout: 15).output + lastState = GuestPowerState.parse(output) + if let state = lastState, state.isGameplayReady { + telemetry.recordReceipt( + key: "guest_gameplay_power_state", + value: "powered=true,stay_on=true,wakefulness=Awake", + source: "dumpsys battery/settings/power", + confidence: "DIRECT" + ) + telemetry.recordEvent("GUEST_GAMEPLAY_POWER_READY", payload: [ + "virtual_ac_powered": state.isPowered, + "stay_on": state.stayOn, + "wakefulness": state.wakefulness, + "prevents_secure_unlock_timeout": true + ]) + return + } + try await Task.sleep(for: .milliseconds(250)) + } + telemetry.recordEvent("GUEST_GAMEPLAY_POWER_FAILED", payload: [ + "virtual_ac_powered": lastState?.isPowered ?? false, + "stay_on": lastState?.stayOn ?? false, + "wakefulness": lastState?.wakefulness ?? "UNKNOWN" + ]) + throw TFTMACRuntimeError( + "Android did not confirm powered, stay-awake gameplay state before secure unlock." + ) + } + + private func sampleRuntime(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry, emulatorPID: Int32) async throws { + var sampleIndex = 0 + while !stopping { + try Task.checkCancellation() + let ps = try? Self.runCommand( + URL(fileURLWithPath: "/bin/ps"), + ["-o", "%cpu=,rss=", "-p", "\(emulatorPID)"], + timeout: 10 + ).output.trimmingCharacters(in: .whitespacesAndNewlines) + let pieces = ps?.split(whereSeparator: \.isWhitespace) ?? [] + let cpu = pieces.first.flatMap { Double($0) } + let rss = pieces.dropFirst().first.flatMap { Int64($0) } + var gamePID = currentGamePID + do { + gamePID = try Self.readTFTProcessID(paths: paths) + observeGameProcess( + gamePID, + paths: paths, + telemetry: telemetry, + observer: "FIVE_SECOND_RUNTIME_SAMPLER" + ) + } catch { + telemetry.recordEvent("TFT_PROCESS_OBSERVER_UNAVAILABLE", payload: [ + "observer": "FIVE_SECOND_RUNTIME_SAMPLER", + "state_changed": false, + "error": error.localizedDescription + ]) + } + let activity = try? Self.adb(paths: paths, ["shell", "dumpsys", "activity", "activities"], timeout: 15).output + .split(whereSeparator: \.isNewline) + .first(where: { $0.contains("mResumedActivity") || $0.contains("topResumedActivity") }) + .map(String.init) ?? "unknown" + telemetry.recordResourceSample( + emulatorPID: emulatorPID, + emulatorCPU: cpu, + emulatorRSSKiB: rss, + gamePID: gamePID, + topActivity: activity ?? "unknown" + ) + if let memoryOutput = try? Self.adb(paths: paths, ["shell", "cat", "/proc/meminfo"], timeout: 10).output, + let memory = Self.parseGuestMemory(memoryOutput) { + telemetry.recordGuestMemory(memory) + } + telemetry.recordHostResource(Self.collectHostResourceSample()) + let logcatURL = telemetry.captureDirectory.appendingPathComponent("logcat.raw.txt") + if let aggregate = Self.readLogcatAggregate(url: logcatURL, from: &logcatReadOffset) { + telemetry.recordLogcatAggregate(aggregate) + if aggregate.anrCount > 0 || aggregate.inputTimeoutCount > 0 + || aggregate.fatalCount > 0 || aggregate.memoryKillCount > 0 + || aggregate.angleWarningCount > 0 || aggregate.vulkanWarningCount > 0 + || aggregate.audioErrorCount > 0 { + telemetry.recordEvent("ANDROID_RUNTIME_INCIDENTS", payload: [ + "anr_count": aggregate.anrCount, + "input_timeout_count": aggregate.inputTimeoutCount, + "fatal_count": aggregate.fatalCount, + "memory_kill_count": aggregate.memoryKillCount, + "angle_warning_count": aggregate.angleWarningCount, + "vulkan_warning_count": aggregate.vulkanWarningCount, + "audio_error_count": aggregate.audioErrorCount, + "raw_log_byte_start": String(aggregate.byteStart), + "raw_log_byte_end": String(aggregate.byteEnd) + ]) + } + } + let emulatorStdout = telemetry.captureDirectory.appendingPathComponent("emulator.stdout.log") + if let aggregate = Self.readPipelineLogAggregate( + url: emulatorStdout, + sourceStream: "emulator_stdout", + from: &emulatorStdoutReadOffset + ) { + telemetry.recordPipelineLogAggregate(aggregate) + if aggregate.signals.gfxstreamWarningCount > 0 || aggregate.signals.asgStallCount > 0 + || aggregate.signals.vulkanErrorCount > 0 || aggregate.signals.moltenVKWarningCount > 0 + || aggregate.signals.shaderErrorCount > 0 || aggregate.signals.fenceTimeoutCount > 0 { + telemetry.recordEvent("GRAPHICS_PIPELINE_INCIDENTS", payload: [ + "source_stream": aggregate.sourceStream, + "gfxstream_warning_count": aggregate.signals.gfxstreamWarningCount, + "asg_stall_count": aggregate.signals.asgStallCount, + "vulkan_error_count": aggregate.signals.vulkanErrorCount, + "moltenvk_warning_count": aggregate.signals.moltenVKWarningCount, + "shader_error_count": aggregate.signals.shaderErrorCount, + "fence_timeout_count": aggregate.signals.fenceTimeoutCount + ]) + } + } + let emulatorStderr = telemetry.captureDirectory.appendingPathComponent("emulator.stderr.log") + if let aggregate = Self.readPipelineLogAggregate( + url: emulatorStderr, + sourceStream: "emulator_stderr", + from: &emulatorStderrReadOffset + ) { + telemetry.recordPipelineLogAggregate(aggregate) + if aggregate.signals.gfxstreamWarningCount > 0 || aggregate.signals.asgStallCount > 0 + || aggregate.signals.vulkanErrorCount > 0 || aggregate.signals.moltenVKWarningCount > 0 + || aggregate.signals.shaderErrorCount > 0 || aggregate.signals.fenceTimeoutCount > 0 { + telemetry.recordEvent("GRAPHICS_PIPELINE_INCIDENTS", payload: [ + "source_stream": aggregate.sourceStream, + "gfxstream_warning_count": aggregate.signals.gfxstreamWarningCount, + "asg_stall_count": aggregate.signals.asgStallCount, + "vulkan_error_count": aggregate.signals.vulkanErrorCount, + "moltenvk_warning_count": aggregate.signals.moltenVKWarningCount, + "shader_error_count": aggregate.signals.shaderErrorCount, + "fence_timeout_count": aggregate.signals.fenceTimeoutCount + ]) + } + } + if sampleIndex.isMultiple(of: 6) { + recordClockSync(paths: paths, telemetry: telemetry) + recordThirtySecondRuntimeReceipt(paths: paths, telemetry: telemetry) + if gamePID != nil { + recordDiagnosticSnapshot(paths: paths, telemetry: telemetry, label: "gameplay_periodic") + recordGraphicsPipelineSnapshot(paths: paths, telemetry: telemetry, label: "gameplay_periodic") + } + } + sampleIndex += 1 + try await Task.sleep(for: .seconds(5)) + } + } + + private func observeGameProcess( + _ observedGamePID: Int32?, + paths: TFTMACRuntimePaths, + telemetry: TFTMACNativeTelemetry, + observer: String + ) { + guard observedGamePID != currentGamePID else { return } + let previousGamePID = currentGamePID + let previousValue: Any = previousGamePID.map { NSNumber(value: $0) } ?? NSNull() + let currentValue: Any = observedGamePID.map { NSNumber(value: $0) } ?? NSNull() + + if previousGamePID != nil { + recordGraphicsPipelineSnapshot( + paths: paths, + telemetry: telemetry, + label: observedGamePID == nil ? "tft_process_ended" : "tft_process_replaced" + ) + telemetry.endGraphicsRun( + reason: observedGamePID == nil ? "TFT_PROCESS_ENDED" : "TFT_PROCESS_REPLACED" + ) + telemetry.recordEvent("TFT_GRAPHICS_RUN_ENDED", payload: [ + "game_pid": previousValue, + "observer": observer, + "reason": observedGamePID == nil ? "TFT_PROCESS_ENDED" : "TFT_PROCESS_REPLACED" + ]) + currentGamePID = nil + currentExactLayerName = nil + consecutiveBadGraphicsWindows = 0 + } + + telemetry.recordEvent( + observedGamePID == nil ? "TFT_PROCESS_ENDED" : + (previousGamePID == nil ? "TFT_PROCESS_STARTED" : "TFT_PROCESS_REPLACED"), + payload: [ + "previous_pid": previousValue, + "current_pid": currentValue, + "observer": observer, + "graphics_logger_automatic": true + ] + ) + telemetry.recordGameProcessTransition(previousPID: previousGamePID, currentPID: observedGamePID) + + if let observedGamePID { + currentGamePID = observedGamePID + graphicsAutomaticTraceCount = 0 + graphicsIncidentTraceCount = 0 + lastGraphicsAutomaticTraceNS = 0 + telemetry.beginOrUpdateGraphicsRun( + gamePID: observedGamePID, + exactLayerName: currentExactLayerName, + reason: previousGamePID == nil ? "TFT_PROCESS_STARTED" : "TFT_PROCESS_REPLACED" + ) + telemetry.recordEvent("TFT_GRAPHICS_RUN_STARTED", payload: [ + "game_pid": observedGamePID, + "start_trigger": "PROCESS_OBSERVED", + "observer": observer, + "manual_start_required": false, + "target_fps": profile.refreshHz + ]) + recordGraphicsPipelineSnapshot( + paths: paths, + telemetry: telemetry, + label: previousGamePID == nil ? "tft_process_started" : "tft_process_replaced_started" + ) + } + } + + private func sampleGameFrames(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) async throws { + var sampler = GameFrameTelemetrySampler() + var lastBoundaryNS = DispatchTime.now().uptimeNanoseconds + var lastCollectorErrorEventNS: UInt64 = 0 + var previousExactLayerName: String? + + while !stopping { + try Task.checkCancellation() + let observedNS = DispatchTime.now().uptimeNanoseconds + do { + let observedGamePID = try Self.readTFTProcessID(paths: paths) + observeGameProcess( + observedGamePID, + paths: paths, + telemetry: telemetry, + observer: "ONE_SECOND_GRAPHICS_SAMPLER" + ) + let layers = try Self.adb( + paths: paths, + ["shell", "dumpsys", "SurfaceFlinger", "--list"], + timeout: 10 + ).output + let layerStatus = sampler.updateLayerList(layers) + guard case .available = layerStatus, let layer = sampler.selectedLayer else { + consecutiveBadGraphicsWindows = 0 + if let lostLayer = currentExactLayerName { + telemetry.recordEvent("TFT_SURFACE_LAYER_LOST", payload: [ + "previous_layer": lostLayer, + "graphics_run_remains_open_until_process_exit": true + ]) + currentExactLayerName = nil + telemetry.updateGraphicsRunLayer(nil) + } + let window = Self.unavailableGameFrameWindow( + status: layerStatus, + layerName: nil, + startedNS: lastBoundaryNS, + endedNS: observedNS + ) + telemetry.recordGameFrameWindow(window) + await gameFrame(window) + latestGameFrameWindow = window + ingestCombatFrameUpdate(nil, window: window) + lastBoundaryNS = observedNS + try await Task.sleep(for: .seconds(1)) + continue + } + if let previousExactLayerName, previousExactLayerName != layer { + telemetry.recordEvent("TFT_SURFACE_LAYER_REPLACED", payload: [ + "previous_layer": previousExactLayerName, + "current_layer": layer, + "benchmark_active": activeCombatBenchmark != nil + ]) + } + if currentExactLayerName != layer { + let isFirstObservedLayer = currentExactLayerName == nil + currentExactLayerName = layer + telemetry.beginOrUpdateGraphicsRun( + gamePID: currentGamePID, + exactLayerName: layer, + reason: currentGamePID == nil ? "TFT_LAYER_OBSERVED" : "TFT_PROCESS_LAYER_ACTIVE" + ) + telemetry.recordEvent( + isFirstObservedLayer ? "TFT_SURFACE_LAYER_ACTIVE" : "TFT_SURFACE_LAYER_REPLACED", + payload: [ + "layer": layer, + "game_pid": currentGamePID.map { NSNumber(value: $0) } ?? NSNull(), + "manual_logger_start_required": false + ] + ) + recordGraphicsPipelineSnapshot( + paths: paths, + telemetry: telemetry, + label: isFirstObservedLayer ? "tft_surface_active" : "tft_surface_replaced" + ) + } + previousExactLayerName = layer + + let latency = try Self.adb( + paths: paths, + ["shell", GameFrameTelemetry.surfaceFlingerLatencyShellCommand(layerName: layer)], + timeout: 10 + ).output + let update = sampler.ingestLatency( + latency, + observedMonotonicNS: DispatchTime.now().uptimeNanoseconds + ) + telemetry.recordGameFrameUpdate( + update, + layerName: update.layerName, + refreshPeriodNS: update.refreshPeriodNS + ) + if let window = update.window { + await gameFrame(window) + latestGameFrameWindow = window + ingestCombatFrameUpdate(update, window: window) + lastBoundaryNS = window.endedMonotonicNS + let lowFPSDegradation = window.frameCount >= 10 + && (window.onePercentLowFPS ?? window.effectiveFPS) < 30 + let severeDegradation = window.severeCount > 0 || (window.p99MS ?? 0) >= 50 + if currentGamePID != nil, + currentExactLayerName == layer, + (lowFPSDegradation || severeDegradation) { + consecutiveBadGraphicsWindows += 1 + } else { + consecutiveBadGraphicsWindows = 0 + } + if consecutiveBadGraphicsWindows >= 2 { + let traceSequence = requestDiagnosticTrace( + scope: activeCombatBenchmark == nil ? .automaticGraphics : .combatBenchmark, + trigger: "AUTO_GAME_FRAME_DEGRADATION", + automatic: true, + durationSeconds: 15, + bufferMiB: 32, + benchmarkStartTrace: false + ) + consecutiveBadGraphicsWindows = 0 + telemetry.recordEvent("GAME_FRAME_DEGRADATION", payload: [ + "evidence_level": "SURFACEFLINGER_ACTUAL_PRESENT", + "effective_fps": window.effectiveFPS, + "one_percent_low_fps": window.onePercentLowFPS ?? NSNull(), + "p99_interval_ms": window.p99MS ?? NSNull(), + "maximum_interval_ms": window.maximumMS ?? NSNull(), + "jank_count": window.jankCount, + "severe_count": window.severeCount, + "missed_vsync_equivalents": window.missedVsyncEquivalents, + "trace_sequence": traceSequence.map { NSNumber(value: $0) } ?? NSNull(), + "first_observed_divergent_boundary": "TFT_SURFACE_ACTUAL_PRESENT", + "cause": "UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE" + ]) + recordGraphicsPipelineIncident( + trigger: "AUTO_GAME_FRAME_DEGRADATION", + window: window, + traceSequence: traceSequence + ) + recordCombatIncident( + trigger: "AUTO_GAME_FRAME_DEGRADATION", + window: window, + traceSequence: traceSequence + ) + } + } else if case .unavailable = update.status { + consecutiveBadGraphicsWindows = 0 + let unavailable = Self.unavailableGameFrameWindow( + status: update.status, + layerName: update.layerName, + startedNS: lastBoundaryNS, + endedNS: DispatchTime.now().uptimeNanoseconds + ) + telemetry.recordGameFrameWindow(unavailable) + await gameFrame(unavailable) + latestGameFrameWindow = unavailable + ingestCombatFrameUpdate(nil, window: unavailable) + lastBoundaryNS = unavailable.endedMonotonicNS + } + } catch is CancellationError { + throw CancellationError() + } catch { + consecutiveBadGraphicsWindows = 0 + let failedAt = DispatchTime.now().uptimeNanoseconds + let unavailable = Self.unavailableGameFrameWindow( + status: .unavailable(.adbError), + layerName: sampler.selectedLayer, + startedNS: lastBoundaryNS, + endedNS: failedAt + ) + telemetry.recordGameFrameWindow(unavailable) + latestGameFrameWindow = unavailable + ingestCombatFrameUpdate(nil, window: unavailable) + if lastCollectorErrorEventNS == 0 + || failedAt &- lastCollectorErrorEventNS >= 30_000_000_000 { + lastCollectorErrorEventNS = failedAt + telemetry.recordEvent("GAME_FRAME_COLLECTOR_UNAVAILABLE", payload: [ + "reason": "ADB_ERROR", + "error": error.localizedDescription, + "event_rate_limit_seconds": 30 + ]) + } + await gameFrame(unavailable) + lastBoundaryNS = failedAt + } + try await Task.sleep(for: .seconds(1)) + } + } + + nonisolated private static func unavailableGameFrameWindow( + status: GameFrameTelemetryStatus, + layerName: String?, + startedNS: UInt64, + endedNS: UInt64 + ) -> GameFrameTelemetryWindow { + GameFrameTelemetryWindow( + status: status, + layerName: layerName, + refreshPeriodNS: nil, + startedMonotonicNS: startedNS, + endedMonotonicNS: max(startedNS, endedNS), + frameCount: 0, + effectiveFPS: 0, + p50MS: nil, + p95MS: nil, + p99MS: nil, + maximumMS: nil, + onePercentLowFPS: nil, + jankCount: 0, + severeCount: 0, + missedVsyncEquivalents: 0, + historyTruncated: false + ) + } + + private func ingestCombatFrameUpdate( + _ update: GameFrameTelemetryUpdate?, + window: GameFrameTelemetryWindow + ) { + guard var active = activeCombatBenchmark else { return } + let traceActive: Bool + if let traceStart = traceCaptureMeasurementStartNS, + let traceEnd = traceCaptureMeasurementEndNS { + traceActive = window.endedMonotonicNS > traceStart + && window.startedMonotonicNS < traceEnd + } else { + traceActive = false + } + active.ingest(update: update, window: window, traceActive: traceActive) + activeCombatBenchmark = active + } + + private func recordCombatIncident( + trigger: String, + window: GameFrameTelemetryWindow?, + traceSequence: Int? + ) { + guard let active = activeCombatBenchmark, let telemetry else { return } + let incident = CombatIncidentRecord( + incidentID: UUID().uuidString.lowercased(), + benchmarkID: active.benchmarkID, + sessionID: active.sessionID, + presetID: active.presetID, + trigger: trigger, + observedMonotonicNS: DispatchTime.now().uptimeNanoseconds, + effectiveFPS: window?.effectiveFPS, + onePercentLowFPS: window?.onePercentLowFPS, + p99IntervalMilliseconds: window?.p99MS, + severeCount: window?.severeCount ?? 0, + traceSequence: traceSequence, + firstDivergentBoundary: "UNKNOWN_PENDING_TRACE_CORRELATION", + confidence: "UNKNOWN", + explicitUnknowns: "ASG_VS_GFXSTREAM_VS_MOLTENVK_OWNERSHIP_REQUIRES_FRAME_ID_RING" + ) + telemetry.recordCombatIncident(incident) + do { try labStore?.record(incident) } + catch { telemetry.recordEvent("COMBAT_INCIDENT_PERSISTENCE_FAILED", payload: ["error": error.localizedDescription]) } + } + + private func recordGraphicsPipelineIncident( + trigger: String, + window: GameFrameTelemetryWindow?, + traceSequence: Int? + ) { + guard let telemetry, let window else { return } + telemetry.recordGraphicsPipelineIncident(GraphicsPipelineIncident( + incidentID: UUID().uuidString.lowercased(), + trigger: trigger, + observedMonotonicNS: DispatchTime.now().uptimeNanoseconds, + window: window, + traceSequence: traceSequence, + firstObservedDivergentBoundary: "TFT_SURFACE_ACTUAL_PRESENT", + causalOwner: "UNKNOWN_UPSTREAM_OF_OR_AT_GUEST_SURFACE", + causalConfidence: "LOW", + explicitUnknowns: [ + "UNREAL_RENDER_THREAD_TIMING", + "ANGLE_SUBMIT_TIMING", + "ASG_QUEUE_DEPTH", + "GFXSTREAM_HOST_RECEIVE_AND_SUBMIT_TIMING", + "MOLTENVK_COMMAND_BUFFER_TIMING", + "SHARED_CROSS_STACK_FRAME_ID" + ] + )) + } + + private func recordCorrectnessRejection(reason: String) { + guard let telemetry else { return } + let nowNS = DispatchTime.now().uptimeNanoseconds + let receipt = profile.experimentConfigurationReceipt + let metrics = CombatBenchmarkMetrics( + combatDurationSeconds: 0, + surfaceAvailability: 0, + clockCoverage: 0, + p95ClockRoundTripMilliseconds: 0, + frameHistoryTruncated: false, + exactLayerStable: false, + correctnessPassed: false, + weightedFPS: 0, + onePercentLowFPS: 0, + p95IntervalMilliseconds: 0, + p99IntervalMilliseconds: 0, + jankRate: 0, + severeRate: 0, + missedVsyncRate: 0 + ) + let run = CombatBenchmarkRun( + benchmarkID: "correctness-\(UUID().uuidString.lowercased())", + sessionID: telemetry.sessionIdentifier, + presetID: profile.experimentPreset, + configurationSHA256: receipt.sha256, + comparisonIdentitySHA256: profile.comparisonConfigurationSHA256, + configurationJSON: receipt.canonicalJSON, + tftPackageVersion: tftPackageVersion, + performanceModeConfirmed: false, + startedUTC: Self.utcNow(), + endedUTC: Self.utcNow(), + startedMonotonicNS: nowNS, + endedMonotonicNS: nowNS, + exactLayerIdentity: nil, + metrics: metrics, + p50IntervalMilliseconds: 0, + maximumIntervalMilliseconds: 0, + observerOverheadInvalid: false + ) + telemetry.recordCombatBenchmark(run) + telemetry.recordEvent("REJECTED_CORRECTNESS", payload: [ + "preset_id": profile.experimentPreset.rawValue, + "configuration_sha256": receipt.sha256, + "reason": reason + ]) + try? labStore?.record(run) + } + + @discardableResult + private func requestDiagnosticTrace( + scope: DiagnosticTraceScope, + trigger: String, + automatic: Bool, + durationSeconds: Int, + bufferMiB: Int, + benchmarkStartTrace: Bool + ) -> Int? { + guard !stopping, let paths, let telemetry else { return nil } + let now = DispatchTime.now().uptimeNanoseconds + switch scope { + case .combatBenchmark: + guard activeCombatBenchmark != nil else { return nil } + case .automaticGraphics: + guard currentGamePID != nil, currentExactLayerName != nil else { + telemetry.recordEvent("DIAGNOSTIC_TRACE_SKIPPED", payload: [ + "scope": scope.rawValue, + "trigger": trigger, + "reason": "NO_ACTIVE_TFT_GRAPHICS_RUN" + ]) + return nil + } + } + if traceCaptureInProgress { + telemetry.recordEvent("DIAGNOSTIC_TRACE_SKIPPED", payload: [ + "scope": scope.rawValue, + "trigger": trigger, + "reason": "TRACE_ALREADY_RUNNING" + ]) + return nil + } + switch scope { + case .combatBenchmark: + if !benchmarkStartTrace, incidentTraceCount >= 2 { + telemetry.recordEvent("DIAGNOSTIC_TRACE_SKIPPED", payload: [ + "scope": scope.rawValue, + "trigger": trigger, "reason": "BENCHMARK_INCIDENT_TRACE_LIMIT", "limit": 2 + ]) + return nil + } + if automatic { + guard automaticTraceCount < 2 else { return nil } + guard lastAutomaticTraceNS == 0 || now &- lastAutomaticTraceNS >= 120_000_000_000 else { return nil } + automaticTraceCount += 1 + lastAutomaticTraceNS = now + } + if !benchmarkStartTrace { incidentTraceCount += 1 } + case .automaticGraphics: + guard !benchmarkStartTrace else { return nil } + if graphicsIncidentTraceCount >= 2 { + telemetry.recordEvent("DIAGNOSTIC_TRACE_SKIPPED", payload: [ + "scope": scope.rawValue, + "trigger": trigger, "reason": "GRAPHICS_RUN_INCIDENT_TRACE_LIMIT", "limit": 2 + ]) + return nil + } + if automatic { + guard graphicsAutomaticTraceCount < 2 else { return nil } + guard lastGraphicsAutomaticTraceNS == 0 + || now &- lastGraphicsAutomaticTraceNS >= 120_000_000_000 else { return nil } + graphicsAutomaticTraceCount += 1 + lastGraphicsAutomaticTraceNS = now + } + graphicsIncidentTraceCount += 1 + } + traceCaptureInProgress = true + traceCaptureMeasurementStartNS = now + traceCaptureMeasurementEndNS = now &+ UInt64(durationSeconds) * 1_000_000_000 + traceCaptureCount += 1 + let sequence = traceCaptureCount + let graphicsContext = telemetry.currentGraphicsContext() + telemetry.recordEvent("DIAGNOSTIC_TRACE_STARTED", payload: [ + "scope": scope.rawValue, + "trigger": trigger, + "duration_seconds": durationSeconds, + "capture_started_monotonic_ns": now, + "capture_ends_monotonic_ns": traceCaptureMeasurementEndNS ?? now, + "sequence": sequence, + "buffer_mib": bufferMiB, + "analysis_state": "RAW_CAPTURE_PENDING" + ]) + + traceCaptureTask = Task.detached(priority: .utility) { [paths, telemetry] in + do { + let artifact = try Self.capturePerfettoTrace( + paths: paths, + telemetry: telemetry, + graphicsRunID: graphicsContext.runID, + graphicsStackSHA256: graphicsContext.stackSHA256, + captureScope: scope.rawValue, + trigger: trigger, + sequence: sequence, + durationSeconds: durationSeconds, + bufferMiB: bufferMiB + ) + await self.finishDiagnosticTrace( + artifact: artifact, + errorDescription: nil, + scope: scope.rawValue, + trigger: trigger + ) + } catch { + await self.finishDiagnosticTrace( + artifact: nil, + errorDescription: error.localizedDescription, + scope: scope.rawValue, + trigger: trigger + ) + } + } + return sequence + } + + private func finishDiagnosticTrace( + artifact: DiagnosticArtifact?, + errorDescription: String?, + scope: String, + trigger: String + ) { + traceCaptureInProgress = false + traceCaptureTask = nil + if let artifact { + telemetry?.recordDiagnosticArtifact(artifact) + telemetry?.recordEvent("DIAGNOSTIC_TRACE_COMPLETED", payload: [ + "scope": artifact.captureScope, + "trigger": artifact.trigger, + "relative_path": artifact.relativePath, + "byte_count": artifact.byteCount, + "sha256": artifact.sha256, + "analysis_state": artifact.analysisState, + "normalized_relative_path": artifact.normalizedRelativePath, + "normalized_sha256": artifact.normalizedSHA256, + "trace_processor_sha256": artifact.traceProcessorSHA256 + ]) + } else { + traceCaptureMeasurementEndNS = DispatchTime.now().uptimeNanoseconds + telemetry?.recordEvent("DIAGNOSTIC_TRACE_FAILED", payload: [ + "scope": scope, + "trigger": trigger, + "error": errorDescription ?? "unknown", + "raw_capture_available": false + ]) + } + } + + nonisolated private static func capturePerfettoTrace( + paths: TFTMACRuntimePaths, + telemetry: TFTMACNativeTelemetry, + graphicsRunID: String?, + graphicsStackSHA256: String?, + captureScope: String, + trigger: String, + sequence: Int, + durationSeconds: Int, + bufferMiB: Int + ) throws -> DiagnosticArtifact { + let manager = FileManager.default + let traceDirectory = telemetry.captureDirectory.appendingPathComponent("perfetto", isDirectory: true) + try manager.createDirectory(at: traceDirectory, withIntermediateDirectories: true) + let safeTrigger = trigger.lowercased().map { character -> Character in + character.isLetter || character.isNumber || character == "-" || character == "_" ? character : "-" + } + let label = String(safeTrigger).trimmingCharacters(in: CharacterSet(charactersIn: "-")) + let stamp = ISO8601DateFormatter().string(from: Date()) + .replacingOccurrences(of: ":", with: "-") + let fileName = "native-\(label.isEmpty ? "trace" : label)-\(sequence)-\(stamp).pftrace" + let hostURL = traceDirectory.appendingPathComponent(fileName) + let metadataURL = hostURL.appendingPathExtension("json") + let remotePath = "/data/misc/perfetto-traces/tftmac-native-\(UUID().uuidString).pftrace" + let durationMS = max(1_000, durationSeconds * 1_000) + let config = """ + buffers { size_kb: \(max(1, bufferMiB) * 1024) fill_policy: RING_BUFFER } + data_sources { config { name: "android.surfaceflinger.frame" target_buffer: 0 } } + data_sources { config { name: "android.surfaceflinger.frametimeline" target_buffer: 0 } } + data_sources { config { name: "android.surfaceflinger.layers" target_buffer: 0 } } + data_sources { config { name: "android.gpu.memory" target_buffer: 0 } } + data_sources { config { name: "linux.process_stats" target_buffer: 0 process_stats_config { scan_all_processes_on_start: true } } } + data_sources { config { name: "linux.sys_stats" target_buffer: 0 sys_stats_config { meminfo_period_ms: 1000 stat_period_ms: 1000 } } } + data_sources { + config { + name: "linux.ftrace" + target_buffer: 0 + ftrace_config { + ftrace_events: "sched/sched_switch" + ftrace_events: "sched/sched_wakeup" + ftrace_events: "sched/sched_waking" + ftrace_events: "power/cpu_frequency" + atrace_apps: "com.riotgames.league.teamfighttactics" + } + } + } + duration_ms: \(durationMS) + """ + let trace = try runCommand( + paths.adb, + ["-P", "5038", "-s", "emulator-5582", "shell", "perfetto", "--txt", "-c", "-", "-o", remotePath], + environment: adbEnvironment(paths: paths), + input: Data(config.utf8), + timeout: TimeInterval(durationSeconds + 30) + ) + guard trace.status == 0 else { + _ = try? adb(paths: paths, ["shell", "rm", "-f", remotePath], timeout: 10) + throw TFTMACRuntimeError("Perfetto capture failed: \(trace.output.suffix(1200))") + } + defer { _ = try? adb(paths: paths, ["shell", "rm", "-f", remotePath], timeout: 10) } + _ = try adb(paths: paths, ["pull", remotePath, hostURL.path], timeout: 120) + let data = try Data(contentsOf: hostURL) + guard !data.isEmpty else { throw TFTMACRuntimeError("Perfetto returned an empty trace.") } + let digest = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + let normalized: (url: URL, data: Data, sha256: String, processorSHA256: String) + do { + normalized = try normalizePerfettoTrace(hostURL: hostURL) + } catch { + try? manager.removeItem(at: hostURL) + throw TFTMACRuntimeError("Perfetto trace normalization failed and the unprocessed raw trace was removed: \(error.localizedDescription)") + } + let createdUTC = ISO8601DateFormatter().string(from: Date()) + let metadata: [String: Any] = [ + "schema": 1, + "created_utc": createdUTC, + "capture_scope": captureScope, + "graphics_run_id": graphicsRunID ?? NSNull(), + "graphics_stack_sha256": graphicsStackSHA256 ?? NSNull(), + "trigger": trigger, + "duration_seconds": durationSeconds, + "buffer_mib": bufferMiB, + "trace_file_name": fileName, + "byte_count": data.count, + "sha256": digest, + "normalized_file_name": normalized.url.lastPathComponent, + "normalized_sha256": normalized.sha256, + "trace_processor_version": "58.2", + "trace_processor_sha256": normalized.processorSHA256, + "data_sources": [ + "android.surfaceflinger.frame", + "android.surfaceflinger.frametimeline", + "android.surfaceflinger.layers", + "android.gpu.memory", + "linux.process_stats", + "linux.sys_stats", + "linux.ftrace:sched_switch,sched_wakeup,sched_waking,power/cpu_frequency" + ], + "analysis_state": "NORMALIZED_TRACE_PROCESSOR_V58_2" + ] + let metadataData = try JSONSerialization.data( + withJSONObject: metadata, + options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] + ) + try metadataData.write(to: metadataURL, options: .atomic) + return DiagnosticArtifact( + graphicsRunID: graphicsRunID, + graphicsStackSHA256: graphicsStackSHA256, + captureScope: captureScope, + createdUTC: createdUTC, + createdMonotonicNS: DispatchTime.now().uptimeNanoseconds, + kind: "PERFETTO_FRAME_PIPELINE_TRACE", + trigger: trigger, + relativePath: "perfetto/\(fileName)", + byteCount: Int64(data.count), + sha256: digest, + analysisState: "NORMALIZED_TRACE_PROCESSOR_V58_2", + normalizedRelativePath: "perfetto/\(normalized.url.lastPathComponent)", + normalizedSHA256: normalized.sha256, + normalizedSummaryCSV: String(decoding: normalized.data, as: UTF8.self), + traceProcessorSHA256: normalized.processorSHA256 + ) + } + + nonisolated private static func normalizePerfettoTrace( + hostURL: URL + ) throws -> (url: URL, data: Data, sha256: String, processorSHA256: String) { + let expectedProcessorSHA = "d29864d1ba3b36855527bb1b0ca3aa7f703cdce338b9680bb922c5c151b358fa" + guard let resourceURL = Bundle.main.resourceURL else { + throw TFTMACRuntimeError("TFTMAC has no resource directory for the pinned trace processor.") + } + let processorURL = resourceURL.appendingPathComponent("trace_processor_shell") + guard FileManager.default.isExecutableFile(atPath: processorURL.path) else { + throw TFTMACRuntimeError("The pinned Perfetto trace_processor_shell is missing from TFTMAC.app.") + } + let processorData = try Data(contentsOf: processorURL) + let processorSHA = SHA256.hash(data: processorData).map { String(format: "%02x", $0) }.joined() + guard processorSHA == expectedProcessorSHA else { + throw TFTMACRuntimeError("The packaged Perfetto trace processor failed its SHA-256 receipt.") + } + let query = """ + SELECT + (SELECT start_ts FROM trace_bounds) AS trace_start_ns, + (SELECT end_ts FROM trace_bounds) AS trace_end_ns, + (SELECT COUNT(*) FROM process) AS process_rows, + (SELECT COUNT(*) FROM thread) AS thread_rows, + (SELECT COUNT(*) FROM sched) AS scheduler_slices, + (SELECT COUNT(*) FROM counter) AS counter_rows, + (SELECT COUNT(*) FROM slice WHERE name GLOB '*SurfaceFlinger*' OR name GLOB '*FrameTimeline*') AS surfaceflinger_slices, + (SELECT COUNT(*) FROM process WHERE name = 'com.riotgames.league.teamfighttactics') AS tft_process_rows; + """ + let result = try runCommand( + processorURL, + ["query", hostURL.path, query], + timeout: 180 + ) + guard result.status == 0 else { + throw TFTMACRuntimeError("trace_processor query failed: \(result.output.suffix(1200))") + } + let normalizedData = Data(result.output.utf8) + guard !normalizedData.isEmpty else { + throw TFTMACRuntimeError("trace_processor returned an empty normalized summary.") + } + let normalizedURL = hostURL.appendingPathExtension("normalized.csv") + try normalizedData.write(to: normalizedURL, options: .atomic) + let normalizedSHA = SHA256.hash(data: normalizedData).map { String(format: "%02x", $0) }.joined() + return (normalizedURL, normalizedData, normalizedSHA, processorSHA) + } + + private func recordClockSync(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) { + let hostT0 = DispatchTime.now().uptimeNanoseconds + guard let output = try? Self.adb(paths: paths, ["shell", "cat", "/proc/uptime"], timeout: 10).output, + let seconds = output.split(whereSeparator: \.isWhitespace).first.flatMap({ Double($0) }) else { return } + let hostT1 = DispatchTime.now().uptimeNanoseconds + let guestNS = UInt64(max(0, seconds) * 1_000_000_000) + telemetry.recordClockSync(hostT0NS: hostT0, guestUptimeNS: guestNS, hostT1NS: hostT1) + if var active = activeCombatBenchmark { + active.recordClock( + hostMidpointNS: hostT0 &+ ((hostT1 &- hostT0) / 2), + roundTripNS: hostT1 &- hostT0 + ) + activeCombatBenchmark = active + } + } + + private func recordDiagnosticSnapshot(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry?, label: String) { + guard let telemetry else { return } + if let output = try? Self.adb(paths: paths, ["shell", "dumpsys", "SurfaceFlinger"], timeout: 20).output, + let sample = Self.parseSurfaceFlinger(output) { + telemetry.recordSurfaceFlinger(sample, label: label) + } + if let output = try? Self.adb(paths: paths, ["shell", "dumpsys", "media.audio_flinger"], timeout: 20).output, + let sample = Self.parseAudioFlinger(output) { + telemetry.recordAudioFlinger(sample, label: label) + } + } + + private func recordThirtySecondRuntimeReceipt(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) { + let geometry = try? Self.adb( + paths: paths, + ["shell", "sh", "-c", "wm size; wm density; dumpsys display | grep -m 1 -E 'DisplayDeviceInfo|fps|refreshRate'"], + timeout: 15 + ).output.trimmingCharacters(in: .whitespacesAndNewlines) + let properties = try? Self.adb( + paths: paths, + ["shell", "getprop"], + timeout: 15 + ).output + telemetry.recordEvent("RUNTIME_THIRTY_SECOND_RECEIPT", payload: [ + "preset_id": profile.experimentPreset.rawValue, + "configuration_sha256": profile.experimentConfigurationReceipt.sha256, + "emulator_features_requested": profile.effectiveEmulatorFeatures, + "display_geometry": geometry ?? "unavailable", + "guest_egl": Self.firstRegexText("\\[ro.hardware.egl\\]: \\[(.*?)\\]", in: properties ?? "") ?? "unknown", + "guest_vulkan": Self.firstRegexText("\\[ro.hardware.vulkan\\]: \\[(.*?)\\]", in: properties ?? "") ?? "unknown", + "cross_boundary_attribution": "CLOCK_SYNC_GATED" + ]) + } + + private func recordGraphicsPipelineSnapshot( + paths: TFTMACRuntimePaths, + telemetry: TFTMACNativeTelemetry, + label: String + ) { + let stdout = Self.readTailText( + telemetry.captureDirectory.appendingPathComponent("emulator.stdout.log"), + maximumBytes: 16 * 1024 * 1024 + ) + let stderr = Self.readTailText( + telemetry.captureDirectory.appendingPathComponent("emulator.stderr.log"), + maximumBytes: 16 * 1024 * 1024 + ) + let emulatorText = stdout + "\n" + stderr + let lower = emulatorText.lowercased() + let layerOutput = try? Self.adb( + paths: paths, + ["shell", "dumpsys", "SurfaceFlinger", "--list"], + timeout: 10 + ).output + let surfaceState: String + let exactLayerName: String? + if let layerOutput { + switch GameFrameTelemetry.selectTFTSurfaceViewLayer(from: layerOutput) { + case .selected(let layer): + surfaceState = "EXACT_LAYER_ACTIVE" + exactLayerName = layer + case .unavailable(.multipleTFTSurfaceViews): + surfaceState = "AMBIGUOUS_MULTIPLE_LAYERS" + exactLayerName = nil + case .unavailable: + surfaceState = "NOT_OBSERVED" + exactLayerName = nil + } + } else { + surfaceState = "ADB_UNAVAILABLE" + exactLayerName = nil + } + let packageAngleInstance = Self.firstRegexText( + "(?i)(Created VkInstance:[^\\n]*application:'com\\.riotgames\\.league\\.teamfighttactics'[^\\n]*engine:'ANGLE')", + in: emulatorText + ) != nil + let unrealEngine = Self.firstRegexText( + "(?i)Created VkInstance:[^\\n]*application:'TFT'[^\\n]*engine:'(UnrealEngine[^']*)'", + in: emulatorText + ) + let gameGraphicsAPI = unrealEngine == nil ? "UNKNOWN" : "UNREAL_ENGINE_VULKAN" + let gameGraphicsAPIConfidence = unrealEngine == nil ? "UNKNOWN" : "DIRECT_VKINSTANCE_RUNTIME_LOG" + let emulatorVersion = Self.firstRegexText( + "(?i)Android emulator version\\s+([^\\s]+)", + in: emulatorText + ) + let emulatorBuildID = Self.firstRegexText( + "(?i)\\(build_id\\s+([^\\)]+)\\)", + in: emulatorText + ) + let emulatorGPUSelection = Self.firstRegexText( + "(?i)emuglConfig_init:\\s*([^\\r\\n]+)", + in: emulatorText + ) + let rawGfxstreamFeatures = Self.firstRegexText( + "(?is)Gfxstream features:\\s*(.*?)Gfxstream initialized successfully", + in: emulatorText + ) + let gfxstreamFeatureReceipt = rawGfxstreamFeatures.map { raw in + String(raw.split(whereSeparator: \.isNewline).map { + String($0) + .replacingOccurrences(of: "INFO |", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + }.filter { !$0.isEmpty }.joined(separator: ";").prefix(8_192)) + } + let moltenVKVersion = Self.firstRegexText( + "(?i)(Graphics API Version[^\\r\\n]*VK_DRIVER_ID_MOLTENVK[^\\r\\n]*)", + in: emulatorText + ) + let hostDevice = Self.firstRegexText("(?i)Selecting Vulkan device:\\s*([^\\r\\n]+)", in: emulatorText)? + .trimmingCharacters(in: .whitespacesAndNewlines) + let vulkanComposition = Self.firstRegexText("(?i)useVulkanComposition:\\s*(true|false)", in: emulatorText) + .flatMap(Self.parseBoolean) + let nativeSwapchain = Self.firstRegexText("(?i)useVulkanNativeSwapchain:\\s*(true|false)", in: emulatorText) + .flatMap(Self.parseBoolean) + let guestEGL = try? Self.adb(paths: paths, ["shell", "getprop", "ro.hardware.egl"], timeout: 10).output + let guestVulkan = try? Self.adb(paths: paths, ["shell", "getprop", "ro.hardware.vulkan"], timeout: 10).output + let angleSelection = try? Self.adb( + paths: paths, + ["shell", "settings", "get", "global", "angle_gl_driver_all_angle"], + timeout: 10 + ).output + let packageAnglePackages = try? Self.adb( + paths: paths, + ["shell", "settings", "get", "global", "angle_gl_driver_selection_pkgs"], + timeout: 10 + ).output + let packageAngleValues = try? Self.adb( + paths: paths, + ["shell", "settings", "get", "global", "angle_gl_driver_selection_values"], + timeout: 10 + ).output + let packageAngleSelection = [ + Self.nonemptyDiagnosticValue(packageAnglePackages).map { "packages=\($0)" }, + Self.nonemptyDiagnosticValue(packageAngleValues).map { "values=\($0)" } + ].compactMap { $0 }.joined(separator: ";") + let metalDevice = MTLCreateSystemDefaultDevice() + let metalDeviceName = metalDevice?.name + let metalRegistryID = metalDevice.map { String(format: "0x%016llx", $0.registryID) } + let gfxstreamActive = lower.contains("gfxstream initialized successfully") + let moltenVKActive = lower.contains("moltenvk_icd.json") + || lower.contains("vk_driver_id_moltenvk") + || lower.contains("graphics adapter vendor moltenvk") + let angleState = packageAngleInstance + ? "PACKAGE_PROCESS_ANGLE_INSTANCE_OBSERVED" + : "NOT_OBSERVED" + let moltenVKConfiguration = "{\"MVK_CONFIG_FAST_MATH_ENABLED\":\"1\",\"MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE\":\"64\",\"MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS\":\"0\",\"confidence\":\"REQUESTED_LAUNCH_ENVIRONMENT\"}" + let completeDirectPath = unrealEngine != nil + && exactLayerName != nil + && gfxstreamActive + && moltenVKActive + && hostDevice != nil + && vulkanComposition == true + && nativeSwapchain == true + && metalDevice != nil + let pipelinePath = completeDirectPath + ? "RUN_OBSERVED_COMPONENT_CHAIN:UNREAL_VULKAN->GFXSTREAM->HOST_VULKAN_MOLTENVK->METAL;OUTPUT:EMULATOR_IMAGE_STREAM->TFTMAC_METAL_PRESENTER;NOT_FRAME_CORRELATED" + : "UNKNOWN_INCOMPLETE_RUNTIME_RECEIPT" + let receipt = GraphicsStackReceipt(fields: [ + "tft_package_version": GraphicsStackReceiptField( + value: tftPackageVersion, + source: "adb dumpsys package com.riotgames.league.teamfighttactics", + confidence: tftPackageVersion == "unknown" ? "UNKNOWN" : "DIRECT" + ), + "tft_surface": GraphicsStackReceiptField( + value: exactLayerName ?? surfaceState, + source: "SurfaceFlinger --list exact GameActivity SurfaceView selection", + confidence: exactLayerName == nil ? "UNKNOWN" : "DIRECT" + ), + "unreal_engine": GraphicsStackReceiptField( + value: unrealEngine ?? "UNKNOWN", + source: "emulator VkInstance runtime log for application TFT", + confidence: unrealEngine == nil ? "UNKNOWN" : "DIRECT" + ), + "game_graphics_api": GraphicsStackReceiptField( + value: gameGraphicsAPI, + source: "emulator VkInstance runtime log for application TFT", + confidence: gameGraphicsAPIConfidence + ), + "angle": GraphicsStackReceiptField( + value: angleState, + source: "emulator VkInstance runtime log for official TFT package process", + confidence: packageAngleInstance ? "DIRECT" : "UNKNOWN" + ), + "gfxstream": GraphicsStackReceiptField( + value: gfxstreamActive ? "ACTIVE" : "UNKNOWN", + source: "emulator gfxstream initialization log", + confidence: gfxstreamActive ? "DIRECT" : "UNKNOWN" + ), + "gfxstream_features": GraphicsStackReceiptField( + value: gfxstreamFeatureReceipt ?? "UNKNOWN", + source: "emulator Gfxstream features receipt", + confidence: gfxstreamFeatureReceipt == nil ? "UNKNOWN" : "DIRECT" + ), + "gfxstream_internal_tracing": GraphicsStackReceiptField( + value: "NOT_PROVEN_IN_PACKAGED_RUNTIME", + source: "packaged runtime has no direct GFXSTREAM_BUILD_WITH_TRACING receipt", + confidence: "UNKNOWN" + ), + "moltenvk": GraphicsStackReceiptField( + value: moltenVKActive ? "ACTIVE" : "UNKNOWN", + source: "MoltenVK ICD/driver runtime log", + confidence: moltenVKActive ? "DIRECT" : "UNKNOWN" + ), + "moltenvk_version": GraphicsStackReceiptField( + value: moltenVKVersion ?? "UNKNOWN", + source: "host Vulkan driver runtime log", + confidence: moltenVKVersion == nil ? "UNKNOWN" : "DIRECT" + ), + "moltenvk_configuration": GraphicsStackReceiptField( + value: moltenVKConfiguration, + source: "TFTMAC app-host launch environment", + confidence: "REQUESTED" + ), + "host_vulkan_device": GraphicsStackReceiptField( + value: hostDevice ?? "UNKNOWN", + source: "emulator Vulkan device-selection log", + confidence: hostDevice == nil ? "UNKNOWN" : "DIRECT" + ), + "metal_device": GraphicsStackReceiptField( + value: [metalDeviceName, metalRegistryID].compactMap { $0 }.joined(separator: ";"), + source: "MTLCreateSystemDefaultDevice", + confidence: metalDevice == nil ? "UNKNOWN" : "DIRECT" + ), + "native_presenter": GraphicsStackReceiptField( + value: "TFTMAC_MTKVIEW_METAL_PRESENTER", + source: "TFTMAC native presenter implementation", + confidence: "DIRECT_SOURCE" + ), + "configuration_sha256": GraphicsStackReceiptField( + value: profile.experimentConfigurationReceipt.sha256, + source: "canonical effective runtime configuration", + confidence: "DIRECT" + ), + "pipeline_path": GraphicsStackReceiptField( + value: pipelinePath, + source: "composition of run-local component receipts; continuous edges require frame-ID correlation", + confidence: completeDirectPath ? "CORRELATED_NOT_CAUSAL" : "UNKNOWN" + ), + "cross_stack_frame_id": GraphicsStackReceiptField( + value: "NOT_IMPLEMENTED", + source: "current packaged runtime", + confidence: "UNKNOWN" + ) + ]) + telemetry.recordGraphicsPipelineSnapshot(GraphicsPipelineSnapshot( + label: label, + gamePID: currentGamePID, + exactLayerName: exactLayerName ?? currentExactLayerName, + tftSurfaceState: surfaceState, + gameGraphicsAPI: gameGraphicsAPI, + gameGraphicsAPIConfidence: gameGraphicsAPIConfidence, + angleState: angleState, + gfxstreamState: gfxstreamActive ? "PROVEN_ACTIVE" : "NOT_OBSERVED", + moltenVKState: moltenVKActive ? "PROVEN_ACTIVE" : "NOT_OBSERVED", + emulatorVersion: Self.nonemptyDiagnosticValue(emulatorVersion), + emulatorBuildID: Self.nonemptyDiagnosticValue(emulatorBuildID), + emulatorGPUSelection: Self.nonemptyDiagnosticValue(emulatorGPUSelection), + gfxstreamFeatureReceipt: Self.nonemptyDiagnosticValue(gfxstreamFeatureReceipt), + gfxstreamTracingState: "UNKNOWN_NOT_PROVEN_IN_PACKAGED_RUNTIME", + moltenVKVersion: Self.nonemptyDiagnosticValue(moltenVKVersion), + moltenVKConfiguration: moltenVKConfiguration, + hostVulkanDevice: Self.nonemptyDiagnosticValue(hostDevice), + vulkanComposition: vulkanComposition, + nativeSwapchain: nativeSwapchain, + guestEGLImplementation: Self.nonemptyDiagnosticValue(guestEGL), + guestVulkanImplementation: Self.nonemptyDiagnosticValue(guestVulkan), + globalAngleSelection: Self.nonemptyDiagnosticValue(angleSelection), + packageAngleSelection: Self.nonemptyDiagnosticValue(packageAngleSelection), + metalDeviceName: Self.nonemptyDiagnosticValue(metalDeviceName), + metalRegistryID: Self.nonemptyDiagnosticValue(metalRegistryID), + receipt: receipt + )) + } + + nonisolated private static func fileSize(_ url: URL) -> UInt64 { + guard let attributes = try? FileManager.default.attributesOfItem(atPath: url.path), + let size = attributes[.size] as? NSNumber else { return 0 } + return size.uint64Value + } + + nonisolated private static func readTailText(_ url: URL, maximumBytes: UInt64) -> String { + let end = fileSize(url) + guard end > 0, let handle = try? FileHandle(forReadingFrom: url) else { return "" } + defer { try? handle.close() } + do { + try handle.seek(toOffset: end > maximumBytes ? end - maximumBytes : 0) + let data = try handle.readToEnd() ?? Data() + return String(decoding: data, as: UTF8.self) + } catch { + return "" + } + } + + nonisolated private static func parseBoolean(_ value: String) -> Bool? { + switch value.lowercased() { + case "true", "1", "yes": return true + case "false", "0", "no": return false + default: return nil + } + } + + nonisolated private static func nonemptyDiagnosticValue(_ value: String?) -> String? { + guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), + !trimmed.isEmpty, trimmed.lowercased() != "null" else { return nil } + return String(trimmed.prefix(512)) + } + + nonisolated private static func parseGuestMemory(_ output: String) -> GuestMemorySample? { + var values = [String: Int64]() + for line in output.split(whereSeparator: \.isNewline) { + let pair = line.split(separator: ":", maxSplits: 1) + guard pair.count == 2, + let value = pair[1].split(whereSeparator: \.isWhitespace).first.flatMap({ Int64($0) }) else { continue } + values[String(pair[0])] = value + } + guard let total = values["MemTotal"], let available = values["MemAvailable"] ?? values["MemFree"] else { return nil } + return GuestMemorySample( + totalKiB: total, + availableKiB: available, + swapTotalKiB: values["SwapTotal"], + swapFreeKiB: values["SwapFree"] + ) + } + + nonisolated private static func collectHostResourceSample() -> HostResourceSample { + let vmOutput = try? runCommand( + URL(fileURLWithPath: "/usr/bin/vm_stat"), + [], + timeout: 5 + ).output + let pageSize = vmOutput.flatMap { firstRegexInt("page size of ([0-9]+) bytes", in: $0) } ?? 16_384 + func pages(_ label: String) -> Int64? { + vmOutput.flatMap { firstRegexInt("\(NSRegularExpression.escapedPattern(for: label)):\\s*([0-9]+)", in: $0) } + } + let availablePages = ["Pages free", "Pages inactive", "Pages speculative"] + .compactMap(pages) + .reduce(0, +) + let compressedPages = pages("Pages occupied by compressor") + let pageouts = pages("Pageouts") + let swapOutput = try? runCommand( + URL(fileURLWithPath: "/usr/sbin/sysctl"), + ["-n", "vm.swapusage"], + timeout: 5 + ).output + let swapUsedKiB: Int64? = swapOutput.flatMap { output in + guard let amount = firstRegexDouble("used = ([0-9.]+)", in: output), + let unit = firstRegexText("used = [0-9.]+([KMGT])", in: output) else { return nil } + let multiplier: Double + switch unit { + case "K": multiplier = 1 + case "M": multiplier = 1_024 + case "G": multiplier = 1_024 * 1_024 + case "T": multiplier = 1_024 * 1_024 * 1_024 + default: multiplier = 1 + } + return Int64(amount * multiplier) + } + let powerOutput = try? runCommand( + URL(fileURLWithPath: "/usr/bin/pmset"), + ["-g", "batt"], + timeout: 5 + ).output + let powerSource: String + if powerOutput?.contains("AC Power") == true { powerSource = "AC" } + else if powerOutput?.contains("Battery Power") == true { powerSource = "BATTERY" } + else { powerSource = "UNKNOWN" } + let thermalState: String + switch ProcessInfo.processInfo.thermalState { + case .nominal: thermalState = "NOMINAL" + case .fair: thermalState = "FAIR" + case .serious: thermalState = "SERIOUS" + case .critical: thermalState = "CRITICAL" + @unknown default: thermalState = "UNKNOWN" + } + return HostResourceSample( + availableKiB: availablePages > 0 ? (availablePages * pageSize) / 1_024 : nil, + compressedKiB: compressedPages.map { ($0 * pageSize) / 1_024 }, + swapUsedKiB: swapUsedKiB, + pageouts: pageouts, + thermalState: thermalState, + powerSource: powerSource + ) + } + + nonisolated private static func parseSurfaceFlinger(_ output: String) -> SurfaceFlingerSample? { + let renderRate = firstRegexDouble("renderRate=([0-9.]+)\\s*Hz", in: output) + let totalMissed = firstRegexInt("Total missed frame count:\\s*([0-9]+)", in: output) + let hwcMissed = firstRegexInt("HWC missed frame count:\\s*([0-9]+)", in: output) + let gpuMissed = firstRegexInt("GPU missed frame count:\\s*([0-9]+)", in: output) + let requestedRates = regexDoubles( + "com\\.riotgames\\.league\\.teamfighttactics[^\\n]*requestedFrameRate:\\s*\\{([0-9.]+)\\s*Hz", + in: output + ) + guard renderRate != nil || totalMissed != nil || hwcMissed != nil || gpuMissed != nil else { return nil } + return SurfaceFlingerSample( + renderRateHz: renderRate, + totalMissedFrames: totalMissed, + hwcMissedFrames: hwcMissed, + gpuMissedFrames: gpuMissed, + tftRequestedRateHz: requestedRates.max() + ) + } + + nonisolated private static func parseAudioFlinger(_ output: String) -> AudioFlingerSample? { + let chunks = output.components(separatedBy: "Output thread ") + guard let activeChunk = chunks.first(where: { $0.contains("Standby: no") }) else { + return output.contains("Output thread ") + ? AudioFlingerSample( + activeOutput: false, sampleRateHz: nil, stereoOutput: false, + activeTracks: 0, partialUnderruns: nil, emptyUnderruns: nil + ) + : nil + } + let sampleRate = firstRegexInt("Sample rate:\\s*([0-9]+)\\s*Hz", in: activeChunk).map(Int.init) + let channelText = firstRegexText("Channel mask:[^\\n]*\\(([^)]*)\\)", in: activeChunk)?.lowercased() ?? "" + let stereo = (channelText.contains("front-left") && channelText.contains("front-right")) + || (channelText.contains("left") && channelText.contains("right")) + let activeTracks = firstRegexInt("[0-9]+ Tracks of which ([0-9]+) are active", in: activeChunk).map(Int.init) + return AudioFlingerSample( + activeOutput: true, + sampleRateHz: sampleRate, + stereoOutput: stereo, + activeTracks: activeTracks, + partialUnderruns: firstRegexInt("underrun counters:\\s*partial=([0-9]+)", in: activeChunk), + emptyUnderruns: firstRegexInt("underrun counters:[^\\n]*empty=([0-9]+)", in: activeChunk) + ) + } + + nonisolated private static func readLogcatAggregate(url: URL, from offset: inout UInt64) -> LogcatAggregate? { + let end = fileSize(url) + guard end > offset else { return nil } + let maximumReadBytes: UInt64 = 4 * 1024 * 1024 + let requestedStart = offset + let actualStart = end - min(end - requestedStart, maximumReadBytes) + let skipped = actualStart - requestedStart + guard let handle = try? FileHandle(forReadingFrom: url) else { return nil } + defer { try? handle.close() } + do { + try handle.seek(toOffset: actualStart) + let data = try handle.read(upToCount: Int(end - actualStart)) ?? Data() + offset = end + let text = String(decoding: data, as: UTF8.self) + let lines = text.split(whereSeparator: \.isNewline) + var anr = 0 + var inputTimeout = 0 + var fatal = 0 + var memoryKill = 0 + var choreographer = 0 + var angleWarning = 0 + var vulkanWarning = 0 + var audioError = 0 + for rawLine in lines { + let line = String(rawLine) + let lower = line.lowercased() + let warningOrError = lower.contains(" warning") || lower.contains(" error") + || lower.contains(" failed") || lower.contains(" fatal") + || line.contains(" W ") || line.contains(" E ") + if lower.contains("anr in com.riotgames.league.teamfighttactics") { anr += 1 } + if lower.contains("input dispatching timed out") || lower.contains("input timeout") { inputTimeout += 1 } + if lower.contains("com.riotgames.league.teamfighttactics") + && (lower.contains("fatal exception") || lower.contains("fatal signal") || lower.contains("signal 11")) { fatal += 1 } + if TelemetrySignalClassifier.isConfirmedGuestMemoryKill(line) { memoryKill += 1 } + if lower.contains("choreographer") && lower.contains("skipped") { choreographer += 1 } + if lower.contains("angle") && warningOrError { angleWarning += 1 } + if lower.contains("vulkan") && warningOrError { vulkanWarning += 1 } + if lower.contains("pcm_writei") && (warningOrError || lower.contains("underrun")) { audioError += 1 } + } + return LogcatAggregate( + byteStart: actualStart, byteEnd: end, skippedBytes: skipped, lineCount: lines.count, + anrCount: anr, inputTimeoutCount: inputTimeout, fatalCount: fatal, + memoryKillCount: memoryKill, choreographerSkipCount: choreographer, + angleWarningCount: angleWarning, vulkanWarningCount: vulkanWarning, + audioErrorCount: audioError + ) + } catch { + return nil + } + } + + nonisolated private static func readPipelineLogAggregate( + url: URL, + sourceStream: String, + from offset: inout UInt64 + ) -> PipelineLogAggregate? { + let end = fileSize(url) + guard end > offset else { return nil } + let maximumReadBytes: UInt64 = 4 * 1024 * 1024 + let requestedStart = offset + let actualStart = end - min(end - requestedStart, maximumReadBytes) + let skipped = actualStart - requestedStart + guard let handle = try? FileHandle(forReadingFrom: url) else { return nil } + defer { try? handle.close() } + do { + try handle.seek(toOffset: actualStart) + let data = try handle.read(upToCount: Int(end - actualStart)) ?? Data() + offset = end + let lines = String(decoding: data, as: UTF8.self).split(whereSeparator: \.isNewline) + var signals = PipelineLogSignals() + for line in lines { + signals = signals + TelemetrySignalClassifier.pipelineSignals(in: String(line)) + } + return PipelineLogAggregate( + sourceStream: sourceStream, + byteStart: actualStart, + byteEnd: end, + skippedBytes: skipped, + lineCount: lines.count, + signals: signals + ) + } catch { + return nil + } + } + + nonisolated private static func firstRegexInt(_ pattern: String, in text: String) -> Int64? { + firstRegexText(pattern, in: text).flatMap(Int64.init) + } + + nonisolated private static func firstRegexDouble(_ pattern: String, in text: String) -> Double? { + firstRegexText(pattern, in: text).flatMap(Double.init) + } + + nonisolated private static func firstRegexText(_ pattern: String, in text: String) -> String? { + guard let regex = try? NSRegularExpression(pattern: pattern), + let match = regex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)), + match.numberOfRanges > 1, + let range = Range(match.range(at: 1), in: text) else { return nil } + return String(text[range]) + } + + nonisolated private static func regexDoubles(_ pattern: String, in text: String) -> [Double] { + guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } + return regex.matches(in: text, range: NSRange(text.startIndex..., in: text)).compactMap { match in + guard match.numberOfRanges > 1, let range = Range(match.range(at: 1), in: text) else { return nil } + return Double(text[range]) + } + } + + nonisolated private static func runController( + discovery: EmulatorControllerDiscovery, + profile: TFTMACRuntimeProfile, + mailbox: LatestFrameMailbox, + telemetry: TFTMACNativeTelemetry, + inputStream: AsyncStream, + status: @escaping StatusHandler + ) async throws { + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(address: "127.0.0.1", port: discovery.port), + transportSecurity: .plaintext + ) + try await withGRPCClient(transport: transport) { grpc in + let client = Android_Emulation_Control_EmulatorController.Client(wrapping: grpc) + let metadata: GRPCCore.Metadata = ["authorization": "Bearer \(discovery.token)"] + let statusRequest = GRPCCore.ClientRequest( + message: SwiftProtobuf.Google_Protobuf_Empty(), + metadata: metadata + ) + let emulatorStatus: Android_Emulation_Control_EmulatorStatus = try await client.getStatus( + request: statusRequest, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer() + ) + guard emulatorStatus.version.contains("37.1.11") else { + throw TFTMACRuntimeError("Unexpected Android Emulator version: \(emulatorStatus.version)") + } + telemetry.recordEvent("CONTROLLER_AUTHENTICATED", payload: [ + "version": emulatorStatus.version, + "booted": emulatorStatus.booted, + "cpu_cores": emulatorStatus.vmConfig.numberOfCpuCores, + "ram_mib": emulatorStatus.vmConfig.ramSizeBytes / 1024 / 1024 + ]) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + var requestFormat = Android_Emulation_Control_ImageFormat() + requestFormat.format = .rgba8888 + requestFormat.width = 0 + requestFormat.height = 0 + requestFormat.display = 0 + let request = GRPCCore.ClientRequest(message: requestFormat, metadata: metadata) + var callOptions = GRPCCore.CallOptions.defaults + callOptions.waitForReady = true + // grpc-swift-nio-transport 2.9.1 wires maxRequestMessageBytes + // into the HTTP/2 stream decoder's payload ceiling, so the + // pinned transport requires both limits for an 8.3 MB frame. + callOptions.maxRequestMessageBytes = 16 * 1024 * 1024 + callOptions.maxResponseMessageBytes = 16 * 1024 * 1024 + + let admission = NativeFrameAdmissionState(mailbox: mailbox, telemetry: telemetry) + let clock = ContinuousClock() + let firstFrameDeadline = clock.now.advanced(by: .seconds(120)) + var initialImage: Android_Emulation_Control_Image? + var recordedFrameWait = false + while initialImage == nil { + do { + initialImage = try await client.getScreenshot( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: callOptions + ) + } catch let error as GRPCCore.RPCError where error.code == .failedPrecondition { + if !recordedFrameWait { + telemetry.recordEvent("FIRST_NATIVE_FRAME_WAITING", payload: [ + "grpc_code": "failedPrecondition", + "reason": error.message, + "timeout_seconds": 120 + ]) + recordedFrameWait = true + } + guard clock.now < firstFrameDeadline else { + throw TFTMACRuntimeError("Timed out waiting for Android to post its first native frame.") + } + try await Task.sleep(for: .milliseconds(250)) + } + } + guard let initialImage else { + throw TFTMACRuntimeError("Android did not return an initial native frame.") + } + _ = try admission.admit(initialImage) + try await client.streamScreenshot( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: callOptions + ) { response in + for try await image in response.messages { + try Task.checkCancellation() + _ = try admission.admit(image) + } + throw TFTMACRuntimeError("The authenticated screenshot stream ended unexpectedly.") + } + } + group.addTask { + var recordedTouchPipeline = false + for await input in inputStream { + try Task.checkCancellation() + switch input { + case .touch(let touch): + var contact = Android_Emulation_Control_Touch() + contact.x = touch.x + contact.y = touch.y + contact.identifier = touch.identifier + contact.pressure = touch.pressure + contact.expiration = .unspecified + var event = Android_Emulation_Control_TouchEvent() + event.touches = [contact] + event.display = 0 + let request = GRPCCore.ClientRequest(message: event, metadata: metadata) + _ = try await client.sendTouch( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer() + ) + if !recordedTouchPipeline { + telemetry.recordEvent("PRIMARY_TOUCH_INPUT_ACTIVE", payload: [ + "transport": "EmulatorController.sendTouch", + "display": 0, + "identifier": touch.identifier + ]) + recordedTouchPipeline = true + } + case .mouse(let mouse): + var event = Android_Emulation_Control_MouseEvent() + event.x = mouse.x + event.y = mouse.y + event.buttons = mouse.buttons + event.display = 0 + let request = GRPCCore.ClientRequest(message: event, metadata: metadata) + _ = try await client.sendMouse( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer() + ) + case .keyboard(let keyboard): + var event = Android_Emulation_Control_KeyboardEvent() + event.eventType = .keypress + if let key = keyboard.key { event.key = key } + else if let text = keyboard.text { event.text = text } + let request = GRPCCore.ClientRequest(message: event, metadata: metadata) + _ = try await client.sendKey( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer() + ) + } + } + } + _ = try await group.next() + group.cancelAll() + } + } + } + + private func prepareAVD(paths: TFTMACRuntimePaths, telemetry: TFTMACNativeTelemetry) throws -> AVDConfigurationTransaction { + let transaction = try AVDConfigurationTransaction( + configURL: paths.avdConfig, + stateRoot: paths.applicationSupport.appendingPathComponent("State", isDirectory: true), + captureDirectory: telemetry.captureDirectory, + profile: profile + ) + telemetry.recordEvent("AVD_CONFIG_APPLIED", payload: [ + "original_sha256": transaction.originalSHA256, + "applied_sha256": transaction.appliedSHA256, + "reversible": true + ]) + return transaction + } + + private func recoverInterruptedAVDTransaction(paths: TFTMACRuntimePaths) throws { + try AVDConfigurationTransaction.recoverIfNeeded( + configURL: paths.avdConfig, + stateRoot: paths.applicationSupport.appendingPathComponent("State", isDirectory: true), + captureRoot: paths.applicationSupport.appendingPathComponent("Captures", isDirectory: true) + ) + } + + nonisolated private static func adbEnvironment(paths: TFTMACRuntimePaths) -> [String: String] { + var environment = ProcessInfo.processInfo.environment + environment.removeValue(forKey: "ADB_VENDOR_KEYS") + environment.removeValue(forKey: "ADB_SERVER_SOCKET") + environment.removeValue(forKey: "ANDROID_ADB_SERVER_ADDRESS") + environment["ANDROID_SDK_ROOT"] = paths.sdkRoot.path + environment["ANDROID_AVD_HOME"] = paths.avdHome.path + environment["ANDROID_ADB_SERVER_PORT"] = "5038" + environment["ADB_MDNS_AUTO_CONNECT"] = "" + return environment + } + + nonisolated private static func adb(paths: TFTMACRuntimePaths, _ arguments: [String], timeout: TimeInterval) throws -> ProcessResult { + let result = try runCommand( + paths.adb, + ["-P", "5038", "-s", "emulator-5582"] + arguments, + environment: adbEnvironment(paths: paths), + timeout: timeout + ) + guard result.status == 0 else { + throw TFTMACRuntimeError("ADB command failed: \(result.output.suffix(1200))") + } + return result + } + + nonisolated private static func readTFTProcessID(paths: TFTMACRuntimePaths) throws -> Int32? { + let result = try runCommand( + paths.adb, + [ + "-P", "5038", "-s", "emulator-5582", "shell", "pidof", + "com.riotgames.league.teamfighttactics" + ], + environment: adbEnvironment(paths: paths), + timeout: 10 + ) + let output = result.output.trimmingCharacters(in: .whitespacesAndNewlines) + if result.status != 0 { + if output.isEmpty { return nil } + throw TFTMACRuntimeError("ADB TFT process query failed: \(output.suffix(1200))") + } + return output.split(whereSeparator: \.isWhitespace).first.flatMap { Int32($0) } + } + + nonisolated private static func runCommand( + _ executable: URL, + _ arguments: [String], + environment: [String: String]? = nil, + input: Data? = nil, + timeout: TimeInterval + ) throws -> ProcessResult { + let process = Process() + process.executableURL = executable + process.arguments = arguments + process.environment = environment + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + let inputPipe = input.map { _ in Pipe() } + if let inputPipe { process.standardInput = inputPipe } + try process.run() + if let input, let inputPipe { + inputPipe.fileHandleForWriting.write(input) + try? inputPipe.fileHandleForWriting.close() + } + let timeoutWork = DispatchWorkItem { + if process.isRunning { process.terminate() } + } + DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutWork) + let data = pipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + timeoutWork.cancel() + let output = String(data: data, encoding: .utf8) ?? "" + return ProcessResult(status: process.terminationStatus, output: output) + } + + nonisolated private static func parseINI(_ text: String) -> [String: String] { + var result = [String: String]() + for line in text.split(whereSeparator: \.isNewline) { + let pair = line.split(separator: "=", maxSplits: 1).map(String.init) + if pair.count == 2 { + result[pair[0].trimmingCharacters(in: .whitespaces)] = pair[1].trimmingCharacters(in: .whitespaces) + } + } + return result + } + + nonisolated private static func processExists(_ processIdentifier: Int32) -> Bool { + if Darwin.kill(processIdentifier, 0) == 0 { return true } + return errno != ESRCH + } + + nonisolated private static func processMatchesLaunchedIdentity( + _ processIdentifier: Int32, + sessionMarker: String? = nil + ) -> Bool { + guard processExists(processIdentifier), + let result = try? runCommand( + URL(fileURLWithPath: "/bin/ps"), + ["-p", "\(processIdentifier)", "-ww", "-o", "command="], + timeout: 5 + ), result.status == 0 else { return false } + let baseIdentityMatches = result.output.contains("qemu-system-aarch64") + && result.output.contains("@TFT_Ultra_Tablet") + && result.output.contains("-port 5582") + return baseIdentityMatches && (sessionMarker.map(result.output.contains) ?? true) + } + + nonisolated private static func findOwnedEmulatorPID(sessionMarker: String) -> Int32? { + guard let result = try? runCommand( + URL(fileURLWithPath: "/bin/ps"), + ["-axo", "pid=,command="], + timeout: 10 + ), result.status == 0 else { return nil } + for line in result.output.split(whereSeparator: \.isNewline) { + guard line.contains("qemu-system-aarch64"), + line.contains("@TFT_Ultra_Tablet"), + line.contains(sessionMarker), + let pid = line.split(whereSeparator: \.isWhitespace).first.flatMap({ Int32($0) }) else { continue } + return pid + } + return nil + } + + nonisolated private static func anyEmulatorUsingSharedAVD() -> Bool { + guard let result = try? runCommand( + URL(fileURLWithPath: "/bin/ps"), + ["-axo", "command="], + timeout: 10 + ), result.status == 0 else { return true } + return result.output.split(whereSeparator: \.isNewline).contains { line in + line.contains("qemu-system-aarch64") && line.contains("@TFT_Ultra_Tablet") + } + } + + nonisolated private static func controllerDiscoveryRoots(paths: TFTMACRuntimePaths) -> [URL] { + let home = FileManager.default.homeDirectoryForCurrentUser + let candidates = [ + home.appendingPathComponent("Library/Caches/TemporaryItems/avd/running", isDirectory: true), + FileManager.default.temporaryDirectory.appendingPathComponent("avd/running", isDirectory: true), + paths.avdHome.appendingPathComponent("running", isDirectory: true), + home.appendingPathComponent(".android/avd/running", isDirectory: true) + ] + var unique = [URL]() + for candidate in candidates where !unique.contains(candidate) { unique.append(candidate) } + return unique + } + + nonisolated private static func utcNow() -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: Date()) + } +} + +private struct ProcessResult: Sendable { + let status: Int32 + let output: String +} + +private final class AVDConfigurationTransaction: @unchecked Sendable { + let originalSHA256: String + let appliedSHA256: String + + private let configURL: URL + private let backupURL: URL + private let markerURL: URL + + init( + configURL: URL, + stateRoot: URL, + captureDirectory: URL, + profile: TFTMACRuntimeProfile + ) throws { + try FileManager.default.createDirectory(at: stateRoot, withIntermediateDirectories: true) + self.configURL = configURL + backupURL = captureDirectory.appendingPathComponent("avd-config.before.ini") + markerURL = stateRoot.appendingPathComponent("avd-config-transaction.json") + let original = try Data(contentsOf: configURL) + originalSHA256 = Self.sha256(original) + try original.write(to: backupURL, options: .atomic) + var config = String(decoding: original, as: UTF8.self) + let values: [String: String] = [ + "hw.cpu.ncore": "\(profile.vCPU)", + "hw.ramSize": "\(profile.ramMiB)", + "hw.lcd.width": "\(profile.width)", + "hw.lcd.height": "\(profile.height)", + "hw.lcd.density": "\(profile.densityDPI)", + "hw.gpu.enabled": "yes", + "hw.gpu.mode": profile.gpuMode, + "hw.gltransport": profile.graphicsTransport, + "hw.gltransport.drawFlushInterval": "\(profile.asgDrawFlushInterval)", + "hw.gltransport.asg.writeBufferSize": "\(profile.asgWriteBufferSize)", + "hw.gltransport.asg.writeStepSize": "\(profile.asgWriteStepSize)", + "hw.gltransport.asg.dataRingSize": "\(profile.asgDataRingSize)", + "showDeviceFrame": "no", + "skin.name": "\(profile.width)x\(profile.height)", + "fastboot.forceColdBoot": "yes", + "fastboot.forceFastBoot": "no" + ] + for (key, value) in values { config = Self.setting(key: key, value: value, in: config) } + let applied = Data(config.utf8) + appliedSHA256 = Self.sha256(applied) + let marker: [String: Any] = [ + "schema": 1, + "config": configURL.path, + "backup": backupURL.path, + "original_sha256": originalSHA256, + "applied_sha256": appliedSHA256 + ] + let markerData = try JSONSerialization.data(withJSONObject: marker, options: [.prettyPrinted, .sortedKeys]) + try markerData.write(to: markerURL, options: .atomic) + try applied.write(to: configURL, options: .atomic) + } + + func restore() throws { + let original = try Data(contentsOf: backupURL) + guard Self.sha256(original) == originalSHA256 else { + throw TFTMACRuntimeError("The AVD backup hash changed; automatic restore stopped safely.") + } + let current = try Data(contentsOf: configURL) + let currentSHA256 = Self.sha256(current) + let decision = try AVDTransactionGuard.restoreDecision( + currentSHA256: currentSHA256, + originalSHA256: originalSHA256, + appliedSHA256: appliedSHA256 + ) + if decision == .alreadyOriginal { + try? FileManager.default.removeItem(at: markerURL) + return + } + try original.write(to: configURL, options: .atomic) + try? FileManager.default.removeItem(at: markerURL) + } + + static func recoverIfNeeded(configURL expectedConfigURL: URL, stateRoot: URL, captureRoot: URL) throws { + let markerURL = stateRoot.appendingPathComponent("avd-config-transaction.json") + guard let data = try? Data(contentsOf: markerURL), + let marker = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let config = marker["config"] as? String, + let backup = marker["backup"] as? String, + let expected = marker["original_sha256"] as? String, + let applied = marker["applied_sha256"] as? String else { return } + let markerConfigURL = URL(fileURLWithPath: config) + let backupURL = URL(fileURLWithPath: backup) + try AVDTransactionGuard.validateRecoveryPaths( + markerConfigURL: markerConfigURL, + expectedConfigURL: expectedConfigURL, + backupURL: backupURL, + captureRoot: captureRoot + ) + let backupData = try Data(contentsOf: backupURL) + guard sha256(backupData) == expected else { + throw TFTMACRuntimeError("A prior AVD transaction backup failed its hash check.") + } + let currentData = try Data(contentsOf: expectedConfigURL) + let currentSHA256 = sha256(currentData) + let decision = try AVDTransactionGuard.restoreDecision( + currentSHA256: currentSHA256, + originalSHA256: expected, + appliedSHA256: applied + ) + if decision == .alreadyOriginal { + try FileManager.default.removeItem(at: markerURL) + return + } + try backupData.write(to: expectedConfigURL, options: .atomic) + try FileManager.default.removeItem(at: markerURL) + } + + private static func setting(key: String, value: String, in text: String) -> String { + var lines = text.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) + if let index = lines.firstIndex(where: { $0.hasPrefix("\(key)=") }) { + lines[index] = "\(key)=\(value)" + } else { + lines.append("\(key)=\(value)") + } + return lines.joined(separator: "\n") + } + + private static func sha256(_ data: Data) -> String { + SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + } +} + +@MainActor +final class TFTMACRuntimeController { + private let service: TFTMACRuntimeService + private var runTask: Task? + private(set) var failed = false + + init( + profile: TFTMACRuntimeProfile, + mailbox: LatestFrameMailbox, + status: @escaping TFTMACRuntimeService.StatusHandler, + gameFrame: @escaping TFTMACRuntimeService.GameFrameHandler + ) { + service = TFTMACRuntimeService(profile: profile, mailbox: mailbox, status: status, gameFrame: gameFrame) + } + + func start() { + guard runTask == nil else { return } + runTask = Task { [service] in + do { try await service.run() } + catch is CancellationError { } + catch { + await MainActor.run { self.failed = true } + } + } + } + + func sendMouse(x: Int32, y: Int32, buttons: Int32) { + Task { await service.sendMouse(MouseInput(x: x, y: y, buttons: buttons)) } + } + + func sendTouch(_ input: TouchInput) { + Task { await service.sendTouch(input) } + } + + func sendKeyboard(text: String? = nil, key: String? = nil) { + guard text?.isEmpty == false || key?.isEmpty == false else { return } + Task { await service.sendKeyboard(KeyboardInput(text: text, key: key)) } + } + + func recordPresentation(_ sample: PresentationSample) { + Task { await service.recordPresentation(sample) } + } + + func recordHostPresentation(_ sample: HostPresentationWindow) { + Task { await service.recordHostPresentation(sample) } + } + + func recordMarker(_ marker: String) { + Task { await service.recordMarker(marker) } + } + + func startCombatBenchmark(performanceModeConfirmed: Bool) { + Task { await service.startCombatBenchmark(performanceModeConfirmed: performanceModeConfirmed) } + } + + func markVisibleStutter() { + Task { await service.markVisibleStutter() } + } + + func endCombatBenchmark(correctnessPassed: Bool = true) { + Task { await service.endCombatBenchmark(correctnessPassed: correctnessPassed) } + } + + func recordSettingsChange(previous: TFTMACRuntimeProfile, next: TFTMACRuntimeProfile) { + Task { await service.recordSettingsChange(previous: previous, next: next) } + } + + func stop() async { + await service.stop() + runTask?.cancel() + _ = await runTask?.result + runTask = nil + } +} diff --git a/tftmac/Runtime/TelemetrySignalClassifier.swift b/tftmac/Runtime/TelemetrySignalClassifier.swift new file mode 100644 index 0000000..44deb0d --- /dev/null +++ b/tftmac/Runtime/TelemetrySignalClassifier.swift @@ -0,0 +1,72 @@ +import Foundation + +struct PipelineLogSignals: Equatable, Sendable { + var gfxstreamWarningCount = 0 + var asgStallCount = 0 + var vulkanErrorCount = 0 + var moltenVKWarningCount = 0 + var shaderErrorCount = 0 + var fenceTimeoutCount = 0 + + static func + (lhs: Self, rhs: Self) -> Self { + Self( + gfxstreamWarningCount: lhs.gfxstreamWarningCount + rhs.gfxstreamWarningCount, + asgStallCount: lhs.asgStallCount + rhs.asgStallCount, + vulkanErrorCount: lhs.vulkanErrorCount + rhs.vulkanErrorCount, + moltenVKWarningCount: lhs.moltenVKWarningCount + rhs.moltenVKWarningCount, + shaderErrorCount: lhs.shaderErrorCount + rhs.shaderErrorCount, + fenceTimeoutCount: lhs.fenceTimeoutCount + rhs.fenceTimeoutCount + ) + } +} + +enum TelemetrySignalClassifier { + /// Counts only lines that identify an actual memory-pressure victim. + /// LMKD startup, socket, memevent, tracepoint, and policy messages are not kills. + static func isConfirmedGuestMemoryKill(_ line: String) -> Bool { + let lower = line.lowercased() + + if lower.contains("out of memory: killed process") { + return true + } + + guard lower.contains("lmkd") || lower.contains("lowmemorykiller") else { + return false + } + + return lower.contains("kill '") + || lower.contains("killing '") + || lower.contains("kill process ") + || lower.contains("killing process ") + || lower.contains("killed process ") + } + + /// Counts only diagnostic lines that already carry a failure, warning, + /// timeout, or stall word. Component names in normal startup/configuration + /// receipts must not become fabricated pipeline failures. + static func pipelineSignals(in line: String) -> PipelineLogSignals { + let lower = line.lowercased() + let warningOrFailure = lower.contains("warn") + || lower.contains("error") + || lower.contains("failed") + || lower.contains("failure") + || lower.contains("timeout") + || lower.contains("timed out") + || lower.contains("stall") + || lower.contains("fatal") + guard warningOrFailure else { return PipelineLogSignals() } + + return PipelineLogSignals( + gfxstreamWarningCount: lower.contains("gfxstream") ? 1 : 0, + asgStallCount: lower.contains("asg") + && (lower.contains("stall") || lower.contains("timeout") || lower.contains("failed") || lower.contains("error")) ? 1 : 0, + vulkanErrorCount: (lower.contains("vulkan") || lower.contains(" vk_")) + && (lower.contains("error") || lower.contains("failed") || lower.contains("fatal") || lower.contains("timeout")) ? 1 : 0, + moltenVKWarningCount: (lower.contains("moltenvk") || lower.contains("[mvk]")) ? 1 : 0, + shaderErrorCount: lower.contains("shader") + && (lower.contains("error") || lower.contains("failed") || lower.contains("fatal")) ? 1 : 0, + fenceTimeoutCount: lower.contains("fence") + && (lower.contains("timeout") || lower.contains("timed out") || lower.contains("stall") || lower.contains("failed")) ? 1 : 0 + ) + } +} diff --git a/tftmac/Runtime/TouchInput.swift b/tftmac/Runtime/TouchInput.swift new file mode 100644 index 0000000..a7eb721 --- /dev/null +++ b/tftmac/Runtime/TouchInput.swift @@ -0,0 +1,52 @@ +enum TouchPhase: Sendable, Equatable { + case contact + case release + + var pressure: Int32 { + switch self { + case .contact: 1 + case .release: 0 + } + } +} + +struct TouchInput: Sendable, Equatable { + static let primaryIdentifier: Int32 = 0 + + let x: Int32 + let y: Int32 + let identifier: Int32 + let phase: TouchPhase + + var pressure: Int32 { phase.pressure } + + static func primary(x: Int32, y: Int32, isContact: Bool) -> Self { + Self( + x: x, + y: y, + identifier: primaryIdentifier, + phase: isContact ? .contact : .release + ) + } +} + +struct TouchPoint: Sendable, Equatable { + let x: Int32 + let y: Int32 +} + +struct PrimaryTouchSequence: Sendable { + private var lastContactPoint: TouchPoint? + + mutating func contact(at point: TouchPoint?) -> TouchInput? { + guard let point else { return nil } + lastContactPoint = point + return .primary(x: point.x, y: point.y, isContact: true) + } + + mutating func release(at point: TouchPoint?) -> TouchInput? { + guard let releasePoint = point ?? lastContactPoint else { return nil } + lastContactPoint = nil + return .primary(x: releasePoint.x, y: releasePoint.y, isContact: false) + } +} diff --git a/tftmac/Sources/README.md b/tftmac/Sources/README.md new file mode 100644 index 0000000..ec1a19e --- /dev/null +++ b/tftmac/Sources/README.md @@ -0,0 +1,6 @@ +# Retired shell source + +These Swift files are preserved only as historical donor evidence. They are not +members of the native Xcode target and no build script compiles them. The only +shipping implementation lives under `tftmac/App`, `tftmac/Presentation`, and +`tftmac/Runtime`; `scripts/build-native-app.command` is the sole build authority. diff --git a/tools/Mactician Game Host.app/Contents/Info.plist b/tools/Mactician Game Host.app/Contents/Info.plist deleted file mode 100644 index 5b2ecd9..0000000 --- a/tools/Mactician Game Host.app/Contents/Info.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDisplayName - Mactician Game Host - CFBundleExecutable - MacticianGameHost - CFBundleIdentifier - dev.sergeinaumov.mactician.experiment-emulator - CFBundleName - Mactician Game Host - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - GCSupportsGameMode - - LSApplicationCategoryType - public.app-category.games - LSSupportsGameMode - - NSHighResolutionCapable - - - diff --git a/tools/Mactician Game Host.app/Contents/MacOS/MacticianGameHost b/tools/Mactician Game Host.app/Contents/MacOS/MacticianGameHost deleted file mode 100755 index 888ae18..0000000 --- a/tools/Mactician Game Host.app/Contents/MacOS/MacticianGameHost +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/zsh -set -euo pipefail - -readonly PROJECT_DIR="${0:A:h:h:h:h:h}" -source "$PROJECT_DIR/scripts/android-environment.sh" -SDK_ROOT="$(tft_resolve_android_sdk_root)" -readonly SDK_ROOT -EMULATOR="$(tft_resolve_emulator)" -readonly EMULATOR -readonly ADB_SERVER_PORT="${TFT_ADB_SERVER_PORT:-5038}" - -if [[ ! -x "$EMULATOR" ]]; then - print -u2 "Android Emulator executable not found: $EMULATOR" - exit 1 -fi - -if [[ "$ADB_SERVER_PORT" != <-> ]] \ - || (( ADB_SERVER_PORT < 1024 || ADB_SERVER_PORT > 65534 )); then - print -u2 "TFT_ADB_SERVER_PORT must be a TCP port from 1024 through 65534." - exit 2 -fi - -export ANDROID_SDK_ROOT="$SDK_ROOT" -export ANDROID_AVD_HOME="$(tft_resolve_avd_home)" -unset ADB_SERVER_SOCKET ANDROID_ADB_SERVER_ADDRESS -export ANDROID_ADB_SERVER_PORT="$ADB_SERVER_PORT" - -exec "$EMULATOR" "$@" diff --git a/tools/clara-task.mjs b/tools/clara-task.mjs deleted file mode 100644 index 63f5cf2..0000000 --- a/tools/clara-task.mjs +++ /dev/null @@ -1,1003 +0,0 @@ -#!/usr/bin/env node -import { createHash } from 'node:crypto'; -import { createReadStream, existsSync } from 'node:fs'; -import { mkdir, readFile, rename, rm, stat, writeFile, copyFile, readdir } from 'node:fs/promises'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { spawn, spawnSync } from 'node:child_process'; - -const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const PRIVATE = join(ROOT, 'private'); -const ACQ = join(PRIVATE, 'live-acquisition'); -const SDK = join(ACQ, 'sdk'); -const DOWNLOADS = join(ACQ, 'downloads'); -const AVD_HOME = join(ACQ, 'avd'); -const AVD_NAME = 'TftLiveStore'; -const PERFORMANCE_AVD_NAME = 'TftHighEndTablet'; -const SERIAL = 'emulator-5590'; -const PERFORMANCE_SERIAL = 'emulator-5592'; -const ADB_PORT = '5040'; -const PACKAGE = 'com.riotgames.league.teamfighttactics'; -const EXPECTED_ACTIVITY = 'com.riotgames.leagueoflegends.RiotNativeActivity'; -const LIVE_ROOT = join(PRIVATE, 'live-apks'); -const LIVE_CURRENT = join(LIVE_ROOT, 'current'); -const LIVE_MANIFEST = join(LIVE_ROOT, 'manifest.json'); -const RELEASE_TEMPLATE = join(ROOT, 'launcher', 'Resources', 'release-manifest.json'); -const PLAY_REPO = 'https://dl.google.com/android/repository/sys-img/google_apis_playstore/sys-img2-3.xml'; -const PLAY_REPO_BASE = 'https://dl.google.com/android/repository/sys-img/google_apis_playstore/'; -const PLAY_PACKAGE_PATH = 'system-images;android-36;google_apis_playstore;arm64-v8a'; - -function die(message) { - console.error(`TFTMAC: ${message}`); - process.exit(1); -} - -function run(executable, args, options = {}) { - const result = spawnSync(executable, args, { - cwd: options.cwd ?? ROOT, - env: options.env ?? process.env, - encoding: 'utf8', - stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit', - maxBuffer: 16 * 1024 * 1024 - }); - if (result.error) die(`${executable} failed to start: ${result.error.message}`); - if (result.status !== 0) { - if (options.capture) { - if (result.stdout) process.stdout.write(result.stdout); - if (result.stderr) process.stderr.write(result.stderr); - } - die(`${executable} exited with ${result.status}`); - } - return options.capture ? String(result.stdout ?? '').trim() : ''; -} - -async function hashFile(path, algorithm = 'sha256') { - return new Promise((resolveHash, reject) => { - const hash = createHash(algorithm); - const stream = createReadStream(path); - stream.on('error', reject); - stream.on('data', chunk => hash.update(chunk)); - stream.on('end', () => resolveHash(hash.digest('hex'))); - }); -} - -async function download(url, destination, expectedHash, algorithm = 'sha256') { - await mkdir(dirname(destination), { recursive: true }); - if (existsSync(destination) && expectedHash) { - if (await hashFile(destination, algorithm) === expectedHash) return; - await rm(destination, { force: true }); - } - const partial = `${destination}.partial`; - run('/usr/bin/curl', ['-fL', '--retry', '3', '--retry-delay', '2', '--continue-at', '-', url, '-o', partial]); - if (expectedHash) { - const actual = await hashFile(partial, algorithm); - if (actual !== expectedHash) { - await rm(partial, { force: true }); - die(`checksum mismatch for ${url}: expected ${expectedHash}, got ${actual}`); - } - } - await rename(partial, destination); -} - -async function extractZip(zip, destination) { - await rm(destination, { recursive: true, force: true }); - await mkdir(destination, { recursive: true }); - run('/usr/bin/unzip', ['-q', zip, '-d', destination]); -} - -async function ensureCoreSDK() { - const template = JSON.parse(await readFile(RELEASE_TEMPLATE, 'utf8')); - for (const id of ['platform-tools', 'emulator']) { - const component = template.components.find(entry => entry.id === id); - if (!component) die(`release template lacks ${id}`); - const target = join(ACQ, component.installPath); - const marker = id === 'platform-tools' ? join(target, 'adb') : join(target, 'emulator'); - if (existsSync(marker)) continue; - const archive = join(DOWNLOADS, `${id}.zip`); - await download(component.url, archive, component.sha256, 'sha256'); - const stage = join(ACQ, `.stage-${id}`); - await extractZip(archive, stage); - const source = join(stage, component.archiveRoot); - if (!existsSync(source)) die(`${id} archive did not contain ${component.archiveRoot}`); - await mkdir(dirname(target), { recursive: true }); - await rm(target, { recursive: true, force: true }); - await rename(source, target); - await rm(stage, { recursive: true, force: true }); - } -} - -function xmlBlockForPackage(xml, packagePath) { - const escaped = packagePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = xml.match(new RegExp(`]*path=["']${escaped}["'][^>]*>([\\s\\S]*?)<\\/remotePackage>`)); - if (!match) die(`Google repository does not list ${packagePath}`); - return match[1]; -} - -function xmlText(block, tag) { - const match = block.match(new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`)); - return match?.[1]?.trim() ?? null; -} - -async function ensurePlayImage() { - const install = join(SDK, 'system-images', 'android-36', 'google_apis_playstore', 'arm64-v8a'); - if (existsSync(join(install, 'system.img')) && existsSync(join(install, 'source.properties'))) return; - await mkdir(DOWNLOADS, { recursive: true }); - const repoXML = join(DOWNLOADS, 'google-play-system-images.xml'); - run('/usr/bin/curl', ['-fL', '--retry', '3', PLAY_REPO, '-o', repoXML]); - const xml = await readFile(repoXML, 'utf8'); - const block = xmlBlockForPackage(xml, PLAY_PACKAGE_PATH); - const archives = block.match(/([\s\S]*?)<\/archives>/)?.[1] ?? block; - const archive = archives.match(/([\s\S]*?[\s\S]*?<\/complete>[\s\S]*?)<\/archive>/)?.[1]; - if (!archive) die('Google Play system-image metadata has no complete archive'); - const complete = archive.match(/([\s\S]*?)<\/complete>/)?.[1]; - if (!complete) die('Google Play system-image metadata is incomplete'); - const filename = xmlText(complete, 'url'); - const checksumMatch = complete.match(/([0-9a-fA-F]+)<\/checksum>/); - if (!filename || !checksumMatch) die('Google Play system-image URL/checksum missing'); - const algorithm = checksumMatch[1].toLowerCase(); - const expected = checksumMatch[2].toLowerCase(); - const archivePath = join(DOWNLOADS, filename.split('/').pop()); - await download(new URL(filename, PLAY_REPO_BASE).href, archivePath, expected, algorithm); - const stage = join(ACQ, '.stage-play-image'); - await extractZip(archivePath, stage); - const source = existsSync(join(stage, 'arm64-v8a')) ? join(stage, 'arm64-v8a') : stage; - if (!existsSync(join(source, 'system.img'))) die('Google Play archive did not contain system.img'); - await mkdir(dirname(install), { recursive: true }); - await rm(install, { recursive: true, force: true }); - if (source === stage) { - await mkdir(install, { recursive: true }); - for (const entry of await readdir(stage)) await rename(join(stage, entry), join(install, entry)); - await rm(stage, { recursive: true, force: true }); - } else { - await rename(source, install); - await rm(stage, { recursive: true, force: true }); - } -} - -async function ensureAcquisitionAVD() { - const avd = join(AVD_HOME, `${AVD_NAME}.avd`); - const ini = join(AVD_HOME, `${AVD_NAME}.ini`); - if (existsSync(join(avd, 'config.ini')) && existsSync(ini)) return false; - await mkdir(avd, { recursive: true }); - const config = `AvdId=${AVD_NAME}\navd.ini.displayname=TFTMAC Google Play Acquisition\nabi.type=arm64-v8a\nhw.cpu.arch=arm64\nhw.cpu.ncore=4\nhw.lcd.density=320\nhw.lcd.height=1080\nhw.lcd.width=1920\nhw.ramSize=4096\nhw.vmHeapSize=576\nhw.gpu.enabled=yes\nhw.gpu.mode=host\nhw.keyboard=yes\nskin.name=1920x1080\nshowDeviceFrame=no\ndisk.dataPartition.size=12288M\nimage.sysdir.1=system-images/android-36/google_apis_playstore/arm64-v8a/\ntag.id=google_apis_playstore\ntag.display=Google Play\nPlayStore.enabled=true\nfastboot.forceColdBoot=yes\nfastboot.forceFastBoot=no\navd.ini.encoding=UTF-8\n`; - await writeFile(join(avd, 'config.ini'), config); - await writeFile(ini, `avd.ini.encoding=UTF-8\npath=${avd}\npath.rel=${AVD_NAME}.avd\ntarget=android-36\n`); - const qemuImg = join(SDK, 'emulator', 'qemu-img'); - run(qemuImg, ['create', '-f', 'qcow2', join(avd, 'userdata-qemu.img'), '12G']); - const encryption = join(SDK, 'system-images', 'android-36', 'google_apis_playstore', 'arm64-v8a', 'encryptionkey.img'); - if (existsSync(encryption)) await copyFile(encryption, join(avd, 'encryptionkey.img')); - return true; -} - -function adb(args, capture = true) { - const executable = join(SDK, 'platform-tools', 'adb'); - return run(executable, ['-P', ADB_PORT, '-s', SERIAL, ...args], { - capture, - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } - }); -} - -function adbMaybe(args) { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 16 * 1024 * 1024 - }); - return result.status === 0 ? String(result.stdout ?? '').trim() : null; -} - -async function waitUntil(description, seconds, test) { - const deadline = Date.now() + seconds * 1000; - while (Date.now() < deadline) { - const value = await test(); - if (value) return value; - await new Promise(resolveWait => setTimeout(resolveWait, 1000)); - } - die(`timed out waiting for ${description}`); -} - -async function acquireLive() { - await mkdir(ACQ, { recursive: true }); - console.log('TFTMAC: preparing official Google Android components...'); - await ensureCoreSDK(); - await ensurePlayImage(); - const firstBoot = await ensureAcquisitionAVD(); - const adbExe = join(SDK, 'platform-tools', 'adb'); - run(adbExe, ['-P', ADB_PORT, 'start-server'], { env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } }); - - let emulator = null; - if (!adbMaybe(['get-state'])) { - const emulatorExe = join(SDK, 'emulator', 'emulator'); - const args = [`@${AVD_NAME}`, '-id', 'TFTMAC-Google-Play', '-port', '5590', '-gpu', 'host', '-skin', '1920x1080', '-cores', '4', '-memory', '4096', '-no-snapshot', '-no-metrics', '-no-boot-anim', '-no-audio', '-crash-report-mode', 'disabled']; - if (firstBoot) args.push('-wipe-data'); - emulator = spawn(emulatorExe, args, { - cwd: ROOT, - env: { ...process.env, ANDROID_SDK_ROOT: SDK, ANDROID_AVD_HOME: AVD_HOME, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - stdio: 'ignore' - }); - emulator.on('error', error => die(`Google Play emulator failed: ${error.message}`)); - } - - await waitUntil('Google Play emulator ADB', 240, () => adbMaybe(['get-state']) === 'device'); - await waitUntil('Android boot', 240, () => adbMaybe(['shell', 'getprop', 'sys.boot_completed'])?.replace(/\r/g, '') === '1'); - await writeFile(join(AVD_HOME, `${AVD_NAME}.avd`, '.tftmac-initialized'), new Date().toISOString()); - - const installed = adbMaybe(['shell', 'pm', 'path', PACKAGE]); - if (!installed) { - adb(['shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', `market://details?id=${PACKAGE}`], false); - console.log('\nTFTMAC_GOOGLE_PLAY_READY'); - console.log('Google Play is open in the Android window. Sign in there and install "TFT: Teamfight Tactics" from Riot Games.'); - console.log('Do not enter Google or Riot credentials anywhere outside the Google Play / TFT Android windows.'); - console.log('This process will detect the completed install automatically.\n'); - await waitUntil('live TFT installation from Google Play', 1800, () => adbMaybe(['shell', 'pm', 'path', PACKAGE])); - } - - const installerLine = adb(['shell', 'pm', 'list', 'packages', '-i', PACKAGE]); - if (!installerLine.includes(`package:${PACKAGE}`) || !installerLine.includes('installer=com.android.vending')) { - die(`live TFT was not installed by Google Play: ${installerLine || 'no installer record'}`); - } - - const packagePathsOutput = adb(['shell', 'pm', 'path', PACKAGE]).replace(/\r/g, ''); - const packagePaths = packagePathsOutput.split('\n').map(line => line.replace(/^package:/, '').trim()).filter(Boolean); - if (!packagePaths.length) die('Google Play TFT has no APK paths'); - packagePaths.sort((a, b) => (a.endsWith('/base.apk') ? -1 : b.endsWith('/base.apk') ? 1 : a.localeCompare(b))); - if (!packagePaths[0].endsWith('/base.apk')) die('Google Play TFT base.apk was not found'); - - const dump = adb(['shell', 'dumpsys', 'package', PACKAGE]); - const versionName = dump.match(/versionName=([^\s]+)/)?.[1]; - const versionCodeText = dump.match(/versionCode=(\d+)/)?.[1]; - if (!versionName || !versionCodeText) die('could not determine live TFT version'); - const versionCode = Number(versionCodeText); - - const resolved = adbMaybe(['shell', 'cmd', 'package', 'resolve-activity', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.LAUNCHER', PACKAGE]) ?? ''; - if (!resolved.includes(EXPECTED_ACTIVITY) && !dump.includes(EXPECTED_ACTIVITY)) { - die(`live TFT no longer exposes expected Unreal activity ${EXPECTED_ACTIVITY}; observed ${resolved || 'none'}`); - } - - const stage = join(LIVE_ROOT, `.next-${Date.now()}`); - await rm(stage, { recursive: true, force: true }); - await mkdir(stage, { recursive: true }); - const apks = []; - const used = new Set(); - for (const remotePath of packagePaths) { - let name = remotePath.split('/').pop(); - if (used.has(name)) die(`duplicate APK split filename from Google Play: ${name}`); - used.add(name); - const localPath = join(stage, name); - adb(['pull', remotePath, localPath], false); - const info = await stat(localPath); - const sha256 = await hashFile(localPath, 'sha256'); - apks.push({ name, size: info.size, sha256 }); - } - - const base = apks[0]; - const basePath = join(stage, 'base.apk'); - const zipList = run('/usr/bin/unzip', ['-l', basePath], { capture: true }); - const hasUECommandLine = zipList.includes('assets/UECommandLine.txt'); - - const manifest = { - packageName: PACKAGE, - version: versionName, - versionCode, - launchActivity: resolved.split('\n').pop()?.trim() || null, - baseSHA256: base.sha256, - donorUECommandLineCompatible: hasUECommandLine, - apks - }; - await mkdir(LIVE_ROOT, { recursive: true }); - const manifestNext = join(LIVE_ROOT, '.manifest.next.json'); - await writeFile(manifestNext, `${JSON.stringify(manifest, null, 2)}\n`); - await rm(LIVE_CURRENT, { recursive: true, force: true }); - await rename(stage, LIVE_CURRENT); - await rename(manifestNext, LIVE_MANIFEST); - console.log(`TFTMAC_ACQUIRED ${versionName} versionCode=${versionCode} splits=${apks.length}`); - console.log(`TFTMAC_BASE_SHA256 ${base.sha256}`); - console.log(`TFTMAC_LAUNCH_ACTIVITY ${manifest.launchActivity ?? 'unknown'}`); - console.log(`TFTMAC_UECOMMANDLINE ${hasUECommandLine ? 'present' : 'absent'}`); - - adbMaybe(['emu', 'kill']); - if (emulator) { - await new Promise(resolveWait => { - const timer = setTimeout(resolveWait, 15000); - emulator.once('exit', () => { clearTimeout(timer); resolveWait(); }); - }); - } -} - -async function prepareHighEndTablet() { - await mkdir(ACQ, { recursive: true }); - await ensureCoreSDK(); - await ensurePlayImage(); - await ensureAcquisitionAVD(); - if (adbMaybe(['get-state'])) { - die('Google Play acquisition device is still running; finish the Riot patch and shut it down cleanly before cloning the high-end tablet'); - } - const sourceAVD = join(AVD_HOME, `${AVD_NAME}.avd`); - const targetAVD = join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.avd`); - const sourceINI = join(AVD_HOME, `${AVD_NAME}.ini`); - const targetINI = join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.ini`); - if (!existsSync(sourceAVD) || !existsSync(sourceINI)) die('source Google Play AVD is missing'); - await rm(targetAVD, { recursive: true, force: true }); - await rm(targetINI, { force: true }); - run('/bin/cp', ['-R', sourceAVD, targetAVD]); - let config = await readFile(join(targetAVD, 'config.ini'), 'utf8'); - const set = (key, value) => { - const pattern = new RegExp(`^${key.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}=.*$`, 'm'); - if (pattern.test(config)) config = config.replace(pattern, `${key}=${value}`); - else config += `\n${key}=${value}`; - }; - set('AvdId', PERFORMANCE_AVD_NAME); - set('avd.ini.displayname', 'TFTMAC High-End Tablet'); - set('hw.device.manufacturer', 'Google'); - set('hw.device.name', 'pixel_tablet'); - set('hw.initialOrientation', 'Landscape'); - set('hw.cpu.ncore', '8'); - set('hw.ramSize', '8192'); - set('hw.vmHeapSize', '768'); - set('hw.lcd.width', '1920'); - set('hw.lcd.height', '1080'); - set('hw.lcd.density', '280'); - set('hw.gpu.enabled', 'yes'); - set('hw.gpu.mode', 'host'); - set('hw.keyboard', 'yes'); - set('showDeviceFrame', 'no'); - set('skin.name', '1920x1080'); - set('runtime.network.speed', 'full'); - set('runtime.network.latency', 'none'); - set('fastboot.forceColdBoot', 'yes'); - set('fastboot.forceFastBoot', 'no'); - await writeFile(join(targetAVD, 'config.ini'), config); - await rm(join(targetAVD, 'hardware-qemu.ini'), { force: true }); - await rm(join(targetAVD, 'hardware-qemu.ini.lock'), { force: true }); - await rm(join(targetAVD, 'multiinstance.lock'), { force: true }); - await writeFile(targetINI, `avd.ini.encoding=UTF-8\npath=${targetAVD}\npath.rel=${PERFORMANCE_AVD_NAME}.avd\ntarget=android-36\n`); - const contract = { - schemaVersion: 1, - avdName: PERFORMANCE_AVD_NAME, - donorProfile: 'Pixel Tablet / custom high-end tablet', - sourceImage: 'Google Play ARM64 Android 16 API 36', - renderTarget: '1920x1080', - densityDpi: 280, - smallestWidthDp: Math.floor(1080 * 160 / 280), - cpuCores: 8, - ramMB: 8192, - vmHeapMB: 768, - gpuMode: 'host', - enhancedBootFlags: [ - '-feature', 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,AsyncComposeSupport,VirtioGpuFenceContexts', - '-append-userspace-opt', 'androidboot.opengles.version=196610' - ], - createdAt: new Date().toISOString() - }; - await writeFile(join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.json`), `${JSON.stringify(contract, null, 2)}\n`); - console.log(JSON.stringify(contract, null, 2)); -} - -async function stopAcquisition() { - await ensureCoreSDK(); - if (!adbMaybe(['get-state'])) { - console.log('TFTMAC_ACQUISITION_STOPPED already_off'); - return; - } - adbMaybe(['emu', 'kill']); - await waitUntil('Google Play acquisition shutdown', 60, () => !adbMaybe(['get-state'])); - console.log('TFTMAC_ACQUISITION_STOPPED clean'); -} - -async function playHighEndTablet() { - await ensureCoreSDK(); - await ensurePlayImage(); - const targetAVD = join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.avd`); - const targetINI = join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.ini`); - if (!existsSync(join(targetAVD, 'config.ini')) || !existsSync(targetINI)) { - die('high-end tablet is not prepared yet'); - } - const adbExe = join(SDK, 'platform-tools', 'adb'); - run(adbExe, ['-P', ADB_PORT, 'start-server'], { env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } }); - const performanceState = (() => { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, 'get-state'], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } - }); - return result.status === 0 ? String(result.stdout ?? '').trim() : null; - })(); - if (performanceState !== 'device') { - const emulatorExe = join(SDK, 'emulator', 'emulator'); - const child = spawn(emulatorExe, [ - `@${PERFORMANCE_AVD_NAME}`, '-id', 'TFTMAC-High-End-Tablet', '-port', '5592', - '-gpu', 'host', '-skin', '1920x1080', '-cores', '8', '-memory', '8192', - '-no-snapshot', '-no-metrics', '-no-boot-anim', '-crash-report-mode', 'disabled', - '-feature', 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,AsyncComposeSupport,VirtioGpuFenceContexts', - '-append-userspace-opt', 'androidboot.opengles.version=196610' - ], { - cwd: ROOT, - env: { ...process.env, ANDROID_SDK_ROOT: SDK, ANDROID_AVD_HOME: AVD_HOME, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - detached: true, - stdio: 'ignore' - }); - child.unref(); - } - const perfADB = args => { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 16 * 1024 * 1024 - }); - return result.status === 0 ? String(result.stdout ?? '').trim() : null; - }; - await waitUntil('high-end tablet ADB', 240, () => perfADB(['get-state']) === 'device'); - await waitUntil('high-end tablet Android boot', 240, () => perfADB(['shell', 'getprop', 'sys.boot_completed'])?.replace(/\r/g, '') === '1'); - perfADB(['shell', 'wm', 'size', '1920x1080']); - perfADB(['shell', 'wm', 'density', '280']); - const installed = perfADB(['shell', 'pm', 'path', PACKAGE]); - if (!installed) die('TFT is missing from high-end tablet clone'); - perfADB(['shell', 'am', 'force-stop', PACKAGE]); - perfADB(['shell', 'am', 'start', '-n', `${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`]); - const pid = await waitUntil('TFT on high-end tablet', 120, () => perfADB(['shell', 'pidof', PACKAGE])); - const gles = perfADB(['shell', 'getprop', 'ro.opengles.version']); - const bootGles = perfADB(['shell', 'getprop', 'ro.boot.opengles.version']); - const vulkan = perfADB(['shell', 'getprop', 'ro.hardware.vulkan']); - const size = perfADB(['shell', 'wm', 'size']); - const density = perfADB(['shell', 'wm', 'density']); - console.log(JSON.stringify({ - ok: true, - avdName: PERFORMANCE_AVD_NAME, - pid, - packageName: PACKAGE, - renderTarget: size, - density, - roOpenGLESVersion: gles, - bootOpenGLESVersion: bootGles, - vulkanHardware: vulkan, - cpuCores: 8, - ramMB: 8192 - }, null, 2)); -} - -async function repairHighEndLaunchState() { - await ensureCoreSDK(); - const runPerf = args => { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 16 * 1024 * 1024 - }); - return { status: result.status, stdout: String(result.stdout ?? '').trim(), stderr: String(result.stderr ?? '').trim() }; - }; - const component = `${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`; - const packageEnable = runPerf(['shell', 'pm', 'enable', '--user', '0', PACKAGE]); - const unstop = runPerf(['shell', 'cmd', 'package', 'set-stopped-state', '--user', '0', PACKAGE, 'false']); - const activityEnable = runPerf(['shell', 'pm', 'enable', '--user', '0', component]); - const query = runPerf(['shell', 'cmd', 'package', 'resolve-activity', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.LAUNCHER', PACKAGE]); - const start = runPerf(['shell', 'am', 'start', '-W', '--user', '0', '-n', component]); - await new Promise(resolveWait => setTimeout(resolveWait, 3000)); - const pid = runPerf(['shell', 'pidof', PACKAGE]); - const top = runPerf(['shell', 'dumpsys', 'activity', 'activities']); - const result = { - packageEnable, - unstop, - activityEnable, - resolvedAfterRepair: query, - start, - pid: pid.stdout || null, - topActivities: top.stdout.split('\n').filter(line => /topResumedActivity|mResumedActivity|teamfighttactics/i.test(line)).slice(0, 100) - }; - console.log(JSON.stringify(result, null, 2)); -} - -async function reinstallHighEndFromOfficialApk() { - await ensureCoreSDK(); - await ensurePlayImage(); - const targetAVD = join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.avd`); - const targetINI = join(AVD_HOME, `${PERFORMANCE_AVD_NAME}.ini`); - if (!existsSync(join(targetAVD, 'config.ini')) || !existsSync(targetINI)) { - die('high-end tablet is not prepared yet'); - } - const adbExe = join(SDK, 'platform-tools', 'adb'); - run(adbExe, ['-P', ADB_PORT, 'start-server'], { - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } - }); - const deviceState = spawnSync(adbExe, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, 'get-state'], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } - }); - if (deviceState.status !== 0 || String(deviceState.stdout ?? '').trim() !== 'device') { - const emulatorExe = join(SDK, 'emulator', 'emulator'); - const child = spawn(emulatorExe, [ - `@${PERFORMANCE_AVD_NAME}`, '-id', 'TFTMAC-High-End-Tablet', '-port', '5592', - '-gpu', 'host', '-skin', '1920x1080', '-cores', '8', '-memory', '8192', - '-no-snapshot', '-no-metrics', '-no-boot-anim', '-crash-report-mode', 'disabled', - '-feature', 'GLESDynamicVersion,Vulkan,GuestAngle,-GLPipeChecksum,AsyncComposeSupport,VirtioGpuFenceContexts', - '-append-userspace-opt', 'androidboot.opengles.version=196610' - ], { - cwd: ROOT, - env: { ...process.env, ANDROID_SDK_ROOT: SDK, ANDROID_AVD_HOME: AVD_HOME, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - detached: true, - stdio: 'ignore' - }); - child.unref(); - } - const ready = args => { - const result = spawnSync(adbExe, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 32 * 1024 * 1024 - }); - return result.status === 0 ? String(result.stdout ?? '').trim() : null; - }; - await waitUntil('high-end tablet ADB', 240, () => ready(['get-state']) === 'device'); - await waitUntil('high-end tablet Android boot', 240, () => ready(['shell', 'getprop', 'sys.boot_completed'])?.replace(/\r/g, '') === '1'); - const activeGLES = ready(['shell', 'getprop', 'ro.opengles.version']); - if (activeGLES !== '196610') die(`high-end tablet booted without ES 3.2 capability: ${activeGLES ?? 'unknown'}`); - if (!existsSync(LIVE_MANIFEST) || !existsSync(join(LIVE_CURRENT, 'base.apk'))) { - die('official Google Play TFT capture is missing'); - } - const manifest = JSON.parse(await readFile(LIVE_MANIFEST, 'utf8')); - const basePath = join(LIVE_CURRENT, 'base.apk'); - const actualHash = await hashFile(basePath, 'sha256'); - if (manifest.packageName !== PACKAGE || manifest.baseSHA256 !== actualHash) { - die('official TFT APK hash no longer matches the captured Google Play manifest'); - } - const executable = join(SDK, 'platform-tools', 'adb'); - const install = spawnSync(executable, [ - '-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, - 'install', '--no-streaming', '-r', '-g', basePath - ], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 32 * 1024 * 1024 - }); - const runPerf = args => { - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 32 * 1024 * 1024 - }); - return { status: result.status, stdout: String(result.stdout ?? '').trim(), stderr: String(result.stderr ?? '').trim() }; - }; - const component = `${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`; - const query = runPerf(['shell', 'cmd', 'package', 'resolve-activity', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.LAUNCHER', PACKAGE]); - const start = runPerf(['shell', 'am', 'start', '-W', '--user', '0', '-n', component]); - await new Promise(resolveWait => setTimeout(resolveWait, 3000)); - const pid = runPerf(['shell', 'pidof', PACKAGE]); - console.log(JSON.stringify({ - officialBaseSHA256: actualHash, - install: { - status: install.status, - stdout: String(install.stdout ?? '').trim(), - stderr: String(install.stderr ?? '').trim() - }, - resolvedAfterInstall: query, - start, - pid: pid.stdout || null - }, null, 2)); - if (install.status !== 0) die('reinstalling the exact Google Play APK onto the high-end clone failed'); - if (!pid.stdout) die('TFT still did not stay running after PackageManager refresh'); -} - -async function inspectHighEndPackageState() { - await ensureCoreSDK(); - const runPerf = args => { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 32 * 1024 * 1024 - }); - return { status: result.status, stdout: String(result.stdout ?? '').trim(), stderr: String(result.stderr ?? '').trim() }; - }; - const dump = runPerf(['shell', 'dumpsys', 'package', PACKAGE]); - const lines = dump.stdout.split('\n'); - const userIndex = lines.findIndex(line => line.includes('User 0:')); - const userState = userIndex >= 0 ? lines.slice(userIndex, userIndex + 80) : []; - const activityIndex = lines.findIndex(line => line.includes(`${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`)); - const activityContext = activityIndex >= 0 ? lines.slice(Math.max(0, activityIndex - 10), activityIndex + 30) : []; - const disabledPackages = runPerf(['shell', 'pm', 'list', 'packages', '-d']); - const enabledPackages = runPerf(['shell', 'pm', 'list', 'packages', '-e']); - const queryMain = runPerf(['shell', 'cmd', 'package', 'query-activities', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.LAUNCHER', PACKAGE]); - const resolveDefault = runPerf(['shell', 'cmd', 'package', 'resolve-activity', '--brief', PACKAGE]); - const result = { - packageDisabled: disabledPackages.stdout.includes(`package:${PACKAGE}`), - packageEnabled: enabledPackages.stdout.includes(`package:${PACKAGE}`), - queryMain, - resolveDefault, - userState, - activityContext - }; - console.log(JSON.stringify(result, null, 2)); -} - -async function diagnoseHighEndLaunch() { - await ensureCoreSDK(); - const perfRun = args => { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 32 * 1024 * 1024 - }); - return { - status: result.status, - stdout: String(result.stdout ?? '').trim(), - stderr: String(result.stderr ?? '').trim() - }; - }; - const packageDump = perfRun(['shell', 'dumpsys', 'package', PACKAGE]); - const activities = packageDump.stdout.split('\n').filter(line => - /RiotNativeActivity|SplashActivity|MAIN|LAUNCHER|activity|enabled=|stopped=/i.test(line) - ).slice(0, 500); - const explicit = perfRun(['shell', 'am', 'start', '-W', '-n', `${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`]); - const monkey = perfRun(['shell', 'monkey', '-p', PACKAGE, '-c', 'android.intent.category.LAUNCHER', '1']); - await new Promise(resolveWait => setTimeout(resolveWait, 2000)); - const pid = perfRun(['shell', 'pidof', PACKAGE]); - const top = perfRun(['shell', 'dumpsys', 'activity', 'activities']); - const log = perfRun(['logcat', '-d', '-t', '700']); - const relevantLog = log.stdout.split('\n').filter(line => - /teamfighttactics|RiotNativeActivity|leagueoflegends|ActivityTaskManager|PackageManager|Permission Denial|SecurityException|FATAL EXCEPTION|AndroidRuntime|Unable to start|class.*not found/i.test(line) - ).slice(-300); - const result = { - explicitStart: explicit, - monkeyStart: monkey, - pid: pid.stdout || null, - topActivities: top.stdout.split('\n').filter(line => /topResumedActivity|mResumedActivity|teamfighttactics/i.test(line)).slice(0, 100), - packageActivityEvidence: activities, - relevantLog - }; - console.log(JSON.stringify(result, null, 2)); -} - -async function inspectHighEndTablet() { - await ensureCoreSDK(); - const perfADB = args => { - const executable = join(SDK, 'platform-tools', 'adb'); - const result = spawnSync(executable, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 32 * 1024 * 1024 - }); - return { - ok: result.status === 0, - status: result.status, - stdout: String(result.stdout ?? '').trim(), - stderr: String(result.stderr ?? '').trim() - }; - }; - const state = perfADB(['get-state']); - const boot = perfADB(['shell', 'getprop', 'sys.boot_completed']); - const packagePath = perfADB(['shell', 'pm', 'path', PACKAGE]); - const resolve = perfADB(['shell', 'cmd', 'package', 'resolve-activity', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.LAUNCHER', PACKAGE]); - const pid = perfADB(['shell', 'pidof', PACKAGE]); - const activity = perfADB(['shell', 'dumpsys', 'activity', 'activities']); - const packageDump = perfADB(['shell', 'dumpsys', 'package', PACKAGE]); - const gles = perfADB(['shell', 'getprop', 'ro.opengles.version']); - const bootGles = perfADB(['shell', 'getprop', 'ro.boot.opengles.version']); - const vulkan = perfADB(['shell', 'getprop', 'ro.hardware.vulkan']); - const angle = perfADB(['shell', 'getprop', 'ro.hardware.egl']); - const size = perfADB(['shell', 'wm', 'size']); - const density = perfADB(['shell', 'wm', 'density']); - const display = perfADB(['shell', 'dumpsys', 'display']); - const crash = perfADB(['logcat', '-d', '-t', '1200']); - const filteredLog = crash.stdout.split('\n').filter(line => - /teamfighttactics|RiotNativeActivity|leagueoflegends|FATAL EXCEPTION|AndroidRuntime|SIG(SEGV|ABRT)|crash|vulkan|angle|egl|gles|linker/i.test(line) - ).slice(-400); - const topActivities = activity.stdout.split('\n').filter(line => - /topResumedActivity|mResumedActivity|teamfighttactics|AccountPicker|SignInHub/i.test(line) - ).slice(0, 200); - const result = { - ok: state.ok && state.stdout === 'device' && boot.stdout.replace(/\r/g, '') === '1', - state, - boot: boot.stdout, - packageInstalled: packagePath.ok && packagePath.stdout.includes('package:'), - packagePath: packagePath.stdout, - resolvedActivity: resolve.stdout, - pid: pid.stdout || null, - openGLESVersion: gles.stdout, - bootOpenGLESVersion: bootGles.stdout, - vulkanHardware: vulkan.stdout, - eglHardware: angle.stdout, - displaySize: size.stdout, - displayDensity: density.stdout, - displayTabletEvidence: display.stdout.split('\n').filter(line => /smallest|density|DisplayDeviceInfo|1920|1080/i.test(line)).slice(0, 100), - topActivities, - packageEnabledState: packageDump.stdout.split('\n').filter(line => /User 0:|enabled=|stopped=|hidden=|suspended=/i.test(line)).slice(0, 50), - filteredLog - }; - await writeFile(join(LIVE_ROOT, 'high-end-tablet-inspection.json'), `${JSON.stringify(result, null, 2)}\n`); - console.log(JSON.stringify(result, null, 2)); -} - -async function inspectPostPatch() { - await mkdir(ACQ, { recursive: true }); - await ensureCoreSDK(); - await ensurePlayImage(); - await ensureAcquisitionAVD(); - const adbExe = join(SDK, 'platform-tools', 'adb'); - run(adbExe, ['-P', ADB_PORT, 'start-server'], { env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } }); - if (!adbMaybe(['get-state'])) die('Google Play TFT device is not running; launch TFT before post-patch inspection'); - const pid = adbMaybe(['shell', 'pidof', PACKAGE]) ?? ''; - const activity = adbMaybe(['shell', 'dumpsys', 'activity', 'activities']) ?? ''; - const packageDump = adbMaybe(['shell', 'dumpsys', 'package', PACKAGE]) ?? ''; - const layers = adbMaybe(['shell', 'dumpsys', 'SurfaceFlinger', '--list']) ?? ''; - const gl = adbMaybe(['shell', 'getprop']) ?? ''; - const storageCandidates = []; - for (const root of [ - `/sdcard/Android/data/${PACKAGE}`, - `/storage/emulated/0/Android/data/${PACKAGE}`, - `/sdcard/Android/obb/${PACKAGE}`, - `/storage/emulated/0/Android/obb/${PACKAGE}` - ]) { - const listing = adbMaybe(['shell', 'find', root, '-maxdepth', '6', '-type', 'f']) ?? ''; - const matches = listing.split('\n').filter(Boolean).filter(name => /(?:unreal|\.pak$|\.ucas$|\.utoc$|deviceprofile|engine\.ini|rhi|vulkan|shader|patch)/i.test(name)); - if (matches.length) storageCandidates.push({ root, matches: matches.slice(0, 300) }); - } - const maps = pid ? (adbMaybe(['shell', 'cat', `/proc/${pid.split(/\s+/)[0]}/maps`]) ?? '') : ''; - const mappedLibraries = maps.split('\n') - .map(line => line.trim().split(/\s+/).pop()) - .filter(Boolean) - .filter(path => /(?:unreal|leagueoflegends|riot|vulkan|angle|gles|egl|shader)/i.test(path)); - const topActivity = activity.split('\n').find(line => line.includes('topResumedActivity=ActivityRecord'))?.trim() ?? null; - const gameLayers = layers.split('\n').filter(line => /teamfighttactics|unreal|surfaceview/i.test(line)).slice(0, 100); - const graphicsProperties = gl.split('\n').filter(line => /(?:egl|gles|vulkan|angle|gpu)/i.test(line)).slice(0, 200); - const result = { - observedAt: new Date().toISOString(), - packageName: PACKAGE, - pid: pid || null, - topActivity, - versionName: packageDump.match(/versionName=([^\s]+)/)?.[1] ?? null, - versionCode: packageDump.match(/versionCode=(\d+)/)?.[1] ?? null, - unrealEvidence: mappedLibraries.some(path => /unreal/i.test(path)) || storageCandidates.some(entry => entry.matches.some(path => /unreal|\.pak$|\.ucas$|\.utoc$/i.test(path))), - mappedLibraries: [...new Set(mappedLibraries)].slice(0, 300), - gameLayers, - graphicsProperties, - storageCandidates - }; - await writeFile(join(LIVE_ROOT, 'post-patch-inspection.json'), `${JSON.stringify(result, null, 2)}\n`); - console.log(JSON.stringify(result, null, 2)); -} - -async function inspectLive() { - if (!existsSync(LIVE_MANIFEST)) die('live TFT has not been acquired yet'); - const manifest = JSON.parse(await readFile(LIVE_MANIFEST, 'utf8')); - if (manifest.packageName !== PACKAGE || !Array.isArray(manifest.apks) || manifest.apks.length < 1 || manifest.apks[0].name !== 'base.apk') die('live manifest identity is invalid'); - for (const apk of manifest.apks) { - const path = join(LIVE_CURRENT, apk.name); - if (!existsSync(path)) die(`missing live split ${apk.name}`); - const info = await stat(path); - const sha = await hashFile(path); - if (info.size !== apk.size || sha !== apk.sha256) die(`chain-of-custody mismatch for ${apk.name}`); - } - const engineEvidence = []; - for (const apk of manifest.apks) { - const listing = run('/usr/bin/unzip', ['-l', join(LIVE_CURRENT, apk.name)], { capture: true }); - const names = listing - .split('\n') - .map(line => line.trim().split(/\s+/).pop()) - .filter(Boolean); - const candidates = names.filter(name => /(?:libUnreal|libUE4|libmain|libGame|libRiot|libunity|libil2cpp|libmono|UECommandLine|DefaultEngine|DeviceProfiles|\.uproject)/i.test(name)); - const nativeLibraries = names.filter(name => /^lib\/arm64-v8a\/[^/]+\.so$/i.test(name)); - if (candidates.length || nativeLibraries.length) engineEvidence.push({ - apk: apk.name, - candidates: [...new Set(candidates)].slice(0, 200), - nativeLibraries: [...new Set(nativeLibraries)].slice(0, 300) - }); - } - console.log(JSON.stringify({ - ok: true, - packageName: manifest.packageName, - version: manifest.version, - versionCode: manifest.versionCode, - launchActivity: manifest.launchActivity ?? null, - donorUECommandLineCompatible: manifest.donorUECommandLineCompatible === true, - splits: manifest.apks.length, - baseSHA256: manifest.baseSHA256, - engineEvidence - }, null, 2)); -} - -function runZsh(script, env = {}) { - run('/bin/zsh', [script], { env: { ...process.env, ...env } }); -} - -async function playStock() { - await mkdir(ACQ, { recursive: true }); - await ensureCoreSDK(); - await ensurePlayImage(); - await ensureAcquisitionAVD(); - const adbExe = join(SDK, 'platform-tools', 'adb'); - run(adbExe, ['-P', ADB_PORT, 'start-server'], { env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } }); - if (!adbMaybe(['get-state'])) { - const emulatorExe = join(SDK, 'emulator', 'emulator'); - const child = spawn(emulatorExe, [`@${AVD_NAME}`, '-id', 'TFTMAC-Live', '-port', '5590', '-gpu', 'host', '-skin', '1920x1080', '-cores', '6', '-memory', '6144', '-no-snapshot', '-no-metrics', '-no-boot-anim', '-crash-report-mode', 'disabled'], { - cwd: ROOT, - env: { ...process.env, ANDROID_SDK_ROOT: SDK, ANDROID_AVD_HOME: AVD_HOME, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - detached: true, - stdio: 'ignore' - }); - child.unref(); - } - await waitUntil('live TFT emulator ADB', 240, () => adbMaybe(['get-state']) === 'device'); - await waitUntil('Android boot', 240, () => adbMaybe(['shell', 'getprop', 'sys.boot_completed'])?.replace(/\r/g, '') === '1'); - const installed = adbMaybe(['shell', 'pm', 'path', PACKAGE]); - if (!installed) die('live TFT is no longer installed in the Google Play device'); - const manifest = existsSync(LIVE_MANIFEST) ? JSON.parse(await readFile(LIVE_MANIFEST, 'utf8')) : null; - const component = manifest?.launchActivity || `${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`; - adb(['shell', 'am', 'force-stop', PACKAGE], false); - adb(['shell', 'am', 'start', '-n', component], false); - const pid = await waitUntil('live TFT process', 120, () => adbMaybe(['shell', 'pidof', PACKAGE])); - console.log(`TFTMAC_STOCK_LAUNCHED pid=${pid} component=${component}`); - console.log('TFTMAC: Riot live TFT is running in the Google Play device.'); -} - -function loggedInUserHome() { - const user = run('/usr/bin/id', ['-un'], { capture: true }); - const record = run('/usr/bin/dscl', ['.', '-read', `/Users/${user}`, 'NFSHomeDirectory'], { capture: true }); - const marker = 'NFSHomeDirectory:'; - if (!record.startsWith(marker)) die(`could not resolve home directory for ${user}`); - return record.slice(marker.length).trim(); -} - -async function installStableRuntime() { - const home = loggedInUserHome(); - const stable = join(home, 'Library', 'Application Support', 'TFTMAC'); - const stableNext = `${stable}.next`; - const adbExe = join(SDK, 'platform-tools', 'adb'); - const adbState = serial => { - const result = spawnSync(adbExe, ['-P', ADB_PORT, '-s', serial, 'get-state'], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } - }); - return result.status === 0 ? String(result.stdout ?? '').trim() : null; - }; - const killDevice = serial => { - spawnSync(adbExe, ['-P', ADB_PORT, '-s', serial, 'emu', 'kill'], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' } - }); - }; - killDevice(SERIAL); - killDevice(PERFORMANCE_SERIAL); - const deadline = Date.now() + 60000; - while (Date.now() < deadline && (adbState(SERIAL) || adbState(PERFORMANCE_SERIAL))) { - await new Promise(resolveWait => setTimeout(resolveWait, 500)); - } - if (adbState(SERIAL) || adbState(PERFORMANCE_SERIAL)) { - die('Android devices did not shut down cleanly before runtime migration'); - } - await rm(stableNext, { recursive: true, force: true }); - await mkdir(stableNext, { recursive: true }); - run('/bin/cp', ['-R', join(ACQ, 'sdk'), join(stableNext, 'sdk')]); - run('/bin/cp', ['-R', join(ACQ, 'avd'), join(stableNext, 'avd')]); - for (const avdName of [AVD_NAME, PERFORMANCE_AVD_NAME]) { - const ini = join(stableNext, 'avd', `${avdName}.ini`); - if (!existsSync(ini)) continue; - const finalAVD = join(stable, 'avd', `${avdName}.avd`); - await writeFile(ini, `avd.ini.encoding=UTF-8\npath=${finalAVD}\npath.rel=${avdName}.avd\ntarget=android-36\n`); - } - await writeFile(join(stableNext, 'runtime.json'), `${JSON.stringify({ - schemaVersion: 1, - packageName: PACKAGE, - launchActivity: `${PACKAGE}/com.riotgames.leagueoflegends.RiotNativeActivity`, - acquiredVersion: existsSync(LIVE_MANIFEST) ? JSON.parse(await readFile(LIVE_MANIFEST, 'utf8')).version : null, - source: 'Google Play', - defaultAVD: PERFORMANCE_AVD_NAME, - renderTarget: '1920x1080', - densityDpi: 280, - cpuCores: 8, - ramMB: 8192, - enhancedOpenGLESVersion: 196610, - installedAt: new Date().toISOString() - }, null, 2)}\n`); - const backup = `${stable}.previous`; - await rm(backup, { recursive: true, force: true }); - if (existsSync(stable)) await rename(stable, backup); - await rename(stableNext, stable); - await rm(backup, { recursive: true, force: true }); - console.log(`TFTMAC_RUNTIME_INSTALLED ${stable}`); -} - -async function testAll() { - runZsh(join(ROOT, 'scripts', 'test-mactician.command')); - const source = join(ROOT, 'tftmac', 'Sources', 'TFTMACApp.swift'); - run('/usr/bin/xcrun', ['swiftc', '-parse-as-library', '-typecheck', '-target', 'arm64-apple-macosx12.0', source]); - const shellSource = await readFile(source, 'utf8'); - if (!shellSource.includes('"1920x1080"') || !shellSource.includes('androidboot.opengles.version=196610')) { - die('TFTMAC Enhanced 1080p graphics contract is missing'); - } - const graphicsContract = await readFile(join(ROOT, 'docs', 'TFTMAC_GRAPHICS_ARCHITECTURE.md'), 'utf8'); - if (!graphicsContract.includes('Primary target') || !graphicsContract.includes('617dp tablet class') || !graphicsContract.includes('UnrealEnhancedAdapter')) { - die('TFTMAC graphics architecture contract is incomplete'); - } - console.log('TFTMAC_TESTS_OK donor=green native_shell=green graphics_contract=green'); -} - -async function build() { - await inspectLive(); - const source = join(ROOT, 'tftmac', 'Sources', 'TFTMACApp.swift'); - const plist = join(ROOT, 'tftmac', 'Info.plist'); - const dist = join(ROOT, 'dist'); - const app = join(dist, 'TFTMAC.app'); - const contents = join(app, 'Contents'); - const macOS = join(contents, 'MacOS'); - const version = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', plist], { capture: true }); - const dmg = join(dist, `TFTMAC-${version}.dmg`); - await rm(app, { recursive: true, force: true }); - await rm(dmg, { force: true }); - await mkdir(macOS, { recursive: true }); - await copyFile(plist, join(contents, 'Info.plist')); - run('/usr/bin/xcrun', ['swiftc', '-O', '-parse-as-library', '-target', 'arm64-apple-macosx12.0', source, '-o', join(macOS, 'TFTMAC')]); - run('/usr/bin/codesign', ['--force', '--sign', '-', '--timestamp=none', '--options', 'runtime', app]); - run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=2', app]); - const dmgRoot = join(dist, '.tftmac-dmg-root'); - await rm(dmgRoot, { recursive: true, force: true }); - await mkdir(dmgRoot, { recursive: true }); - run('/bin/cp', ['-R', app, join(dmgRoot, 'TFTMAC.app')]); - run('/bin/ln', ['-s', '/Applications', join(dmgRoot, 'Applications')]); - run('/usr/bin/hdiutil', ['create', '-size', '32m', '-fs', 'HFS+', '-volname', 'TFTMAC', '-srcfolder', dmgRoot, '-ov', '-format', 'UDZO', dmg]); - await rm(dmgRoot, { recursive: true, force: true }); - console.log(`TFTMAC_BUILT app=${app}`); - console.log(`TFTMAC_DMG ${dmg}`); -} - -async function acceptance() { - const app = join(ROOT, 'dist', 'TFTMAC.app'); - const version = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', join(ROOT, 'tftmac', 'Info.plist')], { capture: true }); - const dmg = join(ROOT, 'dist', `TFTMAC-${version}.dmg`); - if (!existsSync(app) || !existsSync(dmg)) die('TFTMAC app/DMG build outputs are missing'); - run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=2', app]); - const identifier = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', join(app, 'Contents', 'Info.plist')], { capture: true }); - const displayName = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleDisplayName', join(app, 'Contents', 'Info.plist')], { capture: true }); - if (identifier !== 'com.flashls1.tftmac' || displayName !== 'TFTMAC') die(`unexpected built identity: ${identifier} / ${displayName}`); - await installStableRuntime(); - const installedApp = '/Applications/TFTMAC.app'; - await rm(installedApp, { recursive: true, force: true }); - run('/bin/cp', ['-R', app, installedApp]); - run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=2', installedApp]); - run('/usr/bin/open', ['-n', installedApp]); - const home = loggedInUserHome(); - const stable = join(home, 'Library', 'Application Support', 'TFTMAC'); - const stableADB = join(stable, 'sdk', 'platform-tools', 'adb'); - const stableRun = args => { - const result = spawnSync(stableADB, ['-P', ADB_PORT, '-s', PERFORMANCE_SERIAL, ...args], { - encoding: 'utf8', - env: { ...process.env, ANDROID_ADB_SERVER_PORT: ADB_PORT, ADB_MDNS_AUTO_CONNECT: '' }, - maxBuffer: 16 * 1024 * 1024 - }); - return result.status === 0 ? String(result.stdout ?? '').trim() : null; - }; - await waitUntil('installed TFTMAC high-end tablet ADB', 240, () => stableRun(['get-state']) === 'device'); - await waitUntil('installed TFTMAC Android boot', 240, () => stableRun(['shell', 'getprop', 'sys.boot_completed'])?.replace(/\r/g, '') === '1'); - const pid = await waitUntil('installed TFTMAC Riot process', 120, () => stableRun(['shell', 'pidof', PACKAGE])); - const gles = stableRun(['shell', 'getprop', 'ro.opengles.version']); - const egl = stableRun(['shell', 'getprop', 'ro.hardware.egl']); - const size = stableRun(['shell', 'wm', 'size']); - const density = stableRun(['shell', 'wm', 'density']); - const resolved = stableRun(['shell', 'cmd', 'package', 'resolve-activity', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.LAUNCHER', PACKAGE]); - if (gles !== '196610' || egl !== 'angle' || !size?.includes('1920x1080') || !density?.includes('280') || !resolved?.includes(EXPECTED_ACTIVITY)) { - die(`installed TFTMAC runtime did not match enhanced 1080p contract: gles=${gles} egl=${egl} size=${size} density=${density} activity=${resolved}`); - } - console.log(`TFTMAC_ACCEPTED app=${installedApp} pid=${pid} gles=${gles} egl=${egl} size=${size} density=${density}`); -} - -const action = process.argv[2]; -switch (action) { - case 'test': await testAll(); break; - case 'acquire-live': await acquireLive(); break; - case 'inspect-live': await inspectLive(); break; - case 'inspect-post-patch': await inspectPostPatch(); break; - case 'stop-acquisition': await stopAcquisition(); break; - case 'prepare-high-end-tablet': await prepareHighEndTablet(); break; - case 'play-high-end-tablet': await playHighEndTablet(); break; - case 'inspect-high-end-tablet': await inspectHighEndTablet(); break; - case 'diagnose-high-end-launch': await diagnoseHighEndLaunch(); break; - case 'inspect-high-end-package-state': await inspectHighEndPackageState(); break; - case 'repair-high-end-launch-state': await repairHighEndLaunchState(); break; - case 'reinstall-high-end-official-apk': await reinstallHighEndFromOfficialApk(); break; - case 'play-stock': await playStock(); break; - case 'build': await build(); break; - case 'acceptance': await acceptance(); break; - default: die('usage: node tools/clara-task.mjs test|acquire-live|inspect-live|inspect-post-patch|stop-acquisition|prepare-high-end-tablet|play-high-end-tablet|inspect-high-end-tablet|diagnose-high-end-launch|inspect-high-end-package-state|repair-high-end-launch-state|reinstall-high-end-official-apk|play-stock|build|acceptance'); -} diff --git a/tools/tft-input-bridge.swift b/tools/tft-input-bridge.swift index b024428..9a89297 100644 --- a/tools/tft-input-bridge.swift +++ b/tools/tft-input-bridge.swift @@ -302,7 +302,7 @@ private final class InputDiagnostics { private final class TapDispatcher { private let configuration: Configuration - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.input-bridge.adb") + private let queue = DispatchQueue(label: "com.flashls1.tftmac.input-bridge.adb") private var process: Process? private var input: FileHandle? private var showTraitsNext = true @@ -395,7 +395,7 @@ private final class AndroidActivityMonitor { private static let stateMaxAge: TimeInterval = 3 private let configuration: Configuration - private let queue = DispatchQueue(label: "dev.sergeinaumov.mactician.input-bridge.activity") + private let queue = DispatchQueue(label: "com.flashls1.tftmac.input-bridge.activity") private let stateLock = NSLock() private var state = ForegroundActivityState.unknown private var stateUpdatedAt: TimeInterval = 0 diff --git a/tools/tftmac-clean-source-layers.mjs b/tools/tftmac-clean-source-layers.mjs new file mode 100644 index 0000000..cf3bf5a --- /dev/null +++ b/tools/tftmac-clean-source-layers.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const buildRoot = '/Volumes/MAC MINI M4/TFTMAC/Build'; +const reclaimRoot = '/Volumes/MAC MINI M4/TFTMAC/Build.reclaiming'; +const runtimeRoot = '/Volumes/MAC MINI M4/TFTMAC/Runtime'; +const receipt = path.resolve('ssot/storage-reclamation.json'); +const sourceAuthority = path.resolve('Vendor/AndroidEmulator/SOURCE.json'); + +function run(exe, args, options = {}) { + return spawnSync(exe, args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, ...options }); +} +function allocatedBytes(root) { + if (!fs.existsSync(root)) return 0; + const result = run('/usr/bin/du', ['-sk', root]); + if (result.status !== 0) throw new Error(`du failed for ${root}: ${result.stderr || result.stdout}`); + const kib = Number(String(result.stdout).trim().split(/\s+/)[0]); + if (!Number.isFinite(kib)) throw new Error(`Could not parse du output for ${root}`); + return kib * 1024; +} +function sha256(file) { + const h = crypto.createHash('sha256'); + h.update(fs.readFileSync(file)); + return h.digest('hex'); +} +function fail(message) { throw new Error(message); } +function df() { + const result = run('/bin/df', ['-kP', '/Volumes/MAC MINI M4']); + if (result.status !== 0) fail(`df failed: ${result.stderr || result.stdout}`); + return result.stdout.trim(); +} + +if (!fs.existsSync('/Volumes/MAC MINI M4')) fail('External volume is not mounted.'); +if (!fs.existsSync(runtimeRoot)) fail('Authoritative TFTMAC Runtime root is missing.'); +if (!path.resolve(buildRoot).startsWith('/Volumes/MAC MINI M4/TFTMAC/')) fail('Unsafe Build path.'); +if (path.resolve(buildRoot) === path.resolve(runtimeRoot)) fail('Build and Runtime roots collide.'); + +const ps = run('/bin/ps', ['-axo', 'pid=,command=']); +if (ps.status !== 0) fail('Could not inspect process table.'); +const liveBuildUsers = ps.stdout.split(/\r?\n/).filter(line => line.includes(buildRoot) || line.includes(reclaimRoot)); +if (liveBuildUsers.length) fail(`Build tree is not quiescent: ${liveBuildUsers.join(' | ')}`); + +const existingReceipt = fs.existsSync(receipt) ? JSON.parse(fs.readFileSync(receipt, 'utf8')) : null; +const dfBefore = existingReceipt?.dfBefore ?? df(); +const runtimeBytesBefore = existingReceipt?.runtimeBytesBefore ?? allocatedBytes(runtimeRoot); +const buildBytesBefore = existingReceipt?.buildBytesBefore ?? allocatedBytes(fs.existsSync(buildRoot) ? buildRoot : reclaimRoot); + +if (fs.existsSync(buildRoot) && fs.existsSync(reclaimRoot)) fail('Both Build and Build.reclaiming exist; refusing ambiguous deletion.'); +if (fs.existsSync(buildRoot)) fs.renameSync(buildRoot, reclaimRoot); + +const staged = { + schema: 1, + observedAt: existingReceipt?.observedAt ?? new Date().toISOString(), + buildRoot, + reclaimRoot, + runtimeRoot, + quiescence: { liveBuildUsers: [] }, + buildBytesBefore, + runtimeBytesBefore, + dfBefore, + state: fs.existsSync(reclaimRoot) ? 'RECLAIMING' : 'ALREADY_ABSENT' +}; +fs.writeFileSync(receipt, `${JSON.stringify(staged, null, 2)}\n`); + +if (fs.existsSync(reclaimRoot)) fs.rmSync(reclaimRoot, { recursive: true, force: false, maxRetries: 3, retryDelay: 250 }); +if (fs.existsSync(buildRoot) || fs.existsSync(reclaimRoot)) fail('Build reclamation is incomplete.'); + +const authority = JSON.parse(fs.readFileSync(sourceAuthority, 'utf8')); +const sdkRoot = path.join(runtimeRoot, 'sdk'); +const emulator = path.join(sdkRoot, 'emulator', 'emulator'); +const adb = path.join(sdkRoot, 'platform-tools', 'adb'); +const installedProto = path.join(sdkRoot, 'emulator', 'lib', 'emulator_controller.proto'); +const avdIni = path.join(runtimeRoot, 'avd', 'TFT_Ultra_Tablet.ini'); +for (const required of [emulator, adb, installedProto, avdIni]) { + if (!fs.existsSync(required)) fail(`Authoritative Runtime component missing after reclamation: ${required}`); +} +const installedProtoSHA256 = sha256(installedProto); +if (installedProtoSHA256 !== authority.installedProtoSHA256 || installedProtoSHA256 !== authority.vendoredProtoSHA256) { + fail(`Runtime protocol authority changed: ${installedProtoSHA256}`); +} + +const runtimeBytesAfter = allocatedBytes(runtimeRoot); +const output = { + ...staged, + completedAt: new Date().toISOString(), + buildBytesAfter: 0, + reclaimedBytes: buildBytesBefore, + runtimeBytesAfter, + runtimeAllocatedByteDelta: runtimeBytesAfter - runtimeBytesBefore, + runtimeIntegrity: { + emulatorPresent: true, + adbPresent: true, + avdPresent: true, + installedProtoSHA256, + frozenProtoMatch: true + }, + dfAfter: df(), + state: 'COMPLETE', + result: 'PASS' +}; +fs.writeFileSync(receipt, `${JSON.stringify(output, null, 2)}\n`); +process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); diff --git a/tools/tftmac-direct-control.mjs b/tools/tftmac-direct-control.mjs index 77e1b06..40955de 100644 --- a/tools/tftmac-direct-control.mjs +++ b/tools/tftmac-direct-control.mjs @@ -17,15 +17,15 @@ const PLAY_PROFILE_LABEL = 'TFT Ultra Tablet - 13.5in Freeform / Galaxy Tab S10 const PLAY_DISPLAY_WIDTH = 2960; const PLAY_DISPLAY_HEIGHT = 1848; const PLAY_DISPLAY_DENSITY = 320; -const PLAY_RAM_MB = 8192; -const DONOR_PROFILE = Object.freeze({ - id: 'mactician_compatible_official_v0', - label: 'Mactician-compatible official TFT control', +const PLAY_RAM_MB = 5120; +const BASELINE_PROFILE = Object.freeze({ + id: 'tftmac_5gb_native_v1', + label: 'TFTMAC native playable 5 GiB baseline', width: 1920, height: 1080, density: 320, vcpu: 6, - ramMB: 6144, + ramMB: 5120, refreshHz: 60, glTransport: 'virtio-gpu-asg', asgWriteBufferSize: 1048576, @@ -36,9 +36,11 @@ const DONOR_PROFILE = Object.freeze({ angleEnabledFeatures: 'exposeNonConformantExtensionsAndVersions:exposeES32ForTesting', angleDisabledFeatures: 'preferSubmitAtFBOBoundary' }); -const ADB_PORT = '5040'; -const SERIAL = 'emulator-5592'; -const EMULATOR_PORT = '5592'; +// Preserve the donor topology that completed real TFT games in the logged-in +// macOS user session. 5040/5592 was the service-context regression. +const ADB_PORT = '5038'; +const SERIAL = 'emulator-5582'; +const EMULATOR_PORT = '5582'; const EXTERNAL_ROOT = '/Volumes/MAC MINI M4/TFTMAC/Runtime'; function resolveConsoleUserHome() { const userResult = spawnSync('/usr/bin/stat', ['-f', '%Su', '/dev/console'], { encoding: 'utf8' }); @@ -79,6 +81,66 @@ function writeJSON(file, value) { ensureDir(path.dirname(file)); fs.writeFileSyn function sha256File(file) { const h = crypto.createHash('sha256'); h.update(fs.readFileSync(file)); return h.digest('hex'); } function matchNumber(text, pattern) { const match = String(text ?? '').match(pattern); return match ? Number(match[1]) : null; } +function bootstrapNativeGate1Authority() { + const runtime = discover(); + const installedProto = path.join(runtime.sdkRoot, 'emulator', 'lib', 'emulator_controller.proto'); + if (!exists(installedProto)) throw new Error(`INSTALLED_EMULATOR_CONTROLLER_PROTO_MISSING: ${installedProto}`); + const vendorRoot = path.join(repoRoot, 'Vendor', 'AndroidEmulator'); + const vendoredProto = path.join(vendorRoot, 'emulator_controller.proto'); + const sourcePath = path.join(vendorRoot, 'SOURCE.json'); + ensureDir(vendorRoot); + const installedSHA256 = sha256File(installedProto); + if (exists(vendoredProto)) { + const currentSHA256 = sha256File(vendoredProto); + if (currentSHA256 !== installedSHA256) { + throw new Error(`EMULATOR_CONTROLLER_PROTO_DRIFT: vendored=${currentSHA256} installed=${installedSHA256}`); + } + } else { + fs.copyFileSync(installedProto, vendoredProto); + } + const phase0 = readJSON(path.join(repoRoot, 'ssot', 'phase0-source.json'), {}); + const source = { + schema: 1, + observedAt: nowISO(), + authority: 'INSTALLED_ANDROID_EMULATOR', + emulatorVersion: runtime.emulatorVersion, + emulatorBinary: runtime.emulator, + installedProtoPath: installedProto, + installedProtoSHA256: installedSHA256, + vendoredProtoPath: path.relative(repoRoot, vendoredProto), + vendoredProtoSHA256: sha256File(vendoredProto), + aemuAuthority: { + branch: phase0.aemuBranch ?? null, + qemuCommit: phase0.commits?.qemu ?? null, + aemuCommit: phase0.commits?.aemu ?? null, + manifestSHA256: phase0.manifestSHA256 ?? null + }, + contract: 'The vendored protocol bytes come directly from the installed stock Android Emulator runtime used by TFTMAC. Build/generation must fail on byte drift until the authority is intentionally refreshed.' + }; + writeJSON(sourcePath, source); + return { + action: 'NATIVE_GATE1_EMULATOR_PROTOCOL_FROZEN', + emulatorVersion: runtime.emulatorVersion, + installedProto, + installedProtoSHA256: installedSHA256, + vendoredProto: path.relative(repoRoot, vendoredProto), + source: path.relative(repoRoot, sourcePath), + sourceSHA256: sha256File(sourcePath) + }; +} + +function nativeGate1Script(scriptName, actionName) { + const script = path.join(repoRoot, 'scripts', scriptName); + if (!exists(script)) throw new Error(`NATIVE_GATE1_SCRIPT_MISSING: ${script}`); + const result = command('/bin/zsh', [script], { allowFailure: true, timeout: 3600000, maxBuffer: 128 * 1024 * 1024 }); + if (result.status !== 0) { + const stdoutTail = result.stdout.slice(-24000); + const stderrTail = result.stderr.slice(-24000); + throw new Error(`${actionName}_FAILED\nSTDOUT_TAIL:\n${stdoutTail}\nSTDERR_TAIL:\n${stderrTail}`); + } + return { action: actionName, status: result.status, stdout: result.stdout.trim(), stderr: result.stderr.trim() || null }; +} + function command(executablePath, args = [], options = {}) { const result = spawnSync(executablePath, args, { encoding: 'utf8', @@ -653,16 +715,16 @@ function startEmulator(runtime, captureDir) { return child.pid; } -function donorConfigBackupPath(runtime) { - if (!runtime.avdDir) throw new Error('Donor control requires a resolved AVD directory.'); - return path.join(runtime.avdDir, 'config.ini.tftmac-donor-backup'); +function baselineConfigBackupPath(runtime) { + if (!runtime.avdDir) throw new Error('Baseline control requires a resolved AVD directory.'); + return path.join(runtime.avdDir, 'config.ini.tftmac-baseline-backup'); } -function prepareDonorAVD(runtime, drawFlushInterval = DONOR_PROFILE.drawFlushInterval) { - if (!runtime.avdDir || !runtime.avdConfig) throw new Error('Donor control requires an existing official Play AVD.'); - if (!isUnder(runtime.avdDir, EXTERNAL_ROOT)) throw new Error('Donor control AVD must remain on the external runtime volume.'); +function prepareBaselineAVD(runtime, drawFlushInterval = BASELINE_PROFILE.drawFlushInterval) { + if (!runtime.avdDir || !runtime.avdConfig) throw new Error('Baseline control requires an existing official Play AVD.'); + if (!isUnder(runtime.avdDir, EXTERNAL_ROOT)) throw new Error('Baseline control AVD must remain on the external runtime volume.'); const configPath = path.join(runtime.avdDir, 'config.ini'); - const backupPath = donorConfigBackupPath(runtime); + const backupPath = baselineConfigBackupPath(runtime); if (exists(backupPath)) { const restored = fs.readFileSync(backupPath, 'utf8'); fs.writeFileSync(configPath, restored); @@ -672,27 +734,27 @@ function prepareDonorAVD(runtime, drawFlushInterval = DONOR_PROFILE.drawFlushInt fs.writeFileSync(backupPath, baseline); let config = baseline; const values = { - 'hw.cpu.ncore': String(DONOR_PROFILE.vcpu), - 'hw.ramSize': String(DONOR_PROFILE.ramMB), - 'hw.lcd.width': String(DONOR_PROFILE.width), - 'hw.lcd.height': String(DONOR_PROFILE.height), - 'hw.lcd.density': String(DONOR_PROFILE.density), + 'hw.cpu.ncore': String(BASELINE_PROFILE.vcpu), + 'hw.ramSize': String(BASELINE_PROFILE.ramMB), + 'hw.lcd.width': String(BASELINE_PROFILE.width), + 'hw.lcd.height': String(BASELINE_PROFILE.height), + 'hw.lcd.density': String(BASELINE_PROFILE.density), 'hw.gpu.enabled': 'yes', 'hw.gpu.mode': 'host', - 'hw.gltransport': DONOR_PROFILE.glTransport, + 'hw.gltransport': BASELINE_PROFILE.glTransport, 'hw.gltransport.drawFlushInterval': String(drawFlushInterval), - 'hw.gltransport.asg.writeBufferSize': String(DONOR_PROFILE.asgWriteBufferSize), - 'hw.gltransport.asg.writeStepSize': String(DONOR_PROFILE.asgWriteStepSize), - 'hw.gltransport.asg.dataRingSize': String(DONOR_PROFILE.asgDataRingSize), + 'hw.gltransport.asg.writeBufferSize': String(BASELINE_PROFILE.asgWriteBufferSize), + 'hw.gltransport.asg.writeStepSize': String(BASELINE_PROFILE.asgWriteStepSize), + 'hw.gltransport.asg.dataRingSize': String(BASELINE_PROFILE.asgDataRingSize), 'showDeviceFrame': 'no', - 'skin.name': `${DONOR_PROFILE.width}x${DONOR_PROFILE.height}`, + 'skin.name': `${BASELINE_PROFILE.width}x${BASELINE_PROFILE.height}`, 'fastboot.forceColdBoot': 'yes', 'fastboot.forceFastBoot': 'no' }; for (const [key, value] of Object.entries(values)) config = setIniValue(config, key, value); fs.writeFileSync(configPath, config); return { - profile: DONOR_PROFILE, + profile: BASELINE_PROFILE, configPath, configSHA256: sha256File(configPath), baselineSHA256: crypto.createHash('sha256').update(baseline).digest('hex'), @@ -700,41 +762,42 @@ function prepareDonorAVD(runtime, drawFlushInterval = DONOR_PROFILE.drawFlushInt }; } -function restoreDonorAVD(runtime) { +function restoreBaselineAVD(runtime) { if (!runtime?.avdDir) return { restored: false, reason: 'AVD_UNRESOLVED' }; const configPath = path.join(runtime.avdDir, 'config.ini'); - const backupPath = donorConfigBackupPath(runtime); - if (!exists(backupPath)) return { restored: false, reason: 'NO_DONOR_BACKUP' }; + const backupPath = baselineConfigBackupPath(runtime); + if (!exists(backupPath)) return { restored: false, reason: 'NO_BASELINE_BACKUP' }; const baseline = fs.readFileSync(backupPath, 'utf8'); fs.writeFileSync(configPath, baseline); fs.unlinkSync(backupPath); return { restored: true, configPath, configSHA256: sha256File(configPath) }; } -function startDonorEmulator(runtime, captureDir, ramMB = DONOR_PROFILE.ramMB, controlProfileId = DONOR_PROFILE.id) { +function startBaselineEmulator(runtime, captureDir, ramMB = BASELINE_PROFILE.ramMB, controlProfileId = BASELINE_PROFILE.id, embeddedControl = false) { if (!runtime.avdHome || !runtime.avdIni || !runtime.avdDir) throw new Error(`Official AVD ${AVD_NAME} is not present under the external runtime.`); const out = fs.openSync(path.join(captureDir, 'emulator.stdout.log'), 'a'); const err = fs.openSync(path.join(captureDir, 'emulator.stderr.log'), 'a'); const args = [ `@${AVD_NAME}`, '-id', 'TFTMAC', '-port', EMULATOR_PORT, - '-gpu', 'host', '-audio', 'coreaudio', '-feature', DONOR_PROFILE.featureList, + '-gpu', 'host', '-audio', 'coreaudio', '-feature', BASELINE_PROFILE.featureList, '-append-userspace-opt', 'androidboot.opengles.version=196610', '-append-userspace-opt', 'androidboot.tftmac.graphics_profile=tftmac', - '-skin', `${DONOR_PROFILE.width}x${DONOR_PROFILE.height}`, - '-vsync-rate', String(DONOR_PROFILE.refreshHz), + '-skin', `${BASELINE_PROFILE.width}x${BASELINE_PROFILE.height}`, + '-vsync-rate', String(BASELINE_PROFILE.refreshHz), '-dns-server', '1.1.1.1,8.8.8.8', - '-cores', String(DONOR_PROFILE.vcpu), '-memory', String(ramMB), + '-cores', String(BASELINE_PROFILE.vcpu), '-memory', String(ramMB), '-no-hidpi-scaling', '-no-snapshot', '-no-metrics', '-no-boot-anim', '-crash-report-mode', 'disabled' ]; - if (process.env.TFTMAC_NATIVE_FULLSCREEN === '1') { + if (embeddedControl) args.push('-qt-hide-window', '-grpc-use-token', '-idle-grpc-timeout', '300'); + if (process.env.TFTMAC_NATIVE_FULLSCREEN === '1' && !embeddedControl) { const screenWidth = Number(process.env.TFTMAC_HOST_SCREEN_WIDTH ?? 0); const screenHeight = Number(process.env.TFTMAC_HOST_SCREEN_HEIGHT ?? 0); const controlWidth = Number(process.env.TFTMAC_NATIVE_CONTROL_WIDTH ?? 64); const titleChrome = Number(process.env.TFTMAC_NATIVE_TOPBAR_HEIGHT ?? 30); const backingScale = Number(process.env.TFTMAC_HOST_BACKING_SCALE ?? 1) || 1; - const widthScalePoints = screenWidth > controlWidth ? (screenWidth - controlWidth) / DONOR_PROFILE.width : 1; - const heightScalePoints = screenHeight > titleChrome ? (screenHeight - titleChrome) / DONOR_PROFILE.height : 1; + const widthScalePoints = screenWidth > controlWidth ? (screenWidth - controlWidth) / BASELINE_PROFILE.width : 1; + const heightScalePoints = screenHeight > titleChrome ? (screenHeight - titleChrome) / BASELINE_PROFILE.height : 1; const logicalScale = Math.max(0.1, Math.min(widthScalePoints, heightScalePoints, 4)); const scale = logicalScale * backingScale; args.push('-scale', scale.toFixed(6)); @@ -745,16 +808,38 @@ function startDonorEmulator(runtime, captureDir, ramMB = DONOR_PROFILE.ramMB, co ...runtime.env, ANDROID_AVD_HOME: runtime.avdHome, ANDROID_EMULATOR_USE_SYSTEM_LIBS: '0', - ANGLE_FEATURE_OVERRIDES_ENABLED: DONOR_PROFILE.angleEnabledFeatures, - ANGLE_FEATURE_OVERRIDES_DISABLED: DONOR_PROFILE.angleDisabledFeatures, + ANGLE_FEATURE_OVERRIDES_ENABLED: BASELINE_PROFILE.angleEnabledFeatures, + ANGLE_FEATURE_OVERRIDES_DISABLED: BASELINE_PROFILE.angleDisabledFeatures, MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS: '0', MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE: '64', MVK_CONFIG_FAST_MATH_ENABLED: '1' }; - const child = spawn(runtime.emulator, args, { cwd: repoRoot, env, detached: true, stdio: ['ignore', out, err] }); + delete env.ADB_VENDOR_KEYS; + const hostApp = path.join(repoRoot, 'dist', 'TFTMAC.app', 'Contents', 'Resources', 'TFTMAC Emulator Host.app'); + if (!exists(hostApp)) throw new Error(`TFTMAC_EMULATOR_HOST_MISSING: build the native app first: ${hostApp}`); + const openArgs = [ + '-n', '-W', + '--env', `TFT_EMULATOR=${runtime.emulator}`, + '--env', `TFT_ADB_SERVER_PORT=${ADB_PORT}`, + '--env', `ANDROID_ADB_SERVER_PORT=${ADB_PORT}`, + '--env', 'ADB_MDNS_AUTO_CONNECT=', + '--env', `ANDROID_SDK_ROOT=${runtime.sdkRoot}`, + '--env', `ANDROID_AVD_HOME=${runtime.avdHome}`, + '--env', 'ANDROID_EMULATOR_USE_SYSTEM_LIBS=0', + '--env', `ANGLE_FEATURE_OVERRIDES_ENABLED=${env.ANGLE_FEATURE_OVERRIDES_ENABLED}`, + '--env', `ANGLE_FEATURE_OVERRIDES_DISABLED=${env.ANGLE_FEATURE_OVERRIDES_DISABLED}`, + '--env', `MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS=${env.MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS}`, + '--env', `MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE=${env.MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE}`, + '--env', `MVK_CONFIG_FAST_MATH_ENABLED=${env.MVK_CONFIG_FAST_MATH_ENABLED}`, + '--env', `TFT_HOST_STDOUT=${path.join(captureDir, 'emulator.stdout.log')}`, + '--env', `TFT_HOST_STDERR=${path.join(captureDir, 'emulator.stderr.log')}`, + hostApp, '--args', ...args + ]; + const child = spawn('/usr/bin/open', openArgs, { cwd: repoRoot, env, detached: true, stdio: ['ignore', out, err] }); child.unref(); appendJSONL(path.join(captureDir, 'host-events.jsonl'), { - utc: nowISO(), event: 'DONOR_EMULATOR_STARTED', pid: child.pid, args, + utc: nowISO(), event: 'BASELINE_EMULATOR_STARTED', launcherPid: child.pid, args, + launcher: '/usr/bin/open -n -W --env ... --args ...', adbVendorKeysPresent: false, profile: controlProfileId, env: { ANGLE_FEATURE_OVERRIDES_ENABLED: env.ANGLE_FEATURE_OVERRIDES_ENABLED, @@ -767,14 +852,14 @@ function startDonorEmulator(runtime, captureDir, ramMB = DONOR_PROFILE.ramMB, co return child.pid; } -function donorRuntimeState(runtime, captureDir, prepared, bootClass, ramMB = DONOR_PROFILE.ramMB, controlProfileId = DONOR_PROFILE.id, drawFlushInterval = DONOR_PROFILE.drawFlushInterval) { +function baselineRuntimeState(runtime, captureDir, prepared, bootClass, ramMB = BASELINE_PROFILE.ramMB, controlProfileId = BASELINE_PROFILE.id, drawFlushInterval = BASELINE_PROFILE.drawFlushInterval) { const display = adb(runtime, ['shell', 'wm', 'size'], { allowFailure: true }).stdout.trim(); const density = adb(runtime, ['shell', 'wm', 'density'], { allowFailure: true }).stdout.trim(); const state = { observedAt: nowISO(), control: controlProfileId, compatibilityAdapter: true, - compatibilitySource: 'Mactician 1.1.0 measured runtime on this host', + compatibilitySource: 'TFTMAC measured playable runtime on this host', externalRoot: runtime.externalRoot, sdkRoot: runtime.sdkRoot, emulatorVersion: runtime.emulatorVersion, @@ -787,23 +872,23 @@ function donorRuntimeState(runtime, captureDir, prepared, bootClass, ramMB = DON adbSerial: SERIAL, adbServerPort: Number(ADB_PORT), emulatorConsolePort: Number(EMULATOR_PORT), - vcpu: DONOR_PROFILE.vcpu, + vcpu: BASELINE_PROFILE.vcpu, ramMB, - ramSource: ramMB === DONOR_PROFILE.ramMB ? 'profile' : 'emulator-command-line-override', - displayRequested: `${DONOR_PROFILE.width}x${DONOR_PROFILE.height}`, - densityRequested: DONOR_PROFILE.density, - refreshTargetHz: DONOR_PROFILE.refreshHz, + ramSource: ramMB === BASELINE_PROFILE.ramMB ? 'profile' : 'emulator-command-line-override', + displayRequested: `${BASELINE_PROFILE.width}x${BASELINE_PROFILE.height}`, + densityRequested: BASELINE_PROFILE.density, + refreshTargetHz: BASELINE_PROFILE.refreshHz, gpuMode: 'host', - graphicsTransportRequested: DONOR_PROFILE.glTransport, + graphicsTransportRequested: BASELINE_PROFILE.glTransport, guestAngleRequested: true, vulkanRequested: true, glesCompatibilityExposure: 196610, - angleDisabledFeatures: DONOR_PROFILE.angleDisabledFeatures, + angleDisabledFeatures: BASELINE_PROFILE.angleDisabledFeatures, moltenVK: { synchronousQueueSubmits: false, maxActiveMetalCommandBuffersPerQueue: 64, fastMath: true }, asg: { - writeBufferSize: DONOR_PROFILE.asgWriteBufferSize, - writeStepSize: DONOR_PROFILE.asgWriteStepSize, - dataRingSize: DONOR_PROFILE.asgDataRingSize, + writeBufferSize: BASELINE_PROFILE.asgWriteBufferSize, + writeStepSize: BASELINE_PROFILE.asgWriteStepSize, + dataRingSize: BASELINE_PROFILE.asgDataRingSize, drawFlushInterval }, audioEnabled: true, @@ -818,18 +903,31 @@ function donorRuntimeState(runtime, captureDir, prepared, bootClass, ramMB = DON return state; } -async function startDonorControl(ramMB = DONOR_PROFILE.ramMB, controlProfileId = DONOR_PROFILE.id, drawFlushInterval = DONOR_PROFILE.drawFlushInterval) { +async function startBaselineControl(ramMB = BASELINE_PROFILE.ramMB, controlProfileId = BASELINE_PROFILE.id, drawFlushInterval = BASELINE_PROFILE.drawFlushInterval, embeddedControl = false) { const preAudit = runtimeProcessAudit(); - const hasActiveRuntime = preAudit.processes.some(item => ['ANDROID_EMULATOR', 'TFTMAC_SAMPLER'].includes(item.kind)); - if (!hasActiveRuntime && preAudit.adb5040 === 'LISTENER_PRESENT') cleanupTftmacAdbResidue(); + const hasEmulator = preAudit.processes.some(item => item.kind === 'ANDROID_EMULATOR'); + if (!hasEmulator) { + const staleSamplers = preAudit.processes.filter(item => item.kind === 'TFTMAC_SAMPLER'); + for (const sampler of staleSamplers) { + try { process.kill(sampler.pid, 'SIGTERM'); } catch {} + } + if (staleSamplers.length) await sleep(250); + for (const sampler of staleSamplers) { + if (processAlive(sampler.pid)) { + try { process.kill(sampler.pid, 'SIGKILL'); } catch {} + } + } + cleanupTftmacAdbResidue(); + if (staleSamplers.length) await sleep(100); + } singleRuntimePreflight(); const runtime = discover(); if (!isUnder(runtime.sdkRoot, EXTERNAL_ROOT)) throw new Error('Selected SDK is not on the required external runtime volume.'); if (!runtime.requiredImagePresent) throw new Error(`Required official Play image is missing: ${runtime.requiredImagePath}`); if (!runtime.avdIni || !runtime.avdConfig || !runtime.avdDir) throw new Error(`Required official Play AVD ${AVD_NAME} was not found.`); ensureDir(STATE_ROOT); ensureDir(CAPTURE_ROOT); ensureDir(DIAGNOSTICS_ROOT); - const prepared = prepareDonorAVD(runtime, drawFlushInterval); - const windowFit = prepareEmulatorWindowFit(runtime, DONOR_PROFILE.width, DONOR_PROFILE.height); + const prepared = prepareBaselineAVD(runtime, drawFlushInterval); + const windowFit = embeddedControl ? { prepared: false, reason: 'HIDDEN_EMBEDDED_CONTROL' } : prepareEmulatorWindowFit(runtime, BASELINE_PROFILE.width, BASELINE_PROFILE.height); const sessionId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomUUID()}`; const captureDir = path.join(CAPTURE_ROOT, sessionId); for (const d of [captureDir, path.join(captureDir, 'surfaceflinger'), path.join(captureDir, 'gfxinfo')]) ensureDir(d); @@ -847,27 +945,31 @@ async function startDonorControl(ramMB = DONOR_PROFILE.ramMB, controlProfileId = appendJSONL(path.join(captureDir, 'host-events.jsonl'), { utc: nowISO(), event: 'SAMPLER_STARTED', pid: samplerPid }); await sleep(350); if (!processAlive(samplerPid)) throw new Error(`LOGGER_START_FAILED: sampler PID ${samplerPid} did not remain alive.`); - adbServer(runtime); - const emulatorPid = startDonorEmulator(runtime, captureDir, ramMB, controlProfileId); + let emulatorPid = null; try { + adbServer(runtime); + const launcherPid = startBaselineEmulator(runtime, captureDir, ramMB, controlProfileId, embeddedControl); await waitForBoot(runtime); + const observedEmulator = runtimeProcessAudit().processes.find(item => item.kind === 'ANDROID_EMULATOR' && item.command.includes(`-port ${EMULATOR_PORT}`)); + if (!observedEmulator) throw new Error(`EMULATOR_PID_NOT_OBSERVED_AFTER_OPEN: console port ${EMULATOR_PORT}`); + emulatorPid = observedEmulator.pid; const guestUnlock = wakeGuestScreen(runtime, captureDir); const fullscreenPolicy = prepareGuestFullscreenPolicy(runtime); const clockPreflight = await ensureGuestClock(runtime, captureDir); - const runtimeObserved = donorRuntimeState(runtime, captureDir, prepared, 'COLD', ramMB, controlProfileId, drawFlushInterval); + const runtimeObserved = baselineRuntimeState(runtime, captureDir, prepared, 'COLD', ramMB, controlProfileId, drawFlushInterval); const pkg = packageState(runtime, captureDir, false); const renderer = rendererState(runtime, captureDir); const state = { - schema: 1, sessionId, captureDir, samplerPid, emulatorPid, reusedRunningEmulator: false, + schema: 1, sessionId, captureDir, samplerPid, emulatorPid, launcherPid, reusedRunningEmulator: false, sdkRoot: runtime.sdkRoot, avdHome: runtime.avdHome, startedUTC: session.startedUTC, - packageState: pkg.state, controlProfile: controlProfileId, donorConfigBackupPath: prepared.backupPath + packageState: pkg.state, controlProfile: controlProfileId, baselineConfigBackupPath: prepared.backupPath, embeddedControl }; writeJSON(CONTROL_STATE, state); return { ...state, package: pkg, clockPreflight, runtime: runtimeObserved, renderer, next: pkg.state === 'MISSING' ? 'run play-action to install official TFT from Google Play' : 'run play-action to verify/update official TFT, then launch-game' }; } catch (error) { if (processAlive(samplerPid)) { try { process.kill(samplerPid, 'SIGTERM'); } catch {} } try { adb(runtime, ['emu', 'kill'], { allowFailure: true, timeout: 10000 }); } catch {} - restoreDonorAVD(runtime); + restoreBaselineAVD(runtime); throw error; } } @@ -1403,7 +1505,7 @@ function ingestApproximateLatestMatch() { try { if (initialize) db.exec(fs.readFileSync(schemaPath, 'utf8')); db.exec('PRAGMA foreign_keys = ON;'); - const configId = 'mactician_compatible_official_v0'; + const configId = BASELINE_PROFILE.id; const labSessionId = `${analysis.sessionId}-match-${analysis.matchOrdinal}-approx`; const packageInfo = readJSON(path.join(captureDir, 'package-state.json'), {}); const packageFile = path.join(captureDir, 'package-state.json'); @@ -1892,7 +1994,7 @@ function ingestContinuousRunIntoLab() { const sessionFile = readJSON(path.join(captureDir, 'session.json'), {}); const packageFile = path.join(captureDir, 'package-state.json'); const rendererFile = path.join(captureDir, 'renderer-state.json'); - const currentConfigId = 'mactician_compatible_official_v0'; + const currentConfigId = BASELINE_PROFILE.id; const now = nowISO(); db.exec('BEGIN IMMEDIATE;'); try { @@ -2137,12 +2239,12 @@ function ingestAnalysisIntoLab() { .sort((a, b) => b.mtimeMs - a.mtimeMs); if (metas[0]) latestTraceMetadata = readJSON(metas[0].path, null); } catch {} - const currentConfigId = 'mactician_compatible_official_v0'; - const candidateConfigId = 'mactician_compatible_5gb_v1'; + const currentConfigId = BASELINE_PROFILE.id; + const candidateConfigId = 'tftmac_5gb_baseline_v1'; db.prepare('INSERT OR REPLACE INTO lab_meta(key,value) VALUES(?,?)').run('last_gameplay_ingest_at', now); db.prepare('INSERT OR REPLACE INTO lab_meta(key,value) VALUES(?,?)').run('current_gameplay_baseline_session', labSessionId); - db.prepare('INSERT OR REPLACE INTO lab_meta(key,value) VALUES(?,?)').run('current_optimization_priority', '1 preserve continuous-run logger; 2 use pre-play app refresh when memory-pressure thresholds fire; 3 one-factor guest RAM 6144->5120 A/B; 4 test Performance Mode/FPS/graphics by setting timestamps; 5 only then graphics transport/queue experiments'); + db.prepare('INSERT OR REPLACE INTO lab_meta(key,value) VALUES(?,?)').run('current_optimization_priority', 'Hold the proven 5120-MiB native baseline; correlate native frame windows, SurfaceFlinger deltas, memory, audio and explicit setting markers; change one restart-bound variable at a time.'); db.prepare('INSERT OR REPLACE INTO lab_meta(key,value) VALUES(?,?)').run('current_game_settings', JSON.stringify(analysis.match.gameSettings)); if (latestTraceMetadata) db.prepare('INSERT OR REPLACE INTO lab_meta(key,value) VALUES(?,?)').run('native_trace_collector_smoke', JSON.stringify({ label: latestTraceMetadata.label, durationSeconds: latestTraceMetadata.durationSeconds, byteCount: latestTraceMetadata.byteCount, sha256: latestTraceMetadata.sha256, dataSources: latestTraceMetadata.dataSources, parseState: latestTraceMetadata.parseState })); @@ -2160,9 +2262,9 @@ function ingestAnalysisIntoLab() { refresh_hz=excluded.refresh_hz,gpu_mode=excluded.gpu_mode,audio_enabled=excluded.audio_enabled, graphics_transport=excluded.graphics_transport,angle_mode=excluded.angle_mode,vulkan_mode=excluded.vulkan_mode, moltenvk_mode=excluded.moltenvk_mode,presentation_mode=excluded.presentation_mode,state=excluded.state,notes=excluded.notes`); - const currentNotes = 'First playable official TFT control: API36 Play ARM64, ANGLE ES3.2 compatibility exposure, Vulkan/ranchu, virtio-gpu-asg, gfxstream, MoltenVK/Metal. First full match placed 1st.'; - configStmt.run(currentConfigId,null,'Mactician-compatible official TFT control v0',null,'37.1.11','37.0.1',REQUIRED_IMAGE,REQUIRED_IMAGE_MIN_REVISION,AVD_NAME,SERIAL,Number(ADB_PORT),Number(EMULATOR_PORT),6,6144,1920,1080,320,60,'host',1,'virtio-gpu-asg','GuestAngle + explicit ES3.2 compatibility exposure','ranchu / guest Vulkan','gfxstream host Vulkan -> MoltenVK/Metal','direct emulator window','CONTROL',sessionFile.startedUTC ?? now,currentNotes); - configStmt.run(candidateConfigId,currentConfigId,'RAM 5 GiB candidate',null,'37.1.11','37.0.1',REQUIRED_IMAGE,REQUIRED_IMAGE_MIN_REVISION,AVD_NAME,SERIAL,Number(ADB_PORT),Number(EMULATOR_PORT),6,5120,1920,1080,320,60,'host',1,'virtio-gpu-asg','same as baseline','same as baseline','same as baseline','same as baseline','CANDIDATE',now,'One-factor candidate: only guest RAM changes from 6144 MB to 5120 MB; 4096 MB is deferred because observed guest headroom makes a 2 GiB cut too aggressive.'); + const currentNotes = 'Current native baseline: API36 Play ARM64, 5038/5582 donor identity, 5 GiB, authenticated gRPC raw frames, AppKit/Metal presentation, ANGLE ES3.2 compatibility exposure, Vulkan/ranchu, virtio-gpu-asg, gfxstream and MoltenVK/Metal.'; + configStmt.run(currentConfigId,null,'TFTMAC native playable 5 GiB baseline',null,'37.1.11','37.0.1',REQUIRED_IMAGE,REQUIRED_IMAGE_MIN_REVISION,AVD_NAME,SERIAL,Number(ADB_PORT),Number(EMULATOR_PORT),BASELINE_PROFILE.vcpu,BASELINE_PROFILE.ramMB,BASELINE_PROFILE.width,BASELINE_PROFILE.height,BASELINE_PROFILE.density,BASELINE_PROFILE.refreshHz,'host',1,BASELINE_PROFILE.glTransport,'GuestAngle + explicit ES3.2 compatibility exposure','ranchu / guest Vulkan','gfxstream host Vulkan -> MoltenVK/Metal','native AppKit Metal embedded','CONTROL',sessionFile.startedUTC ?? now,currentNotes); + configStmt.run(candidateConfigId,'tftmac_official_baseline_v1','RAM 5 GiB candidate (historical)',null,'37.1.11','37.0.1',REQUIRED_IMAGE,REQUIRED_IMAGE_MIN_REVISION,AVD_NAME,SERIAL,Number(ADB_PORT),Number(EMULATOR_PORT),6,5120,1920,1080,320,60,'host',1,'virtio-gpu-asg','same as historical 6 GiB baseline','same as historical baseline','same as historical baseline','same as historical baseline','HISTORICAL',now,'Preserved historical 6144 -> 5120 MiB experiment receipt; current authority is tftmac_5gb_native_v1.'); db.prepare(`INSERT INTO sessions( id,runtime_config_id,started_utc,ended_utc,host_start_mono_ns,host_end_mono_ns,boot_class,workload_class, @@ -2246,7 +2348,7 @@ function ingestAnalysisIntoLab() { db.prepare(`INSERT INTO experiments(id,hypothesis_id,name,experiment_type,baseline_config_id,candidate_config_id,run_class,one_factor,state,required_cold_confirmation,semantic_gate,created_at,completed_at,notes) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET baseline_config_id=excluded.baseline_config_id,candidate_config_id=excluded.candidate_config_id,state=excluded.state,semantic_gate=excluded.semantic_gate,notes=excluded.notes`) - .run('exp_ram_5gb_ab','h_guest_ram_host_pressure','Guest RAM 6144 -> 5120 MB A/B','INTERVENTION',currentConfigId,candidateConfigId,'HEAVY',1,'PLANNED',1,'Same official TFT version, same renderer/transport/display/vCPU; compare full-match or matched heavy-combat resource pressure plus native frame timing once available.',now,null,'Safer first RAM intervention selected after Game 2: reduce one GiB only and compare continuous-run pressure.'); + .run('exp_ram_5gb_ab','h_guest_ram_host_pressure','Guest RAM 6144 -> 5120 MB A/B','INTERVENTION','tftmac_official_baseline_v1',candidateConfigId,'HEAVY',1,'COMPLETE',1,'Historical one-factor experiment preserved; current native runs hold 5120 MiB unless a new explicit experiment is selected.',now,now,'Historical RAM experiment closed by promotion of the 5120-MiB native baseline.'); db.prepare(`INSERT INTO experiments(id,hypothesis_id,name,experiment_type,baseline_config_id,candidate_config_id,run_class,one_factor,state,required_cold_confirmation,semantic_gate,created_at,completed_at,notes) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET baseline_config_id=excluded.baseline_config_id,state=excluded.state,semantic_gate=excluded.semantic_gate,notes=excluded.notes`) @@ -2254,7 +2356,7 @@ function ingestAnalysisIntoLab() { db.prepare(`UPDATE experiments SET state='COMPLETE',baseline_config_id=?,completed_at=?,notes=? WHERE id='exp_control_direct_play'`) .run(currentConfigId,now,`First official full match completed with placement ${analysis.match.placement}; resource telemetry valid; native frame timing unavailable through gfxinfo.`); db.prepare(`UPDATE experiments SET state='CANCELLED',notes=COALESCE(notes,'') || ? WHERE id IN ('exp_control_repeat_warm','exp_transition_capture','exp_heavy_capture') AND baseline_config_id='control_stock_direct_v0'`) - .run(' Superseded by the proven mactician_compatible_official_v0 baseline and native Perfetto trace experiment.'); + .run(' Superseded by the proven tftmac_official_baseline_v1 baseline and native Perfetto trace experiment.'); db.prepare('INSERT OR REPLACE INTO experiment_sessions(experiment_id,session_id,role) VALUES(?,?,?)').run('exp_control_direct_play',labSessionId,analysis.match.matchOrdinal === 1 ? 'BASELINE' : 'DIAGNOSTIC'); db.exec('COMMIT;'); @@ -3215,12 +3317,12 @@ function fitEmulatorWindow() { const result = command('/usr/bin/osascript', ['-e', script], { allowFailure: true, timeout: 10000 }); const output = `${result.stdout}${result.stderr}`.trim(); if (result.status !== 0) { - const nextLaunch = prepareEmulatorWindowFit(runtime, DONOR_PROFILE.width, DONOR_PROFILE.height); - return { action: 'EMULATOR_WINDOW_FIT_LIVE_PERMISSION_REQUIRED', output, emulatorPid: observedPid, guestResolutionPreserved: true, guestResolution: `${DONOR_PROFILE.width}x${DONOR_PROFILE.height}`, nextLaunch, nextBootGuestPolicy, manualLiveAction: 'Use macOS fullscreen (Control-Command-F) or resize the emulator window smaller.' }; + const nextLaunch = prepareEmulatorWindowFit(runtime, BASELINE_PROFILE.width, BASELINE_PROFILE.height); + return { action: 'EMULATOR_WINDOW_FIT_LIVE_PERMISSION_REQUIRED', output, emulatorPid: observedPid, guestResolutionPreserved: true, guestResolution: `${BASELINE_PROFILE.width}x${BASELINE_PROFILE.height}`, nextLaunch, nextBootGuestPolicy, manualLiveAction: 'Use macOS fullscreen (Control-Command-F) or resize the emulator window smaller.' }; } - const nextLaunch = prepareEmulatorWindowFit(runtime, DONOR_PROFILE.width, DONOR_PROFILE.height); + const nextLaunch = prepareEmulatorWindowFit(runtime, BASELINE_PROFILE.width, BASELINE_PROFILE.height); if (state?.captureDir) appendJSONL(path.join(state.captureDir, 'host-events.jsonl'), { utc: nowISO(), event: 'EMULATOR_WINDOW_FIT_HOST', source: 'macOS window resize', emulatorPid: observedPid, result: output, nextLaunch }); - return { action: 'EMULATOR_WINDOW_FIT_HOST', output, emulatorPid: observedPid, guestResolutionPreserved: true, guestResolution: `${DONOR_PROFILE.width}x${DONOR_PROFILE.height}`, nextLaunch, nextBootGuestPolicy }; + return { action: 'EMULATOR_WINDOW_FIT_HOST', output, emulatorPid: observedPid, guestResolutionPreserved: true, guestResolution: `${BASELINE_PROFILE.width}x${BASELINE_PROFILE.height}`, nextLaunch, nextBootGuestPolicy }; } async function status() { @@ -3992,7 +4094,7 @@ function normalizePerformanceLab(captureDir, frames, metrics, storage, manifestS const safeConfigHash = !hashOwner || hashOwner === runtimeConfigId ? observedConfigHash : null; const runtimeConfigExists = Boolean(db.prepare('SELECT 1 AS ok FROM runtime_configs WHERE id=?').get(runtimeConfigId)); if (!runtimeConfigExists) { - const baselineId = 'mactician_compatible_official_v0'; + const baselineId = BASELINE_PROFILE.id; const baselineExists = Boolean(db.prepare('SELECT 1 AS ok FROM runtime_configs WHERE id=?').get(baselineId)); const parentConfigId = runtimeConfigId !== baselineId && baselineExists ? baselineId : null; db.prepare(`INSERT INTO runtime_configs( @@ -4292,8 +4394,8 @@ async function stopControl() { writeJSON(path.join(captureDir, 'control-result.json'), controlResult); const result = { sessionId: state.sessionId, captureDir, ...controlResult }; try { adb(runtime, ['emu', 'kill'], { allowFailure: true, timeout: 10000 }); } catch {} - if (state.controlProfile === DONOR_PROFILE.id || String(state.controlProfile ?? '').startsWith('mactician_compatible_')) { - try { result.donorAvdRestore = restoreDonorAVD(runtime); } catch (error) { result.donorAvdRestore = { restored: false, error: error instanceof Error ? error.message : String(error) }; } + if (state.controlProfile === BASELINE_PROFILE.id || String(state.controlProfile ?? '').startsWith('tftmac_')) { + try { result.baselineAvdRestore = restoreBaselineAVD(runtime); } catch (error) { result.baselineAvdRestore = { restored: false, error: error instanceof Error ? error.message : String(error) }; } } try { fs.unlinkSync(CONTROL_STATE); } catch {} return result; @@ -4470,8 +4572,6 @@ function buildApp() { const sdkPath = command('/usr/bin/xcrun', ['--sdk', 'macosx', '--show-sdk-path'], { env: xcodeEnv }).stdout.trim(); command(swiftc, ['-O', '-parse-as-library', '-target', 'arm64-apple-macosx14.0', '-sdk', sdkPath, ...sources, '-o', binary], { timeout: 240000, env: xcodeEnv }); fs.copyFileSync(path.join(repoRoot, 'tftmac', 'Info.plist'), path.join(contents, 'Info.plist')); - const iconSource = path.join(repoRoot, 'branding', 'generated', 'Mactician.icns'); - if (exists(iconSource)) fs.copyFileSync(iconSource, path.join(contents, 'Resources', 'TFTMAC.icns')); fs.copyFileSync(scriptPath, path.join(contents, 'Resources', 'tftmac-direct-control.mjs')); const labSource = path.join(repoRoot, 'ssot', 'TFTMAC_PERFORMANCE_LAB.sql'); if (exists(labSource)) fs.copyFileSync(labSource, path.join(contents, 'Resources', 'TFTMAC_PERFORMANCE_LAB.sql')); @@ -4484,7 +4584,7 @@ function buildApp() { function runtimeProcessAudit() { const ps = command('/bin/ps', ['axo', 'pid=,ppid=,etime=,command='], { allowFailure: true, timeout: 10000, maxBuffer: 16 * 1024 * 1024 }); const lines = ps.stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean); - const relevant = lines.filter(line => /(TFTMAC|Mactician|TftPBE|TFT_Ultra_Tablet|TFTMAC_Live_API36|qemu-system-aarch64|\/emulator(?:\s|$)|adb.*fork-server|tftmac-direct-control\.mjs\s+sampler)/i.test(line)); + const relevant = lines.filter(line => /(TFTMAC|TFT_Ultra_Tablet|TFTMAC_Live_API36|qemu-system-aarch64|\/emulator(?:\s|$)|adb.*fork-server|tftmac-direct-control\.mjs\s+sampler)/i.test(line)); const classified = relevant.map(line => { const match = line.match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/); const pid = match ? Number(match[1]) : null; @@ -4494,7 +4594,6 @@ function runtimeProcessAudit() { let kind = 'OTHER_RELEVANT'; if (/tftmac-direct-control\.mjs\s+sampler/i.test(commandLine)) kind = 'TFTMAC_SAMPLER'; else if (/TFTMAC\.app\/Contents\/MacOS\/TFTMAC/i.test(commandLine)) kind = 'TFTMAC_APP'; - else if (/Mactician\.app\/Contents\/MacOS\/Mactician/i.test(commandLine)) kind = 'MACTICIAN_APP'; else if (/qemu-system-aarch64|\/emulator(?:\s|$)/i.test(commandLine)) kind = 'ANDROID_EMULATOR'; else if (/adb.*fork-server/i.test(commandLine)) kind = 'ADB_SERVER'; return { pid, ppid, elapsed, kind, command: commandLine }; @@ -4511,7 +4610,6 @@ function runtimeProcessAudit() { controlState: readJSON(CONTROL_STATE), duplicateRisk: { tftmacApps: classified.filter(item => item.kind === 'TFTMAC_APP').length, - macticianApps: classified.filter(item => item.kind === 'MACTICIAN_APP').length, emulators: classified.filter(item => item.kind === 'ANDROID_EMULATOR').length, samplers: classified.filter(item => item.kind === 'TFTMAC_SAMPLER').length, adbServers: classified.filter(item => item.kind === 'ADB_SERVER').length @@ -4523,19 +4621,30 @@ function cleanupTftmacAdbResidue() { const auditBefore = runtimeProcessAudit(); const tftmacAdb = auditBefore.processes.filter(item => item.kind === 'ADB_SERVER' && /tcp:5040\b/.test(item.command)); if (!tftmacAdb.length) return { action: 'TFTMAC_ADB_RESIDUE_ABSENT', auditBefore, auditAfter: auditBefore }; - const runtime = discover(); - const killed = command(runtime.adb, ['-P', ADB_PORT, 'kill-server'], { env: runtime.env, allowFailure: true, timeout: 10000 }); + for (const item of tftmacAdb) { + try { process.kill(item.pid, 'SIGTERM'); } catch {} + } + const deadline = Date.now() + 1000; + while (Date.now() < deadline && tftmacAdb.some(item => processAlive(item.pid))) { + const until = Date.now() + 50; + while (Date.now() < until) {} + } + for (const item of tftmacAdb) { + if (processAlive(item.pid)) { + try { process.kill(item.pid, 'SIGKILL'); } catch {} + } + } const auditAfter = runtimeProcessAudit(); if (auditAfter.processes.some(item => item.kind === 'ADB_SERVER' && /tcp:5040\b/.test(item.command))) { - throw new Error(`TFTMAC_ADB_RESIDUE_CLEANUP_FAILED: ${(killed.stderr || killed.stdout || '').trim()}`); + throw new Error('TFTMAC_ADB_RESIDUE_CLEANUP_FAILED: private ADB listener remains on port 5040'); } - return { action: 'TFTMAC_ADB_RESIDUE_CLEANED', killedPids: tftmacAdb.map(item => item.pid), killStatus: killed.status, auditBefore, auditAfter }; + return { action: 'TFTMAC_ADB_RESIDUE_CLEANED', killedPids: tftmacAdb.map(item => item.pid), auditBefore, auditAfter }; } function singleRuntimePreflight() { const audit = runtimeProcessAudit(); const tftmacApps = audit.processes.filter(item => item.kind === 'TFTMAC_APP'); - const blockers = audit.processes.filter(item => ['MACTICIAN_APP', 'ANDROID_EMULATOR', 'TFTMAC_SAMPLER', 'ADB_SERVER'].includes(item.kind)); + const blockers = audit.processes.filter(item => ['ANDROID_EMULATOR', 'TFTMAC_SAMPLER', 'ADB_SERVER'].includes(item.kind)); if (tftmacApps.length > 1) blockers.push(...tftmacApps.slice(1)); if (blockers.length || audit.ports.length) { throw new Error(`SINGLE_RUNTIME_PREFLIGHT_BLOCKED: ${JSON.stringify({ blockers, ports: audit.ports, adb5040: audit.adb5040, tftmacLauncherCount: tftmacApps.length })}`); @@ -4543,142 +4652,6 @@ function singleRuntimePreflight() { return { pass: true, observedAt: audit.observedAt, tftmacLauncherCount: tftmacApps.length, audit }; } -function launchMacticianControl() { - const preflight = singleRuntimePreflight(); - const app = '/Applications/Mactician.app'; - const binary = path.join(app, 'Contents', 'MacOS', 'Mactician'); - if (!executable(binary)) throw new Error(`MACTICIAN_APP_MISSING: ${binary}`); - const env = { ...process.env, ANDROID_ADB_SERVER_PORT: '5038', ADB_MDNS_AUTO_CONNECT: '' }; - const child = spawn(binary, [], { env, detached: true, stdio: 'ignore' }); - child.unref(); - return { action: 'MACTICIAN_LAUNCHED', app, binary, pid: child.pid, inheritedAdbServerPort: 5038, preflight, observedAt: nowISO() }; -} - -async function stopMacticianControl() { - const ps = command('/bin/ps', ['axo', 'pid=,command='], { allowFailure: true, timeout: 10000 }).stdout; - const lines = ps.split(/\r?\n/).map(line => line.trim()).filter(Boolean); - const targets = lines.flatMap(line => { - const match = line.match(/^(\d+)\s+(.+)$/); - if (!match) return []; - const pid = Number(match[1]); - const cmd = match[2]; - if (/Mactician\.app\/Contents\/MacOS\/Mactician|\/Mactician\/sdk\/emulator\/.*qemu-system-aarch64|launcher-runtime\.command|run-asg-experiment\.command|run-tft-root-affinity\.command/i.test(cmd)) return [{ pid, cmd }]; - return []; - }); - for (const target of [...targets].sort((a, b) => b.pid - a.pid)) { - try { process.kill(target.pid, 'SIGTERM'); } catch {} - } - const adbCandidates = [ - path.join(USER_HOME, 'Library', 'Application Support', 'Mactician', 'sdk', 'platform-tools', 'adb'), - '/Volumes/MAC MINI M4/Mactician/sdk/platform-tools/adb' - ].filter(executable); - if (adbCandidates.length) command(adbCandidates[0], ['-P', '5038', 'kill-server'], { allowFailure: true, timeout: 10000 }); - await sleep(1200); - const after = runtimeProcessAudit(); - const remaining = after.processes.filter(item => ['MACTICIAN_APP','ANDROID_EMULATOR'].includes(item.kind) || (item.kind === 'ADB_SERVER' && /tcp:5038\b/.test(item.command))); - if (remaining.length) throw new Error(`MACTICIAN_STOP_INCOMPLETE: ${JSON.stringify(remaining)}`); - return { action: 'MACTICIAN_STOPPED', terminated: targets, observedAt: nowISO(), auditAfter: after }; -} - -function newestMatchingFile(roots, pattern, maximum = 20000) { - const candidates = []; - for (const root of roots) { - if (!exists(root)) continue; - for (const file of walk(root, 5, maximum)) { - try { - const stat = fs.statSync(file); - if (stat.isFile() && pattern.test(file)) candidates.push({ file, mtimeMs: stat.mtimeMs }); - } catch {} - } - } - candidates.sort((a, b) => b.mtimeMs - a.mtimeMs); - return candidates[0]?.file ?? null; -} - -function macticianRuntimeAudit() { - const processAudit = runtimeProcessAudit(); - const ps = command('/bin/ps', ['axo', 'pid=,ppid=,etime=,command='], { allowFailure: true, timeout: 10000, maxBuffer: 32 * 1024 * 1024 }); - const processTree = ps.stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean) - .filter(line => /(Mactician|TftPBE|emulator-5582|qemu-system-aarch64|\/emulator(?:\s|$))/i.test(line)); - const app = '/Applications/Mactician.app'; - const appVersion = exists(path.join(app, 'Contents', 'Info.plist')) - ? command('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', path.join(app, 'Contents', 'Info.plist')], { allowFailure: true, timeout: 10000 }).stdout.trim() || null - : null; - const appBuild = exists(path.join(app, 'Contents', 'Info.plist')) - ? command('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleVersion', path.join(app, 'Contents', 'Info.plist')], { allowFailure: true, timeout: 10000 }).stdout.trim() || null - : null; - const macticianAdbPort = processTree.map(line => line.match(/adb\s+-L\s+tcp:(\d+)\s+fork-server/i)?.[1]).find(Boolean) - ?? processAudit.processes.map(item => item.command.match(/adb\s+-L\s+tcp:(\d+)\s+fork-server/i)?.[1]).find(Boolean) - ?? '5038'; - const adbCandidates = [ - path.join(USER_HOME, 'Library', 'Application Support', 'Mactician', 'sdk', 'platform-tools', 'adb'), - '/Volumes/MAC MINI M4/Mactician/sdk/platform-tools/adb' - ].filter(executable); - const adbSnapshots = adbCandidates.map(adbPath => { - const devices = command(adbPath, ['-P', macticianAdbPort, 'devices', '-l'], { allowFailure: true, timeout: 10000 }); - const serials = [...devices.stdout.matchAll(/^(emulator-\d+)\s+device\b/gm)].map(match => match[1]); - const snapshots = serials.map(serial => { - const shell = args => command(adbPath, ['-P', macticianAdbPort, '-s', serial, ...args], { allowFailure: true, timeout: 20000, maxBuffer: 24 * 1024 * 1024 }).stdout.trim(); - const packageDump = shell(['shell', 'dumpsys', 'package', PACKAGE]); - const props = {}; - for (const key of ['ro.boot.qemu.avd_name','ro.hardware.egl','ro.hardware.vulkan','ro.opengles.version','ro.boot.qemu.gltransport','ro.boot.qemu.gles']) props[key] = shell(['shell', 'getprop', key]) || null; - const angleSettings = {}; - for (const key of ['angle_gl_driver_all_angle','angle_gl_driver_selection_pkgs','angle_gl_driver_selection_values']) angleSettings[key] = shell(['shell', 'settings', 'get', 'global', key]) || null; - const surfaceFlinger = shell(['shell', 'dumpsys', 'SurfaceFlinger']); - return { - serial, - bootCompleted: shell(['shell', 'getprop', 'sys.boot_completed']) || null, - avdName: shell(['emu', 'avd', 'name']) || props['ro.boot.qemu.avd_name'], - packagePath: shell(['shell', 'pm', 'path', PACKAGE]) || null, - versionName: packageDump.match(/versionName=([^\s]+)/)?.[1] ?? null, - versionCode: packageDump.match(/versionCode=(\d+)/)?.[1] ?? null, - packagePid: shell(['shell', 'pidof', PACKAGE]) || null, - topActivity: shell(['shell', 'dumpsys', 'activity', 'activities']).split(/\r?\n/).filter(line => /topResumedActivity|mResumedActivity|teamfighttactics|leagueoflegends/i.test(line)).slice(0, 40), - properties: props, - angleSettings, - displaySize: shell(['shell', 'wm', 'size']) || null, - displayDensity: shell(['shell', 'wm', 'density']) || null, - surfaceFlingerGraphics: surfaceFlinger.split(/\r?\n/).filter(line => /GLES|OpenGL|Vulkan|ANGLE|gfxstream|GPU|renderer/i.test(line)).slice(0, 160) - }; - }); - return { adbPath, devices: devices.stdout.trim(), snapshots }; - }); - const logRoots = [ - path.join(USER_HOME, 'Library', 'Application Support', 'Mactician', 'logs'), - path.join(USER_HOME, 'Library', 'Application Support', 'Mactician'), - '/Volumes/MAC MINI M4/Mactician' - ]; - const latestLog = newestMatchingFile(logRoots, /(?:launcher|emulator|mactician|tft).*\.log$/i); - let graphicsLogEvidence = []; - let failureLogEvidence = []; - let latestLogTail = []; - if (latestLog) { - try { - const logLines = fs.readFileSync(latestLog, 'utf8').split(/\r?\n/); - latestLogTail = logLines.slice(-240); - graphicsLogEvidence = logLines.filter(line => /ANGLE|ASG|gfxstream|MoltenVK|Metal|Vulkan|Setting ICD|gltransport|GuestAngle|feature/i.test(line)).slice(-200); - failureLogEvidence = logLines.filter(line => /error|failed|failure|fatal|abort|offline|not found|missing|exit|terminated|could not|cannot|timed out|timeout/i.test(line)).slice(-160); - } catch {} - } - const relevantPorts = command('/usr/sbin/lsof', ['-nP', '-iTCP:5037', '-iTCP:5038', '-iTCP:5040', '-iTCP:5582', '-iTCP:5592'], { allowFailure: true, timeout: 10000, maxBuffer: 8 * 1024 * 1024 }).stdout.split(/\r?\n/).filter(Boolean); - return { observedAt: nowISO(), appVersion, appBuild, macticianAdbPort: Number(macticianAdbPort), processAudit, processTree, relevantPorts, adbSnapshots, latestLog, latestLogTail, failureLogEvidence, graphicsLogEvidence }; -} - -function cleanupObserverAdb5037() { - const ps = command('/bin/ps', ['axo', 'pid=,command='], { allowFailure: true, timeout: 10000 }).stdout; - const observed = ps.split(/\r?\n/).map(line => line.trim()).filter(line => /adb\s+-L\s+tcp:5037\s+fork-server/i.test(line)); - if (!observed.length) return { action: 'OBSERVER_ADB_5037_ABSENT' }; - const adbCandidates = [ - path.join(USER_HOME, 'Library', 'Application Support', 'Mactician', 'sdk', 'platform-tools', 'adb'), - '/Volumes/MAC MINI M4/Mactician/sdk/platform-tools/adb' - ].filter(executable); - if (!adbCandidates.length) throw new Error('No Mactician adb binary available to remove observer-created 5037 server.'); - const killed = command(adbCandidates[0], ['-P', '5037', 'kill-server'], { allowFailure: true, timeout: 10000 }); - const after = command('/bin/ps', ['axo', 'pid=,command='], { allowFailure: true, timeout: 10000 }).stdout.split(/\r?\n/).map(line => line.trim()).filter(line => /adb\s+-L\s+tcp:5037\s+fork-server/i.test(line)); - if (after.length) throw new Error(`OBSERVER_ADB_5037_CLEANUP_FAILED: ${(killed.stderr || killed.stdout || '').trim()}`); - return { action: 'OBSERVER_ADB_5037_CLEANED', observed, killStatus: killed.status }; -} - function openPlayWeb() { const url = `https://play.google.com/store/apps/details?id=${PACKAGE}`; const result = command('/usr/bin/open', [url], { allowFailure: true, timeout: 30000 }); @@ -4715,6 +4688,9 @@ async function main() { if (action === 'prepare') { json(prepareAVD()); return; } if (action === 'engineering-map-selftest') { json(engineeringMapSelfTest()); return; } if (action === 'lab-selftest') { json(labSelfTest()); return; } + if (action === 'native-gate1-bootstrap') { json(bootstrapNativeGate1Authority()); return; } + if (action === 'native-gate1-build') { json(nativeGate1Script('build-native-app.command', 'NATIVE_GATE1_BUILD')); return; } + if (action === 'native-gate1-test') { json(nativeGate1Script('test-native-app.command', 'NATIVE_GATE1_TEST')); return; } if (action === 'build') { json(buildApp()); return; } if (action === 'install-app') { json(installApp()); return; } if (action === 'launch-app') { json(launchApp()); return; } @@ -4722,14 +4698,11 @@ async function main() { if (action === 'runtime-process-audit') { json(runtimeProcessAudit()); return; } if (action === 'cleanup-tftmac-adb-residue') { json(cleanupTftmacAdbResidue()); return; } if (action === 'single-runtime-preflight') { json(singleRuntimePreflight()); return; } - if (action === 'launch-mactician-control') { json(launchMacticianControl()); return; } - if (action === 'stop-mactician-control') { json(await stopMacticianControl()); return; } - if (action === 'mactician-runtime-audit') { json(macticianRuntimeAudit()); return; } - if (action === 'cleanup-observer-adb-5037') { json(cleanupObserverAdb5037()); return; } if (action === 'start') { json(await startControl()); return; } - if (action === 'start-donor-control') { json(await startDonorControl()); return; } - if (action === 'start-donor-control-5gb') { json(await startDonorControl(5120, 'mactician_compatible_5gb_v1', 800)); return; } - if (action === 'start-donor-control-5gb-flush400') { json(await startDonorControl(5120, 'mactician_compatible_5gb_flush400_v1', 400)); return; } + if (action === 'start-baseline-control') { json(await startBaselineControl()); return; } + if (action === 'start-baseline-control-5gb') { json(await startBaselineControl(5120, 'tftmac_5gb_baseline_v1', 800)); return; } + if (action === 'start-native-controller-probe') { json(await startBaselineControl(5120, 'tftmac_5gb_baseline_v1', 800, true)); return; } + if (action === 'start-baseline-control-5gb-flush400') { json(await startBaselineControl(5120, 'tftmac_5gb_flush400_exp_v1', 400)); return; } if (action === 'play-action') { json(await playAction()); return; } if (action === 'play-probe') { json(await playProbe()); return; } if (action === 'launch-game') { json(await launchGame()); return; } @@ -4836,7 +4809,7 @@ async function main() { await sampler(args[captureIndex + 1], args[sessionIndex + 1]); return; } - throw new Error('Usage: tftmac-direct-control.mjs inventory|prepare|lab-selftest|build|launch-app|runtime-process-audit|single-runtime-preflight|launch-mactician-control|mactician-runtime-audit|start|start-donor-control|play-action|launch-game|gles-capability-probe|launch-failure-probe|status|play-certification|marker|match-entry|combat-start|stop|package-state'); + throw new Error('Usage: tftmac-direct-control.mjs inventory|prepare|lab-selftest|build|launch-app|runtime-process-audit|single-runtime-preflight|start|start-baseline-control|play-action|launch-game|gles-capability-probe|launch-failure-probe|status|play-certification|marker|match-entry|combat-start|stop|package-state'); } main().catch(error => { process.stderr.write(`${error.stack || error.message}\n`); process.exit(1); }); diff --git a/tools/tftmac-v2.mjs b/tools/tftmac-v2.mjs deleted file mode 100644 index 960d1cc..0000000 --- a/tools/tftmac-v2.mjs +++ /dev/null @@ -1,1760 +0,0 @@ -#!/usr/bin/env node -import { createHash } from 'node:crypto'; -import { closeSync, createReadStream, existsSync, openSync } from 'node:fs'; -import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, symlink, writeFile } from 'node:fs/promises'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { homedir } from 'node:os'; -import { spawn, spawnSync } from 'node:child_process'; -import { DatabaseSync } from 'node:sqlite'; - -function resolveConsoleUserHome() { - const userResult = spawnSync('/usr/bin/stat', ['-f', '%Su', '/dev/console'], { encoding: 'utf8' }); - const user = String(userResult.stdout ?? '').trim(); - if (user && user !== 'root' && user !== 'loginwindow') { - const homeResult = spawnSync('/usr/bin/dscl', ['.', '-read', `/Users/${user}`, 'NFSHomeDirectory'], { encoding: 'utf8' }); - const match = String(homeResult.stdout ?? '').match(/NFSHomeDirectory:\s+(.+)/); - if (match?.[1]) return match[1].trim(); - } - return homedir(); -} - -const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const USER_HOME = resolveConsoleUserHome(); -const SANDBOX_HOME = homedir(); -const APP = join(USER_HOME, 'Library', 'Application Support', 'TFTMAC'); -const DEFAULT_BUILD_VOLUME = '/Volumes/MAC MINI M4'; -const DEFAULT_BUILD_ROOT = join(DEFAULT_BUILD_VOLUME, 'TFTMAC', 'Build'); -const BUILD = resolve(process.env.TFTMAC_BUILD_ROOT ?? DEFAULT_BUILD_ROOT); -const DEFAULT_RUNTIME_ROOT = join(DEFAULT_BUILD_VOLUME, 'TFTMAC', 'Runtime'); -const RUNTIME = resolve(process.env.TFTMAC_RUNTIME_ROOT ?? DEFAULT_RUNTIME_ROOT); -const SDK = join(RUNTIME, 'SDK'); -const AVD_HOME = join(RUNTIME, 'AVD'); -const PACKAGES = join(RUNTIME, 'Packages'); -const PROBES = join(RUNTIME, 'Probes'); -const MANIFESTS = join(RUNTIME, 'Manifests'); -const LOGS = join(APP, 'Logs'); -const DIAGNOSTICS = join(APP, 'Diagnostics'); -const ROLLBACK = join(APP, 'Rollback'); -const SSOT = join(REPO, 'ssot'); -const SOURCE_WORKER_STATE = join(LOGS, 'phase0-source-worker.json'); -const SOURCE_WORKER_STDOUT = join(LOGS, 'phase0-source-worker.stdout.log'); -const SOURCE_WORKER_STDERR = join(LOGS, 'phase0-source-worker.stderr.log'); -const PHASE1_BUILD_STATE = join(MANIFESTS, 'phase1-build-worker.json'); -const PHASE1_BUILD_LOG_ROOT = join(BUILD, 'logs'); -const PHASE1_BUILD_STDOUT = join(PHASE1_BUILD_LOG_ROOT, 'phase1-build.stdout.log'); -const PHASE1_BUILD_STDERR = join(PHASE1_BUILD_LOG_ROOT, 'phase1-build.stderr.log'); -const PHASE1_AEMU_ALIAS = '/private/tmp/tftmac-aemu'; -const DIRECT_CONTROL_REQUEST = join(REPO, '.tftmac-direct-control-request.json'); - -const EXPECTED = Object.freeze({ - architecture: 'arm64', - xcodeVersion: '26.6', - xcodeBuild: '17F113', - commandLineToolsArchive: 'commandlinetools-mac_arm64-15859902_latest.zip', - commandLineToolsURL: 'https://dl.google.com/android/repository/commandlinetools-mac_arm64-15859902_latest.zip', - commandLineToolsSHA256: '835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e', - androidPackages: [ - 'platform-tools', - 'emulator', - 'platforms;android-36', - 'build-tools;37.0.0', - 'system-images;android-36;google_apis_playstore;arm64-v8a' - ], - playImagePackage: 'system-images;android-36;google_apis_playstore;arm64-v8a', - minimumPlayImageRevision: 7, - avdName: 'TFTMAC_Live_API36', - aemuBranch: 'emu-master-dev', - vulkanSDKVersion: '1.4.357.0', - vulkanSDKSHA256: '539433589c83522e6f31b1c7b418a4167e21597a4a361ab119e1dc0760cf3865', - moltenVKReferenceTag: 'v1.4.2', - glesCTSTag: 'opengl-es-cts-3.2.14.1', - vulkanCTSTag: 'vulkan-cts-1.4.6.1', - vulkanCTSCommit: '5c8aae22885448d70a2873e94a93b24b49505c32', - vulkanSamplesCommit: '89dd3af22d41f9244eeab6e0650460112285c0e1' -}); - -function die(message, code = 1) { - console.error(`TFTMAC_V2: ${message}`); - process.exit(code); -} - -function run(executable, args = [], options = {}) { - const result = spawnSync(executable, args, { - cwd: options.cwd ?? REPO, - env: options.env ?? process.env, - encoding: 'utf8', - input: options.input, - maxBuffer: options.maxBuffer ?? 64 * 1024 * 1024, - stdio: ['pipe', 'pipe', 'pipe'] - }); - if (result.error) { - throw new Error(`${executable} failed to start: ${result.error.message}`); - } - const stdout = String(result.stdout ?? ''); - const stderr = String(result.stderr ?? ''); - if (result.status !== 0 && !options.allowFailure) { - const detail = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n'); - throw new Error(`${executable} ${args.join(' ')} exited ${result.status}${detail ? `\n${detail}` : ''}`); - } - return { status: result.status ?? 1, stdout, stderr }; -} - -function runInherited(executable, args = [], options = {}) { - const result = spawnSync(executable, args, { - cwd: options.cwd ?? REPO, - env: options.env ?? process.env, - stdio: 'inherit' - }); - if (result.error) { - throw new Error(`${executable} failed to start: ${result.error.message}`); - } - if (result.status !== 0) { - throw new Error(`${executable} ${args.join(' ')} exited ${result.status}`); - } - return { status: result.status ?? 1 }; -} - -function pidIsAlive(pid) { - if (!Number.isInteger(pid) || pid <= 0) return false; - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -async function readSourceWorkerState() { - if (!existsSync(SOURCE_WORKER_STATE)) return null; - try { - return JSON.parse(await readFile(SOURCE_WORKER_STATE, 'utf8')); - } catch { - return { schema: 1, status: 'INVALID_STATE_FILE', path: SOURCE_WORKER_STATE }; - } -} - -async function readPhase1BuildState() { - if (!existsSync(PHASE1_BUILD_STATE)) return null; - try { - return JSON.parse(await readFile(PHASE1_BUILD_STATE, 'utf8')); - } catch { - return { schema: 1, status: 'INVALID_STATE_FILE', path: PHASE1_BUILD_STATE }; - } -} - -function firstExisting(paths) { - return paths.find(path => path && existsSync(path)) ?? null; -} - -async function sha256(path) { - return new Promise((resolveHash, reject) => { - const hash = createHash('sha256'); - const stream = createReadStream(path); - stream.on('error', reject); - stream.on('data', chunk => hash.update(chunk)); - stream.on('end', () => resolveHash(hash.digest('hex'))); - }); -} - -async function atomicWrite(path, content) { - await mkdir(dirname(path), { recursive: true }); - const temp = `${path}.tmp-${process.pid}`; - await writeFile(temp, content); - await rename(temp, path); -} - -async function ensureRoots() { - const usesDefaultExternalBuild = !process.env.TFTMAC_BUILD_ROOT; - const usesDefaultExternalRuntime = !process.env.TFTMAC_RUNTIME_ROOT; - if ((usesDefaultExternalBuild || usesDefaultExternalRuntime) && !existsSync(DEFAULT_BUILD_VOLUME)) { - die(`TFTMAC_EXTERNAL_VOLUME_REQUIRED: ${DEFAULT_BUILD_VOLUME} is not mounted; refusing to create Build or Runtime data on the internal disk.`, 17); - } - for (const path of [BUILD, SDK, AVD_HOME, PACKAGES, PROBES, MANIFESTS, LOGS, DIAGNOSTICS, ROLLBACK, SSOT]) { - await mkdir(path, { recursive: true }); - } -} - -function parseXcode(text) { - const version = text.match(/^Xcode\s+([^\s]+)/m)?.[1] ?? null; - const build = text.match(/^Build version\s+([^\s]+)/m)?.[1] ?? null; - return { version, build }; -} - -function discoverInstalledXcodes() { - const applications = '/Applications'; - const listing = run('/bin/ls', ['-1', applications], { allowFailure: true }).stdout.split(/\r?\n/).filter(Boolean); - const applicationCandidates = listing.filter(name => /^Xcode.*\.app$/i.test(name)).map(name => join(applications, name)); - const spotlight = run('/usr/bin/mdfind', ["kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'"], { allowFailure: true }).stdout - .split(/\r?\n/).map(value => value.trim()).filter(value => /Xcode.*\.app$/i.test(value)); - const candidateApps = [...new Set([ - ...applicationCandidates, - ...spotlight, - join(USER_HOME, 'Downloads', 'Xcode.app'), - join(USER_HOME, 'Applications', 'Xcode.app') - ])]; - const discovered = []; - for (const app of candidateApps) { - if (!existsSync(app)) continue; - const developerDir = join(app, 'Contents', 'Developer'); - if (!existsSync(developerDir)) continue; - const result = run('/usr/bin/xcodebuild', ['-version'], { - env: { ...process.env, DEVELOPER_DIR: developerDir }, - allowFailure: true - }); - discovered.push({ app, developerDir, status: result.status, version: parseXcode(result.stdout) }); - } - return discovered; -} - -function findRequiredXcode(discovered) { - return discovered.find(item => item.status === 0 && item.version.version === EXPECTED.xcodeVersion && item.version.build === EXPECTED.xcodeBuild) ?? null; -} - -function parseMacOS(text) { - const values = {}; - for (const line of text.split(/\r?\n/)) { - const [key, ...rest] = line.split(':'); - if (rest.length) values[key.trim()] = rest.join(':').trim(); - } - return { - productName: values.ProductName ?? null, - productVersion: values.ProductVersion ?? null, - buildVersion: values.BuildVersion ?? null - }; -} - -function parseHardware(text) { - const modelName = text.match(/^\s*Model Name:\s*(.+)$/m)?.[1]?.trim() ?? null; - const modelIdentifier = text.match(/^\s*Model Identifier:\s*(.+)$/m)?.[1]?.trim() ?? null; - const chip = text.match(/^\s*Chip:\s*(.+)$/m)?.[1]?.trim() ?? null; - const memory = text.match(/^\s*Memory:\s*(.+)$/m)?.[1]?.trim() ?? null; - return { modelName, modelIdentifier, chip, memory }; -} - -function parseProperties(text) { - const result = {}; - for (const raw of text.split(/\r?\n/)) { - const line = raw.trim(); - if (!line || line.startsWith('#')) continue; - const index = line.indexOf('='); - if (index < 0) continue; - result[line.slice(0, index).trim()] = line.slice(index + 1).trim(); - } - return result; -} - -function yamlScalar(value) { - if (value === null) return 'null'; - if (typeof value === 'number' || typeof value === 'boolean') return String(value); - return JSON.stringify(String(value)); -} - -function setYamlScalar(text, path, value) { - const lines = text.split('\n'); - const stack = []; - for (let index = 0; index < lines.length; index += 1) { - const match = lines[index].match(/^(\s*)([A-Za-z0-9_]+):(?:\s*(.*))?$/); - if (!match) continue; - const indent = match[1].length; - if (indent % 2 !== 0) continue; - const level = indent / 2; - stack.length = level; - const key = match[2]; - const current = [...stack, key]; - if (current.length === path.length && current.every((part, i) => part === path[i])) { - lines[index] = `${match[1]}${key}: ${yamlScalar(value)}`; - return lines.join('\n'); - } - const remainder = match[3]; - if (remainder === undefined || remainder === '') stack[level] = key; - } - throw new Error(`STACK.lock.yaml path not found: ${path.join('.')}`); -} - -async function updateStackLock(updates) { - const path = join(SSOT, 'STACK.lock.yaml'); - let text = await readFile(path, 'utf8'); - for (const [yamlPath, value] of updates) text = setYamlScalar(text, yamlPath, value); - await atomicWrite(path, text); -} - -function parseInstalledPackages(text) { - const packages = new Map(); - for (const line of text.split(/\r?\n/)) { - if (!line.includes('|')) continue; - const columns = line.split('|').map(value => value.trim()); - if (columns.length < 2 || !columns[0] || columns[0] === 'Path' || /^-+$/.test(columns[0])) continue; - packages.set(columns[0], columns[1]); - } - return packages; -} - -const CRITICAL_LOCK_PATHS = Object.freeze([ - ['frozen_at'], - ['host', 'architecture'], - ['host', 'macos_version'], - ['host', 'macos_build'], - ['host', 'xcode_version'], - ['host', 'xcode_build'], - ['host', 'macos_sdk_path'], - ['host', 'hardware_model'], - ['android_command_line_tools', 'installed_revision'], - ['android', 'play_image_revision'], - ['android', 'platform_tools_revision'], - ['android', 'emulator_revision'], - ['android', 'platform_revision'], - ['android', 'build_tools_revision'], - ['aemu', 'resolved_manifest_sha256'], - ['aemu', 'qemu_commit'], - ['aemu', 'aemu_commit'], - ['aemu', 'gfxstream_commit'], - ['aemu', 'integrated_angle_commit'], - ['aemu', 'integrated_moltenvk_commit'], - ['aemu', 'guestangle_authority'], - ['vulkan_sdk', 'vulkaninfo_version'], - ['moltenvk', 'reference_commit'], - ['generality', 'gles_cts_commit'], - ['generality', 'vulkan_required_cases_sha256'] -]); - -function parseSimpleYamlScalars(text) { - const scalars = new Map(); - const stack = []; - for (const line of text.split(/\r?\n/)) { - if (!line.trim() || line.trim().startsWith('#') || /^\s*-\s/.test(line)) continue; - const match = line.match(/^(\s*)([A-Za-z0-9_]+):(?:\s*(.*))?$/); - if (!match) continue; - const indent = match[1].length; - if (indent % 2 !== 0) continue; - const level = indent / 2; - stack.length = level; - const key = match[2]; - const raw = match[3]; - const path = [...stack, key]; - if (raw === undefined || raw === '') { - stack[level] = key; - continue; - } - let value = raw.trim(); - if (value === 'null') value = null; - else if (value === 'true') value = true; - else if (value === 'false') value = false; - else if (/^-?\d+(?:\.\d+)?$/.test(value)) value = Number(value); - else if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - try { value = JSON.parse(value); } catch { value = value.slice(1, -1); } - } - scalars.set(path.join('.'), value); - } - return scalars; -} - -async function phase0Host() { - await ensureRoots(); - const arch = run('/usr/bin/uname', ['-m']).stdout.trim(); - const sw = parseMacOS(run('/usr/bin/sw_vers').stdout); - const installedXcodes = discoverInstalledXcodes(); - const requiredXcode = findRequiredXcode(installedXcodes); - const xcodeEnv = requiredXcode ? { ...process.env, DEVELOPER_DIR: requiredXcode.developerDir } : process.env; - const xcodeRaw = run('/usr/bin/xcodebuild', ['-version'], { env: xcodeEnv }).stdout; - const xcode = parseXcode(xcodeRaw); - const sdkPath = run('/usr/bin/xcrun', ['--sdk', 'macosx', '--show-sdk-path'], { env: xcodeEnv }).stdout.trim(); - const hardwareRaw = run('/usr/sbin/system_profiler', ['SPHardwareDataType']).stdout; - const hardware = parseHardware(hardwareRaw); - - const checks = { - architecture: arch === EXPECTED.architecture, - xcodeVersion: xcode.version === EXPECTED.xcodeVersion, - xcodeBuild: xcode.build === EXPECTED.xcodeBuild, - macosVersionPresent: Boolean(sw.productVersion), - macosSDKPresent: Boolean(sdkPath), - appleSiliconChip: /Apple\s+M\d|Apple Silicon/i.test(hardware.chip ?? '') - }; - const pass = Object.values(checks).every(Boolean); - const artifact = { - schema: 1, - observedAt: new Date().toISOString(), - expected: { - architecture: EXPECTED.architecture, - xcodeVersion: EXPECTED.xcodeVersion, - xcodeBuild: EXPECTED.xcodeBuild - }, - observed: { - architecture: arch, - macOS: sw, - xcode, - selectedXcodeBundle: requiredXcode?.app ?? null, - selectedDeveloperDir: requiredXcode?.developerDir ?? null, - installedXcodes, - macosSDKPath: sdkPath, - hardware - }, - checks, - pass - }; - await atomicWrite(join(SSOT, 'host-preflight.json'), `${JSON.stringify(artifact, null, 2)}\n`); - const toolVersions = [ - `uname=${arch}`, - `macos=${sw.productVersion ?? 'unknown'} (${sw.buildVersion ?? 'unknown'})`, - `xcode=${xcode.version ?? 'unknown'} (${xcode.build ?? 'unknown'})`, - `macos_sdk=${sdkPath}`, - `node=${process.version}`, - `git=${run('/usr/bin/git', ['--version']).stdout.trim()}` - ]; - await atomicWrite(join(SSOT, 'tool-versions.txt'), `${toolVersions.join('\n')}\n`); - await updateStackLock([ - [['host', 'architecture'], arch], - [['host', 'macos_version'], sw.productVersion], - [['host', 'macos_build'], sw.buildVersion], - [['host', 'hardware_model'], hardware.modelIdentifier ?? hardware.modelName] - ]); - if (requiredXcode && checks.xcodeVersion && checks.xcodeBuild && checks.macosSDKPresent) { - await updateStackLock([ - [['host', 'xcode_version'], xcode.version], - [['host', 'xcode_build'], xcode.build], - [['host', 'macos_sdk_path'], sdkPath] - ]); - } - console.log(JSON.stringify({ phase: '0-host', pass, checks, observed: artifact.observed }, null, 2)); - if (!pass) die('PHASE_0_HOST_PREFLIGHT_FAILED', 2); -} - -async function downloadVerified(url, destination, expectedSHA256) { - await mkdir(dirname(destination), { recursive: true }); - if (existsSync(destination)) { - const current = await sha256(destination); - if (current === expectedSHA256) return; - await rm(destination, { force: true }); - } - const partial = `${destination}.partial`; - run('/usr/bin/curl', ['-fL', '--retry', '3', '--retry-delay', '2', '--continue-at', '-', url, '-o', partial]); - const actual = await sha256(partial); - if (actual !== expectedSHA256) { - await rm(partial, { force: true }); - throw new Error(`checksum mismatch for ${url}: expected ${expectedSHA256}, got ${actual}`); - } - await rename(partial, destination); -} - -async function installCommandLineTools() { - await ensureRoots(); - const archive = join(PACKAGES, EXPECTED.commandLineToolsArchive); - await downloadVerified(EXPECTED.commandLineToolsURL, archive, EXPECTED.commandLineToolsSHA256); - const sdkmanager = join(SDK, 'cmdline-tools', 'latest', 'bin', 'sdkmanager'); - if (!existsSync(sdkmanager)) { - const stage = join(PACKAGES, '.cmdline-tools-stage'); - await rm(stage, { recursive: true, force: true }); - await mkdir(stage, { recursive: true }); - run('/usr/bin/unzip', ['-q', archive, '-d', stage]); - const source = join(stage, 'cmdline-tools'); - if (!existsSync(source)) throw new Error('Android command-line tools archive has unexpected layout'); - const latest = join(SDK, 'cmdline-tools', 'latest'); - await rm(latest, { recursive: true, force: true }); - await mkdir(dirname(latest), { recursive: true }); - await rename(source, latest); - await rm(stage, { recursive: true, force: true }); - } - const hash = await sha256(archive); - console.log(JSON.stringify({ archive, sha256: hash, sdkmanager }, null, 2)); -} - -async function seedPreviouslyAcceptedAndroidLicenses() { - const target = join(SDK, 'licenses'); - if (existsSync(join(target, 'android-sdk-license'))) return { seeded: false, source: 'already-local' }; - const spotlightLicenses = run('/usr/bin/mdfind', ["kMDItemFSName == 'android-sdk-license'"], { allowFailure: true }).stdout - .split(/\r?\n/).map(value => value.trim()).filter(Boolean).map(path => dirname(dirname(path))); - const candidates = [...new Set([ - process.env.ANDROID_SDK_ROOT, - process.env.ANDROID_HOME, - join(USER_HOME, 'Library', 'Android', 'sdk'), - ...spotlightLicenses - ].filter(Boolean))]; - for (const candidate of candidates) { - const source = join(candidate, 'licenses'); - if (!existsSync(join(source, 'android-sdk-license'))) continue; - await mkdir(target, { recursive: true }); - for (const name of ['android-sdk-license', 'android-sdk-preview-license', 'google-gdk-license']) { - const from = join(source, name); - if (existsSync(from)) await copyFile(from, join(target, name)); - } - return { seeded: true, source }; - } - return { seeded: false, source: null }; -} - -function resolveJavaHome() { - const tool = run('/usr/libexec/java_home', ['-v', '17'], { allowFailure: true }).stdout.trim(); - const candidates = [ - process.env.JAVA_HOME, - tool, - '/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home', - '/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home', - '/Applications/Android Studio.app/Contents/jbr/Contents/Home', - '/Applications/Android Studio.app/Contents/jre/Contents/Home' - ].filter(Boolean); - return candidates.find(home => existsSync(join(home, 'bin', 'java'))) ?? null; -} - -function androidEnv() { - const javaHome = resolveJavaHome(); - if (!javaHome) throw new Error('JAVA_17_REQUIRED: no usable Java 17 runtime was found for Android sdkmanager.'); - return { - ...process.env, - JAVA_HOME: javaHome, - PATH: `${join(javaHome, 'bin')}:${process.env.PATH ?? ''}`, - ANDROID_SDK_ROOT: SDK, - ANDROID_HOME: SDK, - ANDROID_AVD_HOME: AVD_HOME, - ANDROID_ADB_SERVER_PORT: '5040', - ADB_MDNS_AUTO_CONNECT: '' - }; -} - -async function phase0Android() { - await ensureRoots(); - await installCommandLineTools(); - const license = await seedPreviouslyAcceptedAndroidLicenses(); - const sdkmanager = join(SDK, 'cmdline-tools', 'latest', 'bin', 'sdkmanager'); - const androidCli = join(SDK, 'cmdline-tools', 'latest', 'bin', 'android'); - const avdmanager = join(SDK, 'cmdline-tools', 'latest', 'bin', 'avdmanager'); - if (!existsSync(join(SDK, 'licenses', 'android-sdk-license'))) { - await atomicWrite(join(SSOT, 'android-license-status.json'), `${JSON.stringify({ acceptedLicenseEvidenceFound: false, checkedAt: new Date().toISOString() }, null, 2)}\n`); - die('ANDROID_LICENSE_ACCEPTANCE_REQUIRED: no previously accepted Android SDK license evidence was found; no license was accepted automatically.', 3); - } - if (!existsSync(androidCli)) throw new Error(`ANDROID_CLI_REQUIRED: ${androidCli} was not found.`); - const env = androidEnv(); - const api36CatalogResult = run(androidCli, [`--sdk=${SDK}`, 'sdk', 'list', '.*(36|Baklava).*', '--all', '--all-versions'], { env, allowFailure: true, maxBuffer: 128 * 1024 * 1024 }); - await atomicWrite(join(SSOT, 'android-api36-catalog.txt'), `${api36CatalogResult.stdout}${api36CatalogResult.stderr}`); - run(androidCli, [`--sdk=${SDK}`, 'sdk', 'install', 'platform-tools', 'emulator', 'build-tools/37.0.0'], { env, maxBuffer: 128 * 1024 * 1024 }); - run(androidCli, [`--sdk=${SDK}`, 'sdk', 'install', '--canary', 'platforms/android-36', 'system-images/android-36/google_apis_playstore/arm64-v8a'], { env, maxBuffer: 128 * 1024 * 1024 }); - - const revisionAt = async relative => { - const propertiesPath = join(SDK, relative, 'source.properties'); - if (!existsSync(propertiesPath)) return null; - return parseProperties(await readFile(propertiesPath, 'utf8'))['Pkg.Revision'] ?? null; - }; - const discoverPlatform36 = async () => { - const root = join(SDK, 'platforms'); - if (!existsSync(root)) return null; - for (const entry of await readdir(root, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const propertiesPath = join(root, entry.name, 'source.properties'); - if (!existsSync(propertiesPath)) continue; - const properties = parseProperties(await readFile(propertiesPath, 'utf8')); - if (!String(properties['AndroidVersion.ApiLevel'] ?? '').startsWith('36')) continue; - return { directory: entry.name, revision: properties['Pkg.Revision'] ?? null, codeName: properties['AndroidVersion.CodeName'] ?? null }; - } - return null; - }; - const platform36 = await discoverPlatform36(); - const requiredVersions = { - 'platform-tools': await revisionAt('platform-tools'), - 'emulator': await revisionAt('emulator'), - 'platforms;android-36': platform36?.revision ?? null, - 'build-tools;37.0.0': await revisionAt('build-tools/37.0.0'), - [EXPECTED.playImagePackage]: await revisionAt('system-images/android-36/google_apis_playstore/arm64-v8a') - }; - const missingPackages = Object.entries(requiredVersions).filter(([, version]) => !version).map(([path]) => path); - if (missingPackages.length) throw new Error(`required Android packages missing after install: ${missingPackages.join(', ')}`); - const installed = `${JSON.stringify({ schema: 1, observedAt: new Date().toISOString(), requiredVersions }, null, 2)}\n`; - await atomicWrite(join(SSOT, 'android-sdk-packages.txt'), installed); - const playRevision = Number.parseInt(requiredVersions[EXPECTED.playImagePackage], 10); - if (!Number.isFinite(playRevision) || playRevision < EXPECTED.minimumPlayImageRevision) { - throw new Error(`Android Play image revision ${requiredVersions[EXPECTED.playImagePackage]} is below required ${EXPECTED.minimumPlayImageRevision}`); - } - const cmdlinePropertiesPath = join(SDK, 'cmdline-tools', 'latest', 'source.properties'); - const cmdlineRevision = existsSync(cmdlinePropertiesPath) ? parseProperties(await readFile(cmdlinePropertiesPath, 'utf8'))['Pkg.Revision'] ?? null : null; - if (!cmdlineRevision) throw new Error('could not resolve installed Android command-line tools revision'); - - const ini = join(AVD_HOME, `${EXPECTED.avdName}.ini`); - const configPath = join(AVD_HOME, `${EXPECTED.avdName}.avd`, 'config.ini'); - if (!existsSync(configPath) || !existsSync(ini)) { - run(avdmanager, [ - 'create', 'avd', '--name', EXPECTED.avdName, - '--package', EXPECTED.playImagePackage, - '--device', 'pixel_tablet', '--force' - ], { env: androidEnv(), input: 'no\n' }); - } - let config = existsSync(configPath) ? await readFile(configPath, 'utf8') : ''; - const values = { - AvdId: EXPECTED.avdName, - 'avd.ini.displayname': 'TFTMAC Live', - 'hw.device.manufacturer': 'Google', - 'hw.device.name': 'pixel_tablet', - 'hw.initialOrientation': 'Landscape', - 'hw.cpu.arch': 'arm64', - 'hw.cpu.ncore': '6', - 'hw.ramSize': '6144', - 'hw.vmHeapSize': '768', - 'hw.lcd.width': '1920', - 'hw.lcd.height': '1080', - 'hw.lcd.density': '320', - 'hw.gpu.enabled': 'yes', - 'hw.gpu.mode': 'host', - 'hw.audioInput': 'yes', - 'hw.keyboard': 'yes', - showDeviceFrame: 'no', - 'skin.name': '1920x1080', - 'disk.dataPartition.size': '16G', - 'runtime.network.speed': 'full', - 'runtime.network.latency': 'none', - 'PlayStore.enabled': 'true', - 'fastboot.forceColdBoot': 'yes', - 'fastboot.forceFastBoot': 'no' - }; - for (const [key, value] of Object.entries(values)) { - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const pattern = new RegExp(`^${escaped}=.*$`, 'm'); - config = pattern.test(config) ? config.replace(pattern, `${key}=${value}`) : `${config.replace(/\s*$/, '')}\n${key}=${value}\n`; - } - await atomicWrite(configPath, config); - await atomicWrite(join(SSOT, 'android-license-status.json'), `${JSON.stringify({ acceptedLicenseEvidenceFound: true, seededFrom: license.source, checkedAt: new Date().toISOString() }, null, 2)}\n`); - await updateStackLock([ - [['android_command_line_tools', 'installed_revision'], cmdlineRevision], - [['android', 'play_image_revision'], playRevision], - [['android', 'platform_tools_revision'], requiredVersions['platform-tools']], - [['android', 'emulator_revision'], requiredVersions['emulator']], - [['android', 'platform_revision'], requiredVersions['platforms;android-36']], - [['android', 'build_tools_revision'], requiredVersions['build-tools;37.0.0']] - ]); - console.log(JSON.stringify({ phase: '0-android', pass: true, license, avd: EXPECTED.avdName, sdk: SDK, commandLineToolsRevision: cmdlineRevision, requiredVersions }, null, 2)); -} - -async function phase0Vulkan() { - await ensureRoots(); - const externalSDKRoot = join(RUNTIME, 'VulkanSDK', EXPECTED.vulkanSDKVersion); - const externalVulkanInfo = join(externalSDKRoot, 'macOS', 'bin', 'vulkaninfo'); - const expectedUserSDKRoot = join(USER_HOME, 'VulkanSDK', EXPECTED.vulkanSDKVersion, 'macOS'); - let vulkanInfo = firstExisting([ - process.env.VULKAN_SDK ? join(process.env.VULKAN_SDK, 'bin', 'vulkaninfo') : null, - externalVulkanInfo, - join(expectedUserSDKRoot, 'bin', 'vulkaninfo'), - '/opt/homebrew/bin/vulkaninfo', - '/usr/local/bin/vulkaninfo' - ]); - - if (!vulkanInfo) { - const archive = join(PACKAGES, `vulkansdk-macos-${EXPECTED.vulkanSDKVersion}.zip`); - await downloadVerified(`https://sdk.lunarg.com/sdk/download/${EXPECTED.vulkanSDKVersion}/mac/vulkan_sdk.zip`, archive, EXPECTED.vulkanSDKSHA256); - const stage = join(PACKAGES, `vulkansdk-macos-${EXPECTED.vulkanSDKVersion}-installer`); - if (!existsSync(stage)) { - await mkdir(stage, { recursive: true }); - run('/usr/bin/unzip', ['-q', archive, '-d', stage], { maxBuffer: 128 * 1024 * 1024 }); - } - const installerApp = run('/usr/bin/find', [stage, '-maxdepth', '3', '-name', 'vulkansdk-macOS-*.app', '-print'], { allowFailure: true }).stdout - .split(/\r?\n/).map(value => value.trim()).find(Boolean) ?? null; - const installerBinary = installerApp ? join(installerApp, 'Contents', 'MacOS', installerApp.split('/').pop().replace(/\.app$/, '')) : null; - await atomicWrite(join(SSOT, 'vulkan-install-required.json'), `${JSON.stringify({ - schema: 1, - version: EXPECTED.vulkanSDKVersion, - archive, - archiveSHA256: EXPECTED.vulkanSDKSHA256, - installerApp, - installerBinary, - installRoot: externalSDKRoot, - copyOnly: true, - licenseAcceptanceRequired: true - }, null, 2)}\n`); - die(`VULKAN_SDK_LICENSE_ACCEPTANCE_REQUIRED: verified installer staged at ${installerApp ?? stage}; install root must be ${externalSDKRoot}.`, 4); - } - - const versionOutput = run(vulkanInfo, ['--version'], { allowFailure: true }).stdout.trim(); - const sdkPathVersionMatch = vulkanInfo.includes(`/${EXPECTED.vulkanSDKVersion}/`); - const envVersionMatch = Boolean(process.env.VULKAN_SDK?.includes(EXPECTED.vulkanSDKVersion)); - if (!sdkPathVersionMatch && !envVersionMatch) { - die(`VULKAN_SDK_VERSION_NOT_PROVEN: found ${vulkanInfo}, but frozen ${EXPECTED.vulkanSDKVersion} identity is not proven.`, 16); - } - const sdkRoot = resolve(vulkanInfo, '..', '..'); - const env = { - ...process.env, - VULKAN_SDK: sdkRoot, - PATH: `${join(sdkRoot, 'bin')}:${process.env.PATH ?? ''}`, - DYLD_LIBRARY_PATH: `${join(sdkRoot, 'lib')}:${process.env.DYLD_LIBRARY_PATH ?? ''}`, - VK_ICD_FILENAMES: join(sdkRoot, 'share', 'vulkan', 'icd.d', 'MoltenVK_icd.json'), - VK_LAYER_PATH: join(sdkRoot, 'share', 'vulkan', 'explicit_layer.d') - }; - const summary = run(vulkanInfo, ['--summary'], { env, maxBuffer: 64 * 1024 * 1024 }).stdout; - await atomicWrite(join(SSOT, 'vulkaninfo-summary.txt'), summary); - await updateStackLock([[['vulkan_sdk', 'vulkaninfo_version'], versionOutput || EXPECTED.vulkanSDKVersion]]); - console.log(JSON.stringify({ phase: '0-vulkan', pass: true, vulkaninfo: vulkanInfo, sdkRoot, version: versionOutput || null }, null, 2)); -} - -async function ensureRepoTool() { - const systemRepo = firstExisting(['/opt/homebrew/bin/repo', '/usr/local/bin/repo']); - if (systemRepo) return systemRepo; - const localRepo = join(BUILD, 'bin', 'repo'); - if (!existsSync(localRepo)) { - await mkdir(dirname(localRepo), { recursive: true }); - run('/usr/bin/curl', ['-fL', '--retry', '3', 'https://storage.googleapis.com/git-repo-downloads/repo', '-o', localRepo]); - await chmod(localRepo, 0o755); - } - return localRepo; -} - -function gitCommit(path) { - return run('/usr/bin/git', ['-C', path, 'rev-parse', 'HEAD']).stdout.trim(); -} - -function lineEvidence(text, patterns) { - const lines = text.split(/\r?\n/); - const evidence = []; - for (let index = 0; index < lines.length; index += 1) { - if (patterns.some(pattern => pattern.test(lines[index]))) { - evidence.push({ line: index + 1, text: lines[index].trim() }); - } - } - return evidence; -} - -function resolveRemoteTag(url, tag) { - const output = run('/usr/bin/git', ['ls-remote', '--tags', url, `refs/tags/${tag}`, `refs/tags/${tag}^{}`]).stdout; - const rows = output.trim().split(/\r?\n/).filter(Boolean).map(line => line.split(/\s+/)); - const peeled = rows.find(row => row[1]?.endsWith('^{}')); - const direct = rows.find(row => row[1] === `refs/tags/${tag}`); - return peeled?.[0] ?? direct?.[0] ?? null; -} - -async function discoverIntegratedMoltenVK(components, commits) { - const probes = []; - for (const [component, path] of Object.entries(components)) { - const grep = run('/usr/bin/git', ['-C', path, 'grep', '-n', '-i', 'MoltenVK', '--', '.'], { - allowFailure: true, - maxBuffer: 32 * 1024 * 1024 - }); - const matches = grep.stdout.split(/\r?\n/).filter(Boolean); - const hashCandidates = [...new Set(matches.flatMap(line => line.match(/\b[0-9a-f]{40}\b/g) ?? []))]; - probes.push({ - component, - path, - commit: commits[component], - matchCount: matches.length, - hashCandidates, - matches: matches.slice(0, 80) - }); - } - const artifact = { - schema: 1, - observedAt: new Date().toISOString(), - branchAuthority: EXPECTED.aemuBranch, - probes - }; - await atomicWrite(join(SSOT, 'moltenvk-integration-discovery.json'), `${JSON.stringify(artifact, null, 2)}\n`); - return artifact; -} - -async function phase0SourceForeground() { - await ensureRoots(); - const repoTool = await ensureRepoTool(); - const aemuRoot = join(BUILD, 'aemu'); - await mkdir(aemuRoot, { recursive: true }); - const repoEnv = { ...process.env, HOME: USER_HOME }; - runInherited(repoTool, ['init', '-u', 'https://android.googlesource.com/platform/manifest', '-b', EXPECTED.aemuBranch], { - cwd: aemuRoot, - env: repoEnv - }); - runInherited(repoTool, ['sync', '-c', '-j8'], { - cwd: aemuRoot, - env: repoEnv - }); - - const resolvedManifest = run(repoTool, ['manifest', '-r'], { - cwd: aemuRoot, - env: repoEnv, - maxBuffer: 256 * 1024 * 1024 - }).stdout; - const manifestPath = join(SSOT, 'upstreams-aemu.lock.xml'); - await atomicWrite(manifestPath, resolvedManifest); - const manifestSHA256 = await sha256(manifestPath); - - const components = { - qemu: join(aemuRoot, 'external', 'qemu'), - aemu: join(aemuRoot, 'hardware', 'google', 'aemu'), - gfxstream: join(aemuRoot, 'hardware', 'google', 'gfxstream'), - angle: join(aemuRoot, 'external', 'angle'), - moltenvkPrebuilt: join(aemuRoot, 'prebuilts', 'android-emulator') - }; - for (const [name, path] of Object.entries(components)) { - if (!existsSync(path)) throw new Error(`locked AEMU manifest is missing required component ${name}: ${path}`); - } - const commits = Object.fromEntries(Object.entries(components).map(([name, path]) => [name, gitCommit(path)])); - const moltenVKConfigPath = join(components.qemu, 'android', 'build', 'cmake', 'config', 'emu-vulkan-config.cmake'); - const moltenVKConfigText = await readFile(moltenVKConfigPath, 'utf8'); - const moltenVKPrebuiltEvidence = lineEvidence(moltenVKConfigText, [/PREBUILT_ROOT.*libMoltenVK\.dylib/, /PREBUILT_ROOT.*MoltenVK_icd\.json/]); - if (moltenVKPrebuiltEvidence.length < 2) { - throw new Error('MOLTENVK_PREBUILT_AUTHORITY_NOT_PROVEN: qemu does not prove MoltenVK is supplied from PREBUILT_ROOT/icds.'); - } - const integratedMoltenVKCommit = commits.moltenvkPrebuilt; - await atomicWrite(join(SSOT, 'moltenvk-integration-discovery.json'), `${JSON.stringify({ - schema: 2, - observedAt: new Date().toISOString(), - branchAuthority: EXPECTED.aemuBranch, - authorityType: 'resolved-manifest-prebuilt', - authorityPath: 'prebuilts/android-emulator', - authorityCommit: integratedMoltenVKCommit, - qemuCommit: commits.qemu, - qemuEvidencePath: 'external/qemu/android/build/cmake/config/emu-vulkan-config.cmake', - evidence: moltenVKPrebuiltEvidence, - pass: true - }, null, 2)}\n`); - - const guestAngleSource = join(components.qemu, 'android', 'android-emu', 'android', 'userspace-boot-properties.cpp'); - const sourceText = await readFile(guestAngleSource, 'utf8'); - const sourceSHA256 = await sha256(guestAngleSource); - const checks = { - guestAngleFeatureGate: /fc::isEnabled\(fc::GuestAngle\)/.test(sourceText), - hardwareEGLAngle: /params\.push_back\(\{\"androidboot\.hardwareegl\",\s*\"angle\"\}\)/.test(sourceText), - guestAngleRequiresVulkan: /Cannot use GuestAngle without Vulkan enabled/.test(sourceText), - hardwareVulkanRanchu: /params\.push_back\(\{\"androidboot\.hardware\.vulkan\",\s*\"ranchu\"\}\)/.test(sourceText), - nonconformantExposureExplicitlyDisabled: /Without turning off exposeNonConformantExtensionsAndVersions/.test(sourceText) && /extensionLimitStr\s*=\s*\"exposeN\*\"/.test(sourceText) && /angle_overrides_disabled\s*\+=\s*extensionLimitStr/.test(sourceText) - }; - const guestAnglePass = Object.values(checks).every(Boolean); - const guestAngleArtifact = { - schema: 1, - observedAt: new Date().toISOString(), - branchAuthority: EXPECTED.aemuBranch, - qemuCommit: commits.qemu, - sourcePath: 'external/qemu/android/android-emu/android/userspace-boot-properties.cpp', - sourceSHA256, - checks, - evidence: lineEvidence(sourceText, [ - /GuestAngle/, - /androidboot\.hardwareegl/, - /androidboot\.hardware\.vulkan/, - /exposeNonConformantExtensionsAndVersions/, - /extensionLimitStr/ - ]), - pass: guestAnglePass - }; - await atomicWrite(join(SSOT, 'guestangle-authority.json'), `${JSON.stringify(guestAngleArtifact, null, 2)}\n`); - - const referenceRoot = join(BUILD, 'references'); - await mkdir(referenceRoot, { recursive: true }); - const moltenVKReference = join(referenceRoot, 'MoltenVK-1.4.2'); - if (!existsSync(join(moltenVKReference, '.git'))) { - await rm(moltenVKReference, { recursive: true, force: true }); - run('/usr/bin/git', ['clone', '--depth', '1', '--branch', EXPECTED.moltenVKReferenceTag, 'https://github.com/KhronosGroup/MoltenVK.git', moltenVKReference], { maxBuffer: 128 * 1024 * 1024 }); - } - const moltenVKReferenceCommit = gitCommit(moltenVKReference); - const glesCTSCommit = resolveRemoteTag('https://github.com/KhronosGroup/VK-GL-CTS.git', EXPECTED.glesCTSTag); - if (!glesCTSCommit) throw new Error(`could not resolve ${EXPECTED.glesCTSTag}`); - - const repoVersion = run(repoTool, ['--version'], { cwd: aemuRoot, env: repoEnv, allowFailure: true }).stdout.trim(); - const repoToolSHA256 = await sha256(repoTool); - const sourceArtifact = { - schema: 1, - observedAt: new Date().toISOString(), - aemuBranch: EXPECTED.aemuBranch, - manifestSHA256, - commits, - guestAnglePass, - moltenVKIntegration: { authority: 'prebuilts/android-emulator', commit: integratedMoltenVKCommit, discovery: 'ssot/moltenvk-integration-discovery.json' }, - moltenVKReference: { tag: EXPECTED.moltenVKReferenceTag, commit: moltenVKReferenceCommit }, - generality: { - glesCTSTag: EXPECTED.glesCTSTag, - glesCTSCommit, - vulkanCTSTag: EXPECTED.vulkanCTSTag, - vulkanCTSCommit: EXPECTED.vulkanCTSCommit, - vulkanSamplesCommit: EXPECTED.vulkanSamplesCommit - }, - repoTool: { path: repoTool, sha256: repoToolSHA256, version: repoVersion } - }; - await atomicWrite(join(SSOT, 'phase0-source.json'), `${JSON.stringify(sourceArtifact, null, 2)}\n`); - await atomicWrite(join(SSOT, 'source-hashes.txt'), [ - `upstreams-aemu.lock.xml sha256=${manifestSHA256}`, - `userspace-boot-properties.cpp sha256=${sourceSHA256}`, - `repo sha256=${repoToolSHA256}`, - `integrated-moltenvk commit=${integratedMoltenVKCommit}`, - `moltenvk-reference commit=${moltenVKReferenceCommit}`, - `gles-cts commit=${glesCTSCommit}` - ].join('\n') + '\n'); - await updateStackLock([ - [['aemu', 'resolved_manifest_sha256'], manifestSHA256], - [['aemu', 'qemu_commit'], commits.qemu], - [['aemu', 'aemu_commit'], commits.aemu], - [['aemu', 'gfxstream_commit'], commits.gfxstream], - [['aemu', 'integrated_angle_commit'], commits.angle], - [['aemu', 'integrated_moltenvk_commit'], integratedMoltenVKCommit], - [['aemu', 'guestangle_authority'], guestAnglePass ? 'PASS' : 'FAIL'], - [['moltenvk', 'reference_commit'], moltenVKReferenceCommit], - [['generality', 'gles_cts_commit'], glesCTSCommit] - ]); - console.log(JSON.stringify(sourceArtifact, null, 2)); - if (!guestAnglePass) throw new Error('GUESTANGLE_AUTHORITY_FAILED: locked source semantics do not satisfy the SSOT.'); -} - -async function phase0SourceWorker() { - await ensureRoots(); - const startedAt = process.env.TFTMAC_SOURCE_WORKER_STARTED_AT ?? new Date().toISOString(); - const baseState = { - schema: 1, - pid: process.pid, - startedAt, - buildRoot: BUILD, - aemuRoot: join(BUILD, 'aemu'), - branch: EXPECTED.aemuBranch, - stdoutPath: SOURCE_WORKER_STDOUT, - stderrPath: SOURCE_WORKER_STDERR - }; - await atomicWrite(SOURCE_WORKER_STATE, `${JSON.stringify({ ...baseState, status: 'RUNNING' }, null, 2)}\n`); - try { - await phase0SourceForeground(); - await atomicWrite(SOURCE_WORKER_STATE, `${JSON.stringify({ ...baseState, status: 'SUCCEEDED', endedAt: new Date().toISOString() }, null, 2)}\n`); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - await atomicWrite(SOURCE_WORKER_STATE, `${JSON.stringify({ ...baseState, status: 'FAILED', endedAt: new Date().toISOString(), error: message }, null, 2)}\n`); - throw error; - } -} - -async function phase0Source() { - await ensureRoots(); - const existing = await readSourceWorkerState(); - if (existing?.pid && pidIsAlive(existing.pid)) { - console.log(JSON.stringify({ - phase: '0-source', - launched: false, - alreadyRunning: true, - worker: existing - }, null, 2)); - return; - } - - const startedAt = new Date().toISOString(); - const stdoutFd = openSync(SOURCE_WORKER_STDOUT, 'a'); - const stderrFd = openSync(SOURCE_WORKER_STDERR, 'a'); - try { - const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'phase0-source-worker'], { - cwd: REPO, - env: { ...process.env, TFTMAC_SOURCE_WORKER_STARTED_AT: startedAt }, - detached: true, - stdio: ['ignore', stdoutFd, stderrFd] - }); - child.unref(); - console.log(JSON.stringify({ - phase: '0-source', - launched: true, - pid: child.pid, - startedAt, - buildRoot: BUILD, - branch: EXPECTED.aemuBranch, - statePath: SOURCE_WORKER_STATE, - stdoutPath: SOURCE_WORKER_STDOUT, - stderrPath: SOURCE_WORKER_STDERR - }, null, 2)); - } finally { - closeSync(stdoutFd); - closeSync(stderrFd); - } -} - -const AUTHORITY_FILES = Object.freeze([ - { - path: join(REPO, 'TFTMAC_FULL_IMPLEMENTATION_PLAN.md'), - sha256: '81bd386c1d9d47f26659c93af914ec9a92568ee689e7bed47eb0f2afd6dd5f1a' - }, - { - path: join(REPO, 'TFTMAC_GPU_RUNTIME_SSOT.md'), - sha256: '4c905ad35a676aa8f4ad0a22416e73d640120851ce9f41e742349ac61ceab1ea' - } -]); - -async function importAuthorityFiles() { - const verified = []; - for (const entry of AUTHORITY_FILES) { - if (!existsSync(entry.path)) throw new Error(`canonical authority document is unavailable: ${entry.path}`); - const actualSHA256 = await sha256(entry.path); - if (actualSHA256 !== entry.sha256) { - throw new Error(`canonical authority hash mismatch for ${entry.path}: expected ${entry.sha256}, got ${actualSHA256}`); - } - verified.push({ path: entry.path.replace(`${REPO}/`, ''), sha256: actualSHA256 }); - } - console.log(JSON.stringify({ phase: '0-authority-verify', pass: true, verified }, null, 2)); -} - -async function phase0Policy() { - await ensureRoots(); - const authority = []; - for (const entry of AUTHORITY_FILES) { - if (!existsSync(entry.path)) { - authority.push({ path: entry.path.replace(`${REPO}/`, ''), expectedSHA256: entry.sha256, exists: false, hashMatches: false }); - continue; - } - const actualSHA256 = await sha256(entry.path); - authority.push({ - path: entry.path.replace(`${REPO}/`, ''), - expectedSHA256: entry.sha256, - actualSHA256, - exists: true, - hashMatches: actualSHA256 === entry.sha256 - }); - } - - const legacyChecks = []; - const legacyFiles = [ - ['tftmac/Sources/TFTMACApp.swift', [ - ['opengles-version-boot-property', 'androidboot.opengles.version=196610'], - ['legacy-avd-name', 'TftHighEndTablet'] - ]], - ['tools/clara-task.mjs', [ - ['legacy-api36-image', 'system-images;android-36;google_apis_playstore;arm64-v8a'], - ['legacy-acquisition-avd', "const AVD_NAME = 'TftLiveStore'"], - ['legacy-performance-avd', "const PERFORMANCE_AVD_NAME = 'TftHighEndTablet'"] - ]], - ['TFTMAC.md', [ - ['legacy-api36-doc', 'Android 16 / API 36'], - ['legacy-196610-proof-doc', 'OpenGL ES: **3.2** (`196610`)'] - ]] - ]; - for (const [relative, checks] of legacyFiles) { - const path = join(REPO, relative); - const text = existsSync(path) ? await readFile(path, 'utf8') : ''; - for (const [id, needle] of checks) { - legacyChecks.push({ id, path: relative, present: text.includes(needle), classification: 'LEGACY_DONOR_NOT_V2_ACCEPTANCE' }); - } - } - - const forbiddenRuntimeProof = legacyChecks.filter(item => item.present).map(item => item.id); - const authorityPass = authority.every(item => item.exists && item.hashMatches); - const result = { - schema: 1, - observedAt: new Date().toISOString(), - authority, - legacyChecks, - authorityPass, - legacyRuntimeClaimsQuarantined: true, - forbiddenRuntimeProof, - pass: authorityPass - }; - await atomicWrite(join(SSOT, 'phase0-policy.json'), `${JSON.stringify(result, null, 2)}\n`); - console.log(JSON.stringify(result, null, 2)); - if (!result.pass) die('PHASE_0_POLICY_FAILED: canonical authority documents are missing or do not match approved hashes.', 14); -} - -async function ensureVulkanRequiredCases() { - await ensureRoots(); - const outputPath = join(SSOT, 'vulkan-required-cases.txt'); - const selectionPath = join(SSOT, 'vulkan-required-cases-selection.json'); - const ctsRoot = join(BUILD, 'references', `VK-GL-CTS-${EXPECTED.vulkanCTSTag}`); - - if (!existsSync(join(ctsRoot, '.git'))) { - await rm(ctsRoot, { recursive: true, force: true }); - run('/usr/bin/git', [ - 'clone', '--filter=blob:none', '--no-checkout', '--depth', '1', - '--branch', EXPECTED.vulkanCTSTag, - 'https://github.com/KhronosGroup/VK-GL-CTS.git', ctsRoot - ], { maxBuffer: 128 * 1024 * 1024 }); - } else { - run('/usr/bin/git', ['-C', ctsRoot, 'fetch', '--depth', '1', 'origin', `refs/tags/${EXPECTED.vulkanCTSTag}:refs/tags/${EXPECTED.vulkanCTSTag}`], { allowFailure: true, maxBuffer: 128 * 1024 * 1024 }); - } - run('/usr/bin/git', ['-C', ctsRoot, 'sparse-checkout', 'init', '--cone'], { allowFailure: true }); - run('/usr/bin/git', ['-C', ctsRoot, 'sparse-checkout', 'set', 'external/vulkancts/mustpass/main']); - let checkout = run('/usr/bin/git', ['-C', ctsRoot, 'checkout', '--detach', EXPECTED.vulkanCTSCommit], { allowFailure: true, maxBuffer: 128 * 1024 * 1024 }); - if (checkout.status !== 0) { - run('/usr/bin/git', ['-C', ctsRoot, 'fetch', '--depth', '1', 'origin', EXPECTED.vulkanCTSCommit], { maxBuffer: 128 * 1024 * 1024 }); - run('/usr/bin/git', ['-C', ctsRoot, 'checkout', '--detach', EXPECTED.vulkanCTSCommit], { maxBuffer: 128 * 1024 * 1024 }); - } - const observedCommit = gitCommit(ctsRoot); - if (observedCommit !== EXPECTED.vulkanCTSCommit) { - throw new Error(`VULKAN_CTS_COMMIT_MISMATCH: expected ${EXPECTED.vulkanCTSCommit}, got ${observedCommit}`); - } - - const mustpassRoot = join(ctsRoot, 'external', 'vulkancts', 'mustpass', 'main'); - const indexPath = join(mustpassRoot, 'vk-default.txt'); - if (!existsSync(indexPath)) throw new Error(`VULKAN_CTS_MUSTPASS_MISSING: ${indexPath}`); - - const visited = new Set(); - const allCases = []; - async function collectList(path) { - const absolute = resolve(path); - if (visited.has(absolute)) return; - visited.add(absolute); - const text = await readFile(absolute, 'utf8'); - for (const raw of text.split(/\r?\n/)) { - const line = raw.trim(); - if (!line || line.startsWith('#')) continue; - if (line.startsWith('dEQP-VK.')) { - allCases.push(line); - continue; - } - const candidates = [join(dirname(absolute), line), join(mustpassRoot, line), join(mustpassRoot, 'vk-default', line)]; - const nested = candidates.find(candidate => existsSync(candidate)); - if (nested) await collectList(nested); - } - } - await collectList(indexPath); - const uniqueCases = [...new Set(allCases)].sort(); - if (!uniqueCases.length) throw new Error('VULKAN_CTS_MUSTPASS_EMPTY: no dEQP-VK cases found in pinned mustpass tree.'); - - const boundaries = [ - ['core-draw', [/^dEQP-VK\.draw\./i, /\.draw\./i]], - ['geometry', [/geometry/i]], - ['tessellation', [/tessellation/i]], - ['shader-cull-distance', [/cull_distance/i, /culldistance/i]], - ['compute', [/^dEQP-VK\.compute\./i, /\.compute\./i]], - ['images', [/copy_and_blit/i, /\.image/i, /images?/i]], - ['descriptors', [/binding_model/i, /descriptor/i]], - ['indirect-draw', [/indirect.*draw/i, /draw.*indirect/i, /indirect/i]], - ['timeline-semaphore', [/timeline.*semaphore/i, /semaphore.*timeline/i]], - ['synchronization2', [/synchronization2/i, /sync2/i]], - ['dynamic-rendering', [/dynamic_rendering/i]], - ['buffer-device-address', [/buffer_device_address/i]], - ['subgroups', [/subgroup/i]], - ['sampler-filter', [/sampler/i, /filter/i]], - ['wsi-presentation', [/^dEQP-VK\.wsi\./i, /present/i]] - ]; - const selection = {}; - for (const [id, patterns] of boundaries) { - const matches = uniqueCases.filter(test => patterns.some(pattern => pattern.test(test))).slice(0, 2); - if (!matches.length) throw new Error(`VULKAN_CTS_REQUIRED_BOUNDARY_UNRESOLVED: ${id}`); - selection[id] = matches; - } - const selectedCases = [...new Set(Object.values(selection).flat())].sort(); - await atomicWrite(outputPath, `${selectedCases.join('\n')}\n`); - const outputSHA256 = await sha256(outputPath); - await atomicWrite(selectionPath, `${JSON.stringify({ - schema: 1, - ctsTag: EXPECTED.vulkanCTSTag, - ctsCommit: EXPECTED.vulkanCTSCommit, - mustpassRoot: 'external/vulkancts/mustpass/main', - totalMustpassCasesObserved: uniqueCases.length, - selection, - selectedCaseCount: selectedCases.length, - output: 'ssot/vulkan-required-cases.txt', - outputSHA256 - }, null, 2)}\n`); - await updateStackLock([[['generality', 'vulkan_required_cases_sha256'], outputSHA256]]); - return { outputPath, outputSHA256, selectedCaseCount: selectedCases.length, totalMustpassCasesObserved: uniqueCases.length }; -} - -async function preflightReport() { - await ensureVulkanRequiredCases(); - await ensureRoots(); - const artifactNames = [ - 'AUTHORITY_INPUTS.sha256', - 'STACK.lock.yaml', - 'host-preflight.json', - 'xcode-discovery.json', - 'android-license-status.json', - 'android-sdk-packages.txt', - 'vulkaninfo-summary.txt', - 'upstreams-aemu.lock.xml', - 'phase0-source.json', - 'source-hashes.txt', - 'guestangle-authority.json', - 'vulkan-required-cases.spec.json', - 'vulkan-required-cases.txt', - 'phase0-policy.json' - ]; - const rows = []; - for (const name of artifactNames) { - const path = join(SSOT, name); - if (!existsSync(path)) { - rows.push({ name, state: 'MISSING', sha256: null }); - continue; - } - rows.push({ name, state: 'PRESENT', sha256: await sha256(path) }); - } - const host = existsSync(join(SSOT, 'host-preflight.json')) ? JSON.parse(await readFile(join(SSOT, 'host-preflight.json'), 'utf8')) : null; - const policy = existsSync(join(SSOT, 'phase0-policy.json')) ? JSON.parse(await readFile(join(SSOT, 'phase0-policy.json'), 'utf8')) : null; - const guestAngle = existsSync(join(SSOT, 'guestangle-authority.json')) ? JSON.parse(await readFile(join(SSOT, 'guestangle-authority.json'), 'utf8')) : null; - const stackText = await readFile(join(SSOT, 'STACK.lock.yaml'), 'utf8'); - const stackScalars = parseSimpleYamlScalars(stackText); - const unresolvedCriticalLockFields = CRITICAL_LOCK_PATHS - .map(path => path.join('.')) - .filter(path => !stackScalars.has(path) || stackScalars.get(path) === null || stackScalars.get(path) === ''); - const blockers = []; - if (!host?.pass) blockers.push('Host preflight is not green (frozen Xcode 26.6 / 17F113 required).'); - if (!policy?.pass) blockers.push('Canonical authority documents are missing or hash-mismatched.'); - if (!existsSync(join(SSOT, 'android-sdk-packages.txt'))) blockers.push('Android API 37 package freeze is incomplete.'); - if (!existsSync(join(SSOT, 'vulkaninfo-summary.txt'))) blockers.push('Vulkan SDK 1.4.357.0 validation is incomplete.'); - if (!existsSync(join(SSOT, 'upstreams-aemu.lock.xml'))) blockers.push('Resolved emu-master-dev manifest is not frozen.'); - if (!guestAngle?.pass) blockers.push('Locked-source GuestAngle authority proof is not green.'); - if (!existsSync(join(SSOT, 'vulkan-required-cases.txt'))) blockers.push('Exact Vulkan CTS required-case list is not frozen.'); - if (unresolvedCriticalLockFields.length) blockers.push(`STACK.lock.yaml has ${unresolvedCriticalLockFields.length} unresolved critical field(s): ${unresolvedCriticalLockFields.join(', ')}`); - - const markdown = [ - '# TFTMAC GPU Runtime v2.0 — Phase 0 Preflight Report', - '', - `Generated: ${new Date().toISOString()}`, - '', - `Status: **${blockers.length === 0 ? 'PASS' : 'BLOCKED'}**`, - '', - '## Blockers', - '', - ...(blockers.length ? blockers.map(item => `- ${item}`) : ['- None.']), - '', - '## Artifact inventory', - '', - '| Artifact | State | SHA-256 |', - '|---|---|---|', - ...rows.map(row => `| \`ssot/${row.name}\` | ${row.state} | ${row.sha256 ?? '—'} |`), - '', - 'Phase 0 may be declared complete only when this report is PASS and `STACK.lock.yaml` contains no unresolved critical-path null.' - ].join('\n') + '\n'; - await atomicWrite(join(SSOT, 'preflight-report.md'), markdown); - console.log(markdown); - if (blockers.length) die(`PHASE_0_BLOCKED: ${blockers.length} unresolved blocker(s).`, 15); -} - -async function cleanupSandboxBootstrap() { - const sandboxApp = join(SANDBOX_HOME, 'Library', 'Application Support', 'TFTMAC'); - if (sandboxApp === APP) { - console.log(JSON.stringify({ removed: false, reason: 'sandbox-home-is-console-home', path: sandboxApp }, null, 2)); - return; - } - if (!sandboxApp.includes('/CLARA-RUNTIME/process-home/')) { - die(`REFUSING_SANDBOX_CLEANUP_OUTSIDE_CLARA_RUNTIME: ${sandboxApp}`, 6); - } - const existed = existsSync(sandboxApp); - if (existed) await rm(sandboxApp, { recursive: true, force: true }); - console.log(JSON.stringify({ removed: existed, path: sandboxApp, canonicalAppRoot: APP }, null, 2)); -} - -async function discoverXcodes() { - const discovered = discoverInstalledXcodes(); - const match = findRequiredXcode(discovered); - await atomicWrite(join(SSOT, 'xcode-discovery.json'), `${JSON.stringify({ observedAt: new Date().toISOString(), required: { version: EXPECTED.xcodeVersion, build: EXPECTED.xcodeBuild }, selected: match, discovered }, null, 2)}\n`); - console.log(JSON.stringify({ phase: '0-xcode-discovery', required: { version: EXPECTED.xcodeVersion, build: EXPECTED.xcodeBuild }, selected: match, discovered }, null, 2)); - if (!match) die(`XCODE_${EXPECTED.xcodeVersion}_${EXPECTED.xcodeBuild}_NOT_FOUND`, 5); -} - -async function phase1BuildWorker() { - await ensureRoots(); - await mkdir(PHASE1_BUILD_LOG_ROOT, { recursive: true }); - const aemuRoot = join(BUILD, 'aemu'); - const qemuRoot = join(aemuRoot, 'external', 'qemu'); - const outRoot = join(qemuRoot, 'objs'); - const toolchainGeneratorPath = join(qemuRoot, 'android', 'scripts', 'unix', 'gen-android-sdk-toolchain.sh'); - const toolchainBackupPath = join(BUILD, 'compat', 'gen-android-sdk-toolchain.original.sh'); - await mkdir(dirname(toolchainBackupPath), { recursive: true }); - if (existsSync(toolchainBackupPath) && existsSync(toolchainGeneratorPath)) { - const backupText = await readFile(toolchainBackupPath, 'utf8'); - const currentText = await readFile(toolchainGeneratorPath, 'utf8'); - if (backupText !== currentText && currentText.includes('26.5')) { - await writeFile(toolchainGeneratorPath, backupText); - } - } - await rm(PHASE1_AEMU_ALIAS, { recursive: true, force: true }); - await symlink(aemuRoot, PHASE1_AEMU_ALIAS, 'dir'); - const buildAemuRoot = PHASE1_AEMU_ALIAS; - const buildQemuRoot = join(buildAemuRoot, 'external', 'qemu'); - const buildOutRoot = join(buildQemuRoot, 'objs'); - const startedAt = process.env.TFTMAC_PHASE1_BUILD_STARTED_AT ?? new Date().toISOString(); - const stackText = await readFile(join(SSOT, 'STACK.lock.yaml'), 'utf8'); - const stack = parseSimpleYamlScalars(stackText); - if (stack.get('phase0_status') !== 'PASS') throw new Error('PHASE1_REQUIRES_PHASE0_PASS'); - const lockedQemuCommit = stack.get('aemu.qemu_commit'); - if (!lockedQemuCommit) throw new Error('PHASE1_QEMU_LOCK_MISSING'); - if (!existsSync(qemuRoot)) throw new Error(`PHASE1_QEMU_SOURCE_MISSING: ${qemuRoot}`); - const observedQemuCommit = gitCommit(qemuRoot); - if (observedQemuCommit !== lockedQemuCommit) { - throw new Error(`PHASE1_QEMU_COMMIT_MISMATCH: expected ${lockedQemuCommit}, got ${observedQemuCommit}`); - } - const xcode = findRequiredXcode(discoverInstalledXcodes()); - if (!xcode) throw new Error(`PHASE1_XCODE_${EXPECTED.xcodeVersion}_${EXPECTED.xcodeBuild}_REQUIRED`); - const env = { - ...process.env, - DEVELOPER_DIR: xcode.developerDir, - ANDROID_SDK_ROOT: SDK, - ANDROID_HOME: SDK, - ANDROID_AVD_HOME: AVD_HOME, - ANDROID_ADB_SERVER_PORT: '5040' - }; - const ninjaCandidates = run('/usr/bin/find', [join(aemuRoot, 'prebuilts'), '-type', 'f', '-name', 'ninja', '-print'], { allowFailure: true, maxBuffer: 4 * 1024 * 1024 }).stdout - .split(/\r?\n/).map(value => value.trim()).filter(Boolean); - const ninjaPhysical = ninjaCandidates[0] ?? null; - const ninjaBinary = ninjaPhysical ? ninjaPhysical.replace(aemuRoot, buildAemuRoot) : null; - if (ninjaBinary) env.PATH = `${dirname(ninjaBinary)}:${env.PATH ?? ''}`; - const baseState = { - schema: 1, - pid: process.pid, - startedAt, - qemuCommit: observedQemuCommit, - xcode: { version: EXPECTED.xcodeVersion, build: EXPECTED.xcodeBuild, developerDir: xcode.developerDir }, - sourceRoot: qemuRoot, - buildAliasRoot: buildAemuRoot, - outRoot, - stdoutPath: PHASE1_BUILD_STDOUT, - stderrPath: PHASE1_BUILD_STDERR - }; - const writeState = async state => atomicWrite(PHASE1_BUILD_STATE, `${JSON.stringify({ ...baseState, ...state }, null, 2)}\n`); - const qtRoot = join(aemuRoot, 'prebuilts', 'android-emulator-build', 'qt'); - const qtRegularLibexec = join(qtRoot, 'darwin-aarch64', 'libexec'); - const qtNoWebLibexec = join(qtRoot, 'darwin-aarch64-nowebengine', 'libexec'); - let qtLibexecBridgeCreated = false; - if (!existsSync(qtRegularLibexec)) { - if (!existsSync(qtNoWebLibexec)) throw new Error(`PHASE1_QT_HOST_TOOLS_MISSING: ${qtNoWebLibexec}`); - await symlink(qtNoWebLibexec, qtRegularLibexec, 'dir'); - qtLibexecBridgeCreated = true; - } - const removeQtLibexecBridge = async () => { - if (qtLibexecBridgeCreated && existsSync(qtRegularLibexec)) { - await rm(qtRegularLibexec, { force: true }); - } - }; - const originalToolchainGenerator = await readFile(toolchainGeneratorPath, 'utf8'); - const originalToolchainGeneratorSHA256 = await sha256(toolchainGeneratorPath); - await writeFile(toolchainBackupPath, originalToolchainGenerator); - const sourceLines = originalToolchainGenerator.split(/\r?\n/); - const patchedLines = []; - let supportedSdkEdits = 0; - let sdkProbeEdits = 0; - let xcodePathEdits = 0; - let xcodeClangEdits = 0; - for (let index = 0; index < sourceLines.length; index += 1) { - let line = sourceLines[index]; - if (line.includes('OSX_SDK_SUPPORTED=') && line.includes('15.2') && !line.includes('26.5')) { - line = line.replace('15.2"', '15.2 26.5"'); - supportedSdkEdits += 1; - } - if (line.includes('OSX_SDK_INSTALLED_LIST=$(xcodebuild -showsdks')) { - const indent = line.match(/^\s*/)?.[0] ?? ''; - patchedLines.push(`${indent}OSX_SDK_INSTALLED_LIST=$(xcrun --sdk macosx --show-sdk-version 2>/dev/null)`); - index += 2; - sdkProbeEdits += 1; - continue; - } - if (line.includes('XCODE_PATH=$(xcode-select -print-path 2>/dev/null)')) { - line = line.replace('XCODE_PATH=$(xcode-select -print-path 2>/dev/null)', 'XCODE_PATH="${DEVELOPER_DIR:-$(xcode-select -print-path 2>/dev/null)}"'); - xcodePathEdits += 1; - } - if (line.trim() === 'CLANG_BINDIR=$PREBUILT_TOOLCHAIN_DIR/bin') { - line = `${line.match(/^\s*/)?.[0] ?? ''}CLANG_BINDIR="$XCODE_PATH/Toolchains/XcodeDefault.xctoolchain/usr/bin"`; - xcodeClangEdits += 1; - } - patchedLines.push(line); - } - if (supportedSdkEdits < 1 || sdkProbeEdits < 1 || xcodePathEdits < 1 || xcodeClangEdits < 1) { - throw new Error(`PHASE1_SDK_COMPAT_PATCH_SHAPE_CHANGED: supported=${supportedSdkEdits} probe=${sdkProbeEdits} xcode=${xcodePathEdits} clang=${xcodeClangEdits}`); - } - const patchedToolchainGenerator = patchedLines.join('\n'); - await writeFile(toolchainGeneratorPath, patchedToolchainGenerator); - const patchedToolchainGeneratorSHA256 = await sha256(toolchainGeneratorPath); - const restoreToolchainGenerator = async () => { - await writeFile(toolchainGeneratorPath, originalToolchainGenerator); - const restoredSHA256 = await sha256(toolchainGeneratorPath); - if (restoredSHA256 !== originalToolchainGeneratorSHA256) { - throw new Error(`PHASE1_TOOLCHAIN_RESTORE_FAILED: expected ${originalToolchainGeneratorSHA256}, got ${restoredSHA256}`); - } - return restoredSHA256; - }; - try { - await rm(outRoot, { recursive: true, force: true }); - const toolchainRoot = join(outRoot, 'toolchain'); - await mkdir(toolchainRoot, { recursive: true }); - const xcodeToolBin = join(xcode.developerDir, 'Toolchains', 'XcodeDefault.xctoolchain', 'usr', 'bin'); - const xcodeClang = join(xcodeToolBin, 'clang'); - const xcodeClangXX = join(xcodeToolBin, 'clang++'); - if (!existsSync(xcodeClang) || !existsSync(xcodeClangXX)) { - throw new Error(`PHASE1_XCODE_CLANG_MISSING: ${xcodeToolBin}`); - } - const macosSDKPath = run('/usr/bin/xcrun', ['--sdk', 'macosx', '--show-sdk-path'], { env }).stdout.trim(); - const makeCompilerWrapper = async (name, compiler) => { - const path = join(toolchainRoot, name); - await writeFile(path, [ - '#!/bin/bash', - `export SDKROOT=${JSON.stringify(macosSDKPath)}`, - `exec ${JSON.stringify(compiler)} -mmacosx-version-min=10.14 \"$@\"`, - '' - ].join('\n')); - await chmod(path, 0o755); - return path; - }; - const compilerWrappers = []; - for (const name of ['cc', 'gcc', 'clang']) compilerWrappers.push(await makeCompilerWrapper(name, xcodeClang)); - for (const name of ['c++', 'g++', 'clang++']) compilerWrappers.push(await makeCompilerWrapper(name, xcodeClangXX)); - const hostToolWrappers = []; - const makeHostToolWrapper = async (name, candidates) => { - const executable = firstExisting(candidates); - if (!executable) return null; - const path = join(toolchainRoot, name); - await writeFile(path, [ - '#!/bin/bash', - `exec ${JSON.stringify(executable)} \"$@\"`, - '' - ].join('\n')); - await chmod(path, 0o755); - hostToolWrappers.push({ name, path, executable }); - return path; - }; - await makeHostToolWrapper('ranlib', [join(xcodeToolBin, 'ranlib'), '/usr/bin/ranlib']); - await makeHostToolWrapper('ar', [join(xcodeToolBin, 'ar'), '/usr/bin/ar']); - await makeHostToolWrapper('nm', [join(xcodeToolBin, 'nm'), '/usr/bin/nm']); - await makeHostToolWrapper('strip', [join(xcodeToolBin, 'strip'), '/usr/bin/strip']); - await makeHostToolWrapper('ld', [join(xcodeToolBin, 'ld'), '/usr/bin/ld']); - await makeHostToolWrapper('libtool', [join(xcodeToolBin, 'libtool'), '/usr/bin/libtool']); - await makeHostToolWrapper('strings', [join(xcodeToolBin, 'strings'), '/usr/bin/strings']); - await makeHostToolWrapper('otool', [join(xcodeToolBin, 'otool'), '/usr/bin/otool']); - await makeHostToolWrapper('install_name_tool', [join(xcodeToolBin, 'install_name_tool'), '/usr/bin/install_name_tool']); - await makeHostToolWrapper('dsymutil', [join(xcodeToolBin, 'dsymutil'), '/usr/bin/dsymutil']); - await writeState({ - status: 'RUNNING', stage: 'BUILD', - compatibility: { - kind: 'temporary-host-sdk-parity', - sdk: '26.5', - sourceSHA256: originalToolchainGeneratorSHA256, - patchedSHA256: patchedToolchainGeneratorSHA256, - supportedSdkEdits, - sdkProbeEdits, - xcodePathEdits, - xcodeClangEdits, - ninjaBinary, - ninjaCandidates: ninjaCandidates.slice(0, 20), - compilerWrappers, - hostToolWrappers, - macosSDKPath, - qtLibexecBridgeCreated, - qtLibexecBridgeTarget: qtNoWebLibexec - } - }); - const buildPython = firstExisting([ - join(buildAemuRoot, 'prebuilts', 'python', 'darwin-x86', 'bin', 'python3'), - join(buildAemuRoot, 'prebuilts', 'python', 'darwin-arm64', 'bin', 'python3') - ]); - if (!buildPython) throw new Error(`PHASE1_AOSP_PREBUILT_PYTHON_MISSING: ${join(aemuRoot, 'prebuilts', 'python')}`); - const cmakeCompatLauncher = '/private/tmp/tftmac-aemu-cmake-compat.py'; - await writeFile(cmakeCompatLauncher, [ - 'import argparse, sys', - 'from pathlib import Path', - `sys.path.insert(0, ${JSON.stringify(join(buildQemuRoot, 'android', 'build', 'python'))})`, - '_orig = argparse.ArgumentParser.parse_known_args', - 'def _parse(self, *args, **kwargs):', - ' ns, rest = _orig(self, *args, **kwargs)', - ' for name in ("aosp", "out", "dist"):', - ' if hasattr(ns, name):', - ' value = getattr(ns, name)', - ' if isinstance(value, str): setattr(ns, name, Path(value))', - ' return ns, rest', - 'argparse.ArgumentParser.parse_known_args = _parse', - 'from aemu.tasks.configure import ConfigureTask', - 'def _without_webengine(self): return self', - 'ConfigureTask.with_webengine = _without_webengine', - 'from aemu import cmake', - 'cmake.launch()', - '' - ].join('\n')); - runInherited(buildPython, [ - cmakeCompatLauncher, - '--aosp', buildAemuRoot, - '--out', buildOutRoot, - '--ccache', 'auto', - '--task-disable', 'clean', - '--verbose' - ], { cwd: buildQemuRoot, env }); - - await writeState({ status: 'RUNNING', stage: 'CTEST' }); - const ctestBinary = join(buildAemuRoot, 'prebuilts', 'cmake', 'darwin-x86', 'bin', 'ctest'); - if (!existsSync(ctestBinary)) throw new Error(`PHASE1_CTEST_MISSING: ${ctestBinary}`); - runInherited(ctestBinary, ['-j8', '--output-on-failure'], { cwd: buildOutRoot, env }); - - await writeState({ status: 'RUNNING', stage: 'GFXSTREAM_BUILD_CHECK' }); - runInherited(buildPython, [ - cmakeCompatLauncher, - '--aosp', buildAemuRoot, - '--out', buildOutRoot, - '--gfxstream', - '--task-disable', 'clean' - ], { cwd: buildQemuRoot, env }); - - await removeQtLibexecBridge(); - const restoredToolchainSHA256 = await restoreToolchainGenerator(); - const emulatorCandidates = run('/usr/bin/find', [outRoot, '-type', 'f', '-name', 'emulator', '-perm', '+111', '-print'], { allowFailure: true, maxBuffer: 8 * 1024 * 1024 }).stdout - .split(/\r?\n/).map(value => value.trim()).filter(Boolean); - const emulatorBinary = emulatorCandidates[0] ?? null; - const artifact = { - schema: 1, - phase: 1, - pass: true, - completedAt: new Date().toISOString(), - qemuCommit: observedQemuCommit, - xcode: baseState.xcode, - sourceRoot: qemuRoot, - outRoot, - emulatorBinary, - checks: { build: 'PASS', ctest: 'PASS', gfxstreamBuildCheck: 'PASS', toolchainSourceRestored: restoredToolchainSHA256 === originalToolchainGeneratorSHA256 }, - hostCompatibility: { - macosSDK: '26.5', - originalToolchainGeneratorSHA256, - patchedToolchainGeneratorSHA256, - restoredToolchainSHA256 - } - }; - await atomicWrite(join(DIAGNOSTICS, 'phase1-build.json'), `${JSON.stringify(artifact, null, 2)}\n`); - await writeState({ status: 'SUCCEEDED', stage: 'COMPLETE', endedAt: new Date().toISOString(), emulatorBinary }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - let restoreError = null; - try { - await removeQtLibexecBridge(); - await restoreToolchainGenerator(); - } catch (failure) { - restoreError = failure instanceof Error ? failure.message : String(failure); - } - await writeState({ status: 'FAILED', endedAt: new Date().toISOString(), error: message, restoreError }); - if (restoreError) throw new Error(`${message}; ${restoreError}`); - throw error; - } -} - -async function phase1Build() { - await ensureRoots(); - await mkdir(PHASE1_BUILD_LOG_ROOT, { recursive: true }); - const existing = await readPhase1BuildState(); - if (existing?.pid && pidIsAlive(existing.pid)) { - console.log(JSON.stringify({ phase: '1-build', launched: false, alreadyRunning: true, worker: existing }, null, 2)); - return; - } - const startedAt = new Date().toISOString(); - const stdoutFd = openSync(PHASE1_BUILD_STDOUT, 'w'); - const stderrFd = openSync(PHASE1_BUILD_STDERR, 'w'); - try { - const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'phase1-build-worker'], { - cwd: REPO, - env: { ...process.env, TFTMAC_PHASE1_BUILD_STARTED_AT: startedAt }, - detached: true, - stdio: ['ignore', stdoutFd, stderrFd] - }); - child.unref(); - console.log(JSON.stringify({ - phase: '1-build', launched: true, pid: child.pid, startedAt, - sourceRoot: join(BUILD, 'aemu', 'external', 'qemu'), outRoot: join(BUILD, 'aemu', 'external', 'qemu', 'objs'), - statePath: PHASE1_BUILD_STATE, stdoutPath: PHASE1_BUILD_STDOUT, stderrPath: PHASE1_BUILD_STDERR - }, null, 2)); - } finally { - closeSync(stdoutFd); - closeSync(stderrFd); - } -} - -async function validateEngineeringMap() { - const mapPath = join(SSOT, 'TFTMAC_ENGINEERING_MAP.sql'); - if (!existsSync(mapPath)) return { pass: false, error: 'ENGINEERING_MAP_MISSING', path: mapPath }; - const sql = await readFile(mapPath, 'utf8'); - const db = new DatabaseSync(':memory:'); - try { - db.exec(sql); - const foreignKeyProblems = db.prepare('PRAGMA foreign_key_check').all(); - const schemaVersion = db.prepare("SELECT value FROM map_meta WHERE key='schema_version'").get()?.value ?? null; - const tableCount = Number(db.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").get().count); - const viewCount = Number(db.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type='view'").get().count); - const fieldMetadataCount = Number(db.prepare('SELECT COUNT(*) AS count FROM field_metadata').get().count); - const versionCount = Number(db.prepare('SELECT COUNT(*) AS count FROM version_catalog').get().count); - const compatibilityClaimCount = Number(db.prepare('SELECT COUNT(*) AS count FROM compatibility_claims').get().count); - const environmentCount = Number(db.prepare('SELECT COUNT(*) AS count FROM environment_snapshots').get().count); - const documentCount = Number(db.prepare('SELECT COUNT(*) AS count FROM source_documents').get().count); - const deploymentCandidateCount = Number(db.prepare('SELECT COUNT(*) AS count FROM deployment_target_candidates').get().count); - const openBlockingUnknowns = Number(db.prepare("SELECT COUNT(*) AS count FROM unknowns WHERE blocking=1 AND status IN ('OPEN','TESTING')").get().count); - return { - pass: foreignKeyProblems.length === 0 && schemaVersion === '3', - path: mapPath, - sha256: await sha256(mapPath), - bytes: (await stat(mapPath)).size, - schemaVersion, - tableCount, - viewCount, - fieldMetadataCount, - versionCount, - compatibilityClaimCount, - environmentCount, - documentCount, - deploymentCandidateCount, - openBlockingUnknowns, - foreignKeyProblems - }; - } catch (error) { - return { pass: false, path: mapPath, error: error instanceof Error ? error.message : String(error) }; - } finally { - db.close(); - } -} - -function directControl(action, args = []) { - return runInherited(process.execPath, [join(REPO, 'tools', 'tftmac-direct-control.mjs'), action, ...args]); -} - -async function consumeDirectControlRequest() { - if (!existsSync(DIRECT_CONTROL_REQUEST)) return null; - const request = JSON.parse(await readFile(DIRECT_CONTROL_REQUEST, 'utf8')); - const allowed = new Set(['inventory', 'prepare', 'engineering-map-selftest', 'lab-selftest', 'build', 'launch-app', 'open-play-web', 'runtime-process-audit', 'cleanup-tftmac-adb-residue', 'single-runtime-preflight', 'launch-mactician-control', 'stop-mactician-control', 'mactician-runtime-audit', 'cleanup-observer-adb-5037', 'start', 'start-donor-control', 'start-donor-control-5gb', 'start-donor-control-5gb-flush400', 'play-action', 'play-probe', 'gles-capability-probe', 'launch-failure-probe', 'recover-anr-wait', 'logger-health', 'restart-game', 'preplay-optimize', 'restart-effect-analysis', 'analyze-latest-closed-run-trends', 'compare-latest-runs', 'screen-state-probe', 'reveal-lock-screen', 'wake-guest-screen', 'audio-health', 'audio-backend-probe', 'disconnect-window-audit', 'runtime-fault-audit', 'graphics-pipeline-audit', 'analyze-session', 'ingest-analysis', 'trace-capabilities', 'native-trace-smoke', 'native-trace-combat', 'presentation-probe', 'window-inventory', 'fit-window', 'play-certification', 'play-diagnose', 'google-account-ui', 'image-check', 'device-profiles', 'image-upgrade-start', 'image-upgrade-status', 'launch-game', 'status', 'marker', 'game-settings','quality-report','match-boundary-probe','analyze-approx-match','ingest-approx-match', 'match-entry', 'combat-start', 'first-place', 'stop', 'package-state', 'package-launch-probe', 'auth-brief', 'install-diagnose', 'play-install-brief', 'play-store-repair']); - const placementAction = /^placement-[1-8]$/.test(request?.action ?? ''); - if (!allowed.has(request?.action) && !placementAction) throw new Error(`DIRECT_CONTROL_ACTION_INVALID: ${request?.action ?? ''}`); - await rm(DIRECT_CONTROL_REQUEST, { force: true }); - const startedAt = new Date().toISOString(); - const requestArgs = Array.isArray(request?.args) ? request.args.map(value => String(value)).slice(0, 8) : []; - const result = run(process.execPath, [join(REPO, 'tools', 'tftmac-direct-control.mjs'), request.action, ...requestArgs], { - allowFailure: true, - timeout: 900000, - maxBuffer: 128 * 1024 * 1024 - }); - let parsed = null; - try { parsed = JSON.parse(result.stdout); } catch {} - return { - action: request.action, - startedAt, - endedAt: new Date().toISOString(), - exitCode: result.status, - result: parsed, - stdout: parsed ? null : result.stdout.trim() || null, - stderr: result.stderr.trim() || null - }; -} - -async function status() { - await ensureRoots(); - const directControlResult = await consumeDirectControlRequest(); - if (directControlResult) { - console.log(JSON.stringify({ - appRoot: APP, - buildRoot: BUILD, - runtimeRoot: RUNTIME, - repository: REPO, - directControlResult - }, null, 2)); - return; - } - const paths = [ - 'STACK.lock.yaml', 'host-preflight.json', 'android-sdk-packages.txt', 'tool-versions.txt', - 'source-hashes.txt', 'guestangle-authority.json', 'vulkan-required-cases.txt', 'preflight-report.md', - 'upstreams-aemu.lock.xml', 'vulkaninfo-summary.txt', 'TFTMAC_ENGINEERING_MAP.sql' - ]; - const artifacts = {}; - for (const name of paths) { - const path = join(SSOT, name); - artifacts[name] = existsSync(path) ? { exists: true, bytes: (await stat(path)).size, sha256: await sha256(path) } : { exists: false }; - } - const sourceWorker = await readSourceWorkerState(); - const sourceWorkerAlive = Boolean(sourceWorker?.pid && pidIsAlive(sourceWorker.pid)); - const sourceWorkerObservedStatus = sourceWorker?.status === 'RUNNING' && !sourceWorkerAlive - ? 'INTERRUPTED' - : sourceWorker?.status ?? 'NOT_STARTED'; - const phase1BuildWorker = await readPhase1BuildState(); - const phase1BuildAlive = Boolean(phase1BuildWorker?.pid && pidIsAlive(phase1BuildWorker.pid)); - const phase1BuildObservedStatus = phase1BuildWorker?.status === 'RUNNING' && !phase1BuildAlive - ? 'INTERRUPTED' - : phase1BuildWorker?.status ?? 'NOT_STARTED'; - let phase1BuildLogTail = null; - if (phase1BuildWorker && phase1BuildObservedStatus !== 'RUNNING') { - const tail = async path => { - if (!existsSync(path)) return null; - const text = await readFile(path, 'utf8'); - return text.slice(-8192); - }; - const rebuildPath = join(BUILD, 'aemu', 'external', 'qemu', 'android', 'rebuild.sh'); - let rebuildSnippet = null; - if (existsSync(rebuildPath)) { - const lines = (await readFile(rebuildPath, 'utf8')).split(/\r?\n/); - rebuildSnippet = lines.slice(38, 62).map((text, index) => ({ line: index + 39, text })); - } - const filteredSourceLines = async (path, pattern, maximum = 120) => { - if (!existsSync(path)) return null; - return (await readFile(path, 'utf8')).split(/\r?\n/) - .map((text, index) => ({ line: index + 1, text })) - .filter(item => pattern.test(item.text)) - .slice(0, maximum); - }; - const phase1AliasPython = firstExisting([ - join(PHASE1_AEMU_ALIAS, 'prebuilts', 'python', 'darwin-x86', 'bin', 'python3'), - join(PHASE1_AEMU_ALIAS, 'prebuilts', 'python', 'darwin-arm64', 'bin', 'python3') - ]); - const cmakeHelp = phase1AliasPython ? run(phase1AliasPython, [ - join(PHASE1_AEMU_ALIAS, 'external', 'qemu', 'android', 'build', 'python', 'cmake.py'), '--help' - ], { allowFailure: true, maxBuffer: 4 * 1024 * 1024 }) : null; - phase1BuildLogTail = { - stdout: await tail(PHASE1_BUILD_STDOUT), - stderr: await tail(PHASE1_BUILD_STDERR), - rebuildSnippet, - toolchainGenerator: await filteredSourceLines( - join(BUILD, 'aemu', 'external', 'qemu', 'android', 'scripts', 'unix', 'gen-android-sdk-toolchain.sh'), - /AOSP|aosp|gcc|g\+\+|clang|xcrun|ln |toolchain|realpath|dirname|SDK|sdk|xcodebuild|showsdks|SUPPORTED/ - ), - cmakeDriver: await filteredSourceLines( - join(BUILD, 'aemu', 'external', 'qemu', 'android', 'build', 'python', 'cmake.py'), - /aosp|AOSP|out-dir|argparse|ccache|toolchain/ - ), - cmakeHelp: cmakeHelp ? { status: cmakeHelp.status, stdout: cmakeHelp.stdout, stderr: cmakeHelp.stderr } : null, - cmakeAospParserUse: await filteredSourceLines( - join(BUILD, 'aemu', 'external', 'qemu', 'android', 'build', 'python', 'cmake.py'), - /aosp|Path\(|argument_parser|ArgumentParser|add_argument/, - 160 - ), - cmakeLauncherHead: (() => null)(), - cmakeModuleAospUse: await filteredSourceLines( - join(BUILD, 'aemu', 'external', 'qemu', 'android', 'build', 'python', 'aemu', 'cmake.py'), - /aosp|Path\(|add_argument|ArgumentParser|parser|args\./, - 220 - ), - sdkGateSnippet: null, - phase1FilesystemProbe: { - toolchain: run('/bin/ls', ['-la', join(BUILD, 'aemu', 'external', 'qemu', 'objs', 'toolchain')], { allowFailure: true, maxBuffer: 4 * 1024 * 1024 }), - clangHosts: run('/usr/bin/find', [join(BUILD, 'aemu', 'prebuilts', 'clang', 'host'), '-maxdepth', '4', '-type', 'f', '-name', 'clang', '-print'], { allowFailure: true, maxBuffer: 4 * 1024 * 1024 }), - ninja: run('/usr/bin/find', [join(BUILD, 'aemu', 'prebuilts'), '-type', 'f', '-name', 'ninja', '-print'], { allowFailure: true, maxBuffer: 4 * 1024 * 1024 }), - qtTools: run('/usr/bin/find', [join(BUILD, 'aemu', 'prebuilts', 'android-emulator-build', 'qt'), '-maxdepth', '5', '-type', 'f', '(', '-name', 'uic', '-o', '-name', 'moc', '-o', '-name', 'qmake', '-o', '-name', 'qtpaths', ')', '-print'], { allowFailure: true, maxBuffer: 8 * 1024 * 1024 }), - qtDirs: run('/usr/bin/find', [join(BUILD, 'aemu', 'prebuilts', 'android-emulator-build', 'qt'), '-maxdepth', '2', '-type', 'd', '-print'], { allowFailure: true, maxBuffer: 8 * 1024 * 1024 }), - qtWebengineLogic: run('/usr/bin/grep', ['-RniE', 'noqtwebengine|QTWEBENGINE|qtwebengine', join(BUILD, 'aemu', 'external', 'qemu', 'android', 'build', 'python', 'aemu')], { allowFailure: true, maxBuffer: 8 * 1024 * 1024 }), - qtConfigureSnippet: run('/usr/bin/sed', ['-n', '160,185p', join(BUILD, 'aemu', 'external', 'qemu', 'android', 'build', 'python', 'aemu', 'tasks', 'configure.py')], { allowFailure: true, maxBuffer: 2 * 1024 * 1024 }) - } - }; - { - const generatorPath = join(BUILD, 'aemu', 'external', 'qemu', 'android', 'scripts', 'unix', 'gen-android-sdk-toolchain.sh'); - if (existsSync(generatorPath)) { - const lines = (await readFile(generatorPath, 'utf8')).split(/\r?\n/); - phase1BuildLogTail.sdkGateSnippet = lines.slice(512, 566).map((text, index) => ({ line: index + 513, text })); - phase1BuildLogTail.wrapperProgramSnippet = lines.slice(150, 401).map((text, index) => ({ line: index + 151, text })); - phase1BuildLogTail.wrapperGeneratorSnippet = lines.slice(400, 475).map((text, index) => ({ line: index + 401, text })); - } - } - { - const cmakeLauncherPath = join(BUILD, 'aemu', 'external', 'qemu', 'android', 'build', 'python', 'cmake.py'); - if (existsSync(cmakeLauncherPath)) { - const text = await readFile(cmakeLauncherPath, 'utf8'); - phase1BuildLogTail.cmakeLauncherHead = text.split(/\r?\n/).slice(0, 260).map((line, index) => ({ line: index + 1, text: line })); - } - } - } - const engineeringMap = await validateEngineeringMap(); - console.log(JSON.stringify({ - appRoot: APP, - buildRoot: BUILD, - buildRootSource: process.env.TFTMAC_BUILD_ROOT ? 'TFTMAC_BUILD_ROOT' : 'default-external', - runtimeRoot: RUNTIME, - runtimeRootSource: process.env.TFTMAC_RUNTIME_ROOT ? 'TFTMAC_RUNTIME_ROOT' : 'default-external', - repository: REPO, - directControlResult, - sourceWorker: sourceWorker ? { ...sourceWorker, alive: sourceWorkerAlive, observedStatus: sourceWorkerObservedStatus } : null, - phase1BuildWorker: phase1BuildWorker ? { ...phase1BuildWorker, alive: phase1BuildAlive, observedStatus: phase1BuildObservedStatus } : null, - phase1BuildLogTail, - engineeringMap, - artifacts - }, null, 2)); -} - -const action = process.argv[2] ?? 'status'; -try { - switch (action) { - case 'phase0-host': await phase0Host(); break; - case 'phase0-android': await phase0Android(); break; - case 'phase0-vulkan': await phase0Vulkan(); break; - case 'phase0-source': await phase0Source(); break; - case 'phase0-source-worker': await phase0SourceWorker(); break; - case 'phase0-authority': await importAuthorityFiles(); break; - case 'phase0-policy': await phase0Policy(); break; - case 'phase0-report': await preflightReport(); break; - case 'phase0-xcode-discovery': await discoverXcodes(); break; - case 'phase1-build': await phase1Build(); break; - case 'phase1-build-worker': await phase1BuildWorker(); break; - case 'cleanup-sandbox-bootstrap': await cleanupSandboxBootstrap(); break; - case 'direct-inventory': directControl('inventory'); break; - case 'direct-build': directControl('build'); break; - case 'direct-launch-app': directControl('launch-app'); break; - case 'direct-start': directControl('start'); break; - case 'direct-play-action': directControl('play-action'); break; - case 'direct-launch-game': directControl('launch-game'); break; - case 'direct-status': directControl('status'); break; - case 'direct-stop': directControl('stop'); break; - case 'direct-package-state': directControl('package-state'); break; - case 'status': await status(); break; - default: die(`unknown action: ${action}`); - } -} catch (error) { - die(error instanceof Error ? error.message : String(error)); -}