All Downloads are FREE. Search and download functionalities are using the official Maven repository.

linuxMain.korlibs.render.X11GameWindow.kt Maven / Gradle / Ivy

There is a newer version: 4.0.10
Show newest version
package korlibs.render

import X11Embed.*
import korlibs.event.*
import korlibs.graphics.gl.*
import korlibs.image.bitmap.*
import korlibs.io.lang.*
import korlibs.kgl.*
import korlibs.memory.*
import korlibs.memory.dyn.*
import korlibs.time.*
import kotlinx.cinterop.*
import platform.posix.*

internal object X11 : DynamicLibrary("libX11") {
    val XDefaultScreen by func<(d: CDisplayPointer) -> Int>()
    val XRootWindow by func<(d: CDisplayPointer, scr: Int) -> Window>()
    fun XBlackPixel(d: CDisplayPointer, scr: Int): Int = 0
    fun XWhitePixel(d: CDisplayPointer, scr: Int): Int = -1
    //val XBlackPixel by func<(d: CDisplayPointer, scr: Int) -> Int>()
    //val XWhitePixel by func<(d: CDisplayPointer, scr: Int) -> Int>()
    val XStoreName by func<(d: CDisplayPointer, w: Window, title: CString) -> Unit>()
    val XSetIconName by func<(d: CDisplayPointer, w: Window, title: CString) -> Unit>()
    val XDestroyWindow by func<(d: CDisplayPointer, w: Window) -> Unit>()
    val XCloseDisplay by func<(d: CDisplayPointer) -> Unit>()
    val XFlush by func<(d: CDisplayPointer) -> Unit>()
    val XInternAtom by func<(d: CDisplayPointer, name: CString, p3: Int) -> Atom>()
    val XOpenDisplay by func<(name: CString?) -> CDisplayPointer>()
    val XResourceManagerString by func<(display: CDisplayPointer) -> CString?>()
    val XDefaultRootWindow by func<(d: CDisplayPointer) -> Window>()
    val XDisplayWidth by func<(d: CDisplayPointer, scr: Int) -> Int>()
    val XDisplayHeight by func<(d: CDisplayPointer, scr: Int) -> Int>()
    val XSelectInput by func<(d: CDisplayPointer, w: Window, mask: Int) -> Unit>()
    val XMapWindow by func<(d: CDisplayPointer, w: Window) -> Unit>()
    val XSetWMProtocols by func<(d: CDisplayPointer, w: Window, array: CPointer, count: Int) -> Unit>()
    val XPending by func<(d: CDisplayPointer) -> Int>()
    val XCreateSimpleWindow by func<(d: CDisplayPointer, parent: Window, x: Int, y: Int, width: UInt, height: UInt, border_width: UInt, border: Int, background: Int) -> Window>()
    val XNextEvent by func<(d: CDisplayPointer, event: CPointer) -> Unit>()
    val XSendEvent by func<(d: CDisplayPointer, w: Window, propagate: Int, event_mask: Int, event_send: CPointer) -> Status>()
    val XLookupKeysym by func<(event: CPointer, index: Int) -> KeySym>()
    val XChangeProperty by func<(display: CDisplayPointer, w: Window, property: Atom, type: Atom, format: Int, mode: Int, data: CPointer, nelements: Int) -> Unit>()
    val XDeleteProperty by func<(display: CDisplayPointer, w: Window, property: Atom) -> Unit>()

    val XOpenIM by func<(COpaquePointer?, Int, Int, Int) -> COpaquePointer?>()
    val XCreateIC by func<(COpaquePointer?, COpaquePointer?, COpaquePointer?, COpaquePointer?, COpaquePointer?, COpaquePointer?, COpaquePointer?, COpaquePointer?) -> COpaquePointer?>()
    val XSetICFocus by func<(COpaquePointer?) -> Int>()
    val XFilterEvent by func<(event: CPointer, type: Int) -> Int>()

    //val XLookupString by func<(event_struct: CPointer, buffer_return: CPointer, bytes_buffer: Int, keysym_return: CPointer?, status_in_out: COpaquePointer?) -> Int>()
    //int Xutf8LookupString(XIC ic, XKeyPressedEvent *event, char *buffer_return, int bytes_buffer, KeySym *keysym_return, Status *status_return);
    val Xutf8LookupString by func<(ic: COpaquePointer?, event: CPointer?, buffer_return: CPointer?, bytes_buffer: Int, keysym_return: CPointer?, status_return: CPointer?) -> Int>()
    }

const val None = 0

const val XBufferOverflow =		-1
const val XLookupNone =		1
const val XLookupChars =		2
const val XLookupKeySym =		3
const val XLookupBoth =		4

const val XIMPreeditArea =		0x0001
const val XIMPreeditCallbacks =	0x0002
const val XIMPreeditPosition =	0x0004
const val XIMPreeditNothing =	0x0008
const val XIMPreeditNone =		0x0010
const val XIMStatusArea =		0x0100
const val XIMStatusCallbacks =	0x0200
const val XIMStatusNothing =	0x0400
const val XIMStatusNone =		0x0800


//actual fun CreateDefaultGameWindow(): GameWindow = glutGameWindow
actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow {
    val engine = korlibs.io.lang.Environment["KORGW_NATIVE_ENGINE"]
        ?: "default"
    println("Engine: $engine")
    return when (engine) {
        "sdl" -> SdlGameWindowNative()
        else -> X11GameWindow()
    }
}

// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_swap_control.txt
private val swapIntervalEXT by GLFuncNull<(CPointer?, GLXDrawable, Int) -> Unit>("swapIntervalEXT")

// https://www.khronos.org/opengl/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX)
class X11OpenglContext(val d: CPointer?, val w: Window, val doubleBuffered: Boolean = true) {
    companion object {
        fun chooseVisuals(d: CPointer?, scr: Int = X11.XDefaultScreen(d)): CPointer? {
            val GLX_END = 0
            val attrsList = listOf(
                intArrayOf(GLX_RGBA, GLX_DOUBLEBUFFER, GLX_DEPTH_SIZE, 24, GLX_STENCIL_SIZE, 8, GLX_END),
                intArrayOf(GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_STENCIL_SIZE, 8, GLX_END),
                intArrayOf(GLX_RGBA, GLX_DOUBLEBUFFER, GLX_DEPTH_SIZE, 16, GLX_STENCIL_SIZE, 8, GLX_END),
                intArrayOf(GLX_RGBA, GLX_DEPTH_SIZE, 16, GLX_STENCIL_SIZE, 8, GLX_END),
                intArrayOf(GLX_RGBA, GLX_DOUBLEBUFFER, GLX_END),
                intArrayOf(GLX_RGBA, GLX_END),
                intArrayOf(GLX_END)
            )
            for (attrs in attrsList) {
                attrs.usePinned {
                    println("Trying glXChooseVisual[d=$d, scr=$scr]: ${attrs.toList()}")
                    fflush(stdout)
                    println("Trying glXChooseVisual[${GLLib.glXChooseVisual}][d=$d, scr=$scr]: ${attrs.toList()}")
                    fflush(stdout)
                    val res = GLLib.glXChooseVisual(d, scr, it.addressOf(0))
                    println(" -> $res")
                    fflush(stdout)
                    if (res != null) return res
                }
            }
            println("VI: null")
            return null
        }
    }
    init {
        println("Creating OpenGL context")
    }
    val vi = chooseVisuals(d, 0)
    init {
        println("VI: $vi")
    }
    val glc = GLLib.glXCreateContext(d, vi, null, 1)

    init {
        println("VI: $vi, d: $d, w: $w, glc: $glc")
        makeCurrent()
        println("GL_VENDOR: " + NativeBaseKmlGl.glGetStringExt(NativeBaseKmlGl.GL_VENDOR)?.toKString())
        println("GL_VERSION: " + NativeBaseKmlGl.glGetStringExt(NativeBaseKmlGl.GL_VERSION)?.toKString())
    }

    fun makeCurrent() {
        GLLib.glXMakeCurrent(d, w, glc)
    }

    fun swapBuffers() {
        GLLib.glXSwapBuffers(d, w)
    }
}

@OptIn(ExperimentalUnsignedTypes::class)
class X11GameWindow : EventLoopGameWindow() {
    override val dialogInterface = ZenityDialogs

    //init { println("X11GameWindow") }
    override val ag: AGOpengl = AGOpengl(korlibs.kgl.KmlGlNative())


    override val pixelsPerInch: Float by TimedCache(0.5.seconds) {
        val str = X11.XResourceManagerString([email protected])?.toKStringFromUtf8() ?: ""
        val Xftdpi = str.lines().firstOrNull { it.contains("Xft.dpi") }
        val dpiInt = Xftdpi?.split(':')?.lastOrNull()?.trim()?.toDoubleOrNull()
        //println("X11AgOpengl.pixelsPerInch: str=$str, dpiInt=$dpiInt")
        kotlin.math.max(dpiInt ?: 96.0, 96.0).toFloat()
    }

    override var width: Int = 200; private set
    override var height: Int = 200; private set
    override var title: String = "Korgw"
        set(value) {
            field = value
            realSetTitle(value)
        }
    override var icon: Bitmap? = null
        set(value) {
            field = value
            realSetIcon(value)
        }
    override var fullscreen: Boolean = false
        set(value) {
            field = value
            realSetFullscreen(value)
        }
    override var visible: Boolean = true
        set(value) {
            field = value
            realSetVisible(value)
        }
    override var quality: Quality = Quality.AUTOMATIC

    override fun setSize(width: Int, height: Int) {
        this.width = width
        this.height = height
    }

    var d: CPointer? = null
    val NilWin: Window = 0UL
    var root: Window = 0UL
    var w: Window = 0UL
    var s: Int = 0

    fun realSetTitle(title: String) {
        if (d == null || w == NilWin) return
        try {
            memScoped {
                //X.XSetWMIconName(d, w, )
                X11.XStoreName(d, w, cstr(title))
                X11.XSetIconName(d, w, cstr(title))
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }

    fun realSetIcon(value: Bitmap?) {
        if (d == null || w == NilWin || value == null) return
        try {
            memScoped {
                val property = X11.XInternAtom(d, cstr("_NET_WM_ICON"), 0)
                val bmp = value.toBMP32()
                val VSIZE = Platform.arch.bits / 8
                val bytes = ByteArray((bmp.area + 2) * VSIZE)
                bytes.write32LE(0, bmp.width)
                bytes.write32LE(VSIZE, bmp.height)
                for (n in 0 until bmp.area) {
                    val pos = VSIZE * (2 + n)
                    val c = bmp.getRgbaAtIndex(n)
                    bytes[pos + 0] = c.r.toByte()
                    bytes[pos + 1] = c.g.toByte()
                    bytes[pos + 2] = c.b.toByte()
                    bytes[pos + 3] = c.a.toByte()
                }
                bytes.usePinned { pin ->
                    val XA_CARDINAL: Atom = 6.convert()
                    X11.XChangeProperty(
                        d, w, property, XA_CARDINAL, 32, PropModeReplace,
                        pin.startAddressOf.reinterpret(), bytes.size / VSIZE
                    )
                }
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }

    private fun ArenaBase.cstr(str: String) = str.cstr.placeTo(this)

    // https://stackoverflow.com/questions/9065669/x11-glx-fullscreen-mode
    fun realSetFullscreen(value: Boolean) {
        println("realSetFullscreen. value=$value")
        if (d == null || w == NilWin) return
        try {
            val fullscreen = value
            memScoped {
                val _NET_WM_STATE = X11.XInternAtom(d, cstr("_NET_WM_STATE"), 1)
                val _NET_WM_STATE_ADD = X11.XInternAtom(d, cstr("_NET_WM_STATE_ADD"), 1)
                val _NET_WM_STATE_REMOVE = X11.XInternAtom(d, cstr("_NET_WM_STATE_REMOVE"), 1)
                val _NET_WM_STATE_FULLSCREEN = X11.XInternAtom(d, cstr("_NET_WM_STATE_FULLSCREEN"), 1)

                //println("realSetFullscreen: wm_state=$_NET_WM_STATE, wm_fullscreen=$_NET_WM_STATE_FULLSCREEN")

                //val attributes = alloc()
                //val CWOverrideRedirect = (1L shl 9)
                //attributes.override_redirect = if (value) 1 else 0
                //XChangeWindowAttributes(d, w, CWOverrideRedirect.convert(), attributes.ptr)

                val isWindowMapped = true
                if (isWindowMapped) {
                    val e = alloc()
                    e.xany.type = ClientMessage
                    e.xclient.message_type = _NET_WM_STATE
                    e.xclient.format = 32
                    e.xclient.window = w
                    e.xclient.data.l[0] = (if (fullscreen) _NET_WM_STATE_ADD else _NET_WM_STATE_REMOVE).convert()
                    e.xclient.data.l[1] = _NET_WM_STATE_FULLSCREEN.convert()
                    e.xclient.data.l[3] = 0.convert()

                    X11.XSendEvent(
                        d,
                        X11.XDefaultRootWindow(d),
                        0.convert(),
                        SubstructureNotifyMask or SubstructureRedirectMask,
                        e.ptr
                    )

                } else {
                    val atoms = allocArray(3)
                    var count = 0
                    if (fullscreen) {
                        atoms[count++] = _NET_WM_STATE_FULLSCREEN
                    }
                    val XA_ATOM: Atom = 4.convert()
                    if (count > 0) {
                        X11.XChangeProperty(d, w, _NET_WM_STATE, XA_ATOM, 32, PropModeReplace, atoms.getPointer(this).reinterpret(), count)
                    } else {
                        X11.XDeleteProperty(d, w, _NET_WM_STATE);
                    }
                }
                X11.XFlush(d)
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }

    fun realSetVisible(value: Boolean) {
        if (d == null || w == NilWin) return
    }

    var xim: COpaquePointer? = null
    var xic: COpaquePointer? = null

    override fun doInitialize() {
        println("doInitialize")

        try {
            memScoped {
                d = X11.XOpenDisplay(null) ?: error("Can't open main display")
                s = X11.XDefaultScreen(d)
                root = X11.XDefaultRootWindow(d)

                //val cmap = XCreateColormap(d, root, vi->visual, AllocNone);
                val screenWidth = X11.XDisplayWidth(d, s)
                val screenHeight = X11.XDisplayHeight(d, s)

                val gameWindow = this@X11GameWindow

                val winX = screenWidth / 2 - width / 2
                val winY = screenHeight / 2 - height / 2

                println("screenWidth: $screenWidth, screenHeight: $screenHeight, winX=$winX, winY=$winY, width=$width, height=$height")

                w = X11.XCreateSimpleWindow(
                    d, X11.XRootWindow(d, s),
                    winX, winY,
                    width.convert(), height.convert(),
                    1.convert(),
                    X11.XBlackPixel(d, s), X11.XWhitePixel(d, s)
                )

                println("XCreateSimpleWindow=$w, d=$d, s=$s")

                val eventMask = (ExposureMask
                    or StructureNotifyMask
                    or EnterWindowMask
                    or LeaveWindowMask
                    or KeyPressMask
                    or KeyReleaseMask
                    or PointerMotionMask
                    or ButtonPressMask
                    or ButtonReleaseMask
                    or ButtonMotionMask
                    )

                X11.XSelectInput(d, w, eventMask)
                X11.XMapWindow(d, w)
                realSetIcon(icon)
                realSetVisible(fullscreen)
                realSetVisible(visible)
                realSetTitle(title)

                ctx = X11OpenglContext(d, w, doubleBuffered = doubleBuffered)
                ctx.makeCurrent()

                val wmDeleteMessage = X11.XInternAtom(d, cstr("WM_DELETE_WINDOW"), 0)
                memScoped {
                    val protocolsArray = allocArray(1)
                    protocolsArray[0] = wmDeleteMessage
                    X11.XSetWMProtocols(d, w, protocolsArray, 1)
                }


                xim = X11.XOpenIM(d, 0, 0, 0)
                xic = X11.XCreateIC(
                    xim,
                    "inputStyle".cstr.getPointer(mem), (XIMPreeditNothing or XIMStatusNothing).toLong().toCPointer(),
                    "clientWindow".cstr.getPointer(mem), w.toLong().toCPointer(),
                    "focusWindow".cstr.getPointer(mem), w.toLong().toCPointer(),
                    null
                )
                X11.XSetICFocus(xic)

                //println("/doInitialize")
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }

    lateinit var ctx: X11OpenglContext

    val doubleBuffered = false
    val mem = Arena()
    val composeStatus: CPointer = mem.allocArray(16)
    //val doubleBuffered = true

    override fun close(exitCode: Int) {
        //println("close: $exitCode")
        super.close(exitCode)
    }

    private val linuxJoyEventAdapter = korlibs.render.x11.LinuxJoyEventAdapter()

    override fun doHandleEvents() = memScoped {
        linuxJoyEventAdapter.updateGamepads([email protected])
        //println("doHandleEvents")
        val e = alloc()
        val TEMP_SIZE = 64
        val temp = allocArray(TEMP_SIZE)
        val status = alloc()
        loop@ while (running) {
            //println("---")
            if (X11.XPending(d) == 0) return
            X11.XNextEvent(d, e.ptr)
            if (X11.XFilterEvent(e.ptr, None) != 0) continue
            //println("EVENT: ${e.type}")
            when (e.type) {
                Expose -> if (e.xexpose.count == 0) render(doUpdate = false)
                ClientMessage, DestroyNotify -> close()
                ConfigureNotify -> {
                    render(doUpdate = false) {
                        val conf = e.xconfigure
                        width = conf.width
                        height = conf.height
                        //dispatchReshapeEvent(conf.x, conf.y, conf.width, conf.height)
                        dispatchReshapeEvent(0, 0, conf.width, conf.height)
                        !doubleBuffered
                    }
                    //println("RESIZED! ${conf.width} ${conf.height}")
                }
                KeyPress, KeyRelease -> {
                    val pressing = e.type == KeyPress
                    val ev =
                        if (pressing) korlibs.event.KeyEvent.Type.DOWN else korlibs.event.KeyEvent.Type.UP
                    val keyCode = e.xkey.keycode.toInt()
                    val kkey = XK_KeyMap[X11.XLookupKeysym(e.xkey.ptr, 0).toInt()] ?: Key.UNKNOWN

                    //println("KEY: $ev, ${keyCode.toChar()}, $kkey, $keyCode, keySym=$kkey")
                    dispatchKeyEvent(ev, 0, keyCode.toInt().toChar(), kkey, keyCode.toInt().convert())

                    if (pressing) {
                        // https://gist.github.com/baines/5a49f1334281b2685af5dcae81a6fa8a
                        val result = X11.Xutf8LookupString(xic, e.xkey.ptr, temp, TEMP_SIZE - 1, null, status.ptr)
                        val str = temp.toKStringFromUtf8()
                        //println("pressing=$pressing, result=$result, temp=${temp.toKStringFromUtf8()}, status.value=${status.value}")
                        when (status.value) {
                            XLookupChars, XLookupBoth -> {
                                dispatchKeyEvent(KeyEvent.Type.TYPE, 0, str.getOrElse(0) { '\u0000' }, kkey, keyCode.toInt().convert(), str)
                            }
                        }
                    }
                //break@loop
                }
                MotionNotify, ButtonPress, ButtonRelease -> {
                    val mot = e.xmotion
                    val but = e.xbutton
                    val ev = when (e.type) {
                        MotionNotify -> MouseEvent.Type.MOVE
                        ButtonPress -> MouseEvent.Type.DOWN
                        ButtonRelease -> MouseEvent.Type.UP
                        else -> MouseEvent.Type.MOVE
                    }
                    val button = when (but.button.toInt()) {
                        1 -> MouseButton.LEFT
                        2 -> MouseButton.MIDDLE
                        3 -> MouseButton.RIGHT
                        // http://who-t.blogspot.com/2011/09/whats-new-in-xi-21-smooth-scrolling.html
                        4 -> MouseButton.BUTTON4 // WHEEL_UP!
                        5 -> MouseButton.BUTTON5 // WHEEL_DOWN!
                        6 -> MouseButton.BUTTON6 // WHEEL_LEFT!
                        7 -> MouseButton.BUTTON7 // WHEEL_RIGHT!
                        else -> MouseButton.BUTTON_UNKNOWN
                    }
                    //println(XMotionEvent().size())
                    //println(mot.size)
                    //println("MOUSE ${ev} ${mot.x} ${mot.y} ${mot.button}")

                    dispatchSimpleMouseEvent(ev, 0, mot.x, mot.y, button, simulateClickOnUp = false)
                }
                else -> {
                    //println("OTHER EVENT ${e.type}")
                }
            }
        }
    }

    override fun doInitRender() {
        try {
            ctx.makeCurrent()
            NativeBaseKmlGl.glViewportExt(0, 0, width, height)
            NativeBaseKmlGl.glClearColorExt(.3f, .6f, .3f, 1f)
            NativeBaseKmlGl.glClearExt(NativeBaseKmlGl.GL_COLOR_BUFFER_BIT)

            // https://github.com/spurious/SDL-mirror/blob/4c1c6d03ddaa3095b3c63c38ddd0a6cfad58b752/src/video/windows/SDL_windowsopengl.c#L439-L447
            val dpy = GLLib.glXGetCurrentDisplay()
            val drawable = GLLib.glXGetCurrentDrawable()
            swapIntervalEXT?.invoke(dpy, drawable, vsync.toInt())
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }

    override fun doSwapBuffers() {
        ctx.swapBuffers()
    }

    override fun doDestroy() {
        X11.XDestroyWindow(d, w)
        X11.XCloseDisplay(d)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy