Unverified Commit 87e0a74f authored by Igor Demin's avatar Igor Demin Committed by GitHub

Merge pull request #44 from JetBrains/vsync

Support vsync, refactor rendering, rendering tests
parents 1733eea7 52efd986
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="test" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/SkijaInjectSample" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="test" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="false" tasks="publishToMavenLocal" externalProjectPath="$PROJECT_DIR$/skiko" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>
\ No newline at end of file
......@@ -44,6 +44,7 @@ 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.skiko:skiko-jvm-runtime-$target:$version"
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit'
......@@ -52,3 +53,18 @@ dependencies {
application {
mainClassName = 'SkijaInjectSample.AppKt'
}
run {
systemProperty("skiko.fps.enabled", "true")
}
test {
systemProperty("skiko.test.screenshots.dir", new File(project.projectDir, "src/test/screenshots").absolutePath)
// Tests should be deterministic, so disable scaling.
// On MacOs we need the actual scale, otherwise we will have aliased screenshots because of scaling.
if (System.getProperty("os.name") != "Mac OS X") {
systemProperty("sun.java2d.dpiaware", "false")
systemProperty("sun.java2d.uiScale", "1")
}
}
\ No newline at end of file
package SkijaInjectSample
import org.jetbrains.skiko.SkiaWindow
import java.awt.event.MouseEvent
import javax.swing.WindowConstants
import javax.swing.event.MouseInputAdapter
import org.jetbrains.skija.*
import org.jetbrains.skiko.SkiaRenderer
import java.awt.event.MouseMotionAdapter
import kotlin.math.cos
import kotlin.math.sin
import org.jetbrains.skija.paragraph.FontCollection
import org.jetbrains.skija.paragraph.ParagraphBuilder
import org.jetbrains.skija.paragraph.ParagraphStyle
import org.jetbrains.skija.paragraph.TextStyle
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.KeyEvent
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaRenderer
import org.jetbrains.skiko.SkiaWindow
import java.awt.Toolkit
import javax.swing.JFrame
import javax.swing.JMenu
import javax.swing.JMenuBar
import javax.swing.JMenuItem
import javax.swing.JOptionPane
import javax.swing.KeyStroke
import java.awt.event.*
import javax.swing.*
import kotlin.math.cos
import kotlin.math.sin
fun main(args: Array<String>) {
createWindow("First window")
repeat(1) {
createWindow("window $it")
}
}
fun createWindow(title: String) {
fun createWindow(title: String) = SwingUtilities.invokeLater {
var mouseX = 0
var mouseY = 0
val window = SkiaWindow()
window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.title = title
// Create menu.
......@@ -68,15 +59,14 @@ fun createWindow(title: String) {
val state = State()
state.text = title
window.layer.renderer = Renderer {
renderer, w, h -> displayScene(renderer, w, h, mouseX, mouseY, state)
window.layer.renderer = Renderer(window.layer) {
renderer, w, h, nanoTime -> displayScene(renderer, w, h, nanoTime, mouseX, mouseY, state)
}
window.layer.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(event: MouseEvent) {
mouseX = event.x
mouseY = event.y
window.display()
}
})
......@@ -85,7 +75,10 @@ fun createWindow(title: String) {
window.setVisible(true)
}
class Renderer(val displayScene: (Renderer, Int, Int) -> Unit): SkiaRenderer {
class Renderer(
val layer: SkiaLayer,
val displayScene: (Renderer, Int, Int, Long) -> Unit
): SkiaRenderer {
val typeface = Typeface.makeFromFile("fonts/JetBrainsMono-Regular.ttf")
val font = Font(typeface, 40f)
val paint = Paint().apply {
......@@ -96,18 +89,12 @@ class Renderer(val displayScene: (Renderer, Int, Int) -> Unit): SkiaRenderer {
var canvas: Canvas? = null
override fun onInit() {
}
override fun onDispose() {
}
override fun onReshape(width: Int, height: Int) {
}
override fun onRender(canvas: Canvas, width: Int, height: Int) {
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
this.canvas = canvas
displayScene(this, width, height)
val contentScale = layer.contentScale
canvas.scale(contentScale, contentScale)
displayScene(this, (width / contentScale).toInt(), (height / contentScale).toInt(), nanoTime)
layer.needRedraw()
}
}
......@@ -116,7 +103,10 @@ class State {
var text: String = "Hello Skija"
}
fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: Int, state: State) {
private val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.getDefault())
fun displayScene(renderer: Renderer, width: Int, height: Int, nanoTime: Long, xpos: Int, ypos: Int, state: State) {
val canvas = renderer.canvas!!
val watchFill = Paint().setColor(0xFFFFFFFF.toInt())
val watchStroke = Paint().setColor(0xFF000000.toInt()).setMode(PaintMode.STROKE).setStrokeWidth(1f)
......@@ -140,7 +130,7 @@ fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: I
)
angle += (2.0 * Math.PI / 12.0).toFloat()
}
val time = System.currentTimeMillis() % 60000 +
val time = (nanoTime / 1E6) % 60000 +
(x.toFloat() / width * 5000).toLong() +
(y.toFloat() / width * 5000).toLong()
......@@ -160,8 +150,6 @@ fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: I
val text = "${state.text} ${state.frame++}!"
canvas.drawString(text, xpos.toFloat(), ypos.toFloat(), renderer.font, renderer.paint)
val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.getDefault())
val style = ParagraphStyle()
val paragraph = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt()))
......
package SkijaInjectSample
import java.awt.BorderLayout
import javax.swing.JFrame
import javax.swing.WindowConstants
fun pureSwing() {
val window = JFrame()
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.setSize(800, 600)
window.contentPane.add(Button("Skiko").apply {
addActionListener {
createWindow("Skiko")
}
}, BorderLayout.NORTH)
window.contentPane.add(Button("Swing").apply {
addActionListener {
pureSwing()
}
}, BorderLayout.WEST)
window.contentPane.add(Button("Swing+Skiko").apply {
addActionListener {
SwingSkia()
}
}, BorderLayout.EAST)
window.setVisible(true)
}
\ No newline at end of file
package org.jetbrains.skiko
package SkijaInjectSample
import org.jetbrains.skiko.ClipComponent
import org.jetbrains.skiko.SkiaLayer
import java.awt.Color
import java.awt.Component
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JLayeredPane
import org.jetbrains.skija.Rect
open class SkiaPanel: JLayeredPane {
val layer = SkiaLayer()
......@@ -16,7 +17,7 @@ open class SkiaPanel: JLayeredPane {
}
override fun add(component: Component): Component {
layer.clipComponets.add(ClipComponent(component))
layer.clipComponents.add(ClipComponent(component))
return super.add(component, Integer.valueOf(0))
}
......@@ -26,7 +27,6 @@ open class SkiaPanel: JLayeredPane {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
layer.reinit()
layer.setSize(width, height)
}
})
......
......@@ -5,16 +5,9 @@ import java.awt.Color
import java.awt.Dimension
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionAdapter
import javax.swing.JFrame
import javax.swing.JButton
import javax.swing.JLayeredPane
import javax.swing.JPanel
import javax.swing.event.MouseInputAdapter
import javax.swing.WindowConstants
import org.jetbrains.skiko.SkiaPanel
import javax.swing.*
fun Button(text: String): JButton {
......@@ -23,10 +16,10 @@ fun Button(text: String): JButton {
return btn
}
fun SwingSkia() {
fun SwingSkia() = SwingUtilities.invokeLater {
val window = JFrame()
window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.title = "SwingSkiaWindow"
val panel = SkiaPanel()
......@@ -63,13 +56,12 @@ fun SwingSkia() {
}
})
panel.layer.renderer = Renderer { renderer, w, h -> displayScene(renderer, w, h, mouseX, mouseY, state) }
panel.layer.renderer = Renderer(panel.layer) { renderer, w, h, nanoTime -> displayScene(renderer, w, h, nanoTime, mouseX, mouseY, state) }
panel.layer.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(event: MouseEvent) {
mouseX = event.x
mouseY = event.y
panel.layer.display()
}
})
......
/*
* This Kotlin source file was generated by the Gradle 'init' task.
*/
package SkijaInjectSample
import kotlin.test.Test
import kotlin.test.assertNotNull
class AppTest {
// @Test fun testAppHasAGreeting() {
// val classUnderTest = App()
// assertNotNull(classUnderTest.greeting, "app should have a greeting")
// }
}
package org.jetbrains.skiko
import java.awt.image.BufferedImage
fun isContentSame(img1: BufferedImage, img2: BufferedImage): Boolean {
if (img1.width == img2.width && img1.height == img2.height) {
for (x in 0 until img1.width) {
for (y in 0 until img1.height) {
if (img1.getRGB(x, y) != img2.getRGB(x, y)) {
return false
}
}
}
} else {
return false
}
return true
}
\ No newline at end of file
package org.jetbrains.skiko
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.awt.Rectangle
import java.awt.Robot
import java.io.File
import javax.imageio.ImageIO
// WARNING!!!
// macOS has wrong colors ([128, 128, 128] isn't [128, 128, 128] on screenshot). Only white, black, red and green are correct.
// So use only these color for cross-platform screenshots tests.
// TODO fix colors on macOS
class ScreenshotTestRule(private val robot: Robot) : TestRule {
private lateinit var testIdentifier: String
private val screenshotsDir = File(System.getProperty("skiko.test.screenshots.dir")!!)
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
testIdentifier = "${description.className}_${description.methodName}"
.replace(".", "_")
.replace(",", "_")
.replace(" ", "_")
.replace("(", "_")
.replace(")", "_")
.replace("__", "_")
.replace("__", "_")
.removePrefix("_")
.removeSuffix("_")
base.evaluate()
}
}
}
fun assert(rectangle: Rectangle, id: String = "") {
val actual = robot.createScreenCapture(rectangle)
val name = if (id.isNotEmpty()) "${testIdentifier}_$id" else testIdentifier
val actualFile = File(screenshotsDir, "${name}_actual.png")
val expectedFile = File(screenshotsDir, "$name.png")
if (actualFile.exists()) {
actualFile.delete()
}
if (expectedFile.exists()) {
val expected = ImageIO.read(expectedFile)
if (!isContentSame(expected, actual)) {
ImageIO.write(actual, "png", actualFile)
throw AssertionError(
"Image mismatch! Expected image ${expectedFile.absolutePath}, actual: ${actualFile.absolutePath}"
)
}
} else {
ImageIO.write(actual, "png", actualFile)
throw AssertionError(
"Missing screenshot image " +
"${actualFile.absolutePath}. " +
"Did you mean to check in a new image?"
)
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skija.*
import org.junit.Test
import java.awt.event.WindowEvent
import java.util.*
import javax.swing.JFrame
import javax.swing.WindowConstants
import kotlin.math.abs
import kotlin.math.log2
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
class SkiaWindowPerfomanceTest {
// TODO uncomment fontManager and fix native crash (Windows)
/*
J 1680 org.jetbrains.skija.impl.Managed._nInvokeFinalizer(JJ)V (0 bytes) @ 0x0000027907c261f4 [0x0000027907c261a0+0x0000000000000054]
J 1674 c1 org.jetbrains.skija.impl.Managed$CleanerThunk.run()V (31 bytes) @ 0x0000027900aac2cc [0x0000027900aabbe0+0x00000000000006ec]
J 724 c1 jdk.internal.ref.CleanerImpl$PhantomCleanableRef.performCleanup()V java.base@14.0.2 (10 bytes) @ 0x000002790088b3ec [0x000002790088b2c0+0x000000000000012c]
J 723 c1 jdk.internal.ref.PhantomCleanable.clean()V java.base@14.0.2 (16 bytes) @ 0x000002790088b91c [0x000002790088b740+0x00000000000001dc]
j jdk.internal.ref.CleanerImpl.run()V+77 java.base@14.0.2
j java.lang.Thread.run()V+11 java.base@14.0.2
j jdk.internal.misc.InnocuousThread.run()V+20 java.base@14.0.2
v ~StubRoutines::call_stub
native crash in SkiaWindowTest "render single window"
*/
// private val fontManager = FontMgr.getDefault()
// private val fontCollection = FontCollection()
// .setDefaultFontManager(fontManager)
//
// private fun paragraph(size: Float, text: String) =
// ParagraphBuilder(ParagraphStyle(), fontCollection)
// .pushStyle(
// TextStyle()
// .setColor(Color.RED.rgb)
// .setFontSize(size)
// )
// .addText(text)
// .popStyle()
// .build()
@Test
fun `FPS is near display refresh rate (multiple windows)`() = swingTest {
class TestWindow(
width: Int,
height: Int,
private val frameCount: Int,
private val deviatedTerminalCount: Int
) : SkiaWindow() {
private val expectedDeviatePercent1 = 0.05
private val expectedDeviatePercent2 = 0.15
private val expectedDeviatePercent3 = 0.30
private val expectedDeviatePercentTerminal = 0.50
private val expectedFrameNanos = 1E9 / graphicsConfiguration.device.displayMode.refreshRate
private val frameTimes = mutableListOf<Long>()
private var canCollect = false
val frameTimeDeltas get() = frameTimes.zipWithNext { a, b -> b - a }
val isCollected get() = frameTimes.size >= frameCount
private fun deviated(deviatePercent: Double) = frameTimeDeltas.filter {
abs(log2(it / expectedFrameNanos)) > log2(1 + deviatePercent)
}
init {
setLocation(200,200)
setSize(width, height)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = object : SkiaRenderer {
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
if (canCollect && frameTimes.size < frameCount) {
frameTimes.add(System.nanoTime()) // we check the real time, not the time provided by the argument
}
layer.needRedraw()
}
}
isUndecorated = true
isVisible = true
}
fun startCollect() {
canCollect = true
}
fun printInfo() {
println("[Window frame times ($frameCount frames)]")
val millis = frameTimeDeltas.map { it / 1E6 }
println("Deltas " + millis.map { String.format("%.1f", it) })
println("Average %.2f".format(millis.average()))
println("Standard deviation %.2f".format(millis.stddev()))
fun deviateMessage(percent: Double, deviated: List<Long>): String {
val deviatedStr = deviated.map { String.format("%.1f", it / 1E6) }
val percentTStr = (percent * 100).roundToInt()
return "$deviatedStr deviate by $percentTStr%"
}
val deviated1 = deviated(expectedDeviatePercent1)
val deviated2 = deviated(expectedDeviatePercent2)
val deviated3 = deviated(expectedDeviatePercent3)
val deviatedTerminal = deviated(expectedDeviatePercentTerminal)
println(deviateMessage(expectedDeviatePercent1, deviated1 - deviated2 - deviated3 - deviatedTerminal))
println(deviateMessage(expectedDeviatePercent2, deviated2 - deviated3 - deviatedTerminal))
println(deviateMessage(expectedDeviatePercent3, deviated3 - deviatedTerminal))
if (deviatedTerminal.size > deviatedTerminalCount) {
throw AssertionError(deviateMessage(expectedDeviatePercentTerminal, deviatedTerminal))
} else {
println(deviateMessage(expectedDeviatePercentTerminal, deviatedTerminal))
}
println()
}
private fun List<Double>.stddev(): Double {
val average = average()
fun f(x: Double) = (x - average) * (x - average)
return sqrt(map(::f).average())
}
}
suspend fun awaitFrameCollection(windows: List<TestWindow>) {
while (!windows.all(TestWindow::isCollected)) {
delay(100)
}
}
val windows = (1..3).map {
TestWindow(width = 40, height = 20, frameCount = 300, deviatedTerminalCount = 10)
}
delay(1000)
windows.forEach(TestWindow::startCollect)
awaitFrameCollection(windows)
windows.forEach(TestWindow::printInfo)
windows.forEach(TestWindow::close)
}
// TODO fix native crash on macOs in previous test if this test is performed before it
/*
Stack: [0x00007ffee09ff000,0x00007ffee11ff000], sp=0x00007ffee11f9630, free space=8169k
Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [libskiko-macos-x64.dylib+0xe999b] decltype(fp((SkRecords::NoOp)())) SkRecord::Record::visit<SkRecords::Draw&>(SkRecords::Draw&) const+0xb
C [libskiko-macos-x64.dylib+0xe940f] SkRecordDraw(SkRecord const&, SkCanvas*, SkPicture const* const*, SkDrawable* const*, int, SkBBoxHierarchy const*, SkPicture::AbortCallback*)+0x20f
C [libskiko-macos-x64.dylib+0x188783] SkBigPicture::playback(SkCanvas*, SkPicture::AbortCallback*) const+0xa3
C [libskiko-macos-x64.dylib+0x65b4b] SkCanvas::onDrawPicture(SkPicture const*, SkMatrix const*, SkPaint const*)+0x13b
C [libskiko-macos-x64.dylib+0x659d6] SkCanvas::drawPicture(SkPicture const*, SkMatrix const*, SkPaint const*)+0x146
C [libskiko-macos-x64.dylib+0x169c4] Java_org_jetbrains_skija_Canvas__1nDrawPicture+0x34
j org.jetbrains.skija.Canvas._nDrawPicture(JJ[FJ)V+0
j org.jetbrains.skija.Canvas.drawPicture(Lorg/jetbrains/skija/Picture;Lorg/jetbrains/skija/Matrix33;Lorg/jetbrains/skija/Paint;)Lorg/jetbrains/skija/Canvas;+27
j org.jetbrains.skija.Canvas.drawPicture(Lorg/jetbrains/skija/Picture;)Lorg/jetbrains/skija/Canvas;+4
j org.jetbrains.skiko.SkiaLayer.draw$skiko()V+229
j org.jetbrains.skiko.redrawer.MacOsRedrawer$drawLayer$1.draw()V+7
*/
//@Test
fun `check FPS (elementary picture)`() = swingTest {
// we don't count FPS straightforward because in window mode there is always vsync (we can get rid of it only in exclusive fullscreen mode)
// FPS will be capped by display's refresh rate.
//
// So we want to reach the point when the FPS is much below refresh rate drawing very big amount of pictures avery frame.
// When we reach that point, we can approximate FPS: FPS = 1000.0 / calculatedFrameTime * picturesPerFrame
val frameCheckCount = 10
val refreshRatePercent = 0.02 // We need to reach FPS = refreshRatePercent * refreshRate
var picturesPerFrame = 1000
val smoothFPSCounter = FPSCounter(count = frameCheckCount)
val onComplete = CompletableDeferred<Unit>()
fun renderer(window: SkiaWindow) = object : SkiaRenderer {
var t1 = Long.MAX_VALUE
val refreshRate = window.graphicsConfiguration.device.displayMode.refreshRate
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
val t2 = System.nanoTime()
val frameTime = (t2 - t1).coerceAtLeast(0)
t1 = t2
val currentFPS = 1E9 / frameTime
val targetFPS = refreshRate * refreshRatePercent
val smoothFPS = smoothFPSCounter.tick()
if (currentFPS > targetFPS) {
picturesPerFrame *= 2
}
if (smoothFPS < targetFPS && smoothFPSCounter.isCountReached) {
onComplete.complete(Unit)
}
val random = Random(123)
repeat(picturesPerFrame) {
canvas.save()
canvas.translate(width * random.nextFloat(), height * random.nextFloat())
canvas.drawTestPicture()
canvas.restore()
}
window.layer.needRedraw()
}
}
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.setSize(400, 400)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.isUndecorated = true
window.isVisible = true
delay(1000)
window.layer.renderer = renderer(window)
window.layer.needRedraw()
onComplete.await()
val fps = (smoothFPSCounter.fps * picturesPerFrame).roundToInt()
println("FPS is $fps")
println("We draw $picturesPerFrame pictures per frame to avoid VSYNC lag")
} finally {
window.close()
}
}
//private val paragraph by lazy { paragraph(10f, "Text") }
fun Canvas.drawTestPicture() {
save()
clipRect(Rect(2f, 2f, 18f, 18f))
drawRect(Rect(0f, 0f, 20f, 20f), Paint().apply {
color = 0x88FF0000.toInt()
})
drawRRect(RRect.makeLTRB(0f, 0f, 20f, 20f, 4f), Paint().apply {
mode = PaintMode.STROKE
strokeWidth = 2f
color = 0x8800FF00.toInt()
})
drawLine(0f, 0f, 10f, 10f, Paint().apply {
color = 0x880000FF.toInt()
isAntiAlias = true
})
//paragraph.layout(Float.POSITIVE_INFINITY) // TODO fix native crash on Linux (free(): invalid pointer)
//paragraph.paint(this, 0f, 0f)
restore()
}
private fun swingTest(block: suspend CoroutineScope.() -> Unit) {
runBlocking(Dispatchers.Swing) {
block()
}
}
private class FPSCounter(
private val count: Int
) {
private val times = LinkedList<Long>()
private var t1 = System.nanoTime()
val isCountReached get() = times.size == count
val fps get() = 1E9 / times.average()
fun tick(): Double {
val t2 = System.nanoTime()
val frameTime = t2 - t1
t1 = t2
times.add(frameTime)
if (times.size > count) {
times.removeFirst()
}
return fps
}
}
}
private fun JFrame.close() = dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.FontMgr
import org.jetbrains.skija.Paint
import org.jetbrains.skija.Rect
import org.jetbrains.skija.paragraph.FontCollection
import org.jetbrains.skija.paragraph.ParagraphBuilder
import org.jetbrains.skija.paragraph.ParagraphStyle
import org.jetbrains.skija.paragraph.TextStyle
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import java.awt.Color
import java.awt.Robot
import java.awt.event.WindowEvent
import javax.swing.JFrame
import javax.swing.WindowConstants
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
class SkiaWindowTest {
private val robot = Robot()
private val fontManager = FontMgr.getDefault()
private val fontCollection = FontCollection()
.setDefaultFontManager(fontManager)
private fun paragraph(size: Float, text: String) =
ParagraphBuilder(ParagraphStyle(), fontCollection)
.pushStyle(
TextStyle()
.setColor(Color.RED.rgb)
.setFontSize(size)
)
.addText(text)
.popStyle()
.build()
@get:Rule
val screenshots = ScreenshotTestRule(robot)
@Test
fun `render single window`() = swingTest {
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.setSize(400, 200)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
val renderer = RectRenderer(window.layer, 200, 100, Color.RED)
window.layer.renderer = renderer
window.isUndecorated = true
window.isVisible = true
delay(1000)
screenshots.assert(window.bounds, "frame1")
renderer.rectWidth = 100
window.layer.needRedraw()
delay(1000)
screenshots.assert(window.bounds, "frame2")
} finally {
window.close()
}
}
@Test
fun `resize window`() = swingTest {
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.setSize(40, 20)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.layer.renderer = RectRenderer(window.layer, 20, 10, Color.RED)
window.isUndecorated = true
window.isVisible = true
delay(1000)
window.setSize(80, 40)
delay(1000)
screenshots.assert(window.bounds)
} finally {
window.close()
}
}
@Test
fun `render three windows`() = swingTest {
fun window(color: Color) = SkiaWindow().apply {
setLocation(200,200)
setSize(400, 200)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = RectRenderer(layer, 200, 100, color)
isUndecorated = true
isVisible = true
}
val window1 = window(Color.RED)
val window2 = window(Color.GREEN)
val window3 = window(Color.BLACK)
try {
delay(1000)
window1.toFront()
delay(1000)
screenshots.assert(window1.bounds, "window1")
window2.toFront()
delay(1000)
screenshots.assert(window2.bounds, "window2")
window3.toFront()
delay(1000)
screenshots.assert(window3.bounds, "window3")
} finally {
window1.close()
window2.close()
window3.close()
}
}
@Test
fun `should call onRender after init, after resize, and only once after needRedraw`() = swingTest {
var renderCount = 0
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.setSize(40, 20)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.layer.renderer = object : SkiaRenderer {
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
renderCount++
}
}
window.isUndecorated = true
window.isVisible = true
delay(1000)
assertTrue(renderCount > 0)
renderCount = 0
window.setSize(50, 20)
delay(1000)
assertTrue(renderCount > 0)
renderCount = 0
window.layer.needRedraw()
delay(1000)
assertEquals(1, renderCount)
} finally {
window.close()
}
}
@Test
fun `open windows stress test`() = swingTest {
fun window(isAnimated: Boolean) = SkiaWindow().apply {
setLocation(200,200)
setSize(40, 20)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = if (isAnimated) {
AnimatedBoxRenderer(layer, pixelsPerSecond = 20.0, size = 2.0)
} else {
RectRenderer(layer, 20, 10, Color.RED)
}
isUndecorated = true
isVisible = true
}
val random = Random(31415926)
val openedWindows = mutableListOf<SkiaWindow>()
repeat(10) {
val needOpen = random.nextDouble() > 0.5f
repeat(10) {
if (needOpen) {
val window = window(isAnimated = random.nextDouble() > 0.5f)
openedWindows.add(window)
} else if (openedWindows.size > 0) {
val index = (random.nextDouble() * (openedWindows.size - 1)).toInt()
openedWindows.removeAt(index).close()
}
}
val delayCount = random.nextLong(5)
if (delayCount > 0) {
delay(delayCount * 10)
}
}
openedWindows.forEach(JFrame::close)
delay(5000)
}
@Test
fun `render text (Windows)`() {
testRenderText(OS.Windows)
}
@Test
fun `render text (Linux)`() {
testRenderText(OS.Linux)
}
@Test
fun `render text (MacOS)`() {
testRenderText(OS.MacOS)
}
private fun testRenderText(os: OS) = swingTest {
assumeTrue(hostOs == os)
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.setSize(400, 200)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
val paragraph by lazy { paragraph(window.layer.contentScale * 40, "=-+Нп") }
window.layer.renderer = object : SkiaRenderer {
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
paragraph.layout(Float.POSITIVE_INFINITY)
paragraph.paint(canvas, 0f, 0f)
}
}
window.isUndecorated = true
window.isVisible = true
delay(1000)
screenshots.assert(window.bounds)
} finally {
window.close()
}
}
private fun swingTest(block: suspend CoroutineScope.() -> Unit) {
runBlocking(Dispatchers.Swing) {
block()
}
}
private class RectRenderer(
private val layer: SkiaLayer,
var rectWidth: Int,
var rectHeight: Int,
private val rectColor: Color
) : SkiaRenderer {
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
val dpi = layer.contentScale
canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), Paint().apply {
color = Color.WHITE.rgb
})
canvas.drawRect(Rect(0f, 0f, rectWidth * dpi, rectHeight * dpi), Paint().apply {
color = rectColor.rgb
})
}
}
private class AnimatedBoxRenderer(
private val layer: SkiaLayer,
private val pixelsPerSecond: Double,
private val size: Double
) : SkiaRenderer {
private var oldNanoTime = Long.MAX_VALUE
private var x = 0.0
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
canvas.clear(Color.WHITE.rgb)
val dt = (nanoTime - oldNanoTime).coerceAtLeast(0) / 1E9
oldNanoTime = nanoTime
x += dt * pixelsPerSecond
if (x - size > width) {
x = 0.0
}
canvas.drawRect(Rect(x.toFloat(), 0f, x.toFloat() + size.toFloat(), size.toFloat()), Paint().apply {
color = Color.RED.rgb
})
layer.needRedraw()
}
}
}
private fun JFrame.close() = dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
\ No newline at end of file
import de.undercouch.gradle.tasks.download.Download
import kotlin.text.capitalize
import org.gradle.crypto.checksum.Checksum
plugins {
......@@ -167,6 +166,7 @@ kotlin {
kotlin.srcDirs(skijaSrcDir)
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.4.1")
compileOnly(lombok)
compileOnly(jetbrainsAnnotations)
}
......@@ -275,20 +275,21 @@ tasks.withType(CppCompile::class.java).configureEach {
project.tasks.register<Exec>("objcCompile") {
val inputDir = "$projectDir/src/jvmMain/objectiveC/${targetOs.id}"
val outDir = "$buildDir/objc/$target"
val objcSrc = "drawlayer"
val names = File(inputDir).listFiles()!!.map { it.name.removeSuffix(".m") }
val srcs = names.map { "$inputDir/$it.m" }.toTypedArray()
val outs = names.map { "$outDir/$it.o" }.toTypedArray()
workingDir = File(outDir)
commandLine = listOf(
"clang",
"-mmacosx-version-min=10.13",
"-I$jdkHome/include",
"-I$jdkHome/include/darwin",
"-c",
"$inputDir/$objcSrc.m",
"-o",
"$outDir/$objcSrc.o"
*srcs
)
file(outDir).mkdirs()
inputs.files("$inputDir/$objcSrc.m")
outputs.files("$outDir/$objcSrc.o")
inputs.files(srcs)
outputs.files(outs)
}
fun localSign(signer: String, lib: File) {
......@@ -367,6 +368,7 @@ tasks.withType(LinkSharedLibrary::class.java).configureEach {
linkerArgs.addAll(
listOf(
"gdi32.lib",
"Dwmapi.lib",
"opengl32.lib",
"shcore.lib",
"user32.lib"
......
// drawcanvas.cpp : Defines the exported functions for the DLL application.
#include <jawt_md.h>
#include <GL/gl.h>
#include <GL/glx.h>
......@@ -8,191 +7,19 @@
#include <cstdlib>
#include <unistd.h>
#include <stdio.h>
#include <set>
using namespace std;
typedef GLXContext (*glXCreateContextAttribsARBProc)(Display *, GLXFBConfig, GLXContext, Bool, const int *);
extern "C" jboolean Skiko_GetAWT(JNIEnv *env, JAWT *awt);
JavaVM *jvm = NULL;
class LayerHandler
{
public:
jobject canvasGlobalRef;
GLXContext context;
void updateLayerContent()
{
draw();
}
void disposeLayer(JNIEnv *env)
{
env->DeleteGlobalRef(canvasGlobalRef);
canvasGlobalRef = NULL;
context = NULL;
}
private:
void draw()
{
if (jvm != NULL)
{
JNIEnv *env;
jvm->GetEnv((void **)&env, JNI_VERSION_10);
JAWT awt;
JAWT_DrawingSurface *ds = NULL;
JAWT_DrawingSurfaceInfo *dsi = NULL;
jboolean result = JNI_FALSE;
jint lock = 0;
JAWT_X11DrawingSurfaceInfo *dsi_x11;
awt.version = (jint)JAWT_VERSION_9;
result = Skiko_GetAWT(env, &awt);
if (result == JNI_FALSE)
{
fprintf(stderr, "JAWT_GetAWT failed! Result is JNI_FALSE\n");
return;
}
ds = awt.GetDrawingSurface(env, canvasGlobalRef);
lock = ds->Lock(ds);
dsi = ds->GetDrawingSurfaceInfo(ds);
dsi_x11 = (JAWT_X11DrawingSurfaceInfo *)dsi->platformInfo;
Display *display = dsi_x11->display;
Window window = dsi_x11->drawable;
if (dsi != NULL)
{
jvm->AttachCurrentThread((void **)&env, NULL);
glXMakeCurrent(display, window, context);
static jclass wndClass = NULL;
if (!wndClass) wndClass = env->GetObjectClass(canvasGlobalRef);
static jmethodID drawMethod = NULL;
if (!drawMethod) drawMethod = env->GetMethodID(wndClass, "draw", "()V");
if (NULL == drawMethod)
{
fprintf(stderr, "The method Window.draw() not found!\n");
return;
}
env->CallVoidMethod(canvasGlobalRef, drawMethod);
glFinish();
glXSwapBuffers(display, window);
}
ds->FreeDrawingSurfaceInfo(dsi);
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
}
}
};
set<LayerHandler *> *layerStorage = NULL;
LayerHandler *findByObject(JNIEnv *env, jobject object)
{
if (layerStorage == NULL)
{
return NULL;
}
for (auto &layer : *layerStorage)
{
if (env->IsSameObject(object, layer->canvasGlobalRef) == JNI_TRUE)
{
return layer;
}
}
return NULL;
}
extern "C"
{
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_updateLayer(JNIEnv *env, jobject canvas)
{
if (layerStorage != NULL)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
return;
}
}
else
{
layerStorage = new set<LayerHandler *>();
}
if (jvm == NULL)
{
env->GetJavaVM(&jvm);
}
JAWT awt;
JAWT_DrawingSurface *ds = NULL;
JAWT_DrawingSurfaceInfo *dsi = NULL;
jboolean result = JNI_FALSE;
jint lock = 0;
JAWT_X11DrawingSurfaceInfo *dsi_x11;
awt.version = (jint)JAWT_VERSION_9;
result = Skiko_GetAWT(env, &awt);
if (result == JNI_FALSE)
{
fprintf(stderr, "JAWT_GetAWT failed! Result is JNI_FALSE\n");
return;
}
ds = awt.GetDrawingSurface(env, canvas);
lock = ds->Lock(ds);
dsi = ds->GetDrawingSurfaceInfo(ds);
dsi_x11 = (JAWT_X11DrawingSurfaceInfo *)dsi->platformInfo;
Display *display = dsi_x11->display;
Window window = dsi_x11->drawable;
if (dsi != NULL)
{
GLint att[] = {GLX_RGBA, GLX_DOUBLEBUFFER, True, None};
XVisualInfo *vi = glXChooseVisual(display, 0, att);
GLXContext context = glXCreateContext(display, vi, NULL, GL_TRUE);
LayerHandler *layer = new LayerHandler();
layerStorage->insert(layer);
jobject canvasRef = env->NewGlobalRef(canvas);
layer->canvasGlobalRef = canvasRef;
layer->context = context;
}
ds->FreeDrawingSurfaceInfo(dsi);
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_redrawLayer(JNIEnv *env, jobject canvas)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_init(JNIEnv *env, jobject canvas)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
layer->updateLayerContent();
}
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_disposeLayer(JNIEnv *env, jobject canvas)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_dispose(JNIEnv *env, jobject canvas)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
layerStorage->erase(layer);
layer->disposeLayer(env);
delete layer;
}
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_HardwareLayer_getWindowHandle(JNIEnv *env, jobject canvas)
......@@ -214,11 +41,6 @@ extern "C"
return -1;
}
if (jvm == NULL)
{
env->GetJavaVM(&jvm);
}
ds = awt.GetDrawingSurface(env, canvas);
lock = ds->Lock(ds);
dsi = ds->GetDrawingSurfaceInfo(ds);
......@@ -274,11 +96,6 @@ extern "C"
return -1;
}
if (jvm == NULL)
{
env->GetJavaVM(&jvm);
}
ds = awt.GetDrawingSurface(env, component);
lock = ds->Lock(ds);
dsi = ds->GetDrawingSurfaceInfo(ds);
......
#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>
typedef GLXContext (*glXCreateContextAttribsARBProc)(Display *, GLXFBConfig, GLXContext, Bool, const int *);
extern "C" jboolean Skiko_GetAWT(JNIEnv *env, JAWT *awt);
extern "C"
{
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_lockDrawingSurfaceNative(JNIEnv *env, jobject redrawer, jobject layer)
{
JAWT awt;
awt.version = (jint)JAWT_VERSION_9;
if (!Skiko_GetAWT(env, &awt))
{
fprintf(stderr, "JAWT_GetAWT failed! Result is JNI_FALSE\n");
return 0;
}
JAWT_DrawingSurface *ds = awt.GetDrawingSurface(env, layer);
ds->Lock(ds);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(ds));
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_unlockDrawingSurfaceNative(JNIEnv *env, jobject redrawer, jlong drawingSurfacePtr)
{
JAWT_DrawingSurface *ds = reinterpret_cast<JAWT_DrawingSurface *>(static_cast<uintptr_t>(drawingSurfacePtr));
JAWT awt;
awt.version = (jint)JAWT_VERSION_9;
if (!Skiko_GetAWT(env, &awt))
{
fprintf(stderr, "JAWT_GetAWT failed! Result is JNI_FALSE\n");
return 0;
}
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(ds));
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_getDisplay(JNIEnv *env, jobject redrawer, jlong drawingSurfacePtr)
{
JAWT_DrawingSurface *ds = reinterpret_cast<JAWT_DrawingSurface *>(static_cast<uintptr_t>(drawingSurfacePtr));
JAWT_DrawingSurfaceInfo *dsi = ds->GetDrawingSurfaceInfo(ds);
JAWT_X11DrawingSurfaceInfo *dsi_x11 = (JAWT_X11DrawingSurfaceInfo *)dsi->platformInfo;
Display *display = dsi_x11->display;
ds->FreeDrawingSurfaceInfo(dsi);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(display));
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_getWindow(JNIEnv *env, jobject redrawer, jlong drawingSurfacePtr)
{
JAWT_DrawingSurface *ds = reinterpret_cast<JAWT_DrawingSurface *>(static_cast<uintptr_t>(drawingSurfacePtr));
JAWT_DrawingSurfaceInfo *dsi = ds->GetDrawingSurfaceInfo(ds);
JAWT_X11DrawingSurfaceInfo *dsi_x11 = (JAWT_X11DrawingSurfaceInfo *)dsi->platformInfo;
Window window = dsi_x11->drawable;
ds->FreeDrawingSurfaceInfo(dsi);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(window));
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_setSwapInterval(JNIEnv *env, jobject redrawer, jlong displayPtr, jlong windowPtr, jint interval)
{
Display *display = reinterpret_cast<Display *>(static_cast<uintptr_t>(displayPtr));
Window window = reinterpret_cast<Window>(static_cast<uintptr_t>(windowPtr));
// according to:
// https://opengl.gpuinfo.org/listreports.php?extension=GLX_EXT_swap_control
// https://opengl.gpuinfo.org/listreports.php?extension=GLX_MESA_swap_control
// https://opengl.gpuinfo.org/listreports.php?extension=GLX_SGI_swap_control
// there is no Linux that doesn't support at least one of these extensions
static PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC) glXGetProcAddress((const GLubyte*)"glXSwapIntervalEXT");
if (glXSwapIntervalEXT != NULL)
{
glXSwapIntervalEXT(display, window, interval);
}
else
{
static PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC) glXGetProcAddress((const GLubyte*)"glXSwapIntervalMESA");
if (glXSwapIntervalMESA != NULL)
{
glXSwapIntervalMESA(interval);
}
else
{
static PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI = (PFNGLXSWAPINTERVALSGIPROC) glXGetProcAddress((const GLubyte*)"glXSwapIntervalSGI");
if (glXSwapIntervalSGI != NULL)
{
glXSwapIntervalSGI(interval);
}
}
}
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_swapBuffers(JNIEnv *env, jobject redrawer, jlong displayPtr, jlong windowPtr)
{
Display *display = reinterpret_cast<Display *>(static_cast<uintptr_t>(displayPtr));
Window window = reinterpret_cast<Window>(static_cast<uintptr_t>(windowPtr));
glXSwapBuffers(display, window);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_makeCurrent(JNIEnv *env, jobject redrawer, jlong displayPtr, jlong windowPtr, jlong contextPtr)
{
Display *display = reinterpret_cast<Display *>(static_cast<uintptr_t>(displayPtr));
Window window = reinterpret_cast<Window>(static_cast<uintptr_t>(windowPtr));
GLXContext *context = reinterpret_cast<GLXContext *>(static_cast<uintptr_t>(contextPtr));
glXMakeCurrent(display, window, *context);
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_createContext(JNIEnv *env, jobject redrawer, jlong displayPtr)
{
Display *display = reinterpret_cast<Display *>(static_cast<uintptr_t>(displayPtr));
GLint att[] = {GLX_RGBA, GLX_DOUBLEBUFFER, True, None};
XVisualInfo *vi = glXChooseVisual(display, 0, att);
GLXContext *context = new GLXContext(glXCreateContext(display, vi, NULL, GL_TRUE));
return static_cast<jlong>(reinterpret_cast<uintptr_t>(context));
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_LinuxRedrawerKt_destroyContext(JNIEnv *env, jobject redrawer, jlong displayPtr, jlong contextPtr)
{
Display *display = reinterpret_cast<Display *>(static_cast<uintptr_t>(displayPtr));
GLXContext *context = reinterpret_cast<GLXContext *>(static_cast<uintptr_t>(contextPtr));
glXDestroyContext(display, *context);
delete context;
}
}
\ No newline at end of file
// drawcanvas.cpp : Defines the exported functions for the DLL application.
#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <gl/GL.h>
#include <jawt_md.h>
#include <set>
#include <Shellscalingapi.h>
#include <stdio.h>
#include <Wingdi.h>
using namespace std;
JavaVM *jvm = NULL;
extern "C" jboolean Skiko_GetAWT(JNIEnv *env, JAWT *awt);
class LayerHandler
{
public:
jobject canvasGlobalRef;
HGLRC context;
HDC device;
void updateLayerContent()
{
draw();
}
void disposeLayer(JNIEnv *env)
{
env->DeleteGlobalRef(canvasGlobalRef);
canvasGlobalRef = NULL;
context = NULL;
device = NULL;
}
private:
void draw()
{
if (jvm != NULL)
{
JNIEnv *env;
jvm->GetEnv((void **)&env, JNI_VERSION_10);
wglMakeCurrent(device, context);
static jclass wndClass = NULL;
if (!wndClass) wndClass = env->GetObjectClass(canvasGlobalRef);
static jmethodID drawMethod = NULL;
if (!drawMethod) drawMethod = env->GetMethodID(wndClass, "draw", "()V");
if (NULL == drawMethod)
{
fprintf(stderr, "The method Window.draw() not found!\n");
return;
}
env->CallVoidMethod(canvasGlobalRef, drawMethod);
glFinish();
SwapBuffers(device);
}
}
};
set<LayerHandler *> *layerStorage = NULL;
LayerHandler *findByObject(JNIEnv *env, jobject object)
{
if (layerStorage == NULL)
{
return NULL;
}
for (auto &layer : *layerStorage)
{
if (env->IsSameObject(object, layer->canvasGlobalRef) == JNI_TRUE)
{
return layer;
}
}
return NULL;
}
extern "C"
{
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_updateLayer(JNIEnv *env, jobject canvas)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_init(JNIEnv *env, jobject canvas)
{
if (layerStorage != NULL)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
return;
}
}
else
{
layerStorage = new set<LayerHandler *>();
}
JAWT awt;
JAWT_DrawingSurface *ds = NULL;
JAWT_DrawingSurfaceInfo *dsi = NULL;
PIXELFORMATDESCRIPTOR pixFormatDscr;
HGLRC context = NULL;
jboolean result = JNI_FALSE;
jint lock = 0;
JAWT_Win32DrawingSurfaceInfo *dsi_win;
awt.version = (jint)JAWT_VERSION_9;
result = Skiko_GetAWT(env, &awt);
if (result == JNI_FALSE)
{
fprintf(stderr, "JAWT_GetAWT failed! Result is JNI_FALSE\n");
return;
}
if (jvm == NULL)
{
env->GetJavaVM(&jvm);
}
ds = awt.GetDrawingSurface(env, canvas);
lock = ds->Lock(ds);
dsi = ds->GetDrawingSurfaceInfo(ds);
dsi_win = (JAWT_Win32DrawingSurfaceInfo *)dsi->platformInfo;
HWND hwnd = dsi_win->hwnd;
HDC device = GetDC(hwnd);
if (dsi != NULL)
{
memset(&pixFormatDscr, 0, sizeof(PIXELFORMATDESCRIPTOR));
pixFormatDscr.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pixFormatDscr.nVersion = 1;
pixFormatDscr.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pixFormatDscr.iPixelType = PFD_TYPE_RGBA;
pixFormatDscr.cColorBits = 32;
int iPixelFormat = ChoosePixelFormat(device, &pixFormatDscr);
SetPixelFormat(device, iPixelFormat, &pixFormatDscr);
DescribePixelFormat(device, iPixelFormat, sizeof(PIXELFORMATDESCRIPTOR), &pixFormatDscr);
context = wglCreateContext(device);
LayerHandler *layer = new LayerHandler();
layerStorage->insert(layer);
jobject canvasRef = env->NewGlobalRef(canvas);
layer->canvasGlobalRef = canvasRef;
layer->context = context;
layer->device = device;
}
ds->FreeDrawingSurfaceInfo(dsi);
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_redrawLayer(JNIEnv *env, jobject canvas)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_dispose(JNIEnv *env, jobject canvas)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
layer->updateLayerContent();
}
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_disposeLayer(JNIEnv *env, jobject canvas)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
layerStorage->erase(layer);
layer->disposeLayer(env);
delete layer;
}
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_HardwareLayer_getWindowHandle(JNIEnv *env, jobject canvas)
......@@ -182,8 +18,6 @@ extern "C"
JAWT awt;
JAWT_DrawingSurface *ds = NULL;
JAWT_DrawingSurfaceInfo *dsi = NULL;
PIXELFORMATDESCRIPTOR pixFormatDscr;
HGLRC context = NULL;
jboolean result = JNI_FALSE;
jint lock = 0;
......@@ -198,11 +32,6 @@ extern "C"
return -1;
}
if (jvm == NULL)
{
env->GetJavaVM(&jvm);
}
ds = awt.GetDrawingSurface(env, canvas);
lock = ds->Lock(ds);
dsi = ds->GetDrawingSurfaceInfo(ds);
......
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <gl/GL.h>
#include <jawt_md.h>
#include <dwmapi.h>
extern "C" jboolean Skiko_GetAWT(JNIEnv *env, JAWT *awt);
extern "C"
{
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_getDevice(JNIEnv *env, jobject redrawer, jobject layer)
{
JAWT awt;
awt.version = (jint)JAWT_VERSION_9;
if (!Skiko_GetAWT(env, &awt))
{
fprintf(stderr, "JAWT_GetAWT failed! Result is JNI_FALSE\n");
return 0;
}
JAWT_DrawingSurface *ds = awt.GetDrawingSurface(env, layer);
ds->Lock(ds);
JAWT_DrawingSurfaceInfo *dsi = ds->GetDrawingSurfaceInfo(ds);
JAWT_Win32DrawingSurfaceInfo *dsi_win = (JAWT_Win32DrawingSurfaceInfo *)dsi->platformInfo;
HWND hwnd = dsi_win->hwnd;
HDC device = GetDC(hwnd);
if (dsi != NULL)
{
PIXELFORMATDESCRIPTOR pixFormatDscr;
memset(&pixFormatDscr, 0, sizeof(PIXELFORMATDESCRIPTOR));
pixFormatDscr.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pixFormatDscr.nVersion = 1;
pixFormatDscr.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pixFormatDscr.iPixelType = PFD_TYPE_RGBA;
pixFormatDscr.cColorBits = 32;
int iPixelFormat = ChoosePixelFormat(device, &pixFormatDscr);
SetPixelFormat(device, iPixelFormat, &pixFormatDscr);
DescribePixelFormat(device, iPixelFormat, sizeof(PIXELFORMATDESCRIPTOR), &pixFormatDscr);
}
ds->FreeDrawingSurfaceInfo(dsi);
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(device));
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_setSwapInterval(JNIEnv *env, jobject redrawer, jint interval)
{
typedef BOOL (WINAPI *PFNWGLSWAPINTERVALEXTPROC)(int interval);
// according to [https://opengl.gpuinfo.org/listreports.php?extension=WGL_EXT_swap_control&option=not] (filter by OS=windows)
// there a very few devices that doesn't support swap control
static PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC) wglGetProcAddress("wglSwapIntervalEXT");
if (wglSwapIntervalEXT != NULL)
{
wglSwapIntervalEXT(interval);
}
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_swapBuffers(JNIEnv *env, jobject redrawer, jlong devicePtr)
{
HDC device = reinterpret_cast<HDC>(static_cast<uintptr_t>(devicePtr));
SwapBuffers(device);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_makeCurrent(JNIEnv *env, jobject redrawer, jlong devicePtr, jlong contextPtr)
{
HDC device = reinterpret_cast<HDC>(static_cast<uintptr_t>(devicePtr));
HGLRC context = reinterpret_cast<HGLRC>(static_cast<uintptr_t>(contextPtr));
wglMakeCurrent(device, context);
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_createContext(JNIEnv *env, jobject redrawer, jlong devicePtr)
{
HDC device = reinterpret_cast<HDC>(static_cast<uintptr_t>(devicePtr));
return static_cast<jlong>(reinterpret_cast<uintptr_t>(wglCreateContext(device)));
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_deleteContext(JNIEnv *env, jobject redrawer, jlong contextPtr)
{
HGLRC context = reinterpret_cast<HGLRC>(static_cast<uintptr_t>(contextPtr));
wglDeleteContext(context);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_WindowsRedrawerKt_dwmFlush(JNIEnv *env, jobject redrawer)
{
DwmFlush();
}
}
\ No newline at end of file
package org.jetbrains.skiko
interface Drawable {
fun redrawLayer()
fun updateLayer()
fun disposeLayer()
val windowHandle: Long
val contentScale: Float
}
\ No newline at end of file
package org.jetbrains.skiko
import java.util.*
import kotlin.math.roundToInt
internal class FPSCounter(
private val count: Int,
private val probability: Double
) {
private var i = 0
private val times = LinkedList<Double>()
private var t1 = System.nanoTime()
/**
* [value] 0.0 - min, 1.0 - max, 0.5 - median
*/
private fun MutableList<Double>.quantile(value: Double) : Double {
val index = (value * (size - 1)).toInt()
return sorted()[index]
}
fun tick() {
val t2 = System.nanoTime()
val frameTime = (t2 - t1) / 1E6
t1 = t2
i++
times.add(frameTime)
if (times.size > count) {
times.removeFirst()
}
if (i % count == 0) {
val quantile = (1 - probability) / 2.0
val average = (1000.0 / times.average()).roundToInt()
val min = (1000.0 / times.quantile(1 - quantile)).roundToInt()
val max = (1000.0 / times.quantile(quantile)).roundToInt()
val probability = (100 * probability).roundToInt()
println("FPS $average ($min-$max $probability%)")
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import kotlin.coroutines.CoroutineContext
/**
* Dispatch frame after call of [scheduleFrame]
*/
class FrameDispatcher(
context: CoroutineContext,
private val onFrame: suspend () -> Unit
) {
private var needFrame = CompletableDeferred<Unit>()
private val job = GlobalScope.launch(context) {
while (true) {
needFrame.await()
needFrame = CompletableDeferred()
onFrame()
yield()
}
}
fun cancel() {
job.cancel()
}
fun scheduleFrame() {
needFrame.complete(Unit)
}
}
\ No newline at end of file
package org.jetbrains.skiko
import java.awt.Graphics
import java.awt.Canvas
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
abstract class HardwareLayer : Canvas(), Drawable {
import java.awt.Graphics
import java.awt.event.HierarchyEvent
abstract class HardwareLayer : Canvas() {
companion object {
init {
Library.load()
}
}
override fun paint(g: Graphics) {
display()
// getDpiScale is expensive operation on some platforms, so we cache it
private var _contentScale: Float? = null
private var isInit = false
init {
@Suppress("LeakingThis")
addHierarchyListener {
if (it.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() != 0L) {
checkIsShowing()
}
}
}
open fun display() {
this.updateLayer()
this.redrawLayer()
private fun checkIsShowing() {
if (!isInit && isShowing) {
_contentScale = getDpiScale()
init()
isInit = true
}
}
open fun draw() {}
protected open external fun init()
open external fun dispose()
external override fun redrawLayer()
protected open fun contentScaleChanged() = Unit
external override fun updateLayer()
override fun paint(g: Graphics) {
val contentScale = getDpiScale()
if (contentScale != _contentScale) {
_contentScale = contentScale
contentScaleChanged()
}
}
external override fun disposeLayer()
private fun getDpiScale(): Float {
val scale = platformOperations.getDpiScale(this)
check(scale > 0) { "HardwareLayer.contentScale isn't positive: $contentScale"}
return scale
}
override val windowHandle: Long
external get
// Should be called in Swing thread
internal abstract suspend fun update(nanoTime: Long)
override val contentScale: Float
get() = platformOperations.getDpiScale(this)
// Should be called in the OpenGL thread, and only once after update
internal abstract fun draw()
val absoluteX: Int
get() = convertPoint(this, x, y, getRootPane(this)).x
val windowHandle: Long
external get
val absoluteY: Int
get() = convertPoint(this, x, y, getRootPane(this)).y
val contentScale: Float
get() = _contentScale!!
var fullscreen: Boolean
get() = platformOperations.isFullscreen(this)
......
package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.LinuxRedrawer
import org.jetbrains.skiko.redrawer.MacOsRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.WindowsRedrawer
import java.awt.Component
import java.awt.Window
import javax.swing.SwingUtilities
......@@ -8,6 +12,7 @@ internal interface PlatformOperations {
fun isFullscreen(component: Component): Boolean
fun setFullscreen(component: Component, value: Boolean)
fun getDpiScale(component: Component): Float
fun createHardwareRedrawer(layer: HardwareLayer): Redrawer
}
internal val platformOperations: PlatformOperations by lazy {
......@@ -24,6 +29,8 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float {
return component.graphicsConfiguration.defaultTransform.scaleX.toFloat()
}
override fun createHardwareRedrawer(layer: HardwareLayer) = MacOsRedrawer(layer)
}
OS.Windows -> {
object: PlatformOperations {
......@@ -42,6 +49,8 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float {
return component.graphicsConfiguration.defaultTransform.scaleX.toFloat()
}
override fun createHardwareRedrawer(layer: HardwareLayer) = WindowsRedrawer(layer)
}
}
OS.Linux -> {
......@@ -61,6 +70,8 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float {
return linuxGetDpiScaleNative(component)
}
override fun createHardwareRedrawer(layer: HardwareLayer) = LinuxRedrawer(layer)
}
}
}
......
package org.jetbrains.skiko
import java.awt.Component
import org.jetbrains.skija.BackendRenderTarget
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.ColorSpace
import org.jetbrains.skija.DirectContext
import org.jetbrains.skija.FramebufferFormat
import org.jetbrains.skija.Rect
import org.jetbrains.skija.Surface
import org.jetbrains.skija.SurfaceColorFormat
import org.jetbrains.skija.SurfaceOrigin
import org.jetbrains.skija.ClipMode
import org.jetbrains.skija.*
import org.jetbrains.skiko.redrawer.Redrawer
import java.awt.Graphics
import javax.swing.SwingUtilities.isEventDispatchThread
private class SkijaState {
val bleachConstant = if (hostOs == OS.MacOS) 0 else -1
......@@ -26,89 +19,155 @@ private class SkijaState {
}
interface SkiaRenderer {
fun onInit()
fun onRender(canvas: Canvas, width: Int, height: Int)
fun onReshape(width: Int, height: Int)
fun onDispose()
suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
}
open class SkiaLayer() : HardwareLayer() {
private class PictureHolder(val instance: Picture, val width: Int, val height: Int)
open class SkiaLayer : HardwareLayer() {
open val api: GraphicsApi = GraphicsApi.OPENGL
var renderer: SkiaRenderer? = null
val clipComponets = mutableListOf<ClipRectangle>()
val clipComponents = mutableListOf<ClipRectangle>()
private val skijaState = SkijaState()
protected var inited: Boolean = false
fun reinit() {
inited = false
@Volatile
private var isDisposed = false
private var redrawer: Redrawer? = null
@Volatile
private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder()
private val pictureLock = Any()
override fun init() {
super.init()
redrawer = platformOperations.createHardwareRedrawer(this)
redrawer?.syncSize()
needRedraw()
}
override fun dispose() {
check(!isDisposed)
check(isEventDispatchThread())
redrawer?.dispose()
picture?.instance?.close()
pictureRecorder.close()
isDisposed = true
super.dispose()
}
override fun disposeLayer() {
super.disposeLayer()
renderer?.onDispose()
override fun setBounds(x: Int, y: Int, width: Int, height: Int) {
super.setBounds(x, y, width, height)
redrawer?.syncSize()
needRedraw()
}
override fun paint(g: Graphics) {
super.paint(g)
redrawer?.syncSize()
needRedraw()
}
fun needRedraw() {
check(!isDisposed)
check(isEventDispatchThread())
redrawer?.needRedraw()
}
private val fpsCounter = FPSCounter(
count = System.getProperty("skiko.fps.count")?.toInt() ?: 500,
probability = System.getProperty("skiko.fps.probability")?.toDouble() ?: 0.97
)
override suspend fun update(nanoTime: Long) {
check(!isDisposed)
check(isEventDispatchThread())
if (System.getProperty("skiko.fps.enabled") == "true") {
fpsCounter.tick()
}
val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0)
val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0)
val bounds = Rect.makeWH(pictureWidth.toFloat(), pictureHeight.toFloat())!!
val canvas = pictureRecorder.beginRecording(bounds)!!
// clipping
for (component in clipComponents) {
canvas.clipRectBy(component)
}
renderer?.onRender(canvas, pictureWidth, pictureHeight, nanoTime)
check(!isDisposed)
synchronized(pictureLock) {
picture?.instance?.close()
val picture = pictureRecorder.finishRecordingAsPicture()
this.picture = PictureHolder(picture, pictureWidth, pictureHeight)
}
}
override fun draw() {
if (!inited) {
if (skijaState.context == null) {
skijaState.context = when (api) {
GraphicsApi.OPENGL -> makeGLContext()
GraphicsApi.METAL -> makeMetalContext()
else -> TODO("Unsupported yet")
}
check(!isDisposed)
if (skijaState.context == null) {
skijaState.context = when (api) {
GraphicsApi.OPENGL -> makeGLContext()
GraphicsApi.METAL -> makeMetalContext()
else -> TODO("Unsupported yet")
}
renderer?.onInit()
inited = true
renderer?.onReshape(width, height)
}
initSkija()
skijaState.apply {
canvas!!.clear(bleachConstant)
// cliping
for (component in clipComponets) {
clipRectBy(component)
synchronized(pictureLock) {
val picture = picture
if (picture != null) {
canvas!!.drawPicture(picture.instance)
}
}
renderer?.onRender(canvas!!, width, height)
context!!.flush()
}
}
private fun clipRectBy(rectangle: ClipRectangle) {
skijaState.apply {
canvas!!.clipRect(
Rect.makeLTRB(
rectangle.x,
rectangle.y,
rectangle.x + rectangle.width,
rectangle.y + rectangle.height
),
ClipMode.DIFFERENCE,
true
)
}
private fun Canvas.clipRectBy(rectangle: ClipRectangle) {
val dpi = contentScale
clipRect(
Rect.makeLTRB(
rectangle.x * dpi,
rectangle.y * dpi,
(rectangle.x + rectangle.width) * dpi,
(rectangle.y + rectangle.height) * dpi
),
ClipMode.DIFFERENCE,
true
)
}
private fun initSkija() {
val dpi = contentScale
initRenderTarget(dpi)
initRenderTarget()
initSurface()
scaleCanvas(dpi)
}
private fun initRenderTarget(dpi: Float) {
private fun initRenderTarget() {
skijaState.apply {
clear()
val dpi = contentScale
val width = (width * dpi).toInt().coerceAtLeast(0)
val height = (height * dpi).toInt().coerceAtLeast(0)
renderTarget = when (api) {
GraphicsApi.OPENGL -> {
val gl = OpenGLApi.instance
val fbId = gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING)
makeGLRenderTarget(
(width * dpi).toInt(),
(height * dpi).toInt(),
width,
height,
0,
8,
fbId,
......@@ -116,8 +175,8 @@ open class SkiaLayer() : HardwareLayer() {
)
}
GraphicsApi.METAL -> makeMetalRenderTarget(
(width * dpi).toInt(),
(height * dpi).toInt(),
width,
height,
0
)
else -> TODO("Unsupported yet")
......@@ -137,10 +196,4 @@ open class SkiaLayer() : HardwareLayer() {
canvas = surface!!.canvas
}
}
protected open fun scaleCanvas(dpi: Float) {
skijaState.apply {
canvas!!.scale(dpi, dpi)
}
}
}
package org.jetbrains.skiko
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JFrame
open class SkiaWindow : JFrame() {
......@@ -9,15 +7,10 @@ open class SkiaWindow : JFrame() {
init {
contentPane.add(layer)
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
layer.reinit()
}
})
}
fun display() {
layer.display()
override fun dispose() {
layer.dispose()
super.dispose()
}
}
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi
internal class LinuxRedrawer(
private val layer: HardwareLayer
) : Redrawer {
private val context = layer.lockDrawingSurface {
val context = it.createContext()
it.makeCurrent(context)
it.setSwapInterval(1)
context
}
private var isDisposed = false
private val job = Job()
override fun dispose() {
check(!isDisposed)
layer.lockDrawingSurface {
it.destroyContext(context)
}
isDisposed = true
job.cancel()
}
override fun needRedraw() {
check(!isDisposed)
toRedraw.add(this)
frameDispatcher.scheduleFrame()
}
private suspend fun update(nanoTime: Long) {
withContext(job) {
layer.update(nanoTime)
}
}
private fun draw() {
layer.draw()
}
companion object {
private val toRedraw = mutableSetOf<LinuxRedrawer>()
private val toRedrawCopy = mutableSetOf<LinuxRedrawer>()
private val toRedrawAlive = toRedrawCopy.asSequence().filterNot(LinuxRedrawer::isDisposed)
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
toRedrawCopy.clear()
toRedrawCopy.addAll(toRedraw)
toRedraw.clear()
val nanoTime = System.nanoTime()
for (redrawer in toRedrawAlive) {
try {
redrawer.update(nanoTime)
} catch (e: CancellationException) {
// continue
}
}
val drawingSurfaces = toRedrawAlive.map { lockDrawingSurface(it.layer) }.toList()
try {
toRedrawAlive.forEachIndexed { index, redrawer ->
drawingSurfaces[index].makeCurrent(redrawer.context)
redrawer.draw()
}
toRedrawAlive.forEachIndexed { index, _ ->
drawingSurfaces[index].swapBuffers()
}
toRedrawAlive.forEachIndexed { index, redrawer ->
drawingSurfaces[index].makeCurrent(redrawer.context)
OpenGLApi.instance.glFinish()
}
} finally {
drawingSurfaces.forEach(::unlockDrawingSurface)
}
}
}
}
private inline fun <T> HardwareLayer.lockDrawingSurface(action: (DrawingSurface) -> T): T {
val drawingSurface = lockDrawingSurface(this)
try {
return action(drawingSurface)
} finally {
unlockDrawingSurface(drawingSurface)
}
}
private fun lockDrawingSurface(layer: HardwareLayer): DrawingSurface {
val ptr = lockDrawingSurfaceNative(layer)
return DrawingSurface(ptr, getDisplay(ptr), getWindow(ptr))
}
private fun unlockDrawingSurface(drawingSurface: DrawingSurface) {
unlockDrawingSurfaceNative(drawingSurface.ptr)
}
private class DrawingSurface(
val ptr: Long,
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 lockDrawingSurfaceNative(layer: HardwareLayer): Long
private external fun unlockDrawingSurfaceNative(drawingSurface: Long)
private external fun getDisplay(drawingSurface: Long): Long
private external fun getWindow(drawingSurface: Long): Long
private external fun makeCurrent(display: Long, window: Long, context: Long)
private external fun createContext(display: Long): Long
private external fun destroyContext(display: Long, context: Long)
private external fun setSwapInterval(display: Long, window: Long, interval: Int)
private external fun swapBuffers(display: Long, window: Long)
\ No newline at end of file
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
internal class MacOsRedrawer(
private val layer: HardwareLayer
) : Redrawer {
private val containerLayerPtr = initContainer(layer)
private val drawLayer = object : AWTGLLayer(containerLayerPtr, setNeedsDisplayOnBoundsChange = true) {
override fun draw() = layer.draw()
}
// use a separate layer for vsync, because with single layer we cannot asynchronously update layer
// `update` is suspend, and runBlocking(Dispatchers.Swing) causes dead lock with AppKit Thread.
// AWT has a method to avoid dead locks but it is internal (sun.lwawt.macosx.LWCToolkit.invokeAndWait)
private val vsyncLayer = object : AWTGLLayer(containerLayerPtr, setNeedsDisplayOnBoundsChange = false) {
@Volatile
private var needDraw: CompletableDeferred<Unit>? = null
init {
setFrame(0, 0, 1, 1) // if frame has zero size then it will be not drawn at all
}
override fun draw() {
// Clear layer with transparent color, so it will be not pink color.
val opengl = OpenGLApi.instance
opengl.glClearColor(0f, 0f, 0f, 0f)
opengl.glClear(opengl.GL_COLOR_BUFFER_BIT)
needDraw?.complete(Unit)
}
override fun canDraw(): Boolean {
val canDraw = needDraw != null
if (!canDraw) {
isAsynchronous = false // stop asynchronous mode so we don't waste CPU cycles
}
return canDraw
}
suspend fun sync() {
check(needDraw == null)
needDraw = CompletableDeferred()
// Use asynchronous mode instead of just setNeedsDisplay,
// so Core Animation will wait for the next frame in vsync signal
//
// Asynchronous mode means that Core Animation will automatically
// call canDraw/draw every vsync signal (~16.7ms on 60Hz monitor)
//
// Similar is implemented in Chromium:
// https://chromium.googlesource.com/chromium/chromium/+/0489078bf98350b00876070cf2fdce230905f47e/content/browser/renderer_host/compositing_iosurface_layer_mac.mm#57
if (!isAsynchronous) {
isAsynchronous = true
super.setNeedsDisplay()
}
needDraw!!.await()
needDraw = null
}
}
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
layer.update(System.nanoTime())
drawLayer.setNeedsDisplay()
vsyncLayer.sync()
}
override fun dispose() {
frameDispatcher.cancel()
vsyncLayer.dispose()
drawLayer.dispose()
}
override fun syncSize() {
val globalPosition = convertPoint(layer, layer.x, layer.y, getRootPane(layer))
setContentScale(containerLayerPtr, layer.contentScale)
setContentScale(drawLayer.ptr, layer.contentScale)
drawLayer.setFrame(
globalPosition.x,
globalPosition.y,
layer.width.coerceAtLeast(0),
layer.height.coerceAtLeast(0)
)
}
override fun needRedraw() {
frameDispatcher.scheduleFrame()
}
}
private open class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnBoundsChange: Boolean) {
@Suppress("LeakingThis")
val ptr = initAWTGLLayer(containerPtr, this, setNeedsDisplayOnBoundsChange)
fun setFrame(x: Int, y: Int, width: Int, height: Int) {
setFrame(containerPtr, ptr, x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat())
}
// Called in AWT Thread
open fun dispose() = disposeAWTGLLayer(ptr)
var isAsynchronous: Boolean
get() = isAsynchronous(ptr)
set(value) = setAsynchronous(ptr, value)
open fun setNeedsDisplay() = setNeedsDisplayOnMainThread(ptr)
// Called in AppKit Thread
protected open fun canDraw() = true
// Called in AppKit Thread
protected open fun draw() = Unit
private external fun isAsynchronous(ptr: Long): Boolean
private external fun setAsynchronous(ptr: Long, isAsynchronous: Boolean)
private external fun setNeedsDisplayOnMainThread(nativePtr: Long)
protected external fun setFrame(containerPtr: Long, ptr: Long, x: Float, y: Float, width: Float, height: Float)
}
private external fun initContainer(layer: HardwareLayer): Long
private external fun setContentScale(layerNativePtr: Long, contentScale: Float)
private external fun initAWTGLLayer(containerPtr: Long, layer: AWTGLLayer, setNeedsDisplayOnBoundsChange: Boolean): Long
private external fun disposeAWTGLLayer(ptr: Long)
package org.jetbrains.skiko.redrawer
interface Redrawer {
fun dispose()
fun needRedraw()
fun syncSize() = Unit
}
\ No newline at end of file
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi
internal class WindowsRedrawer(
private val layer: HardwareLayer
) : Redrawer {
private val device = getDevice(layer)
private val context = createContext(device)
private var isDisposed = false
private val job = Job()
init {
makeCurrent()
// For vsync we will use dwmFlush instead of swapInterval,
// because it isn't reliable with DWM (Desktop Windows Manager): interval between frames isn't stable (14-19ms).
// With dwmFlush it is stable (16.6-16.8 ms)
// GLFW also uses dwmFlush (https://www.glfw.org/docs/3.0/window.html#window_swap)
setSwapInterval(0)
}
override fun dispose() {
check(!isDisposed)
deleteContext(context)
isDisposed = true
job.cancel()
}
override fun needRedraw() {
check(!isDisposed)
toRedraw.add(this)
frameDispatcher.scheduleFrame()
}
private suspend fun update(nanoTime: Long) {
withContext(job) {
layer.update(nanoTime)
}
}
private fun draw() {
layer.draw()
}
private fun makeCurrent() = makeCurrent(device, context)
private fun swapBuffers() = swapBuffers(device)
companion object {
private val toRedraw = mutableSetOf<WindowsRedrawer>()
private val toRedrawCopy = mutableSetOf<WindowsRedrawer>()
private val toRedrawAlive = toRedrawCopy.asSequence().filterNot(WindowsRedrawer::isDisposed)
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
toRedrawCopy.clear()
toRedrawCopy.addAll(toRedraw)
toRedraw.clear()
val nanoTime = System.nanoTime()
for (redrawer in toRedrawAlive) {
try {
redrawer.update(nanoTime)
} catch (e: CancellationException) {
// continue
}
}
for (redrawer in toRedrawAlive) {
redrawer.makeCurrent()
redrawer.draw()
}
for (redrawer in toRedrawAlive) {
redrawer.swapBuffers()
}
for (redrawer in toRedrawAlive) {
redrawer.makeCurrent()
OpenGLApi.instance.glFinish()
}
withContext(Dispatchers.IO) {
dwmFlush() // wait for vsync
}
}
}
}
private external fun makeCurrent(device: Long, context: Long)
private external fun getDevice(layer: HardwareLayer): Long
private external fun createContext(device: Long): Long
private external fun deleteContext(context: Long)
private external fun setSwapInterval(interval: Int)
private external fun swapBuffers(device: Long)
// TODO according to https://bugs.chromium.org/p/chromium/issues/detail?id=467617 dwmFlush has lag 3 ms after vsync.
// Maybe we should use D3DKMTWaitForVerticalBlankEvent? See also https://www.vsynctester.com/chromeisbroken.html
// TODO should we support Windows 7? DWM can be disabled on Windows 7.
// it that case there will be a crash or just no frame limit (I don't know exactly).
private external fun dwmFlush()
\ No newline at end of file
......@@ -7,69 +7,13 @@
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
#import <OpenGL/gl3.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <pthread.h>
JavaVM *jvm = NULL;
@interface AWTGLLayer : CAOpenGLLayer
@property jobject canvasGlobalRef;
@end
@implementation AWTGLLayer
- (id)init
{
self = [super init];
if (self)
{
[self removeAllAnimations];
[self setAutoresizingMask: (kCALayerWidthSizable|kCALayerHeightSizable)];
[self setNeedsDisplayOnBoundsChange: YES];
self.canvasGlobalRef = NULL;
}
return self;
}
-(void)drawInCGLContext:(CGLContextObj)ctx
pixelFormat:(CGLPixelFormatObj)pf
forLayerTime:(CFTimeInterval)t
displayTime:(const CVTimeStamp *)ts
{
CGLSetCurrentContext(ctx);
if (jvm != NULL) {
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
static jclass wndClass = NULL;
if (!wndClass) wndClass = (*env)->GetObjectClass(env, self.canvasGlobalRef);
static jmethodID drawMethod = NULL;
if (!drawMethod) drawMethod = (*env)->GetMethodID(env, wndClass, "draw", "()V");
if (NULL == drawMethod)
{
NSLog(@"The method HardwareLayer.draw() not found!");
return;
}
(*env)->CallVoidMethod(env, self.canvasGlobalRef, drawMethod);
}
[super drawInCGLContext:ctx pixelFormat:pf forLayerTime:t displayTime:ts];
}
@end
@interface LayerHandler : NSObject
@property jobject canvasGlobalRef;
@property (retain, strong) CALayer *container;
@property (retain, strong) AWTGLLayer *glLayer;
@property (retain, strong) NSWindow *window;
@end
......@@ -82,107 +26,18 @@ JavaVM *jvm = NULL;
if (self)
{
self.canvasGlobalRef = NULL;
self.container = NULL;
self.glLayer = NULL;
self.window = NULL;
}
return self;
}
- (void) syncLayersSize
{
if (jvm != NULL) {
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
static jclass wndClass = NULL;
if (!wndClass)
{
wndClass = (*env)->GetObjectClass(env, self.glLayer.canvasGlobalRef);
}
// scale factor
static jmethodID contentScaleMethod = NULL;
if (!contentScaleMethod)
{
contentScaleMethod = (*env)->GetMethodID(env, wndClass, "getContentScale", "()F");
}
if (NULL == contentScaleMethod)
{
NSLog(@"The method HardwareLayer.getContentScale() not found!");
return;
}
float scaleFactor = (*env)->CallFloatMethod(env, self.glLayer.canvasGlobalRef, contentScaleMethod);
assert(scaleFactor != 0);
self.container.contentsScale = scaleFactor;
self.glLayer.contentsScale = scaleFactor;
// size & position
static jmethodID getXMethod = NULL;
if (!getXMethod)
{
getXMethod = (*env)->GetMethodID(env, wndClass, "getAbsoluteX", "()I");
}
if (NULL == getXMethod)
{
NSLog(@"The method HardwareLayer.getAbsoluteX() not found!");
return;
}
static jmethodID getYMethod = NULL;
if (!getYMethod)
{
getYMethod = (*env)->GetMethodID(env, wndClass, "getAbsoluteY", "()I");
}
if (NULL == getYMethod)
{
NSLog(@"The method HardwareLayer.getAbsoluteY() not found!");
return;
}
static jmethodID getWidthMethod = NULL;
if (!getWidthMethod)
{
getWidthMethod = (*env)->GetMethodID(env, wndClass, "getWidth", "()I");
}
if (NULL == getWidthMethod)
{
NSLog(@"The method HardwareLayer.getWidth() not found!");
return;
}
static jmethodID getHeightMethod = NULL;
if (!getHeightMethod)
{
getHeightMethod = (*env)->GetMethodID(env, wndClass, "getHeight", "()I");
}
if (NULL == getHeightMethod)
{
NSLog(@"The method HardwareLayer.getHeight() not found!");
return;
}
int x = (*env)->CallIntMethod(env, self.glLayer.canvasGlobalRef, getXMethod);
int y = (*env)->CallIntMethod(env, self.glLayer.canvasGlobalRef, getYMethod);
int w = (*env)->CallIntMethod(env, self.glLayer.canvasGlobalRef, getWidthMethod);
int h = (*env)->CallIntMethod(env, self.glLayer.canvasGlobalRef, getHeightMethod);
y = (int)self.container.frame.size.height - y - h;
CGRect boundsRect = CGRectMake(x, y, w, h);
self.glLayer.frame = boundsRect;
}
}
- (void) updateLayerContent
{
[self.glLayer performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:0 waitUntilDone:NO];
}
- (void) disposeLayer:(JNIEnv *) env
{
[self.glLayer removeFromSuperlayer];
(*env)->DeleteGlobalRef(env, self.glLayer.canvasGlobalRef);
self.glLayer.canvasGlobalRef = NULL;
self.glLayer = NULL;
(*env)->DeleteGlobalRef(env, self.canvasGlobalRef);
self.canvasGlobalRef = NULL;
self.container = NULL;
self.window = NULL;
}
......@@ -206,18 +61,7 @@ JavaVM *jvm = NULL;
@end
NSMutableArray *unknownWindows = nil;
NSMutableSet *layerStorage = nil;
pthread_mutex_t layerStorageMutex = { 0 };
void lockLayers() {
pthread_mutex_lock(&layerStorageMutex);
}
void unlockLayers() {
pthread_mutex_unlock(&layerStorageMutex);
}
LayerHandler * findByObject(JNIEnv *env, jobject object)
{
......@@ -227,7 +71,7 @@ LayerHandler * findByObject(JNIEnv *env, jobject object)
}
for (LayerHandler* layer in layerStorage)
{
if ((*env)->IsSameObject(env, object, layer.glLayer.canvasGlobalRef) == JNI_TRUE)
if ((*env)->IsSameObject(env, object, layer.canvasGlobalRef) == JNI_TRUE)
{
return layer;
}
......@@ -237,20 +81,9 @@ LayerHandler * findByObject(JNIEnv *env, jobject object)
extern jboolean Skiko_GetAWT(JNIEnv* env, JAWT* awt);
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_updateLayer(JNIEnv *env, jobject canvas)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_init(JNIEnv *env, jobject canvas)
{
lockLayers();
if (layerStorage != nil)
{
LayerHandler *layer = findByObject(env, canvas);
if (layer != NULL)
{
[layer syncLayersSize];
unlockLayers();
return;
}
}
else
if (layerStorage == nil)
{
layerStorage = [[NSMutableSet alloc] init];
}
......@@ -258,8 +91,6 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_updateLayer(JNIEnv
JAWT awt;
JAWT_DrawingSurface *ds = NULL;
JAWT_DrawingSurfaceInfo *dsi = NULL;
CGLPixelFormatObj pixFormatObj = NULL;
CGLContextObj context;
jboolean result = JNI_FALSE;
jint lock = 0;
......@@ -269,8 +100,6 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_updateLayer(JNIEnv
result = Skiko_GetAWT(env, &awt);
assert(result != JNI_FALSE);
(*env)->GetJavaVM(env, &jvm);
ds = awt.GetDrawingSurface(env, canvas);
assert(ds != NULL);
......@@ -286,71 +115,35 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_updateLayer(JNIEnv
LayerHandler *layersSet = [[LayerHandler alloc] init];
layersSet.container = [dsi_mac windowLayer];
[layersSet.container removeAllAnimations];
[layersSet.container setAutoresizingMask: (kCALayerWidthSizable|kCALayerHeightSizable)];
[layersSet.container setNeedsDisplayOnBoundsChange: YES];
layersSet.glLayer = [AWTGLLayer new];
[layersSet.container addSublayer: layersSet.glLayer];
jobject canvasGlobalRef = (*env)->NewGlobalRef(env, canvas);
[layersSet setCanvasGlobalRef: canvasGlobalRef];
[layersSet.glLayer setCanvasGlobalRef: canvasGlobalRef];
[layersSet syncLayersSize];
NSMutableArray<NSWindow *> *windows = [NSMutableArray arrayWithArray: [[NSApplication sharedApplication] windows]];
if (unknownWindows == nil)
for (LayerHandler* value in layerStorage)
{
NSMutableArray<NSWindow *> *windows = [NSMutableArray arrayWithArray: [[NSApplication sharedApplication] windows]];
layersSet.window = [windows lastObject];
[windows removeObject: layersSet.window];
unknownWindows = windows;
}
else
{
NSMutableArray<NSWindow *> *windows = [NSMutableArray arrayWithArray: [[NSApplication sharedApplication] windows]];
for (NSWindow* value in unknownWindows)
{
[windows removeObject: value];
}
for (LayerHandler* value in layerStorage)
{
if (layersSet.container == value.container)
{
layersSet.window = value.window;
}
}
if (layersSet.window == NULL)
if (layersSet.container == value.container)
{
layersSet.window = [windows lastObject];
layersSet.window = value.window;
}
}
if (layersSet.window == NULL)
{
layersSet.window = [windows lastObject];
}
[layerStorage addObject: layersSet];
}
ds->FreeDrawingSurfaceInfo(dsi);
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
unlockLayers();
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_redrawLayer(JNIEnv *env, jobject canvas)
{
lockLayers();
LayerHandler *layer = findByObject(env, canvas);
unlockLayers();
if (layer != NULL)
{
[layer updateLayerContent];
}
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_disposeLayer(JNIEnv *env, jobject canvas)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_HardwareLayer_dispose(JNIEnv *env, jobject canvas)
{
lockLayers();
LayerHandler *layer = findByObject(env, canvas);
unlockLayers();
if (layer != NULL)
{
[layerStorage removeObject: layer];
......@@ -365,9 +158,7 @@ JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_HardwareLayer_getWindowHandle(J
JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_PlatformOperationsKt_osxIsFullscreenNative(JNIEnv *env, jobject properties, jobject component)
{
lockLayers();
LayerHandler *layer = findByObject(env, component);
unlockLayers();
if (layer != NULL)
{
return [layer isFullScreen];
......@@ -377,9 +168,7 @@ JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_PlatformOperationsKt_osxIsFu
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_PlatformOperationsKt_osxSetFullscreenNative(JNIEnv *env, jobject properties, jobject component, jboolean value)
{
lockLayers();
LayerHandler *layer = findByObject(env, component);
unlockLayers();
if (layer != NULL)
{
[layer makeFullscreen:value];
......
#import "jawt.h"
#import "jawt_md.h"
#define GL_SILENCE_DEPRECATION
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
#import <OpenGL/gl3.h>
extern jboolean Skiko_GetAWT(JNIEnv* env, JAWT* awt);
JavaVM *jvm = NULL;
@interface AWTGLLayer : CAOpenGLLayer
@property jobject javaRef;
@end
@implementation AWTGLLayer
- (id)init
{
self = [super init];
assert(self != NULL);
[self removeAllAnimations];
[self setAutoresizingMask: (kCALayerWidthSizable|kCALayerHeightSizable)];
return self;
}
-(BOOL)canDrawInCGLContext:(CGLContextObj)ctx
pixelFormat:(CGLPixelFormatObj)pf
forLayerTime:(CFTimeInterval)t
displayTime:(const CVTimeStamp *)ts
{
assert(jvm != NULL);
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
static jclass cls = NULL;
static jmethodID method = NULL;
if (!cls) cls = (*env)->GetObjectClass(env, self.javaRef);
if (!method) method = (*env)->GetMethodID(env, cls, "canDraw", "()Z");
return (*env)->CallBooleanMethod(env, self.javaRef, method);
}
-(void)drawInCGLContext:(CGLContextObj)ctx
pixelFormat:(CGLPixelFormatObj)pf
forLayerTime:(CFTimeInterval)t
displayTime:(const CVTimeStamp *)ts
{
CGLSetCurrentContext(ctx);
assert(jvm != NULL);
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
static jclass cls = NULL;
static jmethodID method = NULL;
if (!cls) cls = (*env)->GetObjectClass(env, self.javaRef);
if (!method) method = (*env)->GetMethodID(env, cls, "draw", "()V");
(*env)->CallVoidMethod(env, self.javaRef, method);
[super drawInCGLContext:ctx pixelFormat:pf forLayerTime:t displayTime:ts];
}
@end
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MacOsRedrawerKt_initContainer(JNIEnv *env, jobject redrawer, jobject layer)
{
JAWT awt;
awt.version = JAWT_VERSION_9;
jboolean result = Skiko_GetAWT(env, &awt);
assert(result != JNI_FALSE);
(*env)->GetJavaVM(env, &jvm);
JAWT_DrawingSurface *ds = awt.GetDrawingSurface(env, layer);
assert(ds != NULL);
jint lock = ds->Lock(ds);
assert((lock & JAWT_LOCK_ERROR) == 0);
JAWT_DrawingSurfaceInfo *dsi = ds->GetDrawingSurfaceInfo(ds);
assert(dsi != NULL);
NSObject<JAWT_SurfaceLayers>* dsi_mac = (__bridge NSObject<JAWT_SurfaceLayers> *) dsi->platformInfo;
CALayer *container = [dsi_mac windowLayer];
[container removeAllAnimations];
[container setAutoresizingMask: (kCALayerWidthSizable|kCALayerHeightSizable)];
[container setNeedsDisplayOnBoundsChange: YES];
ds->FreeDrawingSurfaceInfo(dsi);
ds->Unlock(ds);
awt.FreeDrawingSurface(ds);
return (jlong) container;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MacOsRedrawerKt_setContentScale(JNIEnv *env, jobject obj, jlong layerPtr, jfloat contentScale)
{
CALayer *layer = (CALayer *) layerPtr;
assert(contentScale != 0);
layer.contentsScale = contentScale;
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MacOsRedrawerKt_initAWTGLLayer(JNIEnv *env, jobject obj, jlong containerPtr, jobject layer, jboolean setNeedsDisplayOnBoundsChange)
{
CALayer *container = (CALayer *) containerPtr;
AWTGLLayer *glLayer = [AWTGLLayer new];
glLayer.javaRef = (*env)->NewGlobalRef(env, layer);
[glLayer setNeedsDisplayOnBoundsChange: setNeedsDisplayOnBoundsChange];
[container addSublayer: glLayer];
return (jlong) glLayer;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MacOsRedrawerKt_disposeAWTGLLayer(JNIEnv *env, jobject obj, jlong ptr)
{
AWTGLLayer *glLayer = (AWTGLLayer *) ptr;
[glLayer removeFromSuperlayer];
(*env)->DeleteGlobalRef(env, glLayer.javaRef);
[glLayer release];
}
JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_redrawer_AWTGLLayer_isAsynchronous(JNIEnv *env, jobject obj, jlong ptr)
{
AWTGLLayer *glLayer = (AWTGLLayer *) ptr;
return glLayer.isAsynchronous;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AWTGLLayer_setAsynchronous(JNIEnv *env, jobject obj, jlong ptr, jboolean isAsynchronous)
{
AWTGLLayer *glLayer = (AWTGLLayer *) ptr;
[glLayer setAsynchronous: isAsynchronous];
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AWTGLLayer_setNeedsDisplayOnMainThread(JNIEnv *env, jobject obj, jlong ptr)
{
AWTGLLayer *glLayer = (AWTGLLayer *) ptr;
[glLayer performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:0 waitUntilDone:NO];
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AWTGLLayer_setFrame(JNIEnv *env, jobject obj, jlong containerPtr, jlong ptr, jfloat x, jfloat y, jfloat width, jfloat height)
{
CALayer *container = (AWTGLLayer *) containerPtr;
AWTGLLayer *glLayer = (AWTGLLayer *) ptr;
y = (int)container.frame.size.height - y - height;
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions]; // disable animations
glLayer.frame = CGRectMake(x, y, width, height);
[CATransaction commit];
}
\ 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