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 @@ ...@@ -4,7 +4,7 @@
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <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"> <option name="taskDescriptions">
<list /> <list />
</option> </option>
......
...@@ -962,7 +962,9 @@ tasks.withType<Test>().configureEach { ...@@ -962,7 +962,9 @@ tasks.withType<Test>().configureEach {
systemProperty("skiko.jar.path", jar.absolutePath) systemProperty("skiko.jar.path", jar.absolutePath)
systemProperty("skiko.test.screenshots.dir", File(project.projectDir, "src/jvmTest/screenshots").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. // Tests should be deterministic, so disable scaling.
// On MacOs we need the actual scale, otherwise we will have aliased screenshots because of scaling. // On MacOs we need the actual scale, otherwise we will have aliased screenshots because of scaling.
......
package org.jetbrains.skiko package org.jetbrains.skiko
internal expect inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R internal expect inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R
internal expect fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties
internal expect fun currentNanoTime(): Long 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 ...@@ -12,9 +12,7 @@ import platform.Foundation.NSSelectorFromString
import platform.UIKit.* import platform.UIKit.*
import platform.darwin.NSObject import platform.darwin.NSObject
actual open class SkiaLayer( actual open class SkiaLayer {
val properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties()
) {
fun isShowing(): Boolean { fun isShowing(): Boolean {
return true return true
} }
...@@ -82,7 +80,7 @@ actual open class SkiaLayer( ...@@ -82,7 +80,7 @@ actual open class SkiaLayer(
// We have ':' in selector to take care of function argument. // We have ':' in selector to take care of function argument.
view.addGestureRecognizer(UITapGestureRecognizer(controller, NSSelectorFromString("onTap:"))) view.addGestureRecognizer(UITapGestureRecognizer(controller, NSSelectorFromString("onTap:")))
// TODO: maybe add observer for view.viewDidDisappear() to detach us? // TODO: maybe add observer for view.viewDidDisappear() to detach us?
redrawer = MetalRedrawer(this, properties).apply { redrawer = MetalRedrawer(this).apply {
needRedraw() needRedraw()
} }
} }
......
...@@ -23,8 +23,7 @@ import kotlin.system.getTimeNanos ...@@ -23,8 +23,7 @@ import kotlin.system.getTimeNanos
import platform.CoreGraphics.CGSizeMake import platform.CoreGraphics.CGSizeMake
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer
private val properties: SkiaLayerProperties
) : Redrawer { ) : Redrawer {
private val contextHandler = MetalContextHandler(layer) private val contextHandler = MetalContextHandler(layer)
override val renderInfo: String get() = contextHandler.rendererInfo() override val renderInfo: String get() = contextHandler.rendererInfo()
......
package org.jetbrains.skiko package org.jetbrains.skiko
internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
return SkiaLayerProperties(true, true)
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R = internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
block() 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 ...@@ -6,8 +6,7 @@ import org.w3c.dom.events.InputEvent
import org.w3c.dom.events.KeyboardEvent import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.MouseEvent import org.w3c.dom.events.MouseEvent
actual open class SkiaLayer(properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties() actual open class SkiaLayer {
) {
private var state: CanvasRenderer? = null private var state: CanvasRenderer? = null
actual var renderApi: GraphicsApi = GraphicsApi.WEBGL actual var renderApi: GraphicsApi = GraphicsApi.WEBGL
......
package org.jetbrains.skiko 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 = internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
synchronized(lock, block) synchronized(lock, block)
......
...@@ -2,7 +2,15 @@ package org.jetbrains.skiko ...@@ -2,7 +2,15 @@ package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.* 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 { return object : RenderFactory {
override fun createRedrawer( override fun createRedrawer(
layer: SkiaLayer, layer: SkiaLayer,
......
...@@ -17,7 +17,7 @@ import javax.swing.UIManager ...@@ -17,7 +17,7 @@ import javax.swing.UIManager
actual open class SkiaLayer internal constructor( actual open class SkiaLayer internal constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null, externalAccessibleFactory: ((Component) -> Accessible)? = null,
private val properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties(), private val properties: SkiaLayerProperties,
private val renderFactory: RenderFactory = RenderFactory.Default private val renderFactory: RenderFactory = RenderFactory.Default
) : JPanel() { ) : JPanel() {
...@@ -47,9 +47,19 @@ actual open class SkiaLayer internal constructor( ...@@ -47,9 +47,19 @@ actual open class SkiaLayer internal constructor(
internal val backedLayer: HardwareLayer internal val backedLayer: HardwareLayer
constructor( constructor(
properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties(), externalAccessibleFactory: ((Component) -> Accessible)? = null,
externalAccessibleFactory: ((Component) -> Accessible)? = null isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled,
) : this(externalAccessibleFactory, properties, RenderFactory.Default) isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled,
renderApi: GraphicsApi = SkikoProperties.renderApi,
) : this(
externalAccessibleFactory,
SkiaLayerProperties(
isVsyncEnabled,
isVsyncFramelimitFallbackEnabled,
renderApi
),
RenderFactory.Default
)
val canvas: java.awt.Canvas val canvas: java.awt.Canvas
get() = backedLayer get() = backedLayer
...@@ -200,7 +210,7 @@ actual open class SkiaLayer internal constructor( ...@@ -200,7 +210,7 @@ actual open class SkiaLayer internal constructor(
@Volatile @Volatile
private var isDisposed = false private var isDisposed = false
internal var redrawer: Redrawer? = null internal var redrawer: Redrawer? = null
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue.toMutableList() private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue(properties.renderApi).toMutableList()
private var renderApi_ = fallbackRenderApiQueue[0] private var renderApi_ = fallbackRenderApiQueue[0]
actual var renderApi: GraphicsApi actual var renderApi: GraphicsApi
get() = renderApi_ 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 ...@@ -3,38 +3,38 @@ package org.jetbrains.skiko
// TODO maybe we can get rid of global properties, and pass SkiaLayerProperties to Window -> ComposeWindow -> SkiaLayer // TODO maybe we can get rid of global properties, and pass SkiaLayerProperties to Window -> ComposeWindow -> SkiaLayer
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
internal object SkikoProperties { 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), * 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. * 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 "skiko.vsync.framelimit.fallback.enabled", default = true
) )
val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false) val fpsEnabled: Boolean = property("skiko.fps.enabled", default = false)
val fpsPeriodSeconds: Double by property("skiko.fps.periodSeconds", default = 2.0) val fpsPeriodSeconds: Double = property("skiko.fps.periodSeconds", default = 2.0)
/** /**
* Show long frames which is longer than [fpsLongFramesMillis]. * Show long frames which is longer than [fpsLongFramesMillis].
* If [fpsLongFramesMillis] isn't defined will show frames longer than 1.5 * (1000 / displayRefreshRate) * 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 environment = System.getenv("SKIKO_RENDER_API")
val property = System.getProperty("skiko.renderApi") val property = System.getProperty("skiko.renderApi")
if (environment != null) { return if (environment != null) {
parseRenderApi(environment) parseRenderApi(environment)
} else { } else {
parseRenderApi(property) parseRenderApi(property)
} }
} }
private fun parseRenderApi(text: String?): GraphicsApi { internal fun parseRenderApi(text: String?): GraphicsApi {
when(text) { when(text) {
"SOFTWARE" -> return GraphicsApi.SOFTWARE "SOFTWARE" -> return GraphicsApi.SOFTWARE
"DIRECT_SOFTWARE" -> return GraphicsApi.DIRECT_SOFTWARE "DIRECT_SOFTWARE" -> return GraphicsApi.DIRECT_SOFTWARE
...@@ -60,30 +60,28 @@ internal object SkikoProperties { ...@@ -60,30 +60,28 @@ internal object SkikoProperties {
} }
} }
val fallbackRenderApiQueue : List<GraphicsApi> by lazy { fun fallbackRenderApiQueue(initialApi: GraphicsApi) : List<GraphicsApi> {
val head = renderApi var fallbackApis = when (hostOs) {
var renderApiList = mutableListOf<GraphicsApi>() OS.Linux -> listOf(GraphicsApi.OPENGL, GraphicsApi.DIRECT_SOFTWARE, GraphicsApi.SOFTWARE)
OS.MacOS -> listOf(GraphicsApi.METAL, GraphicsApi.SOFTWARE)
when (hostOs) { OS.Windows -> listOf(GraphicsApi.DIRECT3D, GraphicsApi.OPENGL, GraphicsApi.DIRECT_SOFTWARE, GraphicsApi.SOFTWARE)
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)
OS.JS, OS.Ios -> TODO("commonize me") 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)
} }
private fun property(name: String, default: Boolean) = lazy { return listOf(initialApi) + fallbackApis
System.getProperty(name)?.toBoolean() ?: default
} }
private fun property(name: String, default: Double) = lazy { private fun property(name: String, default: Boolean) =
System.getProperty(name)?.toBoolean() ?: default
private fun property(name: String, default: Double) =
System.getProperty(name)?.toDouble() ?: default 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 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 ...@@ -14,7 +14,9 @@ import org.jetbrains.skia.paragraph.TextStyle
import org.jetbrains.skiko.context.JvmContextHandler import org.jetbrains.skiko.context.JvmContextHandler
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.util.ScreenshotTestRule 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.Assert.assertEquals
import org.junit.Assume.assumeTrue import org.junit.Assume.assumeTrue
import org.junit.Rule import org.junit.Rule
...@@ -27,24 +29,8 @@ import javax.swing.WindowConstants ...@@ -27,24 +29,8 @@ import javax.swing.WindowConstants
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.assertTrue 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") @Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
class SkiaWindowTest { class SkiaLayerTest {
private val fontCollection = FontCollection() private val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.default) .setDefaultFontManager(FontMgr.default)
...@@ -64,7 +50,7 @@ class SkiaWindowTest { ...@@ -64,7 +50,7 @@ class SkiaWindowTest {
val screenshots = ScreenshotTestRule() val screenshots = ScreenshotTestRule()
@Test @Test
fun `should not leak native windows`() = swingTest { fun `should not leak native windows`() = uiTest {
assumeTrue(hostOs.isMacOS) assumeTrue(hostOs.isMacOS)
suspend fun createAndDisposeWindow() { suspend fun createAndDisposeWindow() {
...@@ -100,8 +86,8 @@ class SkiaWindowTest { ...@@ -100,8 +86,8 @@ class SkiaWindowTest {
} }
@Test @Test
fun `render single window`() = swingTest { fun `render single window`() = uiTest {
val window = SkiaWindow() val window = UiTestWindow()
try { try {
window.setLocation(200, 200) window.setLocation(200, 200)
window.setSize(400, 200) window.setSize(400, 200)
...@@ -124,8 +110,8 @@ class SkiaWindowTest { ...@@ -124,8 +110,8 @@ class SkiaWindowTest {
} }
@Test @Test
fun `render single window before window show`() = swingTest { fun `render single window before window show`() = uiTest {
val window = SkiaWindow() val window = UiTestWindow()
try { try {
window.setLocation(200, 200) window.setLocation(200, 200)
window.preferredSize = Dimension(400, 200) window.preferredSize = Dimension(400, 200)
...@@ -150,8 +136,8 @@ class SkiaWindowTest { ...@@ -150,8 +136,8 @@ class SkiaWindowTest {
} }
@Test @Test
fun `resize window`() = swingTest { fun `resize window`() = uiTest {
val window = SkiaWindow() val window = UiTestWindow()
try { try {
window.setLocation(200, 200) window.setLocation(200, 200)
window.setSize(40, 20) window.setSize(40, 20)
...@@ -171,8 +157,8 @@ class SkiaWindowTest { ...@@ -171,8 +157,8 @@ class SkiaWindowTest {
} }
@Test @Test
fun `render three windows`() = swingTest { fun `render three windows`() = uiTest {
fun window(color: Color) = SkiaWindow().apply { fun window(color: Color) = UiTestWindow().apply {
setLocation(200, 200) setLocation(200, 200)
setSize(400, 200) setSize(400, 200)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
...@@ -207,10 +193,10 @@ class SkiaWindowTest { ...@@ -207,10 +193,10 @@ class SkiaWindowTest {
} }
@Test @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 var renderCount = 0
val window = SkiaWindow() val window = UiTestWindow()
try { try {
window.setLocation(200, 200) window.setLocation(200, 200)
window.setSize(40, 20) window.setSize(40, 20)
...@@ -241,8 +227,8 @@ class SkiaWindowTest { ...@@ -241,8 +227,8 @@ class SkiaWindowTest {
} }
@Test(timeout = 60000) @Test(timeout = 60000)
fun `stress test - open multiple windows`() = swingTest { fun `stress test - open multiple windows`() = uiTest {
fun window(isAnimated: Boolean) = SkiaWindow().apply { fun window(isAnimated: Boolean) = UiTestWindow().apply {
setLocation(200, 200) setLocation(200, 200)
setSize(40, 20) setSize(40, 20)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
...@@ -256,7 +242,7 @@ class SkiaWindowTest { ...@@ -256,7 +242,7 @@ class SkiaWindowTest {
} }
val random = Random(31415926) val random = Random(31415926)
val openedWindows = mutableListOf<SkiaWindow>() val openedWindows = mutableListOf<UiTestWindow>()
repeat(10) { repeat(10) {
val needOpen = random.nextDouble() > 0.5f val needOpen = random.nextDouble() > 0.5f
...@@ -283,8 +269,8 @@ class SkiaWindowTest { ...@@ -283,8 +269,8 @@ class SkiaWindowTest {
} }
@Test(timeout = 60000) @Test(timeout = 60000)
fun `stress test - resize and paint immediately`() = swingTest { fun `stress test - resize and paint immediately`() = uiTest {
fun openWindow() = SkiaWindow( fun openWindow() = UiTestWindow(
properties = SkiaLayerProperties(isVsyncEnabled = false, isVsyncFramelimitFallbackEnabled = true) properties = SkiaLayerProperties(isVsyncEnabled = false, isVsyncFramelimitFallbackEnabled = true)
).apply { ).apply {
setLocation(200, 200) setLocation(200, 200)
...@@ -306,8 +292,8 @@ class SkiaWindowTest { ...@@ -306,8 +292,8 @@ class SkiaWindowTest {
} }
@Test(timeout = 60000) @Test(timeout = 60000)
fun `stress test - open and paint immediately`() = swingTest { fun `stress test - open and paint immediately`() = uiTest {
fun openWindow() = SkiaWindow( fun openWindow() = UiTestWindow(
properties = SkiaLayerProperties(isVsyncEnabled = false, isVsyncFramelimitFallbackEnabled = true) properties = SkiaLayerProperties(isVsyncEnabled = false, isVsyncFramelimitFallbackEnabled = true)
).apply { ).apply {
setLocation(200, 200) setLocation(200, 200)
...@@ -332,7 +318,7 @@ class SkiaWindowTest { ...@@ -332,7 +318,7 @@ class SkiaWindowTest {
} }
@Test(timeout = 60000) @Test(timeout = 60000)
fun `fallback to software renderer, fail on init context`() = swingTest { fun `fallback to software renderer, fail on init context`() = uiTest {
testFallbackToSoftware( testFallbackToSoftware(
object : RenderFactory { object : RenderFactory {
override fun createRedrawer( override fun createRedrawer(
...@@ -355,7 +341,7 @@ class SkiaWindowTest { ...@@ -355,7 +341,7 @@ class SkiaWindowTest {
} }
@Test(timeout = 60000) @Test(timeout = 60000)
fun `fallback to software renderer, fail on create redrawer`() = swingTest { fun `fallback to software renderer, fail on create redrawer`() = uiTest {
testFallbackToSoftware( testFallbackToSoftware(
object : RenderFactory { object : RenderFactory {
override fun createRedrawer( override fun createRedrawer(
...@@ -368,7 +354,7 @@ class SkiaWindowTest { ...@@ -368,7 +354,7 @@ class SkiaWindowTest {
} }
@Test(timeout = 60000) @Test(timeout = 60000)
fun `fallback to software renderer, fail on draw`() = swingTest { fun `fallback to software renderer, fail on draw`() = uiTest {
testFallbackToSoftware( testFallbackToSoftware(
object : RenderFactory { object : RenderFactory {
override fun createRedrawer( override fun createRedrawer(
...@@ -387,14 +373,10 @@ class SkiaWindowTest { ...@@ -387,14 +373,10 @@ class SkiaWindowTest {
) )
} }
private suspend fun testFallbackToSoftware(nonSoftwareRenderFactory: RenderFactory) { private suspend fun UiTestScope.testFallbackToSoftware(nonSoftwareRenderFactory: RenderFactory) {
val window = SkiaWindow( val window = UiTestWindow(
layerFactory = {
SkiaLayer(
renderFactory = OverrideNonSoftwareRenderFactory(nonSoftwareRenderFactory) renderFactory = OverrideNonSoftwareRenderFactory(nonSoftwareRenderFactory)
) )
}
)
try { try {
window.setLocation(200, 200) window.setLocation(200, 200)
window.setSize(400, 200) window.setSize(400, 200)
...@@ -435,12 +417,12 @@ class SkiaWindowTest { ...@@ -435,12 +417,12 @@ class SkiaWindowTest {
} }
@Test(timeout = 20000) @Test(timeout = 20000)
fun `render continuously empty content without vsync`() = swingTest { fun `render continuously empty content without vsync`() = uiTest {
val targetDrawCount = 500 val targetDrawCount = 500
var drawCount = 0 var drawCount = 0
val onDrawCompleted = CompletableDeferred<Unit>() val onDrawCompleted = CompletableDeferred<Unit>()
val window = SkiaWindow( val window = UiTestWindow(
properties = SkiaLayerProperties( properties = SkiaLayerProperties(
isVsyncEnabled = false, isVsyncEnabled = false,
isVsyncFramelimitFallbackEnabled = true isVsyncFramelimitFallbackEnabled = true
...@@ -486,10 +468,10 @@ class SkiaWindowTest { ...@@ -486,10 +468,10 @@ class SkiaWindowTest {
testRenderText(OS.MacOS) testRenderText(OS.MacOS)
} }
private fun testRenderText(os: OS) = swingTest { private fun testRenderText(os: OS) = uiTest {
assumeTrue(hostOs == os) assumeTrue(hostOs == os)
val window = SkiaWindow() val window = UiTestWindow()
try { try {
window.setLocation(200, 200) window.setLocation(200, 200)
window.setSize(400, 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 package org.jetbrains.skiko
actual open class SkiaLayer(properties: SkiaLayerProperties) { actual open class SkiaLayer {
actual var renderApi: GraphicsApi actual var renderApi: GraphicsApi
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
set(value) {} 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 ...@@ -4,6 +4,7 @@ import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import org.jetbrains.skiko.context.* import org.jetbrains.skiko.context.*
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
import platform.AppKit.* import platform.AppKit.*
import platform.Foundation.NSMakeRect import platform.Foundation.NSMakeRect
...@@ -14,9 +15,7 @@ import platform.Foundation.addObserver ...@@ -14,9 +15,7 @@ import platform.Foundation.addObserver
import platform.darwin.NSObject import platform.darwin.NSObject
import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGRectMake
actual open class SkiaLayer( actual open class SkiaLayer {
private val properties: SkiaLayerProperties = makeDefaultSkiaLayerProperties()
) {
actual var renderApi: GraphicsApi = GraphicsApi.OPENGL actual var renderApi: GraphicsApi = GraphicsApi.OPENGL
actual val contentScale: Float actual val contentScale: Float
get() = if (this::nsView.isInitialized) nsView.window!!.backingScaleFactor.toFloat() else 1.0f get() = if (this::nsView.isInitialized) nsView.window!!.backingScaleFactor.toFloat() else 1.0f
...@@ -138,7 +137,7 @@ actual open class SkiaLayer( ...@@ -138,7 +137,7 @@ actual open class SkiaLayer(
} }
} }
window.contentView!!.addSubview(nsView) window.contentView!!.addSubview(nsView)
redrawer = createNativeRedrawer(this, GraphicsApi.OPENGL, properties).apply { redrawer = MacOsOpenGLRedrawer(this).apply {
syncSize() syncSize()
needRedraw() needRedraw()
} }
......
...@@ -4,7 +4,6 @@ import kotlinx.cinterop.CPointer ...@@ -4,7 +4,6 @@ import kotlinx.cinterop.CPointer
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.SkikoDispatchers import org.jetbrains.skiko.SkikoDispatchers
import org.jetbrains.skiko.context.ContextHandler import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOSOpenGLContextHandler import org.jetbrains.skiko.context.MacOSOpenGLContextHandler
...@@ -19,8 +18,7 @@ import platform.QuartzCore.* ...@@ -19,8 +18,7 @@ import platform.QuartzCore.*
import kotlin.system.getTimeNanos import kotlin.system.getTimeNanos
internal class MacOsOpenGLRedrawer( internal class MacOsOpenGLRedrawer(
private val skiaLayer: SkiaLayer, private val skiaLayer: SkiaLayer
private val properties: SkiaLayerProperties
) : Redrawer { ) : Redrawer {
private val contextHandler = MacOSOpenGLContextHandler(skiaLayer) private val contextHandler = MacOSOpenGLContextHandler(skiaLayer)
override val renderInfo: String get() = contextHandler.rendererInfo() override val renderInfo: String get() = contextHandler.rendererInfo()
......
package org.jetbrains.skiko package org.jetbrains.skiko
internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
return SkiaLayerProperties(true, true)
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R = internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
block() 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