Unverified Commit afcd9e54 authored by Alexander Maryanovsky's avatar Alexander Maryanovsky Committed by GitHub

Fix drawing issues and add API to request render-and-draw explicitly (#1104)

parent 2044a13b
...@@ -15,8 +15,8 @@ internal class AndroidOpenGLRedrawer( ...@@ -15,8 +15,8 @@ internal class AndroidOpenGLRedrawer(
private val properties: SkiaLayerProperties private val properties: SkiaLayerProperties
) : Redrawer { ) : Redrawer {
override fun dispose() = TODO() override fun dispose() = TODO()
override fun needRedraw(canUpdateImmediately: Boolean) = TODO() override fun needRender(canUpdateImmediately: Boolean) = TODO()
override fun redrawImmediately(updateNeeded: Boolean) = TODO() override fun renderImmediately() = TODO()
override fun update(nanoTime: Long) = TODO() override fun update(nanoTime: Long) = TODO()
override val renderInfo: String get() = "Android renderer" override val renderInfo: String get() = "Android renderer"
......
...@@ -47,7 +47,7 @@ actual open class SkiaLayer { ...@@ -47,7 +47,7 @@ actual open class SkiaLayer {
view.setFocusableInTouchMode(true) view.setFocusableInTouchMode(true)
needRedraw() needRender()
} }
actual fun detach() { actual fun detach() {
...@@ -57,12 +57,14 @@ actual open class SkiaLayer { ...@@ -57,12 +57,14 @@ actual open class SkiaLayer {
} }
} }
actual fun needRedraw(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
glView?.apply { glView?.apply {
scheduleFrame() scheduleFrame()
} }
} }
actual fun needRedraw() = needRender()
actual val pixelGeometry: PixelGeometry actual val pixelGeometry: PixelGeometry
get() = PixelGeometry.UNKNOWN get() = PixelGeometry.UNKNOWN
......
...@@ -113,8 +113,8 @@ actual open class SkiaLayer internal constructor( ...@@ -113,8 +113,8 @@ actual open class SkiaLayer internal constructor(
// 2. HardwareLayer.paint is also not always called. // 2. HardwareLayer.paint is also not always called.
// For example, on macOs when we resize window or change DPI // For example, on macOs when we resize window or change DPI
// //
// 3. to avoid double paint in one single frame, use needRedraw instead of redrawImmediately // 3. to avoid double paint in one single frame, use needRender instead of renderImmediately
redrawer?.needRedraw(throttledToVsync = false) redrawer?.needRender(throttledToVsync = false)
} }
@Suppress("OVERRIDE_DEPRECATION") @Suppress("OVERRIDE_DEPRECATION")
...@@ -124,7 +124,7 @@ actual open class SkiaLayer internal constructor( ...@@ -124,7 +124,7 @@ actual open class SkiaLayer internal constructor(
super.reshape(x, y, width, height) super.reshape(x, y, width, height)
redrawer?.syncBounds() redrawer?.syncBounds()
redrawer?.needRedraw(throttledToVsync = false) redrawer?.needRender(throttledToVsync = false)
} }
override fun getInputMethodRequests(): InputMethodRequests? { override fun getInputMethodRequests(): InputMethodRequests? {
...@@ -384,7 +384,7 @@ actual open class SkiaLayer internal constructor( ...@@ -384,7 +384,7 @@ actual open class SkiaLayer internal constructor(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
super.reshape(x, y, w, h) super.reshape(x, y, w, h)
// Calling redrawImmediately as early as possible improves the situation with // Calling renderImmediately as early as possible improves the situation with
// the visual glitch when the drawn content is scaled during window resize. // the visual glitch when the drawn content is scaled during window resize.
// Note, however, that this actually causes the reverse glitch (content appears // Note, however, that this actually causes the reverse glitch (content appears
// scaled in the other direction from the window size), but this seems to // scaled in the other direction from the window size), but this seems to
...@@ -394,7 +394,7 @@ actual open class SkiaLayer internal constructor( ...@@ -394,7 +394,7 @@ actual open class SkiaLayer internal constructor(
// so apply this fix only for the Direct3D case. // so apply this fix only for the Direct3D case.
if (renderApi == GraphicsApi.DIRECT3D && isShowing) { if (renderApi == GraphicsApi.DIRECT3D && isShowing) {
redrawer?.syncBounds() redrawer?.syncBounds()
redrawer?.redrawImmediately(updateNeeded = true) redrawer?.renderImmediately()
} }
// Setting the bounds of children should be done only in the layout pass, // Setting the bounds of children should be done only in the layout pass,
...@@ -416,15 +416,14 @@ actual open class SkiaLayer internal constructor( ...@@ -416,15 +416,14 @@ actual open class SkiaLayer internal constructor(
override fun paint(g: Graphics) { override fun paint(g: Graphics) {
Logger.debug { "paint called on SkiaLayer $this" } Logger.debug { "paint called on SkiaLayer $this" }
val updateNeeded = checkContentScale() checkContentScale()
redrawer?.redrawImmediately(updateNeeded = updateNeeded) redrawer?.needRender(throttledToVsync = false)
} }
// Workaround for JBR-5274 and JBR-5305 // Workaround for JBR-5274 and JBR-5305
fun checkContentScale(): Boolean { fun checkContentScale() {
val currentGraphicsContextScaleTransform = graphicsConfiguration.defaultTransform val currentGraphicsContextScaleTransform = graphicsConfiguration.defaultTransform
return (currentGraphicsContextScaleTransform != latestReceivedGraphicsContextScaleTransform).also { if (currentGraphicsContextScaleTransform != latestReceivedGraphicsContextScaleTransform) {
if (it) {
firePropertyChange( firePropertyChange(
"graphicsContextScaleTransform", "graphicsContextScaleTransform",
latestReceivedGraphicsContextScaleTransform, latestReceivedGraphicsContextScaleTransform,
...@@ -432,6 +431,12 @@ actual open class SkiaLayer internal constructor( ...@@ -432,6 +431,12 @@ actual open class SkiaLayer internal constructor(
) )
} }
} }
/**
* Updates the layer and redraws synchronously.
*/
fun updateAndDrawImmediately() {
redrawer?.renderImmediately()
} }
// We need to delegate all event listeners to the Canvas (so and focus/input) // We need to delegate all event listeners to the Canvas (so and focus/input)
...@@ -546,12 +551,14 @@ actual open class SkiaLayer internal constructor( ...@@ -546,12 +551,14 @@ actual open class SkiaLayer internal constructor(
/** /**
* Redraw on the next animation Frame (on vsync signal if vsync is enabled). * Redraw on the next animation Frame (on vsync signal if vsync is enabled).
*/ */
actual fun needRedraw(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" } check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
redrawer?.needRedraw(throttledToVsync) redrawer?.needRender(throttledToVsync)
} }
actual fun needRedraw() = needRender()
internal fun update(nanoTime: Long) { internal fun update(nanoTime: Long) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" } check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
...@@ -608,7 +615,7 @@ actual open class SkiaLayer internal constructor( ...@@ -608,7 +615,7 @@ actual open class SkiaLayer internal constructor(
if (!isDisposed) { if (!isDisposed) {
Logger.warn(e) { "Exception in draw scope" } Logger.warn(e) { "Exception in draw scope" }
redrawerManager.findNextWorkingRenderApi() redrawerManager.findNextWorkingRenderApi()
redrawer?.redrawImmediately(updateNeeded = true) redrawer?.renderImmediately()
} }
} }
} }
......
...@@ -29,16 +29,14 @@ internal abstract class AbstractDirectSoftwareRedrawer( ...@@ -29,16 +29,14 @@ internal abstract class AbstractDirectSoftwareRedrawer(
protected var device = 0L protected var device = 0L
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
protected open fun draw() = inDrawScope(contextHandler::draw) protected open fun draw() = inDrawScope(contextHandler::draw)
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
if (updateNeeded) {
update() update()
}
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
draw() draw()
} }
......
...@@ -59,16 +59,14 @@ internal class AngleRedrawer( ...@@ -59,16 +59,14 @@ internal class AngleRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
checkDisposed() checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
checkDisposed() checkDisposed()
if (updateNeeded) {
update() update()
}
inDrawScope { inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately)
......
...@@ -63,16 +63,14 @@ internal class Direct3DRedrawer( ...@@ -63,16 +63,14 @@ internal class Direct3DRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
checkDisposed() checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
checkDisposed() checkDisposed()
if (updateNeeded) {
update() update()
}
inDrawScope { inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately)
......
...@@ -72,17 +72,15 @@ internal class LinuxOpenGLRedrawer( ...@@ -72,17 +72,15 @@ internal class LinuxOpenGLRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
checkDisposed() checkDisposed()
toRedraw.add(this) toRedraw.add(this)
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately(updateNeeded: Boolean) = layer.backedLayer.lockLinuxDrawingSurface { override fun renderImmediately() = layer.backedLayer.lockLinuxDrawingSurface {
checkDisposed() checkDisposed()
if (updateNeeded) {
update() update()
}
inDrawScope { inDrawScope {
it.makeCurrent(context) it.makeCurrent(context)
contextHandler.draw() contextHandler.draw()
......
...@@ -31,8 +31,8 @@ internal class LinuxSoftwareRedrawer( ...@@ -31,8 +31,8 @@ internal class LinuxSoftwareRedrawer(
super.draw() super.draw()
} }
override fun redrawImmediately(updateNeeded: Boolean) = layer.backedLayer.lockLinuxDrawingSurface { override fun renderImmediately() = layer.backedLayer.lockLinuxDrawingSurface {
super.redrawImmediately(updateNeeded) super.renderImmediately()
} }
override fun resize(width: Int, height: Int) = layer.backedLayer.lockLinuxDrawingSurface { override fun resize(width: Int, height: Int) = layer.backedLayer.lockLinuxDrawingSurface {
......
...@@ -94,26 +94,23 @@ internal class MetalRedrawer( ...@@ -94,26 +94,23 @@ internal class MetalRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
checkDisposed() checkDisposed()
frameDispatcher.scheduleFrame(needUpdate = true, throttledToVsync = throttledToVsync) frameDispatcher.scheduleFrame(needUpdate = true, throttledToVsync = throttledToVsync)
} }
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
checkDisposed() checkDisposed()
if (updateNeeded) {
update() update()
}
// Trying to draw immediately in Metal will result in lost (undrawn)
// frames if there are more than two between consecutive vsync events.
if (layer.isShowing) {
frameDispatcher.scheduleFrame(needUpdate = false, throttledToVsync = false)
} else {
// But if the layer isn't showing yet, we want to draw immediately,
// so that if it shows before the next vsync, there is no background flash
inDrawScope { inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
performDraw() performDraw()
// Trying to draw immediately in Metal will result in lost (undrawn)
// frames if there are more than two between consecutive vsync events.
if (SkikoProperties.macOSWaitForPreviousFrameVsyncOnRedrawImmediately) {
runBlocking {
vSyncer?.waitForVSync()
}
} }
} }
} }
......
...@@ -42,15 +42,13 @@ internal class SoftwareRedrawer( ...@@ -42,15 +42,13 @@ internal class SoftwareRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
checkDisposed() checkDisposed()
if (updateNeeded) {
update() update()
}
inDrawScope { inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
contextHandler.draw() contextHandler.draw()
......
...@@ -55,17 +55,15 @@ internal class WindowsOpenGLRedrawer( ...@@ -55,17 +55,15 @@ internal class WindowsOpenGLRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" } check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" }
toRedraw.add(this) toRedraw.add(this)
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" } check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" }
if (updateNeeded) {
update() update()
}
inDrawScope { inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
makeCurrent() makeCurrent()
......
...@@ -59,7 +59,7 @@ class SkiaLayerPerformanceTest { ...@@ -59,7 +59,7 @@ class SkiaLayerPerformanceTest {
if (canCollect && frameTimes.size < frameCount) { if (canCollect && frameTimes.size < frameCount) {
frameTimes.add(System.nanoTime()) // we check the real time, not the time provided by the argument frameTimes.add(System.nanoTime()) // we check the real time, not the time provided by the argument
} }
window.layer.needRedraw() window.layer.needRender()
} }
} }
window.isUndecorated = true window.isUndecorated = true
......
...@@ -106,11 +106,11 @@ class SkiaLayerTest { ...@@ -106,11 +106,11 @@ class SkiaLayerTest {
override fun keyTyped(e: KeyEvent?) { override fun keyTyped(e: KeyEvent?) {
launch { launch {
val redrawer = window.layer.redrawer as MetalRedrawer val redrawer = window.layer.redrawer as MetalRedrawer
redrawer.redrawImmediately(updateNeeded = true) redrawer.renderImmediately()
counter1 += 1 counter1 += 1
redrawer.redrawImmediately(updateNeeded = true) redrawer.renderImmediately()
counter2 += 1 counter2 += 1
redrawer.redrawImmediately(updateNeeded = true) redrawer.renderImmediately()
} }
} }
}) })
...@@ -179,7 +179,7 @@ class SkiaLayerTest { ...@@ -179,7 +179,7 @@ class SkiaLayerTest {
screenshots.assert(window.bounds, "frame1") screenshots.assert(window.bounds, "frame1")
app.rectWidth = 100 app.rectWidth = 100
window.layer.needRedraw() window.layer.needRender()
delay(1000) delay(1000)
screenshots.assert(window.bounds, "frame2") screenshots.assert(window.bounds, "frame2")
...@@ -238,7 +238,7 @@ class SkiaLayerTest { ...@@ -238,7 +238,7 @@ class SkiaLayerTest {
screenshots.assert(window.bounds, "frame1") screenshots.assert(window.bounds, "frame1")
app.rectWidth = 100 app.rectWidth = 100
window.layer.needRedraw() window.layer.needRender()
delay(1000) delay(1000)
screenshots.assert(window.bounds, "frame2") screenshots.assert(window.bounds, "frame2")
...@@ -271,12 +271,12 @@ class SkiaLayerTest { ...@@ -271,12 +271,12 @@ class SkiaLayerTest {
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.isUndecorated = true window.isUndecorated = true
window.isVisible = true window.isVisible = true
layer.needRedraw() layer.needRender()
delay(1000) delay(1000)
assertEquals(0, renderedWidth) assertEquals(0, renderedWidth)
renderedWidth = -1 renderedWidth = -1
layer.needRedraw() layer.needRender()
delay(1000) delay(1000)
assertEquals(0, renderedWidth) assertEquals(0, renderedWidth)
...@@ -439,7 +439,7 @@ class SkiaLayerTest { ...@@ -439,7 +439,7 @@ class SkiaLayerTest {
} }
@Test @Test
fun `should call onRender after init, after resize, and only once after needRedraw`() = uiTest { fun `should call onRender after init, after resize, and only once after needRender`() = uiTest {
var renderCount = 0 var renderCount = 0
val window = UiTestWindow() val window = UiTestWindow()
...@@ -464,7 +464,7 @@ class SkiaLayerTest { ...@@ -464,7 +464,7 @@ class SkiaLayerTest {
assertTrue(renderCount > 0) assertTrue(renderCount > 0)
renderCount = 0 renderCount = 0
window.layer.needRedraw() window.layer.needRender()
delay(1000) delay(1000)
assertEquals(1, renderCount) assertEquals(1, renderCount)
} finally { } finally {
...@@ -561,7 +561,7 @@ class SkiaLayerTest { ...@@ -561,7 +561,7 @@ class SkiaLayerTest {
delay(100) delay(100)
val window = openWindow() val window = openWindow()
window.isVisible = true window.isVisible = true
window.layer.needRedraw() window.layer.needRender()
yield() yield()
window.paint(window.graphics) window.paint(window.graphics)
assertRenderApiFor(window.layer) assertRenderApiFor(window.layer)
...@@ -570,9 +570,12 @@ class SkiaLayerTest { ...@@ -570,9 +570,12 @@ class SkiaLayerTest {
} }
private abstract class BaseTestRedrawer(val layer: SkiaLayer): Redrawer { private abstract class BaseTestRedrawer(val layer: SkiaLayer): Redrawer {
private val frameDispatcher = FrameDispatcher(MainUIDispatcher) {
renderImmediately()
}
override fun dispose() = Unit override fun dispose() = Unit
override fun needRedraw(throttledToVsync: Boolean) = Unit override fun needRender(throttledToVsync: Boolean) = frameDispatcher.scheduleFrame()
override fun redrawImmediately(updateNeeded: Boolean) = Unit override fun renderImmediately() = Unit
override fun update(nanoTime: Long) = layer.update(nanoTime) override fun update(nanoTime: Long) = layer.update(nanoTime)
override val renderInfo: String override val renderInfo: String
...@@ -587,7 +590,7 @@ class SkiaLayerTest { ...@@ -587,7 +590,7 @@ class SkiaLayerTest {
override fun initContext() = false override fun initContext() = false
override fun initCanvas() = Unit override fun initCanvas() = Unit
} }
override fun redrawImmediately(updateNeeded: Boolean) = layer.inDrawScope(contextHandler::draw) override fun renderImmediately() = layer.inDrawScope(contextHandler::draw)
} }
} }
} }
...@@ -601,7 +604,7 @@ class SkiaLayerTest { ...@@ -601,7 +604,7 @@ class SkiaLayerTest {
fun `fallback to software renderer, fail on draw`() = uiTest { fun `fallback to software renderer, fail on draw`() = uiTest {
testFallbackToSoftware { layer, _, _, _ -> testFallbackToSoftware { layer, _, _, _ ->
object : BaseTestRedrawer(layer) { object : BaseTestRedrawer(layer) {
override fun redrawImmediately(updateNeeded: Boolean) = layer.inDrawScope { override fun renderImmediately() = layer.inDrawScope {
throw RenderException() throw RenderException()
} }
} }
...@@ -625,7 +628,7 @@ class SkiaLayerTest { ...@@ -625,7 +628,7 @@ class SkiaLayerTest {
screenshots.assert(window.bounds, "frame1", "testFallbackToSoftware") screenshots.assert(window.bounds, "frame1", "testFallbackToSoftware")
app.rectWidth = 100 app.rectWidth = 100
window.layer.needRedraw() window.layer.needRender()
delay(1000) delay(1000)
screenshots.assert(window.bounds, "frame2", "testFallbackToSoftware") screenshots.assert(window.bounds, "frame2", "testFallbackToSoftware")
...@@ -657,7 +660,7 @@ class SkiaLayerTest { ...@@ -657,7 +660,7 @@ class SkiaLayerTest {
val window = UiTestWindow( val window = UiTestWindow(
renderFactory = OverrideNonSoftwareRenderFactory { layer, _, _, _ -> renderFactory = OverrideNonSoftwareRenderFactory { layer, _, _, _ ->
object : BaseTestRedrawer(layer) { object : BaseTestRedrawer(layer) {
override fun redrawImmediately(updateNeeded: Boolean) = layer.inDrawScope { override fun renderImmediately() = layer.inDrawScope {
throw RenderException() throw RenderException()
} }
} }
...@@ -703,7 +706,7 @@ class SkiaLayerTest { ...@@ -703,7 +706,7 @@ class SkiaLayerTest {
drawCount++ drawCount++
if (drawCount < targetDrawCount) { if (drawCount < targetDrawCount) {
window.layer.needRedraw() window.layer.needRender()
} else { } else {
onDrawCompleted.complete(Unit) onDrawCompleted.complete(Unit)
} }
...@@ -823,7 +826,7 @@ class SkiaLayerTest { ...@@ -823,7 +826,7 @@ class SkiaLayerTest {
repeat(10) { repeat(10) {
window.isVisible = true window.isVisible = true
delay(16) delay(16)
window.layer.needRedraw() window.layer.needRender()
delay(500) delay(500)
window.isVisible = false window.isVisible = false
...@@ -1145,7 +1148,7 @@ class SkiaLayerTest { ...@@ -1145,7 +1148,7 @@ class SkiaLayerTest {
} }
@Test @Test
fun `temporary change is not visible`() = uiTest { fun `temporary change is not visible with needRender(throttledToVsync = false)`() = uiTest {
assumeTrue(hostOs.isMacOS) assumeTrue(hostOs.isMacOS)
// The separation between update and draw is only implemented in MetalRedrawer at the moment // The separation between update and draw is only implemented in MetalRedrawer at the moment
// Don't use assumeTrue, as uiTest iterates over multiple renderers, // Don't use assumeTrue, as uiTest iterates over multiple renderers,
...@@ -1181,18 +1184,19 @@ class SkiaLayerTest { ...@@ -1181,18 +1184,19 @@ class SkiaLayerTest {
val robot = Robot() val robot = Robot()
var tempColorVisibleCount = 0 var tempColorVisibleCount = 0
val testCount = 50
try { try {
repeat(50) { repeat(testCount) {
// Wait for just after the next vsync, so we have plenty of time until the one after it // Wait for just after the next vsync, so we have plenty of time until the one after it
val vSyncer = MetalVSyncer(window.layer.windowHandle) val vSyncer = MetalVSyncer(window.layer.windowHandle)
vSyncer.waitForVSync() vSyncer.waitForVSync()
// Set the color to temp, then immediately back to normal // Set the color to temp, then immediately back to normal
renderDelegate.color = tempColor renderDelegate.color = tempColor
renderDelegate.layer.needRedraw(throttledToVsync = false) renderDelegate.layer.needRender(throttledToVsync = false)
renderChannel.receive() // Wait until render is actually called renderChannel.receive() // Wait until render is actually called
renderDelegate.color = color renderDelegate.color = color
renderDelegate.layer.needRedraw(throttledToVsync = false) renderDelegate.layer.needRender(throttledToVsync = false)
// Check whether the temp color was visible // Check whether the temp color was visible
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
...@@ -1208,15 +1212,15 @@ class SkiaLayerTest { ...@@ -1208,15 +1212,15 @@ class SkiaLayerTest {
// color is reverted, we allow a small percentage of the tries to fail. This way the flakiness of the test // color is reverted, we allow a small percentage of the tries to fail. This way the flakiness of the test
// is reduced. // is reduced.
// Note that in practice, however, this test had never failed on an M1 Ultra machine with a 60Hz monitor. // Note that in practice, however, this test had never failed on an M1 Ultra machine with a 60Hz monitor.
assertTrue(tempColorVisibleCount < 5) assertTrue(tempColorVisibleCount < 5, "Temp color was visible $tempColorVisibleCount/$testCount times")
} finally { } finally {
window.dispose() window.dispose()
} }
} }
@Test @Test
fun `needRedraw throttled and regular calls render and draw once`() = uiTest { fun `needRender throttled and regular calls render and draw once`() = uiTest {
// Check that calling both needRedraw(true) and needRedraw(false) causes only one render and one draw call // Check that calling both needRender(true) and needRender(false) causes only one render and one draw call
var renderCalls = 0 var renderCalls = 0
val renderChannel = Channel<Unit>(Channel.CONFLATED) val renderChannel = Channel<Unit>(Channel.CONFLATED)
...@@ -1259,27 +1263,72 @@ class SkiaLayerTest { ...@@ -1259,27 +1263,72 @@ class SkiaLayerTest {
renderCalls = 0 renderCalls = 0
drawCalls = 0 drawCalls = 0
withContext(MainUIDispatcher) { withContext(MainUIDispatcher) {
window.layer.needRedraw(true) window.layer.needRender(true)
window.layer.needRedraw(false) window.layer.needRender(false)
} }
delay(100) delay(100)
assertEquals("Render was called more than once on needRedraw(true), needRedraw(false)", 1, renderCalls) assertEquals("Render was called more than once on needRender(true), needRender(false)", 1, renderCalls)
assertEquals("Draw was called more than once on needRedraw(true), needRedraw(false)", 1, drawCalls) assertEquals("Draw was called more than once on needRender(true), needRender(false)", 1, drawCalls)
renderCalls = 0 renderCalls = 0
drawCalls = 0 drawCalls = 0
withContext(MainUIDispatcher) { withContext(MainUIDispatcher) {
window.layer.needRedraw(false) window.layer.needRender(false)
window.layer.needRedraw(true) window.layer.needRender(true)
} }
delay(100) delay(100)
assertEquals("Render was called more than once on needRedraw(false), needRedraw(true)", 1, renderCalls) assertEquals("Render was called more than once on needRender(false), needRender(true)", 1, renderCalls)
assertEquals("Draw was called more than once on needRedraw(true), needRedraw(true)", 1, drawCalls) assertEquals("Draw was called more than once on needRender(true), needRender(true)", 1, drawCalls)
} finally { } finally {
window.dispose() window.dispose()
} }
} }
@Test
fun `updateAndDrawImmediately updates and draws synchronously`() = uiTest {
// Check that calling both needRender(true) and needRender(false) causes only one render and one draw call
var renderCalls = 0
val renderChannel = Channel<Unit>(Channel.CONFLATED)
var drawCalls = 0
val deviceAnalytics = object : SkiaLayerAnalytics.DeviceAnalytics {
override fun beforeFrameRender() {
drawCalls++
}
}
val analytics = object : SkiaLayerAnalytics {
@ExperimentalSkikoApi
override fun device(
skikoVersion: String,
os: OS,
api: GraphicsApi,
deviceName: String?
): SkiaLayerAnalytics.DeviceAnalytics {
return deviceAnalytics
}
}
val window = UiTestWindow(analytics = analytics) {
size = Dimension(600, 600)
location = Point(400, 400)
layer.renderDelegate = object: SkikoRenderDelegate {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
renderCalls++
renderChannel.trySend(Unit)
}
}
contentPane.add(layer, BorderLayout.CENTER)
}
window.pack()
val initRenderCalls = renderCalls
val initDrawCalls = drawCalls
window.layer.updateAndDrawImmediately()
// Can't check renderCalls == initRenderCalls+1 because if drawing fails, render will be called again with
// the fallback renderer.
assertTrue(renderCalls > initRenderCalls)
assertTrue(drawCalls > initDrawCalls)
}
private class RectRenderer( private class RectRenderer(
private val getContentScale: () -> Float, private val getContentScale: () -> Float,
var rectWidth: Int, var rectWidth: Int,
...@@ -1341,7 +1390,7 @@ class SkiaLayerTest { ...@@ -1341,7 +1390,7 @@ class SkiaLayerTest {
color = Color.RED.rgb color = Color.RED.rgb
}) })
layer.needRedraw() layer.needRender()
} }
} }
...@@ -1354,7 +1403,7 @@ class SkiaLayerTest { ...@@ -1354,7 +1403,7 @@ class SkiaLayerTest {
var continuousRedraw = continuousRedraw var continuousRedraw = continuousRedraw
set(value) { set(value) {
if (value) if (value)
layer.needRedraw(throttledToVsync = true) layer.needRender(throttledToVsync = true)
field = value field = value
} }
...@@ -1370,7 +1419,7 @@ class SkiaLayerTest { ...@@ -1370,7 +1419,7 @@ class SkiaLayerTest {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), paint) canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), paint)
if (continuousRedraw) { if (continuousRedraw) {
layer.needRedraw() layer.needRender()
} }
} }
} }
......
...@@ -61,7 +61,13 @@ expect open class SkiaLayer { ...@@ -61,7 +61,13 @@ expect open class SkiaLayer {
* @param throttledToVsync Whether to throttle calling [renderDelegate]'s [SkikoRenderDelegate.onRender] to at most * @param throttledToVsync Whether to throttle calling [renderDelegate]'s [SkikoRenderDelegate.onRender] to at most
* once between vsync signals (if vsync is enabled). * once between vsync signals (if vsync is enabled).
*/ */
fun needRedraw(throttledToVsync: Boolean = true) fun needRender(throttledToVsync: Boolean = true)
@Deprecated(
"Use needRender(throttledToVsync) instead",
replaceWith = ReplaceWith("needRender(throttledToVsync)")
)
fun needRedraw() // TODO: Remove this sometime after 2026-07
/** /**
* Drawing function. * Drawing function.
......
...@@ -15,6 +15,6 @@ open class SkiaLayerRenderDelegate( ...@@ -15,6 +15,6 @@ open class SkiaLayerRenderDelegate(
canvas.scale(contentScale, contentScale) canvas.scale(contentScale, contentScale)
renderDelegate.onRender(canvas, (width / contentScale).toInt(), (height / contentScale).toInt(), nanoTime) renderDelegate.onRender(canvas, (width / contentScale).toInt(), (height / contentScale).toInt(), nanoTime)
// Request next frame immediately. // Request next frame immediately.
layer.needRedraw() layer.needRender()
} }
} }
...@@ -6,8 +6,8 @@ private val initialTime = TimeSource.Monotonic.markNow() ...@@ -6,8 +6,8 @@ private val initialTime = TimeSource.Monotonic.markNow()
internal interface Redrawer { internal interface Redrawer {
fun dispose() fun dispose()
fun needRedraw(throttledToVsync: Boolean) fun needRender(throttledToVsync: Boolean)
fun redrawImmediately(updateNeeded: Boolean) fun renderImmediately()
fun syncBounds() = Unit fun syncBounds() = Unit
fun update(nanoTime: Long = initialTime.elapsedNow().inWholeNanoseconds) fun update(nanoTime: Long = initialTime.elapsedNow().inWholeNanoseconds)
fun setVisible(isVisible: Boolean) = Unit fun setVisible(isVisible: Boolean) = Unit
......
...@@ -42,6 +42,10 @@ object SkikoProperties { ...@@ -42,6 +42,10 @@ object SkikoProperties {
} }
} }
val macOSWaitForPreviousFrameVsyncOnRedrawImmediately: Boolean get() {
return getProperty("skiko.rendering.macos.waitForPreviousFrameVsyncOnRedrawImmediately")?.toBoolean() ?: true
}
val windowsWaitForVsyncOnRedrawImmediately: Boolean get() { val windowsWaitForVsyncOnRedrawImmediately: Boolean get() {
return getProperty("skiko.rendering.windows.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false return getProperty("skiko.rendering.windows.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false
} }
......
...@@ -17,9 +17,10 @@ actual open class SkiaLayer { ...@@ -17,9 +17,10 @@ actual open class SkiaLayer {
set(value) {} set(value) {}
actual val component: Any? actual val component: Any?
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
actual fun needRedraw(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
TODO("unimplemented") TODO("unimplemented")
} }
actual fun needRedraw() = needRender()
actual fun attachTo(container: Any) { actual fun attachTo(container: Any) {
TODO("unimplemented") TODO("unimplemented")
} }
......
...@@ -85,13 +85,13 @@ actual open class SkiaLayer { ...@@ -85,13 +85,13 @@ actual open class SkiaLayer {
@ObjCAction @ObjCAction
fun frameDidChange(notification: NSNotification) { fun frameDidChange(notification: NSNotification) {
redrawer?.syncBounds() redrawer?.syncBounds()
redrawer?.redrawImmediately(updateNeeded = true) redrawer?.renderImmediately()
} }
@ObjCAction @ObjCAction
fun windowDidChangeBackingProperties(notification: NSNotification) { fun windowDidChangeBackingProperties(notification: NSNotification) {
redrawer?.syncBounds() redrawer?.syncBounds()
redrawer?.redrawImmediately(updateNeeded = true) redrawer?.renderImmediately()
} }
fun addObserver() { fun addObserver() {
...@@ -127,7 +127,7 @@ actual open class SkiaLayer { ...@@ -127,7 +127,7 @@ actual open class SkiaLayer {
nsViewObserver.addObserver() nsViewObserver.addObserver()
redrawer = createNativeRedrawer(this, renderApi).apply { redrawer = createNativeRedrawer(this, renderApi).apply {
syncBounds() syncBounds()
needRedraw() needRender()
} }
} }
...@@ -140,10 +140,12 @@ actual open class SkiaLayer { ...@@ -140,10 +140,12 @@ actual open class SkiaLayer {
/** /**
* Schedules a frame to an appropriate moment. * Schedules a frame to an appropriate moment.
*/ */
actual fun needRedraw(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
redrawer?.needRedraw(throttledToVsync) redrawer?.needRender(throttledToVsync)
} }
actual fun needRedraw() = needRender()
/** /**
* Updates the [picture] according to current [nanoTime] * Updates the [picture] according to current [nanoTime]
*/ */
......
...@@ -137,7 +137,7 @@ internal class MacOsMetalRedrawer( ...@@ -137,7 +137,7 @@ internal class MacOsMetalRedrawer(
/** /**
* Schedules a frame [draw] to an appropriate moment. * Schedules a frame [draw] to an appropriate moment.
*/ */
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
checkDisposed() checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
...@@ -150,10 +150,10 @@ internal class MacOsMetalRedrawer( ...@@ -150,10 +150,10 @@ internal class MacOsMetalRedrawer(
/** /**
* Invokes [draw] right away. * Invokes [draw] right away.
*/ */
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
checkDisposed() checkDisposed()
autoreleasepool { autoreleasepool {
if (!isDisposed && updateNeeded) { if (!isDisposed) {
update() update()
} }
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
......
...@@ -38,7 +38,7 @@ internal class MacOsOpenGLRedrawer( ...@@ -38,7 +38,7 @@ internal class MacOsOpenGLRedrawer(
} }
private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) { private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
redrawImmediately(updateNeeded = true) renderImmediately()
} }
override fun dispose() { override fun dispose() {
...@@ -70,11 +70,11 @@ internal class MacOsOpenGLRedrawer( ...@@ -70,11 +70,11 @@ internal class MacOsOpenGLRedrawer(
skiaLayer.update(nanoTime) skiaLayer.update(nanoTime)
} }
override fun needRedraw(throttledToVsync: Boolean) { override fun needRender(throttledToVsync: Boolean) {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately(updateNeeded: Boolean) { override fun renderImmediately() {
glLayer.setNeedsDisplay() glLayer.setNeedsDisplay()
skiaLayer.nsView.setNeedsDisplay(true) skiaLayer.nsView.setNeedsDisplay(true)
} }
......
...@@ -23,10 +23,12 @@ actual open class SkiaLayer { ...@@ -23,10 +23,12 @@ actual open class SkiaLayer {
get() = false get() = false
set(_) { throw UnsupportedOperationException() } set(_) { throw UnsupportedOperationException() }
actual fun needRedraw(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
needRedrawCallback.invoke() needRedrawCallback.invoke()
} }
actual fun needRedraw() = needRender()
actual val component: Any? actual val component: Any?
get() = this.view get() = this.view
......
...@@ -65,7 +65,7 @@ class SkikoUIView : UIView { ...@@ -65,7 +65,7 @@ class SkikoUIView : UIView {
} }
) )
skiaLayer.needRedrawCallback = _redrawer::needRedraw skiaLayer.needRedrawCallback = _redrawer::needRender
skiaLayer.view = this skiaLayer.view = this
} }
......
...@@ -190,7 +190,7 @@ internal class MetalRedrawer( ...@@ -190,7 +190,7 @@ internal class MetalRedrawer(
context.close() context.close()
} }
internal fun needRedraw() { internal fun needRender() {
displayLinkConditions.needsRedrawOnNextVsync = true displayLinkConditions.needsRedrawOnNextVsync = true
} }
......
...@@ -48,10 +48,12 @@ actual open class SkiaLayer { ...@@ -48,10 +48,12 @@ actual open class SkiaLayer {
/** /**
* Schedules a drawFrame to the appropriate moment. * Schedules a drawFrame to the appropriate moment.
*/ */
actual fun needRedraw(throttledToVsync: Boolean) { actual fun needRender(throttledToVsync: Boolean) {
state?.needRedraw() state?.needRedraw()
} }
actual fun needRedraw() = needRender()
/** /**
* An implementation of [SkikoRenderDelegate] with content rendering and * An implementation of [SkikoRenderDelegate] with content rendering and
* event processing logic. * event processing logic.
......
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