Skip to content

Commit

Permalink
Call the listeners on PropertyKind.Renderer when the render API chang…
Browse files Browse the repository at this point in the history
…es due to switching to fallback(s).
  • Loading branch information
m-sasha committed Mar 8, 2025
1 parent ea19091 commit f7c429e
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 103 deletions.
10 changes: 2 additions & 8 deletions skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,15 @@ import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.UnsupportedFlavorException
import java.io.IOException
import java.net.MalformedURLException
import java.net.URI
import java.net.URL
import javax.swing.UIManager

actual fun setSystemLookAndFeel() = UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())

internal actual fun makeDefaultRenderFactory(): RenderFactory =
object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
analytics: SkiaLayerAnalytics,
properties: SkiaLayerProperties
): Redrawer = when (hostOs) {
RenderFactory { layer, renderApi, analytics, properties ->
when (hostOs) {
OS.MacOS -> when (renderApi) {
GraphicsApi.SOFTWARE_COMPAT, GraphicsApi.SOFTWARE_FAST -> SoftwareRedrawer(layer, analytics, properties)
else -> MetalRedrawer(layer, analytics, properties)
Expand Down
36 changes: 18 additions & 18 deletions skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -284,22 +284,22 @@ actual open class SkiaLayer internal constructor(
@Volatile
private var isDisposed = false

private val redrawerManager = RedrawerManager<Redrawer>(properties.renderApi) { renderApi, oldRedrawer ->
oldRedrawer?.dispose()
val newRedrawer = renderFactory.createRedrawer(this, renderApi, analytics, properties)
newRedrawer.syncBounds()
newRedrawer
}

internal val redrawer: Redrawer?
get() = redrawerManager.redrawer

actual var renderApi: GraphicsApi
get() = redrawerManager.renderApi
set(value) {
redrawerManager.forceRenderApi(value)
private val redrawerManager = RedrawerManager<Redrawer>(
defaultRenderApi = properties.renderApi,
redrawerFactory = { renderApi, oldRedrawer ->
oldRedrawer?.dispose()
renderFactory.createRedrawer(this, renderApi, analytics, properties).also {
it.syncBounds()
}
},
onRenderApiChanged = {
notifyChange(PropertyKind.Renderer)
}
)

internal val redrawer: Redrawer? by redrawerManager::redrawer

actual var renderApi: GraphicsApi by redrawerManager::renderApi

val renderInfo: String
get() = if (redrawer == null)
Expand All @@ -320,15 +320,15 @@ actual open class SkiaLayer internal constructor(
isInited = true
}

private val stateHandlers =
private val stateChangeListeners =
mutableMapOf<PropertyKind, MutableList<(SkiaLayer) -> Unit>>()

fun onStateChanged(kind: PropertyKind, handler: (SkiaLayer) -> Unit) {
stateHandlers.getOrPut(kind, { mutableListOf() }) += handler
stateChangeListeners.getOrPut(kind, ::mutableListOf) += handler
}

private fun notifyChange(kind: PropertyKind) {
stateHandlers.get(kind)?.let { handlers ->
stateChangeListeners[kind]?.let { handlers ->
handlers.forEach { it(this) }
}
}
Expand Down Expand Up @@ -583,7 +583,7 @@ actual open class SkiaLayer internal constructor(
}
}

actual internal fun draw(canvas: Canvas) {
internal actual fun draw(canvas: Canvas) {
check(!isDisposed) { "SkiaLayer is disposed" }
lockPicture {
canvas.drawPicture(it.instance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import org.jetbrains.skiko.SkikoProperties

internal class RedrawerManager<R>(
defaultRenderApi: GraphicsApi,
private val redrawerFactory: (renderApi: GraphicsApi, oldRedrawer: R?) -> R
private val redrawerFactory: (renderApi: GraphicsApi, oldRedrawer: R?) -> R,
private val onRenderApiChanged: ((GraphicsApi) -> Unit)? = null
) {
private var _redrawer: R? = null
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue(defaultRenderApi).toMutableList()
private var _renderApi = fallbackRenderApiQueue[0]

val redrawer: R?
get() = _redrawer
var redrawer: R? = null
private set

val renderApi: GraphicsApi
get() = _renderApi
var renderApi: GraphicsApi = fallbackRenderApiQueue[0]
set(value) {
field = value
onRenderApiChanged?.invoke(value)
}

fun findNextWorkingRenderApi(recreation: Boolean = false) {
if (recreation) {
Expand All @@ -27,10 +29,10 @@ internal class RedrawerManager<R>(
do {
thrown = false
try {
_renderApi = fallbackRenderApiQueue.removeAt(0)
_redrawer = redrawerFactory(_renderApi, redrawer)
renderApi = fallbackRenderApiQueue.removeAt(0)
redrawer = redrawerFactory(renderApi, redrawer)
} catch (e: RenderException) {
_redrawer = null
redrawer = null
Logger.warn(e) { "Fallback to next API" }
thrown = true
}
Expand All @@ -41,11 +43,7 @@ internal class RedrawerManager<R>(
}
}

fun forceRenderApi(renderApi: GraphicsApi) {
_renderApi = renderApi
}

fun dispose() {
_redrawer = null
redrawer = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.*
import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Component
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.GraphicsConfiguration
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingUtilities.isEventDispatchThread

Expand Down Expand Up @@ -65,10 +65,13 @@ open class SkiaSwingLayer(
get() = this@SkiaSwingLayer.properties.adapterPriority
}

private val redrawerManager = RedrawerManager<SwingRedrawer>(properties.renderApi) { renderApi, oldRedrawer ->
oldRedrawer?.dispose()
createSwingRedrawer(swingLayerProperties, renderDelegateWithClipping, renderApi, analytics)
}
private val redrawerManager = RedrawerManager<SwingRedrawer>(
properties.renderApi,
redrawerFactory = { renderApi, oldRedrawer ->
oldRedrawer?.dispose()
createSwingRedrawer(swingLayerProperties, renderDelegateWithClipping, renderApi, analytics)
}
)

private val redrawer: SwingRedrawer?
get() = redrawerManager.redrawer
Expand Down Expand Up @@ -109,7 +112,7 @@ open class SkiaSwingLayer(
}
}

override fun paint(g: java.awt.Graphics) {
override fun paint(g: Graphics) {
try {
redrawer?.redraw(g as Graphics2D)
} catch (e: RenderException) {
Expand Down
121 changes: 66 additions & 55 deletions skiko/src/awtTest/kotlin/org/jetbrains/skiko/SkiaLayerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -541,63 +541,41 @@ class SkiaLayerTest {
}
}

private abstract class BaseTestRedrawer: Redrawer {
override fun dispose() = Unit
override fun needRedraw() = Unit
override fun redrawImmediately() = Unit
override val renderInfo: String
get() = ""
}

@Test(timeout = 60000)
fun `fallback to software renderer, fail on init context`() = uiTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
analytics: SkiaLayerAnalytics,
properties: SkiaLayerProperties
) = object : Redrawer {
private val contextHandler = object : JvmContextHandler(layer) {
override fun initContext() = false
override fun initCanvas() = Unit
}

override fun dispose() = Unit
override fun needRedraw() = Unit
override fun redrawImmediately() = layer.inDrawScope(contextHandler::draw)
override val renderInfo = ""
testFallbackToSoftware { layer, _, _, _ ->
object : BaseTestRedrawer() {
private val contextHandler = object : JvmContextHandler(layer) {
override fun initContext() = false
override fun initCanvas() = Unit
}
override fun redrawImmediately() = layer.inDrawScope(contextHandler::draw)
}
)
}
}

@Test(timeout = 60000)
fun `fallback to software renderer, fail on create redrawer`() = uiTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
analytics: SkiaLayerAnalytics,
properties: SkiaLayerProperties
) = throw RenderException()
}
)
testFallbackToSoftware { _, _, _, _ -> throw RenderException() }
}

@Test(timeout = 60000)
fun `fallback to software renderer, fail on draw`() = uiTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
analytics: SkiaLayerAnalytics,
properties: SkiaLayerProperties
) = object : Redrawer {
override fun dispose() = Unit
override fun needRedraw() = Unit
override fun redrawImmediately() = layer.inDrawScope {
throw RenderException()
}
override val renderInfo = ""
testFallbackToSoftware { layer, _, _, _ ->
object : BaseTestRedrawer() {
override fun redrawImmediately() = layer.inDrawScope {
throw RenderException()
}
}
)
}
}

private suspend fun UiTestScope.testFallbackToSoftware(nonSoftwareRenderFactory: RenderFactory) {
Expand Down Expand Up @@ -644,6 +622,35 @@ class SkiaLayerTest {
}
}

@Test(timeout = 60000)
fun `renderApi change callback is invoked on fallback`() = uiTest {
val window = UiTestWindow(
renderFactory = OverrideNonSoftwareRenderFactory { layer, _, _, _ ->
object : BaseTestRedrawer() {
override fun redrawImmediately() = layer.inDrawScope {
throw RenderException()
}
}
}
)
try {
var rendererChangedCallbackInvoked = false
window.layer.onStateChanged(SkiaLayer.PropertyKind.Renderer) {
rendererChangedCallbackInvoked = true
}
window.setLocation(200, 200)
window.setSize(400, 200)
window.isVisible = true

delay(1000)

assertEquals(GraphicsApi.SOFTWARE_COMPAT, window.layer.renderApi)
assertTrue(rendererChangedCallbackInvoked)
} finally {
window.close()
}
}

@Test(timeout = 20000)
fun `render continuously empty content without vsync`() = uiTest {
val targetDrawCount = 500
Expand Down Expand Up @@ -981,21 +988,25 @@ class SkiaLayerTest {
}
contentPane.add(layer)
}
window.size = Dimension(400, 400)
window.isVisible = true
try {
window.size = Dimension(400, 400)
window.isVisible = true

repeat(20) {
window.location = window.location.let {
java.awt.Point(it.x + 10, it.y + 10)
repeat(20) {
window.location = window.location.let {
java.awt.Point(it.x + 10, it.y + 10)
}
delay(50)
}
delay(50)
}

// Ideally, layoutCount would be just 1, but Swing appears to call layout one extra time, so it ends up being 2.
// Compare to 3 just to avoid a false-failure if there's another layout for whatever reason.
// What we're interested to validate is that there's no layout occurring on every window move.
assert(layoutCount <= 3) {
"Layout count: $layoutCount"
// Ideally, layoutCount would be just 1, but Swing appears to call layout one extra time, so it ends up being 2.
// Compare to 3 just to avoid a false-failure if there's another layout for whatever reason.
// What we're interested to validate is that there's no layout occurring on every window move.
assert(layoutCount <= 3) {
"Layout count: $layoutCount"
}
} finally {
window.dispose()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.jetbrains.skiko

import org.jetbrains.skiko.redrawer.*

internal interface RenderFactory {
internal fun interface RenderFactory {
fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
Expand Down

0 comments on commit f7c429e

Please sign in to comment.