모바일 런처
ffa40a1 기준입니다.
DroidDeck은 지원되는 Android 기기 안에 Linux 사용자 공간을 준비하고, Steam 클라이언트와 게임 실행에 필요한 부품들을 연결하는 게임 런처입니다. README의 표현인 “SteamOS 경험을 Android로”는 제품 방향을 뜻합니다. 소스에서 확인되는 구현은 Android 앱이 Linux 세션과 화면·입력·오디오를 조율하여 PC 게임 실행 경로를 만드는 구조입니다.
대표 흐름은 APK 설치, Linux 런타임 준비, Play 선택, Steam 최초 다운로드와 로그인 순서입니다. Windows 게임의 운영체제 호출은 Wine 기반 Proton이 담당하고, x86 계열 명령어를 Arm64에서 실행하는 일에는 FEX가 관여합니다. Android 화면과 Linux 창 사이에는 별도의 Wayland 합성기가 있습니다. 게임마다 필요한 경로가 다르므로 모든 프로그램이 같은 변환 단계를 거치는 것은 아닙니다.
게임은 배우, Android는 공연장입니다. 배우의 언어를 바꾸는 통역사(FEX), 무대 장치의 규칙을 맞추는 기술팀(Proton), 실제 화면과 소리를 관객에게 전달하는 무대팀이 필요합니다. DroidDeck은 이들을 순서에 맞게 준비하는 기획사에 가깝습니다.
통역사가 있다고 공연이 반드시 성공하지는 않습니다. 장치·대본·공연장의 제약까지 맞아야 하듯, CPU·GPU·드라이버·게임의 요구 조건이 함께 맞아야 합니다.
app/src/main/AndroidManifest.xml의 .MainActivity는 실제 클래스가 아니라
activity-alias입니다. 연결 대상은 .FrontEndActivity이며,
실제 Kotlin 파일은 app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt입니다.
파일 이름만 추측해서 찾기보다 선언에서 진입점을 추적하는 습관을 배울 수 있습니다.
소스: 앱 진입점 등록.
// Manifest 등록 관계를 읽기 쉽게 줄인 도식
.MainActivity → .FrontEndActivity
│
├─ Compose 화면과 설정
├─ Linux 런타임 설치 상태 확인
└─ SessionService로 실행 요청
처음부터 게임을 실행할 필요는 없습니다. “버튼을 누르면 어느 함수가 어떤 상태와 파일을 바꾸는가”를 설명하는 것이 첫 목표입니다. 이 글의 실습은 원본 저장소를 수정하거나 실행하지 않고, 읽기와 별도 학습 노트만으로 진행할 수 있게 구성했습니다.
선정 근거는 TrendShift Monthly #19입니다. 2026-10-23 GitHub 기준 저장소는 2026-09-24 생성, 분석일은 2026-10-23이며 스타는 12,910개입니다. 약 한 달 사이에 관심을 모은 신생 저장소라는 점은 확인됩니다. 이 순위와 스타 수만으로 일별 증가 추이나 게임 호환성을 판단할 수는 없습니다.
관심의 배경은 기능을 바탕으로 해석할 수 있습니다. Android 휴대용 기기에서 익숙한 Steam UI를 사용하고, 게임 라이브러리·컨트롤러·데스크톱 앱을 연결하려는 목표를 보여 줍니다. 주목할 학습 가치는 부품 하나를 새로 만드는 일보다 서로 다른 시스템의 경계를 맞추는 과정에 있습니다. 이것은 코드와 README에서 읽어 낸 해석이며, 이용자를 대상으로 검증한 인기 원인 분석은 아닙니다.
| 도구 | 주요 접근 | DroidDeck에서 눈여겨볼 차이 |
|---|---|---|
| DroidDeck | Android 기기 안에서 Linux 세션과 Steam·게임 실행 환경을 조립 | 라이브러리와 기기별 화면·입력·드라이버 설정을 한 앱에서 연결 |
| Winlator | 공식 저장소 설명 기준, Wine과 Box86/Box64로 Android에서 Windows 앱 실행 | DroidDeck은 Steam 클라이언트와 ARM64 Proton·FEX를 중심에 둠 |
| Steam Link | Steam을 실행하는 컴퓨터에서 다른 기기로 게임을 스트리밍 | DroidDeck의 대표 경로는 휴대 기기 안에서 실행 환경을 준비하는 방식 |
| Proton / FEX | 각각 Windows 호환성과 CPU 명령어 실행을 담당하는 기반 기술 | DroidDeck이 연결해 쓰는 부품이며 완성된 Android 런처와 역할이 다름 |
비교 근거는 Winlator 공식 저장소, Valve Remote Play 안내, Proton, FEX 공식 저장소입니다. 실행 방식의 비교이며 FPS·호환 게임 수·배터리 수명 순위를 측정한 표는 아닙니다.
README는 Steam Big Picture, Desktop & apps, Flathub 기반 Store,
GOG·Epic Games·Amazon Games 연동을 설명합니다.
소스에서도 app/src/main/java/com/droiddeck/launcher/frontend/Library.kt의 SteamGame에
source·storeId·protonPrefix가 함께 들어 있습니다.
게임을 단순한 실행 파일 경로가 아니라 출처·실행 환경·그림을 가진 항목으로 모델링한 것입니다.
여러 교통수단을 한 앱에서 예약하는 서비스와 비슷합니다. 화면의 “출발”은 하나여도 뒤에서는 승차권, 환승, 짐, 도착 시간을 따로 맞춰야 합니다. 런처의 가치는 이 조율을 사용자가 매번 반복하지 않도록 하는 데 있습니다.
2026-10-23 GitHub 기준 최신 릴리스는 0.6.0, 2026-10-22입니다. 2026-10-23 커밋에는 Wine의 ARM64 문맥 전환 최적화가 들어왔습니다. 따라서 “0.6.0에서 검증된 동작”과 “분석 커밋의 구현”을 같은 말로 쓰면 안 됩니다. 릴리스 기준점과 분석 커밋을 구분해서 보세요.
웹 프로젝트의 프론트엔드·백엔드 구분을 그대로 대입하면 헷갈립니다. 이 저장소의 프론트엔드는 Android 화면이고, 백엔드에 해당하는 일은 서비스·네이티브 라이브러리·Linux 도구가 나눠 맡습니다. 이 앱의 내부 처리는 원격 API 서버 한 대가 아니라 기기 안의 여러 프로세스와 라이브러리로 구성됩니다. 외부 스토어 인증·게임 다운로드에는 네트워크가 필요합니다.
| 기술 | 선언된 버전 | 이 저장소에서 맡는 역할 |
|---|---|---|
| Kotlin | 2.1.0 | 화면 상태, 세션 관리, 스토어·입력 기능 |
| Compose 컴파일러 플러그인 | 2.1.0 | Compose 함수를 Kotlin 빌드에 통합 |
| Compose BOM | 2024.02.00 | UI·Material 3 등 Compose 라이브러리의 버전 묶음 |
| Activity Compose | 1.8.2 | Activity와 Compose 화면 연결 |
| Coroutines Android | 1.7.3 | Android에서 비동기 작업을 다루는 기반 |
| Coil Compose | 2.6.0 | 게임 표지 등 이미지 표시 |
| CameraX | 1.4.2 | 선택 기능인 카메라 추적에 필요한 프레임 입력 |
| AndroidX WebKit | 1.12.1 | WebView 관련 기능 연결 |
버전 출처는 루트 build.gradle과
app/build.gradle입니다.
BOM 번호는 Material 3 자체의 버전 번호가 아닙니다.
app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt의 mutableStateOf와 app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt를 함께 읽으면
데이터가 바뀔 때 화면 구성이 바뀌는 선언형 UI를 배울 수 있습니다.
| 층 | 실제 기술·버전 | 읽을 지점 |
|---|---|---|
| 세션 조율 | Kotlin + Java, JVM 타깃 17 | app/src/main/java/com/droiddeck/launcher/session/SessionService.kt, app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java |
| 네이티브 경계 | JNI, Rust jni 0.21.1 | app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt ↔ Rust app/src/main/rust/stores/src/epic/ffi.rs |
| 다운로드·통신 | reqwest 0.12.28, tokio 1.52.3 | Rust app/src/main/rust/stores/src/fetch_core.rs, 스토어별 네트워크 모듈 |
| 구조화 데이터 | serde_json 1.0.150 | JNI 요청·응답과 스토어 데이터 |
| 압축·검증 | flate2 1.1.9, lzma-rs 0.3.0, sha1 0.10.6, sha2 0.10.9 | 게임 청크 처리와 무결성 검사 |
Rust 표의 숫자는 Cargo.lock에 기록된 고정 버전입니다.
Cargo.toml의
reqwest = "0.12" 같은 선언은 허용 범위를 나타냅니다.
이 크레이트의 이름은 droiddeckstores, 버전은 0.1.0, 에디션은 2021,
결과물 형태는 cdylib입니다. 앱 버전 0.6.0과 별개입니다.
cdylib 결과물은 이 경우 libdroiddeckstores.so가 됩니다.
Kotlin의 System.loadLibrary("droiddeckstores")가 이 파일을 로드합니다.
웹 서버에 요청하는 것과 달리 앱 프로세스 안의 함수 경계를 넘습니다.| 영역 | 버전 근거 | 구조를 읽는 관점 |
|---|---|---|
| Wayland 서버 | 동봉 헤더 WAYLAND_VERSION "1.26.0" | Linux 창과 Android 표시 표면 사이를 연결 |
| Gamescope | gamescope-3.16.29-p12 | 게임·Steam 쪽 화면을 묶어 Wayland로 출력하는 고정 배포물 |
| PRoot | 소스 커밋 4dba3afbf3a63af89b4d9c1a59bf2bda10f4d10f | Android 위에서 Linux 파일 경로와 실행 환경을 맞춤 |
| DirectAudio / PulseAudio | 별도 팩과 빌드 도구로 준비, README의 로컬 빌드는 PulseAudio 13.0 소스 사용 | 게임과 Steam의 오디오 경로를 Android 쪽 출력과 연결 |
| OpenXR 로더 | 1.1.49 | XR 기능을 위한 별도 경로 |
| OpenCV / MediaPipe | 4.13.0 / 1.0.0 | 손·몸 추적 같은 선택 기능용; 기본 게임 실행의 필수 구성과 구별 |
Gamescope는 tools/gamescope/release.env, PRoot는 tools/proot/source.env,
추적 라이브러리는 tools/packs/tracking/release.env에 고정되어 있습니다.
Proton·FEX는 런타임과 구성요소 관리 경로가 있으므로 이 글에서는 모든 실행에 적용되는 단일 버전을 임의로 붙이지 않습니다.
| 항목 | 설정값 | 초보자가 배울 점 |
|---|---|---|
| Gradle / AGP | 8.10.2 / 8.8.0 | 빌드 실행기와 Android 플러그인의 역할 분리 |
| Android SDK | compileSdk 34, minSdkVersion 26, targetSdkVersion 28 | 컴파일 가능 API·설치 하한·동작 정책 기준은 서로 다른 값 |
| NDK / CMake | 27.3.13750724 / 3.22.1 | Android 네이티브 코드의 별도 컴파일 체계 |
| CPU ABI | arm64-v8a만 패키징 | Android APK의 CPU 대상과 게임의 원래 CPU 대상 구별 |
| Rust 빌드 | rustup run stable cargo ndk, --platform 26, --locked | Android 대상 공유 라이브러리 생성과 잠금 파일 사용 |
| 로컬 교차 컴파일 | tools/local-cross.Dockerfile의 ubuntu:24.04 | Linux용 보정 라이브러리를 호스트 OS와 구별해 준비 |
| 검증 | JUnit 4.13.2, Robolectric 4.16.1, Rust 테스트, Python 회귀 검사 | 화면·다운로드·시스템 경계를 여러 수준에서 확인 |
.github/workflows/build.yml에는 ARM64 Xbox 백엔드 작업과 APK 컴파일·테스트 작업이 나뉘어 있습니다.
Xbox 작업은 Rust 1.99.0을 지정하지만 Android 스토어 엔진의 Gradle 작업은 stable을 사용합니다.
하나의 Rust 버전으로 전체 빌드를 설명하면 틀립니다. 로컬 헬퍼는 설치된 NDK를 선택할 수 있어 기본 지정값과 실제 선택값도 구분해야 합니다.
app/build.gradle의 stageSessionScripts는 Linux 오버레이 스크립트를 앱 자산으로 옮깁니다.
verifyRuntimeInputs는 패키징 전에 준비된 입력을 검사합니다.
의존성을 나열하는 데서 멈추지 말고, 소스 → 준비 파일 → APK로 이어지는 변환 관계를 찾아보세요.
아래는 대표 경로를 단순화한 구조도입니다. 게임 종류와 선택한 Steam 클라이언트 아키텍처에 따라 세부 경로는 바뀝니다. 그림에서 “호스트”는 Android 쪽, “게스트”는 준비된 Linux 사용자 공간 쪽을 뜻합니다.
LinuxRuntime.SESSION_SCRIPT는 /usr/local/bin/droiddeck-session입니다.
소스 원본은 tools/linuxfs/overlay/usr/local/bin/droiddeck-session에 있고,
run_gamescope()와 모드 분기에서 실제 프로세스 실행을 확인할 수 있습니다.
Gamescope 인자에는 --backend wayland와 --expose-wayland가 들어갑니다.
소스: Linux 세션 진입 스크립트.
Surface는 그림을 내보내는 표면입니다.
DroidDeck에는 Linux 쪽 Gamescope와 Android 쪽 Wayland 합성기라는 서로 다른 단계가 있으므로,
“게임이 실행됨”과 “첫 화면이 표시됨”을 별개 사건으로 다뤄야 합니다.app/src/main/java/com/droiddeck/launcher/session/SessionService.kt의 runSession(gen: Int)은 세션을 시작할 때
런타임 파일, 오디오, 컨트롤러, 환경변수, 바인딩을 준비하고 마지막에 프로세스를 띄웁니다.
함수 도중 여러 번 gen != sessionGen을 검사합니다.
사용자가 시작 직후 중지하거나 다시 시작하면, 먼저 시작한 작업의 결과가 현재 세션을 덮어쓰지 않도록 막는 장치입니다.
// SessionService.runSession()에서 발췌한 핵심 조건
if (gen != sessionGen || !SessionState.running) {
if (gen == sessionGen) components.clear()
return
}
components.forEach { it.start() }
// 시작 도중 중단된 뒤 정리하는 경로에서 사용
components.reversed().forEach { runCatching { it.stop() } }
app/src/main/java/com/droiddeck/launcher/core/SessionPart.java는 attach(), start(), stop(),
suspendPid()라는 공통 계약을 제공합니다.
서비스는 구체적인 오디오·네트워크 구현을 전부 같은 방식으로 다룰 수 있습니다.
역순 정리는 나중에 준비한 자원부터 해제하는 방식이며, 부분 실패를 고려하는 수명주기 설계의 예입니다.
소스: 세션 실행과 정리.
여기에 상태를 이름으로 남기는 설계가 더해집니다.
app/src/main/java/com/droiddeck/launcher/session/SessionState.kt에는 SessionPhase 열거형이 있고,
app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt의 transition()은 상태 변경과 이벤트 기록을 묶습니다.
다만 전이표로 모든 잘못된 이동을 강제 차단하는 완전한 상태 기계라고 단정할 수는 없습니다.
// 대표 경로를 학습용으로 요약한 상태 흐름
IDLE → PREPARING → STARTING_COMPOSITOR → STARTING_GUEST
│
▼
STARTING_STEAM
│
▼
READY ↔ SUSPENDED
│
▼
STOPPING
// 실패 상태는 FAILED, 설치가 필요하면 INSTALLING_RUNTIME도 등장
특히 SessionEvents.markReadyIfPossible()는 실행 중인지,
guestPid > 1인지, 첫 프레임을 보았는지, 일시 중지가 아닌지를 함께 확인합니다.
프로세스를 띄운 성공과 사용자가 화면을 볼 수 있는 성공을 분리한 점이 이 실행 구조의 핵심입니다.
사건은 events.jsonl에 시간·이벤트·세션 식별자와 함께 추가됩니다.
식당에서 주문서를 받았다고 음식이 나온 것은 아닙니다. PID 생성은 주문 접수, 첫 프레임은 음식이 식탁에 도착한 사건입니다. 화면 로딩을 끝낼 때 어떤 사건을 기준으로 삼을지 설계해야 합니다.
app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt의 ensureLoaded()는 공유 라이브러리의 로드 결과를 캐시하고,
실패 정보를 보관합니다. app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt의 call()은
연산 이름과 JSON 문자열을 Rust로 넘깁니다. Rust의 app/src/main/rust/stores/src/epic/ffi.rs는
JNI 자료형을 Rust 문자열로 바꾸고 service::call()로 위임합니다.
guarded()는 catch_unwind로 Rust 패닉을 잡고 대체 결과를 반환합니다.
이것만으로 잘못된 포인터나 모든 네이티브 장애를 막는 것은 아닙니다.
학습할 지점은 오류 형태와 자료형 변환을 경계에서 명확히 처리하는 방식입니다.
소스: JNI 변환과 패닉 처리.
설치 요청은 조금 더 복잡합니다. Kotlin EpicNative.install()은 네이티브 핸들을 받고,
CountDownLatch로 완료를 기다리며 250ms 간격으로 취소 여부를 확인하고, 취소 요청은 한 번 전달합니다.
finally에서는 nativeRelease(handle)를 부릅니다.
“실행 요청·진행 통지·취소·자원 해제”가 별개 계약이라는 점을 읽어야 합니다.
Rust 안에서도 app/src/main/rust/stores/src/fetch_core.rs의 FetchSink가 다운로드와 저장 방식을 분리합니다.
네트워크 엔진은 FetchItem을 가져오고, 스토어 쪽 구현은 받은 바이트를 처리합니다.
스트리밍을 지원하지 않는 기본 메서드는 SinkError::Fatal을 반환해 제약을 드러냅니다.
// fetch_core.rs의 공통 계약 일부
pub trait FetchSink: Send + Sync {
fn process(
&self,
item: &FetchItem,
body: Vec<u8>
) -> Result<u64, SinkError>;
}
app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java의 installOnce()는
다운로드 완료 후 SHA-256을 다시 계산하고, 새 런타임을 linuxfs.new에 풉니다.
기존 트리는 linuxfs.old로 옮기고, 사용자 홈을 옮긴 다음 새 트리를 정식 이름으로 바꿉니다.
실패하면 이전 트리를 되돌리는 분기도 있습니다.
// 함수의 작업 순서를 요약한 도식
아카이브 다운로드 → SHA-256 검사 → linuxfs.new에 압축 해제
│
기존 linuxfs → linuxfs.old │
│ │
└─ 사용자 /root 홈 이관 ──┘
▼
linuxfs.new → linuxfs
│
▼
이전 트리 정리
recoverInterruptedSwap()는 다음 시도에서 중단된 교체 흔적을 살펴봅니다.
중요한 것은 “임시 폴더를 쓴다”보다 각 단계 사이에서 앱이 종료되면 무엇이 남는가입니다.
여러 번의 이동과 삭제를 묶은 전체 작업이 데이터베이스 트랜잭션처럼 한 번에 원자적으로 완료되는 것은 아닙니다.
소스: 런타임 설치·복구.
더 작은 복구 사례는 app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt에 있습니다.
saveState()는 desired.tsv.tmp를 쓰고 동기화한 뒤,
새 상태와 임시 TSV의 해시를 state.pending.json에 기록합니다.
desired.tsv 공개 후 state.json을 갱신하고 임시 기록을 지웁니다.
recoverState()는 공개 파일의 해시로 상태 갱신을 마무리할지 판단합니다.
설정 화면이 읽는 JSON과 Linux 실행 쪽이 읽는 TSV가 어긋나면 사용자의 선택과 실제 실행이 달라집니다. 임시 기록과 해시는 두 파일을 연결하는 복구 단서입니다. 설정 저장에서도 일관성 문제가 생긴다는 좋은 사례입니다.
게임 파일에서도 같은 원칙을 볼 수 있습니다. Rust app/src/main/rust/stores/src/epic/install/writer.rs의
finish_file()은 sync_all(), 길이 검사, 기대값과 계산값이 모두 있을 때의 SHA-1 검사, 취소 확인,
before_replace() 승인 단계를 거친 뒤 fs::rename()으로 해당 파일을 교체합니다.
이 보장은 파일별 교체에 관한 것이며 게임 설치 전체가 단일 원자적 작업이라는 뜻은 아닙니다.
커밋 ffa40a1의 핵심 변경은 tools/linuxfs/preload/usr2.c입니다.
ARM64 코드에서 raise(int sig)를 가로채되, 신호가 SIGUSR2인지,
호출자가 ntdll.so의 실행 영역 안에 있는지, GCS 하드웨어 기능 지원 비트가 없는지 등을 검사합니다.
신호 처리기 형식이나 차단 상태가 조건에 맞지 않아도 원래 raise()를 호출합니다.
// usr2.c의 첫 번째 우회 조건을 줄바꿈해 발췌
if (sig != SIGUSR2 ||
(getauxval(AT_HWCAP) & BL_HWCAP_GCS) ||
!usr2_from_ntdll(__builtin_return_address(0))) {
return real(sig);
}
조건을 만족하면 bl_usr2_frame()이 레지스터와 부동소수점 상태를 담을 프레임을 만들고,
bl_usr2_run()이 등록된 처리기를 직접 부릅니다.
이후 rt_sigreturn으로 문맥을 복원합니다.
BL_FRAME_SIZE는 4688이며 _Static_assert로 구조체 크기와 오프셋을 검사합니다.
커밋 제목의 “커널 신호 없이”는 이 특정 경로에서 신호를 실제로 발생시키는 과정을 줄인다는 뜻입니다.
신호 설정 조회·마스크 처리와 rt_sigreturn 같은 시스템 호출까지 사라진 것은 아닙니다.
README는 PRoot가 이 신호 때문에 스레드를 멈추는 비용을 줄이려는 의도라고 설명합니다.
이 구현만으로 속도 향상률을 알 수는 없습니다.
libblsession의 일부가 환경 차이를 흡수합니다.
Android용 JNI 라이브러리와 Linux 게스트용 라이브러리는 대상 환경이 다르므로 같은 파일처럼 취급하면 안 됩니다.tools/tests/test_usr2_preload.py는 레지스터 보존, 변경된 문맥으로의 이동, 신호 마스크,
일반 신호 경로 등을 확인하는 회귀 검사 코드를 담고 있습니다.
초보 단계에서는 어셈블리를 모두 이해하기보다 최적화를 적용하는 조건과 적용하지 않는 조건을 먼저 적어 보세요.
구현과
검사 소스를 나란히 읽는 것이 좋습니다.
app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt의 startOrAttach()는 처음에는 합성기를 시작하고,
이미 시작됐다면 Surface만 다시 연결합니다.
JNI의 nativeStartWithSurface()는 Android Surface에서 ANativeWindow를 얻고 렌더러에 전달합니다.
app/src/main/cpp/waylandcomp/src/compositor.c의 droiddeck_wayland_run()은 wayland-0 소켓을 등록합니다.
화면 객체의 수명과 게임 프로세스의 수명이 다르다는 점이 실제 함수 경계에도 드러납니다.
다음 트리의 루트는 DroidDeck/ 저장소 루트입니다. 본문의 소스 경로도 저장소 루트를 기준으로 합니다.
Java 소스 경로 아래에 Kotlin 파일도 놓이는 것은 Android 프로젝트에서 볼 수 있는 구성입니다.
| 경로 | 구별할 점 |
|---|---|
app/src/main/java/com/droiddeck/launcher/store/와 app/src/main/java/com/droiddeck/launcher/stores/ | 앞쪽은 Flathub·사용자 앱 관련 코드, 뒤쪽은 Epic·GOG 등 게임 스토어 코드입니다. 단복수를 놓치면 다른 기능을 읽게 됩니다. |
app/src/main/cpp/와 tools/linuxfs/preload/ | 전자는 Android 네이티브 기능, 후자는 Linux 게스트의 실행 차이를 메우는 코드입니다. 같은 C 계열 언어라도 실행 환경이 다릅니다. |
tools/linuxfs/overlay/와 app/src/main/assets/ | 전자는 Linux 스크립트 원본, 후자는 앱에 담을 자산입니다. Gradle의 복사 작업으로 연결되며 준비 전에는 같은 파일이 양쪽에 모두 있지 않을 수 있습니다. |
디렉터리를 이해했다는 기준은 파일 위치를 외우는 것이 아니라 수정한 원본이 어느 결과물에 들어가는지 설명하는 것입니다. 예를 들어 세션 스크립트는 오버레이에서 시작해 자산 준비를 거쳐 APK에 들어갑니다. JNI 함수는 Android용 공유 라이브러리로, Linux 보정 코드는 별도 교차 컴파일 경로로 갑니다.
처음에는 app/src/main/AndroidManifest.xml, app/src/main/java/com/droiddeck/launcher/session/SessionState.kt, app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt,
app/src/main/java/com/droiddeck/launcher/session/SessionService.kt, app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java, tools/linuxfs/overlay/usr/local/bin/droiddeck-session 순서로 보세요.
거대한 Activity와 스크립트를 처음부터 끝까지 정독하기보다 특정 요청 하나를 따라가면 범위가 작아집니다.
app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt의 mutableStateOf는 화면에 반영할 상태를 보여 줍니다.
반면 app/src/main/java/com/droiddeck/launcher/session/SessionState.kt에는 @Volatile 필드와 Compose 상태가 섞여 있습니다.
둘은 같은 개념이 아닙니다. 스레드 사이에서 값이 보이도록 하는 문제와 화면 재구성을 일으키는 문제를 구별하세요.
더 작게 시작하려면 app/src/main/java/com/droiddeck/launcher/session/LatestRequestCoordinator.kt를 추천합니다.
submit()은 최신 요청과 대기 요청을 저장하고,
takeLatest()는 대기 요청을 꺼내며, isLatest()는 결과 적용 자격을 검사합니다.
비교 값이 같으면 세대 번호를 증가시키지 않는 점도 관찰할 만합니다.
// 실제 클래스 API를 사용한 학습용 순서 예시
val queue = LatestRequestCoordinator<String>()
queue.submit("A")
val a = queue.takeLatest()!!
queue.submit("B")
queue.submit("C")
queue.isLatest(a) // false: A 결과는 적용하면 안 됨
queue.takeLatest()?.value // "C": 대기 중인 B는 최신 값으로 합쳐짐
실습 아이디어: A·B·C 요청의 제출, 계산 완료, 화면 적용 시간을 종이에 표시하세요. 최신 결과 판정은 이전 작업을 실제로 취소하는 것과 다릅니다. 구현과 기존 검사를 비교하면 검색창·설정 미리보기에도 적용할 수 있는 원리입니다.
화면이 사라졌다고 게임 프로세스를 곧바로 끝내면 안 되고, 게임이 끝나지 않았다고 이전 Surface를 계속 붙잡아도 안 됩니다.
app/src/main/java/com/droiddeck/launcher/SessionActivity.kt, app/src/main/java/com/droiddeck/launcher/session/SessionService.kt, app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt를 함께 읽으면
화면 연결과 실행 세션을 다른 수명으로 다루는 이유가 보입니다.
실습 아이디어: “화면 회전”, “다른 앱으로 이동”, “게임 종료”를 행으로 두고 Activity·Service·게스트 프로세스·Surface 중 무엇이 영향을 받는지 표로 정리하세요. 해당 소스에서 확인하지 못한 동작은 예상이라고 표시합니다.
app/src/main/rust/stores/src/fetch_tuning.rs에는 초기 창 8, 호스트별 상한 6,
창 절대 상한 256, 청크 시도 상한 5가 있습니다.
동시에 작업 중인 바이트의 예산은 최소 24 MiB, 최대 96 MiB로 제한합니다.
상수의 256은 언제나 256개를 병렬 실행한다는 뜻이 아닙니다.
// inflight_budget_bytes()의 계산을 수식으로 정리
예산 = clamp(실효 상한 × 1 MiB × 3 / 2, 24 MiB, 96 MiB)
실효 상한 8 → 12 MiB 계산 → 하한 적용 → 24 MiB
실효 상한 32 → 48 MiB
실효 상한 256 → 384 MiB 계산 → 상한 적용 → 96 MiB
이 값은 다운로드 엔진의 작업 승인 예산이며 앱 전체 메모리 한도는 아닙니다.
budget_admits()는 진행 중 작업이 0일 때 큰 작업 하나를 허용하므로 완벽한 바이트 벽으로 해석해서도 안 됩니다.
실습 아이디어: 느린 디스크와 빠른 네트워크를 가정해 대기 바이트가 쌓이는 과정을 그려 보세요.
소스: 예산·재시도·창 크기 상수.
tools/linuxfs/preload/usr2.c에서는 구조체 배치가 운영체제가 기대하는 형식과 맞아야 합니다.
app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c에서는 JVM 객체와 네이티브 윈도우를 연결합니다.
같은 “함수 호출”이라도 메모리 표현과 자원 소유권까지 맞춰야 한다는 공통점이 있습니다.
실습 아이디어: JNI가 문자열을 빌리고 복제하고 돌려주는 지점을 표시하거나,
tools/linuxfs/preload/usr2.c에서 real(sig)로 돌아가는 조건을 모두 수집하세요.
어셈블리 명령 하나씩 해석하는 작업은 그다음 단계로 남겨도 됩니다.
tools/release/runtime_inputs.py는 소스와 준비된 출력의 해시를 관리하고,
필요한 파일 목록을 확인합니다. app/build.gradle은 이를 패키징 작업에 연결합니다.
네이티브 소스를 바꾸고 예전 라이브러리를 그대로 담는 실수를 막기 위한 설계입니다.
실습 아이디어: CI의 tools/release/check_session_assets.py,
tools/release/check_android_libraries.py, cargo test --locked,
testReleaseUnitTest가 각각 어떤 종류의 오류를 찾는지 분류해 보세요.
CI의 Rust 호스트 테스트 단계는 continue-on-error: true로 설정되어 있어 실패해도 전체 작업이 계속됩니다.
좋은 학습은 기술 이름을 많이 아는 데서 끝나지 않고, 그 기술이 막는 실패 상황을 설명할 수 있어야 합니다. 화면 상태는 재구성과, 오래된 결과 무효화는 세대 번호와, 파일 교체는 중단 복구와, 병렬 다운로드는 자원 예산과 연결해서 공부하세요.
| 항목 | 확인된 조건 | 근거와 해석 |
|---|---|---|
| Android 버전 | README는 Android 9 이상 | 빌드는 minSdkVersion 26, 즉 Android 8.0 하한을 선언합니다. 설치 하한이 README의 지원 약속을 넓혀 주지는 않습니다. |
| CPU | arm64-v8a | app/build.gradle의 유일한 ABI입니다. x86 Android 기기용 APK라고 해석할 수 없습니다. |
| GPU | README 기준 Adreno 730 이상 또는 8xx | Adreno 6xx는 실험적이며 DX11은 DXVK 2 경로가 동작할 수 있지만 DX12는 충돌할 수 있다고 설명합니다. |
| 미지원 GPU | README 기준 Mali·Xclipse·PowerVR·Adreno 710 | APK가 설치되거나 Vulkan API가 있다는 사실만으로 지원되는 것은 아닙니다. |
| Vulkan | Manifest에 0x401000, 필수 | Vulkan 1.1 기능 요구 선언입니다. 특정 게임의 그래픽 기능 충족 여부까지 보증하지 않습니다. |
| 저장 공간 | README 기준 런타임 약 3 GB, 데스크톱·에뮬레이터 추가 약 1.1 GB | 게임 파일은 별도입니다. 다운로드 아카이브와 교체용 임시 트리 때문에 업데이트 중에는 더 많은 공간이 필요할 수 있습니다. |
| 메모리 | README·빌드 설정에 공통 최소 RAM 수치 없음 | 게임과 선택 기능별 요구량이 달라 임의의 “8 GB 필수” 같은 기준을 만들 수 없습니다. |
| 루트 권한 | README는 불필요하다고 설명 | PRoot와 앱 권한을 이용하는 구조입니다. Linux의 진짜 관리자 권한이 생긴다는 뜻은 아닙니다. |
README는 Android 14 이상에서 개발자 옵션의 자식 프로세스 제한 해제를 먼저 켜도록 안내합니다.
Android 12·13에서 해당 옵션이 보이지 않으면 최초 실행의 Fix it for me가 설정을 돕는다고 설명합니다.
실제 코드는 app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt에 있습니다.
usesDeviceConfig()는 Android 12(API 31)를 별도 처리하고,
blocksSteam()은 적용 대상이 아니거나 명시적으로 해제된 경우를 제외하면 실행을 막습니다.
운영체제 버전별 정책 차이를 한 개의 참·거짓 값보다 자세한 상태로 모델링한 사례입니다.
이 글의 읽기 실습에는 기기 설정 변경이 필요하지 않습니다.
| 활동 | 준비할 것 |
|---|---|
| 이 글의 실습 | 텍스트 편집기, 소스 사본, 선택적으로 rg·Python. 지원 Android 기기와 Steam 계정이 없어도 진행 가능 |
| APK 빌드 | README 기준 Docker, Java 17, Android SDK·NDK, rustup 기반 Rust, GitHub CLI, Python 3, zstd |
| 로컬 준비 | tools/build_local.sh가 네이티브 부품과 고정 배포물을 준비. 소스 읽기만으로 끝나는 작업과 범위가 다름 |
| Windows 경로 | 준비된 체크아웃용 PowerShell·Python 도구 존재. 도구가 있다는 사실과 새 환경에서 즉시 빌드 가능하다는 주장은 구별 |
| 스마트 안경·추적 | 선택 기능. 모델별 연결 방식·SDK·지원 단계가 달라 별도 지원 표를 확인 |
README의 권장 기기 조건을 우선해서 읽으세요. 기기별 성능 검증은 제한적이라고 README도 밝힙니다. 지원 조건만으로 게임별 FPS, 발열, 배터리 소모, 안티치트 호환 여부까지 보장되는 것은 아닙니다.
모든 과제는 원본을 읽고 별도 노트를 만드는 방식입니다. 아래 검색 명령은 저장소 루트에서 사용하는 읽기 명령입니다. 과제의 완료 기준은 프로그램을 띄웠다는 사실보다 정상·실패·중단 경로를 근거와 함께 설명하는 것입니다.
읽을 파일: app/src/main/AndroidManifest.xml, app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt,
app/src/main/java/com/droiddeck/launcher/SessionActivity.kt, app/src/main/java/com/droiddeck/launcher/session/SessionService.kt, app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java.
할 일: 앱 진입점, 세션 요청, 프로세스 시작, Linux 진입 스크립트를 화살표로 잇습니다. 화살표마다 “Intent 전달”, “함수 호출”, “프로세스 생성” 중 무엇인지 표시하세요.
제출물: 다섯 파일 이상을 연결한 한 장짜리 지도와 근거 함수 목록.
완료 기준: MainActivity.kt를 찾지 못해도 진입점을 설명하고,
LinuxRuntime.SESSION_SCRIPT의 값을 정확히 적을 수 있어야 합니다.
# 저장소 루트에서 코드 위치만 검색
rg -n 'activity-alias|targetActivity|SessionService' app/src/main/AndroidManifest.xml
rg -n 'runSession|HostProcess.start|SESSION_SCRIPT' app/src/main/java
rg -n 'run_gamescope|--backend wayland' tools/linuxfs/overlay/usr/local/bin/droiddeck-session
읽을 파일: app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt의 markReadyIfPossible()와
app/src/main/java/com/droiddeck/launcher/session/SessionState.kt.
할 일: 실행 여부, 유효한 PID, 첫 프레임, 일시 중지 여부를 네 열로 만듭니다. 가능한 16개 조합에서 READY 전환 조건을 계산하세요. 이미 READY인 경우에는 어떤 추가 조건이 적용되는지도 적습니다.
제출물: 조건표와 “PID는 있는데 화면이 검다” 상황의 설명.
완료 기준: 모든 준비 조건이 맞을 때만 전환 후보가 되며,
phase != READY 검사로 중복 전환을 피한다는 점을 찾습니다.
읽을 파일: app/src/main/java/com/droiddeck/launcher/session/LatestRequestCoordinator.kt와 같은 이름의 테스트 파일.
할 일: A 제출 → A 계산 시작 → B 제출 → C 제출 → A 완료라는 시간선을 그립니다.
이어서 같은 값 C를 다시 제출했을 때 세대 번호가 변하는지도 계산합니다.
submit()의 반환값이 요청 성공 여부인지 작업자 시작 필요 여부인지 설명하세요.
제출물: 단계별 generation·running·pending 표.
완료 기준: A의 결과가 무효가 되는 이유와 B가 최신 대기값으로 대체되는 이유를 구분하고,
같은 값의 반복 제출은 진행 중 결과를 무효화하지 않는다는 검사까지 연결합니다.
읽을 파일: app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt의 saveState(),
recoverState(), writeSynced().
할 일: 임시 TSV 쓰기 직후, pending 기록 직후, TSV 공개 직후, 정식 JSON 저장 직후에 앱이 꺼졌다고 가정합니다. 각 시점의 네 파일 존재 여부와 다음 실행의 복구 분기를 적습니다.
제출물: 중단 지점별 파일 상태표와 해시가 필요한 이유 세 문장. 완료 기준: 공개된 TSV와 pending의 해시가 맞고 임시 TSV가 없을 때 상태를 완성하는 조건을 설명합니다. 원본 파일을 직접 지우거나 고쳐서 장애를 만들 필요는 없습니다.
읽을 파일: Rust app/src/main/rust/stores/src/fetch_tuning.rs, app/src/main/rust/stores/src/fetch_core.rs,
app/src/main/rust/stores/src/epic/install/writer.rs.
할 일: 실효 상한 8·32·256의 예산을 계산하고, 호스트별 상한이 별도로 필요한 이유를 적습니다.
이어서 디스크 쓰기가 느려지는 상황과 finish_file()에서 해시가 틀리는 상황을 각각 추적하세요.
제출물: 생산자·다운로드·저장 담당자의 대기열 그림, 예산 계산표, 오류 전파 경로. 완료 기준: “스레드를 늘리면 늘 빨라진다”는 가정의 문제를 설명하고, 네트워크 청크 성공과 최종 파일 공개 성공을 구분합니다.
각 제출물에 커밋, 파일 경로, 함수명, 확인한 조건을 남기세요. 테스트 파일이 어떤 동작을 기대하는지 읽는 것은 유용하지만 실제 테스트 통과나 기기 호환성을 입증한 것과는 구별해야 합니다.
| 주차 | 먼저 익힐 개념 | 저장소에서 읽을 지점 | 남길 결과물 |
|---|---|---|---|
| 1주차 | Kotlin의 클래스·object·null·컬렉션, Android 컴포넌트 등록 | Manifest, app/src/main/java/com/droiddeck/launcher/session/SessionState.kt, app/src/main/java/com/droiddeck/launcher/frontend/Library.kt | 앱 진입점과 게임 데이터 모델 지도 |
| 2주차 | Compose 상태·재구성, 스레드 가시성과 락의 차이 | app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt, app/src/main/java/com/droiddeck/launcher/session/LatestRequestCoordinator.kt | 화면 상태 목록과 A·B·C 요청 시간선 |
| 3주차 | 프로세스·환경변수·파일 바인딩·수명주기 | app/src/main/java/com/droiddeck/launcher/session/SessionService.kt, app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java, tools/linuxfs/overlay/usr/local/bin/droiddeck-session | Android → PRoot → Linux 실행 지도 |
| 4주차 | Rust 소유권·Result·trait·JNI 자료형·취소 | app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt, app/src/main/rust/stores/src/epic/ffi.rs, app/src/main/rust/stores/src/fetch_core.rs | 요청·진행·취소·해제 계약표 |
| 5주차 | 해시·임시 파일·rename·중단 복구·자원 예산 | app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt, app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java, app/src/main/rust/stores/src/epic/install/writer.rs | 중단 시점별 복구표와 메모리 계산표 |
| 6주차 | Wayland·Vulkan·CPU 문맥·ABI·재현 가능한 빌드 입력 | app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt, tools/linuxfs/preload/usr2.c, tools/release/runtime_inputs.py, CI | 화면 표시 흐름과 보정 코드의 적용 조건 목록 |
주차는 강의의 절대 분량이 아니라 읽는 순서입니다. Kotlin이 처음이라면 1·2주차를 늘리고, Rust나 C를 이미 쓴다면 4·6주차에서 경계 처리와 오류 흐름을 더 깊게 보세요. 배울 범위를 넓힐 때도 같은 실행 요청을 다시 따라가면 새 개념이 기존 구조에 연결됩니다.
mutableStateOf를 찾아 화면 상태와 단순 필드를 구분합니다.app/src/main/rust/stores/src/epic/ffi.rs의 변환 함수와 네이티브 핸들 해제 경로를 표시합니다.Vec<u8>의 전달과 FetchSink: Send + Sync 계약을 읽습니다.tools/linuxfs/preload/usr2.c의 조건과 대체 경로를 설명합니다.stageSessionScripts와 verifyRuntimeInputs가 어느 단계에 붙는지 찾습니다.건물의 모든 전선을 한 번에 외우려 하지 마세요. 처음에는 현관에서 방까지 가는 길을 찾고, 다음에는 같은 길의 전기, 그다음에는 수도를 봅니다. 같은 실행 경로 위에 다른 기술 층을 겹쳐 보면 큰 저장소도 작은 문제들로 나뉩니다.
이 세 질문에 파일과 함수로 답할 수 있다면, 다른 런처·다운로더·미디어 앱을 읽을 때도 같은 틀을 사용할 수 있습니다.
| 용어 | 쉬운 설명 |
|---|---|
| 런처 | 실행할 항목을 모으고 필요한 환경을 준비해 시작시키는 앱. 이 저장소에서는 게임 출처·설정·세션까지 연결합니다. |
| Steam Big Picture | 컨트롤러와 큰 화면에 맞춘 Steam 인터페이스. DroidDeck이 제공하려는 경험의 중심입니다. |
| 사용자 공간 | 앱과 라이브러리가 실행되는 영역. Linux 파일 트리를 준비해도 Android 커널을 교체하는 것은 아닙니다. |
| 런타임 / 루트 파일 트리 | 프로그램 실행에 필요한 도구·라이브러리·폴더 묶음. 이 앱은 linuxfs라는 이름으로 관리합니다. |
| PRoot | 관리자 권한 없이 Linux 프로그램이 보는 파일 경로와 실행 환경을 조정하는 도구. 가상 머신이나 새 커널과는 구조가 다릅니다. |
| Proton / Wine | Windows 프로그램의 API 요구를 Linux 쪽에서 처리하는 호환성 기술. Proton은 Wine과 추가 부품을 묶은 Steam Play 도구입니다. |
| FEX / 동적 변환 | x86·x86-64 코드를 Arm64 Linux에서 실행하기 위한 기술. 운영체제 API 호환성과 CPU 명령어 문제를 구별할 때 중요합니다. |
| ABI | 기계어 수준에서 함수·자료형·바이너리가 맞춰야 하는 약속. arm64-v8a는 Android의 대상 ABI 이름입니다. |
| JNI / FFI | 다른 실행 환경·언어 사이에서 함수를 부르는 경계. JNI는 Java 계열과 네이티브 코드의 연결 방식입니다. |
| 공유 라이브러리 | 실행 중 로드해서 함수를 사용하는 바이너리. Android와 Linux 게스트용 파일은 대상 환경을 따로 확인해야 합니다. |
| Wayland | 창을 만드는 프로그램과 화면 합성기가 통신하는 규약. 이 코드에서는 wayland-0 소켓이 연결 지점입니다. |
| Gamescope | 게임 화면과 표시 환경을 다루는 합성기. DroidDeck의 Android 쪽 Wayland 합성기와 구별해서 읽습니다. |
| Surface / AHardwareBuffer | 각각 Android에서 그림을 표시하는 표면과 공유 가능한 하드웨어 버퍼 객체. 화면 경계를 연결하는 코드에서 만납니다. |
| Vulkan / Turnip | Vulkan은 그래픽 API, Turnip은 Mesa의 Adreno용 드라이버입니다. API가 있다는 것과 해당 기기 지원은 다른 문제입니다. |
| DXVK | Direct3D 8·9·10·11 호출을 Vulkan으로 연결하는 기술. Direct3D 12의 변환 경로까지 같은 이름으로 묶으면 안 됩니다. |
| 세대 번호 | 요청이 어느 시점의 것인지 구별하는 번호. 이전 결과가 현재 상태를 덮어쓰는 일을 막는 데 사용합니다. |
| 멱등성 | 같은 작업을 반복해도 결과가 불필요하게 달라지지 않는 성질. 재시도·복구 코드를 읽을 때 따져 볼 질문입니다. |
| 스테이징 / 원자적 교체 | 임시 위치에서 작업한 뒤 이름을 바꿔 결과를 공개하는 방식. 한 파일의 교체 보장과 전체 설치의 보장은 구별합니다. |
| 해시 / 체크섬 | 바이트가 기대한 내용과 같은지 비교하는 요약값. 다운로드 검증뿐 아니라 어떤 설정이 공개됐는지 판단하는 단서가 됩니다. |
| 백프레셔 | 뒤쪽 처리가 밀리면 앞쪽 작업량을 조절하는 원리. 작업 수와 바이트 예산을 함께 보는 이유입니다. |
| 시그널 / 문맥 | 시그널은 프로세스에 전달되는 사건, 문맥은 재개에 필요한 레지스터·실행 위치 등의 상태입니다. |
| 프리로드 | 동적 라이브러리를 먼저 읽어 일부 함수를 대신 처리하는 방식. 적용 조건과 원래 함수로 돌아가는 경로가 중요합니다. |
| Robolectric | Android 관련 동작을 JVM 환경에서 검사하는 도구. 실제 GPU와 게임 호환성 검사까지 대신하지는 않습니다. |
| 잠금 파일 / 고정 입력 | 의존성 버전과 부품 해시를 기록해 빌드 입력을 통제하는 수단. 모든 외부 입력이 자동으로 고정되는 것은 아닙니다. |
| 항목 | 확인값 |
|---|---|
| 저장소·선정 | Droid-Deck/DroidDeck · TrendShift Monthly #19 (2026-10-23 기준) |
| 스타 / 포크 | 12,910 / 662 (2026-10-23 GitHub 기준) |
| 주 언어 | Kotlin (2026-10-23 GitHub 기준; 아래 집계에서도 물리 줄 수가 가장 큼) |
| 생성일 / 최근 push | 2026-09-24 / 2026-10-23 (2026-10-23 GitHub 기준) |
| 최신 릴리스 | 0.6.0 · 2026-10-22 (2026-10-23 GitHub 기준). 분석 커밋의 versionName도 0.6.0, versionCode는 15 |
| 분석 커밋 | ffa40a15d5655fb7fb2aa22cdf51d13f7ba95af1 · 2026-10-23. Wine ARM64 SIGUSR2 문맥 전환 최적화, PR #1371 |
| 코드 규모 | 아래 8개 언어·확장자 묶음 1,405파일, 288,163줄. 빈 줄·주석·테스트·외부 코드·생성 헤더 포함 |
| 추적 파일 | git ls-files 기준 2,007개. 이 숫자는 실행 코드 파일 수와 다름 |
| 테스트 규모의 예 | app/src/test/의 Kotlin 321파일·33,645줄, tools/tests/의 Python 109파일·20,079줄. 커버리지 수치가 아님 |
| 라이선스 | 루트 LICENSE와 2026-10-23 GitHub 기준 GPL-3.0. Rust 크레이트 선언은 GPL-3.0-or-later이며 외부 부품에는 개별 라이선스가 있음 |
| 자료의 적용 범위 | 커밋 ffa40a1의 소스·빌드·CI·검사 코드와 파일 규모. 실제 게임 호환성과 성능은 기기·게임별로 다름 |
| 언어·확장자 | 파일 수 | 물리 줄 수 |
|---|---|---|
| Kotlin(.kt) | 794 | 128,215 |
| Java(.java) | 27 | 7,974 |
| C(.c) | 109 | 30,668 |
| C++(.cpp / .cc) | 76 | 32,799 |
| C/C++ 헤더(.h / .hpp) | 183 | 48,177 |
| Rust(.rs) | 50 | 13,977 |
| Python(.py) | 141 | 24,497 |
| Shell(.sh) | 25 | 1,856 |
집계 대상은 Git 추적 파일 중 일반 파일이며, 개행 수에 마지막 줄의 개행이 없으면 한 줄을 더한 값입니다.
C·C++·헤더에는 app/src/main/cpp/thirdparty/, app/src/main/cpp/adrenotools/, Wayland 생성 코드 등이 포함됩니다.
확장자 없는 스크립트·XML·Gradle 파일은 위 합계에서 제외했습니다.
예를 들어 확장자 없는 tools/linuxfs/overlay/usr/local/bin/droiddeck-session은 별도로 1,267줄입니다.
이 수치를 프로젝트가 새로 작성한 코드량이나 품질 점수로 해석하면 안 됩니다.
# 저장소 루트에서 Kotlin 집계만 재현하는 읽기 예시
python3 - <<'PY'
from pathlib import Path
import subprocess
names = subprocess.check_output(["git", "ls-files", "-z"]).decode().split("\0")
files = [Path(n) for n in names if n.endswith(".kt") and Path(n).is_file()]
lines = 0
for path in files:
data = path.read_bytes()
lines += data.count(b"\n") + int(bool(data) and not data.endswith(b"\n"))
print(len(files), lines)
PY
# 커밋 ffa40a1 기준: 794 128215
README는 기기별 호환성과 성능이 다르고 하드웨어 검증이 제한적이라고 밝힙니다. 데스크톱 합성은 소프트웨어 렌더링을 사용하며 Firefox의 샌드박싱은 PRoot 아래에서 줄어든다고 설명합니다. 게임 화면의 하드웨어 렌더링 경로와 이 데스크톱 제한을 혼동하지 마세요.
Epic의 공식 Linux 런처 프리뷰를 x86-64 클라이언트와 FEX로 연결하는 경로는 README 스스로 Epic이 명시한 Linux 지원 범위 밖이라고 적습니다. 스토어 연결 코드가 있다는 사실이 해당 업체의 ARM 지원을 뜻하지 않습니다.
README는 독립적인 공식 웹사이트가 없으며 공식 팀을 사칭하는 사이트의 다운로드 링크를 사용하지 말라고 안내합니다. APK는 공식 GitHub Releases를 기준으로 확인하세요. Steam·Proton은 Valve 소유이며 이 프로젝트는 Valve와 제휴하지 않았다고 명시합니다.
이 글의 구현 설명은 커밋 ffa40a1 기준입니다. 릴리스 0.6.0과 이 커밋의 구현이 완전히 같다고 전제하거나, 검사 코드가 있다는 사실을 실제 검사 통과로 인용하지 마세요.