Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e9b21948 authored by Alec Mouri's avatar Alec Mouri
Browse files

Add common infrastructure for defining a Scene

Define a simple DSL for defining a scene of SurfaceControls.

For example:

val rootScene = scene {
  content { data ->
      // Make a RenderNode
  }
  ...
  // Add more child scenes
  scene {
      ...
  }
  ...
}

Internally, a Scene will build a tree of SurfaceControls and push
content to the display.

Bug: 405591499
Flag: EXEMPT test only
Test: apk runs locally
Change-Id: I9e6ebf9d32967e21a45fc9e3a1f4a71866062a7b
parent c892ddf1
Loading
Loading
Loading
Loading
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.test.transactionflinger

import android.graphics.ColorSpace
import android.graphics.HardwareBufferRenderer
import android.graphics.RenderNode
import android.hardware.HardwareBuffer
import android.view.Choreographer
import android.view.SurfaceControl
import java.util.concurrent.CompletableFuture

/**
 * A scene of SurfaceControls!
 */
class Scene {
    private val children = mutableListOf<Scene>()
    private var drawFunctor: (Scene.(Choreographer.FrameData) -> RenderNode?)? = null

    /**
     * Time that we first started drawing the first frame of the scene
     * Typically this is for rolling your own animations
     */
    var startTime = 0L
        private set

    /**
     * SurfaceControl that will contain the content for this scene on the display
     */
    val surfaceControl: SurfaceControl =
        SurfaceControl.Builder().setName("scene").setHidden(true).build()

    /**
     * Width in pixels
     */
    var width = 0

    /**
     * Height in pixels
     */
    var height = 0

    /**
     * Adds a child scene
     */
    fun scene(init: Scene.() -> Unit): Scene {
        val scene = Scene()
        scene.init()
        children.add(scene)
        return scene
    }

    /**
     * Specifies a function that will instruct this scene node to draw content
     */
    fun content(draw: Scene.(Choreographer.FrameData) -> RenderNode?) {
        drawFunctor = draw
    }

    /**
     * Draw the scene and its children, and accumulate updates into the provided transaction
     */
    fun onDraw(
        data: Choreographer.FrameData,
        transaction: SurfaceControl.Transaction
    ): CompletableFuture<Void> {
        if (startTime == 0L) {
            startTime = data.preferredFrameTimeline.deadlineNanos
            synchronized(transaction) {
                for (child in children) {
                    transaction.reparent(child.surfaceControl, surfaceControl)
                }
            }
        }
        val futuresList: MutableList<CompletableFuture<Void>> = mutableListOf()
        drawFunctor?.invoke(this@Scene, data)?.let { node ->
            val drawFuture = CompletableFuture<Void>()
            futuresList.add(drawFuture)
            val buffer = HardwareBuffer.create(
                width, height, HardwareBuffer.RGBA_8888, 1,
                HardwareBuffer.USAGE_COMPOSER_OVERLAY or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
                        or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
            )

            val renderer = HardwareBufferRenderer(buffer)
            renderer.setContentRoot(node)
            renderer.obtainRenderRequest()
                .setColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
                .draw(
                    Runnable::run
                ) {
                    synchronized(transaction) {
                        transaction.setBuffer(surfaceControl, buffer, it.fence).setVisibility(
                            surfaceControl, true
                        )
                    }
                    drawFuture.complete(null)
                }
        }
        futuresList.addAll(children.asSequence()
            .map { it.onDraw(data, transaction) }
            .toList())

        return CompletableFuture<Void>.allOf(*futuresList.toTypedArray())
    }
}

/**
 * Creates a root level Scene.
 * Oh no, a DSL.
 */
fun scene(init: Scene.() -> Unit): Scene {
    val scene = Scene()
    scene.init()
    return scene
}
 No newline at end of file
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.test.transactionflinger.activities

import android.os.Bundle
import android.view.Choreographer
import android.view.Choreographer.VsyncCallback
import android.view.SurfaceControl
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.WindowCompat
import com.android.test.transactionflinger.Scene

/**
 * Base implementation for an activity containing a Scene
 */
abstract class SceneActivity : ComponentActivity(), SurfaceHolder.Callback, VsyncCallback {
    private lateinit var surfaceView: SurfaceView
    private lateinit var choreographer: Choreographer
    private lateinit var scene: Scene

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Hide the system bars. Ain't dealing with this when we actually start setting up a scene
        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
        windowInsetsController.hide(WindowInsets.Type.systemBars())
        actionBar?.hide()

        choreographer = Choreographer.getInstance()
        scene = obtainScene()
        setContent {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { context ->
                    surfaceView = SurfaceView(context).apply {
                        holder.addCallback(this@SceneActivity)
                    }
                    surfaceView
                }
            )
        }
    }

    override fun surfaceCreated(holder: SurfaceHolder) {}

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        scene.width = width
        scene.height = height
        SurfaceControl.Transaction()
            .reparent(scene.surfaceControl, surfaceView.surfaceControl).apply()
        choreographer.postVsyncCallback(this)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {}

    override fun onVsync(data: Choreographer.FrameData) {
        val transaction = SurfaceControl.Transaction()
        scene.onDraw(data, transaction).get()
        synchronized(transaction) {
            transaction.setFrameTimelineVsync(data.preferredFrameTimeline.vsyncId)
            transaction.apply()
        }
        choreographer.postVsyncCallback(this@SceneActivity)
    }

    abstract fun obtainScene(): Scene
}
 No newline at end of file
+22 −95
Original line number Diff line number Diff line
@@ -17,77 +17,23 @@
package com.android.test.transactionflinger.activities

import android.graphics.Color
import android.graphics.ColorSpace
import android.graphics.HardwareBufferRenderer
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RenderNode
import android.hardware.HardwareBuffer
import android.os.Bundle
import android.view.Choreographer
import android.view.Choreographer.VsyncCallback
import android.view.SurfaceControl
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import android.view.WindowInsets
import com.android.test.transactionflinger.Scene
import com.android.test.transactionflinger.scene
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds

/**
 * Trivial activity. Not very interesting.
 */
class TrivialActivity : ComponentActivity(), SurfaceHolder.Callback, VsyncCallback {
    private lateinit var surfaceView: SurfaceView
    private lateinit var sceneSurfaceControl: SurfaceControl
    private lateinit var choroegrapher: Choreographer
    private var startTime = 0L
    private var width = 0
    private var height = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Hide the system bars. Ain't dealing with this when we actually start setting up a scene
        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
        windowInsetsController.hide(WindowInsets.Type.systemBars())
        actionBar?.hide()

        choroegrapher = Choreographer.getInstance()
        setContent {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { context ->
                    surfaceView = SurfaceView(context).apply {
                        holder.addCallback(this@TrivialActivity)
                    }
                    surfaceView
                }
            )
        }
    }

    override fun surfaceCreated(holder: SurfaceHolder) {}

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        this.width = width
        this.height = height
        sceneSurfaceControl = SurfaceControl.Builder().setBufferSize(width, height).setHidden(true)
            .setParent(surfaceView.surfaceControl).setName("cogsapp").build()
        choroegrapher.postVsyncCallback(this)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {}

    override fun onVsync(data: Choreographer.FrameData) {
        if (startTime == 0L) {
            startTime = data.preferredFrameTimeline.deadlineNanos
        }
class TrivialActivity : SceneActivity(), SurfaceHolder.Callback, VsyncCallback {
    override fun obtainScene(): Scene {
        return scene {
            content { data ->

                val animationTime =
                    ((data.preferredFrameTimeline.deadlineNanos - startTime) % 2.seconds.inWholeNanoseconds).nanoseconds
@@ -97,33 +43,14 @@ class TrivialActivity : ComponentActivity(), SurfaceHolder.Callback, VsyncCallba
                } else {
                    ((2.seconds - animationTime).inWholeMilliseconds * 255.0 / 1.seconds.inWholeMilliseconds).toInt()
                }

                val renderNode = RenderNode("cogsapp")
                renderNode.setPosition(Rect(0, 0, width, height))
                val paint = Paint()
                paint.color = Color.argb(255, red, 0, 0)
                renderNode.beginRecording(width, height).drawPaint(paint)
                renderNode.endRecording()

        // TODO: use a pool of buffers
        val buffer = HardwareBuffer.create(
            width, height, HardwareBuffer.RGBA_8888, 1,
            HardwareBuffer.USAGE_COMPOSER_OVERLAY or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
                    or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
        )

        val renderer = HardwareBufferRenderer(buffer)
        renderer.setContentRoot(renderNode)
        renderer.obtainRenderRequest().setColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)).draw(
            Runnable::run
        ) {
            SurfaceControl.Transaction()
                .setBuffer(sceneSurfaceControl, buffer, it.fence)
                .setVisibility(
                    sceneSurfaceControl, true
                )
                .apply()
            choroegrapher.postVsyncCallback(this@TrivialActivity)
                renderNode
            }
        }
    }
}
 No newline at end of file