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
544b4b76
Unverified
Commit
544b4b76
authored
Apr 05, 2021
by
Nikolay Igotti
Committed by
GitHub
Apr 05, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix Metal crazy rendering on invisible windows. (#89)
parent
ec704f7b
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
70 additions
and
16 deletions
+70
-16
FrameDispatcherTest.kt
...rc/test/kotlin/org/jetbrains/skiko/FrameDispatcherTest.kt
+34
-3
SkiaWindowPerformanceTest.kt
...t/kotlin/org/jetbrains/skiko/SkiaWindowPerformanceTest.kt
+1
-0
FrameDispatcher.kt
...src/jvmMain/kotlin/org/jetbrains/skiko/FrameDispatcher.kt
+18
-8
MetalRedrawer.kt
...Main/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt
+11
-5
MetalRedrawer.mm
skiko/src/jvmMain/objectiveC/macos/MetalRedrawer.mm
+6
-0
No files found.
samples/SkijaInjectSample/src/test/kotlin/org/jetbrains/skiko/FrameDispatcherTest.kt
View file @
544b4b76
...
@@ -5,6 +5,7 @@ import kotlinx.coroutines.test.TestCoroutineScope
...
@@ -5,6 +5,7 @@ import kotlinx.coroutines.test.TestCoroutineScope
import
kotlinx.coroutines.test.runBlockingTest
import
kotlinx.coroutines.test.runBlockingTest
import
org.junit.Assert.assertEquals
import
org.junit.Assert.assertEquals
import
org.junit.Test
import
org.junit.Test
import
java.util.concurrent.Executors
@OptIn
(
ExperimentalCoroutinesApi
::
class
)
@OptIn
(
ExperimentalCoroutinesApi
::
class
)
class
FrameDispatcherTest
{
class
FrameDispatcherTest
{
...
@@ -77,7 +78,6 @@ class FrameDispatcherTest {
...
@@ -77,7 +78,6 @@ class FrameDispatcherTest {
assertEquals
(
2
,
frameCount
)
assertEquals
(
2
,
frameCount
)
}
}
@Suppress
(
"JoinDeclarationAndAssignment"
)
@Test
@Test
fun
`
scheduleFrame
during
onFrame
`
()
=
test
{
fun
`
scheduleFrame
during
onFrame
`
()
=
test
{
lateinit
var
frameDispatcher
:
FrameDispatcher
lateinit
var
frameDispatcher
:
FrameDispatcher
...
@@ -100,7 +100,6 @@ class FrameDispatcherTest {
...
@@ -100,7 +100,6 @@ class FrameDispatcherTest {
assertEquals
(
4
,
frameCount
)
assertEquals
(
4
,
frameCount
)
}
}
@Suppress
(
"JoinDeclarationAndAssignment"
)
@Test
@Test
fun
`
scheduleFrame
multiple
times
during
onFrame
`
()
=
test
{
fun
`
scheduleFrame
multiple
times
during
onFrame
`
()
=
test
{
lateinit
var
frameDispatcher
:
FrameDispatcher
lateinit
var
frameDispatcher
:
FrameDispatcher
...
@@ -125,7 +124,6 @@ class FrameDispatcherTest {
...
@@ -125,7 +124,6 @@ class FrameDispatcherTest {
assertEquals
(
4
,
frameCount
)
assertEquals
(
4
,
frameCount
)
}
}
@Suppress
(
"JoinDeclarationAndAssignment"
)
@Test
@Test
fun
`
cancel
coroutine
scope
`
()
=
test
{
fun
`
cancel
coroutine
scope
`
()
=
test
{
val
scope
=
CoroutineScope
(
coroutineContext
)
val
scope
=
CoroutineScope
(
coroutineContext
)
...
@@ -154,6 +152,39 @@ class FrameDispatcherTest {
...
@@ -154,6 +152,39 @@ class FrameDispatcherTest {
assertEquals
(
3
,
frameCount
)
assertEquals
(
3
,
frameCount
)
}
}
@Test
fun
`
perform
tasks
scheduled
in
the
frame
after
the
frame
`
()
{
val
dispatcher
=
Executors
.
newSingleThreadExecutor
().
asCoroutineDispatcher
()
val
history
=
mutableListOf
<
String
>()
runBlocking
(
dispatcher
)
{
val
job
=
launch
{
val
scope
=
this
lateinit
var
frameDispatcher
:
FrameDispatcher
frameDispatcher
=
FrameDispatcher
(
scope
=
scope
)
{
history
.
add
(
"frame$frameCount"
)
if
(
frameCount
==
0
)
{
scope
.
launch
{
history
.
add
(
"task"
)
}
frameDispatcher
.
scheduleFrame
()
}
frameCount
++
}
frameDispatcher
.
scheduleFrame
()
}
repeat
(
20
)
{
yield
()
}
job
.
cancel
()
}
assertEquals
(
listOf
(
"frame0"
,
"task"
,
"frame1"
),
history
)
}
private
fun
test
(
private
fun
test
(
block
:
suspend
TestCoroutineScope
.()
->
Unit
block
:
suspend
TestCoroutineScope
.()
->
Unit
)
=
runBlockingTest
{
)
=
runBlockingTest
{
...
...
samples/SkijaInjectSample/src/test/kotlin/org/jetbrains/skiko/SkiaWindowPerformanceTest.kt
View file @
544b4b76
...
@@ -134,6 +134,7 @@ native crash in SkiaWindowTest "render single window"
...
@@ -134,6 +134,7 @@ native crash in SkiaWindowTest "render single window"
fun
`
FPS
is
near
display
refresh
rate
(
multiple
windows
)
`
()
=
swingTest
{
fun
`
FPS
is
near
display
refresh
rate
(
multiple
windows
)
`
()
=
swingTest
{
val
windows
=
(
1
..
3
).
map
{
index
->
val
windows
=
(
1
..
3
).
map
{
index
->
TestWindow
(
width
=
40
,
height
=
20
,
frameCount
=
300
,
deviatedTerminalCount
=
10
).
apply
{
TestWindow
(
width
=
40
,
height
=
20
,
frameCount
=
300
,
deviatedTerminalCount
=
10
).
apply
{
toFront
()
location
=
Point
((
index
+
1
)
*
200
,
200
)
location
=
Point
((
index
+
1
)
*
200
,
200
)
}
}
}
}
...
...
skiko/src/jvmMain/kotlin/org/jetbrains/skiko/FrameDispatcher.kt
View file @
544b4b76
package
org.jetbrains.skiko
package
org.jetbrains.skiko
import
kotlinx.coroutines.CompletableDeferred
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.channels.Channel
import
kotlinx.coroutines.launch
import
kotlinx.coroutines.launch
import
kotlinx.coroutines.yield
import
kotlinx.coroutines.yield
import
kotlin.coroutines.CoroutineContext
import
kotlin.coroutines.CoroutineContext
...
@@ -23,13 +23,20 @@ class FrameDispatcher(
...
@@ -23,13 +23,20 @@ class FrameDispatcher(
onFrame
onFrame
)
)
private
var
needFrame
=
CompletableDeferred
<
Unit
>()
private
val
frameChannel
=
Channel
<
Unit
>(
Channel
.
CONFLATED
)
private
var
frameScheduled
=
false
private
val
job
=
scope
.
launch
{
private
val
job
=
scope
.
launch
{
while
(
true
)
{
while
(
true
)
{
needFrame
.
await
()
frameChannel
.
receive
()
needFrame
=
CompletableDeferred
()
frameScheduled
=
false
onFrame
()
onFrame
()
// As per `yield()` documentation:
//
// For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and
// always suspends to be resumed later regardless of the result of [CoroutineDispatcher.isDispatchNeeded].
//
// What means for Swing dispatcher we'll process all pending events and resume renderer.
yield
()
yield
()
}
}
}
}
...
@@ -41,12 +48,15 @@ class FrameDispatcher(
...
@@ -41,12 +48,15 @@ class FrameDispatcher(
/**
/**
* Schedule next frame to render in the frame loop.
* Schedule next frame to render in the frame loop.
*
*
* Multiple calls of
scheduleFrame before beginning of the frame will cause only one onFrame
.
* Multiple calls of
`scheduleFrame` before beginning of the frame will cause only one `onFrame`
.
*
*
* Multiple calls of
scheduleFrame
after beginning of the frame but before its ending
* Multiple calls of
`scheduleFrame`
after beginning of the frame but before its ending
* will schedule next single
onFrame
after the current one.
* will schedule next single
`onFrame`
after the current one.
*/
*/
fun
scheduleFrame
()
{
fun
scheduleFrame
()
{
needFrame
.
complete
(
Unit
)
if
(!
frameScheduled
)
{
frameScheduled
=
true
frameChannel
.
offer
(
Unit
)
}
}
}
}
}
\ No newline at end of file
skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt
View file @
544b4b76
package
org.jetbrains.skiko.redrawer
package
org.jetbrains.skiko.redrawer
import
kotlinx.coroutines.
Dispatchers
import
kotlinx.coroutines.
*
import
kotlinx.coroutines.swing.Swing
import
kotlinx.coroutines.swing.Swing
import
kotlinx.coroutines.withContext
import
org.jetbrains.skija.BackendRenderTarget
import
org.jetbrains.skija.BackendRenderTarget
import
org.jetbrains.skija.DirectContext
import
org.jetbrains.skija.DirectContext
import
org.jetbrains.skiko.FrameDispatcher
import
org.jetbrains.skiko.FrameDispatcher
...
@@ -19,6 +18,7 @@ internal class MetalRedrawer(
...
@@ -19,6 +18,7 @@ internal class MetalRedrawer(
private
var
isDisposed
=
false
private
var
isDisposed
=
false
private
var
disposeLock
=
Any
()
private
var
disposeLock
=
Any
()
private
val
device
=
layer
.
backedLayer
.
useDrawingSurfacePlatformInfo
(
::
createMetalDevice
)
private
val
device
=
layer
.
backedLayer
.
useDrawingSurfacePlatformInfo
(
::
createMetalDevice
)
private
val
windowHandle
=
layer
.
windowHandle
private
val
frameDispatcher
=
FrameDispatcher
(
Dispatchers
.
Swing
)
{
private
val
frameDispatcher
=
FrameDispatcher
(
Dispatchers
.
Swing
)
{
update
(
System
.
nanoTime
())
update
(
System
.
nanoTime
())
...
@@ -38,9 +38,9 @@ internal class MetalRedrawer(
...
@@ -38,9 +38,9 @@ internal class MetalRedrawer(
override
fun
redrawImmediately
()
{
override
fun
redrawImmediately
()
{
check
(!
isDisposed
)
check
(!
isDisposed
)
// TODO
now we wait until previous layer.draw
is finished. it ends only on the next vsync.
// TODO
: now we wait until previous `layer.draw`
is finished. it ends only on the next vsync.
//
because of that we lose one frame on resize and can theoretically see very small white bars on the sides of the window
//
Because of that we lose one frame on resize and can theoretically see very small white bars on the sides
// to avoid this we should be able to draw in two modes: with vsync and without.
//
of the window
to avoid this we should be able to draw in two modes: with vsync and without.
frameDispatcher
.
scheduleFrame
()
frameDispatcher
.
scheduleFrame
()
}
}
...
@@ -68,6 +68,11 @@ internal class MetalRedrawer(
...
@@ -68,6 +68,11 @@ internal class MetalRedrawer(
}
}
}
}
}
}
// When window is not visible - it doesn't make sense to redraw fast to avoid battery drain.
// In theory, we could be more precise, and just suspend rendering in
// `NSWindowDidChangeOcclusionStateNotification`, but current approach seems to work as well in practise.
if
(
isOccluded
(
windowHandle
))
delay
(
300
)
}
}
}
}
...
@@ -100,4 +105,5 @@ internal class MetalRedrawer(
...
@@ -100,4 +105,5 @@ internal class MetalRedrawer(
private
external
fun
finishFrame
(
device
:
Long
)
private
external
fun
finishFrame
(
device
:
Long
)
private
external
fun
resizeLayers
(
device
:
Long
,
x
:
Int
,
y
:
Int
,
width
:
Int
,
height
:
Int
)
private
external
fun
resizeLayers
(
device
:
Long
,
x
:
Int
,
y
:
Int
,
width
:
Int
,
height
:
Int
)
private
external
fun
setContentScale
(
device
:
Long
,
contentScale
:
Float
)
private
external
fun
setContentScale
(
device
:
Long
,
contentScale
:
Float
)
private
external
fun
isOccluded
(
window
:
Long
):
Boolean
}
}
skiko/src/jvmMain/objectiveC/macos/MetalRedrawer.mm
View file @
544b4b76
...
@@ -241,5 +241,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_disposeDe
...
@@ -241,5 +241,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_disposeDe
[device release];
[device release];
}
}
JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_isOccluded(
JNIEnv *env, jobject redrawer, jlong windowPtr) {
NSWindow* window = (NSWindow*)windowPtr;
return ([window occlusionState] & NSWindowOcclusionStateVisible) == 0;
}
} // extern C
} // extern C
#endif
#endif
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