Skip to content

Commit

Permalink
Call the listeners of PropertyKind.Renderer on render API fallback (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
m-sasha authored Mar 10, 2025
1 parent ea19091 commit 4462fe4
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 4462fe4

Please sign in to comment.