Skip to content
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ and performance and accuracy work measured frame by frame on real hardware.
- **Performance HUD** (FPS, frame time, CPU, GPU, RAM, temperatures) and per-game
frame-rate limits (unlimited, or 30 to 120 FPS).
- **Output scaling and sharpening**, including Snapdragon Game Super Resolution.
- **SGSR1 Edge Direction**, alongside the existing SGSR1 filter, available from
Settings and the in-game Image menu; the default scaling filter is unchanged.
- **Driver manager** for Adreno/Turnip drivers, global or per game, with SHA-256
checks.
- **Play history** with frame-time percentiles, a diagnostics bundle to share,
local compatibility notes and community settings profiles.
- **Frame generation** (experimental, off by default): Win-FG and LSFG (with your own
`Lossless.dll`) turn a 30 fps game into 60 on the screen, and the HUD shows both
rates.
- **ZeroFG** adds Zero (quality) and ReallyZero (lower cost), with an in-game
Off/ReallyZero/Zero selector. It is off by default and needs a qualified Vulkan
1.3 path; see [integration and validation notes](docs/ZEROFG.md).

## Performance snapshot

Expand Down
5 changes: 5 additions & 0 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,17 @@ aparelhos reais.
- **HUD de desempenho** (FPS, tempo de quadro, CPU, GPU, RAM, temperaturas) e limite de
FPS por jogo (ilimitado, ou de 30 a 120 FPS).
- **Escala e nitidez da imagem**, incluindo o Snapdragon Game Super Resolution.
- **SGSR1 Edge Direction**, junto do SGSR1 existente, nas configurações e no menu
de Imagem durante o jogo; o filtro padrão permanece igual.
- **Gerenciador de drivers** Adreno/Turnip, global ou por jogo, com verificação SHA-256.
- **Histórico de jogo** com percentis do tempo de quadro, pacote de diagnóstico para
compartilhar, notas de compatibilidade locais e perfis de ajustes da comunidade.
- **Geração de quadros** (experimental, desligada por padrão): Win-FG e LSFG (com o seu
próprio `Lossless.dll`) transformam um jogo a 30 fps em 60 na tela, e o HUD mostra
as duas taxas.
- **ZeroFG** oferece Zero (qualidade) e ReallyZero (menor custo), com seletor
Off/ReallyZero/Zero durante o jogo. Vem desligado e exige um caminho Vulkan 1.3
qualificado; veja as [notas de integração e validação](docs/ZEROFG.md).

## Desempenho

Expand Down
1 change: 1 addition & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ upstream BSD/MIT files.
|---|---|---|
| Xendroid+ (this project's own code) | This repository | `LICENSE` at the repository root (AGPL-3.0-or-later) |
| Xenia | Existing source tree | `emulator-core/src/main/cpp/xenia/LICENSE` (BSD) |
| ZeroFG | Approved source snapshot `38dcc6f4a69fdf2e009a2908cd9ab6e2d3a57a64`; provenance and mode mapping in `zerofg/SOURCE.md` | `zerofg/LICENSE` (Apache-2.0); Xenia-derived presenter glue retains its BSD notices |
| Mbed TLS (kernel crypto and XEX decryption) | Mbed-TLS/mbedtls `068ff080b369adfac81509f9b57b2afabaf82dc5` (3.6.7; only the crypto sources the build uses) | `xenia/third_party/mbedtls/LICENSE` (Apache-2.0 OR GPL-2.0-or-later) |
| Win-FG | The412Banner/win-fg `fbc69ed6c5bf16b824d0a29bd8b171675101f9dd` | `third_party/winfg/LICENSE` (MIT), FidelityFX notice and THIRD-PARTY.md |
| LSFG host chain | The412Banner/Bannerlator `15d4f73c079824d54c468f079361ce56d88a7c2e` | `third_party/lsfg/LICENSE` and original GPL-3.0-or-later SPDX headers; Eden/WinNative/lsfg-vk credits preserved |
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def engineLicensesDir = layout.buildDirectory.dir('generated/engineLicenseAssets
def syncEngineLicenses = tasks.register('syncEngineLicenses', Sync) {
into engineLicensesDir
into('engine-licenses') {
from('../emulator-core/src/main/cpp/zerofg/LICENSE') { rename { 'zerofg-Apache-2.0.txt' } }
from('../emulator-core/src/main/cpp/third_party/winfg/LICENSE') { rename { 'winfg-MIT.txt' } }
from('../emulator-core/src/main/cpp/third_party/winfg/THIRD-PARTY.md') { rename { 'winfg-third-party.txt' } }
from('../emulator-core/src/main/cpp/third_party/winfg/NOTICE_FIDELITYFX_OPTICALFLOW.md') { rename { 'fidelityfx-notice.txt' } }
Expand Down
83 changes: 78 additions & 5 deletions app/src/main/java/xendroid/compose/EmulatorHostActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import xendroid.compose.core.EmuProcessLink
import xendroid.compose.core.EmulatorRuntime
import xendroid.compose.core.FrontendLaunch
import xendroid.compose.core.EmulatorSession
import xendroid.compose.core.ZeroFgMode
import xendroid.compose.core.ScreenBrightnessSampler
import xendroid.compose.core.SessionLogs
import xendroid.compose.core.HudMetric
Expand Down Expand Up @@ -128,7 +129,8 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
private const val TOUCH_KEY = "HID|show_touch_overlay"
private const val VOLUME_KEY = "APU|volume"
private const val IMAGE_LIVE = "image"
private val MENU_CONFIG_KEYS = listOf(FPS_KEY, SCALING_KEY, AA_KEY, CAS_KEY, DITHER_KEY, TOUCH_KEY, VOLUME_KEY) +
private const val ZEROFG_KEY = "Vulkan|zerofg_mode"
private val MENU_CONFIG_KEYS = listOf(FPS_KEY, SCALING_KEY, AA_KEY, CAS_KEY, DITHER_KEY, TOUCH_KEY, VOLUME_KEY, ZEROFG_KEY) +
xendroid.compose.core.GpuLiveOption.entries.map { it.key }
private val FPS_CHOICES = listOf(0, 30, 45, 60, 90, 120)
private val HUD_DETAILS = listOf(xendroid.compose.core.HudDetail.COMPACT, xendroid.compose.core.HudDetail.FULL,
Expand Down Expand Up @@ -184,6 +186,10 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
}

private val session = EmulatorSession()
private val zeroFgMode = mutableStateOf(ZeroFgMode.OFF)
private val zeroFgDisplayVote = xendroid.compose.core.ZeroFgDisplayVote()
private var zeroFgSurfaceGeneration = 0
private val zeroFgActive = mutableStateOf(false)
/** Durable record of this guest run (sessions/SessionRunStore); null if unavailable. */
@Volatile private var runId: String? = null
private var lastRunHeartbeatMs = 0L
Expand Down Expand Up @@ -920,10 +926,11 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
val effect = scalingEffect.intValue
val image = listOfNotNull(
if (effect < 0) getString(R.string.menu_scaling_inherited)
else listOf("Bilinear", "CAS", "FSR", "SGSR", "Lanczos", "CRT").getOrNull(effect),
else listOf("Bilinear", "CAS", "FSR", "SGSR", "Lanczos", "CRT", "SGSR1 Edge Direction").getOrNull(effect),
xendroid.compose.core.PerformancePanel.resolutionScale(changed)?.let { getString(R.string.panel_resolution, it) },
).joinToString(" · ")
val frameGeneration = when {
zeroFgMode.value != ZeroFgMode.OFF -> zeroFgStatus()
!fg.requested -> getString(R.string.menu_off)
fg.engine == 1 -> "LSFG ×${fg.multiplier}"
else -> "Win-FG"
Expand Down Expand Up @@ -1416,6 +1423,27 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {

val booted by bootedState

// Main Surface Authority: follow the actual native owner, not the requested mode.
LaunchedEffect(booted) {
if (!booted) return@LaunchedEffect
while (isActive) {
zeroFgMode.value = session.zeroFgMode()
val active = session.zeroFgMainSurfaceActive()
zeroFgDisplayVote.update(active,
externalDisplay?.outputSurfaceView ?: surfaceView,
externalDisplay?.outputWindow ?: window, zeroFgSurfaceGeneration)
if (zeroFgActive.value && !active) {
val legacy = session.presentationState()
// Restore the exact legacy request after releasing B's vote,
// including a manual refresh or a target chosen while paused.
if (legacy.requested) selectRefresh(this@EmulatorHostActivity, legacy.hz)
else releaseFrameGenerationRefresh()
}
zeroFgActive.value = active
delay(100)
}
}

val playingTitle by activeTitleState
val controls =
remember(
Expand Down Expand Up @@ -2192,6 +2220,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
holder: SurfaceHolder
) {
if (externalDisplay?.owns(holder) == false) return
++zeroFgSurfaceGeneration
surfaceAvailable = true
recordEvent("surface", if (started) "recreated" else "created, booting the core")
if (!started) {
Expand Down Expand Up @@ -2268,6 +2297,8 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
holder: SurfaceHolder
) {
if (externalDisplay?.owns(holder) == false) return
zeroFgDisplayVote.release()
++zeroFgSurfaceGeneration
surfaceAvailable = false
if (!started) return
recordEvent("surface", "destroyed", flush = true)
Expand Down Expand Up @@ -2347,6 +2378,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
}

override fun onDestroy() {
zeroFgDisplayVote.release()
gameModeSignal.update(xendroid.compose.core.GamePhase.NONE)
performanceHints.close()
gyroCamera.stop()
Expand Down Expand Up @@ -3105,6 +3137,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
(if (Build.VERSION.SDK_INT >= 30) display?.refreshRate else windowManager.defaultDisplay.refreshRate) ?: 60f

private fun frameGenerationNotes(state: PresentationState): List<String> {
if (zeroFgMode.value != ZeroFgMode.OFF) return listOf(zeroFgStatus())
val hz = currentOutputHz()
return if (state.requested && state.state == 2) {
xendroid.compose.core.FrameGenerationReadout.running(lastGuestFps, generatedPerSecond,
Expand Down Expand Up @@ -3197,8 +3230,8 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
InGameAction.DISPLAY_MODE to MenuValue(options = listOf(R.string.menu_opt_fit, R.string.menu_opt_fill, R.string.menu_opt_stretch,
R.string.menu_opt_integer).map { stringResource(it) }, selected = presentation.displayMode.coerceAtLeast(0),
own = InGameChanges.DISPLAY_MODE in own),
InGameAction.SCALING_EFFECT to MenuValue(text = listOf(fromSettings, "Bilinear", "CAS", "FSR", "SGSR", "Lanczos", "CRT")
[(scalingEffect.intValue + 1).coerceIn(0, 6)], own = SCALING_KEY in own),
InGameAction.SCALING_EFFECT to MenuValue(text = listOf(fromSettings, "Bilinear", "CAS", "FSR", "SGSR", "Lanczos", "CRT", "SGSR1 Edge Direction")
[(scalingEffect.intValue + 1).coerceIn(0, 7)], own = SCALING_KEY in own),
InGameAction.ANTIALIASING to MenuValue(text = when (image.antialiasing) {
0 -> off; 1 -> "FXAA"; 2 -> stringResource(R.string.menu_aa_fxaa_extreme); else -> fromSettings
}, own = AA_KEY in own),
Expand All @@ -3225,6 +3258,10 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
xendroid.compose.driver.DriverIdentity.InGame.OTHER_FOR_NEXT_START -> stringResource(R.string.menu_driver_next, label)
}
}),
InGameAction.ZEROFG to MenuValue(options = listOf(off,
stringResource(R.string.menu_zerofg_reallyzero), stringResource(R.string.menu_zerofg_zero)),
selected = zeroFgMode.value.value, own = ZEROFG_KEY in own,
note = if (zeroFgMode.value != ZeroFgMode.OFF) zeroFgStatus() else null),
InGameAction.WINFG to MenuValue(on = presentation.requested && presentation.engine == 0),
InGameAction.WINFG_PRESET to MenuValue(text = stringResource(listOf(R.string.menu_preset_quality, R.string.menu_preset_balanced,
R.string.menu_preset_performance)[fgPreset.intValue.coerceIn(0, 2)])),
Expand Down Expand Up @@ -3508,6 +3545,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
/** Round 2: a pill or a chip of the menu chosen by touch (or a choice's next by A). */
private fun chooseMenuOption(action: InGameAction, index: Int) {
when (action) {
InGameAction.ZEROFG -> setZeroFgMode(ZeroFgMode.fromValue(index))
InGameAction.DISPLAY_MODE -> setDisplayMode(index.coerceIn(0, 3))
InGameAction.FPS_LIMIT -> FPS_CHOICES.getOrNull(index)?.let(::setMenuFps)
InGameAction.HUD_LAYOUT -> updateHudStyle { it.copy(layout = xendroid.compose.core.HudLayout.entries[index.coerceIn(0, 1)]) }
Expand Down Expand Up @@ -3552,6 +3590,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
}

private fun choiceIndex(action: InGameAction): Int = when (action) {
InGameAction.ZEROFG -> zeroFgMode.value.value
InGameAction.DISPLAY_MODE -> presentationState.value.displayMode.coerceIn(0, 3)
InGameAction.FPS_LIMIT -> FPS_CHOICES.indexOf(fpsLimitState.intValue).coerceAtLeast(0)
InGameAction.HUD_LAYOUT -> hudStyle.value.layout.ordinal
Expand All @@ -3563,6 +3602,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
}

private fun choiceCount(action: InGameAction): Int = when (action) {
InGameAction.ZEROFG -> 3
InGameAction.DISPLAY_MODE -> 4
InGameAction.FPS_LIMIT -> FPS_CHOICES.size
InGameAction.HUD_STYLE -> HUD_DETAILS.size
Expand Down Expand Up @@ -3614,7 +3654,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
InGameAction.SHADING_RATE -> setGpuLive(xendroid.compose.core.GpuLiveOption.SHADING_RATE, around((0..3).toList(), gpuValue(xendroid.compose.core.GpuLiveOption.SHADING_RATE)))
InGameAction.SCALING_EFFECT -> {
rememberLive(SCALING_KEY, scalingEffect.intValue)
scalingEffect.intValue = around((-1..5).toList(), scalingEffect.intValue)
scalingEffect.intValue = around((-1..6).toList(), scalingEffect.intValue)
session.setScalingEffect(scalingEffect.intValue)
keepChange(SCALING_KEY, xendroid.compose.core.ImageTuning.scalingValue(scalingEffect.intValue))
}
Expand Down Expand Up @@ -3681,6 +3721,34 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
}
}

private fun zeroFgStatus(): String {
val name = getString(if (zeroFgMode.value == ZeroFgMode.REALLY_ZERO)
R.string.menu_zerofg_reallyzero else R.string.menu_zerofg_zero)
return getString(if (zeroFgActive.value) R.string.menu_zerofg_active else R.string.menu_zerofg_requested, name)
}

/** The render thread owns draining and switching engines; this only publishes the request. */
private fun setZeroFgMode(mode: ZeroFgMode, keep: Boolean = true) {
if (keep) rememberLive(ZEROFG_KEY, session.zeroFgMode())
if (mode != ZeroFgMode.OFF) {
restoreGenerationCap()
releaseFrameGenerationRefresh()
}
session.setZeroFgMode(mode)
zeroFgMode.value = session.zeroFgMode()
presentationState.value = session.presentationState()
if (keep) keepChange(ZEROFG_KEY, mode.key)
recordEvent("zerofg", "${mode.key} requested")
}

private fun noteZeroFgStoppedForLegacy(previous: ZeroFgMode, enabled: Boolean) {
zeroFgMode.value = session.zeroFgMode()
if (enabled && previous != ZeroFgMode.OFF) {
rememberLive(ZEROFG_KEY, previous)
keepChange(ZEROFG_KEY, ZeroFgMode.OFF.key)
}
}

/** Frame generation by LSFG: on or off, a multiplier by hand or an output target (15d). */
private fun runLsfg(action: InGameAction) {
when (action) {
Expand Down Expand Up @@ -3714,7 +3782,9 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
plan != null -> prepareExactCap(plan.cap)
else -> prepareGenerationCap(hz, lsfgMultiplier.intValue)
}
val previousZeroFgMode = session.zeroFgMode()
session.setLsfg(enabled, cache, hz, lsfgMultiplier.intValue)
noteZeroFgStoppedForLegacy(previousZeroFgMode, enabled)
if (enabled) recordEvent("lsfg", plan?.let { "target ${it.target}/s: ${it.multiplier}x from ${it.cap} FPS" }
?: "${lsfgMultiplier.intValue}x by hand")
presentationState.value = session.presentationState()
Expand All @@ -3727,7 +3797,9 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {
val enabled = if (action == InGameAction.WINFG) !(state.requested && state.engine == 0) else state.requested
val hz = if (enabled) frameGenerationRefresh(guestFpsNow() * 2) else currentOutputHz()
if (enabled) prepareGenerationCap(hz) else { restoreGenerationCap(); releaseFrameGenerationRefresh() }
val previousZeroFgMode = session.zeroFgMode()
session.setFrameGeneration(enabled, fgPreset.intValue, hz)
noteZeroFgStoppedForLegacy(previousZeroFgMode, enabled)
presentationState.value = session.presentationState()
}

Expand Down Expand Up @@ -3936,6 +4008,7 @@ class EmulatorHostActivity : ComponentActivity(), SurfaceHolder.Callback {

private fun restoreLive(key: String, value: Any?) {
when (key) {
ZEROFG_KEY -> (value as? ZeroFgMode)?.let { setZeroFgMode(it, keep = false) }
FPS_KEY -> (value as? Int)?.let { generationCap.forget(); session.setFpsLimit(it); fpsLimitState.intValue = session.fpsLimit() }
SCALING_KEY -> (value as? Int)?.let { scalingEffect.intValue = it; session.setScalingEffect(it) }
IMAGE_LIVE -> (value as? xendroid.compose.core.ImageTuning)?.let { imageTuning.value = it; session.setImageTuning(it) }
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/xendroid/compose/FpsOverlay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,9 @@ fun FpsOverlay(
if (generatedNs - previousGeneratedNs >= 1_000_000_000L) {
val state = runCatching { session.presentationState() }.getOrNull()
val active = state != null && state.requested && state.state == 2
fgFps = if (active && previousGenerated >= 0 && state!!.generated >= previousGenerated)
currentFps + (state.generated - previousGenerated) * 1e9 / (generatedNs - previousGeneratedNs) else null
fgFps = if (session.zeroFgMainSurfaceActive()) session.outputFps().takeIf { it > 0 }
else if (active && previousGenerated >= 0 && state!!.generated >= previousGenerated)
currentFps + (state.generated - previousGenerated) * 1e9 / (generatedNs - previousGeneratedNs) else null
previousGenerated = if (active) state!!.generated else -1L
previousGeneratedNs = generatedNs
}
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/xendroid/compose/core/EmulatorSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ class EmulatorSession {
fun setFatalReportPath(path: String?) = core.set_fatal_report_path(path)
fun presenterWork(): LongArray = if (booted) core.presenter_work() else longArrayOf(0, 0, 0)
fun setFrameGeneration(enabled: Boolean, preset: Int, hz: Float) { if (booted) core.set_frame_generation(enabled, preset, hz) }
/** Requested mode; the native presenter drains and switches on its own thread. */
fun zeroFgMode(): ZeroFgMode = if (booted) ZeroFgMode.fromValue(core.zerofg_mode()) else ZeroFgMode.OFF
fun setZeroFgMode(mode: ZeroFgMode) { if (booted) core.set_zerofg_mode(mode.value) }
fun zeroFgMainSurfaceActive(): Boolean = booted && core.zerofg_main_surface_active()
/** Rate of outputs delivered by the native presenter, not inferred from a multiplier. */
fun outputFps(): Double = if (booted) core.output_fps() else 0.0
fun setLsfg(enabled: Boolean, cache: String, hz: Float, multiplier: Int = 2) {
if (booted) core.set_lsfg(enabled, cache, hz, multiplier.coerceIn(2, 4))
}
Expand Down
Loading