mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Lepton's Vulkan driver has no external memory or sync fd extensions, so the Steam Frame gets a native SteamOS build instead. Its backend is the PC's: OpenXR creates Dawn's own Vulkan instance and device, and the eyes are copied into the swapchain on Dawn's queue, with nothing shared between devices. - openxr_vulkan_win32 and Aurora's vulkan_win32_interop now compile on desktop Linux. Dawn links statically there, so the bridge calls the patched package's hooks directly, and exists only when the package's aurora-dawn.json declares the Vulkan hook ABI (AuroraDawnProvider turns AuroraVulkanAbi into AURORA_DAWN_VULKAN_HOOKS); a stock Dawn keeps the stubs and VR falls back to the desktop. - openxr_integration.cpp gains a Linux branch: XR_KHR_vulkan_enable2, the timespec clock, refresh rate, performance level, the Frame controllers and eye gaze; fragment density maps are requested on Linux as on the Quest (fdm.cpp and gpu.cpp, AURORA_FDM=0/1 overrides). - Controller motion uses XR_KHR_convert_timespec_time on any Linux. - CMake: Vulkan headers are fetched for Linux OpenXR builds, the headset option is MKW_HEADSET on Android and Linux, and Linux aarch64 builds take MKW_LINUX_CPU (cortex-x4 for the Steam Frame, else native). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
393 lines
18 KiB
Kotlin
393 lines
18 KiB
Kotlin
import javax.inject.Inject
|
|
import org.gradle.api.provider.Property
|
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
|
|
plugins {
|
|
id("com.android.application")
|
|
id("org.jetbrains.kotlin.android")
|
|
}
|
|
|
|
// Translator output for the disc the user owns (data_sections_init.cpp,
|
|
// RuntimeConfig.h, build_shards/shards.cmake). Never checked in.
|
|
val mkwGeneratedDir = providers.gradleProperty("mkwGeneratedDir").orNull
|
|
// Optional: a directory of already-fetched dependency sources (the installer's
|
|
// BuildWorkspace/Dependencies) so the native configure does not download them.
|
|
val mkwDependenciesDir = providers.gradleProperty("mkwDependenciesDir").orNull
|
|
// Optional: the Dawn package android/Build-QuestDawn.ps1 built with Aurora's patches (fragment density
|
|
// maps for foveated rendering). Without it the stock prebuilt Dawn is used and foveation is off.
|
|
val mkwQuestDawnDir = providers.gradleProperty("mkwQuestDawnDir").orNull
|
|
// Optional: a directory holding a second SDL3 AAR/prefab is not needed; the
|
|
// AAR in app/libs is produced by Prepare-QuestDependencies.ps1.
|
|
val mkwRepoRoot = rootProject.file("..").canonicalFile
|
|
|
|
// The runtime's read-only assets travel inside the APK and are unpacked by
|
|
// QuestActivity into the app's private storage on first launch.
|
|
val runtimeResources = layout.buildDirectory.dir("generated/assets/runtimeResources")
|
|
val prepareRuntimeResources by tasks.registering(Copy::class) {
|
|
val assets = File(mkwRepoRoot, "runtime/assets")
|
|
from(File(assets, "wii")) { into("runtime_resources/wii_bootstrap") }
|
|
from(File(assets, "dsp/dsp_coef.bin")) { into("runtime_resources") }
|
|
from(File(assets, "pipeline/initial_pipeline_cache.db")) { into("runtime_resources") }
|
|
into(runtimeResources)
|
|
}
|
|
|
|
// The disc the launcher accepts when it extracts the player's image: the game ID and
|
|
// the clean PAL main.dol / StaticR.rel hashes, read from the same recomp.yml pins the
|
|
// PC installer's payload manifest uses (Launcher/Build-Installer.ps1).
|
|
val discPins: Map<String, String> = run {
|
|
val manifest = File(mkwRepoRoot, "projects/mkwii/recomp.yml").readText()
|
|
fun pin(pattern: String): String =
|
|
Regex(pattern, RegexOption.DOT_MATCHES_ALL).find(manifest)?.groupValues?.get(1)
|
|
?: throw GradleException("projects/mkwii/recomp.yml has no match for $pattern")
|
|
mapOf(
|
|
"DISC_GAME_ID" to pin("""\n\s*game_id:\s*(\w+)"""),
|
|
"DISC_DOL_SHA256" to pin("""\n\s*dol:.*?sha256:\s*([0-9a-fA-F]{64})"""),
|
|
"DISC_REL_SHA256" to pin("""\n\s*rel:.*?sha256:\s*([0-9a-fA-F]{64})"""),
|
|
)
|
|
}
|
|
|
|
// Whether the translation this build is pointed at includes the Retro Rewind mod, which decides
|
|
// whether the kit can carry that product (emit-build-shards records it in shards.cmake).
|
|
val hasRetroRewindShards: Boolean = run {
|
|
val shards = File(mkwGeneratedDir ?: return@run false, "build_shards/shards.cmake")
|
|
shards.isFile && shards.readText().contains("set(MKW_HAVE_RETRO_REWIND_SHARDS ON)")
|
|
}
|
|
|
|
// Each headset flavour's AArch64 -mcpu target. The game kit records it, and the kit export, the APK
|
|
// audit (Build-Quest.ps1), the PC build (QuestGameKit.psm1), the on-headset build and the package
|
|
// import all refuse a game built for another one.
|
|
val headsetCpus = mapOf(
|
|
// Snapdragon XR2 and newer. Keep this as the default build target.
|
|
"modernQuest" to "cortex-a77",
|
|
// Snapdragon 835. cortex-a77 binaries terminate with SIGILL on Quest 1.
|
|
"quest1" to "kryo",
|
|
// Steam Frame: Snapdragon 8 Gen 3 (Cortex-X4, A720 and A520, all ARMv9.2). Its kernel exposes
|
|
// SVE and SVE2 (HWCAP, inside Lepton too), so the whole cortex-x4 feature set is safe.
|
|
"steamFrame" to "cortex-x4",
|
|
)
|
|
|
|
// android/nod-jni: nod, the disc image library the PC installer runs as nodtool,
|
|
// cross-compiled with cargo for the launcher's "Select disc image". Needs a Rust
|
|
// toolchain with the aarch64-linux-android target (see docs/quest-port.md).
|
|
val nodJniDir = rootProject.file("nod-jni")
|
|
val nodJniTargetDir = layout.buildDirectory.dir("nod-jni")
|
|
val nodJniLibs = layout.buildDirectory.dir("generated/jniLibs/nodJni")
|
|
val buildNodJni by tasks.registering(Exec::class) {
|
|
inputs.dir(File(nodJniDir, "src"))
|
|
inputs.files(File(nodJniDir, "Cargo.toml"), File(nodJniDir, "Cargo.lock"))
|
|
outputs.file(nodJniTargetDir.map { it.file("aarch64-linux-android/release/libnod_jni.so") })
|
|
workingDir = nodJniDir
|
|
val windows = System.getProperty("os.name").startsWith("Windows")
|
|
val cargoHome = System.getenv("CARGO_HOME")?.let(::File) ?: File(System.getProperty("user.home"), ".cargo")
|
|
val cargo = providers.gradleProperty("cargo").orNull
|
|
?: File(cargoHome, if (windows) "bin/cargo.exe" else "bin/cargo").takeIf { it.isFile }?.path
|
|
?: "cargo"
|
|
commandLine(cargo, "build", "--release", "--locked", "--target", "aarch64-linux-android")
|
|
doFirst {
|
|
val host = when {
|
|
windows -> "windows-x86_64"
|
|
System.getProperty("os.name").startsWith("Mac") -> "darwin-x86_64"
|
|
else -> "linux-x86_64"
|
|
}
|
|
val bin = File(androidComponents.sdkComponents.ndkDirectory.get().asFile, "toolchains/llvm/prebuilt/$host/bin")
|
|
val clang = File(bin, "aarch64-linux-android29-clang" + if (windows) ".cmd" else "").path
|
|
environment("CARGO_TARGET_DIR", nodJniTargetDir.get().asFile.path)
|
|
environment("CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER", clang)
|
|
environment("CC_aarch64_linux_android", clang)
|
|
environment("AR_aarch64_linux_android", File(bin, "llvm-ar" + if (windows) ".exe" else "").path)
|
|
}
|
|
}
|
|
val stageNodJni by tasks.registering(Copy::class) {
|
|
from(buildNodJni) { include("**/libnod_jni.so") }
|
|
eachFile { path = "arm64-v8a/$name" }
|
|
includeEmptyDirs = false
|
|
into(nodJniLibs)
|
|
}
|
|
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }.configureEach {
|
|
dependsOn(stageNodJni)
|
|
}
|
|
|
|
android {
|
|
namespace = "org.wiicompiled.quest"
|
|
compileSdk = 36
|
|
ndkVersion = "29.0.14206865"
|
|
|
|
defaultConfig {
|
|
applicationId = "org.wiicompiled.quest"
|
|
// Quest 2 ships Android 10 (API 29); AHardwareBuffer/Vulkan 1.1 need 26+.
|
|
minSdk = 29
|
|
targetSdk = 34
|
|
versionCode = 6
|
|
versionName = "0.6.0-quest"
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
for ((name, value) in discPins) {
|
|
buildConfigField("String", name, "\"$value\"")
|
|
}
|
|
// The APK carries game kits, not games: the player's libmain.so is built from their own
|
|
// disc and loaded from private storage (see android/QuestGameKit.psm1). One app offers
|
|
// both products, like the PC launcher, and the launcher's toggle picks which one to play.
|
|
buildConfigField("boolean", "EXTERNAL_GAME", "true")
|
|
buildConfigField("boolean", "ON_DEVICE_BUILD", "true")
|
|
|
|
ndk {
|
|
abiFilters += "arm64-v8a"
|
|
}
|
|
externalNativeBuild {
|
|
cmake {
|
|
// One probe per product the translation can build. Retro Rewind's exists only
|
|
// once translate-mod and emit-build-shards have run over the mod.
|
|
targets += "mkw_quest_kit_probe"
|
|
if (hasRetroRewindShards) {
|
|
targets += "mkw_quest_kit_probe_retro"
|
|
}
|
|
arguments += listOf(
|
|
"-DANDROID_STL=c++_shared",
|
|
"-DANDROID_PLATFORM=android-29",
|
|
"-DMKW_REPO_ROOT=${mkwRepoRoot.path.replace('\\', '/')}",
|
|
)
|
|
if (mkwGeneratedDir != null) {
|
|
arguments += "-DMKW_GENERATED_DIR=${File(mkwGeneratedDir).canonicalPath.replace('\\', '/')}"
|
|
}
|
|
if (mkwDependenciesDir != null) {
|
|
arguments += "-DMKW_DEPENDENCIES_DIR=${File(mkwDependenciesDir).canonicalPath.replace('\\', '/')}"
|
|
}
|
|
// Always passed, empty when absent, so the native build drops a previous override.
|
|
arguments += "-DMKW_QUEST_DAWN_PACKAGE_DIR=" +
|
|
(mkwQuestDawnDir?.let { File(it).canonicalPath.replace('\\', '/') } ?: "")
|
|
}
|
|
}
|
|
}
|
|
|
|
flavorDimensions += "headset"
|
|
productFlavors {
|
|
create("modernQuest") {
|
|
dimension = "headset"
|
|
manifestPlaceholders["mkwQuestSupportedDevices"] = "quest2|quest3|quest3s|questpro"
|
|
buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "false")
|
|
buildConfigField("boolean", "STEAM_FRAME", "false")
|
|
buildConfigField("String", "ANDROID_CPU", "\"${headsetCpus.getValue("modernQuest")}\"")
|
|
externalNativeBuild {
|
|
cmake {
|
|
arguments += "-DMKW_ANDROID_CPU=${headsetCpus.getValue("modernQuest")}"
|
|
}
|
|
}
|
|
}
|
|
create("quest1") {
|
|
dimension = "headset"
|
|
manifestPlaceholders["mkwQuestSupportedDevices"] = "quest|quest2"
|
|
buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "true")
|
|
buildConfigField("boolean", "STEAM_FRAME", "false")
|
|
buildConfigField("String", "ANDROID_CPU", "\"${headsetCpus.getValue("quest1")}\"")
|
|
externalNativeBuild {
|
|
cmake {
|
|
arguments += "-DMKW_ANDROID_CPU=${headsetCpus.getValue("quest1")}"
|
|
}
|
|
}
|
|
}
|
|
// Valve's Steam Frame, through Lepton (SteamOS's Android layer) and SteamVR's Android
|
|
// OpenXR runtime. Its manifest (src/steamFrame) drops the Horizon OS entries, and
|
|
// MKW_HEADSET gives the runtime the Frame's defaults (runtime_config.h).
|
|
create("steamFrame") {
|
|
dimension = "headset"
|
|
// Horizon OS only; src/steamFrame/AndroidManifest.xml removes the entry.
|
|
manifestPlaceholders["mkwQuestSupportedDevices"] = ""
|
|
buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "false")
|
|
buildConfigField("boolean", "STEAM_FRAME", "true")
|
|
buildConfigField("String", "ANDROID_CPU", "\"${headsetCpus.getValue("steamFrame")}\"")
|
|
externalNativeBuild {
|
|
cmake {
|
|
arguments += listOf(
|
|
"-DMKW_ANDROID_CPU=${headsetCpus.getValue("steamFrame")}",
|
|
"-DMKW_HEADSET=steam_frame",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
externalNativeBuild {
|
|
cmake {
|
|
// aurora-main needs CMake 3.25+, newer than the SDK's bundled 3.22.1;
|
|
// Build-Quest.ps1 points cmake.dir in local.properties at a system CMake.
|
|
path = file("src/main/cpp/CMakeLists.txt")
|
|
}
|
|
}
|
|
|
|
buildFeatures {
|
|
prefab = true
|
|
buildConfig = true
|
|
}
|
|
|
|
sourceSets.named("main") {
|
|
assets.srcDir(runtimeResources)
|
|
jniLibs.srcDir(nodJniLibs)
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
packaging {
|
|
jniLibs {
|
|
useLegacyPackaging = false
|
|
// Built only to produce each flavour's game kit; their objects travel as assets/game_kit.
|
|
excludes += "**/libmkw_quest_kit_probe*.so"
|
|
}
|
|
}
|
|
lint {
|
|
disable += setOf("ChromeOsAbiSupport", "DiscouragedApi")
|
|
}
|
|
}
|
|
|
|
tasks.named("preBuild") {
|
|
dependsOn(prepareRuntimeResources)
|
|
}
|
|
|
|
/**
|
|
* Exports the game kit (android/QuestGameKit.psm1) from the CMake tree this variant's native
|
|
* build just produced, into the variant's assets as game_kit/. The tree is found through the
|
|
* probe library: AGP gives the .cxx directory and the obj directory the same configuration hash.
|
|
*/
|
|
abstract class ExportQuestGameKit : DefaultTask() {
|
|
@get:OutputDirectory
|
|
abstract val outputDir: DirectoryProperty
|
|
|
|
@get:Internal
|
|
abstract val appDir: DirectoryProperty
|
|
|
|
@get:Internal
|
|
abstract val script: RegularFileProperty
|
|
|
|
@get:Internal
|
|
abstract val llvmStrip: RegularFileProperty
|
|
|
|
@get:Input
|
|
abstract val androidCpu: Property<String>
|
|
|
|
@get:Inject
|
|
abstract val execOperations: ExecOperations
|
|
|
|
init {
|
|
// The kit's inputs are CMake's outputs, which Gradle does not track; exporting takes seconds.
|
|
outputs.upToDateWhen { false }
|
|
}
|
|
|
|
@TaskAction
|
|
fun export() {
|
|
val app = appDir.get().asFile
|
|
data class Candidate(val probe: File, val binaryDir: File, val cpu: String?)
|
|
fun cacheValue(cache: String, name: String): String? =
|
|
Regex("(?m)^${Regex.escape(name)}(?::[^=\\r\\n]*)?=([^\\r\\n]*)$")
|
|
.find(cache)?.groupValues?.get(1)?.trim()
|
|
|
|
val candidates = File(app, "build/intermediates/cxx").walkTopDown()
|
|
.filter { it.name == "libmkw_quest_kit_probe.so" && it.parentFile.name == "arm64-v8a" }
|
|
.map { probe ->
|
|
val configuration = probe.parentFile.parentFile.parentFile // <Variant>/<hash>
|
|
val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
|
|
val cacheFile = File(binaryDir, "CMakeCache.txt")
|
|
val cache = cacheFile.takeIf { it.isFile }?.readText().orEmpty()
|
|
Candidate(probe, binaryDir, cacheValue(cache, "MKW_ANDROID_CPU"))
|
|
}
|
|
.toList()
|
|
val expectedCpu = androidCpu.get()
|
|
val matching = candidates.filter {
|
|
it.cpu == expectedCpu && File(it.binaryDir, "build.ninja").isFile
|
|
}
|
|
val chosen = matching.maxByOrNull { it.probe.lastModified() }
|
|
?: throw GradleException(
|
|
"No CMake tree for Android CPU $expectedCpu. Found: " +
|
|
candidates.joinToString { "${it.binaryDir} (cpu=${it.cpu})" }
|
|
)
|
|
val binaryDir = chosen.binaryDir
|
|
val kitDir = File(outputDir.get().asFile, "game_kit")
|
|
execOperations.exec {
|
|
commandLine(
|
|
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
|
|
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
|
|
"-AndroidCpu", expectedCpu,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The toolchain for building the game on the headset (android/Prepare-QuestToolchain.ps1): the
|
|
* translator for Android, clang/lld, and the pin of the NDK files the headset downloads. Packaged
|
|
* into the base variants' assets as quest_toolchain/.
|
|
*/
|
|
abstract class PrepareQuestToolchain : DefaultTask() {
|
|
@get:OutputDirectory
|
|
abstract val outputDir: DirectoryProperty
|
|
|
|
@get:InputFiles
|
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
|
abstract val sources: ConfigurableFileCollection
|
|
|
|
@get:Internal
|
|
abstract val script: RegularFileProperty
|
|
|
|
@get:Internal
|
|
abstract val ndkLlvm: DirectoryProperty
|
|
|
|
@get:Inject
|
|
abstract val execOperations: ExecOperations
|
|
|
|
@TaskAction
|
|
fun prepare() {
|
|
execOperations.exec {
|
|
commandLine(
|
|
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
|
|
"-OutputDir", File(outputDir.get().asFile, "quest_toolchain").path, "-NdkLlvm", ndkLlvm.get().asFile.path,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
androidComponents {
|
|
onVariants { variant ->
|
|
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
|
|
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
|
// The APK ships kits instead of games. AGP packages every library in the CMake output
|
|
// directory, so a game library left over from an earlier build would otherwise travel in it.
|
|
variant.packaging.jniLibs.excludes.add("**/libmain*.so")
|
|
val export = tasks.register<ExportQuestGameKit>("export${capitalized}QuestGameKit") {
|
|
dependsOn("merge${capitalized}NativeLibs")
|
|
appDir.set(layout.projectDirectory)
|
|
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
|
|
androidCpu.set(headsetCpus.getValue(checkNotNull(variant.flavorName) { "${variant.name} has no headset flavour" }))
|
|
outputDir.set(layout.buildDirectory.dir("generated/assets/questGameKit/${variant.name}"))
|
|
llvmStrip.set(sdkComponents.ndkDirectory.map {
|
|
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
|
|
})
|
|
}
|
|
variant.sources.assets?.addGeneratedSourceDirectory(export, ExportQuestGameKit::outputDir)
|
|
|
|
val prepareToolchain = tasks.register<PrepareQuestToolchain>("prepare${capitalized}QuestToolchain") {
|
|
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
|
|
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
|
|
sources.from(fileTree(File(mkwRepoRoot, "translator/src")) { exclude("**/bin/**", "**/obj/**") })
|
|
sources.from(fileTree(File(mkwRepoRoot, "Launcher/WiiCompiled.Setup.Common")) { exclude("**/bin/**", "**/obj/**") })
|
|
ndkLlvm.set(sdkComponents.ndkDirectory.map { it.dir("toolchains/llvm/prebuilt/$host") })
|
|
}
|
|
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
jvmTarget.set(JvmTarget.JVM_17)
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
// SDLActivity and libSDL3.so; the AAR is downloaded by Prepare-QuestDependencies.ps1.
|
|
implementation(files("libs/SDL3-3.4.4.aar"))
|
|
testImplementation("junit:junit:4.13.2")
|
|
// android.jar only stubs org.json; the GameBanana parsing tests need a real one.
|
|
testImplementation("org.json:json:20180813")
|
|
androidTestImplementation("androidx.test:runner:1.6.2")
|
|
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
|
}
|