Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/java/app/gamenative/PrefManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,11 @@ object PrefManager {
setPref(CUSTOM_GAME_MANUAL_FOLDERS, Json.encodeToString(value))
}

// Detailed per-step reporting on the boot splash; off means the plain indeterminate bar
private val VERBOSE_BOOT_PROGRESS = booleanPreferencesKey("verbose_boot_progress")
var verboseBootProgress: Boolean
get() = getPref(VERBOSE_BOOT_PROGRESS, false)
set(value) = setPref(VERBOSE_BOOT_PROGRESS, value)
private val FAVORITE_APP_IDS = stringPreferencesKey("favorite_app_ids")
var favoriteAppIds: Set<String>
get() {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/app/gamenative/events/AndroidEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface AndroidEvent<T> : Event<T> {
data class ShowGameFeedback(val appId: String) : AndroidEvent<Unit>
data class ShowLaunchingOverlay(val appName: String) : AndroidEvent<Unit>
data object HideLaunchingOverlay : AndroidEvent<Unit>
data class SetBootingSplashText(val text: String) : AndroidEvent<Unit>
data class SetBootingSplashText(val text: String, val progress: Float = -1f) : AndroidEvent<Unit>
data object ClearBootingSplash : AndroidEvent<Unit>
data class DownloadPausedDueToConnectivity(val appId: Int) : AndroidEvent<Unit>
data class DownloadStatusChanged(val appId: Int, val isDownloading: Boolean) : AndroidEvent<Unit>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,7 @@ fun PluviaMain(
BootingSplash(
visible = state.showBootingSplash,
text = state.bootingSplashText,
progress = state.bootingSplashProgress,
heroImageUrl = state.bootingSplashHeroImageUrl,
bootAd = state.bootAd,
onAbort = { viewModel.abortBoot() },
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/ui/data/MainState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ data class MainState(
val debugRun: Boolean = false,
val showBootingSplash: Boolean = false,
val bootingSplashText: String = "Booting...",
val bootingSplashProgress: Float = -1f,
val bootingSplashHeroImageUrl: String = "",
val bootAd: BootAdItem? = null,

Expand Down
25 changes: 23 additions & 2 deletions app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import app.gamenative.ui.data.MainState
import app.gamenative.ui.enums.ConnectionState
import app.gamenative.ui.screen.PluviaScreen
import app.gamenative.ui.util.SnackbarManager
import app.gamenative.utils.BootProgress
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.DebugReportUtils
import app.gamenative.utils.IntentLaunchManager
Expand Down Expand Up @@ -271,6 +272,7 @@ class MainViewModel @Inject constructor(

private val onSetBootingSplashText: (AndroidEvent.SetBootingSplashText) -> Unit = {
setBootingSplashText(it.text)
_state.update { state -> state.copy(bootingSplashProgress = it.progress) }
setShowBootingSplash(true)
}

Expand Down Expand Up @@ -351,6 +353,7 @@ class MainViewModel @Inject constructor(
PluviaApp.events.off<SteamEvent.LoggedOut, Unit>(onLoggedOut)
PluviaApp.events.off<AndroidEvent.ServiceReady, Unit>(onServiceReady)
connectionTimeoutJob?.cancel()
BootProgress.stop()
}

fun setTheme(value: AppTheme) {
Expand Down Expand Up @@ -382,6 +385,8 @@ class MainViewModel @Inject constructor(
}

fun setShowBootingSplash(value: Boolean) {
// Single choke point for every dismissal path, so boot reporting can never outlive the splash.
if (!value) BootProgress.stop()
val wasShowing = _state.value.showBootingSplash
if (value && !wasShowing) {
// The splash hides and re-shows between boot phases; a quick re-show is the same
Expand Down Expand Up @@ -428,10 +433,26 @@ class MainViewModel @Inject constructor(
}
// bootAd stays in state so the exit fade keeps rendering it; the next show replaces it.
PluviaApp.isBootingSplashShowing = false
_state.update { it.copy(showBootingSplash = false) }
_state.update {
it.copy(
showBootingSplash = false,
bootingSplashText = MainState().bootingSplashText,
bootingSplashProgress = MainState().bootingSplashProgress,
)
}
} else {
PluviaApp.isBootingSplashShowing = value
_state.update { it.copy(showBootingSplash = value) }
_state.update {
if (value) {
it.copy(showBootingSplash = true)
} else {
it.copy(
showBootingSplash = false,
bootingSplashText = MainState().bootingSplashText,
bootingSplashProgress = MainState().bootingSplashProgress,
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ fun SettingsGroupDebug() {
var enableBox86Logs by rememberSaveable { mutableStateOf(
if (isPreview) false else WinlatorPrefManager.getBoolean("enable_box86_64_logs", false)
) }
var verboseBootProgress by rememberSaveable {
mutableStateOf(if (isPreview) false else PrefManager.verboseBootProgress)
}
var latestCrashFile: File? by rememberSaveable { mutableStateOf(null) }
LaunchedEffect(Unit) {
val crashDir = File(context.getExternalFilesDir(null), "crash_logs")
Expand Down Expand Up @@ -222,6 +225,18 @@ fun SettingsGroupDebug() {
}
},
)
SettingsSwitch(
colors = settingsTileColorsAlt(),
state = verboseBootProgress,
title = { Text(text = stringResource(R.string.settings_debug_verbose_boot_title)) },
subtitle = { Text(text = stringResource(R.string.settings_debug_verbose_boot_subtitle)) },
onCheckedChange = {
verboseBootProgress = it
if (!isPreview) {
PrefManager.verboseBootProgress = it
}
},
)
SettingsMenuLink(
colors = settingsTileColors(),
title = { Text(text = stringResource(R.string.settings_debug_view_crash_title)) },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package app.gamenative.ui.screen.xserver

import android.content.Context
import app.gamenative.PluviaApp
import app.gamenative.data.GameSource
import app.gamenative.events.AndroidEvent
import app.gamenative.service.SteamService
import app.gamenative.service.amazon.AmazonService
import app.gamenative.service.epic.EpicService
import app.gamenative.service.gog.GOGService
import app.gamenative.utils.BootProgress
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.CustomGameScanner
import app.gamenative.utils.FileUtils
Expand Down Expand Up @@ -134,7 +133,7 @@ object XAudioUtils {
return
}

PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Extracting XAudio DLLs..."))
BootProgress.detail("extracting XAudio DLLs", legacy = "Extracting XAudio DLLs...")

val batFile = File(tempDir, "extract_dx_audio_dlls.bat")
val batContent = buildCabarcBatchScript(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import app.gamenative.ui.component.QuickMenu
import app.gamenative.ui.component.QuickMenuAction
import app.gamenative.ui.component.SteamInviteState
import app.gamenative.ui.component.parseBooleanExtra
import app.gamenative.utils.BootProgress
import app.gamenative.ui.component.parsePositiveFpsLimit
import app.gamenative.ui.data.PerformanceHudConfig
import app.gamenative.ui.data.PerformanceHudSize
Expand Down Expand Up @@ -2274,6 +2275,7 @@ fun XServerScreen(

setupExecutor.submit {
try {
BootProgress.start()
val containerManager = ContainerManager(context)
// Configure WinHandler with container's input API settings
val handler = getxServer().winHandler
Expand Down Expand Up @@ -2356,6 +2358,7 @@ fun XServerScreen(
val envVars = EnvVars()
immersiveHooks?.windowsVr?.beforeWineSystemSetup(container)

BootProgress.phase(BootProgress.Phase.WINE_FILES)
runBlocking {
setupWineSystemFiles(
context,
Expand All @@ -2372,6 +2375,7 @@ fun XServerScreen(
extractArm64ecInputDLLs(context, container) // REQUIRED: Uses updated xinput1_3 main.c from x86_64 build, prevents crashes with 3+ players, avoids need for input shim dlls.
extractx86_64InputDlls(context, container)

BootProgress.phase(BootProgress.Phase.GRAPHICS)
runBlocking {
extractGraphicsDriverFiles(
context,
Expand Down Expand Up @@ -3880,18 +3884,14 @@ private fun runSteamHostCegPass(
progressFile.delete()
val batch = File(imageFs.wineprefix, "drive_c/steamhost_ceg.bat")
batch.writeText("@\"C:\\Program Files (x86)\\Steam\\steam.exe\"\r\n")
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Preparing game executable..."))
// Headless Steam never enters DRM through Steamless, so the CEG pass is this boot's DRM step.
BootProgress.phase(BootProgress.Phase.DRM, "Steam: signing in", legacy = "Preparing game executable...")
launcher.envVars.put("STEAMHOST_CEG_ONLY", "1")
val passStartedAt = System.currentTimeMillis()
val pollProgress = AtomicBoolean(true)
thread(name = "steamhost-ceg-progress") {
var lastText = ""
while (pollProgress.get()) {
val text = cegSplashText(progressFile, passStartedAt)
if (text != lastText) {
lastText = text
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText(text))
}
reportCegProgress(progressFile, passStartedAt)
Thread.sleep(500)
}
}
Expand All @@ -3914,11 +3914,20 @@ private fun runSteamHostCegPass(
}
}

private fun cegSplashText(progressFile: File, startedAt: Long): String {
/**
* Puts the steamhost CEG progress file on the splash through [BootProgress]. The legacy text is
* what this pass showed before detailed progress existed; the detailed one drops the elapsed
* seconds because BootProgress shows its own timer.
*/
private fun reportCegProgress(progressFile: File, startedAt: Long) {
val elapsed = (System.currentTimeMillis() - startedAt) / 1000
val fields = runCatching { progressFile.readText().trim().split(' ') }.getOrNull()
if (fields == null || fields.size < 6) {
return "Preparing game executable... signing in to Steam (${elapsed}s)"
BootProgress.detail(
"Steam: signing in",
legacy = "Preparing game executable... signing in to Steam (${elapsed}s)",
)
return
}
val jobsDone = fields[1].toIntOrNull() ?: 0
val jobs = fields[2].toIntOrNull() ?: 0
Expand All @@ -3930,7 +3939,12 @@ private fun cegSplashText(progressFile: File, startedAt: Long): String {
"waiting for Steam's DRM service"
}
val files = if (jobs > 1) ", file ${(jobsDone + 1).coerceAtMost(jobs)} of $jobs" else ""
return "Preparing game executable... $progress$files (${elapsed}s)"
val legacy = "Preparing game executable... $progress$files (${elapsed}s)"
if (total > 0) {
BootProgress.update(bytes.toFloat() / total, "Steam: $progress$files", legacy = legacy)
} else {
BootProgress.detail("Steam: $progress$files", legacy = legacy)
}
}

private fun setupXEnvironment(
Expand All @@ -3950,6 +3964,7 @@ private fun setupXEnvironment(
offline: Boolean = false,
immersiveHooks: app.gamenative.ui.screen.xr.ImmersiveSessionHooks? = null,
): XEnvironment {
BootProgress.phase(BootProgress.Phase.ENVIRONMENT)
ProcessHelper.hardKillStaleWineProcesses()

val gameSource = ContainerUtils.extractGameSourceFromContainerId(appId)
Expand Down Expand Up @@ -4149,9 +4164,9 @@ private fun setupXEnvironment(
runSteamHostCegPass(appId, imageFs, guestProgramLauncherComponent, onGameLaunchError)
}
if (preInstallCommands.isNotEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
BootProgress.phase(BootProgress.Phase.PREREQS, "1/${preInstallCommands.size}")
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
BootProgress.phase(BootProgress.Phase.LAUNCH)
}
}

Expand Down Expand Up @@ -4248,9 +4263,10 @@ private fun setupXEnvironment(
}
val nextRemaining = remaining.drop(1)
if (nextRemaining.isEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
BootProgress.phase(BootProgress.Phase.LAUNCH)
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
val step = preInstallCommands.size - nextRemaining.size + 1
BootProgress.phase(BootProgress.Phase.PREREQS, "$step/${preInstallCommands.size}")
}
chainPreInstallSteps(nextRemaining)
guestProgramLauncherComponent.start()
Expand Down Expand Up @@ -5107,7 +5123,9 @@ private fun unpackExecutableFile(
var output = StringBuilder()
if (needsUnpacking || containerVariantChanged){
try {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing Mono..."))
BootProgress.phase(BootProgress.Phase.MONO)
// msiexec reports nothing back, so track the prefix directory it writes into instead.
BootProgress.watchOutput(File(imageFs.wineprefix, "drive_c/windows/mono"))
val monoCmd = "wine msiexec /i Z:\\opt\\mono-gecko-offline\\wine-mono-11.0.0-x86.msi && wineserver -k"
Timber.i("Install mono command $monoCmd")
val monoOutput = guestProgramLauncherComponent.execShellCommand(monoCmd)
Expand Down Expand Up @@ -5138,7 +5156,7 @@ private fun unpackExecutableFile(

try {
if (!container.isLaunchRealSteam && !container.isLaunchBionicSteam) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM..."))
BootProgress.phase(BootProgress.Phase.DRM, "reading interfaces")
}
// a:/.../GameDir/orig_dll_path.txt (same dir as the EXE inside A:)
val origTxtFile = File("${imageFs.wineprefix}/dosdevices/a:/orig_dll_path.txt")
Expand Down Expand Up @@ -5203,11 +5221,13 @@ private fun unpackExecutableFile(
if (exePaths.isEmpty()) {
Timber.w("No executable path set, skipping Steamless")
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM..."))
BootProgress.phase(BootProgress.Phase.DRM)
for ((index, executablePath) in exePaths.withIndex()) {
if (exePaths.size > 1) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM (${index + 1}/${exePaths.size})"))
}
BootProgress.update(
index.toFloat() / exePaths.size,
"${index + 1}/${exePaths.size}: ${extractExecutableBasename(executablePath)}",
legacy = "Handling DRM (${index + 1}/${exePaths.size})".takeIf { exePaths.size > 1 },
)
var batchFile: File? = null
try {
// Normalize path: use forward slashes for Unix format, backslashes for Windows
Expand Down Expand Up @@ -5419,7 +5439,7 @@ private suspend fun setupWineSystemFiles(

// Download or use cached/bundled openal component
val openalFile = WinComponentDownloader.ensureWinComponentAvailable(context, "openal") { progress ->
Timber.d("Downloading openal component: ${(progress * 100).toInt()}%")
BootProgress.download("OpenAL", progress)
}

if (openalFile == null) {
Expand Down Expand Up @@ -5561,7 +5581,7 @@ private suspend fun extractGraphicsDriverComponent(
onExtractFileListener: OnExtractFileListener? = null
) {
val componentFile = GraphicsDriverDownloader.ensureGraphicsDriverAvailable(context, componentId) { progress ->
Timber.d("Downloading graphics driver $componentId: ${(progress * 100).toInt()}%")
BootProgress.download("graphics driver $componentId", progress)
}

if (componentFile == null) {
Expand Down Expand Up @@ -5597,7 +5617,7 @@ private suspend fun extractDXWrapperComponent(
onExtractFileListener: OnExtractFileListener?
) {
val componentFile = DXWrapperDownloader.ensureDXWrapperAvailable(context, componentId) { progress ->
Timber.d("Downloading dxwrapper $componentId: ${(progress * 100).toInt()}%")
BootProgress.download("dxwrapper $componentId", progress)
}

if (componentFile == null) {
Expand Down Expand Up @@ -5834,7 +5854,7 @@ private suspend fun extractWinComponentFiles(
val componentFile = WinComponentDownloader.ensureWinComponentAvailable(
context, identifier
) { progress ->
Timber.d("Downloading wincomponent $identifier: ${(progress * 100).toInt()}%")
BootProgress.download("component $identifier", progress)
}

if (componentFile == null) {
Expand Down Expand Up @@ -5992,7 +6012,7 @@ private suspend fun extractGraphicsDriverFiles(

// Download or get cached core driver
val driverFile = CoreDriverDownloader.ensureCoreDriverAvailable(context, assetZip) { progress ->
Timber.d("Downloading core driver $assetZip: ${(progress * 100).toInt()}%")
BootProgress.download("core driver $assetZip", progress)
}

// Read manifest name from zip to determine folder name
Expand Down
Loading
Loading