diff --git a/app/src/main/assets/bionic_fg/android_arm64_v8a/VkLayer_BIONIC_framegen.json b/app/src/main/assets/bionic_fg/android_arm64_v8a/VkLayer_BIONIC_framegen.json new file mode 100644 index 0000000000..0e3702cff3 --- /dev/null +++ b/app/src/main/assets/bionic_fg/android_arm64_v8a/VkLayer_BIONIC_framegen.json @@ -0,0 +1,21 @@ +{ + "file_format_version": "1.0.0", + "layer": { + "name": "VK_LAYER_BIONIC_framegen", + "type": "GLOBAL", + "api_version": "1.3.0", + "library_path": "../../../lib/libbionic-fg-layer.so", + "implementation_version": "2", + "description": "Bionic FG standalone frame generation", + "functions": { + "vkGetInstanceProcAddr": "BionicFG_GetInstanceProcAddr", + "vkGetDeviceProcAddr": "BionicFG_GetDeviceProcAddr" + }, + "enable_environment": { + "BIONIC_FG_ENABLE": "1" + }, + "disable_environment": { + "DISABLE_BIONIC_FG": "1" + } + } +} diff --git a/app/src/main/assets/bionic_fg/android_arm64_v8a/libbionic-fg-layer.so b/app/src/main/assets/bionic_fg/android_arm64_v8a/libbionic-fg-layer.so new file mode 100755 index 0000000000..4d481e798e Binary files /dev/null and b/app/src/main/assets/bionic_fg/android_arm64_v8a/libbionic-fg-layer.so differ diff --git a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt index 7b170cf32a..858b91c2f1 100644 --- a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt +++ b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt @@ -115,6 +115,7 @@ private object QuickMenuTab { const val EFFECTS = 2 const val CONTROLLER = 3 const val TOOLS = 4 + const val BIONIC_FG = 5 } data class QuickMenuItem( @@ -254,6 +255,15 @@ fun QuickMenu( isTouchscreenModeActive: Boolean = false, onTouchGestureSettingsClick: () -> Unit = {}, activeToggleIds: Set = emptySet(), + frameGenerationMultiplier: Int = 0, + // Bionic-FG hot-reload state (tab only visible when isBionicFgAvailable) + isBionicFgAvailable: Boolean = false, + bionicFgMultiplier: Int = 2, + bionicFgFlowScale: Float = 0.80f, + bionicFgModel: String = "0", + onBionicFgMultiplierChanged: (Int) -> Unit = {}, + onBionicFgFlowScaleChanged: (Float) -> Unit = {}, + onBionicFgModelChanged: (String) -> Unit = {}, // LSFG hot-reload state (tab only visible when isLsfgAvailable) isLsfgAvailable: Boolean = false, lsfgMultiplier: Int = 2, @@ -326,13 +336,16 @@ fun QuickMenu( var selectedTab by rememberSaveable { mutableIntStateOf( - if (PrefManager.quickMenuLastTab == QuickMenuTab.LSFG && !isLsfgAvailable) - QuickMenuTab.HUD - else PrefManager.quickMenuLastTab + when { + PrefManager.quickMenuLastTab == QuickMenuTab.BIONIC_FG && !isBionicFgAvailable -> QuickMenuTab.HUD + PrefManager.quickMenuLastTab == QuickMenuTab.LSFG && !isLsfgAvailable -> QuickMenuTab.HUD + else -> PrefManager.quickMenuLastTab + } ) } val selectedTabLabelResId = when (selectedTab) { QuickMenuTab.HUD -> R.string.performance_hud + QuickMenuTab.BIONIC_FG -> R.string.bionic_fg_tab_title QuickMenuTab.LSFG -> R.string.lsfg_tab_title QuickMenuTab.EFFECTS -> R.string.screen_effects QuickMenuTab.TOOLS -> R.string.task_manager @@ -340,15 +353,18 @@ fun QuickMenu( } val hudScrollState = rememberScrollState() + val bionicFgScrollState = rememberScrollState() val effectsScrollState = rememberScrollState() val lsfgScrollState = rememberScrollState() val effectsTabFocusRequester = remember { FocusRequester() } val controllerScrollState = rememberScrollState() + val bionicFgTabFocusRequester = remember { FocusRequester() } val lsfgTabFocusRequester = remember { FocusRequester() } val hudTabFocusRequester = remember { FocusRequester() } val controllerTabFocusRequester = remember { FocusRequester() } val toolsTabFocusRequester = remember { FocusRequester() } val hudItemFocusRequester = remember { FocusRequester() } + val bionicFgItemFocusRequester = remember { FocusRequester() } val effectsItemFocusRequester = remember { FocusRequester() } val controllerItemFocusRequester = remember { FocusRequester() } val toolsItemFocusRequester = remember { FocusRequester() } @@ -462,6 +478,20 @@ fun QuickMenu( modifier = Modifier.width(56.dp), focusRequester = hudTabFocusRequester, ) + if (isBionicFgAvailable) { + QuickMenuTabButton( + icon = Icons.Default.Speed, + contentDescriptionResId = R.string.bionic_fg_tab_title, + selected = selectedTab == QuickMenuTab.BIONIC_FG, + accentColor = PluviaTheme.colors.accentPurple, + onSelected = { + selectedTab = QuickMenuTab.BIONIC_FG + PrefManager.quickMenuLastTab = selectedTab + }, + modifier = Modifier.width(56.dp), + focusRequester = bionicFgTabFocusRequester, + ) + } if (isLsfgAvailable) { QuickMenuTabButton( icon = Icons.Default.Speed, @@ -565,7 +595,7 @@ fun QuickMenu( fpsLimiterEnabled = fpsLimiterEnabled, fpsLimiterTarget = fpsLimiterTarget, fpsLimiterMax = fpsLimiterMax, - lsfgMultiplier = if (isLsfgAvailable) lsfgMultiplier else 0, + frameGenerationMultiplier = frameGenerationMultiplier, onTogglePerformanceHud = { onItemSelected(QuickMenuAction.PERFORMANCE_HUD) }, @@ -578,6 +608,20 @@ fun QuickMenu( ) } + QuickMenuTab.BIONIC_FG -> { + BionicFgQuickMenuTab( + multiplier = bionicFgMultiplier, + flowScale = bionicFgFlowScale, + model = bionicFgModel, + onMultiplierChanged = onBionicFgMultiplierChanged, + onFlowScaleChanged = onBionicFgFlowScaleChanged, + onModelChanged = onBionicFgModelChanged, + scrollState = bionicFgScrollState, + focusRequester = bionicFgItemFocusRequester, + modifier = Modifier.fillMaxSize(), + ) + } + QuickMenuTab.LSFG -> { LsfgQuickMenuTab( multiplier = lsfgMultiplier, @@ -672,6 +716,7 @@ fun QuickMenu( try { when (selectedTab) { QuickMenuTab.HUD -> hudItemFocusRequester.requestFocus() + QuickMenuTab.BIONIC_FG -> bionicFgItemFocusRequester.requestFocus() QuickMenuTab.LSFG -> lsfgItemFocusRequester.requestFocus() QuickMenuTab.EFFECTS -> effectsItemFocusRequester.requestFocus() QuickMenuTab.TOOLS -> toolsItemFocusRequester.requestFocus() @@ -741,7 +786,7 @@ private fun PerformanceHudQuickMenuTab( fpsLimiterEnabled: Boolean, fpsLimiterTarget: Int, fpsLimiterMax: Int, - lsfgMultiplier: Int, + frameGenerationMultiplier: Int, onTogglePerformanceHud: () -> Unit, onPerformanceHudConfigChanged: (PerformanceHudConfig) -> Unit, onFpsLimiterEnabledChanged: (Boolean) -> Unit, @@ -759,22 +804,22 @@ private fun PerformanceHudQuickMenuTab( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ── FPS Limiter (topmost) ──────────────────────────────────────── - val limiterControlledByLsfg = lsfgMultiplier >= 2 + val limiterControlledByFrameGeneration = frameGenerationMultiplier >= 2 QuickMenuToggleRow( title = stringResource(R.string.performance_hud_fps_limiter), - subtitle = if (limiterControlledByLsfg) { - stringResource(R.string.performance_hud_fps_limiter_lsfg_override) + subtitle = if (limiterControlledByFrameGeneration) { + stringResource(R.string.performance_hud_fps_limiter_frame_generation_override) } else null, - enabled = fpsLimiterEnabled && !limiterControlledByLsfg, + enabled = fpsLimiterEnabled && !limiterControlledByFrameGeneration, onToggle = { - if (!limiterControlledByLsfg) onFpsLimiterEnabledChanged(!fpsLimiterEnabled) + if (!limiterControlledByFrameGeneration) onFpsLimiterEnabledChanged(!fpsLimiterEnabled) }, accentColor = accentColor, focusRequester = focusRequester, ) AnimatedVisibility( - visible = fpsLimiterEnabled && !limiterControlledByLsfg, + visible = fpsLimiterEnabled && !limiterControlledByFrameGeneration, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(), ) { @@ -1080,6 +1125,104 @@ private fun PerformanceHudQuickMenuTab( } } +@Composable +private fun BionicFgQuickMenuTab( + multiplier: Int, + flowScale: Float, + model: String, + onMultiplierChanged: (Int) -> Unit, + onFlowScaleChanged: (Float) -> Unit, + onModelChanged: (String) -> Unit, + scrollState: ScrollState, + focusRequester: FocusRequester? = null, + modifier: Modifier = Modifier, +) { + val accentColor = PluviaTheme.colors.accentPurple + val selectedModel = if (model == "1") "1" else "0" + val isEnabled = multiplier >= 2 + + Column( + modifier = modifier + .verticalScroll(scrollState) + .focusGroup(), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + QuickMenuSectionHeader( + title = stringResource(R.string.bionic_fg_multiplier), + ) + Row( + modifier = Modifier.padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + listOf(0, 2, 3, 4).forEach { value -> + QuickMenuChoiceChip( + text = if (value == 0) "Off" else "${value}x", + selected = multiplier == value || (value == 0 && multiplier < 2), + accentColor = accentColor, + onClick = { onMultiplierChanged(value) }, + modifier = Modifier.width(56.dp), + focusRequester = if (value == 0) focusRequester else null, + ) + } + } + + AnimatedVisibility( + visible = isEnabled, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Spacer(modifier = Modifier.height(4.dp)) + + QuickMenuAdjustmentRow( + title = stringResource(R.string.bionic_fg_flow_scale), + valueText = String.format(java.util.Locale.US, "%.2f", flowScale), + progress = (flowScale - 0.25f) / 0.75f, + onDecrease = { + val next = (flowScale - 0.05f).coerceIn(0.25f, 1.0f) + onFlowScaleChanged(String.format(java.util.Locale.US, "%.2f", next).toFloat()) + }, + onIncrease = { + val next = (flowScale + 0.05f).coerceIn(0.25f, 1.0f) + onFlowScaleChanged(String.format(java.util.Locale.US, "%.2f", next).toFloat()) + }, + accentColor = accentColor, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + QuickMenuSectionHeader( + title = stringResource(R.string.bionic_fg_model), + subtitle = if (selectedModel == "1") { + stringResource(R.string.bionic_fg_model_1) + } else { + stringResource(R.string.bionic_fg_model_0) + }, + ) + Row( + modifier = Modifier.padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + QuickMenuChoiceChip( + text = "M0", + selected = selectedModel == "0", + accentColor = accentColor, + onClick = { onModelChanged("0") }, + ) + QuickMenuChoiceChip( + text = "M1", + selected = selectedModel == "1", + accentColor = accentColor, + onClick = { onModelChanged("1") }, + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + } +} + @Composable private fun LsfgQuickMenuTab( multiplier: Int, diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt index 0f7232e1be..9e24e17bb2 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt @@ -2,7 +2,6 @@ package app.gamenative.ui.component.dialog import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,6 +17,7 @@ import app.gamenative.ui.component.settings.SettingsListDropdown import app.gamenative.ui.component.settings.SettingsMultiListDropdown import app.gamenative.ui.theme.settingsTileColors import app.gamenative.ui.theme.settingsTileColorsAlt +import app.gamenative.utils.BionicFgManager import app.gamenative.utils.LsfgVkManager import com.alorma.compose.settings.ui.SettingsGroup import com.alorma.compose.settings.ui.SettingsSwitch @@ -325,6 +325,8 @@ fun GraphicsTabContent(state: ContainerConfigState, default: Boolean = false) { // with a Vortek/Adreno graphics driver. if (!default) LsfgSection(state) + if (!default) BionicFgSection(state) + SettingsSwitch( colors = settingsTileColorsAlt(), title = { Text(text = stringResource(R.string.use_dri3)) }, @@ -337,6 +339,68 @@ fun GraphicsTabContent(state: ContainerConfigState, default: Boolean = false) { } } +@Composable +private fun BionicFgSection(state: ContainerConfigState) { + val config = state.config.value + val bionicFgSupported = config.containerVariant.equals(Container.BIONIC, ignoreCase = true) + if (!bionicFgSupported) return + + SettingsGroup { + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.bionic_fg_enable)) }, + subtitle = { Text(text = stringResource(R.string.bionic_fg_description)) }, + state = config.bionicFgEnabled, + onCheckedChange = { enabled -> + state.config.value = config.copy(bionicFgEnabled = enabled) + }, + ) + + if (config.bionicFgEnabled) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text(text = stringResource(R.string.bionic_fg_multiplier)) + Slider( + value = config.bionicFgMultiplier.coerceIn(2, 4).toFloat(), + onValueChange = { newValue -> + val clamped = newValue.roundToInt().coerceIn(2, 4) + state.config.value = config.copy(bionicFgMultiplier = clamped) + }, + valueRange = 2f..4f, + steps = 1, + ) + Text(text = if (config.bionicFgMultiplier < 2) "Off" else "${config.bionicFgMultiplier}x") + } + + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text(text = stringResource(R.string.bionic_fg_flow_scale)) + Slider( + value = config.bionicFgFlowScale, + onValueChange = { newValue -> + val clamped = newValue.coerceIn(0.25f, 1.0f) + state.config.value = config.copy(bionicFgFlowScale = clamped) + }, + valueRange = 0.25f..1.0f, + ) + Text(text = String.format(java.util.Locale.US, "%.2f", config.bionicFgFlowScale)) + } + + val modelOptions = listOf( + stringResource(R.string.bionic_fg_model_0), + stringResource(R.string.bionic_fg_model_1) + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.bionic_fg_model)) }, + value = if (config.bionicFgModel == "1") 1 else 0, + items = modelOptions, + onItemSelected = { idx -> + state.config.value = config.copy(bionicFgModel = if (idx == 1) "1" else "0") + }, + ) + } + } +} + @Composable private fun DxWrapperSection(state: ContainerConfigState) { val config = state.config.value diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 9c914293f1..c5d83b957d 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -100,6 +100,7 @@ import app.gamenative.ui.data.PerformanceHudConfig import app.gamenative.ui.data.PerformanceHudSize import app.gamenative.ui.data.XServerState import app.gamenative.ui.widget.PerformanceHudView +import app.gamenative.utils.BionicFgQuickMenuHelper import app.gamenative.utils.ContainerUtils import app.gamenative.utils.CustomGameScanner import app.gamenative.utils.ExecutableSelectionUtils @@ -443,6 +444,13 @@ fun XServerScreen( var fpsLimiterEnabled by rememberSaveable(container.id) { mutableStateOf(initialFpsLimiterEnabled(container)) } var fpsLimiterTarget by rememberSaveable(container.id) { mutableIntStateOf(initialFpsLimiterTarget(container)) } + // Bionic-FG tab in QuickMenu only visible when enabled in container settings + val isBionicFgAvailable = BionicFgQuickMenuHelper.isAvailable(container) + val initialBionicFgSettings = remember(container.id) { BionicFgQuickMenuHelper.readSettings(container) } + var bionicFgMultiplier by rememberSaveable(container.id) { mutableIntStateOf(initialBionicFgSettings.multiplier) } + var bionicFgFlowScale by rememberSaveable(container.id) { mutableStateOf(initialBionicFgSettings.flowScale) } + var bionicFgModel by rememberSaveable(container.id) { mutableStateOf(initialBionicFgSettings.model) } + // LSFG tab in QuickMenu only visible when enabled in container settings val isLsfgAvailable = LsfgQuickMenuHelper.isAvailable(container) val initialLsfgSettings = remember(container.id) { LsfgQuickMenuHelper.readSettings(container) } @@ -530,8 +538,14 @@ fun XServerScreen( ?.setFrameRateLimit(limit) } + fun activeFrameGenerationMultiplier(): Int = when { + isBionicFgAvailable && bionicFgMultiplier >= 2 -> bionicFgMultiplier + isLsfgAvailable && lsfgMultiplier >= 2 -> lsfgMultiplier + else -> 0 + } + fun effectiveFpsLimit(): Int = - if (isLsfgAvailable && lsfgMultiplier >= 2) 0 + if (activeFrameGenerationMultiplier() >= 2) 0 else if (fpsLimiterEnabled) fpsLimiterTarget else 0 @@ -550,6 +564,30 @@ fun XServerScreen( persistFpsLimiterState() } + fun applyBionicFgSettings() { + BionicFgQuickMenuHelper.applySettings( + container, + BionicFgQuickMenuHelper.Settings(bionicFgMultiplier, bionicFgFlowScale, bionicFgModel), + ) + } + + fun applyBionicFgMultiplier(mult: Int) { + bionicFgMultiplier = BionicFgQuickMenuHelper.sanitizeMultiplier(mult) + applyBionicFgSettings() + applyFpsLimiterToEngines(effectiveFpsLimit()) + } + + fun applyBionicFgFlowScale(scale: Float) { + bionicFgFlowScale = BionicFgQuickMenuHelper.sanitizeFlowScale(scale) + applyBionicFgSettings() + } + + fun applyBionicFgModel(model: String) { + bionicFgModel = BionicFgQuickMenuHelper.sanitizeModel(model) + applyBionicFgSettings() + applyFpsLimiterToEngines(effectiveFpsLimit()) + } + fun applyLsfgSettings() { LsfgQuickMenuHelper.applySettings( container, @@ -654,9 +692,7 @@ fun XServerScreen( val hud = PerformanceHudView( context = context, fpsProvider = { - val raw = frameRating?.currentFPS ?: 0f - val mult = if (isLsfgAvailable && lsfgMultiplier >= 2) lsfgMultiplier else 1 - raw * mult + frameRating?.currentFPS ?: 0f }, initialConfig = performanceHudConfig, initialCompactMode = PrefManager.performanceHudCompactMode, @@ -2395,6 +2431,15 @@ fun XServerScreen( if (isTouchscreenModeActive) add(QuickMenuAction.TOUCHSCREEN_MODE) if (isDisableMouseInput) add(QuickMenuAction.DISABLE_MOUSE) }, + frameGenerationMultiplier = activeFrameGenerationMultiplier(), + // Bionic-FG hot-reload (tab only visible when enabled in container settings) + isBionicFgAvailable = isBionicFgAvailable, + bionicFgMultiplier = bionicFgMultiplier, + bionicFgFlowScale = bionicFgFlowScale, + bionicFgModel = bionicFgModel, + onBionicFgMultiplierChanged = ::applyBionicFgMultiplier, + onBionicFgFlowScaleChanged = ::applyBionicFgFlowScale, + onBionicFgModelChanged = ::applyBionicFgModel, // LSFG hot-reload (tab only visible when enabled in container settings) isLsfgAvailable = isLsfgAvailable, lsfgMultiplier = lsfgMultiplier, diff --git a/app/src/main/java/app/gamenative/utils/BionicFgManager.kt b/app/src/main/java/app/gamenative/utils/BionicFgManager.kt new file mode 100644 index 0000000000..fd63495cda --- /dev/null +++ b/app/src/main/java/app/gamenative/utils/BionicFgManager.kt @@ -0,0 +1,235 @@ +package app.gamenative.utils + +import android.content.Context +import com.winlator.container.Container +import com.winlator.core.FileUtils +import com.winlator.core.envvars.EnvVars +import java.io.File +import java.util.Locale +import timber.log.Timber +import kotlin.jvm.JvmStatic + +object BionicFgManager { + private const val TAG = "BionicFgManager" + + private const val ASSET_DIR = "bionic_fg/android_arm64_v8a" + private const val LIB_FILENAME = "libbionic-fg-layer.so" + private const val MANIFEST_FILENAME = "VkLayer_BIONIC_framegen.json" + private const val VERSION_FILENAME = ".bionic_fg_runtime_version" + private const val RUNTIME_VERSION = "10" + + private const val LIB_RELATIVE_DIR = ".local/lib" + private const val LAYER_RELATIVE_DIR = ".local/share/vulkan/implicit_layer.d" + private const val CONFIG_RELATIVE_PATH = ".config/bionic-fg/conf.toml" + + const val ENV_ENABLE = "BIONIC_FG_ENABLE" + const val ENV_DISABLE = "DISABLE_BIONIC_FG" + const val ENV_CONFIG = "BIONIC_FG_CONFIG" + const val ENV_MULTIPLIER = "BIONIC_FG_MULTIPLIER" + const val ENV_FLOW_SCALE = "BIONIC_FG_FLOW_SCALE" + const val ENV_MODEL = "BIONIC_FG_MODEL" + const val ENV_DEBUG_TIMING = "BIONIC_FG_DEBUG_TIMING" + const val ENV_DEBUG_SUMMARY_EVERY = "BIONIC_FG_DEBUG_SUMMARY_EVERY" + const val ENV_PACE_PRESENT = "BIONIC_FG_PACE_PRESENT" + const val ENV_PACE_INTERVAL_MS = "BIONIC_FG_PACE_INTERVAL_MS" + + const val EXTRA_ENABLED = "bionicFgEnabled" + const val EXTRA_MULTIPLIER = "bionicFgMultiplier" + const val EXTRA_FLOW_SCALE = "bionicFgFlowScale" + const val EXTRA_MODEL = "bionicFgModel" + + @JvmStatic + fun isSupported(container: Container): Boolean = + container.containerVariant.equals(Container.BIONIC, ignoreCase = true) + + fun isEnabled(container: Container): Boolean = + isSupported(container) && container.getExtra(EXTRA_ENABLED, "false") == "true" + + fun multiplier(container: Container): Int { + val raw = container.getExtra(EXTRA_MULTIPLIER, "2").toIntOrNull() ?: 2 + return sanitizeMultiplier(raw) + } + + fun flowScale(container: Container): Float { + val raw = container.getExtra(EXTRA_FLOW_SCALE, "0.80").toFloatOrNull() ?: 0.80f + return raw.coerceIn(0.25f, 1.0f) + } + + fun model(container: Container): String { + val raw = container.getExtra(EXTRA_MODEL, "0") + return if (raw == "1") "1" else "0" + } + + @JvmStatic + fun ensureInstalled(context: Context, container: Container): Boolean { + if (!isSupported(container)) return false + + val rootDir = container.rootDir + val libFile = File(rootDir, "$LIB_RELATIVE_DIR/$LIB_FILENAME") + val manifestFile = File(rootDir, "$LAYER_RELATIVE_DIR/$MANIFEST_FILENAME") + val versionFile = File(rootDir, "$LAYER_RELATIVE_DIR/$VERSION_FILENAME") + + return try { + File(rootDir, LIB_RELATIVE_DIR).mkdirs() + File(rootDir, LAYER_RELATIVE_DIR).mkdirs() + + FileUtils.copy(context, "$ASSET_DIR/$LIB_FILENAME", libFile) + FileUtils.chmod(libFile, 0b111101101) + + FileUtils.copy(context, "$ASSET_DIR/$MANIFEST_FILENAME", manifestFile) + FileUtils.chmod(manifestFile, 0b110100100) + + versionFile.writeText(RUNTIME_VERSION) + FileUtils.chmod(versionFile, 0b110100100) + + Timber.tag(TAG).i("Refreshed Bionic-FG in %s", rootDir) + true + } catch (t: Throwable) { + Timber.tag(TAG).e(t, "Failed to install Bionic-FG") + false + } + } + + @JvmStatic + fun writeConfig(container: Container): Boolean = + updateConfigAtRuntime( + container = container, + enabled = isEnabled(container), + multiplier = multiplier(container), + flowScale = flowScale(container), + model = model(container), + ) + + @JvmStatic + fun updateConfigAtRuntime( + container: Container, + enabled: Boolean, + multiplier: Int, + flowScale: Float, + model: String, + ): Boolean { + if (!isSupported(container)) return false + + return try { + val configFile = configFile(container) + val sanitizedMultiplier = sanitizeMultiplier(multiplier) + val sanitizedFlowScale = flowScale.coerceIn(0.25f, 1.0f) + val sanitizedModel = sanitizeModel(model) + val configText = buildConfigToml( + enabled = enabled, + multiplier = sanitizedMultiplier, + flowScale = sanitizedFlowScale, + model = sanitizedModel, + ) + val ok = FileUtils.writeString(configFile, configText) + if (ok && configFile.exists()) { + FileUtils.chmod(configFile, 0b110100100) + Timber.tag(TAG).i( + "Updated Bionic-FG config: enabled=%s, mult=%d, flow=%.2f, model=%s", + enabled, + sanitizedMultiplier, + sanitizedFlowScale, + sanitizedModel, + ) + } + ok + } catch (t: Throwable) { + Timber.tag(TAG).e(t, "Failed to update Bionic-FG conf.toml") + false + } + } + + @JvmStatic + fun applyLaunchEnv(context: Context, container: Container, envVars: EnvVars): Boolean { + listOf( + ENV_ENABLE, + ENV_DISABLE, + ENV_CONFIG, + ENV_MULTIPLIER, + ENV_FLOW_SCALE, + ENV_MODEL, + ENV_DEBUG_TIMING, + ENV_DEBUG_SUMMARY_EVERY, + ENV_PACE_PRESENT, + ENV_PACE_INTERVAL_MS, + ).forEach { + envVars.remove(it) + } + + if (!isEnabled(container)) { + disableLayerInContainer(container) + envVars.put(ENV_DISABLE, "1") + Timber.tag(TAG).d("Bionic-FG disabled") + return false + } + + ensureInstalled(context, container) + writeConfig(container) + + val layerDir = File(container.rootDir, LAYER_RELATIVE_DIR) + val existingPath = envVars["VK_LAYER_PATH"] ?: "" + envVars.put( + "VK_LAYER_PATH", + if (existingPath.isNotEmpty()) "$existingPath:${layerDir.absolutePath}" + else layerDir.absolutePath, + ) + + envVars.put(ENV_ENABLE, "1") + envVars.remove(ENV_DISABLE) + envVars.put(ENV_CONFIG, configFile(container).absolutePath) + envVars.put(ENV_MULTIPLIER, multiplier(container).toString()) + envVars.put(ENV_FLOW_SCALE, String.format(Locale.US, "%.2f", flowScale(container))) + envVars.put(ENV_MODEL, model(container)) + envVars.put(ENV_DEBUG_TIMING, "1") + envVars.put(ENV_DEBUG_SUMMARY_EVERY, "60") + envVars.put(ENV_PACE_PRESENT, "1") + envVars.put(ENV_PACE_INTERVAL_MS, "8.333") + + Timber.tag(TAG).i( + "Bionic-FG enabled: mult=%d, flow=%.2f, model=%s, debugTiming=%s, summaryEvery=%d, pacePresent=%s, paceIntervalMs=%.3f", + multiplier(container), + flowScale(container), + model(container), + true, + 60, + true, + 8.333f, + ) + return true + } + + private fun configFile(container: Container): File = + File(container.rootDir, CONFIG_RELATIVE_PATH) + + private fun sanitizeMultiplier(multiplier: Int): Int = + if (multiplier < 2) 0 else multiplier.coerceIn(2, 4) + + private fun sanitizeModel(model: String): String = + if (model == "1") "1" else "0" + + private fun buildConfigToml( + enabled: Boolean, + multiplier: Int, + flowScale: Float, + model: String, + ): String = buildString { + appendLine("version = 1") + appendLine() + appendLine("[global]") + appendLine("enabled = ${if (enabled) "true" else "false"}") + appendLine("multiplier = ${sanitizeMultiplier(multiplier)}") + appendLine("flow_scale = ${String.format(Locale.US, "%.2f", flowScale.coerceIn(0.25f, 1.0f))}") + appendLine("model = ${sanitizeModel(model)}") + } + + private fun disableLayerInContainer(container: Container) { + val manifestFile = File(container.rootDir, "$LAYER_RELATIVE_DIR/$MANIFEST_FILENAME") + if (manifestFile.exists()) { + if (manifestFile.delete()) { + Timber.tag(TAG).d("Removed Bionic-FG manifest to disable layer") + } else { + Timber.tag(TAG).w("Failed to remove Bionic-FG manifest at %s", manifestFile.absolutePath) + } + } + } +} diff --git a/app/src/main/java/app/gamenative/utils/BionicFgQuickMenuHelper.kt b/app/src/main/java/app/gamenative/utils/BionicFgQuickMenuHelper.kt new file mode 100644 index 0000000000..aaae03bd4d --- /dev/null +++ b/app/src/main/java/app/gamenative/utils/BionicFgQuickMenuHelper.kt @@ -0,0 +1,50 @@ +package app.gamenative.utils + +import com.winlator.container.Container +import java.util.Locale + +/** Helpers for Quick Menu Bionic-FG state persistence and runtime hot-reload. */ +object BionicFgQuickMenuHelper { + data class Settings( + val multiplier: Int, + val flowScale: Float, + val model: String, + ) + + fun isAvailable(container: Container): Boolean = + BionicFgManager.isSupported(container) && BionicFgManager.isEnabled(container) + + fun readSettings(container: Container): Settings = Settings( + multiplier = BionicFgManager.multiplier(container), + flowScale = BionicFgManager.flowScale(container), + model = BionicFgManager.model(container), + ) + + fun sanitizeMultiplier(multiplier: Int): Int = + if (multiplier < 2) 0 else multiplier.coerceIn(2, 4) + + fun sanitizeFlowScale(flowScale: Float): Float = + flowScale.coerceIn(0.25f, 1.0f) + + fun sanitizeModel(model: String): String = + if (model == "1") "1" else "0" + + fun applySettings(container: Container, settings: Settings) { + val multiplier = sanitizeMultiplier(settings.multiplier) + val flowScale = sanitizeFlowScale(settings.flowScale) + val model = sanitizeModel(settings.model) + + container.putExtra(BionicFgManager.EXTRA_MULTIPLIER, multiplier.toString()) + container.putExtra(BionicFgManager.EXTRA_FLOW_SCALE, String.format(Locale.US, "%.2f", flowScale)) + container.putExtra(BionicFgManager.EXTRA_MODEL, model) + container.saveData() + + BionicFgManager.updateConfigAtRuntime( + container = container, + enabled = true, + multiplier = multiplier, + flowScale = flowScale, + model = model, + ) + } +} diff --git a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt index 3e874958d2..1db0b1c898 100644 --- a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt +++ b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt @@ -7,6 +7,7 @@ import app.gamenative.data.GameSource import app.gamenative.enums.Marker import app.gamenative.service.SteamService import app.gamenative.service.amazon.AmazonService +import app.gamenative.utils.BionicFgManager import app.gamenative.utils.LsfgVkManager import app.gamenative.service.epic.EpicService import app.gamenative.service.gog.GOGConstants @@ -323,8 +324,11 @@ object ContainerUtils { sharpnessEffect = container.getExtra("sharpnessEffect", "None"), sharpnessLevel = container.getExtra("sharpnessLevel", "100").toIntOrNull() ?: 100, sharpnessDenoise = container.getExtra("sharpnessDenoise", "100").toIntOrNull() ?: 100, - // LSFG Vulkan frame generation lsfgEnabled = container.getExtra(LsfgVkManager.EXTRA_ARMED, "false").toBoolean(), + bionicFgEnabled = container.getExtra(BionicFgManager.EXTRA_ENABLED, "false").toBoolean(), + bionicFgMultiplier = container.getExtra(BionicFgManager.EXTRA_MULTIPLIER, "2").toIntOrNull() ?: 2, + bionicFgFlowScale = container.getExtra(BionicFgManager.EXTRA_FLOW_SCALE, "0.80").toFloatOrNull() ?: 0.80f, + bionicFgModel = container.getExtra(BionicFgManager.EXTRA_MODEL, "0"), ) } @@ -492,8 +496,11 @@ object ContainerUtils { container.putExtra("sharpnessEffect", containerData.sharpnessEffect) container.putExtra("sharpnessLevel", containerData.sharpnessLevel.toString()) container.putExtra("sharpnessDenoise", containerData.sharpnessDenoise.toString()) - // LSFG Vulkan frame generation container.putExtra(LsfgVkManager.EXTRA_ARMED, containerData.lsfgEnabled.toString()) + container.putExtra(BionicFgManager.EXTRA_ENABLED, containerData.bionicFgEnabled.toString()) + container.putExtra(BionicFgManager.EXTRA_MULTIPLIER, containerData.bionicFgMultiplier.toString()) + container.putExtra(BionicFgManager.EXTRA_FLOW_SCALE, containerData.bionicFgFlowScale.toString()) + container.putExtra(BionicFgManager.EXTRA_MODEL, containerData.bionicFgModel) try { container.language = containerData.language } catch (e: Exception) { diff --git a/app/src/main/java/com/winlator/container/ContainerData.kt b/app/src/main/java/com/winlator/container/ContainerData.kt index 7fb3a389fe..a823bfddb2 100644 --- a/app/src/main/java/com/winlator/container/ContainerData.kt +++ b/app/src/main/java/com/winlator/container/ContainerData.kt @@ -95,9 +95,11 @@ data class ContainerData( val sharpnessEffect: String = "None", val sharpnessLevel: Int = 100, val sharpnessDenoise: Int = 100, - // LSFG Vulkan frame generation - /** Whether LSFG frame generation is enabled for this container */ val lsfgEnabled: Boolean = false, + val bionicFgEnabled: Boolean = false, + val bionicFgMultiplier: Int = 2, + val bionicFgFlowScale: Float = 0.80f, + val bionicFgModel: String = "0", ) { companion object { val Saver = mapSaver( @@ -163,6 +165,10 @@ data class ContainerData( "sharpnessLevel" to state.sharpnessLevel, "sharpnessDenoise" to state.sharpnessDenoise, "lsfgEnabled" to state.lsfgEnabled, + "bionicFgEnabled" to state.bionicFgEnabled, + "bionicFgMultiplier" to state.bionicFgMultiplier, + "bionicFgFlowScale" to state.bionicFgFlowScale, + "bionicFgModel" to state.bionicFgModel, ) }, restore = { savedMap -> @@ -227,6 +233,10 @@ data class ContainerData( sharpnessLevel = (savedMap["sharpnessLevel"] as? Int) ?: 100, sharpnessDenoise = (savedMap["sharpnessDenoise"] as? Int) ?: 100, lsfgEnabled = (savedMap["lsfgEnabled"] as? Boolean) ?: false, + bionicFgEnabled = (savedMap["bionicFgEnabled"] as? Boolean) ?: false, + bionicFgMultiplier = (savedMap["bionicFgMultiplier"] as? Int) ?: 2, + bionicFgFlowScale = (savedMap["bionicFgFlowScale"] as? Float) ?: 0.80f, + bionicFgModel = (savedMap["bionicFgModel"] as? String) ?: "0", ) }, ) diff --git a/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java b/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java index 09d207eb77..1a051c13a5 100644 --- a/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java +++ b/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java @@ -17,6 +17,7 @@ import com.winlator.PrefManager; +import app.gamenative.utils.BionicFgManager; import app.gamenative.utils.LsfgVkManager; import com.winlator.box86_64.Box86_64Preset; import com.winlator.box86_64.Box86_64PresetManager; @@ -336,6 +337,10 @@ private int execGuestProgram() { LsfgVkManager.applyLaunchEnv(container, envVars); } + if (BionicFgManager.isSupported(container)) { + BionicFgManager.applyLaunchEnv(environment.getContext(), container, envVars); + } + Log.d("BionicProgramLauncherComponent", "env vars are " + envVars.toString()); String emulator = container.getEmulator(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 69074d0826..731788ad03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -291,6 +291,7 @@ Unlimited %1$d FPS Limiter overridden while LSFG is active. Source rate is paced by the multiplier. + Limiter overridden while frame generation is active. Source rate is paced by the multiplier. Presets Quickly switch between common HUD layouts. 1 @@ -1492,6 +1493,14 @@ This will download Lossless Scaling from Steam to enable frame generation. Downloading Lossless Scaling… Reduces quality for higher throughput + Bionic-FG + Enable Bionic-FG frame generation + Alternative Vulkan frame generation (no Steam entitlement required). Only known to work on Adreno 6xx and above. + Frame multiplier + Flow scale + Generation model + Model 0 (Fast) + Model 1 (Accurate) Show game recommendations Show curated indie game picks in your library. Keeping this on helps support indie developers and GameNative.