Loading tests/graphics/TransactionFlinger/Android.bp 0 → 100644 +42 −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 { default_team: "trendy_team_android_core_graphics_stack", // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], } android_test { name: "TransactionFlinger", srcs: [ "**/*.kt", ], platform_apis: true, certificate: "platform", static_libs: [ "androidx.activity_activity-compose", "androidx.appcompat_appcompat", "androidx.compose.foundation_foundation-layout", "androidx.compose.runtime_runtime", "androidx.compose.ui_ui", "androidx.core_core", ], } tests/graphics/TransactionFlinger/AndroidManifest.xml 0 → 100644 +39 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.test.transactionflinger"> <uses-sdk android:minSdkVersion="35"/> <application android:label="TransactionFlinger" android:theme="@android:style/Theme.Material"> <activity android:name=".Main" android:label="TransactionFlinger" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> </intent-filter> </activity> <activity android:name=".activities.TrivialActivity" /> </application> </manifest> tests/graphics/TransactionFlinger/src/com/android/test/transactionflinger/Main.kt 0 → 100644 +124 −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.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseExpandableListAdapter import android.widget.ExpandableListView import android.widget.TextView import androidx.activity.ComponentActivity import com.android.test.transactionflinger.activities.TrivialActivity import kotlin.reflect.KClass class Demo(val name: String, val makeIntent: (Context) -> Intent) { constructor(name: String, activity: KClass<out Activity>) : this(name, { context -> Intent(context, activity.java) }) } data class DemoGroup(val groupName: String, val demos: List<Demo>) private val AllDemos = listOf( DemoGroup( "Workloads", listOf( Demo("TrivialActivity", TrivialActivity::class) ) ) ) /** * Main entry point when manually opening the app */ class Main : ComponentActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val list = ExpandableListView(this) list.setFitsSystemWindows(true) setContentView(list) val inflater = LayoutInflater.from(this) list.setAdapter(object : BaseExpandableListAdapter() { override fun getGroup(groupPosition: Int): DemoGroup { return AllDemos[groupPosition] } override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean = true override fun hasStableIds(): Boolean = true override fun getGroupView( groupPosition: Int, isExpanded: Boolean, convertView: View?, parent: ViewGroup? ): View { val view = (convertView ?: inflater.inflate( android.R.layout.simple_expandable_list_item_1, parent, false )) as TextView view.text = AllDemos[groupPosition].groupName return view } override fun getChildrenCount(groupPosition: Int): Int { return AllDemos[groupPosition].demos.size } override fun getChild(groupPosition: Int, childPosition: Int): Demo { return AllDemos[groupPosition].demos[childPosition] } override fun getGroupId(groupPosition: Int): Long = groupPosition.toLong() override fun getChildView( groupPosition: Int, childPosition: Int, isLastChild: Boolean, convertView: View?, parent: ViewGroup? ): View { val view = (convertView ?: inflater.inflate( android.R.layout.simple_expandable_list_item_1, parent, false )) as TextView view.text = AllDemos[groupPosition].demos[childPosition].name return view } override fun getChildId(groupPosition: Int, childPosition: Int): Long { return (groupPosition.toLong() shl 32) or childPosition.toLong() } override fun getGroupCount(): Int { return AllDemos.size } }) list.setOnChildClickListener { _, _, groupPosition, childPosition, _ -> val demo = AllDemos[groupPosition].demos[childPosition] startActivity(demo.makeIntent(this)) return@setOnChildClickListener true } AllDemos.forEachIndexed { index, _ -> list.expandGroup(index) } } } tests/graphics/TransactionFlinger/src/com/android/test/transactionflinger/activities/TrivialActivity.kt 0 → 100644 +129 −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.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 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 } val animationTime = ((data.preferredFrameTimeline.deadlineNanos - startTime) % 2.seconds.inWholeNanoseconds).nanoseconds val red = if (animationTime < 1.seconds) { (animationTime.inWholeMilliseconds * 255.0 / 1.seconds.inWholeMilliseconds).toInt() } 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) } } } No newline at end of file Loading
tests/graphics/TransactionFlinger/Android.bp 0 → 100644 +42 −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 { default_team: "trendy_team_android_core_graphics_stack", // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], } android_test { name: "TransactionFlinger", srcs: [ "**/*.kt", ], platform_apis: true, certificate: "platform", static_libs: [ "androidx.activity_activity-compose", "androidx.appcompat_appcompat", "androidx.compose.foundation_foundation-layout", "androidx.compose.runtime_runtime", "androidx.compose.ui_ui", "androidx.core_core", ], }
tests/graphics/TransactionFlinger/AndroidManifest.xml 0 → 100644 +39 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.test.transactionflinger"> <uses-sdk android:minSdkVersion="35"/> <application android:label="TransactionFlinger" android:theme="@android:style/Theme.Material"> <activity android:name=".Main" android:label="TransactionFlinger" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> </intent-filter> </activity> <activity android:name=".activities.TrivialActivity" /> </application> </manifest>
tests/graphics/TransactionFlinger/src/com/android/test/transactionflinger/Main.kt 0 → 100644 +124 −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.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseExpandableListAdapter import android.widget.ExpandableListView import android.widget.TextView import androidx.activity.ComponentActivity import com.android.test.transactionflinger.activities.TrivialActivity import kotlin.reflect.KClass class Demo(val name: String, val makeIntent: (Context) -> Intent) { constructor(name: String, activity: KClass<out Activity>) : this(name, { context -> Intent(context, activity.java) }) } data class DemoGroup(val groupName: String, val demos: List<Demo>) private val AllDemos = listOf( DemoGroup( "Workloads", listOf( Demo("TrivialActivity", TrivialActivity::class) ) ) ) /** * Main entry point when manually opening the app */ class Main : ComponentActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val list = ExpandableListView(this) list.setFitsSystemWindows(true) setContentView(list) val inflater = LayoutInflater.from(this) list.setAdapter(object : BaseExpandableListAdapter() { override fun getGroup(groupPosition: Int): DemoGroup { return AllDemos[groupPosition] } override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean = true override fun hasStableIds(): Boolean = true override fun getGroupView( groupPosition: Int, isExpanded: Boolean, convertView: View?, parent: ViewGroup? ): View { val view = (convertView ?: inflater.inflate( android.R.layout.simple_expandable_list_item_1, parent, false )) as TextView view.text = AllDemos[groupPosition].groupName return view } override fun getChildrenCount(groupPosition: Int): Int { return AllDemos[groupPosition].demos.size } override fun getChild(groupPosition: Int, childPosition: Int): Demo { return AllDemos[groupPosition].demos[childPosition] } override fun getGroupId(groupPosition: Int): Long = groupPosition.toLong() override fun getChildView( groupPosition: Int, childPosition: Int, isLastChild: Boolean, convertView: View?, parent: ViewGroup? ): View { val view = (convertView ?: inflater.inflate( android.R.layout.simple_expandable_list_item_1, parent, false )) as TextView view.text = AllDemos[groupPosition].demos[childPosition].name return view } override fun getChildId(groupPosition: Int, childPosition: Int): Long { return (groupPosition.toLong() shl 32) or childPosition.toLong() } override fun getGroupCount(): Int { return AllDemos.size } }) list.setOnChildClickListener { _, _, groupPosition, childPosition, _ -> val demo = AllDemos[groupPosition].demos[childPosition] startActivity(demo.makeIntent(this)) return@setOnChildClickListener true } AllDemos.forEachIndexed { index, _ -> list.expandGroup(index) } } }
tests/graphics/TransactionFlinger/src/com/android/test/transactionflinger/activities/TrivialActivity.kt 0 → 100644 +129 −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.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 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 } val animationTime = ((data.preferredFrameTimeline.deadlineNanos - startTime) % 2.seconds.inWholeNanoseconds).nanoseconds val red = if (animationTime < 1.seconds) { (animationTime.inWholeMilliseconds * 255.0 / 1.seconds.inWholeMilliseconds).toInt() } 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) } } } No newline at end of file