Skip to content
Merged
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
121 changes: 44 additions & 77 deletions app/src/main/java/com/limelight/DisplayModeManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.limelight

import android.os.Build
import android.view.Display
import androidx.annotation.RequiresApi
import com.limelight.preferences.PreferenceConfiguration
import com.limelight.utils.UiHelper
import kotlin.math.abs
import kotlin.math.roundToInt

/**
* 显示模式管理器
Expand All @@ -25,11 +25,11 @@ object DisplayModeManager {
)

fun isRefreshRateEqualMatch(refreshRate: Float, targetFps: Int): Boolean {
return refreshRate >= targetFps && refreshRate <= targetFps + 3
return DisplayModePolicy.isRefreshRateEqualMatch(refreshRate, targetFps)
}

fun isRefreshRateGoodMatch(refreshRate: Float, targetFps: Int): Boolean {
return refreshRate >= targetFps && refreshRate.roundToInt() % targetFps <= 3
return DisplayModePolicy.isRefreshRateGoodMatch(refreshRate, targetFps)
}

fun mayReduceRefreshRate(prefConfig: PreferenceConfiguration): Boolean {
Expand Down Expand Up @@ -74,84 +74,36 @@ object DisplayModeManager {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val supportedModes = display.supportedModes
val eligibleModes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
acceptableHdrTypes.isNotEmpty()
) {
supportedModes.filter { mode ->
mode.supportedHdrTypes.any { supportedType ->
acceptableHdrTypes.any { it == supportedType }
}
}.ifEmpty {
LimeLog.warning("No display mode supports the requested HDR type; using normal mode selection")
supportedModes.asList()
}
val isNativeResolutionStream = prefConfig.usesNativeDisplayMode
val effectiveHdrTypes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
acceptableHdrTypes.toList()
} else {
supportedModes.asList()
emptyList()
}

// Start from the mode Android is actually using. A candidate may be
// rejected by the resolution/refresh-rate constraints below; in that
// case we must keep the current mode instead of retaining an arbitrary
// first HDR-eligible mode that was never validated by those constraints.
var bestMode = display.mode
val isNativeResolutionStream = prefConfig.usesNativeDisplayMode
var refreshRateIsGood = isRefreshRateGoodMatch(bestMode.refreshRate, prefConfig.fps)
var refreshRateIsEqual = isRefreshRateEqualMatch(bestMode.refreshRate, prefConfig.fps)

LimeLog.info("Current display mode: ${bestMode.physicalWidth}x${bestMode.physicalHeight}x${bestMode.refreshRate}")

for (candidate in eligibleModes) {
val refreshRateReduced = candidate.refreshRate < bestMode.refreshRate
val resolutionReduced = candidate.physicalWidth < bestMode.physicalWidth ||
candidate.physicalHeight < bestMode.physicalHeight
val resolutionFitsStream = candidate.physicalWidth >= prefConfig.width &&
candidate.physicalHeight >= prefConfig.height

LimeLog.info("Examining display mode: ${candidate.physicalWidth}x${candidate.physicalHeight}x${candidate.refreshRate}")

if (candidate.physicalWidth > 4096 && prefConfig.width <= 4096) {
continue
}

if (prefConfig.width < 3840 && prefConfig.fps <= 60 && !isNativeResolutionStream) {
if (display.mode.physicalWidth != candidate.physicalWidth ||
display.mode.physicalHeight != candidate.physicalHeight
) {
continue
}
}

if (resolutionReduced && !(prefConfig.fps > 60 && resolutionFitsStream)) {
continue
}

if (mayReduceRefreshRate(prefConfig) && refreshRateIsEqual && !isRefreshRateEqualMatch(candidate.refreshRate, prefConfig.fps)) {
continue
} else if (refreshRateIsGood) {
if (!isRefreshRateGoodMatch(candidate.refreshRate, prefConfig.fps)) {
continue
}

if (mayReduceRefreshRate(prefConfig)) {
if (candidate.refreshRate > bestMode.refreshRate) {
continue
}
} else {
if (refreshRateReduced) {
continue
}
}
} else if (!isRefreshRateGoodMatch(candidate.refreshRate, prefConfig.fps)) {
if (refreshRateReduced) {
continue
}
}

bestMode = candidate
refreshRateIsGood = isRefreshRateGoodMatch(candidate.refreshRate, prefConfig.fps)
refreshRateIsEqual = isRefreshRateEqualMatch(candidate.refreshRate, prefConfig.fps)
val currentMode = display.mode.toPolicyMode()
val policyResult = DisplayModePolicy.selectBestMode(
currentMode = currentMode,
supportedModes = supportedModes.map { it.toPolicyMode() },
request = DisplayModePolicy.Request(
width = prefConfig.width,
height = prefConfig.height,
fps = prefConfig.fps,
usesNativeDisplayMode = isNativeResolutionStream,
mayReduceRefreshRate = mayReduceRefreshRate(prefConfig),
acceptableHdrTypes = effectiveHdrTypes,
),
)
if (effectiveHdrTypes.isNotEmpty() && !policyResult.hdrFilterApplied) {
LimeLog.warning("No display mode supports the requested HDR type; using normal mode selection")
} else if (effectiveHdrTypes.isNotEmpty() &&
policyResult.mode.hdrTypes.none(effectiveHdrTypes::contains)
) {
LimeLog.warning("HDR-capable modes exist but none met the display mode constraints")
}

val bestMode = supportedModes.firstOrNull { it.modeId == policyResult.mode.id } ?: display.mode
Comment thread
qiin2333 marked this conversation as resolved.
LimeLog.info("Current display mode: ${display.mode.physicalWidth}x${display.mode.physicalHeight}x${display.mode.refreshRate}")

LimeLog.info("Best display mode: ${bestMode.physicalWidth}x${bestMode.physicalHeight}x${bestMode.refreshRate}")

if (display.mode.modeId != bestMode.modeId) {
Expand Down Expand Up @@ -220,4 +172,19 @@ object DisplayModeManager {
aspectRatioMatch = aspectRatioMatch
)
}

@RequiresApi(Build.VERSION_CODES.M)
private fun Display.Mode.toPolicyMode(): DisplayModePolicy.Mode {
return DisplayModePolicy.Mode(
id = modeId,
width = physicalWidth,
height = physicalHeight,
refreshRate = refreshRate,
hdrTypes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
supportedHdrTypes.toList()
} else {
emptyList()
},
)
}
}
115 changes: 115 additions & 0 deletions app/src/main/java/com/limelight/DisplayModePolicy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.limelight

import kotlin.math.roundToInt

/** Pure display-mode selection policy, independent of Android framework objects. */
internal object DisplayModePolicy {
data class Mode(
val id: Int,
val width: Int,
val height: Int,
val refreshRate: Float,
val hdrTypes: List<Int> = emptyList(),
)

data class Request(
val width: Int,
val height: Int,
val fps: Int,
val usesNativeDisplayMode: Boolean,
val mayReduceRefreshRate: Boolean,
val acceptableHdrTypes: List<Int> = emptyList(),
)

data class Result(
val mode: Mode,
val hdrFilterApplied: Boolean,
)

fun selectBestMode(
currentMode: Mode,
supportedModes: List<Mode>,
request: Request,
): Result {
val hdrModes = if (request.acceptableHdrTypes.isNotEmpty()) {
supportedModes.filter { it.supportsAny(request.acceptableHdrTypes) }
} else {
emptyList()
}
val hdrFilterApplied = hdrModes.isNotEmpty()
val eligibleModes = if (hdrFilterApplied) hdrModes else supportedModes

// An HDR request must be optimized entirely within the matching HDR set. Keep the
// current mode as the baseline only when it belongs to that set; otherwise the first
// candidate that passes the resolution constraints establishes the HDR baseline.
var bestMode: Mode? = currentMode.takeIf { !hdrFilterApplied || it.supportsAny(request.acceptableHdrTypes) }
var refreshRateIsGood = bestMode?.let { isRefreshRateGoodMatch(it.refreshRate, request.fps) } ?: false
var refreshRateIsEqual = bestMode?.let { isRefreshRateEqualMatch(it.refreshRate, request.fps) } ?: false

for (candidate in eligibleModes) {
val comparisonMode = bestMode ?: currentMode
val resolutionReduced = candidate.width < comparisonMode.width ||
candidate.height < comparisonMode.height
val resolutionFitsStream = candidate.width >= request.width &&
candidate.height >= request.height

if (candidate.width > 4096 && request.width <= 4096) {
continue
}

if (request.width < 3840 && request.fps <= 60 && !request.usesNativeDisplayMode &&
(currentMode.width != candidate.width || currentMode.height != candidate.height)
) {
continue
}

if (resolutionReduced && !(request.fps > 60 && resolutionFitsStream)) {
continue
}

// There is no valid HDR baseline yet. The first resolution-compatible HDR mode
// must not be compared against an SDR mode's refresh rate.
if (bestMode != null) {
val refreshRateReduced = candidate.refreshRate < bestMode.refreshRate

if (request.mayReduceRefreshRate && refreshRateIsEqual &&
!isRefreshRateEqualMatch(candidate.refreshRate, request.fps)
) {
continue
} else if (refreshRateIsGood) {
if (!isRefreshRateGoodMatch(candidate.refreshRate, request.fps)) {
continue
}

if (request.mayReduceRefreshRate) {
if (candidate.refreshRate > bestMode.refreshRate) {
continue
}
} else if (refreshRateReduced) {
continue
}
} else if (!isRefreshRateGoodMatch(candidate.refreshRate, request.fps) && refreshRateReduced) {
continue
}
}

bestMode = candidate
refreshRateIsGood = isRefreshRateGoodMatch(candidate.refreshRate, request.fps)
refreshRateIsEqual = isRefreshRateEqualMatch(candidate.refreshRate, request.fps)
}

return Result(bestMode ?: currentMode, hdrFilterApplied)
}

fun isRefreshRateEqualMatch(refreshRate: Float, targetFps: Int): Boolean {
return refreshRate >= targetFps && refreshRate <= targetFps + 3
}

fun isRefreshRateGoodMatch(refreshRate: Float, targetFps: Int): Boolean {
return refreshRate >= targetFps && refreshRate.roundToInt() % targetFps <= 3
}

private fun Mode.supportsAny(acceptableHdrTypes: List<Int>): Boolean {
return hdrTypes.any(acceptableHdrTypes::contains)
}
}
2 changes: 2 additions & 0 deletions app/src/main/jni/moonlight-core/Android.mk
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \
moonlight-common-c/src/ConnectionTester.c \
moonlight-common-c/src/ControlStream.c \
moonlight-common-c/src/CursorStream.c \
moonlight-common-c/src/Ds5HapticsIrStream.c \
moonlight-common-c/src/Ds5HapticsStream.c \
moonlight-common-c/src/FakeCallbacks.c \
moonlight-common-c/src/InputStream.c \
moonlight-common-c/src/LinkedBlockingQueue.c \
Expand Down
105 changes: 105 additions & 0 deletions app/src/test/java/com/limelight/DisplayModePolicyTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.limelight

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class DisplayModePolicyTest {
@Test
fun selectsLowerRefreshHdrModeWhenCurrentModeDoesNotSupportHdr() {
val currentSdrMode = mode(id = 1, refreshRate = 120f)
val hdrMode = mode(id = 2, refreshRate = 60f, hdrTypes = listOf(HDR10))

val result = DisplayModePolicy.selectBestMode(
currentMode = currentSdrMode,
supportedModes = listOf(currentSdrMode, hdrMode),
request = request(acceptableHdrTypes = listOf(HDR10)),
)

assertEquals(hdrMode, result.mode)
assertTrue(result.hdrFilterApplied)
}

@Test
fun keepsCurrentModeWhenItSupportsRequestedHdrType() {
val currentHdrMode = mode(id = 1, refreshRate = 120f, hdrTypes = listOf(HDR10))
val lowerRefreshHdrMode = mode(id = 2, refreshRate = 60f, hdrTypes = listOf(HDR10))

val result = DisplayModePolicy.selectBestMode(
currentMode = currentHdrMode,
supportedModes = listOf(currentHdrMode, lowerRefreshHdrMode),
request = request(acceptableHdrTypes = listOf(HDR10)),
)

assertEquals(currentHdrMode, result.mode)
assertTrue(result.hdrFilterApplied)
}

@Test
fun fallsBackToNormalSelectionWhenNoModeSupportsRequestedHdrType() {
val currentSdrMode = mode(id = 1, refreshRate = 60f)
val fasterSdrMode = mode(id = 2, refreshRate = 120f)

val result = DisplayModePolicy.selectBestMode(
currentMode = currentSdrMode,
supportedModes = listOf(currentSdrMode, fasterSdrMode),
request = request(acceptableHdrTypes = listOf(HDR10)),
)

assertEquals(fasterSdrMode, result.mode)
assertFalse(result.hdrFilterApplied)
}

@Test
fun doesNotRetainHdrCandidateThatViolatesResolutionConstraints() {
val currentSdrMode = mode(id = 1, width = 3840, height = 2160, refreshRate = 120f)
val undersizedHdrMode = mode(
id = 2,
width = 1920,
height = 1080,
refreshRate = 60f,
hdrTypes = listOf(HDR10),
)

val result = DisplayModePolicy.selectBestMode(
currentMode = currentSdrMode,
supportedModes = listOf(currentSdrMode, undersizedHdrMode),
request = request(acceptableHdrTypes = listOf(HDR10)),
)

assertEquals(currentSdrMode, result.mode)
assertTrue(result.hdrFilterApplied)
}

@Test
fun modeEqualityUsesHdrTypeValues() {
assertEquals(
mode(id = 1, refreshRate = 60f, hdrTypes = listOf(HDR10)),
mode(id = 1, refreshRate = 60f, hdrTypes = listOf(HDR10)),
)
}

private fun mode(
id: Int,
width: Int = 3840,
height: Int = 2160,
refreshRate: Float,
hdrTypes: List<Int> = emptyList(),
) = DisplayModePolicy.Mode(id, width, height, refreshRate, hdrTypes)

private fun request(
acceptableHdrTypes: List<Int>,
) = DisplayModePolicy.Request(
width = 3840,
height = 2160,
fps = 60,
usesNativeDisplayMode = true,
mayReduceRefreshRate = false,
acceptableHdrTypes = acceptableHdrTypes,
)

private companion object {
const val HDR10 = 2
}
}
Loading