Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
skiko
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
liuqiang
skiko
Commits
afcd9e54
Unverified
Commit
afcd9e54
authored
Oct 12, 2025
by
Alexander Maryanovsky
Committed by
GitHub
Oct 12, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix drawing issues and add API to request render-and-draw explicitly (#1104)
parent
2044a13b
Changes
25
Show whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
187 additions
and
127 deletions
+187
-127
Actuals.android.kt
...androidMain/kotlin/org/jetbrains/skiko/Actuals.android.kt
+2
-2
SkiaLayer.android.kt
...droidMain/kotlin/org/jetbrains/skiko/SkiaLayer.android.kt
+4
-2
SkiaLayer.awt.kt
...o/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt
+26
-19
AbstractDirectSoftwareRedrawer.kt
...etbrains/skiko/redrawer/AbstractDirectSoftwareRedrawer.kt
+3
-5
AngleRedrawer.kt
...Main/kotlin/org/jetbrains/skiko/redrawer/AngleRedrawer.kt
+3
-5
Direct3DRedrawer.kt
...n/kotlin/org/jetbrains/skiko/redrawer/Direct3DRedrawer.kt
+3
-5
LinuxOpenGLRedrawer.kt
...otlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt
+3
-5
LinuxSoftwareRedrawer.kt
...lin/org/jetbrains/skiko/redrawer/LinuxSoftwareRedrawer.kt
+2
-2
MetalRedrawer.kt
...Main/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt
+12
-15
SoftwareRedrawer.kt
...n/kotlin/org/jetbrains/skiko/redrawer/SoftwareRedrawer.kt
+3
-5
WindowsOpenGLRedrawer.kt
...lin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt
+3
-5
SkiaLayerPerformanceTest.kt
...st/kotlin/org/jetbrains/skiko/SkiaLayerPerformanceTest.kt
+1
-1
SkiaLayerTest.kt
...o/src/awtTest/kotlin/org/jetbrains/skiko/SkiaLayerTest.kt
+85
-36
SkiaLayer.kt
skiko/src/commonMain/kotlin/org/jetbrains/skiko/SkiaLayer.kt
+7
-1
SkikoRenderDelegate.kt
...monMain/kotlin/org/jetbrains/skiko/SkikoRenderDelegate.kt
+1
-1
Redrawer.kt
...ommonMain/kotlin/org/jetbrains/skiko/redrawer/Redrawer.kt
+2
-2
SkikoProperties.kt
...src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt
+4
-0
SkiaLayer.linux.kt
...c/linuxMain/kotlin/org/jetbrains/skiko/SkiaLayer.linux.kt
+2
-1
SkiaLayer.macos.kt
...c/macosMain/kotlin/org/jetbrains/skiko/SkiaLayer.macos.kt
+7
-5
MetalRedrawer.macos.kt
...otlin/org/jetbrains/skiko/redrawer/MetalRedrawer.macos.kt
+3
-3
OpenGLRedrawer.macos.kt
...tlin/org/jetbrains/skiko/redrawer/OpenGLRedrawer.macos.kt
+3
-3
SkiaLayer.uikit.kt
...c/uikitMain/kotlin/org/jetbrains/skiko/SkiaLayer.uikit.kt
+3
-1
SkikoUIView.kt
...o/src/uikitMain/kotlin/org/jetbrains/skiko/SkikoUIView.kt
+1
-1
MetalRedrawer.uikit.kt
...otlin/org/jetbrains/skiko/redrawer/MetalRedrawer.uikit.kt
+1
-1
SkiaLayer.js.kt
skiko/src/webMain/kotlin/org/jetbrains/skiko/SkiaLayer.js.kt
+3
-1
No files found.
skiko/src/androidMain/kotlin/org/jetbrains/skiko/Actuals.android.kt
View file @
afcd9e54
...
...
@@ -15,8 +15,8 @@ internal class AndroidOpenGLRedrawer(
private
val
properties
:
SkiaLayerProperties
)
:
Redrawer
{
override
fun
dispose
()
=
TODO
()
override
fun
needRe
draw
(
canUpdateImmediately
:
Boolean
)
=
TODO
()
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
TODO
()
override
fun
needRe
nder
(
canUpdateImmediately
:
Boolean
)
=
TODO
()
override
fun
re
nderImmediately
(
)
=
TODO
()
override
fun
update
(
nanoTime
:
Long
)
=
TODO
()
override
val
renderInfo
:
String
get
()
=
"Android renderer"
...
...
skiko/src/androidMain/kotlin/org/jetbrains/skiko/SkiaLayer.android.kt
View file @
afcd9e54
...
...
@@ -47,7 +47,7 @@ actual open class SkiaLayer {
view
.
setFocusableInTouchMode
(
true
)
needRe
draw
()
needRe
nder
()
}
actual
fun
detach
()
{
...
...
@@ -57,12 +57,14 @@ actual open class SkiaLayer {
}
}
actual
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
actual
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
glView
?.
apply
{
scheduleFrame
()
}
}
actual
fun
needRedraw
()
=
needRender
()
actual
val
pixelGeometry
:
PixelGeometry
get
()
=
PixelGeometry
.
UNKNOWN
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt
View file @
afcd9e54
...
...
@@ -113,8 +113,8 @@ actual open class SkiaLayer internal constructor(
// 2. HardwareLayer.paint is also not always called.
// For example, on macOs when we resize window or change DPI
//
// 3. to avoid double paint in one single frame, use needRe
draw instead of redraw
Immediately
redrawer
?.
needRe
draw
(
throttledToVsync
=
false
)
// 3. to avoid double paint in one single frame, use needRe
nder instead of render
Immediately
redrawer
?.
needRe
nder
(
throttledToVsync
=
false
)
}
@Suppress
(
"OVERRIDE_DEPRECATION"
)
...
...
@@ -124,7 +124,7 @@ actual open class SkiaLayer internal constructor(
super
.
reshape
(
x
,
y
,
width
,
height
)
redrawer
?.
syncBounds
()
redrawer
?.
needRe
draw
(
throttledToVsync
=
false
)
redrawer
?.
needRe
nder
(
throttledToVsync
=
false
)
}
override
fun
getInputMethodRequests
():
InputMethodRequests
?
{
...
...
@@ -384,7 +384,7 @@ actual open class SkiaLayer internal constructor(
@Suppress
(
"DEPRECATION"
)
super
.
reshape
(
x
,
y
,
w
,
h
)
// Calling re
draw
Immediately as early as possible improves the situation with
// Calling re
nder
Immediately as early as possible improves the situation with
// the visual glitch when the drawn content is scaled during window resize.
// Note, however, that this actually causes the reverse glitch (content appears
// scaled in the other direction from the window size), but this seems to
...
...
@@ -394,7 +394,7 @@ actual open class SkiaLayer internal constructor(
// so apply this fix only for the Direct3D case.
if
(
renderApi
==
GraphicsApi
.
DIRECT3D
&&
isShowing
)
{
redrawer
?.
syncBounds
()
redrawer
?.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
?.
re
nderImmediately
(
)
}
// Setting the bounds of children should be done only in the layout pass,
...
...
@@ -416,15 +416,14 @@ actual open class SkiaLayer internal constructor(
override
fun
paint
(
g
:
Graphics
)
{
Logger
.
debug
{
"paint called on SkiaLayer $this"
}
val
updateNeeded
=
checkContentScale
()
redrawer
?.
redrawImmediately
(
updateNeeded
=
updateNeeded
)
checkContentScale
()
redrawer
?.
needRender
(
throttledToVsync
=
false
)
}
// Workaround for JBR-5274 and JBR-5305
fun
checkContentScale
()
:
Boolean
{
fun
checkContentScale
()
{
val
currentGraphicsContextScaleTransform
=
graphicsConfiguration
.
defaultTransform
return
(
currentGraphicsContextScaleTransform
!=
latestReceivedGraphicsContextScaleTransform
).
also
{
if
(
it
)
{
if
(
currentGraphicsContextScaleTransform
!=
latestReceivedGraphicsContextScaleTransform
)
{
firePropertyChange
(
"graphicsContextScaleTransform"
,
latestReceivedGraphicsContextScaleTransform
,
...
...
@@ -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)
...
...
@@ -546,12 +551,14 @@ actual open class SkiaLayer internal constructor(
/**
* Redraw on the next animation Frame (on vsync signal if vsync is enabled).
*/
actual
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
actual
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
check
(
isEventDispatchThread
())
{
"Method should be called from AWT event dispatch thread"
}
check
(!
isDisposed
)
{
"SkiaLayer is disposed"
}
redrawer
?.
needRe
draw
(
throttledToVsync
)
redrawer
?.
needRe
nder
(
throttledToVsync
)
}
actual
fun
needRedraw
()
=
needRender
()
internal
fun
update
(
nanoTime
:
Long
)
{
check
(
isEventDispatchThread
())
{
"Method should be called from AWT event dispatch thread"
}
check
(!
isDisposed
)
{
"SkiaLayer is disposed"
}
...
...
@@ -608,7 +615,7 @@ actual open class SkiaLayer internal constructor(
if
(!
isDisposed
)
{
Logger
.
warn
(
e
)
{
"Exception in draw scope"
}
redrawerManager
.
findNextWorkingRenderApi
()
redrawer
?.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
?.
re
nderImmediately
(
)
}
}
}
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/AbstractDirectSoftwareRedrawer.kt
View file @
afcd9e54
...
...
@@ -29,16 +29,14 @@ internal abstract class AbstractDirectSoftwareRedrawer(
protected
var
device
=
0L
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
frameDispatcher
.
scheduleFrame
()
}
protected
open
fun
draw
()
=
inDrawScope
(
contextHandler
::
draw
)
override
fun
redrawImmediately
(
updateNeeded
:
Boolean
)
{
if
(
updateNeeded
)
{
override
fun
renderImmediately
()
{
update
()
}
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
draw
()
}
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/AngleRedrawer.kt
View file @
afcd9e54
...
...
@@ -59,16 +59,14 @@ internal class AngleRedrawer(
super
.
dispose
()
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
checkDisposed
()
frameDispatcher
.
scheduleFrame
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
checkDisposed
()
if
(
updateNeeded
)
{
update
()
}
inDrawScope
{
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
drawAndSwap
(
withVsync
=
SkikoProperties
.
windowsWaitForVsyncOnRedrawImmediately
)
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/Direct3DRedrawer.kt
View file @
afcd9e54
...
...
@@ -63,16 +63,14 @@ internal class Direct3DRedrawer(
super
.
dispose
()
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
checkDisposed
()
frameDispatcher
.
scheduleFrame
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
checkDisposed
()
if
(
updateNeeded
)
{
update
()
}
inDrawScope
{
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
drawAndSwap
(
withVsync
=
SkikoProperties
.
windowsWaitForVsyncOnRedrawImmediately
)
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt
View file @
afcd9e54
...
...
@@ -72,17 +72,15 @@ internal class LinuxOpenGLRedrawer(
super
.
dispose
()
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
checkDisposed
()
toRedraw
.
add
(
this
)
frameDispatcher
.
scheduleFrame
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
layer
.
backedLayer
.
lockLinuxDrawingSurface
{
override
fun
re
nderImmediately
(
)
=
layer
.
backedLayer
.
lockLinuxDrawingSurface
{
checkDisposed
()
if
(
updateNeeded
)
{
update
()
}
inDrawScope
{
it
.
makeCurrent
(
context
)
contextHandler
.
draw
()
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/LinuxSoftwareRedrawer.kt
View file @
afcd9e54
...
...
@@ -31,8 +31,8 @@ internal class LinuxSoftwareRedrawer(
super
.
draw
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
layer
.
backedLayer
.
lockLinuxDrawingSurface
{
super
.
re
drawImmediately
(
updateNeeded
)
override
fun
re
nderImmediately
(
)
=
layer
.
backedLayer
.
lockLinuxDrawingSurface
{
super
.
re
nderImmediately
(
)
}
override
fun
resize
(
width
:
Int
,
height
:
Int
)
=
layer
.
backedLayer
.
lockLinuxDrawingSurface
{
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt
View file @
afcd9e54
...
...
@@ -94,26 +94,23 @@ internal class MetalRedrawer(
super
.
dispose
()
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
checkDisposed
()
frameDispatcher
.
scheduleFrame
(
needUpdate
=
true
,
throttledToVsync
=
throttledToVsync
)
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
checkDisposed
()
if
(
updateNeeded
)
{
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
{
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
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
()
}
}
}
}
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/SoftwareRedrawer.kt
View file @
afcd9e54
...
...
@@ -42,15 +42,13 @@ internal class SoftwareRedrawer(
super
.
dispose
()
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
frameDispatcher
.
scheduleFrame
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
checkDisposed
()
if
(
updateNeeded
)
{
update
()
}
inDrawScope
{
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
contextHandler
.
draw
()
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt
View file @
afcd9e54
...
...
@@ -55,17 +55,15 @@ internal class WindowsOpenGLRedrawer(
super
.
dispose
()
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
check
(!
isDisposed
)
{
"WindowsOpenGLRedrawer is disposed"
}
toRedraw
.
add
(
this
)
frameDispatcher
.
scheduleFrame
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
check
(!
isDisposed
)
{
"WindowsOpenGLRedrawer is disposed"
}
if
(
updateNeeded
)
{
update
()
}
inDrawScope
{
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
makeCurrent
()
...
...
skiko/src/awtTest/kotlin/org/jetbrains/skiko/SkiaLayerPerformanceTest.kt
View file @
afcd9e54
...
...
@@ -59,7 +59,7 @@ class SkiaLayerPerformanceTest {
if
(
canCollect
&&
frameTimes
.
size
<
frameCount
)
{
frameTimes
.
add
(
System
.
nanoTime
())
// we check the real time, not the time provided by the argument
}
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
}
}
window
.
isUndecorated
=
true
...
...
skiko/src/awtTest/kotlin/org/jetbrains/skiko/SkiaLayerTest.kt
View file @
afcd9e54
...
...
@@ -106,11 +106,11 @@ class SkiaLayerTest {
override
fun
keyTyped
(
e
:
KeyEvent
?)
{
launch
{
val
redrawer
=
window
.
layer
.
redrawer
as
MetalRedrawer
redrawer
.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
.
re
nderImmediately
(
)
counter1
+=
1
redrawer
.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
.
re
nderImmediately
(
)
counter2
+=
1
redrawer
.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
.
re
nderImmediately
(
)
}
}
})
...
...
@@ -179,7 +179,7 @@ class SkiaLayerTest {
screenshots
.
assert
(
window
.
bounds
,
"frame1"
)
app
.
rectWidth
=
100
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
delay
(
1000
)
screenshots
.
assert
(
window
.
bounds
,
"frame2"
)
...
...
@@ -238,7 +238,7 @@ class SkiaLayerTest {
screenshots
.
assert
(
window
.
bounds
,
"frame1"
)
app
.
rectWidth
=
100
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
delay
(
1000
)
screenshots
.
assert
(
window
.
bounds
,
"frame2"
)
...
...
@@ -271,12 +271,12 @@ class SkiaLayerTest {
window
.
defaultCloseOperation
=
WindowConstants
.
DISPOSE_ON_CLOSE
window
.
isUndecorated
=
true
window
.
isVisible
=
true
layer
.
needRe
draw
()
layer
.
needRe
nder
()
delay
(
1000
)
assertEquals
(
0
,
renderedWidth
)
renderedWidth
=
-
1
layer
.
needRe
draw
()
layer
.
needRe
nder
()
delay
(
1000
)
assertEquals
(
0
,
renderedWidth
)
...
...
@@ -439,7 +439,7 @@ class SkiaLayerTest {
}
@Test
fun
`
should
call
onRender
after
init
,
after
resize
,
and
only
once
after
needRe
draw
`
()
=
uiTest
{
fun
`
should
call
onRender
after
init
,
after
resize
,
and
only
once
after
needRe
nder
`
()
=
uiTest
{
var
renderCount
=
0
val
window
=
UiTestWindow
()
...
...
@@ -464,7 +464,7 @@ class SkiaLayerTest {
assertTrue
(
renderCount
>
0
)
renderCount
=
0
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
delay
(
1000
)
assertEquals
(
1
,
renderCount
)
}
finally
{
...
...
@@ -561,7 +561,7 @@ class SkiaLayerTest {
delay
(
100
)
val
window
=
openWindow
()
window
.
isVisible
=
true
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
yield
()
window
.
paint
(
window
.
graphics
)
assertRenderApiFor
(
window
.
layer
)
...
...
@@ -570,9 +570,12 @@ class SkiaLayerTest {
}
private
abstract
class
BaseTestRedrawer
(
val
layer
:
SkiaLayer
):
Redrawer
{
private
val
frameDispatcher
=
FrameDispatcher
(
MainUIDispatcher
)
{
renderImmediately
()
}
override
fun
dispose
()
=
Unit
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
=
Unit
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
Unit
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
=
frameDispatcher
.
scheduleFrame
()
override
fun
re
nderImmediately
(
)
=
Unit
override
fun
update
(
nanoTime
:
Long
)
=
layer
.
update
(
nanoTime
)
override
val
renderInfo
:
String
...
...
@@ -587,7 +590,7 @@ class SkiaLayerTest {
override
fun
initContext
()
=
false
override
fun
initCanvas
()
=
Unit
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
layer
.
inDrawScope
(
contextHandler
::
draw
)
override
fun
re
nderImmediately
(
)
=
layer
.
inDrawScope
(
contextHandler
::
draw
)
}
}
}
...
...
@@ -601,7 +604,7 @@ class SkiaLayerTest {
fun
`
fallback
to
software
renderer
,
fail
on
draw
`
()
=
uiTest
{
testFallbackToSoftware
{
layer
,
_
,
_
,
_
->
object
:
BaseTestRedrawer
(
layer
)
{
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
layer
.
inDrawScope
{
override
fun
re
nderImmediately
(
)
=
layer
.
inDrawScope
{
throw
RenderException
()
}
}
...
...
@@ -625,7 +628,7 @@ class SkiaLayerTest {
screenshots
.
assert
(
window
.
bounds
,
"frame1"
,
"testFallbackToSoftware"
)
app
.
rectWidth
=
100
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
delay
(
1000
)
screenshots
.
assert
(
window
.
bounds
,
"frame2"
,
"testFallbackToSoftware"
)
...
...
@@ -657,7 +660,7 @@ class SkiaLayerTest {
val
window
=
UiTestWindow
(
renderFactory
=
OverrideNonSoftwareRenderFactory
{
layer
,
_
,
_
,
_
->
object
:
BaseTestRedrawer
(
layer
)
{
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
=
layer
.
inDrawScope
{
override
fun
re
nderImmediately
(
)
=
layer
.
inDrawScope
{
throw
RenderException
()
}
}
...
...
@@ -703,7 +706,7 @@ class SkiaLayerTest {
drawCount
++
if
(
drawCount
<
targetDrawCount
)
{
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
}
else
{
onDrawCompleted
.
complete
(
Unit
)
}
...
...
@@ -823,7 +826,7 @@ class SkiaLayerTest {
repeat
(
10
)
{
window
.
isVisible
=
true
delay
(
16
)
window
.
layer
.
needRe
draw
()
window
.
layer
.
needRe
nder
()
delay
(
500
)
window
.
isVisible
=
false
...
...
@@ -1145,7 +1148,7 @@ class SkiaLayerTest {
}
@Test
fun
`
temporary
change
is
not
visible
`
()
=
uiTest
{
fun
`
temporary
change
is
not
visible
with
needRender
(
throttledToVsync
=
false
)
`
()
=
uiTest
{
assumeTrue
(
hostOs
.
isMacOS
)
// The separation between update and draw is only implemented in MetalRedrawer at the moment
// Don't use assumeTrue, as uiTest iterates over multiple renderers,
...
...
@@ -1181,18 +1184,19 @@ class SkiaLayerTest {
val
robot
=
Robot
()
var
tempColorVisibleCount
=
0
val
testCount
=
50
try
{
repeat
(
50
)
{
repeat
(
testCount
)
{
// Wait for just after the next vsync, so we have plenty of time until the one after it
val
vSyncer
=
MetalVSyncer
(
window
.
layer
.
windowHandle
)
vSyncer
.
waitForVSync
()
// Set the color to temp, then immediately back to normal
renderDelegate
.
color
=
tempColor
renderDelegate
.
layer
.
needRe
draw
(
throttledToVsync
=
false
)
renderDelegate
.
layer
.
needRe
nder
(
throttledToVsync
=
false
)
renderChannel
.
receive
()
// Wait until render is actually called
renderDelegate
.
color
=
color
renderDelegate
.
layer
.
needRe
draw
(
throttledToVsync
=
false
)
renderDelegate
.
layer
.
needRe
nder
(
throttledToVsync
=
false
)
// Check whether the temp color was visible
val
startTime
=
System
.
currentTimeMillis
()
...
...
@@ -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
// is reduced.
// 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
{
window
.
dispose
()
}
}
@Test
fun
`
needRe
draw
throttled
and
regular
calls
render
and
draw
once
`
()
=
uiTest
{
// Check that calling both needRe
draw(true) and needRedraw
(false) causes only one render and one draw call
fun
`
needRe
nder
throttled
and
regular
calls
render
and
draw
once
`
()
=
uiTest
{
// Check that calling both needRe
nder(true) and needRender
(false) causes only one render and one draw call
var
renderCalls
=
0
val
renderChannel
=
Channel
<
Unit
>(
Channel
.
CONFLATED
)
...
...
@@ -1259,27 +1263,72 @@ class SkiaLayerTest {
renderCalls
=
0
drawCalls
=
0
withContext
(
MainUIDispatcher
)
{
window
.
layer
.
needRe
draw
(
true
)
window
.
layer
.
needRe
draw
(
false
)
window
.
layer
.
needRe
nder
(
true
)
window
.
layer
.
needRe
nder
(
false
)
}
delay
(
100
)
assertEquals
(
"Render was called more than once on needRe
draw(true), needRedraw
(false)"
,
1
,
renderCalls
)
assertEquals
(
"Draw was called more than once on needRe
draw(true), needRedraw
(false)"
,
1
,
drawCalls
)
assertEquals
(
"Render was called more than once on needRe
nder(true), needRender
(false)"
,
1
,
renderCalls
)
assertEquals
(
"Draw was called more than once on needRe
nder(true), needRender
(false)"
,
1
,
drawCalls
)
renderCalls
=
0
drawCalls
=
0
withContext
(
MainUIDispatcher
)
{
window
.
layer
.
needRe
draw
(
false
)
window
.
layer
.
needRe
draw
(
true
)
window
.
layer
.
needRe
nder
(
false
)
window
.
layer
.
needRe
nder
(
true
)
}
delay
(
100
)
assertEquals
(
"Render was called more than once on needRe
draw(false), needRedraw
(true)"
,
1
,
renderCalls
)
assertEquals
(
"Draw was called more than once on needRe
draw(true), needRedraw
(true)"
,
1
,
drawCalls
)
assertEquals
(
"Render was called more than once on needRe
nder(false), needRender
(true)"
,
1
,
renderCalls
)
assertEquals
(
"Draw was called more than once on needRe
nder(true), needRender
(true)"
,
1
,
drawCalls
)
}
finally
{
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
val
getContentScale
:
()
->
Float
,
var
rectWidth
:
Int
,
...
...
@@ -1341,7 +1390,7 @@ class SkiaLayerTest {
color
=
Color
.
RED
.
rgb
})
layer
.
needRe
draw
()
layer
.
needRe
nder
()
}
}
...
...
@@ -1354,7 +1403,7 @@ class SkiaLayerTest {
var
continuousRedraw
=
continuousRedraw
set
(
value
)
{
if
(
value
)
layer
.
needRe
draw
(
throttledToVsync
=
true
)
layer
.
needRe
nder
(
throttledToVsync
=
true
)
field
=
value
}
...
...
@@ -1370,7 +1419,7 @@ class SkiaLayerTest {
override
fun
onRender
(
canvas
:
Canvas
,
width
:
Int
,
height
:
Int
,
nanoTime
:
Long
)
{
canvas
.
drawRect
(
Rect
(
0f
,
0f
,
width
.
toFloat
(),
height
.
toFloat
()),
paint
)
if
(
continuousRedraw
)
{
layer
.
needRe
draw
()
layer
.
needRe
nder
()
}
}
}
...
...
skiko/src/commonMain/kotlin/org/jetbrains/skiko/SkiaLayer.kt
View file @
afcd9e54
...
...
@@ -61,7 +61,13 @@ expect open class SkiaLayer {
* @param throttledToVsync Whether to throttle calling [renderDelegate]'s [SkikoRenderDelegate.onRender] to at most
* 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.
...
...
skiko/src/commonMain/kotlin/org/jetbrains/skiko/SkikoRenderDelegate.kt
View file @
afcd9e54
...
...
@@ -15,6 +15,6 @@ open class SkiaLayerRenderDelegate(
canvas
.
scale
(
contentScale
,
contentScale
)
renderDelegate
.
onRender
(
canvas
,
(
width
/
contentScale
).
toInt
(),
(
height
/
contentScale
).
toInt
(),
nanoTime
)
// Request next frame immediately.
layer
.
needRe
draw
()
layer
.
needRe
nder
()
}
}
skiko/src/commonMain/kotlin/org/jetbrains/skiko/redrawer/Redrawer.kt
View file @
afcd9e54
...
...
@@ -6,8 +6,8 @@ private val initialTime = TimeSource.Monotonic.markNow()
internal
interface
Redrawer
{
fun
dispose
()
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
fun
re
nderImmediately
(
)
fun
syncBounds
()
=
Unit
fun
update
(
nanoTime
:
Long
=
initialTime
.
elapsedNow
().
inWholeNanoseconds
)
fun
setVisible
(
isVisible
:
Boolean
)
=
Unit
...
...
skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt
View file @
afcd9e54
...
...
@@ -42,6 +42,10 @@ object SkikoProperties {
}
}
val
macOSWaitForPreviousFrameVsyncOnRedrawImmediately
:
Boolean
get
()
{
return
getProperty
(
"skiko.rendering.macos.waitForPreviousFrameVsyncOnRedrawImmediately"
)
?.
toBoolean
()
?:
true
}
val
windowsWaitForVsyncOnRedrawImmediately
:
Boolean
get
()
{
return
getProperty
(
"skiko.rendering.windows.waitForFrameVsyncOnRedrawImmediately"
)
?.
toBoolean
()
?:
false
}
...
...
skiko/src/linuxMain/kotlin/org/jetbrains/skiko/SkiaLayer.linux.kt
View file @
afcd9e54
...
...
@@ -17,9 +17,10 @@ actual open class SkiaLayer {
set
(
value
)
{}
actual
val
component
:
Any
?
get
()
=
TODO
(
"Not yet implemented"
)
actual
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
actual
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
TODO
(
"unimplemented"
)
}
actual
fun
needRedraw
()
=
needRender
()
actual
fun
attachTo
(
container
:
Any
)
{
TODO
(
"unimplemented"
)
}
...
...
skiko/src/macosMain/kotlin/org/jetbrains/skiko/SkiaLayer.macos.kt
View file @
afcd9e54
...
...
@@ -85,13 +85,13 @@ actual open class SkiaLayer {
@ObjCAction
fun
frameDidChange
(
notification
:
NSNotification
)
{
redrawer
?.
syncBounds
()
redrawer
?.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
?.
re
nderImmediately
(
)
}
@ObjCAction
fun
windowDidChangeBackingProperties
(
notification
:
NSNotification
)
{
redrawer
?.
syncBounds
()
redrawer
?.
re
drawImmediately
(
updateNeeded
=
true
)
redrawer
?.
re
nderImmediately
(
)
}
fun
addObserver
()
{
...
...
@@ -127,7 +127,7 @@ actual open class SkiaLayer {
nsViewObserver
.
addObserver
()
redrawer
=
createNativeRedrawer
(
this
,
renderApi
).
apply
{
syncBounds
()
needRe
draw
()
needRe
nder
()
}
}
...
...
@@ -140,10 +140,12 @@ actual open class SkiaLayer {
/**
* Schedules a frame to an appropriate moment.
*/
actual
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
redrawer
?.
needRe
draw
(
throttledToVsync
)
actual
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
redrawer
?.
needRe
nder
(
throttledToVsync
)
}
actual
fun
needRedraw
()
=
needRender
()
/**
* Updates the [picture] according to current [nanoTime]
*/
...
...
skiko/src/macosMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.macos.kt
View file @
afcd9e54
...
...
@@ -137,7 +137,7 @@ internal class MacOsMetalRedrawer(
/**
* Schedules a frame [draw] to an appropriate moment.
*/
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
checkDisposed
()
frameDispatcher
.
scheduleFrame
()
}
...
...
@@ -150,10 +150,10 @@ internal class MacOsMetalRedrawer(
/**
* Invokes [draw] right away.
*/
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
checkDisposed
()
autoreleasepool
{
if
(!
isDisposed
&&
updateNeeded
)
{
if
(!
isDisposed
)
{
update
()
}
if
(!
isDisposed
)
{
// Redrawer may be disposed in user code, during `update`
...
...
skiko/src/macosMain/kotlin/org/jetbrains/skiko/redrawer/OpenGLRedrawer.macos.kt
View file @
afcd9e54
...
...
@@ -38,7 +38,7 @@ internal class MacOsOpenGLRedrawer(
}
private
val
frameDispatcher
=
FrameDispatcher
(
SkikoDispatchers
.
Main
)
{
re
drawImmediately
(
updateNeeded
=
true
)
re
nderImmediately
(
)
}
override
fun
dispose
()
{
...
...
@@ -70,11 +70,11 @@ internal class MacOsOpenGLRedrawer(
skiaLayer
.
update
(
nanoTime
)
}
override
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
override
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
frameDispatcher
.
scheduleFrame
()
}
override
fun
re
drawImmediately
(
updateNeeded
:
Boolean
)
{
override
fun
re
nderImmediately
(
)
{
glLayer
.
setNeedsDisplay
()
skiaLayer
.
nsView
.
setNeedsDisplay
(
true
)
}
...
...
skiko/src/uikitMain/kotlin/org/jetbrains/skiko/SkiaLayer.uikit.kt
View file @
afcd9e54
...
...
@@ -23,10 +23,12 @@ actual open class SkiaLayer {
get
()
=
false
set
(
_
)
{
throw
UnsupportedOperationException
()
}
actual
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
actual
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
needRedrawCallback
.
invoke
()
}
actual
fun
needRedraw
()
=
needRender
()
actual
val
component
:
Any
?
get
()
=
this
.
view
...
...
skiko/src/uikitMain/kotlin/org/jetbrains/skiko/SkikoUIView.kt
View file @
afcd9e54
...
...
@@ -65,7 +65,7 @@ class SkikoUIView : UIView {
}
)
skiaLayer
.
needRedrawCallback
=
_redrawer
::
needRe
draw
skiaLayer
.
needRedrawCallback
=
_redrawer
::
needRe
nder
skiaLayer
.
view
=
this
}
...
...
skiko/src/uikitMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.uikit.kt
View file @
afcd9e54
...
...
@@ -190,7 +190,7 @@ internal class MetalRedrawer(
context
.
close
()
}
internal
fun
needRe
draw
()
{
internal
fun
needRe
nder
()
{
displayLinkConditions
.
needsRedrawOnNextVsync
=
true
}
...
...
skiko/src/webMain/kotlin/org/jetbrains/skiko/SkiaLayer.js.kt
View file @
afcd9e54
...
...
@@ -48,10 +48,12 @@ actual open class SkiaLayer {
/**
* Schedules a drawFrame to the appropriate moment.
*/
actual
fun
needRe
draw
(
throttledToVsync
:
Boolean
)
{
actual
fun
needRe
nder
(
throttledToVsync
:
Boolean
)
{
state
?.
needRedraw
()
}
actual
fun
needRedraw
()
=
needRender
()
/**
* An implementation of [SkikoRenderDelegate] with content rendering and
* event processing logic.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment