mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
preview: draw the car screen straight into a SurfaceView
Each frame used to be copied into two new full-size bitmaps on the main thread, so the bigger the preview, the further taps lagged. The display now draws into the preview's own surface, the way the car's encoder gets it, and turning the phone hands the surface over without restarting the car UI.
This commit is contained in:
1 parent
18571622d8
commit
32bef88a75
2 files changed
+34
-59
No files matched your search
@@ -51,7 +51,7 @@ class ScreenProjector(private val context: Context) {
|
|||||||
* preview passes "gearslip-preview".
|
* preview passes "gearslip-preview".
|
||||||
*/
|
*/
|
||||||
fun start(
|
fun start(
|
||||||
surface: Surface,
|
surface: Surface?,
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
densityDpi: Int,
|
densityDpi: Int,
|
||||||
@@ -219,6 +219,14 @@ class ScreenProjector(private val context: Context) {
|
|||||||
target.dispatchKeyEvent(event)
|
target.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points the display at another surface, or none, without restarting what it shows. The
|
||||||
|
* preview's SurfaceView comes and goes as the phone turns; the car UI on it carries on.
|
||||||
|
*/
|
||||||
|
fun setSurface(surface: Surface?) {
|
||||||
|
virtualDisplay?.surface = surface
|
||||||
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
runCatching { presentation?.dismiss() }
|
runCatching { presentation?.dismiss() }
|
||||||
runCatching { owner?.destroy() }
|
runCatching { owner?.destroy() }
|
||||||
|
|||||||
@@ -4,15 +4,8 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.PixelFormat
|
|
||||||
import android.hardware.HardwareBuffer
|
|
||||||
import android.media.ImageReader
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
@@ -39,9 +32,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -59,7 +50,7 @@ import app.seb3thehacker.gearslip.car.CarUi
|
|||||||
* Runs the real car UI on the bench and shows it on the phone.
|
* Runs the real car UI on the bench and shows it on the phone.
|
||||||
*
|
*
|
||||||
* This is the same pipeline the car gets - a VirtualDisplay hosting a Presentation, driven by
|
* This is the same pipeline the car gets - a VirtualDisplay hosting a Presentation, driven by
|
||||||
* the same [ScreenProjector] and fed the same touches - with an ImageReader standing in for the
|
* the same [ScreenProjector] and fed the same touches - with a SurfaceView standing in for the
|
||||||
* H.264 encoder. That matters for anything composited rather than drawn by Compose, such as the
|
* H.264 encoder. That matters for anything composited rather than drawn by Compose, such as the
|
||||||
* SurfaceView a templated app draws its map into: if it appears here, it will reach the car.
|
* SurfaceView a templated app draws its map into: if it appears here, it will reach the car.
|
||||||
*
|
*
|
||||||
@@ -69,7 +60,6 @@ import app.seb3thehacker.gearslip.car.CarUi
|
|||||||
fun CarPreviewScreen(onBack: () -> Unit) {
|
fun CarPreviewScreen(onBack: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val frame by CarEnvironment.frame.collectAsStateWithLifecycle()
|
val frame by CarEnvironment.frame.collectAsStateWithLifecycle()
|
||||||
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
|
||||||
var viewSize by remember { mutableStateOf(0 to 0) }
|
var viewSize by remember { mutableStateOf(0 to 0) }
|
||||||
// Tied to how the phone itself is held, not a button: turn it sideways and the preview goes
|
// Tied to how the phone itself is held, not a button: turn it sideways and the preview goes
|
||||||
// fullscreen, turn it back and the phone's own chrome (the Back/title above the frame) returns.
|
// fullscreen, turn it back and the phone's own chrome (the Back/title above the frame) returns.
|
||||||
@@ -83,35 +73,11 @@ fun CarPreviewScreen(onBack: () -> Unit) {
|
|||||||
|
|
||||||
DisposableEffect(width, height, density) {
|
DisposableEffect(width, height, density) {
|
||||||
CarEnvironment.start(context)
|
CarEnvironment.start(context)
|
||||||
|
// No surface yet: the SurfaceView below hands its own over once it exists, and the
|
||||||
// COMPOSER_OVERLAY is what makes SurfaceFlinger willing to composite into this reader;
|
// display draws straight into it. Copying each frame into a Bitmap instead cost two
|
||||||
// CPU_READ_OFTEN is what makes the resulting buffer lockable, which is how the frame
|
// full-size allocations per frame on the main thread, and taps lagged with the size.
|
||||||
// gets copied into a Bitmap. Without both, frames arrive but every copy fails.
|
projector.start(null, width, height, density, name = "gearslip-preview") { CarUi() }
|
||||||
val reader = ImageReader.newInstance(
|
onDispose { projector.stop() }
|
||||||
width, height, PixelFormat.RGBA_8888, 3,
|
|
||||||
HardwareBuffer.USAGE_COMPOSER_OVERLAY or
|
|
||||||
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or
|
|
||||||
HardwareBuffer.USAGE_CPU_READ_OFTEN,
|
|
||||||
)
|
|
||||||
val handler = Handler(Looper.getMainLooper())
|
|
||||||
reader.setOnImageAvailableListener({ source ->
|
|
||||||
val image = runCatching { source.acquireLatestImage() }.getOrNull() ?: return@setOnImageAvailableListener
|
|
||||||
try {
|
|
||||||
bitmap = image.toBitmap(width, height)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
GearslipLog.e("preview: could not copy a frame", t)
|
|
||||||
} finally {
|
|
||||||
image.close()
|
|
||||||
}
|
|
||||||
}, handler)
|
|
||||||
|
|
||||||
projector.start(reader.surface, width, height, density, name = "gearslip-preview") { CarUi() }
|
|
||||||
|
|
||||||
onDispose {
|
|
||||||
projector.stop()
|
|
||||||
reader.close()
|
|
||||||
bitmap = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The frame is drawn the same way in both layouts; only what surrounds it differs.
|
// The frame is drawn the same way in both layouts; only what surrounds it differs.
|
||||||
@@ -140,14 +106,27 @@ fun CarPreviewScreen(onBack: () -> Unit) {
|
|||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
bitmap?.let {
|
androidx.compose.runtime.key(width, height) {
|
||||||
Image(
|
androidx.compose.ui.viewinterop.AndroidView(
|
||||||
it.asImageBitmap(),
|
factory = { viewContext ->
|
||||||
contentDescription = "Car screen",
|
android.view.SurfaceView(viewContext).apply {
|
||||||
|
// The car's own size; the compositor scales it to the view.
|
||||||
|
holder.setFixedSize(width, height)
|
||||||
|
holder.addCallback(object : android.view.SurfaceHolder.Callback {
|
||||||
|
override fun surfaceCreated(holder: android.view.SurfaceHolder) =
|
||||||
|
projector.setSurface(holder.surface)
|
||||||
|
|
||||||
|
override fun surfaceChanged(holder: android.view.SurfaceHolder, format: Int, w: Int, h: Int) =
|
||||||
|
projector.setSurface(holder.surface)
|
||||||
|
|
||||||
|
override fun surfaceDestroyed(holder: android.view.SurfaceHolder) =
|
||||||
|
projector.setSurface(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
)
|
)
|
||||||
} ?: Text("Waiting for a frame…", color = Color.White)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,18 +229,6 @@ private fun Int.toCarAction(): Int = when (this) {
|
|||||||
* Copies one frame out of the reader. The reader's rows are padded to a hardware-friendly
|
* Copies one frame out of the reader. The reader's rows are padded to a hardware-friendly
|
||||||
* stride, so the copy is made at the padded width and then cropped back.
|
* stride, so the copy is made at the padded width and then cropped back.
|
||||||
*/
|
*/
|
||||||
private fun android.media.Image.toBitmap(width: Int, height: Int): Bitmap {
|
|
||||||
val plane = planes[0]
|
|
||||||
val pixelStride = plane.pixelStride
|
|
||||||
val rowPadding = plane.rowStride - pixelStride * width
|
|
||||||
val padded = Bitmap.createBitmap(
|
|
||||||
width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888,
|
|
||||||
)
|
|
||||||
padded.copyPixelsFromBuffer(plane.buffer)
|
|
||||||
return if (padded.width == width) padded
|
|
||||||
else Bitmap.createBitmap(padded, 0, 0, width, height).also { padded.recycle() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size the preview draws at. The last car's frame by default; this device's whole screen,
|
* The size the preview draws at. The last car's frame by default; this device's whole screen,
|
||||||
* for a head unit running Gearslip on its own (issue #12); or a size typed into Settings. The
|
* for a head unit running Gearslip on its own (issue #12); or a size typed into Settings. The
|
||||||
|
|||||||
Reference in new issue
Block a user