diff --git a/buildSrc/src/main/kotlin/korlibs/root/RootKorlibsPlugin.kt b/buildSrc/src/main/kotlin/korlibs/root/RootKorlibsPlugin.kt index 44f0fb8e85..babec56d67 100644 --- a/buildSrc/src/main/kotlin/korlibs/root/RootKorlibsPlugin.kt +++ b/buildSrc/src/main/kotlin/korlibs/root/RootKorlibsPlugin.kt @@ -809,6 +809,7 @@ val Project.isSample: Boolean get() = project.path.startsWith(":samples:") || pr fun Project.mustAutoconfigureKMM(): Boolean = !project.name.startsWith("korge-gradle-plugin") && project.name != "korge-reload-agent" && + project.name != "korge-ipc" && project.name != "korge-benchmarks" && project.hasBuildGradle() diff --git a/korge-ipc/.gitignore b/korge-ipc/.gitignore new file mode 100644 index 0000000000..616e7c1ef1 --- /dev/null +++ b/korge-ipc/.gitignore @@ -0,0 +1,5 @@ +/build +/.gradle +/.idea +/out +/bin diff --git a/korge-ipc/build.gradle.kts b/korge-ipc/build.gradle.kts new file mode 100644 index 0000000000..d2c64a14db --- /dev/null +++ b/korge-ipc/build.gradle.kts @@ -0,0 +1,62 @@ +import korlibs.korge.gradle.targets.android.* +import korlibs.root.* + +plugins { + //id "kotlin" version "1.6.21" + id("kotlin") + //id "org.jetbrains.kotlin.jvm" + id("maven-publish") +} + +description = "Multiplatform Game Engine written in Kotlin" +group = RootKorlibsPlugin.KORGE_RELOAD_AGENT_GROUP + +val jversion = GRADLE_JAVA_VERSION_STR + +java { + setSourceCompatibility(jversion) + setTargetCompatibility(jversion) +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all { + kotlinOptions { + jvmTarget = jversion + apiVersion = "1.8" + languageVersion = "1.8" + suppressWarnings = true + } +} + +publishing { + publications { + val maven by creating(MavenPublication::class) { + groupId = group.toString() + artifactId = "korge-ipc" + version = version + from(components["kotlin"]) + } + } +} + +val publishJvmPublicationToMavenLocal = tasks.register("publishJvmPublicationToMavenLocal", Task::class) { + group = "publishing" + dependsOn("publishMavenPublicationToMavenLocal") +} + +afterEvaluate { + if (tasks.findByName("publishMavenPublicationToMavenRepository") != null) { + tasks.register("publishJvmPublicationToMavenRepository", Task::class) { + group = "publishing" + dependsOn("publishMavenPublicationToMavenRepository") + } + } +} + +korlibs.NativeTools.groovyConfigurePublishing(project, false) +korlibs.NativeTools.groovyConfigureSigning(project) + +dependencies { + testImplementation(libs.bundles.kotlin.test) +} + +tasks { val jvmTest by creating { dependsOn("test") } } diff --git a/korge-ipc/src/main/kotlin/korlibs/korge/ipc/KorgeIPC.kt b/korge-ipc/src/main/kotlin/korlibs/korge/ipc/KorgeIPC.kt new file mode 100644 index 0000000000..10e051356b --- /dev/null +++ b/korge-ipc/src/main/kotlin/korlibs/korge/ipc/KorgeIPC.kt @@ -0,0 +1,176 @@ +package korlibs.korge.ipc + +import java.io.* +import java.nio.* +import java.nio.channels.* +import java.nio.file.* +import kotlin.reflect.* + +class KorgeIPC(val path: String = System.getenv("KORGE_IPC") ?: DEFAULT_PATH) { + init { + println("KorgeIPC:$path") + } + + companion object { + val DEFAULT_PATH = "/tmp/KORGE_IPC" + } + val frame = KorgeFrameBuffer("$path.frame") + val events = KorgeEventsBuffer("$path.events") + + val availableEvents get() = events.availableRead + fun writeEvent(e: IPCEvent) = events.writeEvent(e) + fun readEvent(e: IPCEvent = IPCEvent()): IPCEvent? = events.readEvent(e) + fun setFrame(f: IPCFrame) = frame.setFrame(f) + fun getFrame(): IPCFrame = frame.getFrame() + fun getFrameId(): Int = frame.getFrameId() +} + +data class IPCEvent( + var timestamp: Long = System.currentTimeMillis(), + var type: Int = 0, + var p0: Int = 0, + var p1: Int = 0, + var p2: Int = 0, + var p3: Int = 0, +) { + fun setNow(): IPCEvent { + timestamp = System.currentTimeMillis() + return this + } + + companion object { + val RESIZE = 1 + + val MOUSE_MOVE = 10 + val MOUSE_DOWN = 11 + val MOUSE_UP = 12 + val MOUSE_CLICK = 13 + + val KEY_DOWN = 20 + val KEY_UP = 21 + val KEY_TYPE = 22 + } +} + +class KorgeEventsBuffer(val path: String) { + val channel = FileChannel.open(Path.of(path), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE) + val HEAD_SIZE = 32 + val EVENT_SIZE = 32 + val MAX_EVENTS = 4096 + var buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0L, (HEAD_SIZE + EVENT_SIZE * MAX_EVENTS).toLong()) + + init { + File(path).deleteOnExit() + } + + var readPos: Long by DelegateBufferLong(buffer, 0) + var writePos: Long by DelegateBufferLong(buffer, 8) + + + fun eventOffset(index: Long): Int = 32 + ((index % MAX_EVENTS).toInt() * 32) + + fun readEvent(index: Long, e: IPCEvent = IPCEvent()): IPCEvent { + val pos = eventOffset(index) + e.timestamp = buffer.getLong(pos + 0) + e.type = buffer.getInt(pos + 8) + e.p0 = buffer.getInt(pos + 12) + e.p1 = buffer.getInt(pos + 16) + e.p2 = buffer.getInt(pos + 20) + e.p3 = buffer.getInt(pos + 24) + return e + } + + fun writeEvent(index: Long, e: IPCEvent) { + val pos = eventOffset(index) + buffer.putLong(pos + 0, e.timestamp) + buffer.putInt(pos + 8, e.type) + buffer.putInt(pos + 12, e.p0) + buffer.putInt(pos + 16, e.p1) + buffer.putInt(pos + 20, e.p2) + buffer.putInt(pos + 24, e.p3) + } + + fun reset() { + readPos = 0L + writePos = 0L + } + + val availableRead: Int get() = (writePos - readPos).toInt() + val availableWriteWithoutOverflow: Int get() = MAX_EVENTS - availableRead + + fun writeEvent(e: IPCEvent) { + //println("EVENT: $e") + writeEvent(writePos++, e) + } + + fun readEvent(e: IPCEvent = IPCEvent()): IPCEvent? { + if (readPos >= writePos) return null + return readEvent(readPos++, e) + } + + fun close() { + channel.close() + } + + class DelegateBufferLong(val buffer: ByteBuffer, val index: Int) { + operator fun getValue(obj: Any, property: KProperty<*>): Long = buffer.getLong(index) + operator fun setValue(obj: Any, property: KProperty<*>, value: Long) { buffer.putLong(index, value) } + } + + class DelegateBufferInt(val buffer: ByteBuffer, val index: Int) { + operator fun getValue(obj: Any, property: KProperty<*>): Int = buffer.getInt(index) + operator fun setValue(obj: Any, property: KProperty<*>, value: Int) { buffer.putInt(index, value) } + } +} + +class IPCFrame(val id: Int, val width: Int, val height: Int, val pixels: IntArray = IntArray(width * height)) + +class KorgeFrameBuffer(val path: String) { + val channel = FileChannel.open(Path.of(path), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE) + var width: Int = 0 + var height: Int = 0 + var buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0L, 16 + 0) + var ibuffer = buffer.asIntBuffer() + + init { + File(path).deleteOnExit() + } + + fun ensureSize(width: Int, height: Int) { + if (this.width < width || this.height < height) { + this.width = width + this.height = height + buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0L, (16 + (width * height * 4)).toLong()) + ibuffer = buffer.asIntBuffer() + } + } + + fun setFrame(frame: IPCFrame) { + ensureSize(frame.width, frame.height) + ibuffer.clear() + ibuffer.put(frame.id) + ibuffer.put(frame.width) + ibuffer.put(frame.height) + ibuffer.put(frame.pixels) + } + + fun getFrameId(): Int { + ibuffer.clear() + return ibuffer.get() + } + + fun getFrame(): IPCFrame { + ibuffer.clear() + val id = ibuffer.get() + val width = ibuffer.get() + val height = ibuffer.get() + ensureSize(width, height) + val pixels = IntArray(width * height) + ibuffer.get(pixels) + return IPCFrame(id, width, height, pixels) + } + + fun close() { + channel.close() + } +} diff --git a/korge-ipc/src/main/kotlin/korlibs/korge/ipc/KorgeIPCJPanel.kt b/korge-ipc/src/main/kotlin/korlibs/korge/ipc/KorgeIPCJPanel.kt new file mode 100644 index 0000000000..4209bcc72b --- /dev/null +++ b/korge-ipc/src/main/kotlin/korlibs/korge/ipc/KorgeIPCJPanel.kt @@ -0,0 +1,81 @@ +package korlibs.korge.ipc + +import java.awt.* +import java.awt.event.* +import java.awt.image.* +import javax.swing.* + +class KorgeIPCJPanel(val ipc: KorgeIPC = KorgeIPC()) : JPanel(), MouseListener, MouseMotionListener, MouseWheelListener, KeyListener { + var image: BufferedImage? = null + + init { + Timer(16) { + readFrame() + //println(events.availableRead) + }.also { it.isRepeats = true }.start() + } + + override fun paint(g: Graphics) { + //g.color = Color.RED + //g.fillRect(0, 0, 100, 100) + if (image != null) { + g.drawImage(image, 0, 0, width, height, null) + } + } + + fun rgbaToBgra(v: Int): Int = ((v shl 16) and 0x00FF0000) or ((v shr 16) and 0x000000FF) or (v and 0xFF00FF00.toInt()) + + var currentFrameId = -1 + + fun readFrame() { + val frameId = ipc.getFrameId() + if (frameId == currentFrameId) return // Do not update + val frame = ipc.getFrame() + if (frame.width == 0 || frame.height == 0) return // Empty frame + currentFrameId = frame.id + val image = BufferedImage(frame.width, frame.height, BufferedImage.TYPE_INT_ARGB) + this.image = image + val imgPixels = (image.raster.dataBuffer as DataBufferInt).data + System.arraycopy(frame.pixels, 0, imgPixels, 0, frame.width * frame.height) + for (n in imgPixels.indices) imgPixels[n] = rgbaToBgra(imgPixels[n]) + repaint() + } + + private fun sendEv(type: Int, e: KeyEvent) = ipc.writeEvent(IPCEvent(type = type, p0 = e.keyCode, p1 = e.keyChar.code)) + private fun sendEv(type: Int, e: MouseEvent) = ipc.writeEvent(IPCEvent(type = type, p0 = e.x, p1 = e.y, p2 = e.button)) + override fun keyTyped(e: KeyEvent) = sendEv(IPCEvent.KEY_TYPE, e) + override fun keyPressed(e: KeyEvent) = sendEv(IPCEvent.KEY_DOWN, e) + override fun keyReleased(e: KeyEvent) = sendEv(IPCEvent.KEY_UP, e) + override fun mouseMoved(e: MouseEvent) = sendEv(IPCEvent.MOUSE_MOVE, e) + override fun mouseDragged(e: MouseEvent) = sendEv(IPCEvent.MOUSE_MOVE, e) + override fun mouseWheelMoved(e: MouseWheelEvent) = sendEv(IPCEvent.MOUSE_MOVE, e) + override fun mouseExited(e: MouseEvent) = sendEv(IPCEvent.MOUSE_MOVE, e) + override fun mouseEntered(e: MouseEvent) = sendEv(IPCEvent.MOUSE_MOVE, e) + override fun mouseReleased(e: MouseEvent) = sendEv(IPCEvent.MOUSE_UP, e) + override fun mousePressed(e: MouseEvent) = sendEv(IPCEvent.MOUSE_DOWN, e) + override fun mouseClicked(e: MouseEvent) = sendEv(IPCEvent.MOUSE_CLICK, e) + + init { + addKeyListener(this) + addMouseListener(this) + addMouseMotionListener(this) + addMouseWheelListener(this) + } + + companion object { + @JvmStatic + fun main() { + val frame = JFrame() + val frameHolder = korlibs.korge.ipc.KorgeIPCJPanel() + frame.add(frameHolder) + frame.addKeyListener(frameHolder) + + frame.preferredSize = Dimension(640, 480) + frame.pack() + frame.setLocationRelativeTo(null) + + frame.isVisible = true + + } + } +} diff --git a/korge/build.gradle.kts b/korge/build.gradle.kts index 38bb83835a..1eb325ac05 100644 --- a/korge/build.gradle.kts +++ b/korge/build.gradle.kts @@ -12,4 +12,5 @@ project.extensions.extraProperties.properties.apply { dependencies { commonMainApi(project(":korge-core")) + jvmMainApi(project(":korge-ipc")) } diff --git a/korge/resources/META-INF/services/korlibs.korge.ViewsCompleter b/korge/resources/META-INF/services/korlibs.korge.ViewsCompleter index a4cd423559..915c252010 100644 --- a/korge/resources/META-INF/services/korlibs.korge.ViewsCompleter +++ b/korge/resources/META-INF/services/korlibs.korge.ViewsCompleter @@ -1 +1,2 @@ korlibs.korge.StandardViewsCompleter +korlibs.korge.IPCViewsCompleter diff --git a/korge/src/korlibs/korge/Korge.kt b/korge/src/korlibs/korge/Korge.kt index 598a810bee..f22b410a3f 100644 --- a/korge/src/korlibs/korge/Korge.kt +++ b/korge/src/korlibs/korge/Korge.kt @@ -35,10 +35,6 @@ import kotlin.time.* typealias KorgeConfig = Korge -suspend fun test() = Korge { - -} - data class KorgeDisplayMode(val scaleMode: ScaleMode, val scaleAnchor: Anchor, val clipBorders: Boolean) { companion object { val DEFAULT get() = CENTER diff --git a/korge/src@jvm/korlibs/korge/KorgeExtJvm.kt b/korge/src@jvm/korlibs/korge/KorgeExtJvm.kt index 72abce5e40..022f37a804 100644 --- a/korge/src@jvm/korlibs/korge/KorgeExtJvm.kt +++ b/korge/src@jvm/korlibs/korge/KorgeExtJvm.kt @@ -1,10 +1,13 @@ package korlibs.korge +import korlibs.event.* +import korlibs.graphics.* import korlibs.image.bitmap.* import korlibs.image.color.* import korlibs.time.* import korlibs.korge.awt.* import korlibs.korge.awt.views +import korlibs.korge.ipc.* import korlibs.korge.render.* import korlibs.korge.time.* import korlibs.korge.view.* @@ -56,6 +59,44 @@ class StandardViewsCompleter : ViewsCompleter { } } +class IPCViewsCompleter : ViewsCompleter { + override fun completeViews(views: Views) { + val korgeIPC = System.getenv("KORGE_IPC") + if (korgeIPC != null) { + val ipc = KorgeIPC(korgeIPC) + + views.onBeforeRender { + while (ipc.availableEvents > 0) { + val e = ipc.readEvent() ?: break + if (e.timestamp < System.currentTimeMillis() - 100) continue + + when (e.type) { + IPCEvent.KEY_DOWN, IPCEvent.KEY_UP -> { + views.dispatch( + KeyEvent(when (e.type) { + IPCEvent.KEY_DOWN -> KeyEvent.Type.DOWN + IPCEvent.KEY_UP -> KeyEvent.Type.UP + else -> KeyEvent.Type.DOWN + }, key = awtKeyCodeToKey(e.p0) + ) + ) + } + else -> { + println(e) + } + } + } + } + + views.onAfterRender { + val bmp = it.ag.readColor(it.currentFrameBuffer) + //channel.trySend(bmp) + ipc.setFrame(IPCFrame(System.currentTimeMillis().toInt(), bmp.width, bmp.height, bmp.ints)) + } + } + } +} + internal actual fun completeViews(views: Views) { for (completer in ServiceLoader.load(ViewsCompleter::class.java).toList()) { completer.completeViews(views) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3c0526560f..772055f58f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,6 +31,7 @@ include(":korge-gradle-plugin") include(":korge-gradle-plugin-common") include(":korge-gradle-plugin-settings") include(":korge-reload-agent") +include(":korge-ipc") if (System.getenv("DISABLE_SANDBOX") != "true") { include(":korge-sandbox") }