Unverified Commit 1fd83777 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Test on multiple render api's (#299)

parent 1202c860
......@@ -4,7 +4,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="jvmTest -Dskiko.test.window.test.enabled=true" />
<option name="scriptParameters" value="jvmTest -Dskiko.test.ui.enabled=true -Dskiko.test.ui.renderApi=all" />
<option name="taskDescriptions">
<list />
</option>
......
......@@ -962,7 +962,9 @@ tasks.withType<Test>().configureEach {
systemProperty("skiko.jar.path", jar.absolutePath)
systemProperty("skiko.test.screenshots.dir", File(project.projectDir, "src/jvmTest/screenshots").absolutePath)
systemProperty("skiko.test.window.test.enabled", System.getProperty("skiko.test.window.test.enabled", "false"))
systemProperty("skiko.test.ui.enabled", System.getProperty("skiko.test.ui.enabled", "false"))
// TODO make "all" by default after we fix all tests
systemProperty("skiko.test.ui.renderApi", System.getProperty("skiko.test.ui.renderApi", "default"))
// Tests should be deterministic, so disable scaling.
// On MacOs we need the actual scale, otherwise we will have aliased screenshots because of scaling.
......
package org.jetbrains.skiko
internal expect inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R
internal expect fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties
internal expect fun currentNanoTime(): Long
\ No newline at end of file
package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.*
internal interface RenderFactory {
fun createRedrawer(layer: SkiaLayer, renderApi: GraphicsApi, properties: SkiaLayerProperties): Redrawer
companion object {
val Default = makeDefaultRenderFactory()
}
}
internal expect fun makeDefaultRenderFactory(): RenderFactory
\ No newline at end of file
package org.jetbrains.skiko
class SkiaLayerProperties(
val isVsyncEnabled: Boolean,
val isVsyncFramelimitFallbackEnabled: Boolean
)
package org.jetbrains.skiko
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.redrawer.MetalRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
internal actual fun createNativeContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler {
TODO()
}
internal actual fun createNativeRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer {
return MetalRedrawer(layer, properties)
}
\ No newline at end of file
......@@ -12,9 +12,7 @@ import platform.Foundation.NSSelectorFromString
import platform.UIKit.*
import platform.darwin.NSObject
actual open class SkiaLayer(
val properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties()
) {
actual open class SkiaLayer {
fun isShowing(): Boolean {
return true
}
......@@ -82,7 +80,7 @@ actual open class SkiaLayer(
// We have ':' in selector to take care of function argument.
view.addGestureRecognizer(UITapGestureRecognizer(controller, NSSelectorFromString("onTap:")))
// TODO: maybe add observer for view.viewDidDisappear() to detach us?
redrawer = MetalRedrawer(this, properties).apply {
redrawer = MetalRedrawer(this).apply {
needRedraw()
}
}
......
......@@ -23,8 +23,7 @@ import kotlin.system.getTimeNanos
import platform.CoreGraphics.CGSizeMake
internal class MetalRedrawer(
private val layer: SkiaLayer,
private val properties: SkiaLayerProperties
private val layer: SkiaLayer
) : Redrawer {
private val contextHandler = MetalContextHandler(layer)
override val renderInfo: String get() = contextHandler.rendererInfo()
......
package org.jetbrains.skiko
internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
return SkiaLayerProperties(true, true)
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
block()
......
package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.*
internal actual fun makeDefaultRenderFactory(): RenderFactory {
return object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer {
TODO()
}
}
}
......@@ -6,8 +6,7 @@ import org.w3c.dom.events.InputEvent
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.MouseEvent
actual open class SkiaLayer(properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties()
) {
actual open class SkiaLayer {
private var state: CanvasRenderer? = null
actual var renderApi: GraphicsApi = GraphicsApi.WEBGL
......
package org.jetbrains.skiko
internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
return SkiaLayerProperties(SkikoProperties.vsyncEnabled, SkikoProperties.vsyncFramelimitFallbackEnabled)
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
synchronized(lock, block)
......
......@@ -2,7 +2,15 @@ package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.*
internal actual fun makeDefaultRenderFactory(): RenderFactory {
internal interface RenderFactory {
fun createRedrawer(layer: SkiaLayer, renderApi: GraphicsApi, properties: SkiaLayerProperties): Redrawer
companion object {
val Default = makeDefaultRenderFactory()
}
}
private fun makeDefaultRenderFactory(): RenderFactory {
return object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
......
......@@ -17,7 +17,7 @@ import javax.swing.UIManager
actual open class SkiaLayer internal constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null,
private val properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties(),
private val properties: SkiaLayerProperties,
private val renderFactory: RenderFactory = RenderFactory.Default
) : JPanel() {
......@@ -47,9 +47,19 @@ actual open class SkiaLayer internal constructor(
internal val backedLayer: HardwareLayer
constructor(
properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties(),
externalAccessibleFactory: ((Component) -> Accessible)? = null
) : this(externalAccessibleFactory, properties, RenderFactory.Default)
externalAccessibleFactory: ((Component) -> Accessible)? = null,
isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled,
isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled,
renderApi: GraphicsApi = SkikoProperties.renderApi,
) : this(
externalAccessibleFactory,
SkiaLayerProperties(
isVsyncEnabled,
isVsyncFramelimitFallbackEnabled,
renderApi
),
RenderFactory.Default
)
val canvas: java.awt.Canvas
get() = backedLayer
......@@ -200,7 +210,7 @@ actual open class SkiaLayer internal constructor(
@Volatile
private var isDisposed = false
internal var redrawer: Redrawer? = null
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue.toMutableList()
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue(properties.renderApi).toMutableList()
private var renderApi_ = fallbackRenderApiQueue[0]
actual var renderApi: GraphicsApi
get() = renderApi_
......
package org.jetbrains.skiko
internal data class SkiaLayerProperties(
val isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled,
val isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled,
val renderApi: GraphicsApi = SkikoProperties.renderApi,
)
......@@ -3,38 +3,38 @@ package org.jetbrains.skiko
// TODO maybe we can get rid of global properties, and pass SkiaLayerProperties to Window -> ComposeWindow -> SkiaLayer
@Suppress("SameParameterValue")
internal object SkikoProperties {
val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true)
val vsyncEnabled: Boolean = property("skiko.vsync.enabled", default = true)
/**
* If vsync is enabled, but platform can't support it (Software renderer, Linux with uninstalled drivers),
* we enable frame limit by the display refresh rate.
*/
val vsyncFramelimitFallbackEnabled: Boolean by property(
val vsyncFramelimitFallbackEnabled: Boolean = property(
"skiko.vsync.framelimit.fallback.enabled", default = true
)
val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false)
val fpsPeriodSeconds: Double by property("skiko.fps.periodSeconds", default = 2.0)
val fpsEnabled: Boolean = property("skiko.fps.enabled", default = false)
val fpsPeriodSeconds: Double = property("skiko.fps.periodSeconds", default = 2.0)
/**
* Show long frames which is longer than [fpsLongFramesMillis].
* If [fpsLongFramesMillis] isn't defined will show frames longer than 1.5 * (1000 / displayRefreshRate)
*/
val fpsLongFramesShow: Boolean by property("skiko.fps.longFrames.show", default = false)
val fpsLongFramesShow: Boolean = property("skiko.fps.longFrames.show", default = false)
val fpsLongFramesMillis: Double? by property("skiko.fps.longFrames.millis", default = null)
val fpsLongFramesMillis: Double? = property("skiko.fps.longFrames.millis", default = null)
val renderApi: GraphicsApi by lazy {
val renderApi: GraphicsApi get() {
val environment = System.getenv("SKIKO_RENDER_API")
val property = System.getProperty("skiko.renderApi")
if (environment != null) {
return if (environment != null) {
parseRenderApi(environment)
} else {
parseRenderApi(property)
}
}
private fun parseRenderApi(text: String?): GraphicsApi {
internal fun parseRenderApi(text: String?): GraphicsApi {
when(text) {
"SOFTWARE" -> return GraphicsApi.SOFTWARE
"DIRECT_SOFTWARE" -> return GraphicsApi.DIRECT_SOFTWARE
......@@ -60,30 +60,28 @@ internal object SkikoProperties {
}
}
val fallbackRenderApiQueue : List<GraphicsApi> by lazy {
val head = renderApi
var renderApiList = mutableListOf<GraphicsApi>()
when (hostOs) {
OS.Linux -> renderApiList = mutableListOf(GraphicsApi.OPENGL, GraphicsApi.DIRECT_SOFTWARE, GraphicsApi.SOFTWARE)
OS.MacOS -> renderApiList = mutableListOf(GraphicsApi.METAL, GraphicsApi.SOFTWARE)
OS.Windows -> renderApiList = mutableListOf(GraphicsApi.DIRECT3D, GraphicsApi.OPENGL, GraphicsApi.DIRECT_SOFTWARE, GraphicsApi.SOFTWARE)
fun fallbackRenderApiQueue(initialApi: GraphicsApi) : List<GraphicsApi> {
var fallbackApis = when (hostOs) {
OS.Linux -> listOf(GraphicsApi.OPENGL, GraphicsApi.DIRECT_SOFTWARE, GraphicsApi.SOFTWARE)
OS.MacOS -> listOf(GraphicsApi.METAL, GraphicsApi.SOFTWARE)
OS.Windows -> listOf(GraphicsApi.DIRECT3D, GraphicsApi.OPENGL, GraphicsApi.DIRECT_SOFTWARE, GraphicsApi.SOFTWARE)
OS.JS, OS.Ios -> TODO("commonize me")
}
renderApiList.remove(head)
listOf(head) + renderApiList
val indexOfInitialApi = fallbackApis.indexOf(initialApi)
if (indexOfInitialApi >= 0) {
fallbackApis = fallbackApis.drop(indexOfInitialApi + 1)
}
return listOf(initialApi) + fallbackApis
}
private fun property(name: String, default: Boolean) = lazy {
private fun property(name: String, default: Boolean) =
System.getProperty(name)?.toBoolean() ?: default
}
private fun property(name: String, default: Double) = lazy {
private fun property(name: String, default: Double) =
System.getProperty(name)?.toDouble() ?: default
}
private fun property(name: String, default: Double?) = lazy {
private fun property(name: String, default: Double?) =
System.getProperty(name)?.toDouble() ?: default
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.delay
import org.jetbrains.skia.*
import org.jetbrains.skiko.util.UiTestScope
import org.jetbrains.skiko.util.UiTestWindow
import org.jetbrains.skiko.util.uiTest
import org.junit.Test
import java.awt.Point
import javax.swing.WindowConstants
import kotlin.math.abs
import kotlin.math.log2
import kotlin.math.roundToInt
import kotlin.math.sqrt
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
class SkiaLayerPerformanceTest {
internal interface PerformanceHelper {
val window: UiTestWindow
val isCollected: Boolean
fun startCollect()
fun printInfo()
}
private fun UiTestScope.performanceHelper(
width: Int,
height: Int,
frameCount: Int,
deviatedTerminalCount: Int
) = object : PerformanceHelper {
override val window = UiTestWindow()
private val expectedDeviatePercent1 = 0.05
private val expectedDeviatePercent2 = 0.15
private val expectedDeviatePercent3 = 0.30
private val expectedDeviatePercentTerminal = 0.50
private val expectedFrameNanos get() = 1E9 / window.layer.backedLayer.getDisplayRefreshRate()
private val frameTimes = mutableListOf<Long>()
private var canCollect = false
val frameTimeDeltas get() = frameTimes.zipWithNext { a, b -> b - a }
override val isCollected get() = frameTimes.size >= frameCount
private fun deviated(deviatePercent: Double) = frameTimeDeltas.filter {
abs(log2(it / expectedFrameNanos)) > log2(1 + deviatePercent)
}
init {
window.setSize(width, height)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.layer.skikoView = object : SkikoView {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
if (canCollect && frameTimes.size < frameCount) {
frameTimes.add(System.nanoTime()) // we check the real time, not the time provided by the argument
}
window.layer.needRedraw()
}
}
window.isUndecorated = true
window.isVisible = true
}
override fun startCollect() {
canCollect = true
}
override fun printInfo() {
println("[Window frame times ($frameCount frames)]")
val millis = frameTimeDeltas.map { it / 1E6 }
println("Deltas " + millis.map { String.format("%.1f", it) })
println("Average %.2f".format(millis.average()))
println("Standard deviation %.2f".format(millis.stddev()))
fun deviateMessage(percent: Double, deviated: List<Long>): String {
val deviatedStr = deviated.map { String.format("%.1f", it / 1E6) }
val percentTStr = (percent * 100).roundToInt()
return "$deviatedStr deviate by $percentTStr%"
}
if (
renderApi == GraphicsApi.SOFTWARE ||
renderApi == GraphicsApi.DIRECT_SOFTWARE
) {
val slowFrames = frameTimeDeltas.filter { it > 1E9 / 55 }
val fastFrames = frameTimeDeltas.filter { it < expectedFrameNanos * 0.5 }
if (slowFrames.size > deviatedTerminalCount) {
val str = slowFrames.map { String.format("%.1f", it / 1E6) }
throw AssertionError("Framerate is too low:\n$str")
}
if (fastFrames.size > deviatedTerminalCount) {
val str = fastFrames.map { String.format("%.1f", it / 1E6) }
throw AssertionError("Framerate is too high:\n$str")
}
} else {
val deviated1 = deviated(expectedDeviatePercent1)
val deviated2 = deviated(expectedDeviatePercent2)
val deviated3 = deviated(expectedDeviatePercent3)
val deviatedTerminal = deviated(expectedDeviatePercentTerminal)
println(deviateMessage(expectedDeviatePercent1, deviated1 - deviated2 - deviated3 - deviatedTerminal))
println(deviateMessage(expectedDeviatePercent2, deviated2 - deviated3 - deviatedTerminal))
println(deviateMessage(expectedDeviatePercent3, deviated3 - deviatedTerminal))
if (deviatedTerminal.size > deviatedTerminalCount) {
throw AssertionError(deviateMessage(expectedDeviatePercentTerminal, deviatedTerminal))
} else {
println(deviateMessage(expectedDeviatePercentTerminal, deviatedTerminal))
}
}
println()
}
private fun List<Double>.stddev(): Double {
val average = average()
fun f(x: Double) = (x - average) * (x - average)
return sqrt(map(::f).average())
}
}
private suspend fun awaitFrameCollection(windows: List<PerformanceHelper>) {
while (!windows.all(PerformanceHelper::isCollected)) {
delay(100)
}
}
@Test
fun `FPS is near display refresh rate (multiple windows)`() = uiTest {
val helpers = (1..3).map { index ->
performanceHelper(width = 40, height = 20, frameCount = 300, deviatedTerminalCount = 20).apply {
window.toFront()
window.location = Point((index + 1) * 200, 200)
}
}
delay(1000)
try {
helpers.forEach { it.startCollect() }
awaitFrameCollection(helpers)
helpers.forEach { it.printInfo() }
} finally {
helpers.forEach { it.window.dispose() }
}
}
}
\ No newline at end of file
......@@ -14,7 +14,9 @@ import org.jetbrains.skia.paragraph.TextStyle
import org.jetbrains.skiko.context.JvmContextHandler
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.util.ScreenshotTestRule
import org.jetbrains.skiko.util.swingTest
import org.jetbrains.skiko.util.UiTestScope
import org.jetbrains.skiko.util.UiTestWindow
import org.jetbrains.skiko.util.uiTest
import org.junit.Assert.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Rule
......@@ -27,24 +29,8 @@ import javax.swing.WindowConstants
import kotlin.random.Random
import kotlin.test.assertTrue
internal open class SkiaWindow(
properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties(),
layerFactory: () -> SkiaLayer = { SkiaLayer(properties) }
) : JFrame() {
val layer = layerFactory()
init {
contentPane.add(layer)
}
override fun dispose() {
layer.dispose()
super.dispose()
}
}
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
class SkiaWindowTest {
class SkiaLayerTest {
private val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.default)
......@@ -64,7 +50,7 @@ class SkiaWindowTest {
val screenshots = ScreenshotTestRule()
@Test
fun `should not leak native windows`() = swingTest {
fun `should not leak native windows`() = uiTest {
assumeTrue(hostOs.isMacOS)
suspend fun createAndDisposeWindow() {
......@@ -100,8 +86,8 @@ class SkiaWindowTest {
}
@Test
fun `render single window`() = swingTest {
val window = SkiaWindow()
fun `render single window`() = uiTest {
val window = UiTestWindow()
try {
window.setLocation(200, 200)
window.setSize(400, 200)
......@@ -124,8 +110,8 @@ class SkiaWindowTest {
}
@Test
fun `render single window before window show`() = swingTest {
val window = SkiaWindow()
fun `render single window before window show`() = uiTest {
val window = UiTestWindow()
try {
window.setLocation(200, 200)
window.preferredSize = Dimension(400, 200)
......@@ -150,8 +136,8 @@ class SkiaWindowTest {
}
@Test
fun `resize window`() = swingTest {
val window = SkiaWindow()
fun `resize window`() = uiTest {
val window = UiTestWindow()
try {
window.setLocation(200, 200)
window.setSize(40, 20)
......@@ -171,8 +157,8 @@ class SkiaWindowTest {
}
@Test
fun `render three windows`() = swingTest {
fun window(color: Color) = SkiaWindow().apply {
fun `render three windows`() = uiTest {
fun window(color: Color) = UiTestWindow().apply {
setLocation(200, 200)
setSize(400, 200)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
......@@ -207,10 +193,10 @@ class SkiaWindowTest {
}
@Test
fun `should call onRender after init, after resize, and only once after needRedraw`() = swingTest {
fun `should call onRender after init, after resize, and only once after needRedraw`() = uiTest {
var renderCount = 0
val window = SkiaWindow()
val window = UiTestWindow()
try {
window.setLocation(200, 200)
window.setSize(40, 20)
......@@ -241,8 +227,8 @@ class SkiaWindowTest {
}
@Test(timeout = 60000)
fun `stress test - open multiple windows`() = swingTest {
fun window(isAnimated: Boolean) = SkiaWindow().apply {
fun `stress test - open multiple windows`() = uiTest {
fun window(isAnimated: Boolean) = UiTestWindow().apply {
setLocation(200, 200)
setSize(40, 20)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
......@@ -256,7 +242,7 @@ class SkiaWindowTest {
}
val random = Random(31415926)
val openedWindows = mutableListOf<SkiaWindow>()
val openedWindows = mutableListOf<UiTestWindow>()
repeat(10) {
val needOpen = random.nextDouble() > 0.5f
......@@ -283,8 +269,8 @@ class SkiaWindowTest {
}
@Test(timeout = 60000)
fun `stress test - resize and paint immediately`() = swingTest {
fun openWindow() = SkiaWindow(
fun `stress test - resize and paint immediately`() = uiTest {
fun openWindow() = UiTestWindow(
properties = SkiaLayerProperties(isVsyncEnabled = false, isVsyncFramelimitFallbackEnabled = true)
).apply {
setLocation(200, 200)
......@@ -306,8 +292,8 @@ class SkiaWindowTest {
}
@Test(timeout = 60000)
fun `stress test - open and paint immediately`() = swingTest {
fun openWindow() = SkiaWindow(
fun `stress test - open and paint immediately`() = uiTest {
fun openWindow() = UiTestWindow(
properties = SkiaLayerProperties(isVsyncEnabled = false, isVsyncFramelimitFallbackEnabled = true)
).apply {
setLocation(200, 200)
......@@ -332,7 +318,7 @@ class SkiaWindowTest {
}
@Test(timeout = 60000)
fun `fallback to software renderer, fail on init context`() = swingTest {
fun `fallback to software renderer, fail on init context`() = uiTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createRedrawer(
......@@ -355,7 +341,7 @@ class SkiaWindowTest {
}
@Test(timeout = 60000)
fun `fallback to software renderer, fail on create redrawer`() = swingTest {
fun `fallback to software renderer, fail on create redrawer`() = uiTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createRedrawer(
......@@ -368,7 +354,7 @@ class SkiaWindowTest {
}
@Test(timeout = 60000)
fun `fallback to software renderer, fail on draw`() = swingTest {
fun `fallback to software renderer, fail on draw`() = uiTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createRedrawer(
......@@ -387,13 +373,9 @@ class SkiaWindowTest {
)
}
private suspend fun testFallbackToSoftware(nonSoftwareRenderFactory: RenderFactory) {
val window = SkiaWindow(
layerFactory = {
SkiaLayer(
renderFactory = OverrideNonSoftwareRenderFactory(nonSoftwareRenderFactory)
)
}
private suspend fun UiTestScope.testFallbackToSoftware(nonSoftwareRenderFactory: RenderFactory) {
val window = UiTestWindow(
renderFactory = OverrideNonSoftwareRenderFactory(nonSoftwareRenderFactory)
)
try {
window.setLocation(200, 200)
......@@ -435,12 +417,12 @@ class SkiaWindowTest {
}
@Test(timeout = 20000)
fun `render continuously empty content without vsync`() = swingTest {
fun `render continuously empty content without vsync`() = uiTest {
val targetDrawCount = 500
var drawCount = 0
val onDrawCompleted = CompletableDeferred<Unit>()
val window = SkiaWindow(
val window = UiTestWindow(
properties = SkiaLayerProperties(
isVsyncEnabled = false,
isVsyncFramelimitFallbackEnabled = true
......@@ -486,10 +468,10 @@ class SkiaWindowTest {
testRenderText(OS.MacOS)
}
private fun testRenderText(os: OS) = swingTest {
private fun testRenderText(os: OS) = uiTest {
assumeTrue(hostOs == os)
val window = SkiaWindow()
val window = UiTestWindow()
try {
window.setLocation(200, 200)
window.setSize(400, 200)
......
package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import org.jetbrains.skia.*
import org.jetbrains.skiko.util.swingTest
import org.junit.Test
import java.awt.Point
import java.awt.event.WindowEvent
import java.util.*
import javax.swing.JFrame
import javax.swing.WindowConstants
import kotlin.math.abs
import kotlin.math.log2
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
class SkiaWindowPerfomanceTest {
// TODO uncomment fontManager and fix native crash (Windows)
/*
J 1680 org.jetbrains.skija.impl.Managed._nInvokeFinalizer(JJ)V (0 bytes) @ 0x0000027907c261f4 [0x0000027907c261a0+0x0000000000000054]
J 1674 c1 org.jetbrains.skija.impl.Managed$CleanerThunk.run()V (31 bytes) @ 0x0000027900aac2cc [0x0000027900aabbe0+0x00000000000006ec]
J 724 c1 jdk.internal.ref.CleanerImpl$PhantomCleanableRef.performCleanup()V java.base@14.0.2 (10 bytes) @ 0x000002790088b3ec [0x000002790088b2c0+0x000000000000012c]
J 723 c1 jdk.internal.ref.PhantomCleanable.clean()V java.base@14.0.2 (16 bytes) @ 0x000002790088b91c [0x000002790088b740+0x00000000000001dc]
j jdk.internal.ref.CleanerImpl.run()V+77 java.base@14.0.2
j java.lang.Thread.run()V+11 java.base@14.0.2
j jdk.internal.misc.InnocuousThread.run()V+20 java.base@14.0.2
v ~StubRoutines::call_stub
native crash in SkiaWindowTest "render single window"
*/
// private val fontManager = FontMgr.getDefault()
// private val fontCollection = FontCollection()
// .setDefaultFontManager(fontManager)
//
// private fun paragraph(size: Float, text: String) =
// ParagraphBuilder(ParagraphStyle(), fontCollection)
// .pushStyle(
// TextStyle()
// .setColor(Color.RED.rgb)
// .setFontSize(size)
// )
// .addText(text)
// .popStyle()
// .build()
private class TestWindow(
width: Int,
height: Int,
private val frameCount: Int,
private val deviatedTerminalCount: Int
) : SkiaWindow() {
private val expectedDeviatePercent1 = 0.05
private val expectedDeviatePercent2 = 0.15
private val expectedDeviatePercent3 = 0.30
private val expectedDeviatePercentTerminal = 0.50
private val expectedFrameNanos get() = 1E9 / layer.backedLayer.getDisplayRefreshRate()
private val frameTimes = mutableListOf<Long>()
private var canCollect = false
val frameTimeDeltas get() = frameTimes.zipWithNext { a, b -> b - a }
val isCollected get() = frameTimes.size >= frameCount
private fun deviated(deviatePercent: Double) = frameTimeDeltas.filter {
abs(log2(it / expectedFrameNanos)) > log2(1 + deviatePercent)
}
init {
setSize(width, height)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.skikoView = object : SkikoView {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
if (canCollect && frameTimes.size < frameCount) {
frameTimes.add(System.nanoTime()) // we check the real time, not the time provided by the argument
}
layer.needRedraw()
}
}
isUndecorated = true
isVisible = true
}
fun startCollect() {
canCollect = true
}
fun printInfo() {
println("[Window frame times ($frameCount frames)]")
val millis = frameTimeDeltas.map { it / 1E6 }
println("Deltas " + millis.map { String.format("%.1f", it) })
println("Average %.2f".format(millis.average()))
println("Standard deviation %.2f".format(millis.stddev()))
fun deviateMessage(percent: Double, deviated: List<Long>): String {
val deviatedStr = deviated.map { String.format("%.1f", it / 1E6) }
val percentTStr = (percent * 100).roundToInt()
return "$deviatedStr deviate by $percentTStr%"
}
val deviated1 = deviated(expectedDeviatePercent1)
val deviated2 = deviated(expectedDeviatePercent2)
val deviated3 = deviated(expectedDeviatePercent3)
val deviatedTerminal = deviated(expectedDeviatePercentTerminal)
println(deviateMessage(expectedDeviatePercent1, deviated1 - deviated2 - deviated3 - deviatedTerminal))
println(deviateMessage(expectedDeviatePercent2, deviated2 - deviated3 - deviatedTerminal))
println(deviateMessage(expectedDeviatePercent3, deviated3 - deviatedTerminal))
if (deviatedTerminal.size > deviatedTerminalCount) {
throw AssertionError(deviateMessage(expectedDeviatePercentTerminal, deviatedTerminal))
} else {
println(deviateMessage(expectedDeviatePercentTerminal, deviatedTerminal))
}
println()
}
private fun List<Double>.stddev(): Double {
val average = average()
fun f(x: Double) = (x - average) * (x - average)
return sqrt(map(::f).average())
}
}
private suspend fun awaitFrameCollection(windows: List<TestWindow>) {
while (!windows.all(TestWindow::isCollected)) {
delay(100)
}
}
@Test
fun `FPS is near display refresh rate (multiple windows)`() = swingTest {
val windows = (1..3).map { index ->
TestWindow(width = 40, height = 20, frameCount = 300, deviatedTerminalCount = 20).apply {
toFront()
location = Point((index + 1) * 200, 200)
}
}
delay(1000)
try {
windows.forEach(TestWindow::startCollect)
awaitFrameCollection(windows)
windows.forEach(TestWindow::printInfo)
} finally {
windows.forEach(TestWindow::close)
}
}
// TODO fix native crash on macOs in previous test if this test is performed before it
/*
Stack: [0x00007ffee09ff000,0x00007ffee11ff000], sp=0x00007ffee11f9630, free space=8169k
Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [libskiko-macos-x64.dylib+0xe999b] decltype(fp((SkRecords::NoOp)())) SkRecord::Record::visit<SkRecords::Draw&>(SkRecords::Draw&) const+0xb
C [libskiko-macos-x64.dylib+0xe940f] SkRecordDraw(SkRecord const&, SkCanvas*, SkPicture const* const*, SkDrawable* const*, int, SkBBoxHierarchy const*, SkPicture::AbortCallback*)+0x20f
C [libskiko-macos-x64.dylib+0x188783] SkBigPicture::playback(SkCanvas*, SkPicture::AbortCallback*) const+0xa3
C [libskiko-macos-x64.dylib+0x65b4b] SkCanvas::onDrawPicture(SkPicture const*, SkMatrix const*, SkPaint const*)+0x13b
C [libskiko-macos-x64.dylib+0x659d6] SkCanvas::drawPicture(SkPicture const*, SkMatrix const*, SkPaint const*)+0x146
C [libskiko-macos-x64.dylib+0x169c4] Java_org_jetbrains_skija_Canvas__1nDrawPicture+0x34
j org.jetbrains.skija.Canvas._nDrawPicture(JJ[FJ)V+0
j org.jetbrains.skija.Canvas.drawPicture(Lorg/jetbrains/skija/Picture;Lorg/jetbrains/skija/Matrix33;Lorg/jetbrains/skija/Paint;)Lorg/jetbrains/skija/Canvas;+27
j org.jetbrains.skija.Canvas.drawPicture(Lorg/jetbrains/skija/Picture;)Lorg/jetbrains/skija/Canvas;+4
j org.jetbrains.skiko.SkiaLayer.draw$skiko()V+229
j org.jetbrains.skiko.redrawer.MacOsRedrawer$drawLayer$1.draw()V+7
*/
//@Test
fun `check FPS (elementary picture)`() = swingTest {
// we don't count FPS straightforward because in window mode there is always vsync (we can get rid of it only in exclusive fullscreen mode)
// FPS will be capped by display's refresh rate.
//
// So we want to reach the point when the FPS is much below refresh rate drawing very big amount of pictures avery frame.
// When we reach that point, we can approximate FPS: FPS = 1000.0 / calculatedFrameTime * picturesPerFrame
val frameCheckCount = 10
val refreshRatePercent = 0.02 // We need to reach FPS = refreshRatePercent * refreshRate
var picturesPerFrame = 1000
val smoothFPSCounter = FPSCounter(count = frameCheckCount)
val onComplete = CompletableDeferred<Unit>()
fun renderer(window: SkiaWindow) = object : SkikoView {
var t1 = Long.MAX_VALUE
val refreshRate = window.graphicsConfiguration.device.displayMode.refreshRate
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
val t2 = System.nanoTime()
val frameTime = (t2 - t1).coerceAtLeast(0)
t1 = t2
val currentFPS = 1E9 / frameTime
val targetFPS = refreshRate * refreshRatePercent
val smoothFPS = smoothFPSCounter.tick()
if (currentFPS > targetFPS) {
picturesPerFrame *= 2
}
if (smoothFPS < targetFPS && smoothFPSCounter.isCountReached) {
onComplete.complete(Unit)
}
val random = Random(123)
repeat(picturesPerFrame) {
canvas.save()
canvas.translate(width * random.nextFloat(), height * random.nextFloat())
canvas.drawTestPicture()
canvas.restore()
}
window.layer.needRedraw()
}
}
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.setSize(400, 400)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.isUndecorated = true
window.isVisible = true
delay(1000)
window.layer.skikoView = renderer(window)
window.layer.needRedraw()
onComplete.await()
val fps = (smoothFPSCounter.fps * picturesPerFrame).roundToInt()
println("FPS is $fps")
println("We draw $picturesPerFrame pictures per frame to avoid VSYNC lag")
} finally {
window.close()
}
}
//private val paragraph by lazy { paragraph(10f, "Text") }
fun Canvas.drawTestPicture() {
save()
clipRect(Rect(2f, 2f, 18f, 18f))
drawRect(Rect(0f, 0f, 20f, 20f), Paint().apply {
color = 0x88FF0000.toInt()
})
drawRRect(RRect.makeLTRB(0f, 0f, 20f, 20f, 4f), Paint().apply {
mode = PaintMode.STROKE
strokeWidth = 2f
color = 0x8800FF00.toInt()
})
drawLine(0f, 0f, 10f, 10f, Paint().apply {
color = 0x880000FF.toInt()
isAntiAlias = true
})
//paragraph.layout(Float.POSITIVE_INFINITY) // TODO fix native crash on Linux (free(): invalid pointer)
//paragraph.paint(this, 0f, 0f)
restore()
}
private class FPSCounter(
private val count: Int
) {
private val times = LinkedList<Long>()
private var t1 = System.nanoTime()
val isCountReached get() = times.size == count
val fps get() = 1E9 / times.average()
fun tick(): Double {
val t2 = System.nanoTime()
val frameTime = t2 - t1
t1 = t2
times.add(frameTime)
if (times.size > count) {
times.removeFirst()
}
return fps
}
}
}
private fun JFrame.close() = dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
\ No newline at end of file
package org.jetbrains.skiko.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import java.awt.GraphicsEnvironment
fun swingTest(block: suspend CoroutineScope.() -> Unit) {
assumeFalse(GraphicsEnvironment.isHeadless())
assumeTrue(System.getProperty("skiko.test.window.test.enabled") != "false")
runBlocking(Dispatchers.Swing) {
block()
}
}
\ No newline at end of file
package org.jetbrains.skiko.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.*
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import java.awt.GraphicsEnvironment
import javax.swing.JFrame
internal fun uiTest(block: suspend UiTestScope.() -> Unit) {
assumeFalse(GraphicsEnvironment.isHeadless())
assumeTrue(System.getProperty("skiko.test.ui.enabled", "false") == "true")
val renderApi = System.getProperty("skiko.test.ui.renderApi", "all")
runBlocking(Dispatchers.Swing) {
if (renderApi == "all") {
SkikoProperties.fallbackRenderApiQueue(SkikoProperties.renderApi).forEach {
println("Testing $it renderApi")
println()
UiTestScope(scope = this, renderApi = it).block()
}
} else {
UiTestScope(scope = this, renderApi = SkikoProperties.parseRenderApi(renderApi)).block()
}
}
}
internal class UiTestScope(
private val scope: CoroutineScope,
val renderApi: GraphicsApi
) : CoroutineScope by scope {
fun UiTestWindow(
properties: SkiaLayerProperties = SkiaLayerProperties(),
renderFactory: RenderFactory = RenderFactory.Default
) = object : UiTestWindow() {
override val layer: SkiaLayer = SkiaLayer(
properties = properties.copy(renderApi = renderApi),
renderFactory = renderFactory
)
init {
contentPane.add(layer)
}
override fun dispose() {
layer.dispose()
super.dispose()
}
}
}
internal abstract class UiTestWindow : JFrame() {
abstract val layer: SkiaLayer
}
package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.context.ContextHandler
internal actual fun createNativeContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler {
TODO()
}
internal actual fun createNativeRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer {
TODO()
}
package org.jetbrains.skiko
actual open class SkiaLayer(properties: SkiaLayerProperties) {
actual open class SkiaLayer {
actual var renderApi: GraphicsApi
get() = TODO("Not yet implemented")
set(value) {}
......
package org.jetbrains.skiko
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOSOpenGLContextHandler
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
internal actual fun createNativeContextHandler(
layer: SkiaLayer, renderApi: GraphicsApi
): ContextHandler = when (renderApi) {
GraphicsApi.OPENGL -> MacOSOpenGLContextHandler(layer)
else -> throw IllegalArgumentException("Unsupported API $renderApi")
}
internal actual fun createNativeRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer {
return MacOsOpenGLRedrawer(layer, properties)
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents
import org.jetbrains.skiko.context.*
import org.jetbrains.skia.*
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import platform.AppKit.*
import platform.Foundation.NSMakeRect
......@@ -14,9 +15,7 @@ import platform.Foundation.addObserver
import platform.darwin.NSObject
import platform.CoreGraphics.CGRectMake
actual open class SkiaLayer(
private val properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties()
) {
actual open class SkiaLayer {
actual var renderApi: GraphicsApi = GraphicsApi.OPENGL
actual val contentScale: Float
get() = if (this::nsView.isInitialized) nsView.window!!.backingScaleFactor.toFloat() else 1.0f
......@@ -138,7 +137,7 @@ actual open class SkiaLayer(
}
}
window.contentView!!.addSubview(nsView)
redrawer = createNativeRedrawer(this, GraphicsApi.OPENGL, properties).apply {
redrawer = MacOsOpenGLRedrawer(this).apply {
syncSize()
needRedraw()
}
......
......@@ -4,7 +4,6 @@ import kotlinx.cinterop.CPointer
import kotlinx.cinterop.useContents
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.SkikoDispatchers
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOSOpenGLContextHandler
......@@ -19,8 +18,7 @@ import platform.QuartzCore.*
import kotlin.system.getTimeNanos
internal class MacOsOpenGLRedrawer(
private val skiaLayer: SkiaLayer,
private val properties: SkiaLayerProperties
private val skiaLayer: SkiaLayer
) : Redrawer {
private val contextHandler = MacOSOpenGLContextHandler(skiaLayer)
override val renderInfo: String get() = contextHandler.rendererInfo()
......
package org.jetbrains.skiko
internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
return SkiaLayerProperties(true, true)
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
block()
......
package org.jetbrains.skiko
import org.jetbrains.skiko.context.*
import org.jetbrains.skiko.redrawer.*
internal expect fun createNativeContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler
internal expect fun createNativeRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer
internal actual fun makeDefaultRenderFactory(): RenderFactory {
return object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer {
return createNativeRedrawer(layer, renderApi, properties)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment