Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 3 additions & 2 deletions app/src/main/java/com/limelight/ConnectionCallbackHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,11 @@ class ConnectionCallbackHandler(private val game: Game) {
shortcutHelper.reportGameLaunched(computer, game.app!!)
}

// 检查是否启用了HDR并主动设置初始状态
// Prepare the output pipeline when HDR is expected. This is intentionally separate from
// the host setHdrMode callback so diagnostics don't claim HDR before the stream activates.
val appSupportsHdr = game.intent.getBooleanExtra(Game.EXTRA_APP_HDR, false)
if (appSupportsHdr && game.prefConfig.enableHdr) {
game.setHdrMode(true, null)
game.prepareInitialHdrOutput()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// 初始化麦克风管理器
Expand Down
41 changes: 37 additions & 4 deletions app/src/main/java/com/limelight/DisplayModeManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ object DisplayModeManager {
/** Immutable display mode choice bound to the display whose mode ids it references. */
data class DisplayModeSelection(
val displayId: Int,
val selectedModeId: Int,
val refreshRate: Float,
val preferredModeId: Int,
val useSetFrameRate: Boolean,
Expand Down Expand Up @@ -60,21 +61,44 @@ object DisplayModeManager {
return false
}

fun selectBestDisplayMode(display: Display, prefConfig: PreferenceConfiguration): DisplayModeSelection {
fun selectBestDisplayMode(
display: Display,
prefConfig: PreferenceConfiguration,
acceptableHdrTypes: IntArray = IntArray(0),
): DisplayModeSelection {
val displayRefreshRate: Float
var selectedModeId = -1
var preferredModeId = -1
var useSetFrameRate = false
var aspectRatioMatch = false

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
var bestMode = display.mode
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()
}
} else {
supportedModes.asList()
}

var bestMode = eligibleModes.firstOrNull { it.modeId == display.mode.modeId }
?: eligibleModes.firstOrNull()
?: display.mode
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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 display.supportedModes) {
for (candidate in eligibleModes) {
val refreshRateReduced = candidate.refreshRate < bestMode.refreshRate
val resolutionReduced = candidate.physicalWidth < bestMode.physicalWidth ||
candidate.physicalHeight < bestMode.physicalHeight
Expand Down Expand Up @@ -129,7 +153,14 @@ object DisplayModeManager {
LimeLog.info("Best display mode: ${bestMode.physicalWidth}x${bestMode.physicalHeight}x${bestMode.refreshRate}")

if (display.mode.modeId != bestMode.modeId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || UiHelper.isColorOS() ||
// setFrameRate() requests only a refresh rate, so Android may choose a different
// same-resolution mode whose HDR types don't match the mode we validated above.
// Pin the exact mode whenever mode-specific HDR capabilities influenced selection.
val requiresExactHdrMode =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
acceptableHdrTypes.isNotEmpty()
if (requiresExactHdrMode ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S || UiHelper.isColorOS() ||
display.mode.physicalWidth != bestMode.physicalWidth ||
display.mode.physicalHeight != bestMode.physicalHeight
) {
Expand All @@ -143,6 +174,7 @@ object DisplayModeManager {
}

displayRefreshRate = bestMode.refreshRate
selectedModeId = bestMode.modeId
} else {
@Suppress("DEPRECATION")
var bestRefreshRate = display.refreshRate
Expand Down Expand Up @@ -179,6 +211,7 @@ object DisplayModeManager {

return DisplayModeSelection(
displayId = display.displayId,
selectedModeId = selectedModeId,
refreshRate = displayRefreshRate,
preferredModeId = preferredModeId,
useSetFrameRate = useSetFrameRate,
Expand Down
106 changes: 88 additions & 18 deletions app/src/main/java/com/limelight/Game.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.limelight.ui.StreamView
import com.limelight.utils.Dialog
import com.limelight.utils.PanZoomHandler
import com.limelight.utils.FullscreenProgressOverlay
import com.limelight.utils.HdrCapabilityHelper
import com.limelight.utils.UiHelper
import com.limelight.utils.NetHelper
import com.limelight.utils.AnalyticsManager
Expand Down Expand Up @@ -732,6 +733,7 @@ class Game : Activity(), SurfaceHolder.Callback,

// endregion

@SuppressLint("InlinedApi")
private fun buildStreamConfiguration(
host: String?, port: Int, httpsPort: Int,
uniqueId: String?, pairName: String?,
Expand All @@ -740,28 +742,60 @@ class Game : Activity(), SurfaceHolder.Callback,
): StreamConfigResult {
val glPrefs = GlPreferences.readPreferences(this)
val connMgr = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val acceptableHdrTypes = if (prefConfig.enableHdr &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
) {
if (prefConfig.hdrMode == MoonBridge.HDR_MODE_HLG) {
intArrayOf(Display.HdrCapabilities.HDR_TYPE_HLG)
} else {
// HDR10+ is backward compatible with HDR10. Keep refresh-rate selection free to
// choose either mode, then enable dynamic metadata only if the selected mode has it.
intArrayOf(
Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS,
Display.HdrCapabilities.HDR_TYPE_HDR10,
)
}
} else {
IntArray(0)
}
val displayModeSelection = selectDisplayModeForRendering(acceptableHdrTypes)
val hdrTypeSupport = HdrCapabilityHelper.getHdrTypeSupport(
currentTargetDisplay,
displayModeSelection.selectedModeId,
)

var willStreamHdr = false
if (prefConfig.enableHdr) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val hdrCaps = currentTargetDisplay.hdrCapabilities

if (hdrCaps != null) {
for (hdrType in hdrCaps.supportedHdrTypes) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
willStreamHdr = true
break
}
}
willStreamHdr = if (prefConfig.hdrMode == MoonBridge.HDR_MODE_HLG) {
hdrTypeSupport.hasHlg
} else {
hdrTypeSupport.hasHdr10 || hdrTypeSupport.hasHdr10Plus
}
if (!willStreamHdr) {
Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show()
val requiredType = if (prefConfig.hdrMode == MoonBridge.HDR_MODE_HLG) "HLG" else "HDR10"
Toast.makeText(this, "Display mode does not support $requiredType", Toast.LENGTH_LONG).show()
}
} else {
Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show()
}
}

// HDR10+ metadata cannot survive the ImageReader/frame-generation path yet. Keep that
// path on static HDR10 until frame generation explicitly supports dynamic metadata.
val framegenRequested = shouldUseFramegen()
val hdr10PlusRequested = willStreamHdr &&
prefConfig.hdrMode == MoonBridge.HDR_MODE_HDR10 &&
hdrTypeSupport.hasHdr10Plus &&
!framegenRequested
if (willStreamHdr &&
prefConfig.hdrMode == MoonBridge.HDR_MODE_HDR10 &&
hdrTypeSupport.hasHdr10Plus &&
framegenRequested
) {
LimeLog.info("HDR10+ disabled while frame generation is enabled; using static HDR10")
}

if (decoderRenderer == null) {
decoderRenderer = MediaCodecDecoderRenderer(
this, prefConfig,
Expand All @@ -777,6 +811,7 @@ class Game : Activity(), SurfaceHolder.Callback,
tombstonePrefs.getInt("CrashCount", 0),
connMgr.isActiveNetworkMetered,
willStreamHdr,
hdr10PlusRequested,
glPrefs.glRenderer,
this
)
Expand All @@ -793,9 +828,26 @@ class Game : Activity(), SurfaceHolder.Callback,
}
}

if (willStreamHdr && decoderRenderer?.isHevcMain10Supported() != true && decoderRenderer?.isAv1Main10Supported() != true) {
val hevcHdrSupported = if (prefConfig.hdrMode == MoonBridge.HDR_MODE_HLG) {
decoderRenderer?.isHevcMain10Supported() == true
} else {
decoderRenderer?.isHevcMain10Hdr10Supported() == true
}
val av1HdrSupported = if (prefConfig.hdrMode == MoonBridge.HDR_MODE_HLG) {
decoderRenderer?.isAv1Main10Supported() == true
} else {
decoderRenderer?.isAv1Main10Hdr10Supported() == true
}
val selectedCodecSupportsHdr = when (prefConfig.videoFormat) {
PreferenceConfiguration.FormatOption.FORCE_HEVC -> hevcHdrSupported
PreferenceConfiguration.FormatOption.FORCE_AV1 -> av1HdrSupported
PreferenceConfiguration.FormatOption.FORCE_H264 -> false
PreferenceConfiguration.FormatOption.AUTO -> hevcHdrSupported || av1HdrSupported
}
if (willStreamHdr && !selectedCodecSupportsHdr) {
willStreamHdr = false
Toast.makeText(this, "Decoder does not support HDR10 profile", Toast.LENGTH_LONG).show()
val requiredProfile = if (prefConfig.hdrMode == MoonBridge.HDR_MODE_HLG) "Main10/HLG" else "HDR10"
Toast.makeText(this, "Decoder does not support $requiredProfile profile", Toast.LENGTH_LONG).show()
}

if (prefConfig.videoFormat == PreferenceConfiguration.FormatOption.FORCE_HEVC && decoderRenderer?.isHevcSupported() != true) {
Expand All @@ -808,13 +860,13 @@ class Game : Activity(), SurfaceHolder.Callback,
var supportedVideoFormats = MoonBridge.VIDEO_FORMAT_H264
if (decoderRenderer?.isHevcSupported() == true) {
supportedVideoFormats = supportedVideoFormats or MoonBridge.VIDEO_FORMAT_H265
if (willStreamHdr && decoderRenderer?.isHevcMain10Supported() == true) {
if (willStreamHdr && hevcHdrSupported) {
supportedVideoFormats = supportedVideoFormats or MoonBridge.VIDEO_FORMAT_H265_MAIN10
}
}
if (decoderRenderer?.isAv1Supported() == true) {
supportedVideoFormats = supportedVideoFormats or MoonBridge.VIDEO_FORMAT_AV1_MAIN8
if (willStreamHdr && decoderRenderer?.isAv1Main10Supported() == true) {
if (willStreamHdr && av1HdrSupported) {
supportedVideoFormats = supportedVideoFormats or MoonBridge.VIDEO_FORMAT_AV1_MAIN10
}
}
Expand All @@ -827,7 +879,7 @@ class Game : Activity(), SurfaceHolder.Callback,
gamepadMask = gamepadMask or 1
}

val displayRefreshRate = prepareDisplayForRendering()
val displayRefreshRate = applyDisplayModeForRendering(displayModeSelection)
LimeLog.info("Display refresh rate: $displayRefreshRate Hz")

performanceOverlayManager?.setActualDisplayRefreshRate(displayRefreshRate)
Expand Down Expand Up @@ -1153,7 +1205,9 @@ class Game : Activity(), SurfaceHolder.Callback,
}
}

private fun prepareDisplayForRendering(): Float {
private fun selectDisplayModeForRendering(
acceptableHdrTypes: IntArray = IntArray(0),
): DisplayModeManager.DisplayModeSelection {
val display = currentTargetDisplay

val presentationFps = framegenPresentationFps()
Expand All @@ -1168,7 +1222,12 @@ class Game : Activity(), SurfaceHolder.Callback,
LimeLog.info("Framegen display target FPS: ${prefConfig.fps} -> ${displayConfig.fps}")
}

val selection = DisplayModeManager.selectBestDisplayMode(display, displayConfig)
return DisplayModeManager.selectBestDisplayMode(display, displayConfig, acceptableHdrTypes)
}

private fun applyDisplayModeForRendering(
selection: DisplayModeManager.DisplayModeSelection,
): Float {
selectedDisplayMode = selection

if (!targetDisplayResolver.isExternalDisplaySelected()) {
Expand Down Expand Up @@ -1735,10 +1794,20 @@ class Game : Activity(), SurfaceHolder.Callback,

override fun setHdrMode(enabled: Boolean, hdrMetadata: ByteArray?) {
LimeLog.info("Display HDR mode: ${if (enabled) "enabled" else "disabled"}")
applyHdrOutputState(enabled)
decoderRenderer?.setHdrMode(enabled, hdrMetadata)
}

/** Prepares the window/framegen path without fabricating a host HDR activation event. */
internal fun prepareInitialHdrOutput() {
LimeLog.info("Preparing initial HDR output state")
applyHdrOutputState(true)
}

private fun applyHdrOutputState(enabled: Boolean) {
framegenInputHdrEnabled = enabled
val framegenHdrMode = if (enabled) prefConfig.hdrMode else MoonBridge.HDR_MODE_SDR
FramegenInterceptor.configureHdrMode(framegenHdrMode, shouldUseFullRangeHdr(framegenHdrMode))
decoderRenderer?.setHdrMode(enabled, hdrMetadata)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notifySystemHdrStatus(enabled)
Expand Down Expand Up @@ -2271,6 +2340,7 @@ class Game : Activity(), SurfaceHolder.Callback,
if (controllerManager != null && performanceInfoDisplays.isNotEmpty()) {
val perfAttrs = HashMap<String, String>()
perfAttrs[getString(R.string.perf_decoder)] = performanceInfo.decoder ?: ""
perfAttrs[getString(R.string.perf_hdr_format)] = performanceInfo.hdrFormat.displayName
perfAttrs[getString(R.string.perf_resolution)] = "${performanceInfo.initialWidth}x${performanceInfo.initialHeight}"
perfAttrs[getString(R.string.perf_fps)] = String.format("%.0f", performanceInfo.totalFps)
perfAttrs[getString(R.string.perf_rx_fps)] = String.format("%.0f", performanceInfo.receivedFps)
Expand Down
7 changes: 3 additions & 4 deletions app/src/main/java/com/limelight/PerformanceOverlayManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,8 @@ class PerformanceOverlayManager(

private fun buildDecoderInfo(performanceInfo: PerformanceInfo): String {
val decoderTypeInfo = getDecoderTypeInfo(performanceInfo.decoder)
// NBSP (\u00A0) 防止 TextView 在 "H265 HDR" 的空格处断行
return if (performanceInfo.isHdrActive) "${decoderTypeInfo.shortName}\u00A0HDR"
else decoderTypeInfo.shortName
// NBSP (\u00A0) keeps the codec and dynamic-range format on one line.
return "${decoderTypeInfo.shortName}\u00A0${performanceInfo.hdrFormat.displayName}"
}

private fun getCurrentMoonPhaseIcon(): String {
Expand Down Expand Up @@ -1085,7 +1084,7 @@ class PerformanceOverlayManager(
decoderInfo.append("Codec: ").append(perfInfo.decoder).append("\n\n")
val decoderTypeInfo = getDecoderTypeInfo(perfInfo.decoder)
decoderInfo.append("Type: ").append(decoderTypeInfo.fullName).append("\n")
decoderInfo.append("HDR: ").append(if (perfInfo.isHdrActive) "Enabled" else "Disabled").append("\n")
decoderInfo.append("Dynamic range: ").append(perfInfo.hdrFormat.displayName).append("\n")
}
decoderInfo.append(activity.getString(R.string.perf_decoder_info))
return decoderInfo.toString()
Expand Down
Loading
Loading