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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.0-test256 (2026-10-22)
- Fixes from the UI review:
- **Menus show all their options:** dialog rows are a little tighter and the room is worked out more truly, so a PC game's menu (Play here, Play on a TV, Change picture, Hide…) no longer hides options below a scroll.
- **One Frame pacing** for PC and PS5, in Picture ("PC and PS5…; Xbox paces itself"). The PlayStation group's own Frame pacing is gone; the PS5 now reads the Picture one (Moonlight's Cap FPS counts as Balanced). A PS5-only choice saved before is no longer used.
- **Add apps on Home** is a quiet tile (plain, with + and its name) instead of a bright cover.
- **Top bar:** "Playing on <TV>" takes at most half the screen and a long TV name ends in "…" (it could run off the screen upright).
- **Settings groups:** the selected group has the same rounded corner as the rest of the app, and the focus ring follows it.

## 1.0-test255 (2026-10-22)
- UI review on the Odin (test254, 20 screens): Home, All games (every tab), Add apps, the game menus, Consoles and every Settings group. Measured: cold start 327 ms; janky frames 0.6% moving along Home, 0.4% scrolling All games, 0.5% moving down the Settings groups (5-12 ms frames).
- Fixed: lists ran on under the hint bar (Consoles' "Find my Xbox" drew over the controller name; a sliver of All games' next row showed below it). A view is clipped by its parent and the content area doesn't clip, so the All games grid, the list pages and the Settings list now sit in a frame that clips them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ class HandheldHomeActivity : Activity() {
v.getTag(R.id.hh_pill) == true -> 9999f
t != null && (t.startsWith("rb:") || t.startsWith("pill:") || t.startsWith("tab:") || t.startsWith("dock:") || t.startsWith("chip:")) -> 9999f
t != null && t.startsWith("app:") -> sw.pxf(13 + 22)
t != null && t.startsWith("group:") -> sw.pxf(13 + 16)
t != null && t.startsWith("g:") -> sw.pxf(13 + sw.tileCorner)
else -> null
}
Expand Down Expand Up @@ -676,7 +677,7 @@ class HandheldHomeActivity : Activity() {
tv != null -> "Playing on ${tv.name}" + if (TvFinder.isHere(tv)) "" else " (not found)"
portrait -> "This ${deviceShort()}"
else -> "Playing on this ${deviceShort()}"
}, 26, sw.ink, 700))
}, 26, sw.ink, 700).apply { maxWidth = resources.displayMetrics.widthPixels / 2 }) // a long TV name ends in …
setOnClickListener { playWhere(null) }
isFocusable = false // a tap target only; the pad starts on the games
isSoundEffectsEnabled = !FeelSounds.enabled(this@HandheldHomeActivity)
Expand Down Expand Up @@ -890,7 +891,8 @@ class HandheldHomeActivity : Activity() {
if (page != Page.GAMES) f.elevation = sw.pxf(8)
sw.pressable(f)
if (g == null) {
f.background = if (tag == "g:all") sw.allArt() else sw.art("add") // g:add and g:devadd
// All games and Add apps are quiet tiles next to the games; Add a console (nothing paired yet) stands out
f.background = when (tag) { "g:all" -> sw.allArt(); "g:devadd" -> sw.round(sw.tile, sw.tileCorner); else -> sw.art("add") }
if (tag != "g:all") f.addView(sw.text("+", size * 0.3f, 0xFFFFFFFF.toInt(), 600).apply { gravity = Gravity.CENTER }, FrameLayout.LayoutParams(-1, -1))
// All games doesn't show the name above the grid, so Add apps carries its own
if (tag == "g:devadd") f.addView(sw.text(label, titleSize, 0xFFFFFFFF.toInt(), 800).apply {
Expand Down Expand Up @@ -1402,7 +1404,7 @@ class HandheldHomeActivity : Activity() {
val on = k == settingsGroup
side.addView(FrameLayout(this).apply {
tag = "group:$k"
if (on) background = sw.round(sw.panel, 6)
if (on) background = sw.round(sw.panel, 16)
if (on) addView(View(this@HandheldHomeActivity).apply { background = sw.round(sw.ring, 3) }, sw.flp(6, -1).apply { topMargin = sw.px(18); bottomMargin = sw.px(18) })
addView(sw.text(label, 30, if (on) sw.ring else sw.sub, 700).apply { gravity = Gravity.CENTER_VERTICAL; setPadding(sw.px(28), 0, 0, 0) }, FrameLayout.LayoutParams(-1, -1))
sw.focusable(this)
Expand Down Expand Up @@ -1503,7 +1505,7 @@ class HandheldHomeActivity : Activity() {
toggle(list, "checkbox_full_range", "Full range colour", "Experimental: deeper blacks and brighter whites. On screens that don't handle it, dark and light detail is lost", false)
choice(list, stream, "list_video_scale_mode", "Picture shape", "Fit with bars, fill and crop, or stretch",
listOf("Fit" to "fit", "Fill" to "fill", "Stretch" to "stretch"), if (stream.getBoolean("checkbox_stretch_video", false)) "stretch" else "fit")
choice(list, stream, "frame_pacing", "Frame pacing", "Trade a little delay for smoother motion",
choice(list, stream, "frame_pacing", "Frame pacing", "PC and PS5: trade a little delay for smoother motion. Xbox paces itself",
listOf("Lowest delay" to "latency", "Balanced" to "balanced", "Smoothest" to "smoothness"), "latency")
toggle(list, "checkbox_enable_perf_overlay", "Stats while playing", "Frame rate, delay and lost frames in a corner", false)
}
Expand Down Expand Up @@ -1577,8 +1579,6 @@ class HandheldHomeActivity : Activity() {
toggle(list, "ps5_swap_cross_circle", "Swap Cross and Circle", "For the Japanese layout", false)
choice(list, stream, com.limelight.Ps5StreamActivity.DECODER_PREF, "Decoder", "Low latency shows each frame sooner. Ultra pushes this device's chip harder. Standard if the picture misbehaves",
listOf("Low latency" to "low", "Ultra low latency" to "ultra", "Standard" to "standard"), "low")
choice(list, stream, com.limelight.Ps5StreamActivity.PACING_PREF, "Frame pacing", "Balanced and Smoothest space the frames evenly, for up to 1 or 3 frames more delay",
listOf("Lowest delay" to "latency", "Balanced" to "balanced", "Smoothest" to "smoothness"), "latency")
val pin = prefs.getString("ps5_login_pin", "") ?: ""
action(list, "pspin", "Console PIN", "Signs in for you when the PS5 asks for its 4-digit login PIN", if (pin.isEmpty()) "Not set" else "••••") {
entry("Console PIN", "The 4-digit PIN your PS5 asks for when it starts. Leave empty to turn it off.", "4 digits", pin, true) { v ->
Expand Down Expand Up @@ -2181,8 +2181,8 @@ class HandheldHomeActivity : Activity() {
private fun optRow(o: Opt, tag: String): View = sw.row().apply {
this.tag = tag
background = sw.round(sw.bg, 18)
setPadding(sw.px(26), sw.px(20), sw.px(28), sw.px(20))
minimumHeight = sw.px(116)
setPadding(sw.px(26), sw.px(14), sw.px(28), sw.px(14))
minimumHeight = sw.px(100)
if (!o.enabled) alpha = 0.45f
// a symbol fills its square; a short word (PC, PS, TV) stays the size of a badge
if (o.icon != null) addView(sw.text(o.icon, if (o.icon.length == 1) 36 else 23, 0xFFFFFFFF.toInt(), 800).apply { gravity = Gravity.CENTER; background = sw.round(o.iconColor, ROW_ICON_R) },
Expand All @@ -2196,7 +2196,7 @@ class HandheldHomeActivity : Activity() {
sw.focusable(this)
setOnClickListener { val n = FeelSounds.serial; hideOverlay(false); o.onClick(); if (n == FeelSounds.serial) FeelSounds.play(this@HandheldHomeActivity, FeelSounds.Cue.SELECT) }
}
layoutParams = sw.lp(-1, -2).apply { leftMargin = sw.px(40); rightMargin = sw.px(40); bottomMargin = sw.px(16) }
layoutParams = sw.lp(-1, -2).apply { leftMargin = sw.px(40); rightMargin = sw.px(40); bottomMargin = sw.px(12) }
}

/** A dialog's buttons along its bottom: rounded buttons side by side; the last is where focus starts. */
Expand Down Expand Up @@ -2232,9 +2232,10 @@ class HandheldHomeActivity : Activity() {
val list = sw.col()
opts.forEachIndexed { i, o -> list.addView(optRow(o, if (i == firstOn) "primary" else "opt:$i")) }
// as many options as the screen has room for; past that the list scrolls, cut through the middle of a row
// so it's plain there's more (a row is about 132 tall with its gap)
val fit = ((sw.designHeight - 620) / 132f).toInt().coerceIn(3, 8)
if (opts.size > fit) card.addView(ColScroll(this, sw.px(30)).apply { isVerticalScrollBarEnabled = false; addView(list) }, sw.lp(-1, fit * 132 - 66))
// so it's plain there's more. A row is about 112 with its gap; title, text and buttons take about 440 (it was
// 620 and 132, so a PC game's menu showed three options and hid the rest below a scroll)
val fit = ((sw.designHeight - 440) / 112f).toInt().coerceIn(3, 8)
if (opts.size > fit) card.addView(ColScroll(this, sw.px(30)).apply { isVerticalScrollBarEnabled = false; addView(list) }, sw.lp(-1, fit * 112 - 56))
else card.addView(list, sw.lp(-1, -2))
buttons(card, btns, primary = firstOn < 0, primaryAt = primaryAt)
showOverlay("dialog", card)
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/com/limelight/Ps5StreamActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class Ps5StreamActivity : Activity(), SurfaceHolder.Callback, com.limelight.home
const val DECODER_PREF = "ps5_decoder_latency"

/** Settings, PlayStation, Frame pacing: latency (default), balanced or smoothness, like the PC's. */
const val PACING_PREF = "ps5_frame_pacing"
/** Picture, Frame pacing: one setting for PC and PS5 (Moonlight's own key). It was a separate PS5 one. */
const val PACING_PREF = "frame_pacing"

/** Stop a PSN connect in flight; older libraries without psnCancel just let it time out. */
private fun cancelPsn() {
Expand Down Expand Up @@ -161,7 +162,8 @@ class Ps5StreamActivity : Activity(), SurfaceHolder.Callback, com.limelight.home
(prefs.getString(if (away) "away_fps_custom" else "ps5_fps_home_custom", "60")?.toIntOrNull() ?: 60) < 45
else fpsSel == "30"
// Settings, PlayStation, Frame pacing: frames spaced at this stream's rate (lowest delay, balanced, smoothest)
val pacing = when (prefs.getString(PACING_PREF, "latency")) { "balanced" -> 1; "smoothness" -> 2; else -> 0 }
// Moonlight's All settings also offers "cap-fps": the nearest PS5 mode for it is Balanced
val pacing = when (prefs.getString(PACING_PREF, "latency")) { "balanced", "cap-fps" -> 1; "smoothness" -> 2; else -> 0 }
try { com.metallic.chiaki.lib.decoderPacing(pacing, if (fps30) 30 else 60) } catch (e: UnsatisfiedLinkError) { }
Log.i(TAG, "frame pacing $pacing at ${if (fps30) 30 else 60} fps")
val preset = ConnectVideoProfile.preset(
Expand Down
4 changes: 1 addition & 3 deletions app/src/tv/java/com/limelight/home/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@ class HomeActivity : Activity(), PhoneLink.Listener {
if (stream.getBoolean("checkbox_stretch_video", false)) "stretch" else "fit")
intChoice(list, "seekbar_video_zoom", "Zoom", "Enlarge the picture if this TV cuts off the edges", listOf(100, 102, 104, 106, 108, 110), 100) {
if (it == 100) "Off" else "$it%" }
choice(list, "frame_pacing", "Frame pacing", "Trade a little delay for smoother motion",
choice(list, "frame_pacing", "Frame pacing", "PC and PS5: trade a little delay for smoother motion. Xbox paces itself",
listOf("Lowest delay" to "latency", "Balanced" to "balanced", "Smoothest" to "smoothness"), "latency")
toggle(list, "checkbox_ultra_low_latency", "Ultra low latency",
"Experimental. Snapdragon 8 Gen 2 or newer, MediaTek, NVIDIA Shield. This device: ${chipName()}", false)
Expand Down Expand Up @@ -1094,8 +1094,6 @@ class HomeActivity : Activity(), PhoneLink.Listener {
toggle(list, "ps5_swap_cross_circle", "Swap Cross and Circle", "For the Japanese layout", false)
choice(list, com.limelight.Ps5StreamActivity.DECODER_PREF, "Decoder", "Low latency shows each frame sooner. Ultra pushes this TV's chip harder. Standard if the picture misbehaves",
listOf("Low latency" to "low", "Ultra low latency" to "ultra", "Standard" to "standard"), "low")
choice(list, com.limelight.Ps5StreamActivity.PACING_PREF, "Frame pacing", "Balanced and Smoothest space the frames evenly, for up to 1 or 3 frames more delay",
listOf("Lowest delay" to "latency", "Balanced" to "balanced", "Smoothest" to "smoothness"), "latency")
val pin = prefs.getString("ps5_login_pin", "") ?: ""
action(list, "set:pspin", "Console PIN", "Signs in for you when the PS5 asks for its 4-digit login PIN",
if (pin.isEmpty()) "Not set" else "••••") { enterLoginPin() }
Expand Down
Loading