fix(android): use OpenGLES on 32-bit TCL TVs
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
internal enum class FlutterRenderer(
|
||||
val diagnosticName: String,
|
||||
val shellArgument: String?
|
||||
) {
|
||||
SKIA("Skia", "--enable-impeller=false"),
|
||||
IMPELLER("Impeller", null),
|
||||
IMPELLER_OPEN_GLES("Impeller (OpenGLES)", "--impeller-backend=opengles")
|
||||
}
|
||||
|
||||
/** Selects the Flutter UI renderer before the engine starts. */
|
||||
internal object FlutterRendererPolicy {
|
||||
private const val ANDROID_12_API = 31
|
||||
|
||||
fun select(
|
||||
isEWaste: Boolean,
|
||||
manufacturer: String,
|
||||
isAndroidTv: Boolean,
|
||||
sdkInt: Int,
|
||||
supportsVulkan11: Boolean,
|
||||
is64Bit: Boolean
|
||||
): FlutterRenderer {
|
||||
if (isEWaste) return FlutterRenderer.SKIA
|
||||
if (manufacturer.equals("NVIDIA", ignoreCase = true)) return FlutterRenderer.SKIA
|
||||
if (manufacturer.equals("Huawei", ignoreCase = true) ||
|
||||
manufacturer.equals("HONOR", ignoreCase = true)
|
||||
) {
|
||||
return FlutterRenderer.SKIA
|
||||
}
|
||||
if (!isAndroidTv) return FlutterRenderer.IMPELLER
|
||||
if (sdkInt < ANDROID_12_API || manufacturer.equals("Amazon", ignoreCase = true) || !supportsVulkan11) {
|
||||
return FlutterRenderer.SKIA
|
||||
}
|
||||
|
||||
// PowerVR BXE-4-32 drivers in TCL's 32-bit Android 12 TV platform leave
|
||||
// stale Vulkan frames on screen while scrolling (#1658, flutter/flutter#189767).
|
||||
// Impeller's OpenGLES backend renders correctly on the same hardware.
|
||||
if (!is64Bit && manufacturer.equals("TCL", ignoreCase = true)) {
|
||||
return FlutterRenderer.IMPELLER_OPEN_GLES
|
||||
}
|
||||
|
||||
return FlutterRenderer.IMPELLER
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class MainActivity : FlutterActivity() {
|
||||
// "2GB" devices report totalMem slightly above 2 GiB after carve-outs.
|
||||
private const val LOW_MEM_THRESHOLD_BYTES = 2252L shl 20
|
||||
|
||||
var usingSkia = false
|
||||
private var selectedFlutterRenderer = FlutterRenderer.IMPELLER
|
||||
}
|
||||
|
||||
private val PIP_CHANNEL = "com.plezy/pip"
|
||||
@@ -469,43 +469,38 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun getFlutterShellArgs(): FlutterShellArgs {
|
||||
val args = super.getFlutterShellArgs()
|
||||
usingSkia = shouldDisableImpeller()
|
||||
if (usingSkia) args.add("--enable-impeller=false")
|
||||
selectedFlutterRenderer = selectFlutterRenderer()
|
||||
selectedFlutterRenderer.shellArgument?.let { args.add(it) }
|
||||
if (isLowRamClass()) {
|
||||
// Bound the memory pools Dart can't reach: Skia's GPU resource cache
|
||||
// is sized from the surface area (hundreds of MB on a 4K-composited
|
||||
// TV) and the Dart old gen defaults to a large fraction of physical
|
||||
// RAM. Both drive LMK kills on 2GB boxes (#1349).
|
||||
if (usingSkia) args.add("--resource-cache-max-bytes-threshold=50331648")
|
||||
if (selectedFlutterRenderer == FlutterRenderer.SKIA) {
|
||||
args.add("--resource-cache-max-bytes-threshold=50331648")
|
||||
}
|
||||
args.add("--old-gen-heap-size=256")
|
||||
Log.i(TAG, "Low-RAM device: capped engine caches (skia=$usingSkia, oldGen=256MB)")
|
||||
Log.i(
|
||||
TAG,
|
||||
"Low-RAM device: capped engine caches " +
|
||||
"(renderer=${selectedFlutterRenderer.diagnosticName}, oldGen=256MB)"
|
||||
)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
private fun shouldDisableImpeller(): Boolean {
|
||||
if (DeviceQuirks.isEWaste) return true
|
||||
// NVIDIA Tegra (Shield TV)
|
||||
if (Build.MANUFACTURER.equals("NVIDIA", ignoreCase = true)) return true
|
||||
// Huawei/HONOR Kirin SoCs use Mali GPUs
|
||||
if (Build.MANUFACTURER.equals("Huawei", ignoreCase = true) ||
|
||||
Build.MANUFACTURER.equals("HONOR", ignoreCase = true)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (isAndroidTvDevice()) return !tvSupportsImpeller()
|
||||
return false
|
||||
}
|
||||
|
||||
// Impeller froze API 30 Fire TV hardware (#749) and Flutter's Vulkan → GLES
|
||||
// fallback still miscompiles gradients/SVGs, so only TV devices on Android 12+
|
||||
// with a Vulkan 1.1 driver leave the Skia path.
|
||||
private fun tvSupportsImpeller(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
||||
// Fire OS reports modern API levels on GPUs whose drivers can't back it up
|
||||
if (Build.MANUFACTURER.equals("Amazon", ignoreCase = true)) return false
|
||||
private fun selectFlutterRenderer(): FlutterRenderer {
|
||||
val isAndroidTv = isAndroidTvDevice()
|
||||
val vulkan11 = 0x401000 // FEATURE_VULKAN_HARDWARE_VERSION encodes 1.1.0 as 0x401000
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, vulkan11)
|
||||
return FlutterRendererPolicy.select(
|
||||
isEWaste = DeviceQuirks.isEWaste,
|
||||
manufacturer = Build.MANUFACTURER,
|
||||
isAndroidTv = isAndroidTv,
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
supportsVulkan11 = isAndroidTv &&
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, vulkan11),
|
||||
is64Bit = Process.is64Bit()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRenderMode(): RenderMode {
|
||||
@@ -628,7 +623,7 @@ class MainActivity : FlutterActivity() {
|
||||
// Splash screen theme: persist user's chosen theme for next launch (API 31+)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, THEME_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getRenderer" -> result.success(if (usingSkia) "Skia" else "Impeller")
|
||||
"getRenderer" -> result.success(selectedFlutterRenderer.diagnosticName)
|
||||
"setSplashTheme" -> {
|
||||
val mode = call.argument<String>("mode")
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class FlutterRendererPolicyTest {
|
||||
|
||||
@Test
|
||||
fun affected32BitTclTvUsesImpellerOpenGles() {
|
||||
val renderer = select(manufacturer = "TCL", is64Bit = false)
|
||||
|
||||
assertEquals(FlutterRenderer.IMPELLER_OPEN_GLES, renderer)
|
||||
assertEquals("--impeller-backend=opengles", renderer.shellArgument)
|
||||
assertEquals("Impeller (OpenGLES)", renderer.diagnosticName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun capable64BitTclTvKeepsAutomaticImpellerBackend() {
|
||||
assertEquals(FlutterRenderer.IMPELLER, select(manufacturer = "TCL", is64Bit = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unsupportedTvsStayOnSkia() {
|
||||
assertEquals(FlutterRenderer.SKIA, select(sdkInt = 30))
|
||||
assertEquals(FlutterRenderer.SKIA, select(supportsVulkan11 = false))
|
||||
assertEquals(FlutterRenderer.SKIA, select(manufacturer = "Amazon"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun existingDeviceDenylistStillTakesPrecedence() {
|
||||
assertEquals(FlutterRenderer.SKIA, select(isEWaste = true, isAndroidTv = false))
|
||||
assertEquals(FlutterRenderer.SKIA, select(manufacturer = "NVIDIA", isAndroidTv = false))
|
||||
assertEquals(FlutterRenderer.SKIA, select(manufacturer = "Huawei", isAndroidTv = false))
|
||||
assertEquals(FlutterRenderer.SKIA, select(manufacturer = "HONOR", isAndroidTv = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ordinaryAndroidDevicesKeepAutomaticImpellerBackend() {
|
||||
assertEquals(
|
||||
FlutterRenderer.IMPELLER,
|
||||
select(manufacturer = "Samsung", isAndroidTv = false, sdkInt = 28, supportsVulkan11 = false)
|
||||
)
|
||||
}
|
||||
|
||||
private fun select(
|
||||
isEWaste: Boolean = false,
|
||||
manufacturer: String = "Google",
|
||||
isAndroidTv: Boolean = true,
|
||||
sdkInt: Int = 31,
|
||||
supportsVulkan11: Boolean = true,
|
||||
is64Bit: Boolean = true
|
||||
): FlutterRenderer = FlutterRendererPolicy.select(
|
||||
isEWaste = isEWaste,
|
||||
manufacturer = manufacturer,
|
||||
isAndroidTv = isAndroidTv,
|
||||
sdkInt = sdkInt,
|
||||
supportsVulkan11 = supportsVulkan11,
|
||||
is64Bit = is64Bit
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user