Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Linux OpenGL renderer to another thread. #351

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,40 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.*
import org.jetbrains.skiko.context.OpenGLContextHandler
import java.util.concurrent.Executors

internal class LinuxOpenGLRedrawer(
private val layer: SkiaLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
private var isDisposed = false

private var context = 0L
private val swapInterval = if (properties.isVsyncEnabled) 1 else 0
private val defaultSwapInterval = if (properties.isVsyncEnabled) 1 else 0
private var swapInterval = -1

private fun LinuxDrawingSurface.setSwapIntervalFast(swapInterval: Int) {
if ([email protected] != swapInterval) {
setSwapInterval(swapInterval)
}
}

init {
layer.backedLayer.lockLinuxDrawingSurface {
context = it.createContext(layer.transparency)
if (context == 0L) {
throw RenderException("Cannot create Linux GL context")
}
it.makeCurrent(context)
if (!isVideoCardSupported(layer.renderApi)) {
throw RenderException("Cannot create Linux GL context")
runBlocking {
inDrawThread {
context = it.createContext(layer.transparency)
if (context == 0L) {
throw RenderException("Cannot create Linux GL context")
}
it.makeCurrent(context)
if (!isVideoCardSupported(layer.renderApi)) {
throw RenderException("Cannot create Linux GL context")
}
it.setSwapIntervalFast(defaultSwapInterval)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems here setSwapInterval shall be used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setSwapIntervalFast implemented in a way, that it has uninitialized value -1, so setSwapIntervalFast will work, and it is more consistent with the other code than simple setSwapInterval

}
it.setSwapInterval(swapInterval)
}
}

private val frameJob = Job()
@Volatile
private var frameLimit = 0.0
Expand All @@ -48,13 +60,16 @@ internal class LinuxOpenGLRedrawer(

override fun dispose() {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" }
layer.backedLayer.lockLinuxDrawingSurface {
// makeCurrent is mandatory to destroy context, otherwise, OpenGL will destroy wrong context (from another window).
// see the official example: https://www.khronos.org/opengl/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX)
it.makeCurrent(context)
// TODO remove in https://github.com/JetBrains/skiko/pull/300
(layer.contextHandler as OpenGLContextHandler).disposeInOpenGLContext()
it.destroyContext(context)

runBlocking {
inDrawThread {
// makeCurrent is mandatory to destroy context, otherwise, OpenGL will destroy wrong context (from another window).
// see the official example: https://www.khronos.org/opengl/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX)
it.makeCurrent(context)
// TODO remove in https://github.com/JetBrains/skiko/pull/300
(layer.contextHandler as OpenGLContextHandler).disposeInOpenGLContext()
it.destroyContext(context)
}
}
runBlocking {
frameJob.cancelAndJoin()
Expand All @@ -68,23 +83,38 @@ internal class LinuxOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}

override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface {
override fun redrawImmediately() {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" }
update(System.nanoTime())
it.makeCurrent(context)
draw()
it.setSwapInterval(0)
it.swapBuffers()
OpenGLApi.instance.glFinish()
it.setSwapInterval(swapInterval)
runBlocking {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we'll block AWT thread here? Shall we?

Copy link
Collaborator Author

@igordmn igordmn Nov 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the semantic of this method is to draw the content in a blocking way, but without vsync synchronization

draw(withVsync = false)
}
}

private fun update(nanoTime: Long) {
layer.update(nanoTime)
}

private fun draw() {
layer.inDrawScope(layer::draw)
private suspend fun draw(withVsync: Boolean) {
layer.inDrawScope {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shalln't we swap the order and do

inDrawThread {
   layer.inDrawScope {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, inDrawScope should be called in Swing thread, as it contain fallback logic

inDrawThread {
it.makeCurrent(context)
layer.draw()
it.setSwapIntervalFast(if (withVsync) defaultSwapInterval else 0)
it.swapBuffers()
OpenGLApi.instance.glFinish()
}
}
}

private suspend fun inDrawThread(body: (LinuxDrawingSurface) -> Unit) {
withContext(drawDispatcher) {
if (!isDisposed) {
layer.backedLayer.lockLinuxDrawingSurface {
body(it)
}
}
}
}

companion object {
Expand All @@ -95,6 +125,12 @@ internal class LinuxOpenGLRedrawer(
.filterNot(LinuxOpenGLRedrawer::isDisposed)
.filter { it.layer.isShowing }

private val drawDispatcher = Executors.newSingleThreadExecutor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So all drawing happens in this thread, no matter how many open windows we got?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe write comment, and also on how we ensure that no races on drawing context happen?

Thread(it).apply {
isDaemon = true
}
}.asCoroutineDispatcher()

private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
toRedrawCopy.addAll(toRedraw)
toRedraw.clear()
Expand All @@ -105,46 +141,25 @@ internal class LinuxOpenGLRedrawer(
val nanoTime = System.nanoTime()

for (redrawer in toRedrawVisible) {
try {
redrawer.update(nanoTime)
} catch (e: CancellationException) {
// continue
}
redrawer.update(nanoTime)
}

val drawingSurfaces = toRedrawVisible.associateWith { lockLinuxDrawingSurface(it.layer.backedLayer) }
try {
for (redrawer in toRedrawVisible) {
drawingSurfaces[redrawer]!!.makeCurrent(redrawer.context)
redrawer.draw()
}
// TODO(demin): How can we properly synchronize multiple windows with multiple displays?
// I checked, and without vsync there is no tearing. Is it only my case (Ubuntu, Nvidia, X11),
// or Ubuntu write all the screen content into an intermediate buffer? If so, then we probably only
// need a frame limiter.

// TODO(demin): How can we properly synchronize multiple windows with multiple displays?
// I checked, and without vsync there is no tearing. Is it only my case (Ubuntu, Nvidia, X11),
// or Ubuntu write all the screen content into an intermediate buffer? If so, then we probably only
// need a frame limiter.

// Synchronize with vsync only for the fastest monitor, for the single window.
// Otherwise, 5 windows will wait for vsync 5 times.
val vsyncRedrawer = toRedrawVisible
.filter { it.properties.isVsyncEnabled }
.maxByOrNull { it.frameLimit }

for (redrawer in toRedrawVisible.filter { it != vsyncRedrawer }) {
drawingSurfaces[redrawer]!!.makeCurrent(redrawer.context)
drawingSurfaces[redrawer]!!.setSwapInterval(0)
drawingSurfaces[redrawer]!!.swapBuffers()
OpenGLApi.instance.glFinish()
}
// Synchronize with vsync only for the fastest monitor, for the single window.
// Otherwise, 5 windows will wait for vsync 5 times.
val vsyncRedrawer = toRedrawVisible
.filter { it.properties.isVsyncEnabled }
.maxByOrNull { it.frameLimit }

if (vsyncRedrawer != null) {
drawingSurfaces[vsyncRedrawer]!!.makeCurrent(vsyncRedrawer.context)
drawingSurfaces[vsyncRedrawer]!!.setSwapInterval(1)
drawingSurfaces[vsyncRedrawer]!!.swapBuffers()
OpenGLApi.instance.glFinish()
}
} finally {
drawingSurfaces.values.forEach(::unlockLinuxDrawingSurface)
for (redrawer in toRedrawVisible.filter { it != vsyncRedrawer }) {
redrawer.draw(withVsync = false)
}
if (vsyncRedrawer?.isDisposed != true && vsyncRedrawer?.layer?.isShowing == true) {
vsyncRedrawer.draw(withVsync = true)
}

// Without clearing we will have a memory leak
Expand Down