Unverified Commit ac7e0091 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Emulate vsync tick on Software renderer and Linux with unsupported vsync (#148)

* Emulate vsync tick on Software renderer and Linux with unsupported vsync

Otherwise there is no delay between frames and we will have high CPU usage

* Discussions

* Fix frame limit on Linux

* Fix tests

* Refactor. Move lockLinuxDrawingSurface

* FrameLimiter. Proper cancel

* Make FrameLimiter local for each window

* Dynamically load Xrandr.

* Search paths

* Fix loadXrandr

* libxrandr-dev for docker
Co-authored-by: 's avatarNikolay Igotti <igotti@gmail.com>
parent 2d070a54
......@@ -58,7 +58,7 @@ jobs:
sudo apt-get install gcc-9 g++-9 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 60 --slave /usr/bin/g++ g++ /usr/bin/g++-9
sudo update-alternatives --config gcc
sudo apt-get install ninja-build fontconfig libfontconfig1-dev libglu1-mesa-dev zip -y
sudo apt-get install ninja-build fontconfig libfontconfig1-dev libglu1-mesa-dev libxrandr-dev zip -y
./gradlew jvmTest
./gradlew publishToMavenLocal
windows:
......
......@@ -36,8 +36,8 @@ if (project.hasProperty("skiko.version")) {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0")
implementation("org.jetbrains.skiko:skiko-jvm-runtime-$target:$version")
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
......
......@@ -17,7 +17,7 @@ ENV PATH=/usr/lib/binutils-2.26/bin:$PATH
ENV DEPOT_TOOLS=/usr/depot_tools
ENV PATH=$DEPOT_TOOLS:$PATH
RUN apt-get install git python wget -y && \
apt-get install fontconfig libfontconfig1-dev libglu1-mesa-dev curl zip -y && \
apt-get install fontconfig libfontconfig1-dev libglu1-mesa-dev libxrandr-dev curl zip -y && \
git clone 'https://chromium.googlesource.com/chromium/tools/depot_tools.git' $DEPOT_TOOLS
# Install Java
......
......@@ -3,7 +3,7 @@ ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get install binutils build-essential -y
RUN apt-get install software-properties-common -y
RUN apt-get install python git fontconfig libfontconfig1-dev libglu1-mesa-dev curl wget -y
RUN apt-get install python git fontconfig libfontconfig1-dev libglu1-mesa-dev libxrandr-dev curl wget -y
RUN apt-get install openjdk-11-jdk -y
RUN apt-get install clang-11 -y && \
apt-get remove g++ -y && \
......
#include <jawt_md.h>
#include <GL/gl.h>
#include <GL/glx.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xresource.h>
#include <cstdlib>
#include <unistd.h>
#include <stdio.h>
#include "jni_helpers.h"
extern "C"
{
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_AWTLinuxDrawingSurfaceKt_getDisplay(JNIEnv *env, jobject redrawer, jlong platformInfoPtr)
{
JAWT_X11DrawingSurfaceInfo *dsi_x11 = fromJavaPointer<JAWT_X11DrawingSurfaceInfo *>(platformInfoPtr);
Display *display = dsi_x11->display;
return toJavaPointer(display);
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_AWTLinuxDrawingSurfaceKt_getWindow(JNIEnv *env, jobject redrawer, jlong platformInfoPtr)
{
JAWT_X11DrawingSurfaceInfo *dsi_x11 = fromJavaPointer<JAWT_X11DrawingSurfaceInfo *>(platformInfoPtr);
Window window = dsi_x11->drawable;
return toJavaPointer(window);
}
}
#include <jawt_md.h>
#include <GL/gl.h>
#include <GL/glx.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xresource.h>
#include <X11/extensions/Xrandr.h>
#include <cstdlib>
#include <dlfcn.h>
#include <unistd.h>
#include <stdio.h>
#include "jni_helpers.h"
static void* loadXrandr() {
static void* result = nullptr;
if (result != nullptr) return result;
result = dlopen("libXrandr.so", RTLD_LAZY | RTLD_LOCAL);
return result;
}
static XRRScreenResources* XRRGetScreenResourcesCurrentDynamic(Display* display, Window window) {
typedef XRRScreenResources* (*XRRGetScreenResourcesCurrent_t)(Display*, Window);
static XRRGetScreenResourcesCurrent_t func = nullptr;
if (!func) {
void* lib = loadXrandr();
if (!lib) return nullptr;
func = (XRRGetScreenResourcesCurrent_t)dlsym(lib, "XRRGetScreenResourcesCurrent");
}
if (!func) return nullptr;
return func(display, window);
}
static XRRCrtcInfo* XRRGetCrtcInfoDynamic(
Display *display, XRRScreenResources *resources, RRCrtc crtc) {
typedef XRRCrtcInfo* (*XRRGetCrtcInfo_t)(Display*, XRRScreenResources*, RRCrtc);
static XRRGetCrtcInfo_t func = nullptr;
if (!func) {
void* lib = loadXrandr();
if (!lib) return nullptr;
func = (XRRGetCrtcInfo_t)dlsym(lib, "XRRGetCrtcInfo");
}
if (!func) return nullptr;
return func(display, resources, crtc);
}
void XRRFreeCrtcInfoDynamic(XRRCrtcInfo* crtcInfo) {
typedef void (*XRRFreeCrtcInfo_t)(XRRCrtcInfo*);
static XRRFreeCrtcInfo_t func = nullptr;
if (!func) {
void* lib = loadXrandr();
if (!lib) return;
func = (XRRFreeCrtcInfo_t)dlsym(lib, "XRRFreeCrtcInfo");
}
if (!func) return;
func(crtcInfo);
}
void XRRFreeScreenResourcesDynamic(XRRScreenResources *resources) {
typedef void (*XRRFreeScreenResources_t)(XRRScreenResources*);
static XRRFreeScreenResources_t func = nullptr;
if (!func) {
void* lib = loadXrandr();
if (!lib) return;
func = (XRRFreeScreenResources_t)dlsym(lib, "XRRFreeScreenResources");
}
if (!func) return;
func(resources);
}
extern "C"
{
JNIEXPORT jdouble JNICALL Java_org_jetbrains_skiko_DisplayKt_getLinuxDisplayRefreshRate(JNIEnv *env, jobject obj, jlong displayPtr, jlong windowPtr)
{
Display *display = fromJavaPointer<Display *>(displayPtr);
Window window = fromJavaPointer<Window>(windowPtr);
XRRScreenResources *screenResources = XRRGetScreenResourcesCurrentDynamic(display, window);
RRMode activeModeId = 0;
if (!screenResources) return 60.0;
for (int i = 0; i < screenResources->ncrtc; ++i) {
XRRCrtcInfo *info = XRRGetCrtcInfoDynamic(display, screenResources, screenResources->crtcs[i]);
if (info->mode != None) {
activeModeId = info->mode;
}
XRRFreeCrtcInfoDynamic(info);
}
double rate = 0;
for (int i = 0; i < screenResources->nmode; ++i) {
XRRModeInfo info = screenResources->modes[i];
if (info.id == activeModeId) {
rate = (double) info.dotClock / ((double) info.hTotal * (double) info.vTotal);
}
}
XRRFreeScreenResourcesDynamic(screenResources);
return rate;
}
}
......@@ -13,20 +13,6 @@ typedef GLXContext (*glXCreateContextAttribsARBProc)(Display *, GLXFBConfig, GLX
extern "C"
{
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxOpenGLRedrawerKt_getDisplay(JNIEnv *env, jobject redrawer, jlong platformInfoPtr)
{
JAWT_X11DrawingSurfaceInfo *dsi_x11 = fromJavaPointer<JAWT_X11DrawingSurfaceInfo *>(platformInfoPtr);
Display *display = dsi_x11->display;
return toJavaPointer(display);
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxOpenGLRedrawerKt_getWindow(JNIEnv *env, jobject redrawer, jlong platformInfoPtr)
{
JAWT_X11DrawingSurfaceInfo *dsi_x11 = fromJavaPointer<JAWT_X11DrawingSurfaceInfo *>(platformInfoPtr);
Window window = dsi_x11->drawable;
return toJavaPointer(window);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_LinuxOpenGLRedrawerKt_setSwapInterval(JNIEnv *env, jobject redrawer, jlong displayPtr, jlong windowPtr, jint interval)
{
Display *display = fromJavaPointer<Display *>(displayPtr);
......
package org.jetbrains.skiko
internal inline fun <T> HardwareLayer.lockLinuxDrawingSurface(action: (LinuxDrawingSurface) -> T): T {
val drawingSurface = lockLinuxDrawingSurface(this)
try {
return action(drawingSurface)
} finally {
unlockLinuxDrawingSurface(drawingSurface)
}
}
internal fun lockLinuxDrawingSurface(layer: HardwareLayer): LinuxDrawingSurface {
val drawingSurface = layer.getDrawingSurface()
drawingSurface.lock()
return drawingSurface.getInfo().use {
LinuxDrawingSurface(
drawingSurface,
getDisplay(it.platformInfo),
getWindow(it.platformInfo)
)
}
}
internal fun unlockLinuxDrawingSurface(drawingSurface: LinuxDrawingSurface) {
drawingSurface.common.unlock()
drawingSurface.common.close()
}
internal class LinuxDrawingSurface(
val common: DrawingSurface,
val display: Long,
val window: Long
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LinuxDrawingSurface
if (display != other.display) return false
if (window != other.window) return false
return true
}
override fun hashCode(): Int {
var result = display.hashCode()
result = 31 * result + window.hashCode()
return result
}
}
private external fun getDisplay(platformInfo: Long): Long
private external fun getWindow(platformInfo: Long): Long
package org.jetbrains.skiko
import kotlin.time.ExperimentalTime
internal const val MinMainstreamMonitorRefreshRate = 60.0
@OptIn(ExperimentalTime::class)
internal fun HardwareLayer.getDisplayRefreshRate(): Double {
// We use different method for Linux, because it.displayMode.refreshRate returns always a wrong value: 50 (probably because of the using the old xrandr API)
return if (hostOs == OS.Linux) {
lockLinuxDrawingSurface {
getLinuxDisplayRefreshRate(it.display, it.window)
}
} else {
graphicsConfiguration
.device
.displayMode
.refreshRate
.toDouble()
.coerceAtLeast(MinMainstreamMonitorRefreshRate)
}
}
private external fun getLinuxDisplayRefreshRate(display: Long, window: Long): Double
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.ExperimentalTime
private const val NanosecondsPerMillisecond = 1_000_000L
/**
* HardwareLayer should not dispose native resources while [scope] is active.
*
* So wait for scope cancellation in dispose method:
* ```
* runBlocking {
* frameJob.cancelAndJoin()
* }
* ```
*/
@OptIn(ExperimentalTime::class)
@Suppress("UNUSED_PARAMETER")
internal fun FrameLimiter(
scope: CoroutineScope,
component: HardwareLayer,
onNewFrameLimit: (frameLimit: Double) -> Unit = {}
): FrameLimiter {
val state = object {
@Volatile
var frameLimit = MinMainstreamMonitorRefreshRate
}
val frames = Channel<Unit>(Channel.CONFLATED)
frames.trySend(Unit)
scope.launch {
while (true) {
frames.receive()
// TODO will lockLinuxDrawingSurface inside getDisplayRefreshRate can cause draw lock too?
// it takes 2ms on my machine on Linux (0.01ms on macOs, 0.1ms on Windows)
state.frameLimit = component.getDisplayRefreshRate()
onNewFrameLimit(state.frameLimit)
delay(1000)
}
}
return FrameLimiter(
scope,
frameMillis = {
frames.trySend(Unit)
(1000 / state.frameLimit).toLong()
}
)
}
/**
* Limit the duration of the frames (to avoid high CPU usage) to [frameMillis].
* The actual delay depends on the precision of the system timer
* (Windows has ~15ms precision by default, Linux/macOs ~2ms).
* FrameLimiter will try to delay frames as close as possible to [frameMillis], but not greater
*/
@OptIn(ExperimentalTime::class)
class FrameLimiter(
private val coroutineScope: CoroutineScope,
private val frameMillis: () -> Long,
private val nanoTime: () -> Long = System::nanoTime
) {
private val channel = RendezvousBroadcastChannel<Unit>()
init {
coroutineScope.launch {
while (true) {
channel.sendAll(Unit)
preciseDelay(frameMillis())
}
}
}
private suspend fun preciseDelay(millis: Long) {
val start = nanoTime()
// delay aren't precise, so we should measure what is the actual precision of delay is,
// so we don't wait longer than we need
var actual1msDelay = 1L
while (nanoTime() - start <= millis * NanosecondsPerMillisecond - actual1msDelay) {
val beforeDelay = nanoTime()
delay(1) // TODO do multiple delays instead of the single one consume more energy? Test it
actual1msDelay = maxOf(actual1msDelay, nanoTime() - beforeDelay)
}
}
/**
* Await the next frame, if it is not ready yet (the previous [awaitNextFrame]
* was called less than [frameMillis] ago)
*/
suspend fun awaitNextFrame() {
withContext(coroutineScope.coroutineContext) {
channel.receive()
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.Direct3DRedrawer
import org.jetbrains.skiko.redrawer.LinuxOpenGLRedrawer
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.SoftwareRedrawer
import org.jetbrains.skiko.redrawer.MetalRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.SoftwareRedrawer
import org.jetbrains.skiko.redrawer.WindowsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.Direct3DRedrawer
import org.jetbrains.skiko.redrawer.MetalRedrawer
import java.awt.Component
import java.awt.Window
import javax.swing.SwingUtilities
......@@ -48,7 +48,7 @@ internal val platformOperations: PlatformOperations by lazy {
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = when(renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer)
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
GraphicsApi.METAL -> MetalRedrawer(layer, properties)
else -> MacOsOpenGLRedrawer(layer, properties)
}
......@@ -82,7 +82,7 @@ internal val platformOperations: PlatformOperations by lazy {
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = when(renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer)
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
GraphicsApi.DIRECT3D -> Direct3DRedrawer(layer, properties)
else -> WindowsOpenGLRedrawer(layer, properties)
}
......@@ -117,7 +117,7 @@ internal val platformOperations: PlatformOperations by lazy {
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = when(renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer)
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
else -> LinuxOpenGLRedrawer(layer, properties)
}
}
......
package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import java.util.concurrent.atomic.AtomicReference
/**
* Behaves as Channel<Unit>(Channel.RENDEZVOUS), but with ability to send value to all current consumers
* (which await on `receive` method)
*/
internal class RendezvousBroadcastChannel<T> {
private val onRequest = Channel<Unit>(Channel.CONFLATED)
private val onResult = AtomicReference(CompletableDeferred<T>())
/**
* Send value to all current consumers which await value on `receive` method, or await for the first one
*/
suspend fun sendAll(value: T) {
onRequest.receive()
onResult.getAndSet(CompletableDeferred()).complete(value)
}
/**
* Wait when the producer will send a value and return it.
*/
suspend fun receive(): T {
onRequest.trySend(Unit)
return onResult.get().await()
}
}
\ No newline at end of file
package org.jetbrains.skiko
class SkiaLayerProperties(
val isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled
val isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled,
val isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled
)
\ No newline at end of file
package org.jetbrains.skiko
// TODO maybe we can get rid of global properties, and pass SkiaLayerProperties to Window -> ComposeWindow -> SkiaLayer
@Suppress("SameParameterValue")
internal object SkikoProperties {
val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true)
/**
* If vsync is enabled, but platform can't support it (Software renderer, Linux with uninstalled drivers),
* we enable frame limit by the display refresh rate.
*/
val vsyncFramelimitFallbackEnabled: Boolean by property(
"skiko.vsync.framelimit.fallback.enabled", default = true
)
val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false)
val fpsPeriodSeconds: Double by property("skiko.fps.periodSeconds", default = 2.0)
......
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.DrawingSurface
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.getDrawingSurface
import org.jetbrains.skiko.isVideoCardSupported
import org.jetbrains.skiko.*
internal class LinuxOpenGLRedrawer(
private val layer: SkiaLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
private val context = layer.backedLayer.lockDrawingSurface {
val result = it.createContext()
it.makeCurrent(result)
if (result == 0L || !isVideoCardSupported(layer.renderApi)) {
private var isDisposed = false
private var context = 0L
private val swapInterval = if (properties.isVsyncEnabled) 1 else 0
init {
layer.backedLayer.lockLinuxDrawingSurface {
context = it.createContext()
it.makeCurrent(context)
if (context == 0L || !isVideoCardSupported(layer.renderApi)) {
throw IllegalArgumentException("Cannot create Linux GL context")
}
result
it.setSwapInterval(swapInterval)
}
}
private val frameJob = Job()
@Volatile
private var frameLimit = 0.0
private val frameLimiter = FrameLimiter(
CoroutineScope(Dispatchers.IO + frameJob),
layer.backedLayer,
onNewFrameLimit = { frameLimit = it }
)
private suspend fun limitFramesIfNeeded() {
// Some Linuxes don't turn vsync on, so we apply additional frame limit (which should be no longer than enabled vsync)
if (properties.isVsyncEnabled) {
try {
frameLimiter.awaitNextFrame()
} catch (e: CancellationException) {
// ignore
}
}
}
private var isDisposed = false
override fun dispose() {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" }
layer.backedLayer.lockDrawingSurface {
layer.backedLayer.lockLinuxDrawingSurface {
it.destroyContext(context)
}
runBlocking {
frameJob.cancelAndJoin()
}
isDisposed = true
}
......@@ -44,7 +63,7 @@ internal class LinuxOpenGLRedrawer(
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() = layer.backedLayer.lockDrawingSurface {
override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" }
update(System.nanoTime())
it.makeCurrent(context)
......@@ -52,6 +71,7 @@ internal class LinuxOpenGLRedrawer(
it.setSwapInterval(0)
it.swapBuffers()
OpenGLApi.instance.glFinish()
it.setSwapInterval(swapInterval)
}
private fun update(nanoTime: Long) {
......@@ -70,6 +90,9 @@ internal class LinuxOpenGLRedrawer(
private val toRedrawAlive = toRedrawCopy.asSequence().filterNot(LinuxOpenGLRedrawer::isDisposed)
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
// we should wait for the window with the maximum frame limit to avoid bottleneck when there is a window on a slower monitor
toRedrawAlive.maxByOrNull { it.frameLimit }?.limitFramesIfNeeded()
toRedrawCopy.clear()
toRedrawCopy.addAll(toRedraw)
toRedraw.clear()
......@@ -84,68 +107,36 @@ internal class LinuxOpenGLRedrawer(
}
}
val isVsyncEnabled = toRedrawAlive.all { it.properties.isVsyncEnabled }
val drawingSurfaces = toRedrawAlive.map { lockDrawingSurface(it.layer.backedLayer) }.toList()
val drawingSurfaces = toRedrawAlive.associateWith { lockLinuxDrawingSurface(it.layer.backedLayer) }
try {
toRedrawAlive.forEachIndexed { index, redrawer ->
drawingSurfaces[index].makeCurrent(redrawer.context)
toRedrawAlive.forEach { redrawer ->
drawingSurfaces[redrawer]!!.makeCurrent(redrawer.context)
redrawer.draw()
}
toRedrawAlive.forEachIndexed { index, _ ->
// it is ok to set swap interval every frame, there is no performance overhead
drawingSurfaces[index].setSwapInterval(if (isVsyncEnabled) 1 else 0)
drawingSurfaces[index].swapBuffers()
// TODO(demin) it seems now vsync doesn't work as expected with two windows (we have fps = refreshRate / windowCount)
// perhaps we should create frameDispatcher for each display.
// Don't know what happened, but on 620547a commit everything was okay. maybe something changed in the code, maybe my system changed
toRedrawAlive.forEach { redrawer ->
drawingSurfaces[redrawer]!!.swapBuffers()
}
toRedrawAlive.forEachIndexed { index, redrawer ->
drawingSurfaces[index].makeCurrent(redrawer.context)
toRedrawAlive.forEach { redrawer ->
drawingSurfaces[redrawer]!!.makeCurrent(redrawer.context)
OpenGLApi.instance.glFinish()
}
} finally {
drawingSurfaces.forEach(::unlockDrawingSurface)
drawingSurfaces.values.forEach(::unlockLinuxDrawingSurface)
}
}
}
}
private inline fun <T> HardwareLayer.lockDrawingSurface(action: (LinuxDrawingSurface) -> T): T {
val drawingSurface = lockDrawingSurface(this)
try {
return action(drawingSurface)
} finally {
unlockDrawingSurface(drawingSurface)
}
}
private fun lockDrawingSurface(layer: HardwareLayer): LinuxDrawingSurface {
val drawingSurface = layer.getDrawingSurface()
drawingSurface.lock()
return drawingSurface.getInfo().use {
LinuxDrawingSurface(drawingSurface, getDisplay(it.platformInfo), getWindow(it.platformInfo))
}
}
private fun unlockDrawingSurface(drawingSurface: LinuxDrawingSurface) {
drawingSurface.common.unlock()
drawingSurface.common.close()
}
private class LinuxDrawingSurface(
val common: DrawingSurface,
val display: Long,
val window: Long
) {
fun createContext() = createContext(display)
fun destroyContext(context: Long) = destroyContext(display, context)
fun makeCurrent(context: Long) = makeCurrent(display, window, context)
fun swapBuffers() = swapBuffers(display, window)
fun setSwapInterval(interval: Int) = setSwapInterval(display, window, interval)
}
private external fun getDisplay(platformInfo: Long): Long
private external fun getWindow(platformInfo: Long): Long
private fun LinuxDrawingSurface.createContext() = createContext(display)
private fun LinuxDrawingSurface.destroyContext(context: Long) = destroyContext(display, context)
private fun LinuxDrawingSurface.makeCurrent(context: Long) = makeCurrent(display, window, context)
private fun LinuxDrawingSurface.swapBuffers() = swapBuffers(display, window)
private fun LinuxDrawingSurface.setSwapInterval(interval: Int) = setSwapInterval(display, window, interval)
private external fun makeCurrent(display: Long, window: Long, context: Long)
private external fun createContext(display: Long): Long
......
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.FrameLimiter
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
internal class SoftwareRedrawer(
private val layer: SkiaLayer
private val layer: SkiaLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
private val frameJob = Job()
private val frameLimiter = FrameLimiter(CoroutineScope(Dispatchers.IO + frameJob), layer.backedLayer)
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
if (properties.isVsyncEnabled && properties.isVsyncFramelimitFallbackEnabled) {
frameLimiter.awaitNextFrame()
}
layer.update(System.nanoTime())
if (layer.prepareDrawContext()) {
layer.draw()
......@@ -18,6 +27,9 @@ internal class SoftwareRedrawer(
override fun dispose() {
frameDispatcher.cancel()
runBlocking {
frameJob.cancelAndJoin()
}
}
override fun needRedraw() {
......
package org.jetbrains.skiko
import kotlinx.coroutines.*
import kotlinx.coroutines.test.DelayController
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.coroutines.CoroutineContext
import kotlin.math.ceil
@OptIn(ExperimentalCoroutinesApi::class)
class FrameLimiterTest {
private val frameCount = 8
private val frames = 0 until frameCount
@Test
fun `limit 10ms, render 0ms`() {
fun frameTicksOf(delayPrecisionMillis: Long) =
frameTicksOf(frameLimitMillis = 10, delayPrecisionMillis, frameRenderMillis = 0)
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 1))
assertEquals(frames.map { it * 9 }, frameTicksOf(delayPrecisionMillis = 3))
assertEquals(frames.map { it * 7 }, frameTicksOf(delayPrecisionMillis = 7))
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 10))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 11))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 21))
}
@Test
fun `limit 10ms, render 1ms`() {
fun frameTicksOf(delayPrecisionMillis: Long) =
frameTicksOf(frameLimitMillis = 10, delayPrecisionMillis, frameRenderMillis = 1)
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 1))
assertEquals(frames.map { it * 9 }, frameTicksOf(delayPrecisionMillis = 3))
assertEquals(frames.map { it * 7 }, frameTicksOf(delayPrecisionMillis = 7))
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 10))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 11))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 21))
}
@Test
fun `limit 10ms, render 9ms`() {
fun frameTicksOf(delayPrecisionMillis: Long) =
frameTicksOf(frameLimitMillis = 10, delayPrecisionMillis, frameRenderMillis = 9)
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 1))
assertEquals(frames.map { it * 9 }, frameTicksOf(delayPrecisionMillis = 3))
assertEquals(frames.map { it * 9 }, frameTicksOf(delayPrecisionMillis = 7))
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 10))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 11))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 21))
}
@Test
fun `limit 10ms, render 10ms`() {
fun frameTicksOf(delayPrecisionMillis: Long) =
frameTicksOf(frameLimitMillis = 10, delayPrecisionMillis, frameRenderMillis = 10)
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 1))
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 3))
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 7))
assertEquals(frames.map { it * 10 }, frameTicksOf(delayPrecisionMillis = 10))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 11))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 21))
}
@Test
fun `limit 10ms, render 11ms`() {
fun frameTicksOf(delayPrecisionMillis: Long) =
frameTicksOf(frameLimitMillis = 10, delayPrecisionMillis, frameRenderMillis = 11)
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 1))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 3))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 7))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 10))
assertEquals(frames.map { it * 11 }, frameTicksOf(delayPrecisionMillis = 11))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 21))
}
@Test
fun `limit 10ms, render 21ms`() {
fun frameTicksOf(delayPrecisionMillis: Long) =
frameTicksOf(frameLimitMillis = 10, delayPrecisionMillis, frameRenderMillis = 21)
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 1))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 3))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 7))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 10))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 11))
assertEquals(frames.map { it * 21 }, frameTicksOf(delayPrecisionMillis = 21))
}
@Suppress("SameParameterValue")
private fun frameTicksOf(
frameLimitMillis: Long,
delayPrecisionMillis: Long,
frameRenderMillis: Long,
): List<Int> {
val ticks = mutableListOf<Int>()
frameLimiterTest(
frameLimitMillis,
delayPrecisionMillis
) { limiter ->
repeat(frameCount) {
limiter.awaitNextFrame()
ticks.add(currentTime.toInt())
advanceTimeBy(frameRenderMillis)
}
}
return ticks
}
@Test
fun `multiple awaiters`() {
val ticks1 = mutableListOf<Int>()
val ticks2 = mutableListOf<Int>()
val ticks3 = mutableListOf<Int>()
frameLimiterTest(
frameLimitMillis = 10,
delayPrecisionMillis = 1
) { limiter ->
launch {
repeat(frameCount) {
limiter.awaitNextFrame()
ticks1.add(currentTime.toInt())
advanceTimeBy(3)
}
}
launch {
repeat(frameCount) {
limiter.awaitNextFrame()
ticks2.add(currentTime.toInt())
advanceTimeBy(1)
}
}
launch {
repeat(frameCount) {
limiter.awaitNextFrame()
ticks3.add(currentTime.toInt())
advanceTimeBy(1)
}
}
}
assertEquals(frames.map { it * 10 }, ticks1)
assertEquals(frames.map { it * 10 }, ticks2)
assertEquals(frames.map { it * 10 }, ticks3)
}
@Test
fun `cancel scope before awaitNextFrame`() = runBlockingTest {
pauseDispatcher()
val scope = CoroutineScope(coroutineContext + Job())
val frameLimiter = FrameLimiter(scope, { 10 }, nanoTime = { currentTime * 1_000_000 })
scope.cancel()
assertThrow<kotlin.coroutines.cancellation.CancellationException> {
frameLimiter.awaitNextFrame()
}
}
@Test
fun `cancel scope after awaitNextFrame`() = runBlockingTest {
pauseDispatcher()
val scope = CoroutineScope(coroutineContext + Job())
val frameLimiter = FrameLimiter(scope, { 10 }, nanoTime = { currentTime * 1_000_000 })
launch {
scope.cancel()
}
assertThrow<kotlin.coroutines.cancellation.CancellationException> {
frameLimiter.awaitNextFrame()
}
}
private inline fun <reified T : Throwable> assertThrow(body: () -> Unit) {
var actualE: Throwable? = null
try {
body()
} catch (e: Throwable) {
actualE = e
}
assertTrue("Actual ${actualE?.javaClass}, expected ${T::class.java}", actualE is T)
}
private fun frameLimiterTest(
frameLimitMillis: Long,
delayPrecisionMillis: Long,
block: suspend TestCoroutineScope.(FrameLimiter) -> Unit
) {
runFrameTest(
delayPrecisionMillis = delayPrecisionMillis
) {
val scope = CoroutineScope(coroutineContext + Job())
val limiter = FrameLimiter(
this,
frameMillis = { frameLimitMillis },
nanoTime = { currentTime * 1_000_000 }
)
block(limiter)
scope.cancel()
}
}
private fun runFrameTest(
delayPrecisionMillis: Long,
block: suspend TestCoroutineScope.() -> Unit
) = runBlockingTest{
val dispatcher = NonpreciseTestCoroutineDispatcher(delayPrecisionMillis)
dispatcher.pauseDispatcher()
val scope = TestCoroutineScope(dispatcher)
scope.launch {
scope.block()
}
dispatcher.advanceUntilIdle()
}
@OptIn(InternalCoroutinesApi::class)
private class NonpreciseTestCoroutineDispatcher(
private val delayPrecisionMillis: Long,
private val original: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : CoroutineDispatcher(), Delay, DelayController by original {
override fun dispatch(context: CoroutineContext, block: Runnable) {
original.dispatch(context, block)
}
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val delay = ceil(timeMillis.toDouble() / delayPrecisionMillis).toInt() * delayPrecisionMillis
original.scheduleResumeAfterDelay(delay, continuation)
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.random.Random
class RendezvousBroadcastChannelTest {
@Test(timeout = 5000)
fun `receive, send`() {
var actualValue = -1
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
launch {
actualValue = channel.receive()
}
launch {
channel.sendAll(1)
}
}
assertEquals(1, actualValue)
}
@Test(timeout = 5000)
fun `send, receive`() {
var actualValue = -1
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
launch {
channel.sendAll(1)
}
launch {
actualValue = channel.receive()
}
}
assertEquals(1, actualValue)
}
@Test(timeout = 5000)
fun `send when there is multiple receivers`() {
val actualValues = mutableListOf<Int>()
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
repeat(5) {
launch {
actualValues.add(channel.receive())
}
}
launch {
channel.sendAll(1)
}
}
assertEquals(listOf(1, 1, 1, 1, 1), actualValues)
}
@Test
fun `first send should not end if there is no received value`() {
var isExceptionThrown = false
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
try {
withTimeout(1000) {
channel.sendAll(1)
}
} catch (e: TimeoutCancellationException) {
isExceptionThrown = true
}
}
assertEquals(true, isExceptionThrown)
}
@Test
fun `second send should not end if there is no second received value`() {
var isExceptionThrown = false
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
launch {
channel.sendAll(1)
try {
withTimeout(1000) {
channel.sendAll(1)
}
} catch (e: TimeoutCancellationException) {
isExceptionThrown = true
}
}
launch {
channel.receive()
}
}
assertEquals(true, isExceptionThrown)
}
@Test
fun `first receive should not end if there is no received value`() {
var isExceptionThrown = false
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
try {
withTimeout(1000) {
channel.receive()
}
} catch (e: TimeoutCancellationException) {
isExceptionThrown = true
}
}
assertEquals(true, isExceptionThrown)
}
@Test
fun `second receive should not end if there is no second received value`() {
var isExceptionThrown = false
val channel = RendezvousBroadcastChannel<Int>()
runBlocking {
launch {
channel.receive()
try {
withTimeout(1000) {
channel.receive()
}
} catch (e: TimeoutCancellationException) {
isExceptionThrown = true
}
}
launch {
channel.sendAll(1)
}
}
assertEquals(true, isExceptionThrown)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `produce values, consume from multiple coroutines`() = runBlockingTest {
pauseDispatcher()
val frames1 = mutableListOf<Int>()
val frames2 = mutableListOf<Int>()
val frames3 = mutableListOf<Int>()
val channel = RendezvousBroadcastChannel<Int>()
val produceJob = launch {
for (i in 0 until 1000) {
channel.sendAll(i)
delay(10)
}
}
val random = Random(5435)
launch {
repeat(1000) {
frames1.add(channel.receive())
delay(random.nextLong(10L))
}
}
launch {
repeat(1000) {
frames2.add(channel.receive())
delay(random.nextLong(10L))
}
}
launch {
repeat(1000) {
frames3.add(channel.receive())
delay(random.nextLong(10L))
}
}
advanceUntilIdle()
produceJob.cancel()
assertEquals((0 until 1000).toList(), frames1)
assertEquals((0 until 1000).toList(), frames2)
assertEquals((0 until 1000).toList(), frames3)
}
}
\ No newline at end of file
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