From 86a247df80883637de7bcc6a61b466ae728f6387 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 13:16:46 +0200 Subject: [PATCH 01/66] Moved WindowsRegistry to korge-core --- .../src/common/korlibs/io/file/registry/WindowsRegistry.kt | 0 .../src/common/korlibs/io/file/registry/WindowsRegistryVfs.kt | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {korge => korge-core}/src/common/korlibs/io/file/registry/WindowsRegistry.kt (100%) rename {korge => korge-core}/src/common/korlibs/io/file/registry/WindowsRegistryVfs.kt (100%) diff --git a/korge/src/common/korlibs/io/file/registry/WindowsRegistry.kt b/korge-core/src/common/korlibs/io/file/registry/WindowsRegistry.kt similarity index 100% rename from korge/src/common/korlibs/io/file/registry/WindowsRegistry.kt rename to korge-core/src/common/korlibs/io/file/registry/WindowsRegistry.kt diff --git a/korge/src/common/korlibs/io/file/registry/WindowsRegistryVfs.kt b/korge-core/src/common/korlibs/io/file/registry/WindowsRegistryVfs.kt similarity index 100% rename from korge/src/common/korlibs/io/file/registry/WindowsRegistryVfs.kt rename to korge-core/src/common/korlibs/io/file/registry/WindowsRegistryVfs.kt From c83d9eb422851501f9269004ee85ee4bb59b2b02 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 16:24:40 +0200 Subject: [PATCH 02/66] [WIP] New Game Window (not working yet) --- .../korge => archive}/GLCanvasKorge.kt | 19 + .../render => archive}/osx/MacosGameWindow.kt | 55 +- .../render => archive}/sdl2/SDLKeyCode.kt | 0 .../render => archive}/sdl2/SDLKeycodeMap.kt | 0 .../render => archive}/sdl2/SDLScancode.kt | 0 .../sdl2/SdlGameWindowJvm.kt | 0 .../render => archive}/sdl2/jna/ISDL.kt | 0 .../render => archive}/sdl2/jna/SDL_Enums.kt | 0 .../render => archive}/sdl2/jna/SDL_Events.kt | 0 .../sdl2/jna/SDL_Structs.kt | 0 .../win32/Win32GameWindow.kt | 0 archive/win32/Win32KeyMap.kt | 401 +++++++++ .../x11/X11GameWindowJvm.kt | 0 archive/x11/X11KeyMap.kt | 507 ++++++++++++ .../annotations/DeprecatedParameter.kt | 6 + .../datastructure/_Datastructure_event.kt | 19 +- .../src/common/korlibs/logger/Logger.kt | 13 +- .../src/jvm/korlibs/memory/dyn/NativeLoad.kt | 16 + .../src/jvm/korlibs/memory/dyn/osx/Cocoa.kt | 12 - .../commonMain/kotlin/samples/MainHaptic.kt | 8 +- .../src/jvmMain/kotlin/AwtSandboxSample.kt | 12 +- .../common/korlibs/event/TextInputManager.kt | 12 - korge/src/common/korlibs/korge/Korge.kt | 485 +++-------- .../korlibs/korge/KorgeViewsConfigureInput.kt | 280 +++++++ .../common/korlibs/korge/input/MouseEvents.kt | 4 +- .../korlibs/korge/render/RenderContext.kt | 2 +- .../korge/service/haptic/HapticFeedback.kt | 12 +- .../korlibs/korge/tests/ViewsForTesting.kt | 36 +- .../korlibs/korge/text/TextEditController.kt | 2 +- korge/src/common/korlibs/korge/ui/UIButton.kt | 2 +- .../korlibs/korge/ui/UIEditableNumber.kt | 2 +- korge/src/common/korlibs/korge/ui/UIWindow.kt | 10 +- korge/src/common/korlibs/korge/view/Views.kt | 13 +- .../common/korlibs/render/DialogInterface.kt | 10 +- ...her.kt => EventLoopCoroutineDispatcher.kt} | 17 +- korge/src/common/korlibs/render/GameWindow.kt | 766 ++---------------- .../render/GameWindowCoroutineDispatcher.kt | 166 ---- .../common/korlibs/render/GameWindowExt.kt | 400 +++++++++ korge/src/common/korlibs/render/Korgw.kt | 6 - .../korlibs/render/DefaultGameWindowIos.kt | 37 +- .../korlibs/render/DefaultGameWindowJvm.kt | 96 +-- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 3 +- .../jvm/korlibs/render/awt/AwtFrameTools.kt | 73 +- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 2 + .../korlibs/render/awt/BaseAwtGameWindow.kt | 28 +- korge/src/jvm/korlibs/render/awt/GLCanvas.kt | 5 +- .../korlibs/render/awt/GLCanvasGameWindow.kt | 19 - .../korlibs/render/awt/NewAwtGameWindow.kt | 58 +- korge/src/jvm/korlibs/render/osx/Cocoa.kt | 3 +- .../osx/DarwinMacosGameControllerJvm.kt | 11 +- .../render/osx/DialogInterfaceJvmCocoa.kt | 7 - korge/src/jvm/korlibs/render/osx/MacGL.kt | 53 ++ .../jvm/korlibs/render/osx/MacosGLContext.kt | 8 +- .../korlibs/render/platform/PlatformTools.kt | 8 - korge/src/jvm/korlibs/render/win32/Win32.kt | 408 +--------- .../jvm/korlibs/render/win32/Win32Tools.kt | 18 +- .../jvm/korlibs/render/x11/X11Constants.kt | 509 +----------- .../korlibs/render/x11/X11OpenglContext.kt | 106 +-- .../common/korlibs/korge/KorgeHeadlessTest.kt | 2 +- .../common/korlibs/render/GameWindowTest.kt | 2 +- .../SyncEventLoopCoroutineDispatcherTest.kt | 6 +- korge/test/jvm/korlibs/korge/awt/AwtSample.kt | 5 +- korge/test/jvm/korlibs/render/TestE2eJava.kt | 2 + .../korlibs/render/awt/AwtGameWindowTest.kt | 35 +- 64 files changed, 2145 insertions(+), 2652 deletions(-) rename {korge/src/jvm/korlibs/korge => archive}/GLCanvasKorge.kt (87%) rename {korge/src/jvm/korlibs/render => archive}/osx/MacosGameWindow.kt (88%) rename {korge/src/common/korlibs/render => archive}/sdl2/SDLKeyCode.kt (100%) rename {korge/src/common/korlibs/render => archive}/sdl2/SDLKeycodeMap.kt (100%) rename {korge/src/common/korlibs/render => archive}/sdl2/SDLScancode.kt (100%) rename {korge/src/jvm/korlibs/render => archive}/sdl2/SdlGameWindowJvm.kt (100%) rename {korge/src/jvm/korlibs/render => archive}/sdl2/jna/ISDL.kt (100%) rename {korge/src/jvm/korlibs/render => archive}/sdl2/jna/SDL_Enums.kt (100%) rename {korge/src/jvm/korlibs/render => archive}/sdl2/jna/SDL_Events.kt (100%) rename {korge/src/jvm/korlibs/render => archive}/sdl2/jna/SDL_Structs.kt (100%) rename {korge/src/jvm/korlibs/render => archive}/win32/Win32GameWindow.kt (100%) create mode 100644 archive/win32/Win32KeyMap.kt rename {korge/src/jvm/korlibs/render => archive}/x11/X11GameWindowJvm.kt (100%) create mode 100644 archive/x11/X11KeyMap.kt create mode 100644 korge-foundation/src/common/korlibs/annotations/DeprecatedParameter.kt create mode 100644 korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt delete mode 100644 korge/src/common/korlibs/event/TextInputManager.kt create mode 100644 korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt rename korge/src/common/korlibs/render/{SyncEventLoopCoroutineDispatcher.kt => EventLoopCoroutineDispatcher.kt} (70%) delete mode 100644 korge/src/common/korlibs/render/GameWindowCoroutineDispatcher.kt create mode 100644 korge/src/common/korlibs/render/GameWindowExt.kt delete mode 100644 korge/src/common/korlibs/render/Korgw.kt delete mode 100644 korge/src/jvm/korlibs/render/awt/GLCanvasGameWindow.kt delete mode 100644 korge/src/jvm/korlibs/render/osx/DialogInterfaceJvmCocoa.kt create mode 100644 korge/src/jvm/korlibs/render/osx/MacGL.kt delete mode 100644 korge/src/jvm/korlibs/render/platform/PlatformTools.kt diff --git a/korge/src/jvm/korlibs/korge/GLCanvasKorge.kt b/archive/GLCanvasKorge.kt similarity index 87% rename from korge/src/jvm/korlibs/korge/GLCanvasKorge.kt rename to archive/GLCanvasKorge.kt index 6f17442bb6..1aa7b08527 100644 --- a/korge/src/jvm/korlibs/korge/GLCanvasKorge.kt +++ b/archive/GLCanvasKorge.kt @@ -1,11 +1,14 @@ package korlibs.korge +/* import korlibs.io.async.* import korlibs.korge.internal.* import korlibs.korge.view.* import korlibs.math.geom.* import korlibs.render.awt.* +import korlibs.render.platform.* import kotlinx.coroutines.* +import java.awt.* import java.io.* fun GLCanvasWithKorge( @@ -99,3 +102,19 @@ class GLCanvasKorge internal constructor( } } } + +open class GLCanvasGameWindow( + val canvas: GLCanvas, +) : BaseAwtGameWindow(canvas.ag) { + init { + exitProcessOnClose = false + canvas.defaultRenderer = { gl, g -> + framePaint(g) + } + } + + override val ctx: BaseOpenglContext get() = canvas.ctx!! + override val component: Component get() = canvas + override val contentComponent: Component get() = canvas +} +*/ diff --git a/korge/src/jvm/korlibs/render/osx/MacosGameWindow.kt b/archive/osx/MacosGameWindow.kt similarity index 88% rename from korge/src/jvm/korlibs/render/osx/MacosGameWindow.kt rename to archive/osx/MacosGameWindow.kt index 03b45bcff9..eef8ae9cd3 100644 --- a/korge/src/jvm/korlibs/render/osx/MacosGameWindow.kt +++ b/archive/osx/MacosGameWindow.kt @@ -8,64 +8,15 @@ import korlibs.image.format.* import korlibs.io.async.* import korlibs.io.util.* import korlibs.kgl.* +import korlibs.memory.dyn.* import korlibs.memory.dyn.osx.* import korlibs.platform.Platform import korlibs.render.* import korlibs.render.platform.* -import korlibs.render.platform.NativeLoad import java.nio.* import kotlin.coroutines.* import kotlin.system.* -//open class MacKmlGL : NativeKgl(MacGL) -open class MacKmlGL : NativeKgl(DirectGL) - - -interface MacGL : INativeGL, Library { - companion object : MacGL by NativeLoad(nativeOpenGLLibraryPath) { - } - - //fun CGLSetParameter(vararg args: Any?): Int - fun CGLEnable(ctx: Pointer?, enable: Int): Int - fun CGLDisable(ctx: Pointer?, enable: Int): Int - fun CGLChoosePixelFormat(attributes: Pointer, pix: Pointer, num: Pointer): Int - fun CGLCreateContext(pix: Pointer?, sharedCtx: Pointer?, ctx: Pointer?): Int - fun CGLDestroyPixelFormat(ctx: Pointer?): Int - fun CGLSetCurrentContext(ctx: Pointer?): Int - fun CGLGetCurrentContext(): Pointer? - fun CGLDestroyContext(ctx: Pointer?): Int - fun CGLGetPixelFormat(ctx: Pointer?): Pointer? - - enum class Error(val id: Int) { - kCGLNoError (0), /* no error */ - kCGLBadAttribute (10000), /* invalid pixel format attribute */ - kCGLBadProperty (10001), /* invalid renderer property */ - kCGLBadPixelFormat (10002), /* invalid pixel format */ - kCGLBadRendererInfo (10003), /* invalid renderer info */ - kCGLBadContext (10004), /* invalid context */ - kCGLBadDrawable (10005), /* invalid drawable */ - kCGLBadDisplay (10006), /* invalid graphics device */ - kCGLBadState (10007), /* invalid context state */ - kCGLBadValue (10008), /* invalid numerical value */ - kCGLBadMatch (10009), /* invalid share context */ - kCGLBadEnumeration (10010), /* invalid enumerant */ - kCGLBadOffScreen (10011), /* invalid offscreen drawable */ - kCGLBadFullScreen (10012), /* invalid fullscreen drawable */ - kCGLBadWindow (10013), /* invalid window */ - kCGLBadAddress (10014), /* invalid pointer */ - kCGLBadCodeModule (10015), /* invalid code module */ - kCGLBadAlloc (10016), /* invalid memory allocation */ - kCGLBadConnection (10017), /* invalid CoreGraphics connection */ - kUnknownError (-1); - - companion object { - val VALUES = values().associateBy { it.id } - - operator fun get(id: Int): Error = VALUES[id] ?: kUnknownError - } - } -} - private fun ByteArray.toNSData(): Long = NSClass("NSData").alloc().msgSend("initWithBytes:length:", ByteBuffer.wrap(this), this.size) val NSThreadClass = NSClass("NSThread") @@ -559,3 +510,7 @@ internal val CharToKeys = mapOf( '0' to Key.N0, '1' to Key.N1, '2' to Key.N2, '3' to Key.N3, '4' to Key.N4, '5' to Key.N5, '6' to Key.N6, '7' to Key.N7, '8' to Key.N8, '9' to Key.N9 ) + +class DialogInterfaceJvmCocoa(val gwProvider: () -> MacGameWindow) : DialogInterface { + val gw get() = gwProvider() +} diff --git a/korge/src/common/korlibs/render/sdl2/SDLKeyCode.kt b/archive/sdl2/SDLKeyCode.kt similarity index 100% rename from korge/src/common/korlibs/render/sdl2/SDLKeyCode.kt rename to archive/sdl2/SDLKeyCode.kt diff --git a/korge/src/common/korlibs/render/sdl2/SDLKeycodeMap.kt b/archive/sdl2/SDLKeycodeMap.kt similarity index 100% rename from korge/src/common/korlibs/render/sdl2/SDLKeycodeMap.kt rename to archive/sdl2/SDLKeycodeMap.kt diff --git a/korge/src/common/korlibs/render/sdl2/SDLScancode.kt b/archive/sdl2/SDLScancode.kt similarity index 100% rename from korge/src/common/korlibs/render/sdl2/SDLScancode.kt rename to archive/sdl2/SDLScancode.kt diff --git a/korge/src/jvm/korlibs/render/sdl2/SdlGameWindowJvm.kt b/archive/sdl2/SdlGameWindowJvm.kt similarity index 100% rename from korge/src/jvm/korlibs/render/sdl2/SdlGameWindowJvm.kt rename to archive/sdl2/SdlGameWindowJvm.kt diff --git a/korge/src/jvm/korlibs/render/sdl2/jna/ISDL.kt b/archive/sdl2/jna/ISDL.kt similarity index 100% rename from korge/src/jvm/korlibs/render/sdl2/jna/ISDL.kt rename to archive/sdl2/jna/ISDL.kt diff --git a/korge/src/jvm/korlibs/render/sdl2/jna/SDL_Enums.kt b/archive/sdl2/jna/SDL_Enums.kt similarity index 100% rename from korge/src/jvm/korlibs/render/sdl2/jna/SDL_Enums.kt rename to archive/sdl2/jna/SDL_Enums.kt diff --git a/korge/src/jvm/korlibs/render/sdl2/jna/SDL_Events.kt b/archive/sdl2/jna/SDL_Events.kt similarity index 100% rename from korge/src/jvm/korlibs/render/sdl2/jna/SDL_Events.kt rename to archive/sdl2/jna/SDL_Events.kt diff --git a/korge/src/jvm/korlibs/render/sdl2/jna/SDL_Structs.kt b/archive/sdl2/jna/SDL_Structs.kt similarity index 100% rename from korge/src/jvm/korlibs/render/sdl2/jna/SDL_Structs.kt rename to archive/sdl2/jna/SDL_Structs.kt diff --git a/korge/src/jvm/korlibs/render/win32/Win32GameWindow.kt b/archive/win32/Win32GameWindow.kt similarity index 100% rename from korge/src/jvm/korlibs/render/win32/Win32GameWindow.kt rename to archive/win32/Win32GameWindow.kt diff --git a/archive/win32/Win32KeyMap.kt b/archive/win32/Win32KeyMap.kt new file mode 100644 index 0000000000..2c78a5d1aa --- /dev/null +++ b/archive/win32/Win32KeyMap.kt @@ -0,0 +1,401 @@ +package korlibs.render.win32 + +import korlibs.event.* + +internal const val VK_LBUTTON = 0x01 /* Left mouse button */ +internal const val VK_RBUTTON = 0x02 /* Right mouse button */ +internal const val VK_CANCEL = 0x03 /* Control-break processing */ +internal const val VK_MBUTTON = 0x04 /* Middle mouse button (three-button mouse) */ +internal const val VK_XBUTTON1 = 0x05 /* Windows 2000/XP: X1 mouse button */ +internal const val VK_XBUTTON2 = 0x06 /* Windows 2000/XP: X2 mouse button */ +internal const val VK_BACK = 0x08 /* BACKSPACE key */ +internal const val VK_TAB = 0x09 /* TAB key */ +internal const val VK_CLEAR = 0x0C /* CLEAR key */ +internal const val VK_RETURN = 0x0D /* ENTER key */ +internal const val VK_SHIFT = 0x10 /* SHIFT key */ +internal const val VK_CONTROL = 0x11 /* CTRL key */ +internal const val VK_MENU = 0x12 /* ALT key */ +internal const val VK_PAUSE = 0x13 /* PAUSE key */ +internal const val VK_CAPITAL = 0x14 /* CAPS LOCK key */ +internal const val VK_KANA = 0x15 /* Input Method Editor (IME) Kana mode */ +internal const val VK_HANGUEL = 0x15 /* IME Hanguel mode (maintained for compatibility; use #define VK_HANGUL) */ +internal const val VK_HANGUL = 0x15 /* IME Hangul mode */ +internal const val VK_JUNJA = 0x17 /* IME Junja mode */ +internal const val VK_FINAL = 0x18 /* IME final mode */ +internal const val VK_HANJA = 0x19 /* IME Hanja mode */ +internal const val VK_KANJI = 0x19 /* IME Kanji mode */ +internal const val VK_HKTG = 0x1A /* Hiragana/Katakana toggle */ +internal const val VK_ESCAPE = 0x1B /* ESC key */ +internal const val VK_CONVERT = 0x1C /* IME convert */ +internal const val VK_NONCONVERT = 0x1D /* IME nonconvert */ +internal const val VK_ACCEPT = 0x1E /* IME accept */ +internal const val VK_MODECHANGE = 0x1F /* IME mode change request */ +internal const val VK_SPACE = 0x20 /* SPACEBAR */ +internal const val VK_PRIOR = 0x21 /* PAGE UP key */ +internal const val VK_NEXT = 0x22 /* PAGE DOWN key */ +internal const val VK_END = 0x23 /* END key */ +internal const val VK_HOME = 0x24 /* HOME key */ +internal const val VK_LEFT = 0x25 /* LEFT ARROW key */ +internal const val VK_UP = 0x26 /* UP ARROW key */ +internal const val VK_RIGHT = 0x27 /* RIGHT ARROW key */ +internal const val VK_DOWN = 0x28 /* DOWN ARROW key */ +internal const val VK_SELECT = 0x29 /* SELECT key */ +internal const val VK_PRINT = 0x2A /* PRINT key */ +internal const val VK_EXECUTE = 0x2B /* EXECUTE key */ +internal const val VK_SNAPSHOT = 0x2C /* PRINT SCREEN key */ +internal const val VK_INSERT = 0x2D /* INS key */ +internal const val VK_DELETE = 0x2E /* DEL key */ +internal const val VK_HELP = 0x2F /* HELP key */ +internal const val VK_KEY_0 = 0x30 /* '0' key */ +internal const val VK_KEY_1 = 0x31 /* '1' key */ +internal const val VK_KEY_2 = 0x32 /* '2' key */ +internal const val VK_KEY_3 = 0x33 /* '3' key */ +internal const val VK_KEY_4 = 0x34 /* '4' key */ +internal const val VK_KEY_5 = 0x35 /* '5' key */ +internal const val VK_KEY_6 = 0x36 /* '6' key */ +internal const val VK_KEY_7 = 0x37 /* '7' key */ +internal const val VK_KEY_8 = 0x38 /* '8' key */ +internal const val VK_KEY_9 = 0x39 /* '9' key */ +internal const val VK_KEY_A = 0x41 /* 'A' key */ +internal const val VK_KEY_B = 0x42 /* 'B' key */ +internal const val VK_KEY_C = 0x43 /* 'C' key */ +internal const val VK_KEY_D = 0x44 /* 'D' key */ +internal const val VK_KEY_E = 0x45 /* 'E' key */ +internal const val VK_KEY_F = 0x46 /* 'F' key */ +internal const val VK_KEY_G = 0x47 /* 'G' key */ +internal const val VK_KEY_H = 0x48 /* 'H' key */ +internal const val VK_KEY_I = 0x49 /* 'I' key */ +internal const val VK_KEY_J = 0x4A /* 'J' key */ +internal const val VK_KEY_K = 0x4B /* 'K' key */ +internal const val VK_KEY_L = 0x4C /* 'L' key */ +internal const val VK_KEY_M = 0x4D /* 'M' key */ +internal const val VK_KEY_N = 0x4E /* 'N' key */ +internal const val VK_KEY_O = 0x4F /* 'O' key */ +internal const val VK_KEY_P = 0x50 /* 'P' key */ +internal const val VK_KEY_Q = 0x51 /* 'Q' key */ +internal const val VK_KEY_R = 0x52 /* 'R' key */ +internal const val VK_KEY_S = 0x53 /* 'S' key */ +internal const val VK_KEY_T = 0x54 /* 'T' key */ +internal const val VK_KEY_U = 0x55 /* 'U' key */ +internal const val VK_KEY_V = 0x56 /* 'V' key */ +internal const val VK_KEY_W = 0x57 /* 'W' key */ +internal const val VK_KEY_X = 0x58 /* 'X' key */ +internal const val VK_KEY_Y = 0x59 /* 'Y' key */ +internal const val VK_KEY_Z = 0x5A /* 'Z' key */ +internal const val VK_LWIN = 0x5B /* Left Windows key (Microsoft Natural keyboard) */ +internal const val VK_RWIN = 0x5C /* Right Windows key (Natural keyboard) */ +internal const val VK_APPS = 0x5D /* Applications key (Natural keyboard) */ +internal const val VK_POWER = 0x5E /* Power key */ +internal const val VK_SLEEP = 0x5F /* Computer Sleep key */ +internal const val VK_NUMPAD0 = 0x60 /* Numeric keypad '0' key */ +internal const val VK_NUMPAD1 = 0x61 /* Numeric keypad '1' key */ +internal const val VK_NUMPAD2 = 0x62 /* Numeric keypad '2' key */ +internal const val VK_NUMPAD3 = 0x63 /* Numeric keypad '3' key */ +internal const val VK_NUMPAD4 = 0x64 /* Numeric keypad '4' key */ +internal const val VK_NUMPAD5 = 0x65 /* Numeric keypad '5' key */ +internal const val VK_NUMPAD6 = 0x66 /* Numeric keypad '6' key */ +internal const val VK_NUMPAD7 = 0x67 /* Numeric keypad '7' key */ +internal const val VK_NUMPAD8 = 0x68 /* Numeric keypad '8' key */ +internal const val VK_NUMPAD9 = 0x69 /* Numeric keypad '9' key */ +internal const val VK_MULTIPLY = 0x6A /* Multiply key */ +internal const val VK_ADD = 0x6B /* Add key */ +internal const val VK_SEPARATOR = 0x6C /* Separator key */ +internal const val VK_SUBTRACT = 0x6D /* Subtract key */ +internal const val VK_DECIMAL = 0x6E /* Decimal key */ +internal const val VK_DIVIDE = 0x6F /* Divide key */ +internal const val VK_F1 = 0x70 /* F1 key */ +internal const val VK_F2 = 0x71 /* F2 key */ +internal const val VK_F3 = 0x72 /* F3 key */ +internal const val VK_F4 = 0x73 /* F4 key */ +internal const val VK_F5 = 0x74 /* F5 key */ +internal const val VK_F6 = 0x75 /* F6 key */ +internal const val VK_F7 = 0x76 /* F7 key */ +internal const val VK_F8 = 0x77 /* F8 key */ +internal const val VK_F9 = 0x78 /* F9 key */ +internal const val VK_F10 = 0x79 /* F10 key */ +internal const val VK_F11 = 0x7A /* F11 key */ +internal const val VK_F12 = 0x7B /* F12 key */ +internal const val VK_F13 = 0x7C /* F13 key */ +internal const val VK_F14 = 0x7D /* F14 key */ +internal const val VK_F15 = 0x7E /* F15 key */ +internal const val VK_F16 = 0x7F /* F16 key */ +internal const val VK_F17 = 0x80 /* F17 key */ +internal const val VK_F18 = 0x81 /* F18 key */ +internal const val VK_F19 = 0x82 /* F19 key */ +internal const val VK_F20 = 0x83 /* F20 key */ +internal const val VK_F21 = 0x84 /* F21 key */ +internal const val VK_F22 = 0x85 /* F22 key */ +internal const val VK_F23 = 0x86 /* F23 key */ +internal const val VK_F24 = 0x87 /* F24 key */ +internal const val VK_NUMLOCK = 0x90 /* NUM LOCK key */ +internal const val VK_SCROLL = 0x91 /* SCROLL LOCK key */ +internal const val VK_LSHIFT = 0xA0 /* Left SHIFT key */ +internal const val VK_RSHIFT = 0xA1 /* Right SHIFT key */ +internal const val VK_LCONTROL = 0xA2 /* Left CONTROL key */ +internal const val VK_RCONTROL = 0xA3 /* Right CONTROL key */ +internal const val VK_LMENU = 0xA4 /* Left MENU key */ +internal const val VK_RMENU = 0xA5 /* Right MENU key */ +internal const val VK_BROWSER_BACK = 0xA6 /* Windows 2000/XP: Browser Back key */ +internal const val VK_BROWSER_FORWARD = 0xA7 /* Windows 2000/XP: Browser Forward key */ +internal const val VK_BROWSER_REFRESH = 0xA8 /* Windows 2000/XP: Browser Refresh key */ +internal const val VK_BROWSER_STOP = 0xA9 /* Windows 2000/XP: Browser Stop key */ +internal const val VK_BROWSER_SEARCH = 0xAA /* Windows 2000/XP: Browser Search key */ +internal const val VK_BROWSER_FAVORITES = 0xAB /* Windows 2000/XP: Browser Favorites key */ +internal const val VK_BROWSER_HOME = 0xAC /* Windows 2000/XP: Browser Start and Home key */ +internal const val VK_VOLUME_MUTE = 0xAD /* Windows 2000/XP: Volume Mute key */ +internal const val VK_VOLUME_DOWN = 0xAE /* Windows 2000/XP: Volume Down key */ +internal const val VK_VOLUME_UP = 0xAF /* Windows 2000/XP: Volume Up key */ +internal const val VK_MEDIA_NEXT_TRACK = 0xB0 /* Windows 2000/XP: Next Track key */ +internal const val VK_MEDIA_PREV_TRACK = 0xB1 /* Windows 2000/XP: Previous Track key */ +internal const val VK_MEDIA_STOP = 0xB2 /* Windows 2000/XP: Stop Media key */ +internal const val VK_MEDIA_PLAY_PAUSE = 0xB3 /* Windows 2000/XP: Play/Pause Media key */ +internal const val VK_LAUNCH_MAIL = 0xB4 /* Windows 2000/XP: Start Mail key */ +internal const val VK_MEDIA_SELECT = 0xB5 /* Windows 2000/XP: Select Media key */ +internal const val VK_LAUNCH_MEDIA_SELECT = 0xB5 /* Windows 2000/XP: Select Media key */ +internal const val VK_LAUNCH_APP1 = 0xB6 /* Windows 2000/XP: Start Application 1 key */ +internal const val VK_LAUNCH_APP2 = 0xB7 /* Windows 2000/XP: Start Application 2 key */ +internal const val VK_OEM_1 = 0xBA /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_PLUS = 0xBB /* Windows 2000/XP: For any country/region, the '+' key */ +internal const val VK_OEM_COMMA = 0xBC /* Windows 2000/XP: For any country/region, the ',' key */ +internal const val VK_OEM_MINUS = 0xBD /* Windows 2000/XP: For any country/region, the '-' key */ +internal const val VK_OEM_PERIOD = 0xBE /* Windows 2000/XP: For any country/region, the '.' key */ +internal const val VK_OEM_2 = 0xBF /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_3 = 0xC0 /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_ABNT_C1 = 0xC1 /* Brazilian (ABNT) Keyboard */ +internal const val VK_ABNT_C2 = 0xC2 /* Brazilian (ABNT) Keyboard */ +internal const val VK_OEM_4 = 0xDB /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_5 = 0xDC /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_6 = 0xDD /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_7 = 0xDE /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_8 = 0xDF /* Used for miscellaneous characters; it can vary by keyboard. */ +internal const val VK_OEM_AX = 0xE1 /* AX key on Japanese AX keyboard */ +internal const val VK_OEM_102 = 0xE2 /* Windows 2000/XP: Either the angle bracket key or */ +internal const val VK_PROCESSKEY = 0xE5 /* Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key */ +internal const val VK_PACKET = 0xE7 /* Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. */ +internal const val VK_OEM_RESET = 0xE9 +internal const val VK_OEM_JUMP = 0xEA +internal const val VK_OEM_PA1 = 0xEB +internal const val VK_OEM_PA2 = 0xEC +internal const val VK_OEM_PA3 = 0xED +internal const val VK_OEM_WSCTRL = 0xEE +internal const val VK_OEM_CUSEL = 0xEF +internal const val VK_OEM_ATTN = 0xF0 +internal const val VK_OEM_FINISH = 0xF1 +internal const val VK_OEM_COPY = 0xF2 +internal const val VK_OEM_AUTO = 0xF3 +internal const val VK_OEM_ENLW = 0xF4 +internal const val VK_OEM_BACKTAB = 0xF5 +internal const val VK_ATTN = 0xF6 /* Attn key */ +internal const val VK_CRSEL = 0xF7 /* CrSel key */ +internal const val VK_EXSEL = 0xF8 /* ExSel key */ +internal const val VK_EREOF = 0xF9 /* Erase EOF key */ +internal const val VK_PLAY = 0xFA /* Play key */ +internal const val VK_ZOOM = 0xFB /* Zoom key */ +internal const val VK_NONAME = 0xFC /* Reserved */ +internal const val VK_PA1 = 0xFD /* PA1 key */ +internal const val VK_OEM_CLEAR = 0xFE /* Clear key */ +internal const val VK_NONE = 0xFF /* no key */ + +val VK_TABLE: Map = mapOf( + KBDEXT to Key.UNKNOWN, + KBDMULTIVK to Key.UNKNOWN, + KBDSPECIAL to Key.UNKNOWN, + KBDNUMPAD to Key.UNKNOWN, + KBDUNICODE to Key.UNKNOWN, + KBDINJECTEDVK to Key.UNKNOWN, + KBDMAPPEDVK to Key.UNKNOWN, + KBDBREAK to Key.UNKNOWN, + VK_LBUTTON to Key.UNKNOWN, + VK_RBUTTON to Key.UNKNOWN, + VK_CANCEL to Key.UNKNOWN, + VK_MBUTTON to Key.UNKNOWN, + VK_XBUTTON1 to Key.UNKNOWN, + VK_XBUTTON2 to Key.UNKNOWN, + VK_BACK to Key.BACKSPACE, + VK_TAB to Key.TAB, + VK_CLEAR to Key.CLEAR, + VK_RETURN to Key.RETURN, + VK_SHIFT to Key.LEFT_SHIFT, + VK_CONTROL to Key.LEFT_CONTROL, + VK_MENU to Key.MENU, + VK_PAUSE to Key.PAUSE, + VK_CAPITAL to Key.UNKNOWN, + VK_KANA to Key.UNKNOWN, + VK_HANGUEL to Key.UNKNOWN, + VK_HANGUL to Key.UNKNOWN, + VK_JUNJA to Key.UNKNOWN, + VK_FINAL to Key.UNKNOWN, + VK_HANJA to Key.UNKNOWN, + VK_KANJI to Key.UNKNOWN, + VK_HKTG to Key.UNKNOWN, + VK_ESCAPE to Key.ESCAPE, + VK_CONVERT to Key.UNKNOWN, + VK_NONCONVERT to Key.UNKNOWN, + VK_ACCEPT to Key.UNKNOWN, + VK_MODECHANGE to Key.UNKNOWN, + VK_SPACE to Key.SPACE, + VK_PRIOR to Key.UNKNOWN, + VK_NEXT to Key.UNKNOWN, + VK_END to Key.END, + VK_HOME to Key.HOME, + VK_LEFT to Key.LEFT, + VK_UP to Key.UP, + VK_RIGHT to Key.RIGHT, + VK_DOWN to Key.DOWN, + VK_SELECT to Key.SELECT_KEY, + VK_PRINT to Key.PRINT_SCREEN, + VK_EXECUTE to Key.UNKNOWN, + VK_SNAPSHOT to Key.UNKNOWN, + VK_INSERT to Key.INSERT, + VK_DELETE to Key.DELETE, + VK_HELP to Key.HELP, + VK_KEY_0 to Key.N0, + VK_KEY_1 to Key.N1, + VK_KEY_2 to Key.N2, + VK_KEY_3 to Key.N3, + VK_KEY_4 to Key.N4, + VK_KEY_5 to Key.N5, + VK_KEY_6 to Key.N6, + VK_KEY_7 to Key.N7, + VK_KEY_8 to Key.N8, + VK_KEY_9 to Key.N9, + VK_KEY_A to Key.A, + VK_KEY_B to Key.B, + VK_KEY_C to Key.C, + VK_KEY_D to Key.D, + VK_KEY_E to Key.E, + VK_KEY_F to Key.F, + VK_KEY_G to Key.G, + VK_KEY_H to Key.H, + VK_KEY_I to Key.I, + VK_KEY_J to Key.J, + VK_KEY_K to Key.K, + VK_KEY_L to Key.L, + VK_KEY_M to Key.M, + VK_KEY_N to Key.N, + VK_KEY_O to Key.O, + VK_KEY_P to Key.P, + VK_KEY_Q to Key.Q, + VK_KEY_R to Key.R, + VK_KEY_S to Key.S, + VK_KEY_T to Key.T, + VK_KEY_U to Key.U, + VK_KEY_V to Key.V, + VK_KEY_W to Key.W, + VK_KEY_X to Key.X, + VK_KEY_Y to Key.Y, + VK_KEY_Z to Key.Z, + VK_LWIN to Key.META, + VK_RWIN to Key.META, + VK_APPS to Key.UNKNOWN, + VK_POWER to Key.UNKNOWN, + VK_SLEEP to Key.UNKNOWN, + VK_NUMPAD0 to Key.NUMPAD0, + VK_NUMPAD1 to Key.NUMPAD1, + VK_NUMPAD2 to Key.NUMPAD2, + VK_NUMPAD3 to Key.NUMPAD3, + VK_NUMPAD4 to Key.NUMPAD4, + VK_NUMPAD5 to Key.NUMPAD5, + VK_NUMPAD6 to Key.NUMPAD6, + VK_NUMPAD7 to Key.NUMPAD7, + VK_NUMPAD8 to Key.NUMPAD8, + VK_NUMPAD9 to Key.NUMPAD9, + VK_MULTIPLY to Key.KP_MULTIPLY, + VK_ADD to Key.KP_ADD, + VK_SEPARATOR to Key.KP_SEPARATOR, + VK_SUBTRACT to Key.KP_SUBTRACT, + VK_DECIMAL to Key.KP_DECIMAL, + VK_DIVIDE to Key.KP_DIVIDE, + VK_F1 to Key.F1, + VK_F2 to Key.F2, + VK_F3 to Key.F3, + VK_F4 to Key.F4, + VK_F5 to Key.F5, + VK_F6 to Key.F6, + VK_F7 to Key.F7, + VK_F8 to Key.F8, + VK_F9 to Key.F9, + VK_F10 to Key.F10, + VK_F11 to Key.F11, + VK_F12 to Key.F12, + VK_F13 to Key.F13, + VK_F14 to Key.F14, + VK_F15 to Key.F15, + VK_F16 to Key.F16, + VK_F17 to Key.F17, + VK_F18 to Key.F18, + VK_F19 to Key.F19, + VK_F20 to Key.F20, + VK_F21 to Key.F21, + VK_F22 to Key.F22, + VK_F23 to Key.F23, + VK_F24 to Key.F24, + VK_NUMLOCK to Key.NUM_LOCK, + VK_SCROLL to Key.SCROLL_LOCK, + VK_LSHIFT to Key.LEFT_SHIFT, + VK_RSHIFT to Key.RIGHT_SHIFT, + VK_LCONTROL to Key.LEFT_CONTROL, + VK_RCONTROL to Key.RIGHT_CONTROL, + VK_LMENU to Key.LEFT_SUPER, + VK_RMENU to Key.RIGHT_SUPER, + VK_BROWSER_BACK to Key.UNKNOWN, + VK_BROWSER_FORWARD to Key.UNKNOWN, + VK_BROWSER_REFRESH to Key.UNKNOWN, + VK_BROWSER_STOP to Key.UNKNOWN, + VK_BROWSER_SEARCH to Key.UNKNOWN, + VK_BROWSER_FAVORITES to Key.UNKNOWN, + VK_BROWSER_HOME to Key.UNKNOWN, + VK_VOLUME_MUTE to Key.UNKNOWN, + VK_VOLUME_DOWN to Key.UNKNOWN, + VK_VOLUME_UP to Key.UNKNOWN, + VK_MEDIA_NEXT_TRACK to Key.UNKNOWN, + VK_MEDIA_PREV_TRACK to Key.UNKNOWN, + VK_MEDIA_STOP to Key.UNKNOWN, + VK_MEDIA_PLAY_PAUSE to Key.UNKNOWN, + VK_LAUNCH_MAIL to Key.UNKNOWN, + VK_MEDIA_SELECT to Key.UNKNOWN, + VK_LAUNCH_MEDIA_SELECT to Key.UNKNOWN, + VK_LAUNCH_APP1 to Key.UNKNOWN, + VK_LAUNCH_APP2 to Key.UNKNOWN, + VK_OEM_1 to Key.UNKNOWN, + VK_OEM_PLUS to Key.PLUS, + VK_OEM_COMMA to Key.UNKNOWN, + VK_OEM_MINUS to Key.MINUS, + VK_OEM_PERIOD to Key.UNKNOWN, + VK_OEM_2 to Key.UNKNOWN, + VK_OEM_3 to Key.UNKNOWN, + VK_ABNT_C1 to Key.UNKNOWN, + VK_ABNT_C2 to Key.UNKNOWN, + VK_OEM_4 to Key.UNKNOWN, + VK_OEM_5 to Key.UNKNOWN, + VK_OEM_6 to Key.UNKNOWN, + VK_OEM_7 to Key.UNKNOWN, + VK_OEM_8 to Key.UNKNOWN, + VK_OEM_AX to Key.UNKNOWN, + VK_OEM_102 to Key.UNKNOWN, + VK_PROCESSKEY to Key.UNKNOWN, + VK_PACKET to Key.UNKNOWN, + VK_OEM_RESET to Key.UNKNOWN, + VK_OEM_JUMP to Key.UNKNOWN, + VK_OEM_PA1 to Key.UNKNOWN, + VK_OEM_PA2 to Key.UNKNOWN, + VK_OEM_PA3 to Key.UNKNOWN, + VK_OEM_WSCTRL to Key.UNKNOWN, + VK_OEM_CUSEL to Key.UNKNOWN, + VK_OEM_ATTN to Key.UNKNOWN, + VK_OEM_FINISH to Key.UNKNOWN, + VK_OEM_COPY to Key.UNKNOWN, + VK_OEM_AUTO to Key.UNKNOWN, + VK_OEM_ENLW to Key.UNKNOWN, + VK_OEM_BACKTAB to Key.UNKNOWN, + VK_ATTN to Key.UNKNOWN, + VK_CRSEL to Key.UNKNOWN, + VK_EXSEL to Key.UNKNOWN, + VK_EREOF to Key.UNKNOWN, + VK_PLAY to Key.UNKNOWN, + VK_ZOOM to Key.UNKNOWN, + VK_NONAME to Key.UNKNOWN, + VK_PA1 to Key.UNKNOWN, + VK_OEM_CLEAR to Key.UNKNOWN, + VK_NONE to Key.UNKNOWN +) diff --git a/korge/src/jvm/korlibs/render/x11/X11GameWindowJvm.kt b/archive/x11/X11GameWindowJvm.kt similarity index 100% rename from korge/src/jvm/korlibs/render/x11/X11GameWindowJvm.kt rename to archive/x11/X11GameWindowJvm.kt diff --git a/archive/x11/X11KeyMap.kt b/archive/x11/X11KeyMap.kt new file mode 100644 index 0000000000..a8b5636025 --- /dev/null +++ b/archive/x11/X11KeyMap.kt @@ -0,0 +1,507 @@ +package korlibs.render.x11 + +import korlibs.datastructure.* +import korlibs.event.* + + +internal const val XK_space = 0x0020 /* U+0020 SPACE */ +internal const val XK_exclam = 0x0021 /* U+0021 EXCLAMATION MARK */ +internal const val XK_quotedbl = 0x0022 /* U+0022 QUOTATION MARK */ +internal const val XK_numbersign = 0x0023 /* U+0023 NUMBER SIGN */ +internal const val XK_dollar = 0x0024 /* U+0024 DOLLAR SIGN */ +internal const val XK_percent = 0x0025 /* U+0025 PERCENT SIGN */ +internal const val XK_ampersand = 0x0026 /* U+0026 AMPERSAND */ +internal const val XK_apostrophe = 0x0027 /* U+0027 APOSTROPHE */ +internal const val XK_quoteright = 0x0027 /* deprecated */ +internal const val XK_parenleft = 0x0028 /* U+0028 LEFT PARENTHESIS */ +internal const val XK_parenright = 0x0029 /* U+0029 RIGHT PARENTHESIS */ +internal const val XK_asterisk = 0x002a /* U+002A ASTERISK */ +internal const val XK_plus = 0x002b /* U+002B PLUS SIGN */ +internal const val XK_comma = 0x002c /* U+002C COMMA */ +internal const val XK_minus = 0x002d /* U+002D HYPHEN-MINUS */ +internal const val XK_period = 0x002e /* U+002E FULL STOP */ +internal const val XK_slash = 0x002f /* U+002F SOLIDUS */ +internal const val XK_0 = 0x0030 /* U+0030 DIGIT ZERO */ +internal const val XK_1 = 0x0031 /* U+0031 DIGIT ONE */ +internal const val XK_2 = 0x0032 /* U+0032 DIGIT TWO */ +internal const val XK_3 = 0x0033 /* U+0033 DIGIT THREE */ +internal const val XK_4 = 0x0034 /* U+0034 DIGIT FOUR */ +internal const val XK_5 = 0x0035 /* U+0035 DIGIT FIVE */ +internal const val XK_6 = 0x0036 /* U+0036 DIGIT SIX */ +internal const val XK_7 = 0x0037 /* U+0037 DIGIT SEVEN */ +internal const val XK_8 = 0x0038 /* U+0038 DIGIT EIGHT */ +internal const val XK_9 = 0x0039 /* U+0039 DIGIT NINE */ +internal const val XK_colon = 0x003a /* U+003A COLON */ +internal const val XK_semicolon = 0x003b /* U+003B SEMICOLON */ +internal const val XK_less = 0x003c /* U+003C LESS-THAN SIGN */ +internal const val XK_equal = 0x003d /* U+003D EQUALS SIGN */ +internal const val XK_greater = 0x003e /* U+003E GREATER-THAN SIGN */ +internal const val XK_question = 0x003f /* U+003F QUESTION MARK */ +internal const val XK_at = 0x0040 /* U+0040 COMMERCIAL AT */ +internal const val XK_A = 0x0041 /* U+0041 LATIN CAPITAL LETTER A */ +internal const val XK_B = 0x0042 /* U+0042 LATIN CAPITAL LETTER B */ +internal const val XK_C = 0x0043 /* U+0043 LATIN CAPITAL LETTER C */ +internal const val XK_D = 0x0044 /* U+0044 LATIN CAPITAL LETTER D */ +internal const val XK_E = 0x0045 /* U+0045 LATIN CAPITAL LETTER E */ +internal const val XK_F = 0x0046 /* U+0046 LATIN CAPITAL LETTER F */ +internal const val XK_G = 0x0047 /* U+0047 LATIN CAPITAL LETTER G */ +internal const val XK_H = 0x0048 /* U+0048 LATIN CAPITAL LETTER H */ +internal const val XK_I = 0x0049 /* U+0049 LATIN CAPITAL LETTER I */ +internal const val XK_J = 0x004a /* U+004A LATIN CAPITAL LETTER J */ +internal const val XK_K = 0x004b /* U+004B LATIN CAPITAL LETTER K */ +internal const val XK_L = 0x004c /* U+004C LATIN CAPITAL LETTER L */ +internal const val XK_M = 0x004d /* U+004D LATIN CAPITAL LETTER M */ +internal const val XK_N = 0x004e /* U+004E LATIN CAPITAL LETTER N */ +internal const val XK_O = 0x004f /* U+004F LATIN CAPITAL LETTER O */ +internal const val XK_P = 0x0050 /* U+0050 LATIN CAPITAL LETTER P */ +internal const val XK_Q = 0x0051 /* U+0051 LATIN CAPITAL LETTER Q */ +internal const val XK_R = 0x0052 /* U+0052 LATIN CAPITAL LETTER R */ +internal const val XK_S = 0x0053 /* U+0053 LATIN CAPITAL LETTER S */ +internal const val XK_T = 0x0054 /* U+0054 LATIN CAPITAL LETTER T */ +internal const val XK_U = 0x0055 /* U+0055 LATIN CAPITAL LETTER U */ +internal const val XK_V = 0x0056 /* U+0056 LATIN CAPITAL LETTER V */ +internal const val XK_W = 0x0057 /* U+0057 LATIN CAPITAL LETTER W */ +internal const val XK_X = 0x0058 /* U+0058 LATIN CAPITAL LETTER X */ +internal const val XK_Y = 0x0059 /* U+0059 LATIN CAPITAL LETTER Y */ +internal const val XK_Z = 0x005a /* U+005A LATIN CAPITAL LETTER Z */ +internal const val XK_bracketleft = 0x005b /* U+005B LEFT SQUARE BRACKET */ +internal const val XK_backslash = 0x005c /* U+005C REVERSE SOLIDUS */ +internal const val XK_bracketright = 0x005d /* U+005D RIGHT SQUARE BRACKET */ +internal const val XK_asciicircum = 0x005e /* U+005E CIRCUMFLEX ACCENT */ +internal const val XK_underscore = 0x005f /* U+005F LOW LINE */ +internal const val XK_grave = 0x0060 /* U+0060 GRAVE ACCENT */ +internal const val XK_quoteleft = 0x0060 /* deprecated */ +internal const val XK_a = 0x0061 /* U+0061 LATIN SMALL LETTER A */ +internal const val XK_b = 0x0062 /* U+0062 LATIN SMALL LETTER B */ +internal const val XK_c = 0x0063 /* U+0063 LATIN SMALL LETTER C */ +internal const val XK_d = 0x0064 /* U+0064 LATIN SMALL LETTER D */ +internal const val XK_e = 0x0065 /* U+0065 LATIN SMALL LETTER E */ +internal const val XK_f = 0x0066 /* U+0066 LATIN SMALL LETTER F */ +internal const val XK_g = 0x0067 /* U+0067 LATIN SMALL LETTER G */ +internal const val XK_h = 0x0068 /* U+0068 LATIN SMALL LETTER H */ +internal const val XK_i = 0x0069 /* U+0069 LATIN SMALL LETTER I */ +internal const val XK_j = 0x006a /* U+006A LATIN SMALL LETTER J */ +internal const val XK_k = 0x006b /* U+006B LATIN SMALL LETTER K */ +internal const val XK_l = 0x006c /* U+006C LATIN SMALL LETTER L */ +internal const val XK_m = 0x006d /* U+006D LATIN SMALL LETTER M */ +internal const val XK_n = 0x006e /* U+006E LATIN SMALL LETTER N */ +internal const val XK_o = 0x006f /* U+006F LATIN SMALL LETTER O */ +internal const val XK_p = 0x0070 /* U+0070 LATIN SMALL LETTER P */ +internal const val XK_q = 0x0071 /* U+0071 LATIN SMALL LETTER Q */ +internal const val XK_r = 0x0072 /* U+0072 LATIN SMALL LETTER R */ +internal const val XK_s = 0x0073 /* U+0073 LATIN SMALL LETTER S */ +internal const val XK_t = 0x0074 /* U+0074 LATIN SMALL LETTER T */ +internal const val XK_u = 0x0075 /* U+0075 LATIN SMALL LETTER U */ +internal const val XK_v = 0x0076 /* U+0076 LATIN SMALL LETTER V */ +internal const val XK_w = 0x0077 /* U+0077 LATIN SMALL LETTER W */ +internal const val XK_x = 0x0078 /* U+0078 LATIN SMALL LETTER X */ +internal const val XK_y = 0x0079 /* U+0079 LATIN SMALL LETTER Y */ +internal const val XK_z = 0x007a /* U+007A LATIN SMALL LETTER Z */ +internal const val XK_braceleft = 0x007b /* U+007B LEFT CURLY BRACKET */ +internal const val XK_bar = 0x007c /* U+007C VERTICAL LINE */ +internal const val XK_braceright = 0x007d /* U+007D RIGHT CURLY BRACKET */ +internal const val XK_asciitilde = 0x007e /* U+007E TILDE */ + +internal const val XK_leftarrow = 0x08fb /* U+2190 LEFTWARDS ARROW */ +internal const val XK_uparrow = 0x08fc /* U+2191 UPWARDS ARROW */ +internal const val XK_rightarrow = 0x08fd /* U+2192 RIGHTWARDS ARROW */ +internal const val XK_downarrow = 0x08fe /* U+2193 DOWNWARDS ARROW */ +internal const val XK_BackSpace = 0xff08 /* Back space, back char */ +internal const val XK_Tab = 0xff09 +internal const val XK_Linefeed = 0xff0a /* Linefeed, LF */ +internal const val XK_Clear = 0xff0b +internal const val XK_Return = 0xff0d /* Return, enter */ +internal const val XK_Pause = 0xff13 /* Pause, hold */ +internal const val XK_Scroll_Lock = 0xff14 +internal const val XK_Sys_Req = 0xff15 +internal const val XK_Escape = 0xff1b +internal const val XK_Delete = 0xffff /* Delete, rubout */ + +internal const val XK_Home = 0xff50 +internal const val XK_Left = 0xff51 /* Move left, left arrow */ +internal const val XK_Up = 0xff52 /* Move up, up arrow */ +internal const val XK_Right = 0xff53 /* Move right, right arrow */ +internal const val XK_Down = 0xff54 /* Move down, down arrow */ +internal const val XK_Prior = 0xff55 /* Prior, previous */ +internal const val XK_Page_Up = 0xff55 +internal const val XK_Next = 0xff56 /* Next */ +internal const val XK_Page_Down = 0xff56 +internal const val XK_End = 0xff57 /* EOL */ +internal const val XK_Begin = 0xff58 /* BOL */ +internal const val XK_Select = 0xff60 /* Select, mark */ +internal const val XK_Print = 0xff61 +internal const val XK_Execute = 0xff62 /* Execute, run, do */ +internal const val XK_Insert = 0xff63 /* Insert, insert here */ +internal const val XK_Undo = 0xff65 +internal const val XK_Redo = 0xff66 /* Redo, again */ +internal const val XK_Menu = 0xff67 +internal const val XK_Find = 0xff68 /* Find, search */ +internal const val XK_Cancel = 0xff69 /* Cancel, stop, abort, exit */ +internal const val XK_Help = 0xff6a /* Help */ +internal const val XK_Break = 0xff6b +internal const val XK_Mode_switch = 0xff7e /* Character set switch */ +internal const val XK_script_switch = 0xff7e /* Alias for mode_switch */ +internal const val XK_Num_Lock = 0xff7f +internal const val XK_KP_Space = 0xff80 /* Space */ +internal const val XK_KP_Tab = 0xff89 +internal const val XK_KP_Enter = 0xff8d /* Enter */ +internal const val XK_KP_F1 = 0xff91 /* PF1, KP_A, ... */ +internal const val XK_KP_F2 = 0xff92 +internal const val XK_KP_F3 = 0xff93 +internal const val XK_KP_F4 = 0xff94 +internal const val XK_KP_Home = 0xff95 +internal const val XK_KP_Left = 0xff96 +internal const val XK_KP_Up = 0xff97 +internal const val XK_KP_Right = 0xff98 +internal const val XK_KP_Down = 0xff99 +internal const val XK_KP_Prior = 0xff9a +internal const val XK_KP_Page_Up = 0xff9a +internal const val XK_KP_Next = 0xff9b +internal const val XK_KP_Page_Down = 0xff9b +internal const val XK_KP_End = 0xff9c +internal const val XK_KP_Begin = 0xff9d +internal const val XK_KP_Insert = 0xff9e +internal const val XK_KP_Delete = 0xff9f +internal const val XK_KP_Equal = 0xffbd /* Equals */ +internal const val XK_KP_Multiply = 0xffaa +internal const val XK_KP_Add = 0xffab +internal const val XK_KP_Separator = 0xffac /* Separator, often comma */ +internal const val XK_KP_Subtract = 0xffad +internal const val XK_KP_Decimal = 0xffae +internal const val XK_KP_Divide = 0xffaf +internal const val XK_KP_0 = 0xffb0 +internal const val XK_KP_1 = 0xffb1 +internal const val XK_KP_2 = 0xffb2 +internal const val XK_KP_3 = 0xffb3 +internal const val XK_KP_4 = 0xffb4 +internal const val XK_KP_5 = 0xffb5 +internal const val XK_KP_6 = 0xffb6 +internal const val XK_KP_7 = 0xffb7 +internal const val XK_KP_8 = 0xffb8 +internal const val XK_KP_9 = 0xffb9 +internal const val XK_F1 = 0xffbe +internal const val XK_F2 = 0xffbf +internal const val XK_F3 = 0xffc0 +internal const val XK_F4 = 0xffc1 +internal const val XK_F5 = 0xffc2 +internal const val XK_F6 = 0xffc3 +internal const val XK_F7 = 0xffc4 +internal const val XK_F8 = 0xffc5 +internal const val XK_F9 = 0xffc6 +internal const val XK_F10 = 0xffc7 +internal const val XK_F11 = 0xffc8 +internal const val XK_L1 = 0xffc8 +internal const val XK_F12 = 0xffc9 +internal const val XK_L2 = 0xffc9 +internal const val XK_F13 = 0xffca +internal const val XK_L3 = 0xffca +internal const val XK_F14 = 0xffcb +internal const val XK_L4 = 0xffcb +internal const val XK_F15 = 0xffcc +internal const val XK_L5 = 0xffcc +internal const val XK_F16 = 0xffcd +internal const val XK_L6 = 0xffcd +internal const val XK_F17 = 0xffce +internal const val XK_L7 = 0xffce +internal const val XK_F18 = 0xffcf +internal const val XK_L8 = 0xffcf +internal const val XK_F19 = 0xffd0 +internal const val XK_L9 = 0xffd0 +internal const val XK_F20 = 0xffd1 +internal const val XK_L10 = 0xffd1 +internal const val XK_F21 = 0xffd2 +internal const val XK_R1 = 0xffd2 +internal const val XK_F22 = 0xffd3 +internal const val XK_R2 = 0xffd3 +internal const val XK_F23 = 0xffd4 +internal const val XK_R3 = 0xffd4 +internal const val XK_F24 = 0xffd5 +internal const val XK_R4 = 0xffd5 +internal const val XK_F25 = 0xffd6 +internal const val XK_R5 = 0xffd6 +internal const val XK_F26 = 0xffd7 +internal const val XK_R6 = 0xffd7 +internal const val XK_F27 = 0xffd8 +internal const val XK_R7 = 0xffd8 +internal const val XK_F28 = 0xffd9 +internal const val XK_R8 = 0xffd9 +internal const val XK_F29 = 0xffda +internal const val XK_R9 = 0xffda +internal const val XK_F30 = 0xffdb +internal const val XK_R10 = 0xffdb +internal const val XK_F31 = 0xffdc +internal const val XK_R11 = 0xffdc +internal const val XK_F32 = 0xffdd +internal const val XK_R12 = 0xffdd +internal const val XK_F33 = 0xffde +internal const val XK_R13 = 0xffde +internal const val XK_F34 = 0xffdf +internal const val XK_R14 = 0xffdf +internal const val XK_F35 = 0xffe0 +internal const val XK_R15 = 0xffe0 +internal const val XK_Shift_L = 0xffe1 /* Left shift */ +internal const val XK_Shift_R = 0xffe2 /* Right shift */ +internal const val XK_Control_L = 0xffe3 /* Left control */ +internal const val XK_Control_R = 0xffe4 /* Right control */ +internal const val XK_Caps_Lock = 0xffe5 /* Caps lock */ +internal const val XK_Shift_Lock = 0xffe6 /* Shift lock */ +internal const val XK_Meta_L = 0xffe7 /* Left meta */ +internal const val XK_Meta_R = 0xffe8 /* Right meta */ +internal const val XK_Alt_L = 0xffe9 /* Left alt */ +internal const val XK_Alt_R = 0xffea /* Right alt */ +internal const val XK_Super_L = 0xffeb /* Left super */ +internal const val XK_Super_R = 0xffec /* Right super */ +internal const val XK_Hyper_L = 0xffed /* Left hyper */ +internal const val XK_Hyper_R = 0xffee /* Right hyper */ + + +internal val XK_KeyMap: IntMap by lazy { + IntMap().apply { + this[XK_space] = Key.SPACE + this[XK_exclam] = Key.UNKNOWN + this[XK_quotedbl] = Key.UNKNOWN + this[XK_numbersign] = Key.UNKNOWN + this[XK_dollar] = Key.UNKNOWN + this[XK_percent] = Key.UNKNOWN + this[XK_ampersand] = Key.UNKNOWN + this[XK_apostrophe] = Key.APOSTROPHE + this[XK_quoteright] = Key.UNKNOWN + this[XK_parenleft] = Key.UNKNOWN + this[XK_parenright] = Key.UNKNOWN + this[XK_asterisk] = Key.UNKNOWN + this[XK_plus] = Key.KP_ADD + this[XK_comma] = Key.COMMA + this[XK_minus] = Key.MINUS + this[XK_period] = Key.PERIOD + this[XK_slash] = Key.SLASH + this[XK_0] = Key.N0 + this[XK_1] = Key.N1 + this[XK_2] = Key.N2 + this[XK_3] = Key.N3 + this[XK_4] = Key.N4 + this[XK_5] = Key.N5 + this[XK_6] = Key.N6 + this[XK_7] = Key.N7 + this[XK_8] = Key.N8 + this[XK_9] = Key.N9 + this[XK_colon] = Key.UNKNOWN + this[XK_semicolon] = Key.SEMICOLON + this[XK_less] = Key.UNKNOWN + this[XK_equal] = Key.EQUAL + this[XK_greater] = Key.UNKNOWN + this[XK_question] = Key.UNKNOWN + this[XK_at] = Key.UNKNOWN + this[XK_A] = Key.A + this[XK_B] = Key.B + this[XK_C] = Key.C + this[XK_D] = Key.D + this[XK_E] = Key.E + this[XK_F] = Key.F + this[XK_G] = Key.G + this[XK_H] = Key.H + this[XK_I] = Key.I + this[XK_J] = Key.J + this[XK_K] = Key.K + this[XK_L] = Key.L + this[XK_M] = Key.M + this[XK_N] = Key.N + this[XK_O] = Key.O + this[XK_P] = Key.P + this[XK_Q] = Key.Q + this[XK_R] = Key.R + this[XK_S] = Key.S + this[XK_T] = Key.T + this[XK_U] = Key.U + this[XK_V] = Key.V + this[XK_W] = Key.W + this[XK_X] = Key.X + this[XK_Y] = Key.Y + this[XK_Z] = Key.Z + this[XK_bracketleft] = Key.UNKNOWN + this[XK_backslash] = Key.BACKSLASH + this[XK_bracketright] = Key.UNKNOWN + this[XK_asciicircum] = Key.UNKNOWN + this[XK_underscore] = Key.UNKNOWN + this[XK_grave] = Key.UNKNOWN + this[XK_quoteleft] = Key.UNKNOWN + this[XK_a] = Key.A + this[XK_b] = Key.B + this[XK_c] = Key.C + this[XK_d] = Key.D + this[XK_e] = Key.E + this[XK_f] = Key.F + this[XK_g] = Key.G + this[XK_h] = Key.H + this[XK_i] = Key.I + this[XK_j] = Key.J + this[XK_k] = Key.K + this[XK_l] = Key.L + this[XK_m] = Key.M + this[XK_n] = Key.N + this[XK_o] = Key.O + this[XK_p] = Key.P + this[XK_q] = Key.Q + this[XK_r] = Key.R + this[XK_s] = Key.S + this[XK_t] = Key.T + this[XK_u] = Key.U + this[XK_v] = Key.V + this[XK_w] = Key.W + this[XK_x] = Key.X + this[XK_y] = Key.Y + this[XK_z] = Key.Z + this[XK_leftarrow] = Key.LEFT + this[XK_uparrow] = Key.UP + this[XK_rightarrow] = Key.RIGHT + this[XK_downarrow] = Key.DOWN + this[XK_BackSpace] = Key.BACKSPACE + this[XK_Tab] = Key.TAB + this[XK_Linefeed] = Key.UNKNOWN + this[XK_Clear] = Key.CLEAR + this[XK_Return] = Key.RETURN + this[XK_Pause] = Key.PAUSE + this[XK_Scroll_Lock] = Key.SCROLL_LOCK + this[XK_Sys_Req] = Key.UNKNOWN + this[XK_Escape] = Key.ESCAPE + this[XK_Delete] = Key.DELETE + this[XK_Home] = Key.HOME + this[XK_Left] = Key.LEFT + this[XK_Up] = Key.UP + this[XK_Right] = Key.RIGHT + this[XK_Down] = Key.DOWN + this[XK_Prior] = Key.UNKNOWN + this[XK_Page_Up] = Key.PAGE_UP + this[XK_Next] = Key.UNKNOWN + this[XK_Page_Down] = Key.PAGE_DOWN + this[XK_End] = Key.END + this[XK_Begin] = Key.UNKNOWN + this[XK_Select] = Key.UNKNOWN + this[XK_Print] = Key.PRINT_SCREEN + this[XK_Execute] = Key.UNKNOWN + this[XK_Insert] = Key.INSERT + this[XK_Undo] = Key.UNKNOWN + this[XK_Redo] = Key.UNKNOWN + this[XK_Menu] = Key.MENU + this[XK_Find] = Key.UNKNOWN + this[XK_Cancel] = Key.CANCEL + this[XK_Help] = Key.HELP + this[XK_Break] = Key.UNKNOWN + this[XK_Mode_switch] = Key.UNKNOWN + this[XK_script_switch] = Key.UNKNOWN + this[XK_Num_Lock] = Key.NUM_LOCK + this[XK_KP_Space] = Key.UNKNOWN + this[XK_KP_Tab] = Key.UNKNOWN + this[XK_KP_Enter] = Key.KP_ENTER + this[XK_KP_F1] = Key.F1 + this[XK_KP_F2] = Key.F2 + this[XK_KP_F3] = Key.F3 + this[XK_KP_F4] = Key.F4 + this[XK_KP_Home] = Key.HOME + this[XK_KP_Left] = Key.KP_LEFT + this[XK_KP_Up] = Key.KP_UP + this[XK_KP_Right] = Key.KP_RIGHT + this[XK_KP_Down] = Key.KP_DOWN + this[XK_KP_Prior] = Key.UNKNOWN + this[XK_KP_Page_Up] = Key.UNKNOWN + this[XK_KP_Next] = Key.UNKNOWN + this[XK_KP_Page_Down] = Key.UNKNOWN + this[XK_KP_End] = Key.END + this[XK_KP_Begin] = Key.HOME + this[XK_KP_Insert] = Key.INSERT + this[XK_KP_Delete] = Key.DELETE + this[XK_KP_Equal] = Key.KP_EQUAL + this[XK_KP_Multiply] = Key.KP_MULTIPLY + this[XK_KP_Add] = Key.KP_ADD + this[XK_KP_Separator] = Key.KP_SEPARATOR + this[XK_KP_Subtract] = Key.KP_SUBTRACT + this[XK_KP_Decimal] = Key.KP_DECIMAL + this[XK_KP_Divide] = Key.KP_DIVIDE + this[XK_KP_0] = Key.KP_0 + this[XK_KP_1] = Key.KP_1 + this[XK_KP_2] = Key.KP_2 + this[XK_KP_3] = Key.KP_3 + this[XK_KP_4] = Key.KP_4 + this[XK_KP_5] = Key.KP_5 + this[XK_KP_6] = Key.KP_6 + this[XK_KP_7] = Key.KP_7 + this[XK_KP_8] = Key.KP_8 + this[XK_KP_9] = Key.KP_9 + this[XK_F1] = Key.F1 + this[XK_F2] = Key.F2 + this[XK_F3] = Key.F3 + this[XK_F4] = Key.F4 + this[XK_F5] = Key.F5 + this[XK_F6] = Key.F6 + this[XK_F7] = Key.F7 + this[XK_F8] = Key.F8 + this[XK_F9] = Key.F9 + this[XK_F10] = Key.F10 + this[XK_F11] = Key.F11 + this[XK_F12] = Key.F12 + this[XK_F13] = Key.F13 + this[XK_F14] = Key.F14 + this[XK_F15] = Key.F15 + this[XK_F16] = Key.F16 + this[XK_F17] = Key.F17 + this[XK_F18] = Key.F18 + this[XK_F19] = Key.F19 + this[XK_F20] = Key.F20 + this[XK_F21] = Key.F21 + this[XK_F22] = Key.F22 + this[XK_F23] = Key.F23 + this[XK_F24] = Key.F24 + this[XK_F25] = Key.F25 + this[XK_F26] = Key.UNKNOWN + this[XK_F27] = Key.UNKNOWN + this[XK_F28] = Key.UNKNOWN + this[XK_F29] = Key.UNKNOWN + this[XK_F30] = Key.UNKNOWN + this[XK_F31] = Key.UNKNOWN + this[XK_F32] = Key.UNKNOWN + this[XK_F33] = Key.UNKNOWN + this[XK_F34] = Key.UNKNOWN + this[XK_F35] = Key.UNKNOWN + + this[XK_R1] = Key.UNKNOWN + this[XK_R2] = Key.UNKNOWN + this[XK_R3] = Key.UNKNOWN + this[XK_R4] = Key.UNKNOWN + this[XK_R5] = Key.UNKNOWN + this[XK_R6] = Key.UNKNOWN + this[XK_R7] = Key.UNKNOWN + this[XK_R8] = Key.UNKNOWN + this[XK_R9] = Key.UNKNOWN + this[XK_R10] = Key.UNKNOWN + this[XK_R11] = Key.UNKNOWN + this[XK_R12] = Key.UNKNOWN + this[XK_R13] = Key.UNKNOWN + this[XK_R14] = Key.UNKNOWN + this[XK_R15] = Key.UNKNOWN + + this[XK_L1] = Key.UNKNOWN + this[XK_L2] = Key.UNKNOWN + this[XK_L3] = Key.UNKNOWN + this[XK_L4] = Key.UNKNOWN + this[XK_L5] = Key.UNKNOWN + this[XK_L6] = Key.UNKNOWN + this[XK_L7] = Key.UNKNOWN + this[XK_L8] = Key.UNKNOWN + this[XK_L9] = Key.UNKNOWN + this[XK_L10] = Key.UNKNOWN + + this[XK_Shift_L] = Key.LEFT_SHIFT + this[XK_Shift_R] = Key.RIGHT_SHIFT + this[XK_Control_L] = Key.LEFT_CONTROL + this[XK_Control_R] = Key.RIGHT_CONTROL + this[XK_Caps_Lock] = Key.CAPS_LOCK + this[XK_Shift_Lock] = Key.CAPS_LOCK + this[XK_Meta_L] = Key.LEFT_SUPER + this[XK_Meta_R] = Key.RIGHT_SUPER + this[XK_Alt_L] = Key.LEFT_ALT + this[XK_Alt_R] = Key.RIGHT_ALT + this[XK_Super_L] = Key.LEFT_SUPER + this[XK_Super_R] = Key.RIGHT_SUPER + this[XK_Hyper_L] = Key.LEFT_SUPER + this[XK_Hyper_R] = Key.RIGHT_SUPER + } +} diff --git a/korge-foundation/src/common/korlibs/annotations/DeprecatedParameter.kt b/korge-foundation/src/common/korlibs/annotations/DeprecatedParameter.kt new file mode 100644 index 0000000000..4536c41d58 --- /dev/null +++ b/korge-foundation/src/common/korlibs/annotations/DeprecatedParameter.kt @@ -0,0 +1,6 @@ +package korlibs.annotations + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class DeprecatedParameter( + val reason: String +) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 42652f76de..b47fbbca4f 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -39,7 +39,10 @@ class SyncEventLoop( } } private val startTime = TimeSource.Monotonic.markNow() - private val now: Duration get() = startTime.elapsedNow() + + var nowProvider: () -> Duration = { startTime.elapsedNow() } + + private val now: Duration get() = nowProvider() private val tasks = ArrayDeque<() -> Unit>() private val timedTasks = TGenPriorityQueue { a, b -> a.compareTo(b) } @@ -109,12 +112,22 @@ class SyncEventLoop( return count } + var uncatchedExceptionHandler: (Throwable) -> Unit = { it.printStackTrace() } + + private inline fun runCatchingExceptions(block: () -> Unit) { + try { + block() + } catch (e: Throwable) { + uncatchedExceptionHandler(e) + } + } + fun runAvailableNextTask(): Boolean { val timedTask = lock { if (timedTasks.isNotEmpty() && shouldTimedTaskRun(timedTasks.head)) timedTasks.removeHead() else null } if (timedTask != null) { - timedTask.callback() + runCatchingExceptions { timedTask.callback() } if (timedTask.interval) { timedTask.timeMark = maxOf(timedTask.timeMark + timedTask.time, now) //println("READDED: timedTask.now=${timedTask.now}") @@ -124,7 +137,7 @@ class SyncEventLoop( val task = lock { if (tasks.isNotEmpty()) tasks.removeFirst() else null } - task?.invoke() + runCatchingExceptions { task?.invoke() } return task != null || timedTask != null } diff --git a/korge-foundation/src/common/korlibs/logger/Logger.kt b/korge-foundation/src/common/korlibs/logger/Logger.kt index b80c74a2f5..54f80892bb 100644 --- a/korge-foundation/src/common/korlibs/logger/Logger.kt +++ b/korge-foundation/src/common/korlibs/logger/Logger.kt @@ -1,5 +1,9 @@ package korlibs.logger +import korlibs.datastructure.lock.* +import korlibs.time.* +import kotlin.time.* + /** * Utility to log messages. * @@ -37,6 +41,7 @@ class Logger private constructor(val name: String, val normalizedName: String, v val isLocalOutputSet: Boolean get() = optOutput != null companion object { + private val Logger_lock = Lock() private val Logger_loggers = HashMap() /** The default [Level] used for all [Logger] that doesn't have its [Logger.level] set */ @@ -46,7 +51,7 @@ class Logger private constructor(val name: String, val normalizedName: String, v var defaultOutput: Output = DefaultLogOutput /** Gets a [Logger] from its [name] */ - operator fun invoke(name: String): Logger { + operator fun invoke(name: String): Logger = Logger_lock { val normalizedName = normalizeName(name) if (Logger_loggers[normalizedName] == null) { val logger = Logger(name, normalizedName, true) @@ -138,6 +143,12 @@ class Logger private constructor(val name: String, val normalizedName: String, v /** Traces the lazily executed [msg] if the [Logger.level] is at least [Level.TRACE] */ inline fun trace(msg: () -> Any?) = log(Level.TRACE, msg) + inline fun logTime(name: String, level: Level = Level.INFO, block: () -> T): T { + val (value, time) = measureTimedValue(block) + log(level) { "$name: $time" } + return value + } + @PublishedApi internal fun actualLog(level: Level, msg: Any?) { output.output(this, level, msg) } } diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt new file mode 100644 index 0000000000..cf52b28e7a --- /dev/null +++ b/korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt @@ -0,0 +1,16 @@ +package korlibs.memory.dyn + +import com.sun.jna.* + +annotation class NativeName(val name: String) { + companion object { + val OPTIONS = mapOf( + Library.OPTION_FUNCTION_MAPPER to FunctionMapper { _, method -> + method.getAnnotation(NativeName::class.java)?.name ?: method.name + } + ) + } +} + +inline fun NativeLoad(name: String): T = Native.load(name, T::class.java, NativeName.OPTIONS) as T + diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt index 00ca7bfab9..8a56d9506d 100644 --- a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt +++ b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt @@ -10,20 +10,8 @@ import java.util.concurrent.* //inline class ID(val id: Long) typealias ID = Long -annotation class NativeName(val name: String) { - companion object { - val OPTIONS = mapOf( - Library.OPTION_FUNCTION_MAPPER to FunctionMapper { _, method -> - method.getAnnotation(NativeName::class.java)?.name ?: method.name - } - ) - } -} - typealias NSRectPtr = Pointer -inline fun NativeLoad(name: String): T = Native.load(name, T::class.java, NativeName.OPTIONS) as T - // https://developer.apple.com/documentation/objectivec/objective-c_runtime interface ObjectiveC : Library { fun objc_copyProtocolList(outCount: IntArray): Pointer diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainHaptic.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainHaptic.kt index 1aec04d7da..48b3725ae0 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainHaptic.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainHaptic.kt @@ -1,10 +1,10 @@ package samples -import korlibs.korge.scene.Scene +import korlibs.korge.scene.* import korlibs.korge.service.haptic.* -import korlibs.korge.ui.clicked -import korlibs.korge.ui.uiButton -import korlibs.korge.view.SContainer +import korlibs.korge.ui.* +import korlibs.korge.view.* +import korlibs.render.* class MainHaptic : Scene() { override suspend fun SContainer.sceneMain() { diff --git a/korge-sandbox/src/jvmMain/kotlin/AwtSandboxSample.kt b/korge-sandbox/src/jvmMain/kotlin/AwtSandboxSample.kt index 2ae64aa027..002291b21d 100644 --- a/korge-sandbox/src/jvmMain/kotlin/AwtSandboxSample.kt +++ b/korge-sandbox/src/jvmMain/kotlin/AwtSandboxSample.kt @@ -1,13 +1,4 @@ - -import korlibs.datastructure.iterators.* -import korlibs.image.color.* -import korlibs.korge.* -import korlibs.korge.input.* -import korlibs.korge.ui.* -import korlibs.math.geom.* -import java.awt.* -import javax.swing.* - +/* object AwtSandboxSample { @JvmStatic fun main(args: Array) { @@ -77,3 +68,4 @@ object AwtSandboxSample { frame.validate() } } +*/ diff --git a/korge/src/common/korlibs/event/TextInputManager.kt b/korge/src/common/korlibs/event/TextInputManager.kt deleted file mode 100644 index 6816f6bd3f..0000000000 --- a/korge/src/common/korlibs/event/TextInputManager.kt +++ /dev/null @@ -1,12 +0,0 @@ -package korlibs.event - -interface TextInputPosition : Comparator { -} - -interface TextInputRange { - val start: TextInputPosition - val end: TextInputPosition -} - -interface TextInputManager { -} diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index e98437074d..9e7596acc2 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -1,32 +1,24 @@ package korlibs.korge -import korlibs.audio.sound.* -import korlibs.datastructure.iterators.* -import korlibs.event.* +import korlibs.annotations.* import korlibs.graphics.log.* import korlibs.image.color.* import korlibs.image.format.* import korlibs.inject.* -import korlibs.io.async.* import korlibs.io.dynamic.* import korlibs.io.file.std.* import korlibs.io.resources.* -import korlibs.io.worker.* -import korlibs.korge.input.* import korlibs.korge.internal.* import korlibs.korge.logger.* import korlibs.korge.render.* import korlibs.korge.resources.* import korlibs.korge.scene.* -import korlibs.korge.stat.* import korlibs.korge.view.* import korlibs.logger.* import korlibs.math.geom.* -import korlibs.memory.* import korlibs.platform.* import korlibs.render.* import korlibs.time.* -import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.reflect.* @@ -43,17 +35,12 @@ data class KorgeDisplayMode(val scaleMode: ScaleMode, val scaleAnchor: Anchor, v } } -@Target(AnnotationTarget.VALUE_PARAMETER) -private annotation class DeprecatedParameter( - val reason: String -) - suspend fun Korge( args: Array = arrayOf(), imageFormats: ImageFormat = RegisteredImageFormats, gameWindow: GameWindow? = null, //val eventDispatcher: EventDispatcher = gameWindow ?: DummyEventDispatcher, // Removed - mainSceneClass: KClass? = null, + @DeprecatedParameter("Create a sceneContainer instead") mainSceneClass: KClass? = null, timeProvider: TimeProvider = TimeProvider, injector: Injector = Injector(), configInjector: Injector.() -> Unit = {}, @@ -80,17 +67,17 @@ suspend fun Korge( @DeprecatedParameter("Use backgroundColor instead") bgcolor: RGBA? = Colors.BLACK, backgroundColor: RGBA? = bgcolor, - quality: GameWindow.Quality = GameWindow.Quality.PERFORMANCE, + quality: GameWindowQuality = GameWindowQuality.PERFORMANCE, icon: String? = null, - multithreaded: Boolean? = null, + @DeprecatedParameter("Ignored") multithreaded: Boolean? = null, forceRenderEveryFrame: Boolean = true, - main: (suspend Stage.() -> Unit) = {}, + @DeprecatedParameter("Use entry instead") main: (suspend Stage.() -> Unit) = {}, debugAg: Boolean = false, debugFontExtraScale: Double = 1.0, debugFontColor: RGBA = Colors.WHITE, stageBuilder: (Views) -> Stage = { Stage(it) }, targetFps: Double = 0.0, - entry: suspend Stage.() -> Unit = {} + entry: suspend Stage.() -> Unit = {}, ): Unit = Korge( args = args, imageFormats = imageFormats, gameWindow = gameWindow, mainSceneClass = mainSceneClass, timeProvider = timeProvider, injector = injector, configInjector = configInjector, debug = debug, @@ -132,7 +119,7 @@ data class Korge( val displayMode: KorgeDisplayMode = KorgeDisplayMode.DEFAULT, val title: String = "Game", val backgroundColor: RGBA? = Colors.BLACK, - val quality: GameWindow.Quality = GameWindow.Quality.PERFORMANCE, + val quality: GameWindowQuality = GameWindowQuality.PERFORMANCE, val icon: String? = null, val multithreaded: Boolean? = null, val forceRenderEveryFrame: Boolean = true, @@ -151,7 +138,14 @@ data class Korge( } suspend fun start(entry: suspend Stage.() -> Unit = this.main) { - KorgeRunner.invoke(this.copy(main = entry)) + if (!Platform.isJsBrowser) { + configureLoggerFromProperties(localCurrentDirVfs["klogger.properties"]) + } + val config = this + val gameWindow = (config.gameWindow ?: coroutineContext[GameWindow] ?: CreateDefaultGameWindow(GameWindowCreationConfig(multithreaded = config.multithreaded, fullscreen = config.fullscreen))) + gameWindow.configureKorge(config) { + entry() + } } } @@ -162,37 +156,90 @@ suspend fun Korge(entry: suspend Stage.() -> Unit) { Korge().start(entry) } suspend fun KorgeWithConfig(config: KorgeConfig, entry: suspend Stage.() -> Unit) { config.start(entry) } -/** - * Entry point for games written in Korge. - * You have to call the [Korge] method by either providing some parameters, or a [Korge.Config] object. - */ -object KorgeRunner { - suspend operator fun invoke(config: Korge) = Worker.init { - RegisteredImageFormats.register(config.imageFormats) +data class KorgeArgs(val args: Array) - val iconPath = config.icon - val imageFormats = config.imageFormats - val entry = config.main - val multithreaded = config.multithreaded - val windowSize = config.windowSize +/** + * Configures a [GameWindow] to run a [Korge] application. + **/ +fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspend Stage.() -> Unit = {}) { + val gameWindow = this + val iconPath = config.icon + val imageFormats = config.imageFormats + val entry = config.main + val multithreaded = config.multithreaded + val windowSize = config.windowSize + val views = Views( + gameWindow = gameWindow, + coroutineContext = gameWindow.coroutineDispatcher, + ag = if (config.debugAg) AGPrint() else gameWindow.ag, + injector = config.injector, + gameId = config.gameId, + timeProvider = config.timeProvider, + settingsFolder = config.settingsFolder, + batchMaxQuads = config.batchMaxQuads, + stageBuilder = config.stageBuilder + ).also { + if (Platform.isJsBrowser) { + Dyn.global["views"] = it + } + } + Korge.logger.logTime("configureGameWindow") { + gameWindow.configure(windowSize, config.title, null, config.fullscreen, config.backgroundColor ?: Colors.BLACK) + } + gameWindow.quality = quality + gameWindow.backgroundColor = config.backgroundColor ?: Colors.BLACK + + RegisteredImageFormats.register(config.imageFormats) + val injector = views.injector + config.configInjector(injector) + injector.mapInstance(views) + injector.mapInstance(views.ag) + injector.mapInstance(KorgeArgs(config.args)) + injector.mapInstance(Resources::class, views.globalResources) + injector.mapSingleton(ResourcesRoot::class) { ResourcesRoot() } + injector.mapInstance(views.input) + injector.mapInstance(views.stats) + injector.mapInstance(CoroutineContext::class, views.coroutineContext) + injector.mapPrototype(EmptyScene::class) { EmptyScene() } + injector.mapInstance(TimeProvider::class, views.timeProvider) + injector.mapInstance(GameWindow::class, gameWindow) + injector.mapInstance(KorgeConfig::class, config) + views.debugViews = gameWindow.debug + views.debugFontExtraScale = config.debugFontExtraScale + views.debugFontColor = config.debugFontColor + views.virtualWidth = config.virtualSize.width.toInt() + views.virtualHeight = config.virtualSize.height.toInt() + views.scaleAnchor = config.displayMode.scaleAnchor + views.scaleMode = config.displayMode.scaleMode + views.clipBorders = config.displayMode.clipBorders + views.targetFps = config.targetFps + + var initialized = false + + val stopwatch = Stopwatch() + gameWindow.onUpdateEvent { + if (initialized) { + //println("UPDATE") + views.update(stopwatch.getElapsedAndRestart()) + } + } - if (!Platform.isJsBrowser) { - configureLoggerFromProperties(localCurrentDirVfs["klogger.properties"]) + gameWindow.onRenderEvent { + if (initialized) { + //println("RENDER") + views.renderNew() } - val realGameWindow = (config.gameWindow ?: coroutineContext[GameWindow] ?: CreateDefaultGameWindow(GameWindowCreationConfig(multithreaded = multithreaded, fullscreen = config.fullscreen))) - realGameWindow.bgcolor = config.backgroundColor ?: Colors.BLACK - realGameWindow.loop { - val gameWindow = this - if (Platform.isNative) println("Korui[0]") - gameWindow.registerTime("configureGameWindow") { - realGameWindow.configure(windowSize, config.title, null, config.fullscreen, config.backgroundColor ?: Colors.BLACK) - } - gameWindow.registerTime("setIcon") { + } + gameWindow.queueSuspend { + Korge.logger.info { "Initializing..." } + // Initialize + try { + Korge.logger.logTime("setIcon") { try { // Do nothing when { //iconDrawable != null -> this.icon = iconDrawable.render() - iconPath != null -> this.icon = resourcesVfs[iconPath].readBitmap(imageFormats) + iconPath != null -> gameWindow.icon = resourcesVfs[iconPath].readBitmap(imageFormats) else -> Unit } } catch (e: Throwable) { @@ -200,352 +247,26 @@ object KorgeRunner { e.printStackTrace() } } - this.quality = quality - if (Platform.isNative) println("CanvasApplicationEx.IN[0]") - val input = Input() - val stats = Stats() - - // Use this once Korgw is on 1.12.5 - //val views = Views(gameWindow.getCoroutineDispatcherWithCurrentContext() + SupervisorJob(), ag, injector, input, timeProvider, stats, gameWindow) - val views: Views = Views( - coroutineContext = coroutineContext + gameWindow.coroutineDispatcher + InjectorContext(config.injector) + SupervisorJob(), - ag = if (config.debugAg) AGPrint() else ag, - injector = config.injector, - input = input, - timeProvider = config.timeProvider, - stats = stats, - gameWindow = gameWindow, - gameId = config.gameId, - settingsFolder = config.settingsFolder, - batchMaxQuads = config.batchMaxQuads, - stageBuilder = config.stageBuilder - ).also { - it.init() - if (Platform.isJsBrowser) { - Dyn.global["views"] = it - } - } - - config.injector.mapInstance(GameWindow::class, gameWindow) - config.injector.mapInstance(KorgeConfig::class, config) - views.debugViews = debug - views.debugFontExtraScale = config.debugFontExtraScale - views.debugFontColor = config.debugFontColor - views.virtualWidth = config.virtualSize.width.toInt() - views.virtualHeight = config.virtualSize.height.toInt() - views.scaleAnchor = config.displayMode.scaleAnchor - views.scaleMode = config.displayMode.scaleMode - views.clipBorders = config.displayMode.clipBorders - views.targetFps = config.targetFps - //Korge.prepareViews(views, gameWindow, bgcolor != null, bgcolor ?: Colors.TRANSPARENT_BLACK) - gameWindow.registerTime("prepareViews") { + Korge.logger.logTime("prepareViews") { KorgeReload.registerEventDispatcher(gameWindow) - prepareViewsBase(views, gameWindow, true, bgcolor, TimeSpan.NIL, config.forceRenderEveryFrame, config.configInjector).await() + @Suppress("OPT_IN_USAGE") + views.prepareViewsBase(gameWindow, true, gameWindow.bgcolor, TimeSpan.NIL, config.forceRenderEveryFrame, config.configInjector).await() } - gameWindow.registerTime("completeViews") { + Korge.logger.logTime("completeViews") { // Here we can install a debugger, etc. completeViews(views) } - - views.launchImmediately { - coroutineScope { - //println("coroutineContext: $coroutineContext") - //println("GameWindow: ${coroutineContext[GameWindow]}") - if (config.mainSceneClass != null) { - views.stage.sceneContainer().changeTo(config.mainSceneClass) - } - entry(views.stage) - if (config.blocking) { - // @TODO: Do not complete to prevent job cancelation? - gameWindow.waitClose() - } - } - } - - if (config.blocking) { - // @TODO: Do not complete to prevent job cancelation? - gameWindow.waitClose() - gameWindow.exit() - } - } - } - - @KorgeInternal - fun prepareViewsBase( - views: Views, - eventDispatcher: EventListener, - clearEachFrame: Boolean = true, - bgcolor: RGBA = Colors.TRANSPARENT, - fixedSizeStep: TimeSpan = TimeSpan.NIL, - forceRenderEveryFrame: Boolean = true, - configInjector: Injector.() -> Unit = {}, - ): CompletableDeferred { - - val injector = views.injector - injector.mapInstance(views) - injector.mapInstance(views.ag) - injector.mapInstance(Resources::class, views.globalResources) - injector.mapSingleton(ResourcesRoot::class) { ResourcesRoot() } - injector.mapInstance(views.input) - injector.mapInstance(views.stats) - injector.mapInstance(CoroutineContext::class, views.coroutineContext) - injector.mapPrototype(EmptyScene::class) { EmptyScene() } - injector.mapInstance(TimeProvider::class, views.timeProvider) - configInjector(injector) - - val input = views.input - val ag = views.ag - var downPos = Point.ZERO - var upPos = Point.ZERO - var downTime = DateTime.EPOCH - var moveTime = DateTime.EPOCH - var upTime = DateTime.EPOCH - var moveMouseOutsideInNextFrame = false - val mouseTouchId = -1 - views.forceRenderEveryFrame = forceRenderEveryFrame - - // devicePixelRatio might change at runtime by changing the resolution or changing the screen of the window - fun getRealXY(x: Double, y: Double, scaleCoords: Boolean): Point { - return views.windowToGlobalCoords(Point(x, y)) - } - - fun mouseDown(type: String, p: Point, button: MouseButton) { - input.toggleButton(button, true) - input.setMouseGlobalPos(p, down = false) - input.setMouseGlobalPos(p, down = true) - views.mouseUpdated() - downPos = input.mousePos - downTime = DateTime.now() - input.mouseInside = true - } - - fun mouseUp(type: String, p: Point, button: MouseButton) { - //Console.log("mouseUp: $name") - input.toggleButton(button, false) - input.setMouseGlobalPos(p, down = false) - views.mouseUpdated() - upPos = views.input.mousePos - } - - fun mouseMove(type: String, p: Point, inside: Boolean) { - views.input.setMouseGlobalPos(p, down = false) - views.input.mouseInside = inside - if (!inside) { - moveMouseOutsideInNextFrame = true - } - views.mouseUpdated() - moveTime = DateTime.now() - } - - fun mouseDrag(type: String, p: Point) { - views.input.setMouseGlobalPos(p, down = false) - views.mouseUpdated() - moveTime = DateTime.now() + } finally { + Korge.logger.info { "Initialized" } + initialized = true } - val mouseTouchEvent = TouchEvent() - - fun dispatchSimulatedTouchEvent( - p: Point, - button: MouseButton, - type: TouchEvent.Type, - status: Touch.Status - ) { - mouseTouchEvent.screen = 0 - mouseTouchEvent.emulated = true - mouseTouchEvent.currentTime = DateTime.now() - mouseTouchEvent.scaleCoords = false - mouseTouchEvent.startFrame(type) - mouseTouchEvent.touch(button.id, p, status, kind = Touch.Kind.MOUSE, button = button) - mouseTouchEvent.endFrame() - views.dispatch(mouseTouchEvent) + config.main(views.stage) + block(views.stage) + if (config.mainSceneClass != null) { + views.stage.sceneContainer().changeTo(config.mainSceneClass) } - - eventDispatcher.onEvents(*MouseEvent.Type.ALL) { e -> - //println("MOUSE: $e") - Korge.logger.trace { "eventDispatcher.addEventListener:$e" } - val p = getRealXY(e.x.toDouble(), e.y.toDouble(), e.scaleCoords) - when (e.type) { - MouseEvent.Type.DOWN -> { - mouseDown("mouseDown", p, e.button) - //updateTouch(mouseTouchId, x, y, start = true, end = false) - dispatchSimulatedTouchEvent(p, e.button, TouchEvent.Type.START, Touch.Status.ADD) - } - - MouseEvent.Type.UP -> { - mouseUp("mouseUp", p, e.button) - //updateTouch(mouseTouchId, x, y, start = false, end = true) - dispatchSimulatedTouchEvent(p, e.button, TouchEvent.Type.END, Touch.Status.REMOVE) - } - - MouseEvent.Type.DRAG -> { - mouseDrag("onMouseDrag", p) - //updateTouch(mouseTouchId, x, y, start = false, end = false) - dispatchSimulatedTouchEvent(p, e.button, TouchEvent.Type.MOVE, Touch.Status.KEEP) - } - - MouseEvent.Type.MOVE -> mouseMove("mouseMove", p, inside = true) - MouseEvent.Type.CLICK -> Unit - MouseEvent.Type.ENTER -> mouseMove("mouseEnter", p, inside = true) - MouseEvent.Type.EXIT -> mouseMove("mouseExit", p, inside = false) - MouseEvent.Type.SCROLL -> Unit - } - views.dispatch(e) - } - - eventDispatcher.onEvents(*KeyEvent.Type.ALL) { e -> - Korge.logger.trace { "eventDispatcher.addEventListener:$e" } - views.dispatch(e) - } - eventDispatcher.onEvents(*GestureEvent.Type.ALL) { e -> - Korge.logger.trace { "eventDispatcher.addEventListener:$e" } - views.dispatch(e) - } - - eventDispatcher.onEvents(*DropFileEvent.Type.ALL) { e -> views.dispatch(e) } - eventDispatcher.onEvent(ResumeEvent) { e -> - views.dispatch(e) - nativeSoundProvider.paused = false - } - eventDispatcher.onEvent(PauseEvent) { e -> - views.dispatch(e) - nativeSoundProvider.paused = true - } - eventDispatcher.onEvent(StopEvent) { e -> views.dispatch(e) } - eventDispatcher.onEvent(DestroyEvent) { e -> - try { - views.dispatch(e) - } finally { - views.launchImmediately { - views.close() - } - } - } - - val touchMouseEvent = MouseEvent() - eventDispatcher.onEvents(*TouchEvent.Type.ALL) { e -> - Korge.logger.trace { "eventDispatcher.addEventListener:$e" } - - input.updateTouches(e) - val ee = input.touch - for (t in ee.touches) { - t.p = getRealXY(t.x.toDouble(), t.y.toDouble(), e.scaleCoords) - } - views.dispatch(ee) - - // Touch to mouse events - if (ee.numTouches == 1) { - val start = ee.isStart - val end = ee.isEnd - val t = ee.touches.first() - val p = t.p - val x = t.x - val y = t.y - val button = MouseButton.LEFT - - //updateTouch(t.id, x, y, start, end) - when { - start -> mouseDown("onTouchStart", p, button) - end -> mouseUp("onTouchEnd", p, button) - else -> mouseMove("onTouchMove", p, inside = true) - } - views.dispatch(touchMouseEvent.also { - it.id = 0 - it.button = button - it.buttons = if (end) 0 else 1 shl button.id - it.x = x.toInt() - it.y = y.toInt() - it.scaleCoords = false - it.emulated = true - it.type = when { - start -> MouseEvent.Type.DOWN - end -> MouseEvent.Type.UP - else -> MouseEvent.Type.DRAG - } - }) - if (end) { - moveMouseOutsideInNextFrame = true - } - } - - } - - fun gamepadUpdated(e: GamePadUpdateEvent) { - e.gamepads.fastForEach { gamepad -> - input.gamepads[gamepad.index].copyFrom(gamepad) - } - input.updateConnectedGamepads() - } - - eventDispatcher.onEvents(*GamePadConnectionEvent.Type.ALL) { e -> - Korge.logger.trace { "eventDispatcher.addEventListener:$e" } - views.dispatch(e) - } - - eventDispatcher.onEvent(GamePadUpdateEvent) { e -> - gamepadUpdated(e) - views.dispatch(e) - } - - eventDispatcher.onEvent(ReshapeEvent) { e -> - //try { throw Exception() } catch (e: Throwable) { e.printStackTrace() } - //println("eventDispatcher.addEventListener: ${ag.backWidth}x${ag.backHeight} : ${e.width}x${e.height}") - //println("resized. ${ag.backWidth}, ${ag.backHeight}") - views.resized(ag.mainFrameBuffer.width, ag.mainFrameBuffer.height) - } - - //println("eventDispatcher.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) : ${views.nativeWidth}x${views.nativeHeight}") - eventDispatcher.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) - - eventDispatcher.onEvent(ReloadEvent) { views.dispatch(it) } - - var renderShown = false - views.clearEachFrame = clearEachFrame - views.clearColor = bgcolor - val firstRenderDeferred = CompletableDeferred() - - fun renderBlock(event: RenderEvent) { - //println("renderBlock: $event") - try { - views.frameUpdateAndRender( - fixedSizeStep = fixedSizeStep, - forceRender = views.forceRenderEveryFrame, - doUpdate = event.update, - doRender = event.render, - ) - - views.input.mouseOutside = false - if (moveMouseOutsideInNextFrame) { - moveMouseOutsideInNextFrame = false - views.input.mouseOutside = true - views.input.mouseInside = false - views.mouseUpdated() - } - } catch (e: Throwable) { - Korge.logger.error { "views.gameWindow.onRenderEvent:" } - e.printStackTrace() - if (views.rethrowRenderError) throw e - } - } - - views.gameWindow.onRenderEvent { event -> - //println("RenderEvent: $event") - if (!event.render) { - renderBlock(event) - } else { - views.renderContext.doRender { - if (!renderShown) { - //println("!!!!!!!!!!!!! views.gameWindow.addEventListener") - renderShown = true - firstRenderDeferred.complete(Unit) - } - renderBlock(event) - } - } - } - - return firstRenderDeferred } } diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt new file mode 100644 index 0000000000..e612806c7b --- /dev/null +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -0,0 +1,280 @@ +package korlibs.korge + +import korlibs.audio.sound.* +import korlibs.datastructure.iterators.* +import korlibs.event.* +import korlibs.image.color.* +import korlibs.inject.* +import korlibs.io.async.* +import korlibs.korge.internal.* +import korlibs.korge.view.* +import korlibs.math.geom.* +import korlibs.time.* +import kotlinx.coroutines.* + +@KorgeInternal +internal fun Views.prepareViewsBase( + eventDispatcher: EventListener, + clearEachFrame: Boolean = true, + bgcolor: RGBA = Colors.TRANSPARENT, + fixedSizeStep: TimeSpan = TimeSpan.NIL, + forceRenderEveryFrame: Boolean = true, + configInjector: Injector.() -> Unit = {}, +): CompletableDeferred { + val views = this + + configInjector(views.injector) + + val input = views.input + val ag = views.ag + var downPos = Point.ZERO + var upPos = Point.ZERO + var downTime = DateTime.EPOCH + var moveTime = DateTime.EPOCH + var upTime = DateTime.EPOCH + var moveMouseOutsideInNextFrame = false + val mouseTouchId = -1 + views.forceRenderEveryFrame = forceRenderEveryFrame + + // devicePixelRatio might change at runtime by changing the resolution or changing the screen of the window + fun getRealXY(x: Double, y: Double, scaleCoords: Boolean): Point { + return views.windowToGlobalCoords(Point(x, y)) + } + + fun mouseDown(type: String, p: Point, button: MouseButton) { + input.toggleButton(button, true) + input.setMouseGlobalPos(p, down = false) + input.setMouseGlobalPos(p, down = true) + views.mouseUpdated() + downPos = input.mousePos + downTime = DateTime.now() + input.mouseInside = true + } + + fun mouseUp(type: String, p: Point, button: MouseButton) { + //Console.log("mouseUp: $name") + input.toggleButton(button, false) + input.setMouseGlobalPos(p, down = false) + views.mouseUpdated() + upPos = views.input.mousePos + } + + fun mouseMove(type: String, p: Point, inside: Boolean) { + views.input.setMouseGlobalPos(p, down = false) + views.input.mouseInside = inside + if (!inside) { + moveMouseOutsideInNextFrame = true + } + views.mouseUpdated() + moveTime = DateTime.now() + } + + fun mouseDrag(type: String, p: Point) { + views.input.setMouseGlobalPos(p, down = false) + views.mouseUpdated() + moveTime = DateTime.now() + } + + val mouseTouchEvent = TouchEvent() + + fun dispatchSimulatedTouchEvent( + p: Point, + button: MouseButton, + type: TouchEvent.Type, + status: Touch.Status + ) { + mouseTouchEvent.screen = 0 + mouseTouchEvent.emulated = true + mouseTouchEvent.currentTime = DateTime.now() + mouseTouchEvent.scaleCoords = false + mouseTouchEvent.startFrame(type) + mouseTouchEvent.touch(button.id, p, status, kind = Touch.Kind.MOUSE, button = button) + mouseTouchEvent.endFrame() + views.dispatch(mouseTouchEvent) + } + + eventDispatcher.onEvents(*MouseEvent.Type.ALL) { e -> + //println("MOUSE: $e") + Korge.logger.trace { "eventDispatcher.addEventListener:$e" } + val p = getRealXY(e.x.toDouble(), e.y.toDouble(), e.scaleCoords) + when (e.type) { + MouseEvent.Type.DOWN -> { + mouseDown("mouseDown", p, e.button) + //updateTouch(mouseTouchId, x, y, start = true, end = false) + dispatchSimulatedTouchEvent(p, e.button, TouchEvent.Type.START, Touch.Status.ADD) + } + + MouseEvent.Type.UP -> { + mouseUp("mouseUp", p, e.button) + //updateTouch(mouseTouchId, x, y, start = false, end = true) + dispatchSimulatedTouchEvent(p, e.button, TouchEvent.Type.END, Touch.Status.REMOVE) + } + + MouseEvent.Type.DRAG -> { + mouseDrag("onMouseDrag", p) + //updateTouch(mouseTouchId, x, y, start = false, end = false) + dispatchSimulatedTouchEvent(p, e.button, TouchEvent.Type.MOVE, Touch.Status.KEEP) + } + + MouseEvent.Type.MOVE -> mouseMove("mouseMove", p, inside = true) + MouseEvent.Type.CLICK -> Unit + MouseEvent.Type.ENTER -> mouseMove("mouseEnter", p, inside = true) + MouseEvent.Type.EXIT -> mouseMove("mouseExit", p, inside = false) + MouseEvent.Type.SCROLL -> Unit + } + views.dispatch(e) + } + + eventDispatcher.onEvents(*KeyEvent.Type.ALL) { e -> + Korge.logger.trace { "eventDispatcher.addEventListener:$e" } + views.dispatch(e) + } + eventDispatcher.onEvents(*GestureEvent.Type.ALL) { e -> + Korge.logger.trace { "eventDispatcher.addEventListener:$e" } + views.dispatch(e) + } + + eventDispatcher.onEvents(*DropFileEvent.Type.ALL) { e -> views.dispatch(e) } + eventDispatcher.onEvent(ResumeEvent) { e -> + views.dispatch(e) + nativeSoundProvider.paused = false + } + eventDispatcher.onEvent(PauseEvent) { e -> + views.dispatch(e) + nativeSoundProvider.paused = true + } + eventDispatcher.onEvent(StopEvent) { e -> views.dispatch(e) } + eventDispatcher.onEvent(DestroyEvent) { e -> + try { + views.dispatch(e) + } finally { + views.launchImmediately { + views.close() + } + } + } + + val touchMouseEvent = MouseEvent() + eventDispatcher.onEvents(*TouchEvent.Type.ALL) { e -> + Korge.logger.trace { "eventDispatcher.addEventListener:$e" } + + input.updateTouches(e) + val ee = input.touch + for (t in ee.touches) { + t.p = getRealXY(t.x.toDouble(), t.y.toDouble(), e.scaleCoords) + } + views.dispatch(ee) + + // Touch to mouse events + if (ee.numTouches == 1) { + val start = ee.isStart + val end = ee.isEnd + val t = ee.touches.first() + val p = t.p + val x = t.x + val y = t.y + val button = MouseButton.LEFT + + //updateTouch(t.id, x, y, start, end) + when { + start -> mouseDown("onTouchStart", p, button) + end -> mouseUp("onTouchEnd", p, button) + else -> mouseMove("onTouchMove", p, inside = true) + } + views.dispatch(touchMouseEvent.also { + it.id = 0 + it.button = button + it.buttons = if (end) 0 else 1 shl button.id + it.x = x.toInt() + it.y = y.toInt() + it.scaleCoords = false + it.emulated = true + it.type = when { + start -> MouseEvent.Type.DOWN + end -> MouseEvent.Type.UP + else -> MouseEvent.Type.DRAG + } + }) + if (end) { + moveMouseOutsideInNextFrame = true + } + } + + } + + fun gamepadUpdated(e: GamePadUpdateEvent) { + e.gamepads.fastForEach { gamepad -> + input.gamepads[gamepad.index].copyFrom(gamepad) + } + input.updateConnectedGamepads() + } + + eventDispatcher.onEvents(*GamePadConnectionEvent.Type.ALL) { e -> + Korge.logger.trace { "eventDispatcher.addEventListener:$e" } + views.dispatch(e) + } + + eventDispatcher.onEvent(GamePadUpdateEvent) { e -> + gamepadUpdated(e) + views.dispatch(e) + } + + eventDispatcher.onEvent(ReshapeEvent) { e -> + //try { throw Exception() } catch (e: Throwable) { e.printStackTrace() } + //println("eventDispatcher.addEventListener: ${ag.backWidth}x${ag.backHeight} : ${e.width}x${e.height}") + //println("resized. ${ag.backWidth}, ${ag.backHeight}") + views.resized(ag.mainFrameBuffer.width, ag.mainFrameBuffer.height) + } + + //println("eventDispatcher.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) : ${views.nativeWidth}x${views.nativeHeight}") + eventDispatcher.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) + + eventDispatcher.onEvent(ReloadEvent) { views.dispatch(it) } + + var renderShown = false + views.clearEachFrame = clearEachFrame + views.clearColor = bgcolor + val firstRenderDeferred = CompletableDeferred() + + fun renderBlock(event: RenderEvent) { + //println("renderBlock: $event") + try { + views.frameUpdateAndRender( + fixedSizeStep = fixedSizeStep, + forceRender = views.forceRenderEveryFrame, + doUpdate = event.update, + doRender = event.render, + ) + + views.input.mouseOutside = false + if (moveMouseOutsideInNextFrame) { + moveMouseOutsideInNextFrame = false + views.input.mouseOutside = true + views.input.mouseInside = false + views.mouseUpdated() + } + } catch (e: Throwable) { + Korge.logger.error { "views.gameWindow.onRenderEvent:" } + e.printStackTrace() + if (views.rethrowRenderError) throw e + } + } + + views.gameWindow.onRenderEvent { event -> + //println("RenderEvent: $event") + if (!event.render) { + renderBlock(event) + } else { + views.renderContext.doRender { + if (!renderShown) { + //println("!!!!!!!!!!!!! views.gameWindow.addEventListener") + renderShown = true + firstRenderDeferred.complete(Unit) + } + renderBlock(event) + } + } + } + + return firstRenderDeferred +} diff --git a/korge/src/common/korlibs/korge/input/MouseEvents.kt b/korge/src/common/korlibs/korge/input/MouseEvents.kt index f153ca73ba..ceef5afdae 100644 --- a/korge/src/common/korlibs/korge/input/MouseEvents.kt +++ b/korge/src/common/korlibs/korge/input/MouseEvents.kt @@ -44,7 +44,7 @@ class MouseEvents(val view: View) : Extra by Extra.Mixin(), Closeable { view = view.parent } - val newCursor = view?.cursor ?: GameWindow.Cursor.DEFAULT + val newCursor = view?.cursor ?: Cursor.DEFAULT if (views.gameWindow.cursor != newCursor) { views.gameWindow.cursor = newCursor } @@ -637,7 +637,7 @@ fun MouseEvents.multiClick(count: Int, callback: (MouseEvents) -> Unit): Closeab } @ThreadLocal // @TODO: Is this required? -var View.cursor: GameWindow.ICursor? by extraProperty { null } +var View.cursor: ICursor? by extraProperty { null } // get() = mouse.cursor // set(value) { mouse.cursor = value } diff --git a/korge/src/common/korlibs/korge/render/RenderContext.kt b/korge/src/common/korlibs/korge/render/RenderContext.kt index 08919c455c..328da965da 100644 --- a/korge/src/common/korlibs/korge/render/RenderContext.kt +++ b/korge/src/common/korlibs/korge/render/RenderContext.kt @@ -53,7 +53,7 @@ class RenderContext( DeviceDimensionsProvider by deviceDimensionsProvider, Closeable { - val quality: GameWindow.Quality get() = windowConfig.quality + val quality: GameWindowQuality get() = windowConfig.quality @PublishedApi internal val _buffers = AGProgramWithUniforms.BufferCache() private val _programs = FastIdentityMap() diff --git a/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt b/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt index 5005bcbe78..f7705ce53e 100644 --- a/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt +++ b/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt @@ -1,17 +1,15 @@ package korlibs.korge.service.haptic -import korlibs.datastructure.extraPropertyThis -import korlibs.time.milliseconds -import korlibs.korge.service.vibration.vibration -import korlibs.korge.view.Views +import korlibs.datastructure.* +import korlibs.korge.service.vibration.* +import korlibs.korge.view.* import korlibs.render.* -import kotlin.native.concurrent.ThreadLocal +import korlibs.time.* +import kotlin.native.concurrent.* @ThreadLocal val Views.hapticFeedback by extraPropertyThis { HapticFeedback(this) } -typealias HapticFeedbackKind = GameWindow.HapticFeedbackKind - // https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/haptics/ open class HapticFeedback(val views: Views) { // @TODO: Kind patterns are temporal. We have to figure out reasonable times diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index ad7d365ab4..280cffcb61 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -37,22 +37,21 @@ open class ViewsForTesting( val timeProvider = object : TimeProvider { override fun now(): DateTime = time } - val dispatcher = GameWindowCoroutineDispatcher( - nowProvider = { time.unixMillisDouble.milliseconds }, - fast = true, - ) - inner class TestGameWindow(initialSize: Size, val dispatcher: GameWindowCoroutineDispatcher) : GameWindowLog() { + inner class TestGameWindow(initialSize: Size) : GameWindowLog() { + init { + eventLoop.immediateRun = true + eventLoop.nowProvider = { time.unixMillisDouble.milliseconds } + } override var androidContextAny: Any? = null override val devicePixelRatio: Double get() = this@ViewsForTesting.devicePixelRatio override var width: Int = initialSize.width.toInt() override var height: Int = initialSize.height.toInt() - override val coroutineDispatcher = dispatcher } open fun filterLogDraw(str: String, kind: AGBaseLog.Kind): Boolean { return kind != AGBaseLog.Kind.SHADER } - val gameWindow = TestGameWindow(windowSize, dispatcher).also { + val gameWindow = TestGameWindow(windowSize).also { enrichTestGameWindow(it) } val ag: AG by lazy { @@ -292,7 +291,7 @@ open class ViewsForTesting( viewsLog.init() this@ViewsForTesting.devicePixelRatio = devicePixelRatio //suspendTest(timeout = timeout, cond = { !OS.isAndroid && !OS.isJs && !OS.isNative }) { - KorgeRunner.prepareViewsBase(views, gameWindow, fixedSizeStep = frameTime, forceRenderEveryFrame = forceRenderEveryFrame) + views.prepareViewsBase(gameWindow, fixedSizeStep = frameTime, forceRenderEveryFrame = forceRenderEveryFrame) injector.mapInstance(KorgeConfig( title = "KorgeViewsForTesting", @@ -303,17 +302,15 @@ open class ViewsForTesting( var completed = false var completedException: Throwable? = null - this@ViewsForTesting.dispatcher.dispatch(coroutineContext, Runnable { - launchImmediately(views.coroutineContext + dispatcher) { - try { - block(views.stage) - } catch (e: Throwable) { - completedException = e - } finally { - completed = true - } - } - }) + gameWindow.queueSuspend { + try { + block(views.stage) + } catch (e: Throwable) { + completedException = e + } finally { + completed = true + } + } //println("[a0]") withTimeoutNullable(timeout ?: TimeSpan.NIL) { @@ -321,7 +318,6 @@ open class ViewsForTesting( while (!completed) { //println("FRAME") simulateFrame() - dispatcher.executePending(availableTime = 1.seconds) } //println("[a2]") diff --git a/korge/src/common/korlibs/korge/text/TextEditController.kt b/korge/src/common/korlibs/korge/text/TextEditController.kt index 4a0c85208d..93017ec9bb 100644 --- a/korge/src/common/korlibs/korge/text/TextEditController.kt +++ b/korge/src/common/korlibs/korge/text/TextEditController.kt @@ -345,7 +345,7 @@ class TextEditController( init { //println(metrics) - this.eventHandler.cursor = GameWindow.Cursor.TEXT + this.eventHandler.cursor = Cursor.TEXT closeables += this.eventHandler.timers.interval(0.5.seconds) { if (!focused) { diff --git a/korge/src/common/korlibs/korge/ui/UIButton.kt b/korge/src/common/korlibs/korge/ui/UIButton.kt index f183ddfba0..bbdf745a72 100644 --- a/korge/src/common/korlibs/korge/ui/UIButton.kt +++ b/korge/src/common/korlibs/korge/ui/UIButton.kt @@ -246,7 +246,7 @@ open class UIButton( } } } - this.cursor = GameWindow.Cursor.HAND + this.cursor = Cursor.HAND setInitialState() } diff --git a/korge/src/common/korlibs/korge/ui/UIEditableNumber.kt b/korge/src/common/korlibs/korge/ui/UIEditableNumber.kt index dcfb166afd..32fce4d53b 100644 --- a/korge/src/common/korlibs/korge/ui/UIEditableNumber.kt +++ b/korge/src/common/korlibs/korge/ui/UIEditableNumber.kt @@ -81,7 +81,7 @@ class UIEditableNumber(value: Double = 0.0, min: Double = 0.0, max: Double = 1.0 init { this.value = value - cursor = GameWindow.Cursor.RESIZE_EAST + cursor = Cursor.RESIZE_EAST var start = 0.0 textInputView.onReturnPressed { setTextInputVisible(false, useValue = true) } textInputView.onEscPressed { setTextInputVisible(false, useValue = false) } diff --git a/korge/src/common/korlibs/korge/ui/UIWindow.kt b/korge/src/common/korlibs/korge/ui/UIWindow.kt index fe15dfefa0..e569b644c7 100644 --- a/korge/src/common/korlibs/korge/ui/UIWindow.kt +++ b/korge/src/common/korlibs/korge/ui/UIWindow.kt @@ -79,7 +79,7 @@ class UIWindow(title: String, size: Size = Size(256, 256)) : UIContainer(size) { val sh = this val anchor = this@ScaleHandler.anchor anchor(Anchor.CENTER) - cursor = GameWindow.Cursor.fromAnchorResize(anchor) + cursor = Cursor.fromAnchorResize(anchor) // @TODO: clamping shouldn't affect (we should use it.start and get initial values to compute based on start and not on deltas) sh.draggable { val obounds: Rectangle = window.getGlobalBounds() @@ -171,7 +171,7 @@ class UIWindow(title: String, size: Size = Size(256, 256)) : UIContainer(size) { val sh = this anchor(Anchor.MIDDLE_LEFT) position(0.0, 0.0) - cursor = GameWindow.Cursor.RESIZE_NORTH + cursor = Cursor.RESIZE_NORTH sh.draggable { sh.x = 0.0 sh.y = 0.0 @@ -188,7 +188,7 @@ class UIWindow(title: String, size: Size = Size(256, 256)) : UIContainer(size) { val sh = this anchor(Anchor.TOP_CENTER) position(width, 0.0) - cursor = GameWindow.Cursor.RESIZE_EAST + cursor = Cursor.RESIZE_EAST sh.draggable { sh.x = sh.x.clamp(minWidth, maxWidth) sh.y = 0.0 @@ -199,7 +199,7 @@ class UIWindow(title: String, size: Size = Size(256, 256)) : UIContainer(size) { val sh = this anchor(Anchor.MIDDLE_LEFT) position(0.0, height) - cursor = GameWindow.Cursor.RESIZE_SOUTH + cursor = Cursor.RESIZE_SOUTH sh.draggable { sh.x = 0.0 sh.y = sh.y.clamp(minHeight, maxHeight) @@ -210,7 +210,7 @@ class UIWindow(title: String, size: Size = Size(256, 256)) : UIContainer(size) { val sh = this anchor(Anchor.MIDDLE_CENTER) position(width, height) - cursor = GameWindow.Cursor.RESIZE_SOUTH_EAST + cursor = Cursor.RESIZE_SOUTH_EAST sh.draggable { sh.x = sh.x.clamp(minWidth, maxWidth) sh.y = sh.y.clamp(minHeight, maxHeight) diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 29346cff47..a1d6845cc8 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -39,20 +39,19 @@ import kotlin.coroutines.* * and contains a reference to the [root] [Stage] view. */ class Views( - override val coroutineContext: CoroutineContext, - val ag: AG, + val gameWindow: GameWindow, + override val coroutineContext: CoroutineContext = gameWindow.coroutineDispatcher, + val ag: AG = gameWindow.ag, val injector: Injector = Injector(), val input: Input = Input(), val timeProvider: TimeProvider = TimeProvider, val stats: Stats = Stats(), - val gameWindow: GameWindow, val gameId: String = "korgegame", val settingsFolder: String? = null, val batchMaxQuads: Int = BatchBuilder2D.DEFAULT_BATCH_QUADS, val bp: BoundsProvider = BoundsProvider.Base(), val stageBuilder: (Views) -> Stage = { Stage(it) } ) : BaseEventListener(), - Extra by Extra.Mixin(), CoroutineScope, ViewsContainer, BoundsProvider by bp, DialogInterfaceProvider by gameWindow, @@ -61,9 +60,7 @@ class Views( InvalidateNotifier, DeviceDimensionsProvider by gameWindow { - constructor(gameWindow: GameWindow) : this( - gameWindow, gameWindow.ag, gameWindow = gameWindow - ) + //constructor(gameWindow: GameWindow) : this(gameWindow, gameWindow.ag, gameWindow = gameWindow) var quality by gameWindow::quality @@ -496,7 +493,7 @@ class ViewsLog constructor( val stats: Stats = Stats(), val gameWindow: GameWindow = GameWindowLog() ) : CoroutineScope { - val views: Views = Views(coroutineContext + InjectorContext(injector), ag, injector, input, timeProvider, stats, gameWindow).also { + val views: Views = Views(gameWindow, coroutineContext + InjectorContext(injector), ag, injector, input, timeProvider, stats).also { it.rethrowRenderError = true } val stage: Stage get() = views.stage diff --git a/korge/src/common/korlibs/render/DialogInterface.kt b/korge/src/common/korlibs/render/DialogInterface.kt index a10a9d1e48..61b3e04ee2 100644 --- a/korge/src/common/korlibs/render/DialogInterface.kt +++ b/korge/src/common/korlibs/render/DialogInterface.kt @@ -6,11 +6,6 @@ import korlibs.io.lang.* import korlibs.io.net.* import korlibs.io.util.* -/** Represents a class that have a reference to a [dialogInterface] */ -interface DialogInterfaceProvider { - val dialogInterface: DialogInterface -} - /** * Provides an interface with typical window dialogs and functionality. * @@ -48,6 +43,11 @@ interface DialogInterface : DialogInterfaceProvider { object Unsupported : DialogInterface } +/** Represents a class that have a reference to a [dialogInterface] */ +interface DialogInterfaceProvider { + val dialogInterface: DialogInterface +} + suspend fun DialogInterfaceProvider.browse(url: URL): Unit = dialogInterface.browse(url) suspend fun DialogInterfaceProvider.alert(message: String): Unit = dialogInterface.alert(message) suspend fun DialogInterfaceProvider.confirm(message: String): Boolean = dialogInterface.confirm(message) diff --git a/korge/src/common/korlibs/render/SyncEventLoopCoroutineDispatcher.kt b/korge/src/common/korlibs/render/EventLoopCoroutineDispatcher.kt similarity index 70% rename from korge/src/common/korlibs/render/SyncEventLoopCoroutineDispatcher.kt rename to korge/src/common/korlibs/render/EventLoopCoroutineDispatcher.kt index d2a00d7072..a6d71d5a75 100644 --- a/korge/src/common/korlibs/render/SyncEventLoopCoroutineDispatcher.kt +++ b/korge/src/common/korlibs/render/EventLoopCoroutineDispatcher.kt @@ -1,16 +1,13 @@ package korlibs.render import korlibs.datastructure.closeable.Closeable -import korlibs.datastructure.event.* import korlibs.io.lang.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* @OptIn(InternalCoroutinesApi::class) -class SyncEventLoopCoroutineDispatcher(val eventLoop: SyncEventLoop) : CoroutineDispatcher(), Delay, Closeable { - constructor(precise: Boolean = true, immediateRun: Boolean = false) : this(SyncEventLoop(precise, immediateRun)) - +class EventLoopCoroutineDispatcher(val eventLoop: korlibs.datastructure.event.EventLoop) : CoroutineDispatcher(), Delay, Closeable { override fun close() { eventLoop.close() } @@ -40,16 +37,4 @@ class SyncEventLoopCoroutineDispatcher(val eventLoop: SyncEventLoop) : Coroutine block.run() }.toDisposable() } - - fun loopForever() { - eventLoop.runTasksForever() - } - - fun loopUntilEmpty() { - eventLoop.runTasksUntilEmpty() - } - - fun executePending() { - eventLoop.runAvailableNextTasks() - } } diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 9cd1956e0f..9bfb7bec7c 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -2,57 +2,25 @@ package korlibs.render import korlibs.datastructure.* import korlibs.datastructure.closeable.* +import korlibs.datastructure.event.* import korlibs.datastructure.lock.* import korlibs.event.* import korlibs.graphics.* import korlibs.graphics.log.* import korlibs.image.bitmap.* import korlibs.image.color.* -import korlibs.image.vector.* -import korlibs.io.* import korlibs.io.async.* -import korlibs.io.file.* import korlibs.korge.view.* import korlibs.logger.* import korlibs.math.geom.* -import korlibs.memory.* -import korlibs.render.GameWindow.Quality.* import korlibs.render.internal.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* -import kotlin.native.concurrent.* import kotlin.properties.* -@ThreadLocal -var GLOBAL_CHECK_GL = false - -data class GameWindowCreationConfig( - val multithreaded: Boolean? = null, - val hdr: Boolean? = null, - val msaa: Int? = null, - val checkGl: Boolean = false, - val logGl: Boolean = false, - val cacheGl: Boolean = false, - val fullscreen: Boolean? = null, -) { - companion object { - val DEFAULT = GameWindowCreationConfig() - } -} - -expect fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow -fun CreateDefaultGameWindow() = CreateDefaultGameWindow(GameWindowCreationConfig()) - -interface GameWindowConfig { - val quality: GameWindow.Quality - - class Impl( - override val quality: GameWindow.Quality = GameWindow.Quality.AUTOMATIC, - ) : GameWindowConfig -} - -typealias GameWindowQuality = GameWindow.Quality +/** Creates the default [GameWindow] for this platform. Typically, a window/frame or a fullscreen screen depending on the OS */ +expect fun CreateDefaultGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig()): GameWindow /** * A [GameWindow] represents a window, canvas or headless virtual frame where a game is displayed and can receive user events, it provides: @@ -71,7 +39,11 @@ typealias GameWindowQuality = GameWindow.Quality * - Input events: [KeyEvent], [MouseEvent], [GestureEvent], [DropFileEvent] * * Window properties: - * - [title], [icon], [cursor], [width], [height], [preferredFps], [fullscreen], [visible], [bgcolor], [quality], [alwaysOnTop] + * - [title], [icon], [cursor], + * - [width], [height], [bufferWidth], [bufferHeight] + * - [fullscreen], [visible], [backgroundColor], [quality], [alwaysOnTop] + * - [preferredFps], [fps] + * - [close] * * Dialogs [DialogInterface]: * - [browse], [alert], [confirm], [prompt], [openFileDialog] @@ -93,105 +65,20 @@ open class GameWindow : Extra by Extra.Mixin() { open val androidContextAny: Any? get() = null - - sealed interface ICursor - override val dialogInterface: DialogInterface get() = DialogInterface.Unsupported - data class CustomCursor(val shape: Shape, val name: String = "custom") : ICursor, Extra by Extra.Mixin() { - val bounds: Rectangle = this.shape.bounds - fun createBitmap(size: Size? = null, native: Boolean = true) = shape.renderWithHotspot(fit = size, native = native) - } - - enum class Cursor : ICursor { - DEFAULT, CROSSHAIR, TEXT, HAND, MOVE, WAIT, - RESIZE_EAST, RESIZE_WEST, RESIZE_SOUTH, RESIZE_NORTH, - RESIZE_NORTH_EAST, RESIZE_NORTH_WEST, RESIZE_SOUTH_EAST, RESIZE_SOUTH_WEST; - - companion object { - val ANGLE_TO_CURSOR = mapOf( - (45.degrees * 0) to RESIZE_EAST, - (45.degrees * 1) to RESIZE_SOUTH_EAST, - (45.degrees * 2) to RESIZE_SOUTH, - (45.degrees * 3) to RESIZE_SOUTH_WEST, - (45.degrees * 4) to RESIZE_WEST, - (45.degrees * 5) to RESIZE_NORTH_WEST, - (45.degrees * 6) to RESIZE_NORTH, - (45.degrees * 7) to RESIZE_NORTH_EAST, - ) - - fun fromAngleResize(angle: Angle?): ICursor? { - var minDistance = 360.degrees - var cursor: ICursor? = null - if (angle != null) { - for ((cangle, ccursor) in ANGLE_TO_CURSOR) { - val cdistance = (angle - cangle).absoluteValue - if (cdistance <= minDistance) { - minDistance = cdistance - cursor = ccursor - } - } - } - return cursor - } - - fun fromAnchorResize(anchor: Anchor): ICursor? { - return when (anchor) { - Anchor.TOP_LEFT -> RESIZE_NORTH_WEST - Anchor.TOP -> RESIZE_NORTH - Anchor.TOP_RIGHT -> RESIZE_NORTH_EAST - Anchor.LEFT -> RESIZE_WEST - Anchor.RIGHT -> RESIZE_EAST - Anchor.BOTTOM_LEFT -> RESIZE_SOUTH_WEST - Anchor.BOTTOM -> RESIZE_SOUTH - Anchor.BOTTOM_RIGHT -> RESIZE_SOUTH_EAST - else -> null - } - } - } - } - - data class MenuItem(val text: String?, val enabled: Boolean = true, val children: List? = null, val action: () -> Unit = {}) - - open fun setMainMenu(items: List) { - } - - open fun showContextMenu(items: List) { - } - - class MenuItemBuilder(private var text: String? = null, private var enabled: Boolean = true, private var action: () -> Unit = {}) { - @PublishedApi internal val children = arrayListOf() - - inline fun separator() { - item(null) - } - - inline fun item(text: String?, enabled: Boolean = true, noinline action: () -> Unit = {}, block: MenuItemBuilder.() -> Unit = {}): MenuItem { - val mib = MenuItemBuilder(text, enabled, action) - block(mib) - val item = mib.toItem() - children.add(item) - return item - } - - fun toItem() = MenuItem(text, enabled, children.ifEmpty { null }, action) - } - - fun showContextMenu(block: MenuItemBuilder.() -> Unit) { - showContextMenu(MenuItemBuilder().also(block).toItem().children ?: listOf()) - } + // Context Menu + open fun setMainMenu(items: List) = Unit + open fun showContextMenu(items: List) = Unit + fun showContextMenu(block: GameWindowMenuItemBuilder.() -> Unit) = showContextMenu(GameWindowMenuItemBuilder().also(block).toItem().children ?: listOf()) + // SOFT KEYBOARD open val isSoftKeyboardVisible: Boolean get() = false + open fun setInputRectangle(windowRect: Rectangle) = Unit + open fun showSoftKeyboard(force: Boolean = true, config: ISoftKeyboardConfig? = null) = Unit + open fun hideSoftKeyboard() = Unit - open fun setInputRectangle(windowRect: Rectangle) { - } - - open fun showSoftKeyboard(force: Boolean = true, config: ISoftKeyboardConfig? = null) { - } - - open fun hideSoftKeyboard() { - } - + // Cursor open var cursor: ICursor = Cursor.DEFAULT /** @@ -200,106 +87,87 @@ open class GameWindow : open var keepScreenOn: Boolean set(value) = Unit get() = false - open var alwaysOnTop: Boolean = false override val key: CoroutineContext.Key<*> get() = CoroutineKey companion object CoroutineKey : CoroutineContext.Key { const val DEFAULT_PREFERRED_FPS = 60 val logger = Logger("GameWindow") - - val MenuItemSeparatror = MenuItem(null) } //override val ag: AG = LogAG() override val ag: AG = AGDummy() - open val myCoroutineDispatcher = SyncEventLoopCoroutineDispatcher() - open val coroutineDispatcher: GameWindowCoroutineDispatcher = GameWindowCoroutineDispatcher() + val eventLoop = SyncEventLoop() + open val coroutineDispatcher = EventLoopCoroutineDispatcher(eventLoop) fun getCoroutineDispatcherWithCurrentContext(coroutineContext: CoroutineContext): CoroutineContext = coroutineContext + coroutineDispatcher suspend fun getCoroutineDispatcherWithCurrentContext(): CoroutineContext = getCoroutineDispatcherWithCurrentContext(coroutineContext) - fun queue(callback: () -> Unit) = coroutineDispatcher.queue(callback) - fun queue(callback: Runnable) = coroutineDispatcher.queue(callback) - fun queueBlocking(callback: () -> T) = coroutineDispatcher.queueBlocking(callback) + // Event Loop + @PublishedApi internal val _updateRenderLock = Lock() + inline fun updateRenderLock(block: () -> Unit) = _updateRenderLock(block) + fun queueSuspend(callback: suspend () -> Unit) { + launch(coroutineDispatcher) { + callback() + } + } + fun queue(callback: () -> Unit) = eventLoop.setImmediate(callback) + fun queue(callback: Runnable) = eventLoop.setImmediate { callback.run() } + @Deprecated("") + fun queueBlocking(callback: () -> T): T { + val deferred = CompletableDeferred() + queue { + deferred.completeWith(runCatching { callback() }) + } + return runBlockingNoJs { deferred.await() } + } - protected val pauseEvent = PauseEvent() - protected val resumeEvent = ResumeEvent() - protected val stopEvent = StopEvent() - protected val destroyEvent = DestroyEvent() - protected val updateEvent = UpdateEvent() - protected val renderEvent = RenderEvent() - protected val initEvent = InitEvent() - protected val disposeEvent = DisposeEvent() - protected val fullScreenEvent = FullScreenEvent() - private val reshapeEvent = ReshapeEvent() - protected val keyEvent = KeyEvent() - protected val mouseEvent = MouseEvent() - protected val gestureEvent = GestureEvent() - protected val dropFileEvent = DropFileEvent() + internal val events = GameWindowEventInstances() + internal val gamepadEmitter: GamepadInfoEmitter = GamepadInfoEmitter(this) + internal val gameWindowInputState = GameWindowInputState() + + var updatedSinceFrame: Int = 0 + val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceFrame > 0 + var onContinuousRenderModeUpdated: ((Boolean) -> Unit)? = null + open var continuousRenderMode: Boolean by Delegates.observable(true) { prop, old, new -> onContinuousRenderModeUpdated?.invoke(new) } /** Happens on the updater thread */ - fun onUpdateEvent(block: (UpdateEvent) -> Unit): Closeable { - return onEvent(UpdateEvent, block) - } + fun onUpdateEvent(block: (UpdateEvent) -> Unit): Closeable = onEvent(UpdateEvent, block) /** Happens on the rendering thread */ - fun onRenderEvent(block: (RenderEvent) -> Unit): Closeable { - return onEvent(RenderEvent, block) - } + fun onRenderEvent(block: (RenderEvent) -> Unit): Closeable = onEvent(RenderEvent, block) + // region: FPS val counterTimePerFrame: TimeSpan get() = (1_000_000.0 / fps).microseconds val timePerFrame: TimeSpan get() = counterTimePerFrame - - open fun computeDisplayRefreshRate(): Int { - return 60 - } - - open fun registerTime(name: String, time: TimeSpan) { - //println("registerTime: $name=$time") - } - - inline fun registerTime(name: String, block: () -> T): T { - val start = PerformanceCounter.microseconds - try { - return block() - } finally { - val end = PerformanceCounter.microseconds - registerTime(name, (end - start).microseconds) - } - } - + open fun computeDisplayRefreshRate(): Int = 60 private val fpsCached by IntTimedCache(1000.milliseconds) { computeDisplayRefreshRate() } - open var preferredFps: Int = DEFAULT_PREFERRED_FPS - open var fps: Int get() = fpsCached @Deprecated("Deprecated setting fps") set(value) = Unit + // PROPS + open var visible: Boolean = false + fun hide() = run { visible = false } + fun show() = run { visible = true } + open var title: String get() = ""; set(value) = Unit open val width: Int = 0 open val height: Int = 0 + open fun setSize(width: Int, height: Int): Unit = Unit open var vsync: Boolean = true - - // Might be different than width and height for example on high dpi screens - open val bufferWidth: Int get() = width - open val bufferHeight: Int get() = height - open var icon: Bitmap? = null open var fullscreen: Boolean = false - open var visible: Boolean = false - open var bgcolor: RGBA = Colors.BLACK - override var quality: Quality get() = Quality.AUTOMATIC; set(value) = Unit + open var backgroundColor: RGBA = Colors.BLACK + var bgcolor: RGBA by this::backgroundColor + override var quality: GameWindowQuality get() = GameWindowQuality.AUTOMATIC; set(value) = Unit - fun hide() { - visible = false - } - fun show() { - visible = true - } + // Might be different from width and height for example on high dpi screens + open val bufferWidth: Int get() = width + open val bufferHeight: Int get() = height val onDebugEnabled = Signal() val onDebugChanged = Signal() @@ -311,46 +179,22 @@ open class GameWindow : if (value) onDebugEnabled() } - /** - * Describes if the rendering should focus on performance or quality. - * [PERFORMANCE] will use lower resolutions, while [QUALITY] will use the devicePixelRatio - * to render high quality images. - */ - enum class Quality(override val level: Float) : korlibs.image.Quality { - /** Will render to lower resolutions, ignoring devicePixelRatio on retina-like screens */ - PERFORMANCE(0f), - /** Will render to higher resolutions, using devicePixelRatio on retina-like screens */ - QUALITY(1f), - /** Will choose [PERFORMANCE] or [QUALITY] based on some heuristics */ - AUTOMATIC(.5f); + var exitProcessOnClose: Boolean = true + protected var exitCode = 0; private set + protected var running = true; protected set + private var closing = false - private val UPPER_BOUND_RENDERED_PIXELS = 4_000_000 + @Deprecated("Use korlibs.render.Cursor", ReplaceWith("korlibs.render.Cursor")) + object Cursor : korlibs.render.Cursor.Alias - fun computeTargetScale( - width: Int, - height: Int, - devicePixelRatio: Float, - targetPixels: Int = UPPER_BOUND_RENDERED_PIXELS - ): Float = when (this) { - PERFORMANCE -> 1f - QUALITY -> devicePixelRatio - AUTOMATIC -> { - listOf(devicePixelRatio, 2f, 1f) - .firstOrNull { width * height * it <= targetPixels } - ?: 1f - } - } - } + @Deprecated("Use GameWindowQuality") + object Quality : GameWindowQuality.Alias + //typealias Quality = GameWindowQuality - open fun setSize(width: Int, height: Int): Unit = Unit // Alias for close + @Deprecated("Use close instead") fun exit(exitCode: Int = 0): Unit = close(exitCode) - var exitProcessOnClose: Boolean = true - var exitCode = 0; private set - var running = true; protected set - private var closing = false - open fun close(exitCode: Int = 0) { if (closing) return closing = true @@ -361,468 +205,26 @@ open class GameWindow : coroutineDispatcher.cancelChildren() } - suspend fun waitClose() { - while (running) { - delay(100.milliseconds) - } - } - - override fun repaint() { - } - - open suspend fun loop(entry: suspend GameWindow.() -> Unit) { - launchImmediately(getCoroutineDispatcherWithCurrentContext()) { - entry() - } - while (running) { - val elapsed = frame() - val available = counterTimePerFrame - elapsed - if (available > TimeSpan.ZERO) delay(available) - } - } + @Deprecated("") + suspend fun waitClose() = run { while (running) delay(100.milliseconds) } + @Deprecated("") + override fun repaint(): Unit = Unit - // Referenced from korge-plugins repo - var renderTime = 0.milliseconds - var updateTime = 0.milliseconds - - var onContinuousRenderModeUpdated: ((Boolean) -> Unit)? = null - open var continuousRenderMode: Boolean by Delegates.observable(true) { prop, old, new -> - onContinuousRenderModeUpdated?.invoke(new) - } - - fun frame(doUpdate: Boolean = true, doRender: Boolean = true, frameStartTime: TimeSpan = PerformanceCounter.reference): TimeSpan { - val startTime = PerformanceCounter.reference - if (doRender) { - renderTime = measureTime { - frameRender(doUpdate = doUpdate, doRender = true) - } - } - //println("renderTime=$renderTime") - if (doUpdate) { - updateTime = measureTime { - if (!doRender) { - frameRender(doUpdate = true, doRender = false) - } - frameUpdate(frameStartTime) - } - //println("updateTime=$updateTime") - } - val endTime = PerformanceCounter.reference - return endTime - startTime - } - - fun frameRender(doUpdate: Boolean = true, doRender: Boolean = true) { - if (doRender) { - if (contextLost) { - contextLost = false - ag.contextLost() - } - if (surfaceChanged) { - surfaceChanged = false - ag.mainFrameBuffer.setSize(surfaceX, surfaceY, surfaceWidth, surfaceHeight) - dispatchReshapeEvent(surfaceX, surfaceY, surfaceWidth, surfaceHeight) - } - } - if (doInitialize) { - doInitialize = false - //ag.mainRenderBuffer.setSize(0, 0, width, height) - println("---------------- Trigger AG.initialized ag.mainFrameBuffer.setSize ($width, $height) --------------") - dispatch(initEvent.reset()) - } - try { - dispatchRenderEvent(update = doUpdate, render = doRender) - } catch (e: Throwable) { - println("ERROR GameWindow.frameRender:") - println(e) - e.printStackTrace() - } - } - - private var surfaceChanged = false - private var surfaceX = -1 - private var surfaceY = -1 - private var surfaceWidth = -1 - private var surfaceHeight = -1 - - private var doInitialize = false - private var initialized = false - private var contextLost = false - - var updatedSinceFrame: Int = 0 - - val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceFrame > 0 - - fun startFrame() { - updatedSinceFrame = 0 - } - - fun invalidatedView() { - updatedSinceFrame++ - } - - fun handleContextLost() { - println("---------------- handleContextLost --------------") - contextLost = true - } - - fun handleInitEventIfRequired() { - if (initialized) return - initialized = true - doInitialize = true - } - - fun handleReshapeEventIfRequired(x: Int, y: Int, width: Int, height: Int) { - if (surfaceX == x && surfaceY == y && surfaceWidth == width && surfaceHeight == height) return - println("handleReshapeEventIfRequired: $x, $y, $width, $height") - surfaceChanged = true - surfaceX = x - surfaceY = y - surfaceWidth = width - surfaceHeight = height - } - - var gamePadTime: TimeSpan = TimeSpan.ZERO - fun frameUpdate(startTime: TimeSpan) { - gamePadTime = measureTime { - updateGamepads() - } - try { - val now = PerformanceCounter.reference - val consumed = now - startTime - //val remaining = (counterTimePerFrame - consumed) - 2.milliseconds // Do not push too much so give two extra milliseconds just in case - //val timeForTasks = coroutineDispatcher.maxAllocatedTimeForTasksPerFrame ?: (remaining * 10) // We would be skipping up to 10 frames by default - val timeForTasks = coroutineDispatcher.maxAllocatedTimeForTasksPerFrame ?: (counterTimePerFrame * 10) // We would be skipping up to 10 frames by default - val finalTimeForTasks = max(timeForTasks, 4.milliseconds) // Avoid having 0 milliseconds or even negative - //println(" - frameUpdate: finalTimeForTasks=$finalTimeForTasks, startTime=$startTime, now=$now") - coroutineDispatcher.executePending(finalTimeForTasks) - } catch (e: Throwable) { - println("ERROR GameWindow.frameRender:") - println(e) - } - } + @Deprecated("") + fun startFrame() = run { updatedSinceFrame = 0 } + @Deprecated("") + fun invalidatedView() = run { updatedSinceFrame++ } + @Deprecated("") + fun handleContextLost() = run { gameWindowInputState.contextLost = true } open fun updateGamepads() { } - fun executePending(availableTime: TimeSpan) { - coroutineDispatcher.executePending(availableTime) - } - - fun dispatchInitEvent() = dispatch(initEvent.reset()) - fun dispatchPauseEvent() = dispatch(pauseEvent.reset()) - fun dispatchResumeEvent() = dispatch(resumeEvent.reset()) - fun dispatchStopEvent() = dispatch(stopEvent.reset()) - fun dispatchDestroyEvent() = dispatch(destroyEvent.reset()) - fun dispatchDisposeEvent() = dispatch(disposeEvent.reset()) - @PublishedApi internal val _updateRenderLock = Lock() - inline fun updateRenderLock(block: () -> Unit) { - _updateRenderLock(block) - } - fun dispatchUpdateEvent() { - updateRenderLock { dispatch(updateEvent) } - } - fun dispatchRenderEvent(update: Boolean = true, render: Boolean = true) { - updateRenderLock { - dispatch(renderEvent.reset { - this.update = update - this.render = render - }) - } - } - fun dispatchNewRenderEvent() { - dispatchRenderEvent(update = false, render = true) - } - fun dispatchDropfileEvent(type: DropFileEvent.Type, files: List?) = dispatch(dropFileEvent.reset { - this.type = type - this.files = files - }) - fun dispatchFullscreenEvent(fullscreen: Boolean) = dispatch(fullScreenEvent.reset { this.fullscreen = fullscreen }) - - fun dispatchReshapeEvent(x: Int, y: Int, width: Int, height: Int) { - dispatchReshapeEventEx(x, y, width, height, width, height) - } - - fun dispatchReshapeEventEx(x: Int, y: Int, width: Int, height: Int, fullWidth: Int, fullHeight: Int) { - ag.mainFrameBuffer.setSize(x, y, width, height, fullWidth, fullHeight) - dispatch(reshapeEvent.reset { - this.x = x - this.y = y - this.width = width - this.height = height - }) - } - - private val keysPresing = BooleanArray(Key.MAX) - private fun pressing(key: Key) = keysPresing[key.ordinal] - private val shift get() = pressing(Key.LEFT_SHIFT) || pressing(Key.RIGHT_SHIFT) - private val ctrl get() = pressing(Key.LEFT_CONTROL) || pressing(Key.RIGHT_CONTROL) - private val alt get() = pressing(Key.LEFT_ALT) || pressing(Key.RIGHT_ALT) - private val meta get() = pressing(Key.META) || pressing(Key.LEFT_SUPER) || pressing(Key.RIGHT_SUPER) - private var mouseButtons = 0 - private val scrollDeltaX = 0f - private val scrollDeltaY = 0f - private val scrollDeltaZ = 0f - private val scaleCoords = false - - fun dispatchKeyEvent(type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { - return dispatchKeyEventEx(type, id, character, key, keyCode, str = str) - } - - fun dispatchKeyEventDownUp(id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { - val cancel1 = dispatchKeyEvent(KeyEvent.Type.DOWN, id, character, key, keyCode, str) - val cancel2 = dispatchKeyEvent(KeyEvent.Type.UP, id, character, key, keyCode, str) - return cancel1 || cancel2 - } - - val gamepadEmitter: GamepadInfoEmitter = GamepadInfoEmitter(this) - - //private val gamePadUpdateEvent = GamePadUpdateEvent() - fun dispatchGamepadUpdateStart() { - gamepadEmitter.dispatchGamepadUpdateStart() - } - - fun dispatchGamepadUpdateAdd(info: GamepadInfo) { - gamepadEmitter.dispatchGamepadUpdateAdd(info) - } - - /** - * Triggers an update envent and potential CONNECTED/DISCONNECTED events. - * - * Returns a list of disconnected gamepads. - */ - fun dispatchGamepadUpdateEnd(out: IntArrayList = IntArrayList()): IntArrayList = - gamepadEmitter.dispatchGamepadUpdateEnd(out) - - fun dispatchKeyEventEx( - type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, - shift: Boolean = this.shift, ctrl: Boolean = this.ctrl, alt: Boolean = this.alt, meta: Boolean = this.meta, - str: String? = null - ): Boolean { - if (type != KeyEvent.Type.TYPE) { - keysPresing[key.ordinal] = (type == KeyEvent.Type.DOWN) - } - dispatch(keyEvent.reset { - this.id = id - this.character = character - this.key = key - this.keyCode = keyCode - this.type = type - this.shift = shift - this.ctrl = ctrl - this.alt = alt - this.meta = meta - if (str != null && str.length == 1) { - this.str = null - this.character = str[0] - this.keyCode = this.character.code - } else { - this.str = str - } - }) - return keyEvent.defaultPrevented - } - - fun dispatchSimpleMouseEvent( - type: MouseEvent.Type, id: Int, x: Int, y: Int, button: MouseButton, simulateClickOnUp: Boolean = false - ) { - dispatchMouseEvent(type, id, x, y, button, simulateClickOnUp = simulateClickOnUp) - } - - fun dispatchMouseEvent( - type: MouseEvent.Type, id: Int, x: Int, y: Int, button: MouseButton, buttons: Int = this.mouseButtons, - scrollDeltaX: Float = this.scrollDeltaX, scrollDeltaY: Float = this.scrollDeltaY, scrollDeltaZ: Float = this.scrollDeltaZ, - isShiftDown: Boolean = this.shift, isCtrlDown: Boolean = this.ctrl, isAltDown: Boolean = this.alt, isMetaDown: Boolean = this.meta, - scaleCoords: Boolean = this.scaleCoords, simulateClickOnUp: Boolean = false, - scrollDeltaMode: MouseEvent.ScrollDeltaMode = MouseEvent.ScrollDeltaMode.LINE - ) { - if (type != MouseEvent.Type.DOWN && type != MouseEvent.Type.UP) { - this.mouseButtons = this.mouseButtons.setBits(if (button != null) 1 shl button.ordinal else 0, type == MouseEvent.Type.DOWN) - } - dispatch(mouseEvent.reset { - this.type = type - this.id = id - this.x = x - this.y = y - this.button = button - this.buttons = buttons - this.scrollDeltaX = scrollDeltaX - this.scrollDeltaY = scrollDeltaY - this.scrollDeltaZ = scrollDeltaZ - this.scrollDeltaMode = scrollDeltaMode - this.isShiftDown = isShiftDown - this.isCtrlDown = isCtrlDown - this.isAltDown = isAltDown - this.isMetaDown = isMetaDown - this.scaleCoords = scaleCoords - }) - //if (simulateClickOnUp && type == MouseEvent.Type.UP) { - // dispatchMouseEvent(MouseEvent.Type.CLICK, id, x, y, button, buttons, scrollDeltaX, scrollDeltaY, scrollDeltaZ, isShiftDown, isCtrlDown, isAltDown, isMetaDown, scaleCoords, simulateClickOnUp = false) - //} - } - - - // @TODO: Is this used? - fun entry(callback: suspend () -> Unit) { - launch(coroutineDispatcher) { - try { - callback() - } catch (e: Throwable) { - println("ERROR GameWindow.entry:") - e.printStackTrace() - running = false - } - } - } - - enum class HapticFeedbackKind { GENERIC, ALIGNMENT, LEVEL_CHANGE } - open val hapticFeedbackGenerateSupport: Boolean get() = false - open fun hapticFeedbackGenerate(kind: HapticFeedbackKind) { - } - - open suspend fun clipboardWrite(data: ClipboardData) { - } - - open suspend fun clipboardRead(): ClipboardData? { - return null - } + open fun hapticFeedbackGenerate(kind: HapticFeedbackKind): Unit = Unit + open suspend fun clipboardWrite(data: ClipboardData): Unit = Unit + open suspend fun clipboardRead(): ClipboardData? = null //open fun lockMousePointer() = println("WARNING: lockMousePointer not implemented") //open fun unlockMousePointer() = Unit } - -interface ClipboardData { -} - -data class TextClipboardData(val text: String, val contentType: String? = null) : ClipboardData - -open class EventLoopGameWindow : GameWindow() { - var fixedTime = PerformanceCounter.reference - override val coroutineDispatcher = GameWindowCoroutineDispatcher(nowProvider = { fixedTime }) - - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - try { - // Required here so setSize is called - launchImmediately(getCoroutineDispatcherWithCurrentContext()) { - try { - entry() - } catch (e: Throwable) { - println("Error initializing application") - println(e) - running = false - } - } - - doInitialize() - dispatchInitEvent() - - while (running) { - render(doUpdate = true) { - doHandleEvents() - mustPerformRender() - } - // Here we can trigger a GC if we have enough time, and we can try to disable GC all the other times. - if (!vsync) { - sleepNextFrame() - } - } - } finally { - try { - dispatchStopEvent() - dispatchDestroyEvent() - } finally { - doDestroy() - } - } - } - - fun mustPerformRender(): Boolean = if (vsync) true else elapsedSinceLastRenderTime() >= counterTimePerFrame - - var lastRenderTime = PerformanceCounter.reference - fun elapsedSinceLastRenderTime() = PerformanceCounter.reference - lastRenderTime - - inline fun render(doUpdate: Boolean, doRender: () -> Boolean = { true }) { - val frameStartTime: TimeSpan = PerformanceCounter.reference - val mustRender = doRender() - if (mustRender) renderInternal(doUpdate = doUpdate, frameStartTime = frameStartTime) - } - - @PublishedApi - internal fun renderInternal(doUpdate: Boolean, frameStartTime: TimeSpan = PerformanceCounter.reference) { - fixedTime = PerformanceCounter.reference - doInitRender() - - var doRender = !doUpdate - if (doUpdate) { - frame(doUpdate = true, doRender = false, frameStartTime = frameStartTime) - if (mustTriggerRender) { - doRender = true - } - } - if (doRender) { - frame(doUpdate = false, doRender = true, frameStartTime = frameStartTime) - } - lastRenderTime = PerformanceCounter.reference - if (doRender) { - doSwapBuffers() - } - } - - @OptIn(ExperimentalStdlibApi::class) - fun sleepNextFrame() { - val now = PerformanceCounter.reference - val frameTime = (1.toDouble() / fps.toDouble()).seconds - val delay = frameTime - (now % frameTime) - if (delay > 0.0.milliseconds) { - //println(delayNanos / 1_000_000) - blockingSleep(delay) - } - } - - protected fun sleep(time: TimeSpan) { - // Reimplement: Spinlock! - val start = PerformanceCounter.reference - while ((PerformanceCounter.reference - start) < time) { - doSmallSleep() - } - } - - @OptIn(ExperimentalStdlibApi::class) - protected fun doSmallSleep() { - if (!vsync) { - blockingSleep(0.1.milliseconds) - } - } - protected open fun doHandleEvents() = Unit - protected open fun doInitRender() = Unit - protected open fun doSwapBuffers() = Unit - protected open fun doInitialize() = Unit - protected open fun doDestroy() = Unit -} - -fun GameWindow.mainLoop(entry: suspend GameWindow.() -> Unit) = Korio { loop(entry) } - -fun GameWindow.toggleFullScreen() { fullscreen = !fullscreen } - -fun GameWindow.configure( - size: Size, - title: String? = "GameWindow", - icon: Bitmap? = null, - fullscreen: Boolean? = null, - bgcolor: RGBA = Colors.BLACK, -) { - this.setSize(size.width.toInt(), size.height.toInt()) - if (title != null) this.title = title - this.icon = icon - if (fullscreen != null) this.fullscreen = fullscreen - this.bgcolor = bgcolor - this.visible = true -} - -fun GameWindow.onDragAndDropFileEvent(block: suspend (DropFileEvent) -> Unit) { - onEvents(*DropFileEvent.Type.ALL) { event -> - launchImmediately(coroutineDispatcher) { - block(event) - } - } -} diff --git a/korge/src/common/korlibs/render/GameWindowCoroutineDispatcher.kt b/korge/src/common/korlibs/render/GameWindowCoroutineDispatcher.kt deleted file mode 100644 index 634838451a..0000000000 --- a/korge/src/common/korlibs/render/GameWindowCoroutineDispatcher.kt +++ /dev/null @@ -1,166 +0,0 @@ -package korlibs.render - -import korlibs.datastructure.* -import korlibs.datastructure.closeable.* -import korlibs.datastructure.lock.* -import korlibs.io.experimental.* -import korlibs.logger.* -import korlibs.time.* -import korlibs.time.measureTime -import kotlinx.coroutines.* -import kotlin.coroutines.* -import kotlin.time.* - -@OptIn(InternalCoroutinesApi::class) -class GameWindowCoroutineDispatcher( - var nowProvider: () -> TimeSpan = { PerformanceCounter.reference }, - var fast: Boolean = false, -) : CoroutineDispatcher(), Delay, Closeable { - override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = dispatch(context, block) - - class TimedTask(val time: TimeSpan, val continuation: CancellableContinuation?, val callback: Runnable?) { - var exception: Throwable? = null - } - - @PublishedApi internal val tasks = Queue() - @PublishedApi internal val timedTasks = TGenPriorityQueue { a, b -> a.time.compareTo(b.time) } - val lock = NonRecursiveLock() - - fun hasTasks() = tasks.isNotEmpty() - - fun queue(block: Runnable?) { - if (block == null) return - lock { tasks.enqueue(block) } - } - - fun queue(block: () -> Unit) = queue(Runnable { block() }) - - @KorioExperimentalApi - fun queueBlocking(block: () -> T): T { - val result = CompletableDeferred() - queue(Runnable { - result.complete(block()) - }) - return korlibs.io.async.runBlockingNoJs { result.await() } - } - - override fun dispatch(context: CoroutineContext, block: Runnable) = queue(block) // @TODO: We are not using the context - - fun now(): Duration { - return nowProvider() - } - - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - scheduleResumeAfterDelay(timeMillis.toDouble().milliseconds, continuation) - } - - fun scheduleResumeAfterDelay(time: TimeSpan, continuation: CancellableContinuation) { - val task = TimedTask(now() + time, continuation, null) - continuation.invokeOnCancellation { - task.exception = it - } - lock { timedTasks.add(task) } - } - - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - val task = TimedTask(now() + timeMillis.toDouble().milliseconds, null, block) - lock { timedTasks.add(task) } - return DisposableHandle { lock { timedTasks.remove(task) } } - } - - var timedTasksTime = 0.milliseconds - var tasksTime = 0.milliseconds - - /** - * Allows to configure how much time per frame is available to execute pending tasks, - * despite time available in the frame. - * When not set it uses the remaining available time in frame - **/ - var maxAllocatedTimeForTasksPerFrame: TimeSpan? = null - - fun executePending(availableTime: TimeSpan) { - try { - val startTime = now() - - var processedTimedTasks = 0 - var processedTasks = 0 - - timedTasksTime = measureTime { - while (true) { - val item = lock { - if (timedTasks.isNotEmpty() && (fast || startTime >= timedTasks.head.time)) timedTasks.removeHead() else null - } ?: break - try { - if (item.exception != null) { - item.continuation?.resumeWithException(item.exception!!) - if (item.callback != null) { - item.exception?.printStackTrace() - } - } else { - item.continuation?.resume(Unit) - item.callback?.run() - } - } catch (e: Throwable) { - e.printStackTrace() - } finally { - processedTimedTasks++ - } - val elapsedTime = now() - startTime - if (elapsedTime >= availableTime) { - informTooManyCallbacksToHandleInThisFrame(elapsedTime, availableTime, processedTimedTasks, processedTasks) - break - } - } - } - tasksTime = measureTime { - while (true) { - val task = lock { (if (tasks.isNotEmpty()) tasks.dequeue() else null) } ?: break - val time = measureTime { - try { - task.run() - } catch (e: Throwable) { - e.printStackTrace() - } finally { - processedTasks++ - } - } - //println("task=$time, task=$task") - val elapsed = now() - startTime - if (elapsed >= availableTime) { - informTooManyCallbacksToHandleInThisFrame(elapsed, availableTime, processedTimedTasks, processedTasks) - break - } - } - } - } catch (e: Throwable) { - println("Error in GameWindowCoroutineDispatcher.executePending:") - e.printStackTrace() - } - } - - val tooManyCallbacksLogger = Logger("Korgw.GameWindow.TooManyCallbacks") - - fun informTooManyCallbacksToHandleInThisFrame(elapsedTime: TimeSpan, availableTime: TimeSpan, processedTimedTasks: Int, processedTasks: Int) { - tooManyCallbacksLogger.warn { "Too many callbacks to handle in this frame elapsedTime=${elapsedTime.roundMilliseconds()}, availableTime=${availableTime.roundMilliseconds()} pending timedTasks=${timedTasks.size}, tasks=${tasks.size}, processedTimedTasks=$processedTimedTasks, processedTasks=$processedTasks" } - } - - override fun close() { - executePending(2.seconds) - logger.info { "GameWindowCoroutineDispatcher.close" } - while (timedTasks.isNotEmpty()) { - timedTasks.removeHead().continuation?.resume(Unit) - } - while (tasks.isNotEmpty()) { - tasks.dequeue().run() - } - } - - val hasMore get() = timedTasks.isNotEmpty() || hasTasks() - - override fun toString(): String = "GameWindowCoroutineDispatcher(setNow=setNow, fast=$fast)" - - - companion object { - val logger = Logger("GameWindow") - } -} diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt new file mode 100644 index 0000000000..8ec3b24b58 --- /dev/null +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -0,0 +1,400 @@ +package korlibs.render + +import korlibs.datastructure.* +import korlibs.event.* +import korlibs.image.bitmap.* +import korlibs.image.color.* +import korlibs.image.vector.* +import korlibs.io.async.* +import korlibs.io.file.* +import korlibs.korge.view.* +import korlibs.math.geom.* +import korlibs.memory.* +import korlibs.render.GameWindowQuality.* +import kotlin.native.concurrent.* + +class GameWindowEventInstances { + val pauseEvent = PauseEvent() + val resumeEvent = ResumeEvent() + val stopEvent = StopEvent() + val destroyEvent = DestroyEvent() + val updateEvent = UpdateEvent() + val renderEvent = RenderEvent() + val initEvent = InitEvent() + val disposeEvent = DisposeEvent() + val fullScreenEvent = FullScreenEvent() + val reshapeEvent = ReshapeEvent() + val keyEvent = KeyEvent() + val mouseEvent = MouseEvent() + val gestureEvent = GestureEvent() + val dropFileEvent = DropFileEvent() +} + +class GameWindowInputState { + val keysPresing = BooleanArray(Key.MAX) + fun pressing(key: Key) = keysPresing[key.ordinal] + val shift get() = pressing(Key.SHIFT) + val ctrl get() = pressing(Key.CONTROL) + val alt get() = pressing(Key.ALT) + val meta get() = pressing(Key.META) || pressing(Key.SUPER) + var mouseButtons = 0 + val scrollDeltaX = 0f + val scrollDeltaY = 0f + val scrollDeltaZ = 0f + val scaleCoords = false + + var surfaceChanged = false + var surfaceX = -1 + var surfaceY = -1 + var surfaceWidth = -1 + var surfaceHeight = -1 + var doInitialize = false + var initialized = false + var contextLost = false +} + +fun GameWindow.dispatchKeyEvent(type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { + return dispatchKeyEventEx(type, id, character, key, keyCode, str = str) +} + +fun GameWindow.dispatchKeyEventDownUp(id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { + val cancel1 = dispatchKeyEvent(KeyEvent.Type.DOWN, id, character, key, keyCode, str) + val cancel2 = dispatchKeyEvent(KeyEvent.Type.UP, id, character, key, keyCode, str) + return cancel1 || cancel2 +} + +//private val gamePadUpdateEvent = GamePadUpdateEvent() +fun GameWindow.dispatchGamepadUpdateStart() { + gamepadEmitter.dispatchGamepadUpdateStart() +} + +fun GameWindow.dispatchGamepadUpdateAdd(info: GamepadInfo) { + gamepadEmitter.dispatchGamepadUpdateAdd(info) +} + +/** + * Triggers an update envent and potential CONNECTED/DISCONNECTED events. + * + * Returns a list of disconnected gamepads. + */ +fun GameWindow.dispatchGamepadUpdateEnd(out: IntArrayList = IntArrayList()): IntArrayList = + gamepadEmitter.dispatchGamepadUpdateEnd(out) + +fun GameWindow.dispatchKeyEventEx( + type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, + shift: Boolean = gameWindowInputState.shift, ctrl: Boolean = gameWindowInputState.ctrl, alt: Boolean = gameWindowInputState.alt, meta: Boolean = gameWindowInputState.meta, + str: String? = null +): Boolean { + if (type != KeyEvent.Type.TYPE) { + gameWindowInputState.keysPresing[key.ordinal] = (type == KeyEvent.Type.DOWN) + } + dispatch(events.keyEvent.reset { + this.id = id + this.character = character + this.key = key + this.keyCode = keyCode + this.type = type + this.shift = shift + this.ctrl = ctrl + this.alt = alt + this.meta = meta + if (str != null && str.length == 1) { + this.str = null + this.character = str[0] + this.keyCode = this.character.code + } else { + this.str = str + } + }) + return events.keyEvent.defaultPrevented +} + +fun GameWindow.dispatchSimpleMouseEvent( + type: MouseEvent.Type, id: Int, x: Int, y: Int, button: MouseButton, simulateClickOnUp: Boolean = false +) { + dispatchMouseEvent(type, id, x, y, button, simulateClickOnUp = simulateClickOnUp) +} + +fun GameWindow.dispatchMouseEvent( + type: MouseEvent.Type, id: Int, x: Int, y: Int, button: MouseButton, buttons: Int = gameWindowInputState.mouseButtons, + scrollDeltaX: Float = gameWindowInputState.scrollDeltaX, scrollDeltaY: Float = gameWindowInputState.scrollDeltaY, scrollDeltaZ: Float = gameWindowInputState.scrollDeltaZ, + isShiftDown: Boolean = gameWindowInputState.shift, isCtrlDown: Boolean = gameWindowInputState.ctrl, isAltDown: Boolean = gameWindowInputState.alt, isMetaDown: Boolean = gameWindowInputState.meta, + scaleCoords: Boolean = gameWindowInputState.scaleCoords, simulateClickOnUp: Boolean = false, + scrollDeltaMode: MouseEvent.ScrollDeltaMode = MouseEvent.ScrollDeltaMode.LINE +) { + if (type != MouseEvent.Type.DOWN && type != MouseEvent.Type.UP) { + gameWindowInputState.mouseButtons = gameWindowInputState.mouseButtons.setBits(1 shl button.ordinal, type == MouseEvent.Type.DOWN) + } + dispatch(events.mouseEvent.reset { + this.type = type + this.id = id + this.x = x + this.y = y + this.button = button + this.buttons = buttons + this.scrollDeltaX = scrollDeltaX + this.scrollDeltaY = scrollDeltaY + this.scrollDeltaZ = scrollDeltaZ + this.scrollDeltaMode = scrollDeltaMode + this.isShiftDown = isShiftDown + this.isCtrlDown = isCtrlDown + this.isAltDown = isAltDown + this.isMetaDown = isMetaDown + this.scaleCoords = scaleCoords + }) + //if (simulateClickOnUp && type == MouseEvent.Type.UP) { + // dispatchMouseEvent(MouseEvent.Type.CLICK, id, x, y, button, buttons, scrollDeltaX, scrollDeltaY, scrollDeltaZ, isShiftDown, isCtrlDown, isAltDown, isMetaDown, scaleCoords, simulateClickOnUp = false) + //} +} + + + +fun GameWindow.dispatchInitEvent() = dispatch(events.initEvent.reset()) +fun GameWindow.dispatchPauseEvent() = dispatch(events.pauseEvent.reset()) +fun GameWindow.dispatchResumeEvent() = dispatch(events.resumeEvent.reset()) +fun GameWindow.dispatchStopEvent() = dispatch(events.stopEvent.reset()) +fun GameWindow.dispatchDestroyEvent() = dispatch(events.destroyEvent.reset()) +fun GameWindow.dispatchDisposeEvent() = dispatch(events.disposeEvent.reset()) +fun GameWindow.dispatchUpdateEvent() { + updateRenderLock { dispatch(events.updateEvent) } +} +fun GameWindow.dispatchRenderEvent(update: Boolean = true, render: Boolean = true) { + updateRenderLock { + dispatch(events.renderEvent.reset { + this.update = update + this.render = render + }) + } +} +fun GameWindow.dispatchNewRenderEvent() { + dispatchRenderEvent(update = false, render = true) +} +fun GameWindow.dispatchDropfileEvent(type: DropFileEvent.Type, files: List?) = dispatch(events.dropFileEvent.reset { + this.type = type + this.files = files +}) +fun GameWindow.dispatchFullscreenEvent(fullscreen: Boolean) = dispatch(events.fullScreenEvent.reset { this.fullscreen = fullscreen }) + +fun GameWindow.dispatchReshapeEvent(x: Int, y: Int, width: Int, height: Int) { + dispatchReshapeEventEx(x, y, width, height, width, height) +} + +fun GameWindow.dispatchReshapeEventEx(x: Int, y: Int, width: Int, height: Int, fullWidth: Int, fullHeight: Int) { + ag.mainFrameBuffer.setSize(x, y, width, height, fullWidth, fullHeight) + dispatch(events.reshapeEvent.reset { + this.x = x + this.y = y + this.width = width + this.height = height + }) +} + +fun GameWindow.handleInitEventIfRequired() { + if (gameWindowInputState.initialized) return + gameWindowInputState.initialized = true + gameWindowInputState.doInitialize = true +} + +fun GameWindow.handleReshapeEventIfRequired(x: Int, y: Int, width: Int, height: Int) { + if (gameWindowInputState.surfaceX == x && gameWindowInputState.surfaceY == y && gameWindowInputState.surfaceWidth == width && gameWindowInputState.surfaceHeight == height) return + println("handleReshapeEventIfRequired: $x, $y, $width, $height") + gameWindowInputState.surfaceChanged = true + gameWindowInputState.surfaceX = x + gameWindowInputState.surfaceY = y + gameWindowInputState.surfaceWidth = width + gameWindowInputState.surfaceHeight = height +} + +@ThreadLocal +var GLOBAL_CHECK_GL = false + +data class GameWindowCreationConfig( + val multithreaded: Boolean? = null, + val hdr: Boolean? = null, + val msaa: Int? = null, + val checkGl: Boolean = false, + val logGl: Boolean = false, + val cacheGl: Boolean = false, + val fullscreen: Boolean? = null, +) { + companion object { + val DEFAULT = GameWindowCreationConfig() + } +} + +interface GameWindowConfig { + val quality: GameWindowQuality + + class Impl( + override val quality: GameWindowQuality = GameWindowQuality.AUTOMATIC, + ) : GameWindowConfig +} + + +/** + * Describes if the rendering should focus on performance or quality. + * [PERFORMANCE] will use lower resolutions, while [QUALITY] will use the devicePixelRatio + * to render high quality images. + */ +enum class GameWindowQuality(override val level: Float) : korlibs.image.Quality { + /** Will render to lower resolutions, ignoring devicePixelRatio on retina-like screens */ + PERFORMANCE(0f), + /** Will render to higher resolutions, using devicePixelRatio on retina-like screens */ + QUALITY(1f), + /** Will choose [PERFORMANCE] or [QUALITY] based on some heuristics */ + AUTOMATIC(.5f); + + private val UPPER_BOUND_RENDERED_PIXELS = 4_000_000 + + fun computeTargetScale( + width: Int, + height: Int, + devicePixelRatio: Float, + targetPixels: Int = UPPER_BOUND_RENDERED_PIXELS + ): Float = when (this) { + PERFORMANCE -> 1f + QUALITY -> devicePixelRatio + AUTOMATIC -> { + listOf(devicePixelRatio, 2f, 1f) + .firstOrNull { width * height * it <= targetPixels } + ?: 1f + } + } + + interface Alias { + val PERFORMANCE get() = GameWindowQuality.PERFORMANCE + val QUALITY get() = GameWindowQuality.QUALITY + val AUTOMATIC get() = GameWindowQuality.AUTOMATIC + } +} + +interface ClipboardData +data class TextClipboardData(val text: String, val contentType: String? = null) : ClipboardData + +fun GameWindow.toggleFullScreen() { fullscreen = !fullscreen } + +fun T.configure( + size: Size, + title: String? = "GameWindow", + icon: Bitmap? = null, + fullscreen: Boolean? = null, + bgcolor: RGBA = Colors.BLACK, +): T { + this.setSize(size.width.toInt(), size.height.toInt()) + if (title != null) this.title = title + this.icon = icon + if (fullscreen != null) this.fullscreen = fullscreen + this.backgroundColor = bgcolor + this.visible = true + return this +} + +@Deprecated("") +fun GameWindow.onDragAndDropFileEvent(block: suspend (DropFileEvent) -> Unit) { + onEvents(*DropFileEvent.Type.ALL) { event -> + launchImmediately(coroutineDispatcher) { + block(event) + } + } +} + +enum class HapticFeedbackKind { GENERIC, ALIGNMENT, LEVEL_CHANGE } + +data class GameWindowMenuItem(val text: String?, val enabled: Boolean = true, val children: List? = null, val action: () -> Unit = {}) { + companion object { + val SEPARATOR = GameWindowMenuItem(null) + } +} + +class GameWindowMenuItemBuilder(private var text: String? = null, private var enabled: Boolean = true, private var action: () -> Unit = {}) { + @PublishedApi internal val children = arrayListOf() + + inline fun separator() { + item(null) + } + + inline fun item(text: String?, enabled: Boolean = true, noinline action: () -> Unit = {}, block: GameWindowMenuItemBuilder.() -> Unit = {}): GameWindowMenuItem { + val mib = GameWindowMenuItemBuilder(text, enabled, action) + block(mib) + val item = mib.toItem() + children.add(item) + return item + } + + fun toItem() = GameWindowMenuItem(text, enabled, children.ifEmpty { null }, action) +} + +sealed interface ICursor + +data class CustomCursor(val shape: Shape, val name: String = "custom") : ICursor, Extra by Extra.Mixin() { + val bounds: Rectangle = this.shape.bounds + fun createBitmap(size: Size? = null, native: Boolean = true) = shape.renderWithHotspot(fit = size, native = native) +} + +enum class Cursor : ICursor { + DEFAULT, CROSSHAIR, TEXT, HAND, MOVE, WAIT, + RESIZE_EAST, RESIZE_WEST, RESIZE_SOUTH, RESIZE_NORTH, + RESIZE_NORTH_EAST, RESIZE_NORTH_WEST, RESIZE_SOUTH_EAST, RESIZE_SOUTH_WEST; + + interface Alias { + val DEFAULT get() = Cursor.DEFAULT + val CROSSHAIR get() = Cursor.CROSSHAIR + val TEXT get() = Cursor.TEXT + val HAND get() = Cursor.HAND + val MOVE get() = Cursor.MOVE + val WAIT get() = Cursor.WAIT + val RESIZE_EAST get() = Cursor.RESIZE_EAST + val RESIZE_WEST get() = Cursor.RESIZE_WEST + val RESIZE_SOUTH get() = Cursor.RESIZE_SOUTH + val RESIZE_NORTH get() = Cursor.RESIZE_NORTH + val RESIZE_NORTH_EAST get() = Cursor.RESIZE_NORTH_EAST + val RESIZE_NORTH_WEST get() = Cursor.RESIZE_NORTH_WEST + val RESIZE_SOUTH_EAST get() = Cursor.RESIZE_SOUTH_EAST + val RESIZE_SOUTH_WEST get() = Cursor.RESIZE_SOUTH_WEST + val ANGLE_TO_CURSOR get() = Cursor.ANGLE_TO_CURSOR + fun fromAngleResize(angle: Angle?): ICursor? = Cursor.fromAngleResize(angle) + fun fromAnchorResize(anchor: Anchor): ICursor? = Cursor.fromAnchorResize(anchor) + } + + companion object { + val ANGLE_TO_CURSOR: Map = mapOf( + (45.degrees * 0) to RESIZE_EAST, + (45.degrees * 1) to RESIZE_SOUTH_EAST, + (45.degrees * 2) to RESIZE_SOUTH, + (45.degrees * 3) to RESIZE_SOUTH_WEST, + (45.degrees * 4) to RESIZE_WEST, + (45.degrees * 5) to RESIZE_NORTH_WEST, + (45.degrees * 6) to RESIZE_NORTH, + (45.degrees * 7) to RESIZE_NORTH_EAST, + ) + + fun fromAngleResize(angle: Angle?): ICursor? { + var minDistance = 360.degrees + var cursor: ICursor? = null + if (angle != null) { + for ((cangle, ccursor) in ANGLE_TO_CURSOR) { + val cdistance = (angle - cangle).absoluteValue + if (cdistance <= minDistance) { + minDistance = cdistance + cursor = ccursor + } + } + } + return cursor + } + + fun fromAnchorResize(anchor: Anchor): ICursor? { + return when (anchor) { + Anchor.TOP_LEFT -> RESIZE_NORTH_WEST + Anchor.TOP -> RESIZE_NORTH + Anchor.TOP_RIGHT -> RESIZE_NORTH_EAST + Anchor.LEFT -> RESIZE_WEST + Anchor.RIGHT -> RESIZE_EAST + Anchor.BOTTOM_LEFT -> RESIZE_SOUTH_WEST + Anchor.BOTTOM -> RESIZE_SOUTH + Anchor.BOTTOM_RIGHT -> RESIZE_SOUTH_EAST + else -> null + } + } + } +} diff --git a/korge/src/common/korlibs/render/Korgw.kt b/korge/src/common/korlibs/render/Korgw.kt deleted file mode 100644 index 9b76f1aa4d..0000000000 --- a/korge/src/common/korlibs/render/Korgw.kt +++ /dev/null @@ -1,6 +0,0 @@ -package korlibs.render - -object Korgw { - object Sample { - } -} diff --git a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt index 46f1b2a674..30911b92c5 100644 --- a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt +++ b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt @@ -21,6 +21,8 @@ import platform.GameController.* import platform.UIKit.* import platform.darwin.* +actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = MyIosGameWindow + expect val iosTvosTools: IosTvosToolsImpl open class IosTvosToolsImpl { @@ -244,6 +246,9 @@ class MyGLKViewController( logger.info { "dispatchInitEvent" } gameWindow.dispatchInitEvent() + gameWindow.queue { + + } gameWindow.entry { logger.info { "Executing entry..." } this.entry() @@ -268,20 +273,10 @@ class MyGLKViewController( gameWindow.dispatchReshapeEvent(0, 0, width, height) } - //this.value++ - //glDisable(GL_SCISSOR_TEST) - //glDisable(GL_STENCIL_TEST) - //glViewport(0, 0, 200, 300) - //glScissor(0, 0, 200, 300) - //glClearColor((this.value % 100).toFloat() / 100f, 0f, 1f, 1f) - //glClear(GL_COLOR_BUFFER_BIT) - darwinGamePad.updateGamepads(gameWindow) gameWindow.frame() } - - override fun didReceiveMemoryWarning() { //super.didReceiveMemoryWarning() //if (this.isViewLoaded && this.view.window != null) { @@ -400,26 +395,6 @@ open class IosGameWindow( glXViewController?.preferredFramesPerSecond = value.convert() } override var fps: Int get() = 60; set(value) = Unit - //override var title: String get() = ""; set(value) = Unit - //override val width: Int get() = 512 - //override val height: Int get() = 512 - //override var icon: Bitmap? get() = null; set(value) = Unit - //override var fullscreen: Boolean get() = false; set(value) = Unit - //override var visible: Boolean get() = false; set(value) = Unit - //override var quality: Quality get() = Quality.AUTOMATIC; set(value) = Unit - - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - println("YAY! IosGameWindow.loop") - // Trick to reference some classes to make them available on iOS - //println("loop[0]") - try { - entry(this) - //println("loop[1]") - } catch (e: Throwable) { - println("ERROR IosGameWindow.loop:") - e.printStackTrace() - } - } override val isSoftKeyboardVisible: Boolean get() = super.isSoftKeyboardVisible lateinit var textField: MyUITextComponent @@ -661,8 +636,6 @@ fun SetInitialIosGameWindow(gameWindow: IosGameWindow): IosGameWindow { return gameWindow } -actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = MyIosGameWindow - /* import UIKit diff --git a/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt b/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt index 2de33c22e7..bc4f11550f 100644 --- a/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt +++ b/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt @@ -1,71 +1,9 @@ package korlibs.render -import SdlGameWindowJvm -import korlibs.event.* import korlibs.graphics.* -import korlibs.image.color.* -import korlibs.platform.* import korlibs.render.awt.* -import korlibs.render.osx.* -import korlibs.render.x11.* -import kotlinx.coroutines.* -actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow { - //if (Platform.isMac) initializeMacOnce() - - val engine = korgwJvmEngine - ?: System.getenv("KORGW_JVM_ENGINE") - ?: System.getProperty("korgw.jvm.engine") - //?: "jogl" - //?: "jna" - ?: "default" - - val checkGl = null - ?: System.getenv("KORGW_CHECK_OPENGL")?.toBooleanOrNull() - ?: System.getProperty("korgw.check.opengl")?.toBooleanOrNull() - ?: GLOBAL_CHECK_GL - - val logGl = null - ?: System.getenv("KORGW_LOG_OPENGL")?.toBooleanOrNull() - ?: System.getProperty("korgw.log.opengl")?.toBooleanOrNull() - ?: false - - val realConfig = config.copy(checkGl = checkGl, logGl = logGl) - - return when (engine) { - "default" -> when { - else -> AwtGameWindow(realConfig) - } - "jna" -> when { - Platform.isMac -> { - when { - isOSXMainThread -> MacGameWindow(checkGl, logGl) - else -> { - println("WARNING. Slower startup: NOT in main thread! Using AWT! (on mac use -XstartOnFirstThread when possible)") - AwtGameWindow(realConfig) - } - } - } - Platform.isLinux -> AwtGameWindow(realConfig) - Platform.isWindows -> AwtGameWindow(realConfig) - else -> X11GameWindow(checkGl) - } - "awt" -> when { - Platform.isMac && isOSXMainThread -> MacGameWindow(checkGl,logGl) - else -> AwtGameWindow(realConfig) - } - // On mac you should install https://www.libsdl.org/release/SDL2-2.0.16.dmg on `/Library/Frameworks` or `~/Library/Frameworks` - "sdl" -> { - if (!isOSXMainThread) { - println("WARNING. NOT in main thread! Can't use SDL backend. Need to use -XstartOnFirstThread") - } - SdlGameWindowJvm(checkGl) - } - else -> { - error("Unsupported KORGW_JVM_ENGINE,korgw.jvm.engine='$engine'") - } - } -} +actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = NewAwtGameWindow(config) object JvmAGFactory : AGFactory { override val supportsNativeFrame: Boolean = true @@ -81,35 +19,3 @@ object JvmAGFactory : AGFactory { } } } - -object TestGameWindow { - @JvmStatic - fun main(args: Array) { - runBlocking { - val gameWindow = CreateDefaultGameWindow() - //val gameWindow = Win32GameWindow() - //val gameWindow = AwtGameWindow() - gameWindow.onEvent(MouseEvent.Type.CLICK) { - //println("MOUSE EVENT $it") - gameWindow.toggleFullScreen() - } - //gameWindow.toggleFullScreen() - gameWindow.setSize(320, 240) - gameWindow.title = "HELLO WORLD" - var step = 0 - gameWindow.loop { - val ag = gameWindow.ag - gameWindow.onRenderEvent { - ag.clear(ag.mainFrameBuffer, RGBA(64, 96, step % 256, 255)) - step++ - } - } - } - } -} - -private fun String.toBooleanOrNull(): Boolean? = try { - toBoolean() -} catch (e: Throwable) { - null -} diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index f84155262c..def0cdb942 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -1,5 +1,6 @@ package korlibs.render.awt +import korlibs.datastructure.* import korlibs.graphics.* import korlibs.graphics.gl.* import korlibs.kgl.* @@ -20,7 +21,7 @@ import javax.swing.* // https://www.oracle.com/java/technologies/painting.html // https://docs.oracle.com/javase/tutorial/extra/fullscreen/rendering.html // https://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html -open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base() { +open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { init { System.setProperty("sun.java2d.opengl", "true") } diff --git a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt index 1ced8a4f5b..116f8446df 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt @@ -21,10 +21,12 @@ import java.io.* import javax.imageio.* import javax.swing.* +object AwtFrameTools + fun JFrame.setIconIncludingTaskbarFromResource(path: String) { runCatching { - val awtImageURL = AwtGameWindow::class.java.getResource("/$path") - ?: AwtGameWindow::class.java.getResource(path) + val awtImageURL = AwtFrameTools::class.java.getResource("/$path") + ?: AwtFrameTools::class.java.getResource(path) ?: ClassLoader.getSystemResource(path) setIconIncludingTaskbarFromImage(awtImageURL?.let { ImageIO.read(it) }) } @@ -170,25 +172,25 @@ val Component.pixelsPerInch: Double get() { } -val GameWindow.Cursor.jvmCursor: java.awt.Cursor get() = java.awt.Cursor(when (this) { - GameWindow.Cursor.DEFAULT -> java.awt.Cursor.DEFAULT_CURSOR - GameWindow.Cursor.CROSSHAIR -> java.awt.Cursor.CROSSHAIR_CURSOR - GameWindow.Cursor.TEXT -> java.awt.Cursor.TEXT_CURSOR - GameWindow.Cursor.HAND -> java.awt.Cursor.HAND_CURSOR - GameWindow.Cursor.MOVE -> java.awt.Cursor.MOVE_CURSOR - GameWindow.Cursor.WAIT -> java.awt.Cursor.WAIT_CURSOR - GameWindow.Cursor.RESIZE_EAST -> java.awt.Cursor.E_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_SOUTH -> java.awt.Cursor.S_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_WEST -> java.awt.Cursor.W_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_NORTH -> java.awt.Cursor.N_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_NORTH_EAST -> java.awt.Cursor.NE_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_NORTH_WEST -> java.awt.Cursor.NW_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_SOUTH_EAST -> java.awt.Cursor.SE_RESIZE_CURSOR - GameWindow.Cursor.RESIZE_SOUTH_WEST -> java.awt.Cursor.SW_RESIZE_CURSOR +val korlibs.render.Cursor.jvmCursor: java.awt.Cursor get() = java.awt.Cursor(when (this) { + korlibs.render.Cursor.DEFAULT -> java.awt.Cursor.DEFAULT_CURSOR + korlibs.render.Cursor.CROSSHAIR -> java.awt.Cursor.CROSSHAIR_CURSOR + korlibs.render.Cursor.TEXT -> java.awt.Cursor.TEXT_CURSOR + korlibs.render.Cursor.HAND -> java.awt.Cursor.HAND_CURSOR + korlibs.render.Cursor.MOVE -> java.awt.Cursor.MOVE_CURSOR + korlibs.render.Cursor.WAIT -> java.awt.Cursor.WAIT_CURSOR + korlibs.render.Cursor.RESIZE_EAST -> java.awt.Cursor.E_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_SOUTH -> java.awt.Cursor.S_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_WEST -> java.awt.Cursor.W_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_NORTH -> java.awt.Cursor.N_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_NORTH_EAST -> java.awt.Cursor.NE_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_NORTH_WEST -> java.awt.Cursor.NW_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_SOUTH_EAST -> java.awt.Cursor.SE_RESIZE_CURSOR + korlibs.render.Cursor.RESIZE_SOUTH_WEST -> java.awt.Cursor.SW_RESIZE_CURSOR else -> java.awt.Cursor.DEFAULT_CURSOR }) -val GameWindow.CustomCursor.jvmCursor: java.awt.Cursor by extraPropertyThis { +val CustomCursor.jvmCursor: java.awt.Cursor by extraPropertyThis { val toolkit = Toolkit.getDefaultToolkit() val size = toolkit.getBestCursorSize(bounds.width.toIntCeil(), bounds.height.toIntCeil()) val result = this.createBitmap(Size(size.width, size.height)) @@ -200,15 +202,15 @@ val GameWindow.CustomCursor.jvmCursor: java.awt.Cursor by extraPropertyThis { } } -val GameWindow.ICursor.jvmCursor: java.awt.Cursor get() { +val ICursor.jvmCursor: java.awt.Cursor get() { return when (this) { - is GameWindow.Cursor -> this.jvmCursor - is GameWindow.CustomCursor -> this.jvmCursor + is korlibs.render.Cursor -> this.jvmCursor + is CustomCursor -> this.jvmCursor else -> java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR) } } -fun Component.hapticFeedbackGenerate(kind: GameWindow.HapticFeedbackKind) { +fun Component.hapticFeedbackGenerate(kind: HapticFeedbackKind) { when { Platform.os.isMac -> { val KIND_GENERIC = 0 @@ -220,9 +222,9 @@ fun Component.hapticFeedbackGenerate(kind: GameWindow.HapticFeedbackKind) { val PERFORMANCE_TIME_DRAW_COMPLETED = 2 val kindInt = when (kind) { - GameWindow.HapticFeedbackKind.GENERIC -> KIND_GENERIC - GameWindow.HapticFeedbackKind.ALIGNMENT -> KIND_ALIGNMENT - GameWindow.HapticFeedbackKind.LEVEL_CHANGE -> KIND_LEVEL_CHANGE + HapticFeedbackKind.GENERIC -> KIND_GENERIC + HapticFeedbackKind.ALIGNMENT -> KIND_ALIGNMENT + HapticFeedbackKind.LEVEL_CHANGE -> KIND_LEVEL_CHANGE } val performanceTime = PERFORMANCE_TIME_NOW @@ -318,3 +320,24 @@ fun Component.registerGestureListeners(dispatcher: GameWindow) { e.printStackTrace() } } + +fun GameWindowMenuItem?.toJMenuItem(): JComponent { + val item = this + return when { + item?.text == null -> JSeparator() + item.children != null -> { + JMenu(item.text).also { + it.isEnabled = item.enabled + it.addActionListener { item.action() } + for (child in item.children) { + it.add(child.toJMenuItem()) + } + } + } + else -> JMenuItem(item.text).also { + it.isEnabled = item.enabled + it.addActionListener { item.action() } + } + } +} + diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 179c35191e..352da6cfed 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -1,5 +1,6 @@ package korlibs.render.awt +/* import korlibs.image.awt.* import korlibs.image.bitmap.* import korlibs.math.* @@ -115,3 +116,4 @@ class AwtGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig. frame.dispose() } } +*/ diff --git a/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt index 63bed47fa2..a363d657f3 100644 --- a/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt @@ -1,5 +1,6 @@ package korlibs.render.awt +/* import korlibs.datastructure.* import korlibs.event.* import korlibs.graphics.* @@ -59,27 +60,7 @@ abstract class BaseAwtGameWindow( component.cursor = value.jvmCursor } - fun MenuItem?.toJMenuItem(): JComponent { - val item = this - return when { - item?.text == null -> JSeparator() - item.children != null -> { - JMenu(item.text).also { - it.isEnabled = item.enabled - it.addActionListener { item.action() } - for (child in item.children) { - it.add(child.toJMenuItem()) - } - } - } - else -> JMenuItem(item.text).also { - it.isEnabled = item.enabled - it.addActionListener { item.action() } - } - } - } - - override fun setMainMenu(items: List) { + override fun setMainMenu(items: List) { val component = this.component if (component !is JFrame) { println("GameWindow.setMainMenu: component=$component") @@ -99,7 +80,7 @@ abstract class BaseAwtGameWindow( println("GameWindow.setMainMenu: component=$component, bar=$bar") } - override fun showContextMenu(items: List) { + override fun showContextMenu(items: List) { val popupMenu = JPopupMenu() for (item in items) { popupMenu.add(item.toJMenuItem()) @@ -246,7 +227,7 @@ abstract class BaseAwtGameWindow( override val width: Int get() = (scaledWidth).toInt() override val height: Int get() = (scaledHeight).toInt() override var visible: Boolean by LazyDelegate { component::visible } - override var bgcolor: RGBA + override var backgroundColor: RGBA get() = component.background.toRgba() set(value) { component.background = value.toAwt() @@ -592,3 +573,4 @@ abstract class BaseAwtGameWindow( override val hapticFeedbackGenerateSupport: Boolean get() = true override fun hapticFeedbackGenerate(kind: HapticFeedbackKind) = component.hapticFeedbackGenerate(kind) } +*/ diff --git a/korge/src/jvm/korlibs/render/awt/GLCanvas.kt b/korge/src/jvm/korlibs/render/awt/GLCanvas.kt index 46d0917994..086acde15e 100644 --- a/korge/src/jvm/korlibs/render/awt/GLCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/GLCanvas.kt @@ -1,14 +1,11 @@ package korlibs.render.awt import korlibs.graphics.* -import korlibs.kgl.* import korlibs.graphics.gl.* import korlibs.kgl.* -import korlibs.korge.view.* import korlibs.render.* import korlibs.render.platform.* import java.awt.* -import java.awt.Graphics import java.io.* open class GLCanvas constructor(checkGl: Boolean = true, val logGl: Boolean = false, cacheGl: Boolean = false) : Canvas(), GameWindowConfig, Closeable { @@ -100,5 +97,5 @@ open class GLCanvas constructor(checkGl: Boolean = true, val logGl: Boolean = fa } } - override var quality: GameWindow.Quality = GameWindow.Quality.AUTOMATIC + override var quality: GameWindowQuality = GameWindowQuality.AUTOMATIC } diff --git a/korge/src/jvm/korlibs/render/awt/GLCanvasGameWindow.kt b/korge/src/jvm/korlibs/render/awt/GLCanvasGameWindow.kt deleted file mode 100644 index 351c192fb4..0000000000 --- a/korge/src/jvm/korlibs/render/awt/GLCanvasGameWindow.kt +++ /dev/null @@ -1,19 +0,0 @@ -package korlibs.render.awt - -import korlibs.render.platform.* -import java.awt.* - -open class GLCanvasGameWindow( - val canvas: GLCanvas, -) : BaseAwtGameWindow(canvas.ag) { - init { - exitProcessOnClose = false - canvas.defaultRenderer = { gl, g -> - framePaint(g) - } - } - - override val ctx: BaseOpenglContext get() = canvas.ctx!! - override val component: Component get() = canvas - override val contentComponent: Component get() = canvas -} diff --git a/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt index 396ac57808..c5cc93c7a6 100644 --- a/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt @@ -1,20 +1,42 @@ package korlibs.render.awt +import korlibs.datastructure.* import korlibs.datastructure.event.* import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.image.awt.* import korlibs.image.bitmap.* -import korlibs.io.async.* import korlibs.render.* import korlibs.time.* import java.awt.* import java.awt.event.* import javax.swing.* -class NewAwtGameWindow : GameWindow() { - val canvas = AwtAGOpenglCanvas() +val AwtAGOpenglCanvas.gameWindow: NewAwtCanvasGameWindow by Extra.PropertyThis { NewAwtCanvasGameWindow(this) } + +open class NewAwtCanvasGameWindow(val canvas: AwtAGOpenglCanvas) : GameWindow() { override val ag: AG get() = canvas.ag + + val thread = nativeThread(name = "NewAwtGameWindow") { + eventLoop.runTasksForever() + } + + init { + canvas.doRender = { + dispatchNewRenderEvent() + } + coroutineDispatcher.eventLoop.setInterval(60.hz) { + dispatchUpdateEvent() + } + } + + override fun close(exitCode: Int) { + super.close(exitCode) + coroutineDispatcher.close() + } +} + +class NewAwtGameWindow(val config: GameWindowCreationConfig = GameWindowCreationConfig()) : NewAwtCanvasGameWindow(AwtAGOpenglCanvas()) { val frame = object : JFrame() { init { isVisible = false @@ -37,39 +59,13 @@ class NewAwtGameWindow : GameWindow() { } } - val thread = nativeThread(name = "NewAwtGameWindow") { - myCoroutineDispatcher.loopForever() - } - - init { - canvas.doRender = { - dispatchNewRenderEvent() - } - myCoroutineDispatcher.eventLoop.setInterval(60.hz) { - dispatchUpdateEvent() - } - } - - override var alwaysOnTop: Boolean - get() = frame.isAlwaysOnTop - set(value) { frame.isAlwaysOnTop = value } + override var alwaysOnTop: Boolean by frame::_isAlwaysOnTop override var title: String by frame::title - override var visible: Boolean - get() = frame.isVisible - set(value) { frame.isVisible = value } + override var visible: Boolean by frame::visible override var icon: Bitmap? = null set(value) { field = value frame.setIconIncludingTaskbarFromImage(value?.toAwt()) } override var fullscreen: Boolean by frame::isFullScreen - - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - launchUnscoped { entry() } - } - - override fun close(exitCode: Int) { - super.close(exitCode) - myCoroutineDispatcher.close() - } } diff --git a/korge/src/jvm/korlibs/render/osx/Cocoa.kt b/korge/src/jvm/korlibs/render/osx/Cocoa.kt index d5719b2a50..95971ee156 100644 --- a/korge/src/jvm/korlibs/render/osx/Cocoa.kt +++ b/korge/src/jvm/korlibs/render/osx/Cocoa.kt @@ -1,10 +1,9 @@ package korlibs.render.osx import com.sun.jna.* +import korlibs.memory.dyn.* import korlibs.memory.dyn.osx.* -import korlibs.memory.dyn.osx.NativeName import korlibs.render.platform.* -import korlibs.render.platform.NativeLoad class NSApplication(id: Long) : NSObject(id) { fun setActivationPolicy(value: Int) = id.msgSend("setActivationPolicy:", value.toLong()) diff --git a/korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt b/korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt index 896c7d4055..84a24315e0 100644 --- a/korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt +++ b/korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt @@ -1,22 +1,19 @@ package korlibs.render.osx +import com.sun.jna.* import korlibs.datastructure.iterators.* -import korlibs.memory.dyn.osx.* import korlibs.event.* -import korlibs.render.* import korlibs.io.annotations.* -import korlibs.io.util.* import korlibs.math.geom.* -import com.sun.jna.* +import korlibs.memory.dyn.osx.* import korlibs.number.* +import korlibs.render.* import kotlin.reflect.* //fun main() { //} -interface FrameworkInt : Library { - -} +interface FrameworkInt : Library @PublishedApi internal inline fun getValueOrNull(obj: ObjcRef, property: KProperty<*>, gen: (Long) -> T): T? = diff --git a/korge/src/jvm/korlibs/render/osx/DialogInterfaceJvmCocoa.kt b/korge/src/jvm/korlibs/render/osx/DialogInterfaceJvmCocoa.kt deleted file mode 100644 index 6c56d27a7e..0000000000 --- a/korge/src/jvm/korlibs/render/osx/DialogInterfaceJvmCocoa.kt +++ /dev/null @@ -1,7 +0,0 @@ -package korlibs.render.osx - -import korlibs.render.* - -class DialogInterfaceJvmCocoa(val gwProvider: () -> MacGameWindow) : DialogInterface { - val gw get() = gwProvider() -} diff --git a/korge/src/jvm/korlibs/render/osx/MacGL.kt b/korge/src/jvm/korlibs/render/osx/MacGL.kt new file mode 100644 index 0000000000..42fb90a243 --- /dev/null +++ b/korge/src/jvm/korlibs/render/osx/MacGL.kt @@ -0,0 +1,53 @@ +package korlibs.render.osx + +import com.sun.jna.* +import korlibs.memory.dyn.* +import korlibs.render.platform.* + +//open class MacKmlGL : NativeKgl(MacGL) +open class MacKmlGL : NativeKgl(DirectGL) + +interface MacGL : INativeGL, Library { + companion object : MacGL by NativeLoad(nativeOpenGLLibraryPath) { + } + + //fun CGLSetParameter(vararg args: Any?): Int + fun CGLEnable(ctx: Pointer?, enable: Int): Int + fun CGLDisable(ctx: Pointer?, enable: Int): Int + fun CGLChoosePixelFormat(attributes: Pointer, pix: Pointer, num: Pointer): Int + fun CGLCreateContext(pix: Pointer?, sharedCtx: Pointer?, ctx: Pointer?): Int + fun CGLDestroyPixelFormat(ctx: Pointer?): Int + fun CGLSetCurrentContext(ctx: Pointer?): Int + fun CGLGetCurrentContext(): Pointer? + fun CGLDestroyContext(ctx: Pointer?): Int + fun CGLGetPixelFormat(ctx: Pointer?): Pointer? + + enum class Error(val id: Int) { + kCGLNoError (0), /* no error */ + kCGLBadAttribute (10000), /* invalid pixel format attribute */ + kCGLBadProperty (10001), /* invalid renderer property */ + kCGLBadPixelFormat (10002), /* invalid pixel format */ + kCGLBadRendererInfo (10003), /* invalid renderer info */ + kCGLBadContext (10004), /* invalid context */ + kCGLBadDrawable (10005), /* invalid drawable */ + kCGLBadDisplay (10006), /* invalid graphics device */ + kCGLBadState (10007), /* invalid context state */ + kCGLBadValue (10008), /* invalid numerical value */ + kCGLBadMatch (10009), /* invalid share context */ + kCGLBadEnumeration (10010), /* invalid enumerant */ + kCGLBadOffScreen (10011), /* invalid offscreen drawable */ + kCGLBadFullScreen (10012), /* invalid fullscreen drawable */ + kCGLBadWindow (10013), /* invalid window */ + kCGLBadAddress (10014), /* invalid pointer */ + kCGLBadCodeModule (10015), /* invalid code module */ + kCGLBadAlloc (10016), /* invalid memory allocation */ + kCGLBadConnection (10017), /* invalid CoreGraphics connection */ + kUnknownError (-1); + + companion object { + val VALUES = values().associateBy { it.id } + + operator fun get(id: Int): Error = VALUES[id] ?: kUnknownError + } + } +} diff --git a/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt b/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt index 862593b76c..35d885bcdc 100644 --- a/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt +++ b/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt @@ -7,10 +7,8 @@ import korlibs.graphics.gl.* import korlibs.io.dynamic.* import korlibs.kgl.* import korlibs.math.geom.* +import korlibs.memory.dyn.* import korlibs.memory.dyn.osx.* -import korlibs.memory.dyn.osx.CGFloat -import korlibs.memory.dyn.osx.NSRect -import korlibs.memory.dyn.osx.NativeName import korlibs.render.* import korlibs.render.platform.* import java.awt.* @@ -20,7 +18,7 @@ import javax.swing.* class MacosGLContext( var contentView: Long = 0L, val window: Long = 0L, - val quality: GameWindow.Quality = GameWindow.Quality.AUTOMATIC, + val quality: GameWindowQuality = GameWindowQuality.AUTOMATIC, val sharedContext: Long = 0L, ) : BaseOpenglContext { companion object { @@ -41,7 +39,7 @@ class MacosGLContext( } val attrs: IntArray by lazy { - val antialias = (this.quality == GameWindow.Quality.QUALITY) + val antialias = (this.quality == GameWindowQuality.QUALITY) val antialiasArray = if (antialias) intArrayOf( NSOpenGLPFAMultisample, NSOpenGLPFASampleBuffers, 1, diff --git a/korge/src/jvm/korlibs/render/platform/PlatformTools.kt b/korge/src/jvm/korlibs/render/platform/PlatformTools.kt deleted file mode 100644 index b44f309986..0000000000 --- a/korge/src/jvm/korlibs/render/platform/PlatformTools.kt +++ /dev/null @@ -1,8 +0,0 @@ -package korlibs.render.platform - -import com.sun.jna.* - -@Deprecated("") -typealias NativeName = korlibs.memory.dyn.osx.NativeName -@Deprecated("", ReplaceWith("korlibs.memory.dyn.osx.NativeLoad(name)", "korlibs")) -inline fun NativeLoad(name: String) = korlibs.memory.dyn.osx.NativeLoad(name) diff --git a/korge/src/jvm/korlibs/render/win32/Win32.kt b/korge/src/jvm/korlibs/render/win32/Win32.kt index 9d050dd4c4..cb1b6f971c 100644 --- a/korge/src/jvm/korlibs/render/win32/Win32.kt +++ b/korge/src/jvm/korlibs/render/win32/Win32.kt @@ -1,13 +1,10 @@ package korlibs.render.win32 -import korlibs.event.Key -import com.sun.jna.Native -import com.sun.jna.Pointer -import com.sun.jna.Structure +import com.sun.jna.* import com.sun.jna.platform.win32.* -import com.sun.jna.ptr.PointerByReference -import com.sun.jna.win32.W32APIOptions -import java.nio.Buffer +import com.sun.jna.ptr.* +import com.sun.jna.win32.* +import java.nio.* object Win32 : MyKernel32 by MyKernel32, MyUser32 by MyUser32, @@ -193,400 +190,3 @@ internal const val KBDUNICODE = 0x1000 internal const val KBDINJECTEDVK = 0x2000 internal const val KBDMAPPEDVK = 0x4000 internal const val KBDBREAK = 0x8000 -internal const val VK_LBUTTON = 0x01 /* Left mouse button */ -internal const val VK_RBUTTON = 0x02 /* Right mouse button */ -internal const val VK_CANCEL = 0x03 /* Control-break processing */ -internal const val VK_MBUTTON = 0x04 /* Middle mouse button (three-button mouse) */ -internal const val VK_XBUTTON1 = 0x05 /* Windows 2000/XP: X1 mouse button */ -internal const val VK_XBUTTON2 = 0x06 /* Windows 2000/XP: X2 mouse button */ -internal const val VK_BACK = 0x08 /* BACKSPACE key */ -internal const val VK_TAB = 0x09 /* TAB key */ -internal const val VK_CLEAR = 0x0C /* CLEAR key */ -internal const val VK_RETURN = 0x0D /* ENTER key */ -internal const val VK_SHIFT = 0x10 /* SHIFT key */ -internal const val VK_CONTROL = 0x11 /* CTRL key */ -internal const val VK_MENU = 0x12 /* ALT key */ -internal const val VK_PAUSE = 0x13 /* PAUSE key */ -internal const val VK_CAPITAL = 0x14 /* CAPS LOCK key */ -internal const val VK_KANA = 0x15 /* Input Method Editor (IME) Kana mode */ -internal const val VK_HANGUEL = 0x15 /* IME Hanguel mode (maintained for compatibility; use #define VK_HANGUL) */ -internal const val VK_HANGUL = 0x15 /* IME Hangul mode */ -internal const val VK_JUNJA = 0x17 /* IME Junja mode */ -internal const val VK_FINAL = 0x18 /* IME final mode */ -internal const val VK_HANJA = 0x19 /* IME Hanja mode */ -internal const val VK_KANJI = 0x19 /* IME Kanji mode */ -internal const val VK_HKTG = 0x1A /* Hiragana/Katakana toggle */ -internal const val VK_ESCAPE = 0x1B /* ESC key */ -internal const val VK_CONVERT = 0x1C /* IME convert */ -internal const val VK_NONCONVERT = 0x1D /* IME nonconvert */ -internal const val VK_ACCEPT = 0x1E /* IME accept */ -internal const val VK_MODECHANGE = 0x1F /* IME mode change request */ -internal const val VK_SPACE = 0x20 /* SPACEBAR */ -internal const val VK_PRIOR = 0x21 /* PAGE UP key */ -internal const val VK_NEXT = 0x22 /* PAGE DOWN key */ -internal const val VK_END = 0x23 /* END key */ -internal const val VK_HOME = 0x24 /* HOME key */ -internal const val VK_LEFT = 0x25 /* LEFT ARROW key */ -internal const val VK_UP = 0x26 /* UP ARROW key */ -internal const val VK_RIGHT = 0x27 /* RIGHT ARROW key */ -internal const val VK_DOWN = 0x28 /* DOWN ARROW key */ -internal const val VK_SELECT = 0x29 /* SELECT key */ -internal const val VK_PRINT = 0x2A /* PRINT key */ -internal const val VK_EXECUTE = 0x2B /* EXECUTE key */ -internal const val VK_SNAPSHOT = 0x2C /* PRINT SCREEN key */ -internal const val VK_INSERT = 0x2D /* INS key */ -internal const val VK_DELETE = 0x2E /* DEL key */ -internal const val VK_HELP = 0x2F /* HELP key */ -internal const val VK_KEY_0 = 0x30 /* '0' key */ -internal const val VK_KEY_1 = 0x31 /* '1' key */ -internal const val VK_KEY_2 = 0x32 /* '2' key */ -internal const val VK_KEY_3 = 0x33 /* '3' key */ -internal const val VK_KEY_4 = 0x34 /* '4' key */ -internal const val VK_KEY_5 = 0x35 /* '5' key */ -internal const val VK_KEY_6 = 0x36 /* '6' key */ -internal const val VK_KEY_7 = 0x37 /* '7' key */ -internal const val VK_KEY_8 = 0x38 /* '8' key */ -internal const val VK_KEY_9 = 0x39 /* '9' key */ -internal const val VK_KEY_A = 0x41 /* 'A' key */ -internal const val VK_KEY_B = 0x42 /* 'B' key */ -internal const val VK_KEY_C = 0x43 /* 'C' key */ -internal const val VK_KEY_D = 0x44 /* 'D' key */ -internal const val VK_KEY_E = 0x45 /* 'E' key */ -internal const val VK_KEY_F = 0x46 /* 'F' key */ -internal const val VK_KEY_G = 0x47 /* 'G' key */ -internal const val VK_KEY_H = 0x48 /* 'H' key */ -internal const val VK_KEY_I = 0x49 /* 'I' key */ -internal const val VK_KEY_J = 0x4A /* 'J' key */ -internal const val VK_KEY_K = 0x4B /* 'K' key */ -internal const val VK_KEY_L = 0x4C /* 'L' key */ -internal const val VK_KEY_M = 0x4D /* 'M' key */ -internal const val VK_KEY_N = 0x4E /* 'N' key */ -internal const val VK_KEY_O = 0x4F /* 'O' key */ -internal const val VK_KEY_P = 0x50 /* 'P' key */ -internal const val VK_KEY_Q = 0x51 /* 'Q' key */ -internal const val VK_KEY_R = 0x52 /* 'R' key */ -internal const val VK_KEY_S = 0x53 /* 'S' key */ -internal const val VK_KEY_T = 0x54 /* 'T' key */ -internal const val VK_KEY_U = 0x55 /* 'U' key */ -internal const val VK_KEY_V = 0x56 /* 'V' key */ -internal const val VK_KEY_W = 0x57 /* 'W' key */ -internal const val VK_KEY_X = 0x58 /* 'X' key */ -internal const val VK_KEY_Y = 0x59 /* 'Y' key */ -internal const val VK_KEY_Z = 0x5A /* 'Z' key */ -internal const val VK_LWIN = 0x5B /* Left Windows key (Microsoft Natural keyboard) */ -internal const val VK_RWIN = 0x5C /* Right Windows key (Natural keyboard) */ -internal const val VK_APPS = 0x5D /* Applications key (Natural keyboard) */ -internal const val VK_POWER = 0x5E /* Power key */ -internal const val VK_SLEEP = 0x5F /* Computer Sleep key */ -internal const val VK_NUMPAD0 = 0x60 /* Numeric keypad '0' key */ -internal const val VK_NUMPAD1 = 0x61 /* Numeric keypad '1' key */ -internal const val VK_NUMPAD2 = 0x62 /* Numeric keypad '2' key */ -internal const val VK_NUMPAD3 = 0x63 /* Numeric keypad '3' key */ -internal const val VK_NUMPAD4 = 0x64 /* Numeric keypad '4' key */ -internal const val VK_NUMPAD5 = 0x65 /* Numeric keypad '5' key */ -internal const val VK_NUMPAD6 = 0x66 /* Numeric keypad '6' key */ -internal const val VK_NUMPAD7 = 0x67 /* Numeric keypad '7' key */ -internal const val VK_NUMPAD8 = 0x68 /* Numeric keypad '8' key */ -internal const val VK_NUMPAD9 = 0x69 /* Numeric keypad '9' key */ -internal const val VK_MULTIPLY = 0x6A /* Multiply key */ -internal const val VK_ADD = 0x6B /* Add key */ -internal const val VK_SEPARATOR = 0x6C /* Separator key */ -internal const val VK_SUBTRACT = 0x6D /* Subtract key */ -internal const val VK_DECIMAL = 0x6E /* Decimal key */ -internal const val VK_DIVIDE = 0x6F /* Divide key */ -internal const val VK_F1 = 0x70 /* F1 key */ -internal const val VK_F2 = 0x71 /* F2 key */ -internal const val VK_F3 = 0x72 /* F3 key */ -internal const val VK_F4 = 0x73 /* F4 key */ -internal const val VK_F5 = 0x74 /* F5 key */ -internal const val VK_F6 = 0x75 /* F6 key */ -internal const val VK_F7 = 0x76 /* F7 key */ -internal const val VK_F8 = 0x77 /* F8 key */ -internal const val VK_F9 = 0x78 /* F9 key */ -internal const val VK_F10 = 0x79 /* F10 key */ -internal const val VK_F11 = 0x7A /* F11 key */ -internal const val VK_F12 = 0x7B /* F12 key */ -internal const val VK_F13 = 0x7C /* F13 key */ -internal const val VK_F14 = 0x7D /* F14 key */ -internal const val VK_F15 = 0x7E /* F15 key */ -internal const val VK_F16 = 0x7F /* F16 key */ -internal const val VK_F17 = 0x80 /* F17 key */ -internal const val VK_F18 = 0x81 /* F18 key */ -internal const val VK_F19 = 0x82 /* F19 key */ -internal const val VK_F20 = 0x83 /* F20 key */ -internal const val VK_F21 = 0x84 /* F21 key */ -internal const val VK_F22 = 0x85 /* F22 key */ -internal const val VK_F23 = 0x86 /* F23 key */ -internal const val VK_F24 = 0x87 /* F24 key */ -internal const val VK_NUMLOCK = 0x90 /* NUM LOCK key */ -internal const val VK_SCROLL = 0x91 /* SCROLL LOCK key */ -internal const val VK_LSHIFT = 0xA0 /* Left SHIFT key */ -internal const val VK_RSHIFT = 0xA1 /* Right SHIFT key */ -internal const val VK_LCONTROL = 0xA2 /* Left CONTROL key */ -internal const val VK_RCONTROL = 0xA3 /* Right CONTROL key */ -internal const val VK_LMENU = 0xA4 /* Left MENU key */ -internal const val VK_RMENU = 0xA5 /* Right MENU key */ -internal const val VK_BROWSER_BACK = 0xA6 /* Windows 2000/XP: Browser Back key */ -internal const val VK_BROWSER_FORWARD = 0xA7 /* Windows 2000/XP: Browser Forward key */ -internal const val VK_BROWSER_REFRESH = 0xA8 /* Windows 2000/XP: Browser Refresh key */ -internal const val VK_BROWSER_STOP = 0xA9 /* Windows 2000/XP: Browser Stop key */ -internal const val VK_BROWSER_SEARCH = 0xAA /* Windows 2000/XP: Browser Search key */ -internal const val VK_BROWSER_FAVORITES = 0xAB /* Windows 2000/XP: Browser Favorites key */ -internal const val VK_BROWSER_HOME = 0xAC /* Windows 2000/XP: Browser Start and Home key */ -internal const val VK_VOLUME_MUTE = 0xAD /* Windows 2000/XP: Volume Mute key */ -internal const val VK_VOLUME_DOWN = 0xAE /* Windows 2000/XP: Volume Down key */ -internal const val VK_VOLUME_UP = 0xAF /* Windows 2000/XP: Volume Up key */ -internal const val VK_MEDIA_NEXT_TRACK = 0xB0 /* Windows 2000/XP: Next Track key */ -internal const val VK_MEDIA_PREV_TRACK = 0xB1 /* Windows 2000/XP: Previous Track key */ -internal const val VK_MEDIA_STOP = 0xB2 /* Windows 2000/XP: Stop Media key */ -internal const val VK_MEDIA_PLAY_PAUSE = 0xB3 /* Windows 2000/XP: Play/Pause Media key */ -internal const val VK_LAUNCH_MAIL = 0xB4 /* Windows 2000/XP: Start Mail key */ -internal const val VK_MEDIA_SELECT = 0xB5 /* Windows 2000/XP: Select Media key */ -internal const val VK_LAUNCH_MEDIA_SELECT = 0xB5 /* Windows 2000/XP: Select Media key */ -internal const val VK_LAUNCH_APP1 = 0xB6 /* Windows 2000/XP: Start Application 1 key */ -internal const val VK_LAUNCH_APP2 = 0xB7 /* Windows 2000/XP: Start Application 2 key */ -internal const val VK_OEM_1 = 0xBA /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_PLUS = 0xBB /* Windows 2000/XP: For any country/region, the '+' key */ -internal const val VK_OEM_COMMA = 0xBC /* Windows 2000/XP: For any country/region, the ',' key */ -internal const val VK_OEM_MINUS = 0xBD /* Windows 2000/XP: For any country/region, the '-' key */ -internal const val VK_OEM_PERIOD = 0xBE /* Windows 2000/XP: For any country/region, the '.' key */ -internal const val VK_OEM_2 = 0xBF /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_3 = 0xC0 /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_ABNT_C1 = 0xC1 /* Brazilian (ABNT) Keyboard */ -internal const val VK_ABNT_C2 = 0xC2 /* Brazilian (ABNT) Keyboard */ -internal const val VK_OEM_4 = 0xDB /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_5 = 0xDC /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_6 = 0xDD /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_7 = 0xDE /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_8 = 0xDF /* Used for miscellaneous characters; it can vary by keyboard. */ -internal const val VK_OEM_AX = 0xE1 /* AX key on Japanese AX keyboard */ -internal const val VK_OEM_102 = 0xE2 /* Windows 2000/XP: Either the angle bracket key or */ -internal const val VK_PROCESSKEY = 0xE5 /* Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key */ -internal const val VK_PACKET = 0xE7 /* Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. */ -internal const val VK_OEM_RESET = 0xE9 -internal const val VK_OEM_JUMP = 0xEA -internal const val VK_OEM_PA1 = 0xEB -internal const val VK_OEM_PA2 = 0xEC -internal const val VK_OEM_PA3 = 0xED -internal const val VK_OEM_WSCTRL = 0xEE -internal const val VK_OEM_CUSEL = 0xEF -internal const val VK_OEM_ATTN = 0xF0 -internal const val VK_OEM_FINISH = 0xF1 -internal const val VK_OEM_COPY = 0xF2 -internal const val VK_OEM_AUTO = 0xF3 -internal const val VK_OEM_ENLW = 0xF4 -internal const val VK_OEM_BACKTAB = 0xF5 -internal const val VK_ATTN = 0xF6 /* Attn key */ -internal const val VK_CRSEL = 0xF7 /* CrSel key */ -internal const val VK_EXSEL = 0xF8 /* ExSel key */ -internal const val VK_EREOF = 0xF9 /* Erase EOF key */ -internal const val VK_PLAY = 0xFA /* Play key */ -internal const val VK_ZOOM = 0xFB /* Zoom key */ -internal const val VK_NONAME = 0xFC /* Reserved */ -internal const val VK_PA1 = 0xFD /* PA1 key */ -internal const val VK_OEM_CLEAR = 0xFE /* Clear key */ -internal const val VK_NONE = 0xFF /* no key */ - -val VK_TABLE: Map = mapOf( - KBDEXT to Key.UNKNOWN, - KBDMULTIVK to Key.UNKNOWN, - KBDSPECIAL to Key.UNKNOWN, - KBDNUMPAD to Key.UNKNOWN, - KBDUNICODE to Key.UNKNOWN, - KBDINJECTEDVK to Key.UNKNOWN, - KBDMAPPEDVK to Key.UNKNOWN, - KBDBREAK to Key.UNKNOWN, - VK_LBUTTON to Key.UNKNOWN, - VK_RBUTTON to Key.UNKNOWN, - VK_CANCEL to Key.UNKNOWN, - VK_MBUTTON to Key.UNKNOWN, - VK_XBUTTON1 to Key.UNKNOWN, - VK_XBUTTON2 to Key.UNKNOWN, - VK_BACK to Key.BACKSPACE, - VK_TAB to Key.TAB, - VK_CLEAR to Key.CLEAR, - VK_RETURN to Key.RETURN, - VK_SHIFT to Key.LEFT_SHIFT, - VK_CONTROL to Key.LEFT_CONTROL, - VK_MENU to Key.MENU, - VK_PAUSE to Key.PAUSE, - VK_CAPITAL to Key.UNKNOWN, - VK_KANA to Key.UNKNOWN, - VK_HANGUEL to Key.UNKNOWN, - VK_HANGUL to Key.UNKNOWN, - VK_JUNJA to Key.UNKNOWN, - VK_FINAL to Key.UNKNOWN, - VK_HANJA to Key.UNKNOWN, - VK_KANJI to Key.UNKNOWN, - VK_HKTG to Key.UNKNOWN, - VK_ESCAPE to Key.ESCAPE, - VK_CONVERT to Key.UNKNOWN, - VK_NONCONVERT to Key.UNKNOWN, - VK_ACCEPT to Key.UNKNOWN, - VK_MODECHANGE to Key.UNKNOWN, - VK_SPACE to Key.SPACE, - VK_PRIOR to Key.UNKNOWN, - VK_NEXT to Key.UNKNOWN, - VK_END to Key.END, - VK_HOME to Key.HOME, - VK_LEFT to Key.LEFT, - VK_UP to Key.UP, - VK_RIGHT to Key.RIGHT, - VK_DOWN to Key.DOWN, - VK_SELECT to Key.SELECT_KEY, - VK_PRINT to Key.PRINT_SCREEN, - VK_EXECUTE to Key.UNKNOWN, - VK_SNAPSHOT to Key.UNKNOWN, - VK_INSERT to Key.INSERT, - VK_DELETE to Key.DELETE, - VK_HELP to Key.HELP, - VK_KEY_0 to Key.N0, - VK_KEY_1 to Key.N1, - VK_KEY_2 to Key.N2, - VK_KEY_3 to Key.N3, - VK_KEY_4 to Key.N4, - VK_KEY_5 to Key.N5, - VK_KEY_6 to Key.N6, - VK_KEY_7 to Key.N7, - VK_KEY_8 to Key.N8, - VK_KEY_9 to Key.N9, - VK_KEY_A to Key.A, - VK_KEY_B to Key.B, - VK_KEY_C to Key.C, - VK_KEY_D to Key.D, - VK_KEY_E to Key.E, - VK_KEY_F to Key.F, - VK_KEY_G to Key.G, - VK_KEY_H to Key.H, - VK_KEY_I to Key.I, - VK_KEY_J to Key.J, - VK_KEY_K to Key.K, - VK_KEY_L to Key.L, - VK_KEY_M to Key.M, - VK_KEY_N to Key.N, - VK_KEY_O to Key.O, - VK_KEY_P to Key.P, - VK_KEY_Q to Key.Q, - VK_KEY_R to Key.R, - VK_KEY_S to Key.S, - VK_KEY_T to Key.T, - VK_KEY_U to Key.U, - VK_KEY_V to Key.V, - VK_KEY_W to Key.W, - VK_KEY_X to Key.X, - VK_KEY_Y to Key.Y, - VK_KEY_Z to Key.Z, - VK_LWIN to Key.META, - VK_RWIN to Key.META, - VK_APPS to Key.UNKNOWN, - VK_POWER to Key.UNKNOWN, - VK_SLEEP to Key.UNKNOWN, - VK_NUMPAD0 to Key.NUMPAD0, - VK_NUMPAD1 to Key.NUMPAD1, - VK_NUMPAD2 to Key.NUMPAD2, - VK_NUMPAD3 to Key.NUMPAD3, - VK_NUMPAD4 to Key.NUMPAD4, - VK_NUMPAD5 to Key.NUMPAD5, - VK_NUMPAD6 to Key.NUMPAD6, - VK_NUMPAD7 to Key.NUMPAD7, - VK_NUMPAD8 to Key.NUMPAD8, - VK_NUMPAD9 to Key.NUMPAD9, - VK_MULTIPLY to Key.KP_MULTIPLY, - VK_ADD to Key.KP_ADD, - VK_SEPARATOR to Key.KP_SEPARATOR, - VK_SUBTRACT to Key.KP_SUBTRACT, - VK_DECIMAL to Key.KP_DECIMAL, - VK_DIVIDE to Key.KP_DIVIDE, - VK_F1 to Key.F1, - VK_F2 to Key.F2, - VK_F3 to Key.F3, - VK_F4 to Key.F4, - VK_F5 to Key.F5, - VK_F6 to Key.F6, - VK_F7 to Key.F7, - VK_F8 to Key.F8, - VK_F9 to Key.F9, - VK_F10 to Key.F10, - VK_F11 to Key.F11, - VK_F12 to Key.F12, - VK_F13 to Key.F13, - VK_F14 to Key.F14, - VK_F15 to Key.F15, - VK_F16 to Key.F16, - VK_F17 to Key.F17, - VK_F18 to Key.F18, - VK_F19 to Key.F19, - VK_F20 to Key.F20, - VK_F21 to Key.F21, - VK_F22 to Key.F22, - VK_F23 to Key.F23, - VK_F24 to Key.F24, - VK_NUMLOCK to Key.NUM_LOCK, - VK_SCROLL to Key.SCROLL_LOCK, - VK_LSHIFT to Key.LEFT_SHIFT, - VK_RSHIFT to Key.RIGHT_SHIFT, - VK_LCONTROL to Key.LEFT_CONTROL, - VK_RCONTROL to Key.RIGHT_CONTROL, - VK_LMENU to Key.LEFT_SUPER, - VK_RMENU to Key.RIGHT_SUPER, - VK_BROWSER_BACK to Key.UNKNOWN, - VK_BROWSER_FORWARD to Key.UNKNOWN, - VK_BROWSER_REFRESH to Key.UNKNOWN, - VK_BROWSER_STOP to Key.UNKNOWN, - VK_BROWSER_SEARCH to Key.UNKNOWN, - VK_BROWSER_FAVORITES to Key.UNKNOWN, - VK_BROWSER_HOME to Key.UNKNOWN, - VK_VOLUME_MUTE to Key.UNKNOWN, - VK_VOLUME_DOWN to Key.UNKNOWN, - VK_VOLUME_UP to Key.UNKNOWN, - VK_MEDIA_NEXT_TRACK to Key.UNKNOWN, - VK_MEDIA_PREV_TRACK to Key.UNKNOWN, - VK_MEDIA_STOP to Key.UNKNOWN, - VK_MEDIA_PLAY_PAUSE to Key.UNKNOWN, - VK_LAUNCH_MAIL to Key.UNKNOWN, - VK_MEDIA_SELECT to Key.UNKNOWN, - VK_LAUNCH_MEDIA_SELECT to Key.UNKNOWN, - VK_LAUNCH_APP1 to Key.UNKNOWN, - VK_LAUNCH_APP2 to Key.UNKNOWN, - VK_OEM_1 to Key.UNKNOWN, - VK_OEM_PLUS to Key.PLUS, - VK_OEM_COMMA to Key.UNKNOWN, - VK_OEM_MINUS to Key.MINUS, - VK_OEM_PERIOD to Key.UNKNOWN, - VK_OEM_2 to Key.UNKNOWN, - VK_OEM_3 to Key.UNKNOWN, - VK_ABNT_C1 to Key.UNKNOWN, - VK_ABNT_C2 to Key.UNKNOWN, - VK_OEM_4 to Key.UNKNOWN, - VK_OEM_5 to Key.UNKNOWN, - VK_OEM_6 to Key.UNKNOWN, - VK_OEM_7 to Key.UNKNOWN, - VK_OEM_8 to Key.UNKNOWN, - VK_OEM_AX to Key.UNKNOWN, - VK_OEM_102 to Key.UNKNOWN, - VK_PROCESSKEY to Key.UNKNOWN, - VK_PACKET to Key.UNKNOWN, - VK_OEM_RESET to Key.UNKNOWN, - VK_OEM_JUMP to Key.UNKNOWN, - VK_OEM_PA1 to Key.UNKNOWN, - VK_OEM_PA2 to Key.UNKNOWN, - VK_OEM_PA3 to Key.UNKNOWN, - VK_OEM_WSCTRL to Key.UNKNOWN, - VK_OEM_CUSEL to Key.UNKNOWN, - VK_OEM_ATTN to Key.UNKNOWN, - VK_OEM_FINISH to Key.UNKNOWN, - VK_OEM_COPY to Key.UNKNOWN, - VK_OEM_AUTO to Key.UNKNOWN, - VK_OEM_ENLW to Key.UNKNOWN, - VK_OEM_BACKTAB to Key.UNKNOWN, - VK_ATTN to Key.UNKNOWN, - VK_CRSEL to Key.UNKNOWN, - VK_EXSEL to Key.UNKNOWN, - VK_EREOF to Key.UNKNOWN, - VK_PLAY to Key.UNKNOWN, - VK_ZOOM to Key.UNKNOWN, - VK_NONAME to Key.UNKNOWN, - VK_PA1 to Key.UNKNOWN, - VK_OEM_CLEAR to Key.UNKNOWN, - VK_NONE to Key.UNKNOWN -) diff --git a/korge/src/jvm/korlibs/render/win32/Win32Tools.kt b/korge/src/jvm/korlibs/render/win32/Win32Tools.kt index 5a1c1cfe80..f122c73145 100644 --- a/korge/src/jvm/korlibs/render/win32/Win32Tools.kt +++ b/korge/src/jvm/korlibs/render/win32/Win32Tools.kt @@ -1,17 +1,17 @@ package korlibs.render.win32 -import korlibs.kgl.* -import korlibs.logger.* -import korlibs.render.* -import korlibs.render.platform.* -import korlibs.image.bitmap.* -import korlibs.io.lang.* import com.sun.jna.* import com.sun.jna.Function import com.sun.jna.platform.win32.* import com.sun.jna.platform.win32.WinDef.* import com.sun.jna.ptr.* import com.sun.jna.win32.* +import korlibs.image.bitmap.* +import korlibs.io.lang.* +import korlibs.kgl.* +import korlibs.logger.* +import korlibs.render.* +import korlibs.render.platform.* import java.awt.* import java.io.* import java.lang.reflect.* @@ -359,9 +359,9 @@ class Win32OpenglContext(val gwconfig: GameWindowConfig, val hWnd: WinDef.HWND, //println("makeCurrent") makeCurrent(hDC, hRC) when (gwconfig.quality) { - GameWindow.Quality.QUALITY -> DirectGL.glEnable(GL_MULTISAMPLE) - GameWindow.Quality.PERFORMANCE, - GameWindow.Quality.AUTOMATIC -> DirectGL.glDisable(GL_MULTISAMPLE) + GameWindowQuality.QUALITY -> DirectGL.glEnable(GL_MULTISAMPLE) + GameWindowQuality.PERFORMANCE, + GameWindowQuality.AUTOMATIC -> DirectGL.glDisable(GL_MULTISAMPLE) } } diff --git a/korge/src/jvm/korlibs/render/x11/X11Constants.kt b/korge/src/jvm/korlibs/render/x11/X11Constants.kt index 3858925a7c..efa1b9e965 100644 --- a/korge/src/jvm/korlibs/render/x11/X11Constants.kt +++ b/korge/src/jvm/korlibs/render/x11/X11Constants.kt @@ -3,14 +3,11 @@ package korlibs.render.x11 import com.sun.jna.* import com.sun.jna.platform.unix.* import com.sun.jna.ptr.* -import korlibs.datastructure.* -import korlibs.event.* import korlibs.ffi.* import korlibs.graphics.shader.gl.* import korlibs.io.annotations.* import korlibs.render.platform.* - typealias XVisualInfo = Pointer typealias GLXContext = Pointer @@ -29,508 +26,6 @@ internal const val GLX_DEPTH_SIZE = 12 internal const val GLX_STENCIL_SIZE = 13 internal const val GLX_DOUBLEBUFFER = 5 -internal const val XK_space = 0x0020 /* U+0020 SPACE */ -internal const val XK_exclam = 0x0021 /* U+0021 EXCLAMATION MARK */ -internal const val XK_quotedbl = 0x0022 /* U+0022 QUOTATION MARK */ -internal const val XK_numbersign = 0x0023 /* U+0023 NUMBER SIGN */ -internal const val XK_dollar = 0x0024 /* U+0024 DOLLAR SIGN */ -internal const val XK_percent = 0x0025 /* U+0025 PERCENT SIGN */ -internal const val XK_ampersand = 0x0026 /* U+0026 AMPERSAND */ -internal const val XK_apostrophe = 0x0027 /* U+0027 APOSTROPHE */ -internal const val XK_quoteright = 0x0027 /* deprecated */ -internal const val XK_parenleft = 0x0028 /* U+0028 LEFT PARENTHESIS */ -internal const val XK_parenright = 0x0029 /* U+0029 RIGHT PARENTHESIS */ -internal const val XK_asterisk = 0x002a /* U+002A ASTERISK */ -internal const val XK_plus = 0x002b /* U+002B PLUS SIGN */ -internal const val XK_comma = 0x002c /* U+002C COMMA */ -internal const val XK_minus = 0x002d /* U+002D HYPHEN-MINUS */ -internal const val XK_period = 0x002e /* U+002E FULL STOP */ -internal const val XK_slash = 0x002f /* U+002F SOLIDUS */ -internal const val XK_0 = 0x0030 /* U+0030 DIGIT ZERO */ -internal const val XK_1 = 0x0031 /* U+0031 DIGIT ONE */ -internal const val XK_2 = 0x0032 /* U+0032 DIGIT TWO */ -internal const val XK_3 = 0x0033 /* U+0033 DIGIT THREE */ -internal const val XK_4 = 0x0034 /* U+0034 DIGIT FOUR */ -internal const val XK_5 = 0x0035 /* U+0035 DIGIT FIVE */ -internal const val XK_6 = 0x0036 /* U+0036 DIGIT SIX */ -internal const val XK_7 = 0x0037 /* U+0037 DIGIT SEVEN */ -internal const val XK_8 = 0x0038 /* U+0038 DIGIT EIGHT */ -internal const val XK_9 = 0x0039 /* U+0039 DIGIT NINE */ -internal const val XK_colon = 0x003a /* U+003A COLON */ -internal const val XK_semicolon = 0x003b /* U+003B SEMICOLON */ -internal const val XK_less = 0x003c /* U+003C LESS-THAN SIGN */ -internal const val XK_equal = 0x003d /* U+003D EQUALS SIGN */ -internal const val XK_greater = 0x003e /* U+003E GREATER-THAN SIGN */ -internal const val XK_question = 0x003f /* U+003F QUESTION MARK */ -internal const val XK_at = 0x0040 /* U+0040 COMMERCIAL AT */ -internal const val XK_A = 0x0041 /* U+0041 LATIN CAPITAL LETTER A */ -internal const val XK_B = 0x0042 /* U+0042 LATIN CAPITAL LETTER B */ -internal const val XK_C = 0x0043 /* U+0043 LATIN CAPITAL LETTER C */ -internal const val XK_D = 0x0044 /* U+0044 LATIN CAPITAL LETTER D */ -internal const val XK_E = 0x0045 /* U+0045 LATIN CAPITAL LETTER E */ -internal const val XK_F = 0x0046 /* U+0046 LATIN CAPITAL LETTER F */ -internal const val XK_G = 0x0047 /* U+0047 LATIN CAPITAL LETTER G */ -internal const val XK_H = 0x0048 /* U+0048 LATIN CAPITAL LETTER H */ -internal const val XK_I = 0x0049 /* U+0049 LATIN CAPITAL LETTER I */ -internal const val XK_J = 0x004a /* U+004A LATIN CAPITAL LETTER J */ -internal const val XK_K = 0x004b /* U+004B LATIN CAPITAL LETTER K */ -internal const val XK_L = 0x004c /* U+004C LATIN CAPITAL LETTER L */ -internal const val XK_M = 0x004d /* U+004D LATIN CAPITAL LETTER M */ -internal const val XK_N = 0x004e /* U+004E LATIN CAPITAL LETTER N */ -internal const val XK_O = 0x004f /* U+004F LATIN CAPITAL LETTER O */ -internal const val XK_P = 0x0050 /* U+0050 LATIN CAPITAL LETTER P */ -internal const val XK_Q = 0x0051 /* U+0051 LATIN CAPITAL LETTER Q */ -internal const val XK_R = 0x0052 /* U+0052 LATIN CAPITAL LETTER R */ -internal const val XK_S = 0x0053 /* U+0053 LATIN CAPITAL LETTER S */ -internal const val XK_T = 0x0054 /* U+0054 LATIN CAPITAL LETTER T */ -internal const val XK_U = 0x0055 /* U+0055 LATIN CAPITAL LETTER U */ -internal const val XK_V = 0x0056 /* U+0056 LATIN CAPITAL LETTER V */ -internal const val XK_W = 0x0057 /* U+0057 LATIN CAPITAL LETTER W */ -internal const val XK_X = 0x0058 /* U+0058 LATIN CAPITAL LETTER X */ -internal const val XK_Y = 0x0059 /* U+0059 LATIN CAPITAL LETTER Y */ -internal const val XK_Z = 0x005a /* U+005A LATIN CAPITAL LETTER Z */ -internal const val XK_bracketleft = 0x005b /* U+005B LEFT SQUARE BRACKET */ -internal const val XK_backslash = 0x005c /* U+005C REVERSE SOLIDUS */ -internal const val XK_bracketright = 0x005d /* U+005D RIGHT SQUARE BRACKET */ -internal const val XK_asciicircum = 0x005e /* U+005E CIRCUMFLEX ACCENT */ -internal const val XK_underscore = 0x005f /* U+005F LOW LINE */ -internal const val XK_grave = 0x0060 /* U+0060 GRAVE ACCENT */ -internal const val XK_quoteleft = 0x0060 /* deprecated */ -internal const val XK_a = 0x0061 /* U+0061 LATIN SMALL LETTER A */ -internal const val XK_b = 0x0062 /* U+0062 LATIN SMALL LETTER B */ -internal const val XK_c = 0x0063 /* U+0063 LATIN SMALL LETTER C */ -internal const val XK_d = 0x0064 /* U+0064 LATIN SMALL LETTER D */ -internal const val XK_e = 0x0065 /* U+0065 LATIN SMALL LETTER E */ -internal const val XK_f = 0x0066 /* U+0066 LATIN SMALL LETTER F */ -internal const val XK_g = 0x0067 /* U+0067 LATIN SMALL LETTER G */ -internal const val XK_h = 0x0068 /* U+0068 LATIN SMALL LETTER H */ -internal const val XK_i = 0x0069 /* U+0069 LATIN SMALL LETTER I */ -internal const val XK_j = 0x006a /* U+006A LATIN SMALL LETTER J */ -internal const val XK_k = 0x006b /* U+006B LATIN SMALL LETTER K */ -internal const val XK_l = 0x006c /* U+006C LATIN SMALL LETTER L */ -internal const val XK_m = 0x006d /* U+006D LATIN SMALL LETTER M */ -internal const val XK_n = 0x006e /* U+006E LATIN SMALL LETTER N */ -internal const val XK_o = 0x006f /* U+006F LATIN SMALL LETTER O */ -internal const val XK_p = 0x0070 /* U+0070 LATIN SMALL LETTER P */ -internal const val XK_q = 0x0071 /* U+0071 LATIN SMALL LETTER Q */ -internal const val XK_r = 0x0072 /* U+0072 LATIN SMALL LETTER R */ -internal const val XK_s = 0x0073 /* U+0073 LATIN SMALL LETTER S */ -internal const val XK_t = 0x0074 /* U+0074 LATIN SMALL LETTER T */ -internal const val XK_u = 0x0075 /* U+0075 LATIN SMALL LETTER U */ -internal const val XK_v = 0x0076 /* U+0076 LATIN SMALL LETTER V */ -internal const val XK_w = 0x0077 /* U+0077 LATIN SMALL LETTER W */ -internal const val XK_x = 0x0078 /* U+0078 LATIN SMALL LETTER X */ -internal const val XK_y = 0x0079 /* U+0079 LATIN SMALL LETTER Y */ -internal const val XK_z = 0x007a /* U+007A LATIN SMALL LETTER Z */ -internal const val XK_braceleft = 0x007b /* U+007B LEFT CURLY BRACKET */ -internal const val XK_bar = 0x007c /* U+007C VERTICAL LINE */ -internal const val XK_braceright = 0x007d /* U+007D RIGHT CURLY BRACKET */ -internal const val XK_asciitilde = 0x007e /* U+007E TILDE */ - -internal const val XK_leftarrow = 0x08fb /* U+2190 LEFTWARDS ARROW */ -internal const val XK_uparrow = 0x08fc /* U+2191 UPWARDS ARROW */ -internal const val XK_rightarrow = 0x08fd /* U+2192 RIGHTWARDS ARROW */ -internal const val XK_downarrow = 0x08fe /* U+2193 DOWNWARDS ARROW */ -internal const val XK_BackSpace = 0xff08 /* Back space, back char */ -internal const val XK_Tab = 0xff09 -internal const val XK_Linefeed = 0xff0a /* Linefeed, LF */ -internal const val XK_Clear = 0xff0b -internal const val XK_Return = 0xff0d /* Return, enter */ -internal const val XK_Pause = 0xff13 /* Pause, hold */ -internal const val XK_Scroll_Lock = 0xff14 -internal const val XK_Sys_Req = 0xff15 -internal const val XK_Escape = 0xff1b -internal const val XK_Delete = 0xffff /* Delete, rubout */ - -internal const val XK_Home = 0xff50 -internal const val XK_Left = 0xff51 /* Move left, left arrow */ -internal const val XK_Up = 0xff52 /* Move up, up arrow */ -internal const val XK_Right = 0xff53 /* Move right, right arrow */ -internal const val XK_Down = 0xff54 /* Move down, down arrow */ -internal const val XK_Prior = 0xff55 /* Prior, previous */ -internal const val XK_Page_Up = 0xff55 -internal const val XK_Next = 0xff56 /* Next */ -internal const val XK_Page_Down = 0xff56 -internal const val XK_End = 0xff57 /* EOL */ -internal const val XK_Begin = 0xff58 /* BOL */ -internal const val XK_Select = 0xff60 /* Select, mark */ -internal const val XK_Print = 0xff61 -internal const val XK_Execute = 0xff62 /* Execute, run, do */ -internal const val XK_Insert = 0xff63 /* Insert, insert here */ -internal const val XK_Undo = 0xff65 -internal const val XK_Redo = 0xff66 /* Redo, again */ -internal const val XK_Menu = 0xff67 -internal const val XK_Find = 0xff68 /* Find, search */ -internal const val XK_Cancel = 0xff69 /* Cancel, stop, abort, exit */ -internal const val XK_Help = 0xff6a /* Help */ -internal const val XK_Break = 0xff6b -internal const val XK_Mode_switch = 0xff7e /* Character set switch */ -internal const val XK_script_switch = 0xff7e /* Alias for mode_switch */ -internal const val XK_Num_Lock = 0xff7f -internal const val XK_KP_Space = 0xff80 /* Space */ -internal const val XK_KP_Tab = 0xff89 -internal const val XK_KP_Enter = 0xff8d /* Enter */ -internal const val XK_KP_F1 = 0xff91 /* PF1, KP_A, ... */ -internal const val XK_KP_F2 = 0xff92 -internal const val XK_KP_F3 = 0xff93 -internal const val XK_KP_F4 = 0xff94 -internal const val XK_KP_Home = 0xff95 -internal const val XK_KP_Left = 0xff96 -internal const val XK_KP_Up = 0xff97 -internal const val XK_KP_Right = 0xff98 -internal const val XK_KP_Down = 0xff99 -internal const val XK_KP_Prior = 0xff9a -internal const val XK_KP_Page_Up = 0xff9a -internal const val XK_KP_Next = 0xff9b -internal const val XK_KP_Page_Down = 0xff9b -internal const val XK_KP_End = 0xff9c -internal const val XK_KP_Begin = 0xff9d -internal const val XK_KP_Insert = 0xff9e -internal const val XK_KP_Delete = 0xff9f -internal const val XK_KP_Equal = 0xffbd /* Equals */ -internal const val XK_KP_Multiply = 0xffaa -internal const val XK_KP_Add = 0xffab -internal const val XK_KP_Separator = 0xffac /* Separator, often comma */ -internal const val XK_KP_Subtract = 0xffad -internal const val XK_KP_Decimal = 0xffae -internal const val XK_KP_Divide = 0xffaf -internal const val XK_KP_0 = 0xffb0 -internal const val XK_KP_1 = 0xffb1 -internal const val XK_KP_2 = 0xffb2 -internal const val XK_KP_3 = 0xffb3 -internal const val XK_KP_4 = 0xffb4 -internal const val XK_KP_5 = 0xffb5 -internal const val XK_KP_6 = 0xffb6 -internal const val XK_KP_7 = 0xffb7 -internal const val XK_KP_8 = 0xffb8 -internal const val XK_KP_9 = 0xffb9 -internal const val XK_F1 = 0xffbe -internal const val XK_F2 = 0xffbf -internal const val XK_F3 = 0xffc0 -internal const val XK_F4 = 0xffc1 -internal const val XK_F5 = 0xffc2 -internal const val XK_F6 = 0xffc3 -internal const val XK_F7 = 0xffc4 -internal const val XK_F8 = 0xffc5 -internal const val XK_F9 = 0xffc6 -internal const val XK_F10 = 0xffc7 -internal const val XK_F11 = 0xffc8 -internal const val XK_L1 = 0xffc8 -internal const val XK_F12 = 0xffc9 -internal const val XK_L2 = 0xffc9 -internal const val XK_F13 = 0xffca -internal const val XK_L3 = 0xffca -internal const val XK_F14 = 0xffcb -internal const val XK_L4 = 0xffcb -internal const val XK_F15 = 0xffcc -internal const val XK_L5 = 0xffcc -internal const val XK_F16 = 0xffcd -internal const val XK_L6 = 0xffcd -internal const val XK_F17 = 0xffce -internal const val XK_L7 = 0xffce -internal const val XK_F18 = 0xffcf -internal const val XK_L8 = 0xffcf -internal const val XK_F19 = 0xffd0 -internal const val XK_L9 = 0xffd0 -internal const val XK_F20 = 0xffd1 -internal const val XK_L10 = 0xffd1 -internal const val XK_F21 = 0xffd2 -internal const val XK_R1 = 0xffd2 -internal const val XK_F22 = 0xffd3 -internal const val XK_R2 = 0xffd3 -internal const val XK_F23 = 0xffd4 -internal const val XK_R3 = 0xffd4 -internal const val XK_F24 = 0xffd5 -internal const val XK_R4 = 0xffd5 -internal const val XK_F25 = 0xffd6 -internal const val XK_R5 = 0xffd6 -internal const val XK_F26 = 0xffd7 -internal const val XK_R6 = 0xffd7 -internal const val XK_F27 = 0xffd8 -internal const val XK_R7 = 0xffd8 -internal const val XK_F28 = 0xffd9 -internal const val XK_R8 = 0xffd9 -internal const val XK_F29 = 0xffda -internal const val XK_R9 = 0xffda -internal const val XK_F30 = 0xffdb -internal const val XK_R10 = 0xffdb -internal const val XK_F31 = 0xffdc -internal const val XK_R11 = 0xffdc -internal const val XK_F32 = 0xffdd -internal const val XK_R12 = 0xffdd -internal const val XK_F33 = 0xffde -internal const val XK_R13 = 0xffde -internal const val XK_F34 = 0xffdf -internal const val XK_R14 = 0xffdf -internal const val XK_F35 = 0xffe0 -internal const val XK_R15 = 0xffe0 -internal const val XK_Shift_L = 0xffe1 /* Left shift */ -internal const val XK_Shift_R = 0xffe2 /* Right shift */ -internal const val XK_Control_L = 0xffe3 /* Left control */ -internal const val XK_Control_R = 0xffe4 /* Right control */ -internal const val XK_Caps_Lock = 0xffe5 /* Caps lock */ -internal const val XK_Shift_Lock = 0xffe6 /* Shift lock */ -internal const val XK_Meta_L = 0xffe7 /* Left meta */ -internal const val XK_Meta_R = 0xffe8 /* Right meta */ -internal const val XK_Alt_L = 0xffe9 /* Left alt */ -internal const val XK_Alt_R = 0xffea /* Right alt */ -internal const val XK_Super_L = 0xffeb /* Left super */ -internal const val XK_Super_R = 0xffec /* Right super */ -internal const val XK_Hyper_L = 0xffed /* Left hyper */ -internal const val XK_Hyper_R = 0xffee /* Right hyper */ - - -internal val XK_KeyMap: IntMap by lazy { - IntMap().apply { - this[XK_space] = Key.SPACE - this[XK_exclam] = Key.UNKNOWN - this[XK_quotedbl] = Key.UNKNOWN - this[XK_numbersign] = Key.UNKNOWN - this[XK_dollar] = Key.UNKNOWN - this[XK_percent] = Key.UNKNOWN - this[XK_ampersand] = Key.UNKNOWN - this[XK_apostrophe] = Key.APOSTROPHE - this[XK_quoteright] = Key.UNKNOWN - this[XK_parenleft] = Key.UNKNOWN - this[XK_parenright] = Key.UNKNOWN - this[XK_asterisk] = Key.UNKNOWN - this[XK_plus] = Key.KP_ADD - this[XK_comma] = Key.COMMA - this[XK_minus] = Key.MINUS - this[XK_period] = Key.PERIOD - this[XK_slash] = Key.SLASH - this[XK_0] = Key.N0 - this[XK_1] = Key.N1 - this[XK_2] = Key.N2 - this[XK_3] = Key.N3 - this[XK_4] = Key.N4 - this[XK_5] = Key.N5 - this[XK_6] = Key.N6 - this[XK_7] = Key.N7 - this[XK_8] = Key.N8 - this[XK_9] = Key.N9 - this[XK_colon] = Key.UNKNOWN - this[XK_semicolon] = Key.SEMICOLON - this[XK_less] = Key.UNKNOWN - this[XK_equal] = Key.EQUAL - this[XK_greater] = Key.UNKNOWN - this[XK_question] = Key.UNKNOWN - this[XK_at] = Key.UNKNOWN - this[XK_A] = Key.A - this[XK_B] = Key.B - this[XK_C] = Key.C - this[XK_D] = Key.D - this[XK_E] = Key.E - this[XK_F] = Key.F - this[XK_G] = Key.G - this[XK_H] = Key.H - this[XK_I] = Key.I - this[XK_J] = Key.J - this[XK_K] = Key.K - this[XK_L] = Key.L - this[XK_M] = Key.M - this[XK_N] = Key.N - this[XK_O] = Key.O - this[XK_P] = Key.P - this[XK_Q] = Key.Q - this[XK_R] = Key.R - this[XK_S] = Key.S - this[XK_T] = Key.T - this[XK_U] = Key.U - this[XK_V] = Key.V - this[XK_W] = Key.W - this[XK_X] = Key.X - this[XK_Y] = Key.Y - this[XK_Z] = Key.Z - this[XK_bracketleft] = Key.UNKNOWN - this[XK_backslash] = Key.BACKSLASH - this[XK_bracketright] = Key.UNKNOWN - this[XK_asciicircum] = Key.UNKNOWN - this[XK_underscore] = Key.UNKNOWN - this[XK_grave] = Key.UNKNOWN - this[XK_quoteleft] = Key.UNKNOWN - this[XK_a] = Key.A - this[XK_b] = Key.B - this[XK_c] = Key.C - this[XK_d] = Key.D - this[XK_e] = Key.E - this[XK_f] = Key.F - this[XK_g] = Key.G - this[XK_h] = Key.H - this[XK_i] = Key.I - this[XK_j] = Key.J - this[XK_k] = Key.K - this[XK_l] = Key.L - this[XK_m] = Key.M - this[XK_n] = Key.N - this[XK_o] = Key.O - this[XK_p] = Key.P - this[XK_q] = Key.Q - this[XK_r] = Key.R - this[XK_s] = Key.S - this[XK_t] = Key.T - this[XK_u] = Key.U - this[XK_v] = Key.V - this[XK_w] = Key.W - this[XK_x] = Key.X - this[XK_y] = Key.Y - this[XK_z] = Key.Z - this[XK_leftarrow] = Key.LEFT - this[XK_uparrow] = Key.UP - this[XK_rightarrow] = Key.RIGHT - this[XK_downarrow] = Key.DOWN - this[XK_BackSpace] = Key.BACKSPACE - this[XK_Tab] = Key.TAB - this[XK_Linefeed] = Key.UNKNOWN - this[XK_Clear] = Key.CLEAR - this[XK_Return] = Key.RETURN - this[XK_Pause] = Key.PAUSE - this[XK_Scroll_Lock] = Key.SCROLL_LOCK - this[XK_Sys_Req] = Key.UNKNOWN - this[XK_Escape] = Key.ESCAPE - this[XK_Delete] = Key.DELETE - this[XK_Home] = Key.HOME - this[XK_Left] = Key.LEFT - this[XK_Up] = Key.UP - this[XK_Right] = Key.RIGHT - this[XK_Down] = Key.DOWN - this[XK_Prior] = Key.UNKNOWN - this[XK_Page_Up] = Key.PAGE_UP - this[XK_Next] = Key.UNKNOWN - this[XK_Page_Down] = Key.PAGE_DOWN - this[XK_End] = Key.END - this[XK_Begin] = Key.UNKNOWN - this[XK_Select] = Key.UNKNOWN - this[XK_Print] = Key.PRINT_SCREEN - this[XK_Execute] = Key.UNKNOWN - this[XK_Insert] = Key.INSERT - this[XK_Undo] = Key.UNKNOWN - this[XK_Redo] = Key.UNKNOWN - this[XK_Menu] = Key.MENU - this[XK_Find] = Key.UNKNOWN - this[XK_Cancel] = Key.CANCEL - this[XK_Help] = Key.HELP - this[XK_Break] = Key.UNKNOWN - this[XK_Mode_switch] = Key.UNKNOWN - this[XK_script_switch] = Key.UNKNOWN - this[XK_Num_Lock] = Key.NUM_LOCK - this[XK_KP_Space] = Key.UNKNOWN - this[XK_KP_Tab] = Key.UNKNOWN - this[XK_KP_Enter] = Key.KP_ENTER - this[XK_KP_F1] = Key.F1 - this[XK_KP_F2] = Key.F2 - this[XK_KP_F3] = Key.F3 - this[XK_KP_F4] = Key.F4 - this[XK_KP_Home] = Key.HOME - this[XK_KP_Left] = Key.KP_LEFT - this[XK_KP_Up] = Key.KP_UP - this[XK_KP_Right] = Key.KP_RIGHT - this[XK_KP_Down] = Key.KP_DOWN - this[XK_KP_Prior] = Key.UNKNOWN - this[XK_KP_Page_Up] = Key.UNKNOWN - this[XK_KP_Next] = Key.UNKNOWN - this[XK_KP_Page_Down] = Key.UNKNOWN - this[XK_KP_End] = Key.END - this[XK_KP_Begin] = Key.HOME - this[XK_KP_Insert] = Key.INSERT - this[XK_KP_Delete] = Key.DELETE - this[XK_KP_Equal] = Key.KP_EQUAL - this[XK_KP_Multiply] = Key.KP_MULTIPLY - this[XK_KP_Add] = Key.KP_ADD - this[XK_KP_Separator] = Key.KP_SEPARATOR - this[XK_KP_Subtract] = Key.KP_SUBTRACT - this[XK_KP_Decimal] = Key.KP_DECIMAL - this[XK_KP_Divide] = Key.KP_DIVIDE - this[XK_KP_0] = Key.KP_0 - this[XK_KP_1] = Key.KP_1 - this[XK_KP_2] = Key.KP_2 - this[XK_KP_3] = Key.KP_3 - this[XK_KP_4] = Key.KP_4 - this[XK_KP_5] = Key.KP_5 - this[XK_KP_6] = Key.KP_6 - this[XK_KP_7] = Key.KP_7 - this[XK_KP_8] = Key.KP_8 - this[XK_KP_9] = Key.KP_9 - this[XK_F1] = Key.F1 - this[XK_F2] = Key.F2 - this[XK_F3] = Key.F3 - this[XK_F4] = Key.F4 - this[XK_F5] = Key.F5 - this[XK_F6] = Key.F6 - this[XK_F7] = Key.F7 - this[XK_F8] = Key.F8 - this[XK_F9] = Key.F9 - this[XK_F10] = Key.F10 - this[XK_F11] = Key.F11 - this[XK_F12] = Key.F12 - this[XK_F13] = Key.F13 - this[XK_F14] = Key.F14 - this[XK_F15] = Key.F15 - this[XK_F16] = Key.F16 - this[XK_F17] = Key.F17 - this[XK_F18] = Key.F18 - this[XK_F19] = Key.F19 - this[XK_F20] = Key.F20 - this[XK_F21] = Key.F21 - this[XK_F22] = Key.F22 - this[XK_F23] = Key.F23 - this[XK_F24] = Key.F24 - this[XK_F25] = Key.F25 - this[XK_F26] = Key.UNKNOWN - this[XK_F27] = Key.UNKNOWN - this[XK_F28] = Key.UNKNOWN - this[XK_F29] = Key.UNKNOWN - this[XK_F30] = Key.UNKNOWN - this[XK_F31] = Key.UNKNOWN - this[XK_F32] = Key.UNKNOWN - this[XK_F33] = Key.UNKNOWN - this[XK_F34] = Key.UNKNOWN - this[XK_F35] = Key.UNKNOWN - - this[XK_R1] = Key.UNKNOWN - this[XK_R2] = Key.UNKNOWN - this[XK_R3] = Key.UNKNOWN - this[XK_R4] = Key.UNKNOWN - this[XK_R5] = Key.UNKNOWN - this[XK_R6] = Key.UNKNOWN - this[XK_R7] = Key.UNKNOWN - this[XK_R8] = Key.UNKNOWN - this[XK_R9] = Key.UNKNOWN - this[XK_R10] = Key.UNKNOWN - this[XK_R11] = Key.UNKNOWN - this[XK_R12] = Key.UNKNOWN - this[XK_R13] = Key.UNKNOWN - this[XK_R14] = Key.UNKNOWN - this[XK_R15] = Key.UNKNOWN - - this[XK_L1] = Key.UNKNOWN - this[XK_L2] = Key.UNKNOWN - this[XK_L3] = Key.UNKNOWN - this[XK_L4] = Key.UNKNOWN - this[XK_L5] = Key.UNKNOWN - this[XK_L6] = Key.UNKNOWN - this[XK_L7] = Key.UNKNOWN - this[XK_L8] = Key.UNKNOWN - this[XK_L9] = Key.UNKNOWN - this[XK_L10] = Key.UNKNOWN - - this[XK_Shift_L] = Key.LEFT_SHIFT - this[XK_Shift_R] = Key.RIGHT_SHIFT - this[XK_Control_L] = Key.LEFT_CONTROL - this[XK_Control_R] = Key.RIGHT_CONTROL - this[XK_Caps_Lock] = Key.CAPS_LOCK - this[XK_Shift_Lock] = Key.CAPS_LOCK - this[XK_Meta_L] = Key.LEFT_SUPER - this[XK_Meta_R] = Key.RIGHT_SUPER - this[XK_Alt_L] = Key.LEFT_ALT - this[XK_Alt_R] = Key.RIGHT_ALT - this[XK_Super_L] = Key.LEFT_SUPER - this[XK_Super_R] = Key.RIGHT_SUPER - this[XK_Hyper_L] = Key.LEFT_SUPER - this[XK_Hyper_R] = Key.RIGHT_SUPER - } -} - //internal fun FFIStructure.display() = pointer() //internal fun FFIStructure.window() = pointer() @@ -684,3 +179,7 @@ internal interface GL : INativeGL, Library { open class X11KmlGl : NativeKgl(X) { override val variant: GLVariant get() = GLVariant.JVM_X11 } + +interface glXSwapIntervalEXTCallback : Callback { + fun callback(dpy: X11.Display?, draw: Pointer, value: Int) +} diff --git a/korge/src/jvm/korlibs/render/x11/X11OpenglContext.kt b/korge/src/jvm/korlibs/render/x11/X11OpenglContext.kt index c224ddf9a4..e542de3b0d 100644 --- a/korge/src/jvm/korlibs/render/x11/X11OpenglContext.kt +++ b/korge/src/jvm/korlibs/render/x11/X11OpenglContext.kt @@ -1,12 +1,11 @@ package korlibs.render.x11 -import korlibs.render.* -import korlibs.render.platform.* -import korlibs.io.lang.* -import com.sun.jna.CallbackReference -import com.sun.jna.Pointer +import com.sun.jna.* import com.sun.jna.platform.unix.* +import korlibs.io.lang.* import korlibs.math.* +import korlibs.render.* +import korlibs.render.platform.* // https://www.khronos.org/opengl/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX) class X11OpenglContext(val gwconfig: GameWindowConfig, val d: X11.Display?, val w: X11.Drawable?, val scr: Int, val vi: XVisualInfo? = chooseVisuals(d, scr), val doubleBuffered: Boolean = true) : BaseOpenglContext { @@ -22,44 +21,9 @@ class X11OpenglContext(val gwconfig: GameWindowConfig, val d: X11.Display?, val for (depth in listOf(24, 16, 0)) { for (stencil in listOf(8, 0)) { for (doubleBuffer in listOf(true, false)) { - val attrs = buildList { - if (specifyRenderType) { - //add(GLX_RENDER_TYPE) - //add(GLX_RGBA_BIT) - add(GLX_RGBA) - } - if (doubleBuffer) { - add(GLX_DOUBLEBUFFER) - add(doubleBuffer.toInt()) - } - if (depth != 0) { - add(GLX_DEPTH_SIZE) - add(depth) - } - if (stencil != 0) { - add(GLX_STENCIL_SIZE) - add(stencil) - } - if (bitsPerColorComponent != 0) { - add(GLX_RED_SIZE) - add(bitsPerColorComponent) - add(GLX_GREEN_SIZE) - add(bitsPerColorComponent) - add(GLX_BLUE_SIZE) - add(bitsPerColorComponent) - } - if (multisampling) { - add(GLX_SAMPLE_BUFFERS) - add(1) - add(GLX_SAMPLES) - add(4) - } - add(X11.None) - }.toIntArray() - val vi = X.glXChooseVisual(d, scr, attrs) - if (vi != null) { - println("VI: $vi (doubleBuffer=$doubleBuffer, depth=$depth, bitsPerColorComponent=$bitsPerColorComponent, specifyRenderType=$specifyRenderType)") - return vi + chooseVisual(d, scr, multisampling, specifyRenderType, bitsPerColorComponent, depth, stencil, doubleBuffer)?.let { + println("VI: $it (doubleBuffer=$doubleBuffer, depth=$depth, bitsPerColorComponent=$bitsPerColorComponent, specifyRenderType=$specifyRenderType)") + return it } } } @@ -68,10 +32,58 @@ class X11OpenglContext(val gwconfig: GameWindowConfig, val d: X11.Display?, val } } - println("VI: null") return null } + + + private fun chooseVisual( + d: X11.Display?, + scr: Int = X.XDefaultScreen(d), + multisampling: Boolean, + specifyRenderType: Boolean, + bitsPerColorComponent: Int, + depth: Int, + stencil: Int, + doubleBuffer: Boolean, + ): XVisualInfo? { + val attrs = buildList { + if (specifyRenderType) { + //add(GLX_RENDER_TYPE) + //add(GLX_RGBA_BIT) + add(GLX_RGBA) + } + if (doubleBuffer) { + add(GLX_DOUBLEBUFFER) + add(doubleBuffer.toInt()) + } + if (depth != 0) { + add(GLX_DEPTH_SIZE) + add(depth) + } + if (stencil != 0) { + add(GLX_STENCIL_SIZE) + add(stencil) + } + if (bitsPerColorComponent != 0) { + add(GLX_RED_SIZE) + add(bitsPerColorComponent) + add(GLX_GREEN_SIZE) + add(bitsPerColorComponent) + add(GLX_BLUE_SIZE) + add(bitsPerColorComponent) + } + if (multisampling) { + add(GLX_SAMPLE_BUFFERS) + add(1) + add(GLX_SAMPLES) + add(4) + } + add(X11.None) + }.toIntArray() + val vi = X.glXChooseVisual(d, scr, attrs) + return vi + } } init { println("Preparing OpenGL context. Screen: $scr") @@ -122,16 +134,16 @@ class X11OpenglContext(val gwconfig: GameWindowConfig, val d: X11.Display?, val } private var glXSwapIntervalEXTSet: Boolean = false - private var swapIntervalEXT: X11GameWindow.glXSwapIntervalEXTCallback? = null + private var swapIntervalEXT: glXSwapIntervalEXTCallback? = null private var swapIntervalEXTPointer: Pointer? = null - private fun getSwapInterval(): X11GameWindow.glXSwapIntervalEXTCallback? { + private fun getSwapInterval(): glXSwapIntervalEXTCallback? { if (!glXSwapIntervalEXTSet) { glXSwapIntervalEXTSet = true swapIntervalEXTPointer = X.glXGetProcAddress("glXSwapIntervalEXT") swapIntervalEXT = when { - swapIntervalEXTPointer != Pointer.NULL -> CallbackReference.getCallback(X11GameWindow.glXSwapIntervalEXTCallback::class.java, swapIntervalEXTPointer) as? X11GameWindow.glXSwapIntervalEXTCallback? + swapIntervalEXTPointer != Pointer.NULL -> CallbackReference.getCallback(glXSwapIntervalEXTCallback::class.java, swapIntervalEXTPointer) as? glXSwapIntervalEXTCallback? else -> null } println("swapIntervalEXT: $swapIntervalEXT") diff --git a/korge/test/common/korlibs/korge/KorgeHeadlessTest.kt b/korge/test/common/korlibs/korge/KorgeHeadlessTest.kt index 39b9c79653..80caa3ce8c 100644 --- a/korge/test/common/korlibs/korge/KorgeHeadlessTest.kt +++ b/korge/test/common/korlibs/korge/KorgeHeadlessTest.kt @@ -58,7 +58,7 @@ class KorgeHeadlessTest { } finally { } */ - gameWindow.frameRender() + //gameWindow.frameRender() views.gameWindow.close() // We close the window, finalizing the test here } diff --git a/korge/test/common/korlibs/render/GameWindowTest.kt b/korge/test/common/korlibs/render/GameWindowTest.kt index 054553c994..9a955f97bf 100644 --- a/korge/test/common/korlibs/render/GameWindowTest.kt +++ b/korge/test/common/korlibs/render/GameWindowTest.kt @@ -9,7 +9,7 @@ import kotlin.test.* class GameWindowTest { @Test fun testCustomCursor() = suspendTest { - val cursor = GameWindow.CustomCursor(buildShape { + val cursor = CustomCursor(buildShape { fill(Colors.RED) { moveTo(Point(0, 0)) lineTo(Point(-32, -32)) diff --git a/korge/test/common/korlibs/render/SyncEventLoopCoroutineDispatcherTest.kt b/korge/test/common/korlibs/render/SyncEventLoopCoroutineDispatcherTest.kt index 4598065267..5f82ff490d 100644 --- a/korge/test/common/korlibs/render/SyncEventLoopCoroutineDispatcherTest.kt +++ b/korge/test/common/korlibs/render/SyncEventLoopCoroutineDispatcherTest.kt @@ -1,5 +1,6 @@ package korlibs.render +import korlibs.datastructure.event.* import korlibs.io.async.* import korlibs.time.* import kotlinx.coroutines.* @@ -8,12 +9,13 @@ import kotlin.test.* class SyncEventLoopCoroutineDispatcherTest { @Test fun test() { - val dispatcher = SyncEventLoopCoroutineDispatcher(precise = true, immediateRun = true) + val eventLoop = SyncEventLoop(precise = true, immediateRun = true) + val dispatcher = EventLoopCoroutineDispatcher(eventLoop) launchImmediately(dispatcher) { println("${DateTime.now()}: a") delay(1000.milliseconds) println("${DateTime.now()}: b") } - dispatcher.loopUntilEmpty() + eventLoop.runTasksUntilEmpty() } } diff --git a/korge/test/jvm/korlibs/korge/awt/AwtSample.kt b/korge/test/jvm/korlibs/korge/awt/AwtSample.kt index 7f8666d000..df732858ce 100644 --- a/korge/test/jvm/korlibs/korge/awt/AwtSample.kt +++ b/korge/test/jvm/korlibs/korge/awt/AwtSample.kt @@ -4,6 +4,7 @@ import korlibs.image.color.* import korlibs.korge.* import korlibs.korge.view.* import korlibs.math.geom.* +import korlibs.render.awt.* import kotlinx.coroutines.* import java.awt.* import javax.swing.* @@ -17,10 +18,10 @@ object AwtSample { frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE frame.layout = GridLayout(5, 1) frame.add(JButton("[1]")) - frame.add(Korge(virtualSize = Size(512, 512), displayMode = KorgeDisplayMode.NO_SCALE).glCanvas { + frame.add(AwtAGOpenglCanvas().apply { gameWindow.configureKorge(KorgeConfig(virtualSize = Size(512, 512), displayMode = KorgeDisplayMode.NO_SCALE)) { views.clearColor = Colors.RED solidRect(100, 100, Colors.YELLOW) - }) + } }) frame.add(JButton("[2]")) /* frame.add(GLCanvasWithKorge(Korge.Config(virtualSize = SizeInt(512, 512), clipBorders = true)) { diff --git a/korge/test/jvm/korlibs/render/TestE2eJava.kt b/korge/test/jvm/korlibs/render/TestE2eJava.kt index 886251def4..74998972af 100644 --- a/korge/test/jvm/korlibs/render/TestE2eJava.kt +++ b/korge/test/jvm/korlibs/render/TestE2eJava.kt @@ -1,5 +1,6 @@ package korlibs.render +/* import korlibs.event.* import korlibs.graphics.* import korlibs.image.bitmap.* @@ -79,3 +80,4 @@ class TestE2eJava { //assertEquals(Colors.DARKGREY, bmp[63, 0]) } } +*/ diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt index 1a8adb49ec..d9d90170d4 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt @@ -3,46 +3,27 @@ package korlibs.render.awt import korlibs.datastructure.thread.* import korlibs.image.color.* import korlibs.image.format.* -import korlibs.io.async.* import korlibs.io.file.std.* +import korlibs.korge.* import korlibs.korge.view.* +import korlibs.logger.* import korlibs.time.* -import kotlinx.coroutines.* import kotlin.test.* class AwtGameWindowTest { @Test - @Ignore + //@Ignore fun test() { + Korge.logger.level = Logger.Level.DEBUG val gameWindow = NewAwtGameWindow() - //gameWindow.icon = runBlocking { resourcesVfs["korge.png"].readBitmap() } - - val views = Views(gameWindow) - gameWindow.onRenderEvent { - views.renderNew() - } - val stopwatch = Stopwatch() - gameWindow.onUpdateEvent { - //println(NativeThread.currentThreadName) - views.update(stopwatch.getElapsedAndRestart()) - //println("UPDATE EVENT!") - } - gameWindow.launchUnscoped { - views.stage.image(resourcesVfs["korge.png"].readBitmap()) - views.stage.solidRect(100, 100, Colors.RED).addUpdater { + gameWindow.configureKorge { + image(resourcesVfs["korge.png"].readBitmap()) + solidRect(100, 100, Colors.RED).addUpdater { this.x++ } - println("[1]") - delay(1.seconds) - println("[2]") } - //gameWindow.eventQueueLater { println("[0]") } - //gameWindow.coroutineDispatcher.queue { println("[1]") } - println("gameWindow=$gameWindow") - //gameWindow.coroutineDispatcher.queue { println("[2]") } - //gameWindow.show() - //gameWindow.mainLoop { println("HELLO") } + gameWindow.show() NativeThread.sleep(100.seconds) } From 54b7572b27f7316d22ea4668177606c208c80607 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 19:51:04 +0200 Subject: [PATCH 03/66] [WIP] Much more work on New Game Window --- .../format/CoreGraphicsImageFormatProvider.kt | 4 +- .../src/common/korlibs/time}/TimedCache.kt | 12 +- .../src/jvm/korlibs/memory/dyn/osx/Cocoa.kt | 13 +- .../gamepad}/X11JoyGameControllerCommon.kt | 7 +- .../gamepad}/XInputGamepadEventAdapter.kt | 2 +- korge/src/common/korlibs/korge/Korge.kt | 15 +- .../korlibs/korge/KorgeViewsConfigureInput.kt | 51 +- .../korlibs/korge/input/GestureEvents.kt | 4 +- korge/src/common/korlibs/korge/view/Stage.kt | 2 + korge/src/common/korlibs/korge/view/Views.kt | 1 + korge/src/common/korlibs/render/GameWindow.kt | 41 +- .../common/korlibs/render/GameWindowExt.kt | 25 +- .../common/korlibs/render/MenuInterface.kt | 42 ++ .../gamepad}/DarwinMacosGameControllerJvm.kt | 19 +- .../jvm/korlibs/kgl/KmlGlContextDefault.kt | 8 +- .../korge/testing/KorgeOffscreenTest.kt | 5 +- .../korlibs/render/DefaultGameWindowJvm.kt | 2 +- .../render/awt/{AwtAg.kt => AGOpenglAWT.kt} | 17 +- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 65 +- .../jvm/korlibs/render/awt/AwtFrameTools.kt | 169 ++++- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 243 +++++--- .../korlibs/render/awt/BaseAwtGameWindow.kt | 576 ------------------ .../render/awt/DesktopGamepadUpdater.kt | 20 + korge/src/jvm/korlibs/render/awt/GLCanvas.kt | 101 --- .../korlibs/render/awt/NewAwtGameWindow.kt | 71 --- .../korlibs/render/awt/OldAwtGameWindow.kt | 409 +++++++++++++ korge/src/jvm/korlibs/render/osx/Cocoa.kt | 3 - korge/src/jvm/korlibs/render/osx/MacGL.kt | 3 +- .../jvm/korlibs/render/osx/MacosGLContext.kt | 68 ++- .../render/x11/LinuxJoyEventAdapterTest.kt | 1 + .../korlibs/render/awt/AwtGameCanvasTest.kt | 15 +- .../korlibs/render/awt/AwtGameWindowTest.kt | 41 +- .../korlibs/render/osx/metal/AGMetalTest.kt | 2 +- 33 files changed, 1013 insertions(+), 1044 deletions(-) rename {korge/src/common/korlibs/render/internal => korge-foundation/src/common/korlibs/time}/TimedCache.kt (68%) rename korge/src/common/korlibs/{render/x11 => event/gamepad}/X11JoyGameControllerCommon.kt (99%) rename korge/src/common/korlibs/{render/win32 => event/gamepad}/XInputGamepadEventAdapter.kt (99%) create mode 100644 korge/src/common/korlibs/render/MenuInterface.kt rename korge/src/jvm/korlibs/{render/osx => event/gamepad}/DarwinMacosGameControllerJvm.kt (96%) rename korge/src/jvm/korlibs/render/awt/{AwtAg.kt => AGOpenglAWT.kt} (56%) delete mode 100644 korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt create mode 100644 korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt delete mode 100644 korge/src/jvm/korlibs/render/awt/GLCanvas.kt delete mode 100644 korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt create mode 100644 korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt diff --git a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt index fb86999123..a430f5102e 100644 --- a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt +++ b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt @@ -93,7 +93,7 @@ open class CoreGraphicsImageFormatProvider : AwtNativeImageFormatProvider() { } } - override suspend fun decodeHeaderInternal(data: ByteArray): ImageInfo = nsAutoreleasePool { + override suspend fun decodeHeaderInternal(data: ByteArray): ImageInfo = autoreleasePool { val cfdata = CoreFoundation.CFDataCreate(null, data, data.size) val imgSource = ImageIO.CGImageSourceCreateWithData(data = cfdata, options = null) val props = ImageIO.CGImageSourceCopyPropertiesAtIndex(imgSource, 0, null) @@ -167,7 +167,7 @@ open class CoreGraphicsImageFormatProvider : AwtNativeImageFormatProvider() { return super.decodeInternal(data, props) } - return nsAutoreleasePool { + return autoreleasePool { //val p = Stopwatch() //println(p.getElapsedAndRestart()) diff --git a/korge/src/common/korlibs/render/internal/TimedCache.kt b/korge-foundation/src/common/korlibs/time/TimedCache.kt similarity index 68% rename from korge/src/common/korlibs/render/internal/TimedCache.kt rename to korge-foundation/src/common/korlibs/time/TimedCache.kt index d7211e01c9..3e61aa861b 100644 --- a/korge/src/common/korlibs/render/internal/TimedCache.kt +++ b/korge-foundation/src/common/korlibs/time/TimedCache.kt @@ -1,12 +1,8 @@ -package korlibs.render.internal +package korlibs.time -import korlibs.time.DateTime -import korlibs.time.TimeSpan -import kotlin.reflect.KProperty +import kotlin.reflect.* -// @TODO: Move to Klock? - -internal class TimedCache(val ttl: TimeSpan, val gen: () -> T) { +class TimedCache(val ttl: TimeSpan, val gen: () -> T) { var last = DateTime.EPOCH lateinit var value: T @@ -22,7 +18,7 @@ internal class TimedCache(val ttl: TimeSpan, val gen: () -> T) { operator fun getValue(obj: Any?, property: KProperty<*>): T = get() } -internal class IntTimedCache(val ttl: TimeSpan, val gen: () -> Int) { +class IntTimedCache(val ttl: TimeSpan, val gen: () -> Int) { var last = DateTime.EPOCH var value: Int = 0 diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt index 8a56d9506d..bbabf3cd94 100644 --- a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt +++ b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt @@ -668,15 +668,6 @@ open class NSClass(id: Long) : NSObject(id) { override fun toString(): String = "NSClass[$id]($name)" } -fun nsAutoreleasePool(block: () -> T): T { - val pool = NSClass("NSAutoreleasePool").alloc().msgSend("init") - try { - return block() - } finally { - pool.msgSend("release") - } -} - open class ObjcProtocol(val name: String) : NSObject(ObjectiveC.objc_getProtocol(name)) { val OBJ_PROTOCOL = id } @@ -1012,10 +1003,10 @@ inline class NSMenu(val id: Long) { } } -inline fun autoreleasePool(body: () -> Unit) { +inline fun autoreleasePool(body: () -> T): T { val autoreleasePool = if (Platform.isMac()) NSClass("NSAutoreleasePool").alloc().msgSend("init") else null try { - body() + return body() } finally { autoreleasePool?.msgSend("drain") } diff --git a/korge/src/common/korlibs/render/x11/X11JoyGameControllerCommon.kt b/korge/src/common/korlibs/event/gamepad/X11JoyGameControllerCommon.kt similarity index 99% rename from korge/src/common/korlibs/render/x11/X11JoyGameControllerCommon.kt rename to korge/src/common/korlibs/event/gamepad/X11JoyGameControllerCommon.kt index 5b378b4164..8615e15bed 100644 --- a/korge/src/common/korlibs/render/x11/X11JoyGameControllerCommon.kt +++ b/korge/src/common/korlibs/event/gamepad/X11JoyGameControllerCommon.kt @@ -1,18 +1,17 @@ -package korlibs.render.x11 +package korlibs.event.gamepad import korlibs.datastructure.* import korlibs.datastructure.iterators.* -import korlibs.time.* -import korlibs.memory.* import korlibs.event.* import korlibs.io.concurrent.* import korlibs.io.concurrent.atomic.* -import korlibs.io.concurrent.atomic.KorAtomicInt import korlibs.io.file.* import korlibs.io.file.sync.* import korlibs.io.lang.* import korlibs.math.* +import korlibs.memory.* import korlibs.platform.* +import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* diff --git a/korge/src/common/korlibs/render/win32/XInputGamepadEventAdapter.kt b/korge/src/common/korlibs/event/gamepad/XInputGamepadEventAdapter.kt similarity index 99% rename from korge/src/common/korlibs/render/win32/XInputGamepadEventAdapter.kt rename to korge/src/common/korlibs/event/gamepad/XInputGamepadEventAdapter.kt index b73f14f6c3..f1852162af 100644 --- a/korge/src/common/korlibs/render/win32/XInputGamepadEventAdapter.kt +++ b/korge/src/common/korlibs/event/gamepad/XInputGamepadEventAdapter.kt @@ -1,4 +1,4 @@ -package korlibs.render.win32 +package korlibs.event.gamepad import korlibs.event.* import korlibs.ffi.* diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index 9e7596acc2..278b2b3f15 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -1,7 +1,6 @@ package korlibs.korge import korlibs.annotations.* -import korlibs.graphics.log.* import korlibs.image.color.* import korlibs.image.format.* import korlibs.inject.* @@ -142,7 +141,8 @@ data class Korge( configureLoggerFromProperties(localCurrentDirVfs["klogger.properties"]) } val config = this - val gameWindow = (config.gameWindow ?: coroutineContext[GameWindow] ?: CreateDefaultGameWindow(GameWindowCreationConfig(multithreaded = config.multithreaded, fullscreen = config.fullscreen))) + val creationConfig = GameWindowCreationConfig(multithreaded = config.multithreaded, fullscreen = config.fullscreen, title = config.title) + val gameWindow = (config.gameWindow ?: coroutineContext[GameWindow] ?: CreateDefaultGameWindow(creationConfig)) gameWindow.configureKorge(config) { entry() } @@ -171,7 +171,8 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen val views = Views( gameWindow = gameWindow, coroutineContext = gameWindow.coroutineDispatcher, - ag = if (config.debugAg) AGPrint() else gameWindow.ag, + //ag = if (config.debugAg) AGPrint() else gameWindow.ag, + ag = gameWindow.ag, injector = config.injector, gameId = config.gameId, timeProvider = config.timeProvider, @@ -183,6 +184,7 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen Dyn.global["views"] = it } } + gameWindow.coroutineContext += InjectorContext(config.injector) Korge.logger.logTime("configureGameWindow") { gameWindow.configure(windowSize, config.title, null, config.fullscreen, config.backgroundColor ?: Colors.BLACK) } @@ -227,11 +229,15 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen gameWindow.onRenderEvent { if (initialized) { //println("RENDER") + //println("gameWindow.size=${gameWindow.frameSize}") views.renderNew() } } + gameWindow.queueSuspend { Korge.logger.info { "Initializing..." } + runCatching { views.init() }.exceptionOrNull()?.let { it.stackTraceToString() } + // Initialize try { Korge.logger.logTime("setIcon") { @@ -251,7 +257,7 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen Korge.logger.logTime("prepareViews") { KorgeReload.registerEventDispatcher(gameWindow) @Suppress("OPT_IN_USAGE") - views.prepareViewsBase(gameWindow, true, gameWindow.bgcolor, TimeSpan.NIL, config.forceRenderEveryFrame, config.configInjector).await() + views.prepareViewsBase(gameWindow, true, gameWindow.bgcolor, TimeSpan.NIL, config.forceRenderEveryFrame, config.configInjector) } Korge.logger.logTime("completeViews") { @@ -269,4 +275,5 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen views.stage.sceneContainer().changeTo(config.mainSceneClass) } } + } diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index e612806c7b..86b7806b07 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -10,7 +10,6 @@ import korlibs.korge.internal.* import korlibs.korge.view.* import korlibs.math.geom.* import korlibs.time.* -import kotlinx.coroutines.* @KorgeInternal internal fun Views.prepareViewsBase( @@ -20,7 +19,7 @@ internal fun Views.prepareViewsBase( fixedSizeStep: TimeSpan = TimeSpan.NIL, forceRenderEveryFrame: Boolean = true, configInjector: Injector.() -> Unit = {}, -): CompletableDeferred { +) { val views = this configInjector(views.injector) @@ -231,50 +230,16 @@ internal fun Views.prepareViewsBase( eventDispatcher.onEvent(ReloadEvent) { views.dispatch(it) } - var renderShown = false views.clearEachFrame = clearEachFrame views.clearColor = bgcolor - val firstRenderDeferred = CompletableDeferred() - fun renderBlock(event: RenderEvent) { - //println("renderBlock: $event") - try { - views.frameUpdateAndRender( - fixedSizeStep = fixedSizeStep, - forceRender = views.forceRenderEveryFrame, - doUpdate = event.update, - doRender = event.render, - ) - - views.input.mouseOutside = false - if (moveMouseOutsideInNextFrame) { - moveMouseOutsideInNextFrame = false - views.input.mouseOutside = true - views.input.mouseInside = false - views.mouseUpdated() - } - } catch (e: Throwable) { - Korge.logger.error { "views.gameWindow.onRenderEvent:" } - e.printStackTrace() - if (views.rethrowRenderError) throw e + views.onAfterRender.add { + views.input.mouseOutside = false + if (moveMouseOutsideInNextFrame) { + moveMouseOutsideInNextFrame = false + views.input.mouseOutside = true + views.input.mouseInside = false + views.mouseUpdated() } } - - views.gameWindow.onRenderEvent { event -> - //println("RenderEvent: $event") - if (!event.render) { - renderBlock(event) - } else { - views.renderContext.doRender { - if (!renderShown) { - //println("!!!!!!!!!!!!! views.gameWindow.addEventListener") - renderShown = true - firstRenderDeferred.complete(Unit) - } - renderBlock(event) - } - } - } - - return firstRenderDeferred } diff --git a/korge/src/common/korlibs/korge/input/GestureEvents.kt b/korge/src/common/korlibs/korge/input/GestureEvents.kt index f615ca09fa..c54434b6b0 100644 --- a/korge/src/common/korlibs/korge/input/GestureEvents.kt +++ b/korge/src/common/korlibs/korge/input/GestureEvents.kt @@ -2,8 +2,8 @@ package korlibs.korge.input import korlibs.datastructure.* import korlibs.event.* -import korlibs.korge.view.* import korlibs.io.async.* +import korlibs.korge.view.* import kotlin.reflect.* class GestureEvents(val view: BaseView) { @@ -17,6 +17,8 @@ class GestureEvents(val view: BaseView) { val id: Int get() = lastEvent.id val amount: Float get() = lastEvent.amount + override fun toString(): String = "GestureEvents(lastEvent=$lastEvent)" + lateinit var views: Views private set diff --git a/korge/src/common/korlibs/korge/view/Stage.kt b/korge/src/common/korlibs/korge/view/Stage.kt index be138e7580..d6d3ab7f40 100644 --- a/korge/src/common/korlibs/korge/view/Stage.kt +++ b/korge/src/common/korlibs/korge/view/Stage.kt @@ -21,6 +21,8 @@ open class Stage internal constructor(override val views: Views) : FixedSizeCont , ResourcesContainer , BoundsProvider by views.bp , InvalidateNotifier + , DialogInterfaceProvider by views + , MenuInterfaceProvider by views { override var clip: Boolean by views::clipBorders diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index a1d6845cc8..000d5ada36 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -55,6 +55,7 @@ class Views( CoroutineScope, ViewsContainer, BoundsProvider by bp, DialogInterfaceProvider by gameWindow, + MenuInterfaceProvider by gameWindow, Closeable, ResourcesContainer, InvalidateNotifier, diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 9bfb7bec7c..f9bfb577ef 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -13,7 +13,6 @@ import korlibs.io.async.* import korlibs.korge.view.* import korlibs.logger.* import korlibs.math.geom.* -import korlibs.render.internal.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* @@ -58,6 +57,7 @@ expect fun CreateDefaultGameWindow(config: GameWindowCreationConfig = GameWindow open class GameWindow : BaseEventListener(), DialogInterfaceProvider, + MenuInterface, DeviceDimensionsProvider, CoroutineContext.Element, AGWindow, @@ -67,11 +67,6 @@ open class GameWindow : open val androidContextAny: Any? get() = null override val dialogInterface: DialogInterface get() = DialogInterface.Unsupported - // Context Menu - open fun setMainMenu(items: List) = Unit - open fun showContextMenu(items: List) = Unit - fun showContextMenu(block: GameWindowMenuItemBuilder.() -> Unit) = showContextMenu(GameWindowMenuItemBuilder().also(block).toItem().children ?: listOf()) - // SOFT KEYBOARD open val isSoftKeyboardVisible: Boolean get() = false open fun setInputRectangle(windowRect: Rectangle) = Unit @@ -100,6 +95,7 @@ open class GameWindow : val eventLoop = SyncEventLoop() open val coroutineDispatcher = EventLoopCoroutineDispatcher(eventLoop) + var coroutineContext: CoroutineContext = EmptyCoroutineContext fun getCoroutineDispatcherWithCurrentContext(coroutineContext: CoroutineContext): CoroutineContext = coroutineContext + coroutineDispatcher suspend fun getCoroutineDispatcherWithCurrentContext(): CoroutineContext = getCoroutineDispatcherWithCurrentContext(coroutineContext) @@ -108,7 +104,7 @@ open class GameWindow : @PublishedApi internal val _updateRenderLock = Lock() inline fun updateRenderLock(block: () -> Unit) = _updateRenderLock(block) fun queueSuspend(callback: suspend () -> Unit) { - launch(coroutineDispatcher) { + launch(coroutineDispatcher + coroutineContext) { callback() } } @@ -143,12 +139,36 @@ open class GameWindow : val timePerFrame: TimeSpan get() = counterTimePerFrame open fun computeDisplayRefreshRate(): Int = 60 private val fpsCached by IntTimedCache(1000.milliseconds) { computeDisplayRefreshRate() } + + /** Update FPS */ open var preferredFps: Int = DEFAULT_PREFERRED_FPS open var fps: Int get() = fpsCached @Deprecated("Deprecated setting fps") set(value) = Unit + /** Rate at which update events happen in the event loop */ + var updateFps: Frequency = DEFAULT_PREFERRED_FPS.hz + set(value) { + field = value + updateUpdateInterval() + } + private var currentUpdateFps: Frequency = 0.hz + private var currentUpdateInterval: Closeable? = null + private fun updateUpdateInterval(fps: Frequency = this.updateFps) { + if (currentUpdateFps != fps) { + currentUpdateFps = fps + currentUpdateInterval?.close() + currentUpdateInterval = eventLoop.setInterval(fps) { + dispatchUpdateEvent() + } + } + } + + init { + updateUpdateInterval() + } + // PROPS open var visible: Boolean = false fun hide() = run { visible = false } @@ -157,6 +177,7 @@ open class GameWindow : open var title: String get() = ""; set(value) = Unit open val width: Int = 0 open val height: Int = 0 + val frameSize get() = Size(width, height) open fun setSize(width: Int, height: Int): Unit = Unit open var vsync: Boolean = true open var icon: Bitmap? = null @@ -198,11 +219,17 @@ open class GameWindow : open fun close(exitCode: Int = 0) { if (closing) return closing = true + println("[1]") + queue { dispatchDestroyEvent() } + println("[2]") running = false this.exitCode = exitCode + println("[3]") logger.info { "GameWindow.close" } coroutineDispatcher.close() + println("[4]") coroutineDispatcher.cancelChildren() + println("[5]") } @Deprecated("") diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index 8ec3b24b58..49ae5c788a 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -216,6 +216,7 @@ data class GameWindowCreationConfig( val logGl: Boolean = false, val cacheGl: Boolean = false, val fullscreen: Boolean? = null, + val title: String = "App", ) { companion object { val DEFAULT = GameWindowCreationConfig() @@ -300,30 +301,6 @@ fun GameWindow.onDragAndDropFileEvent(block: suspend (DropFileEvent) -> Unit) { enum class HapticFeedbackKind { GENERIC, ALIGNMENT, LEVEL_CHANGE } -data class GameWindowMenuItem(val text: String?, val enabled: Boolean = true, val children: List? = null, val action: () -> Unit = {}) { - companion object { - val SEPARATOR = GameWindowMenuItem(null) - } -} - -class GameWindowMenuItemBuilder(private var text: String? = null, private var enabled: Boolean = true, private var action: () -> Unit = {}) { - @PublishedApi internal val children = arrayListOf() - - inline fun separator() { - item(null) - } - - inline fun item(text: String?, enabled: Boolean = true, noinline action: () -> Unit = {}, block: GameWindowMenuItemBuilder.() -> Unit = {}): GameWindowMenuItem { - val mib = GameWindowMenuItemBuilder(text, enabled, action) - block(mib) - val item = mib.toItem() - children.add(item) - return item - } - - fun toItem() = GameWindowMenuItem(text, enabled, children.ifEmpty { null }, action) -} - sealed interface ICursor data class CustomCursor(val shape: Shape, val name: String = "custom") : ICursor, Extra by Extra.Mixin() { diff --git a/korge/src/common/korlibs/render/MenuInterface.kt b/korge/src/common/korlibs/render/MenuInterface.kt new file mode 100644 index 0000000000..d9017514c5 --- /dev/null +++ b/korge/src/common/korlibs/render/MenuInterface.kt @@ -0,0 +1,42 @@ +package korlibs.render + +interface MenuInterfaceProvider { + val menuInterface: MenuInterface +} + +interface MenuInterface : MenuInterfaceProvider { + override val menuInterface: MenuInterface get() = this + + fun setMainMenu(items: List): Unit = Unit + fun showContextMenu(items: List): Unit = Unit +} + +fun MenuInterfaceProvider.setMainMenu(items: List): Unit = menuInterface.setMainMenu(items) +fun MenuInterfaceProvider.showContextMenu(items: List): Unit = menuInterface.showContextMenu(items) +fun MenuInterfaceProvider.setMainMenu(block: MenuItem.Builder.() -> Unit) = setMainMenu(MenuItem.Builder().also(block).toItem().children ?: listOf()) +fun MenuInterfaceProvider.showContextMenu(block: MenuItem.Builder.() -> Unit) = showContextMenu(MenuItem.Builder().also(block).toItem().children ?: listOf()) + +data class MenuItem(val text: String?, val enabled: Boolean = true, val children: List? = null, val action: () -> Unit = {}) { + companion object { + val SEPARATOR = MenuItem(null) + } + + class Builder(private var text: String? = null, private var enabled: Boolean = true, private var action: () -> Unit = {}) { + @PublishedApi internal val children = arrayListOf() + + inline fun separator() { + item(null) + } + + inline fun item(text: String?, enabled: Boolean = true, noinline action: () -> Unit = {}, block: Builder.() -> Unit = {}): MenuItem { + val mib = Builder(text, enabled, action) + block(mib) + val item = mib.toItem() + children.add(item) + return item + } + + fun toItem() = MenuItem(text, enabled, children.ifEmpty { null }, action) + } + +} diff --git a/korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt b/korge/src/jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt similarity index 96% rename from korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt rename to korge/src/jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt index 84a24315e0..0b38f0a17a 100644 --- a/korge/src/jvm/korlibs/render/osx/DarwinMacosGameControllerJvm.kt +++ b/korge/src/jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt @@ -1,4 +1,4 @@ -package korlibs.render.osx +package korlibs.event.gamepad import com.sun.jna.* import korlibs.datastructure.iterators.* @@ -10,10 +10,7 @@ import korlibs.number.* import korlibs.render.* import kotlin.reflect.* -//fun main() { -//} - -interface FrameworkInt : Library +internal interface FrameworkInt : Library @PublishedApi internal inline fun getValueOrNull(obj: ObjcRef, property: KProperty<*>, gen: (Long) -> T): T? = @@ -23,7 +20,7 @@ internal inline fun getValueOrNull(obj: ObjcRef, property: KProperty<*>, gen internal inline fun getValue(obj: ObjcRef, property: KProperty<*>, gen: (Long) -> T): T = obj.id.msgSend(property.name).let { gen(it) } -inline class GCControllerButtonInput(val id: Long) { +internal inline class GCControllerButtonInput(val id: Long) { val analog: Boolean get() = id.msgSendInt(sel_isAnalog) != 0 val touched: Boolean get() = id.msgSendInt(sel_isTouched) != 0 val pressed: Boolean get() = id.msgSendInt(sel_isPressed) != 0 @@ -55,7 +52,7 @@ class GCControllerAxisInput(id: Long) : ObjcRef(id) { override fun toString(): String = value.niceStr(2) } -class GCControllerDirectionPad(id: Long) : ObjcRef(id) { +internal class GCControllerDirectionPad(id: Long) : ObjcRef(id) { @Keep val right by GCControllerButtonInput @Keep val left by GCControllerButtonInput @Keep val up by GCControllerButtonInput @@ -78,7 +75,7 @@ class GCControllerDirectionPad(id: Long) : ObjcRef(id) { override fun toString(): String = "DPad(${up.nice}, ${right.nice}, ${down.nice}, ${left.nice})" } -open class GCMicroGamepad(id: Long) : ObjcRef(id) { +internal open class GCMicroGamepad(id: Long) : ObjcRef(id) { @Keep val buttonA by GCControllerButtonInput @Keep val buttonX by GCControllerButtonInput @Keep val dpad by GCControllerDirectionPad @@ -90,7 +87,7 @@ open class GCMicroGamepad(id: Long) : ObjcRef(id) { } } -open class GCGamepad(id: Long) : GCMicroGamepad(id) { +internal open class GCGamepad(id: Long) : GCMicroGamepad(id) { @Keep val leftShoulder by GCControllerButtonInput @Keep val rightShoulder by GCControllerButtonInput @Keep val buttonB by GCControllerButtonInput @@ -102,7 +99,7 @@ open class GCGamepad(id: Long) : GCMicroGamepad(id) { } } -class GCExtendedGamepad(id: Long) : GCGamepad(id) { +internal class GCExtendedGamepad(id: Long) : GCGamepad(id) { @Keep val leftTrigger by GCControllerButtonInput @Keep val rightTrigger by GCControllerButtonInput @Keep val buttonOptions by GCControllerButtonInput @@ -150,7 +147,7 @@ class GCDeviceBattery(id: Long) : ObjcRef(id) { /** * https://developer.apple.com/documentation/gamecontroller/gccontroller?language=objc */ -class GCController(id: Long) : ObjcRef(id) { +internal class GCController(id: Long) : ObjcRef(id) { val isAttachedToDevice: Boolean get() = id.msgSendInt("isAttachedToDevice") != 0 val playerIndex: Int get() = id.msgSendInt("playerIndex") //val physicalInputProfile: GCPhysicalInputProfile by GCPhysicalInputProfile diff --git a/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt b/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt index 8efd1867e9..86795e93ba 100644 --- a/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt +++ b/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt @@ -241,7 +241,7 @@ open class Win32KmlGlContext(window: Any? = null, parent: KmlGlContext? = null) } // http://renderingpipeline.com/2012/05/windowless-opengl/ -open class X11KmlGlContext(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGL(), parent) { +open class X11KmlGlContext(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGl(), parent) { init { val display = X.XOpenDisplay(null) //X.glXChooseFBConfig() @@ -264,7 +264,7 @@ open class X11KmlGlContext(window: Any? = null, parent: KmlGlContext? = null) : } // https://forums.developer.nvidia.com/t/egl-without-x11/58733 -open class EGLKmlGlContext(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGL(), parent) { +open class EGLKmlGlContext(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGl(), parent) { val display: Pointer? = run { EGL.eglGetDisplay(0).also { if (it == null) error("Can't get EGL main display. Try setting env DISPLAY=:0 ?") @@ -371,7 +371,7 @@ private interface CoreGraphics : Library { companion object : CoreGraphics by NativeLoad("/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics") } -open class MacKmlGlContextManaged(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGL(), parent) { +open class MacKmlGlContextManaged(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGl(), parent) { val glCtx: MacosGLContext = MacosGLContext(sharedContext = (parent as MacKmlGlContextManaged?)?.glCtx?.openGLContext ?: 0L) override fun set() = glCtx.makeCurrent() @@ -387,7 +387,7 @@ open class MacKmlGlContextManaged(window: Any? = null, parent: KmlGlContext? = n // http://renderingpipeline.com/2012/05/windowless-opengl-on-macos-x/ -open class MacKmlGlContextRaw(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGL(), parent) { +open class MacKmlGlContextRaw(window: Any? = null, parent: KmlGlContext? = null) : KmlGlContext(window, MacKmlGl(), parent) { var ctx: com.sun.jna.Pointer? = run { val ctx = Memory(8L).also { it.clear() } // void** checkError("CGLCreateContext", MacGL.CGLCreateContext(pix, (parent as? MacKmlGlContextRaw)?.ctx, ctx)) diff --git a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt index cec1969617..0c8ef1a65e 100644 --- a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt +++ b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt @@ -8,7 +8,6 @@ import korlibs.kgl.* import korlibs.korge.* import korlibs.korge.view.* import korlibs.math.geom.* -import korlibs.render.awt.* import kotlinx.coroutines.* internal fun createKmlGlContext(fboWidth: Int, fboHeight: Int): KmlGlContext { @@ -45,6 +44,7 @@ internal fun createKmlGlContext(fboWidth: Int, fboHeight: Int): KmlGlContext { } fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: Boolean = false, callback: suspend CoroutineScope.(ag: AG) -> Unit) = suspendTest { + /* val fboWidth = fboSize.width.toInt() val fboHeight = fboSize.height.toInt() @@ -61,6 +61,9 @@ fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: B ag.contextsToFree.forEach { it?.unset(); it?.close() } ag.contextsToFree.clear() } + + */ + TODO() } class OffscreenContext(val testClassName: String, val testMethodName: String) { diff --git a/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt b/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt index bc4f11550f..f0e984bd7d 100644 --- a/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt +++ b/korge/src/jvm/korlibs/render/DefaultGameWindowJvm.kt @@ -3,7 +3,7 @@ package korlibs.render import korlibs.graphics.* import korlibs.render.awt.* -actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = NewAwtGameWindow(config) +actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = AwtGameWindow(config) object JvmAGFactory : AGFactory { override val supportsNativeFrame: Boolean = true diff --git a/korge/src/jvm/korlibs/render/awt/AwtAg.kt b/korge/src/jvm/korlibs/render/awt/AGOpenglAWT.kt similarity index 56% rename from korge/src/jvm/korlibs/render/awt/AwtAg.kt rename to korge/src/jvm/korlibs/render/awt/AGOpenglAWT.kt index da6d134374..9c0595ed19 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAg.kt +++ b/korge/src/jvm/korlibs/render/awt/AGOpenglAWT.kt @@ -1,24 +1,19 @@ package korlibs.render.awt +import korlibs.graphics.gl.* import korlibs.kgl.* -import korlibs.memory.* -import korlibs.graphics.gl.AGOpengl import korlibs.platform.* import korlibs.render.* -import korlibs.render.osx.MacKmlGL -import korlibs.render.win32.Win32KmlGl -import korlibs.render.x11.X11KmlGl +import korlibs.render.osx.* +import korlibs.render.win32.* +import korlibs.render.x11.* fun AGOpenglAWT(config: GameWindowCreationConfig, context: KmlGlContext? = null): AGOpengl = AGOpenglAWT(config.checkGl, config.logGl, config.cacheGl, context) fun AGOpenglAWT(checkGl: Boolean = false, logGl: Boolean = false, cacheGl: Boolean = false, context: KmlGlContext? = null): AGOpengl = AGOpengl( when { - //OS.isMac -> MacKmlGL.checked(throwException = false) - Platform.isMac -> MacKmlGL() + Platform.isMac -> MacKmlGl() Platform.isWindows -> Win32KmlGl() else -> X11KmlGl() - } - .checkedIf(checkGl) - .cachedIf(cacheGl) - .logIf(logGl, logBefore = false, logAfter = logGl), + }.checkedIf(checkGl).cachedIf(cacheGl).logIf(logGl, logBefore = false, logAfter = logGl), context ) diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index def0cdb942..39a29ee048 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -21,11 +21,33 @@ import javax.swing.* // https://www.oracle.com/java/technologies/painting.html // https://docs.oracle.com/javase/tutorial/extra/fullscreen/rendering.html // https://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html -open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { - init { - System.setProperty("sun.java2d.opengl", "true") - } - +//open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { +open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setProperty("sun.java2d.opengl", "true") }), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { + val canvas = object : Canvas() { + override fun paint(g: Graphics) { + counter.add() + //super.paint(g) + if (ctx == null) { + contextLost() + ctx = glContextFromComponent(this, GameWindowConfig.Impl()) + if (ctx!!.supportsSwapInterval()) { + ctx?.swapInterval(1) + } + } + //g.fillRect(0, 0, 100, 100) + val ctx = this@AwtAGOpenglCanvas.ctx + if (ctx != null) { + if (autoRepaint && ctx.supportsSwapInterval()) { + SwingUtilities.invokeLater { requestFrame() } + } + if (isUsingMetalPipeline) { + println("!!! ERROR: Using Metal pipeline ${this::class} won't work") + } + //println("CTX: $ctx") + ctx.useContext(g, ag, paintInContextDelegate) + } + } + }.also { layout = GridLayout(1, 1) }.also { add(it) } //override val ag: AGOpengl = AGOpenglAWT(checkGl = true, logGl = true) val ag: AG = AGOpenglAWT() @@ -42,34 +64,6 @@ open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base() contextLost = true } - override fun paint(g: Graphics) { - counter.add() - //super.paint(g) - if (ctx == null) { - contextLost() - ctx = glContextFromComponent(this, GameWindowConfig.Impl()) - if (ctx!!.supportsSwapInterval()) { - ctx?.swapInterval(1) - } - } - //g.fillRect(0, 0, 100, 100) - val ctx = this.ctx - if (ctx != null) { - if (autoRepaint && ctx.supportsSwapInterval()) { - SwingUtilities.invokeLater { requestFrame() } - } - if (isUsingMetalPipeline) { - println("!!! ERROR: Using Metal pipeline ${this::class} won't work") - } - //println("CTX: $ctx") - ctx.useContext(g, ag, paintInContextDelegate) - } - } - - fun requestFrame() { - repaint() - } - private val dl = if (!Platform.isMac) null else OSXDisplayLink { if (autoRepaint && ctx?.supportsSwapInterval() != true) { requestFrame() @@ -78,6 +72,11 @@ open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base() //println("FRAME!") } + fun requestFrame() { + canvas.repaint() + //repaint() + } + init { addHierarchyListener { if (dl == null) return@addHierarchyListener diff --git a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt index 116f8446df..a531ecd067 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt @@ -2,6 +2,7 @@ package korlibs.render.awt import korlibs.datastructure.* import korlibs.event.* +import korlibs.event.MouseEvent import korlibs.graphics.* import korlibs.image.awt.* import korlibs.io.dynamic.* @@ -9,17 +10,23 @@ import korlibs.io.file.* import korlibs.io.file.std.* import korlibs.math.* import korlibs.math.geom.* +import korlibs.memory.* import korlibs.memory.dyn.osx.* import korlibs.platform.* import korlibs.render.* +import korlibs.render.MenuItem +import korlibs.render.platform.* import java.awt.* import java.awt.Point import java.awt.datatransfer.* import java.awt.dnd.* +import java.awt.event.* import java.awt.image.* import java.io.* import javax.imageio.* import javax.swing.* +import java.awt.event.KeyEvent as JKeyEvent +import java.awt.event.MouseEvent as JMouseEvent object AwtFrameTools @@ -321,7 +328,7 @@ fun Component.registerGestureListeners(dispatcher: GameWindow) { } } -fun GameWindowMenuItem?.toJMenuItem(): JComponent { +fun MenuItem?.toJMenuItem(): JComponent { val item = this return when { item?.text == null -> JSeparator() @@ -341,3 +348,163 @@ fun GameWindowMenuItem?.toJMenuItem(): JComponent { } } +fun Component.registerMouseEvents(gameWindow: GameWindow) { + val contentComponent = this + // Here all the robot events have been already processed, so they won't be processed + //println("END ROBOT2") + + var lastMouseX: Int = 0 + var lastMouseY: Int = 0 + var lockingX: Int = 0 + var lockingY: Int = 0 + var locking = false + var mouseX: Int = 0 + var mouseY: Int = 0 + + gameWindow.events.mouseEvent.requestLock = { + val location = MouseInfo.getPointerInfo().location + lockingX = location.x + lockingY = location.y + locking = true + } + + fun handleMouseEvent(e: JMouseEvent) { + val factor = getDisplayScalingFactor(contentComponent) + + val ev = when (e.id) { + JMouseEvent.MOUSE_DRAGGED -> MouseEvent.Type.DRAG + JMouseEvent.MOUSE_MOVED -> MouseEvent.Type.MOVE + JMouseEvent.MOUSE_CLICKED -> MouseEvent.Type.CLICK + JMouseEvent.MOUSE_PRESSED -> MouseEvent.Type.DOWN + JMouseEvent.MOUSE_RELEASED -> MouseEvent.Type.UP + JMouseEvent.MOUSE_ENTERED -> MouseEvent.Type.ENTER + JMouseEvent.MOUSE_EXITED -> MouseEvent.Type.EXIT + else -> MouseEvent.Type.MOVE + } + + //println("MOUSE EVENT: $ev : ${e.button} : ${MouseButton[e.button - 1]}") + gameWindow.queue { + val button = if (e.button == 0) MouseButton.NONE else MouseButton[e.button - 1] + + if (locking) { + Robot().mouseMove(lockingX, lockingY) + if (ev == korlibs.event.MouseEvent.Type.UP) { + locking = false + } + } + + lastMouseX = e.x + lastMouseY = e.y + val sx = e.x * factor + val sy = e.y * factor + val modifiers = e.modifiersEx + mouseX = e.x + mouseY = e.y + //println("ev=$ev") + gameWindow.dispatchMouseEvent( + type = ev, + id = 0, + x = sx.toInt(), + y = sy.toInt(), + button = button, + buttons = 0, + scrollDeltaX = 0f, + scrollDeltaY = 0f, + scrollDeltaZ = 0f, + scrollDeltaMode = korlibs.event.MouseEvent.ScrollDeltaMode.PIXEL, + isShiftDown = modifiers hasFlags JMouseEvent.SHIFT_DOWN_MASK, + isCtrlDown = modifiers hasFlags JMouseEvent.CTRL_DOWN_MASK, + isAltDown = modifiers hasFlags JMouseEvent.ALT_DOWN_MASK, + isMetaDown = modifiers hasFlags JMouseEvent.META_DOWN_MASK, + scaleCoords = false, + simulateClickOnUp = false + ) + } + } + + fun handleMouseWheelEvent(e: MouseWheelEvent) { + val factor = getDisplayScalingFactor(contentComponent) + + gameWindow.queue { + val ev = korlibs.event.MouseEvent.Type.SCROLL + val button = MouseButton[8] + val factor = factor + val sx = e.x * factor + val sy = e.y * factor + val modifiers = e.modifiersEx + //TODO: check this on linux and macos + //val scrollDelta = e.scrollAmount * e.preciseWheelRotation // * e.unitsToScroll + val osfactor = when { + Platform.isMac -> 0.25f + else -> 1.0f + } + val scrollDelta = e.preciseWheelRotation.toFloat() * osfactor + + val isShiftDown = modifiers hasFlags JMouseEvent.SHIFT_DOWN_MASK + gameWindow.dispatchMouseEvent( + type = ev, + id = 0, + x = sx.toInt(), + y = sy.toInt(), + button = button, + buttons = 0, + scrollDeltaX = if (isShiftDown) scrollDelta else 0f, + scrollDeltaY = if (isShiftDown) 0f else scrollDelta, + scrollDeltaZ = 0f, + scrollDeltaMode = korlibs.event.MouseEvent.ScrollDeltaMode.PIXEL, + isShiftDown = isShiftDown, + isCtrlDown = modifiers hasFlags JMouseEvent.CTRL_DOWN_MASK, + isAltDown = modifiers hasFlags JMouseEvent.ALT_DOWN_MASK, + isMetaDown = modifiers hasFlags JMouseEvent.META_DOWN_MASK, + scaleCoords = false, + simulateClickOnUp = false + ) + } + } + + contentComponent.addMouseMotionListener(object : MouseMotionAdapter() { + override fun mouseMoved(e: JMouseEvent) = handleMouseEvent(e) + override fun mouseDragged(e: JMouseEvent) = handleMouseEvent(e) + }) + + contentComponent.addMouseListener(object : MouseAdapter() { + override fun mouseReleased(e: JMouseEvent) = handleMouseEvent(e) + override fun mouseMoved(e: JMouseEvent) = handleMouseEvent(e) + override fun mouseEntered(e: JMouseEvent) = handleMouseEvent(e) + override fun mouseDragged(e: JMouseEvent) = handleMouseEvent(e) + override fun mouseClicked(e: JMouseEvent) = handleMouseEvent(e) + override fun mouseExited(e: JMouseEvent) = handleMouseEvent(e) + override fun mousePressed(e: JMouseEvent) = handleMouseEvent(e) + }) + + contentComponent.addMouseWheelListener { e -> handleMouseWheelEvent(e) } +} + +fun Component.registerKeyEvents(gameWindow: GameWindow) { + val component = this + + fun handleKeyEvent(e: JKeyEvent) { + gameWindow.queue { + val ev = when (e.id) { + JKeyEvent.KEY_TYPED -> korlibs.event.KeyEvent.Type.TYPE + JKeyEvent.KEY_PRESSED -> korlibs.event.KeyEvent.Type.DOWN + JKeyEvent.KEY_RELEASED -> korlibs.event.KeyEvent.Type.UP + else -> korlibs.event.KeyEvent.Type.TYPE + } + val id = 0 + val char = e.keyChar + val keyCode = e.keyCode + val key = awtKeyCodeToKey(e.keyCode) + + gameWindow.dispatchKeyEventEx(ev, id, char, key, keyCode, e.isShiftDown, e.isControlDown, e.isAltDown, e.isMetaDown) + } + } + + component.focusTraversalKeysEnabled = false + component.addKeyListener(object : KeyAdapter() { + override fun keyTyped(e: JKeyEvent) = handleKeyEvent(e) + override fun keyPressed(e: JKeyEvent) = handleKeyEvent(e) + override fun keyReleased(e: JKeyEvent) = handleKeyEvent(e) + }) + +} diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 352da6cfed..636bd193c6 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -1,67 +1,178 @@ package korlibs.render.awt -/* +import korlibs.datastructure.* +import korlibs.datastructure.thread.* +import korlibs.graphics.* import korlibs.image.awt.* import korlibs.image.bitmap.* -import korlibs.math.* -import korlibs.math.geom.* +import korlibs.image.color.* import korlibs.render.* -import korlibs.render.platform.* +import korlibs.render.MenuItem +import korlibs.time.* +import kotlinx.coroutines.* import java.awt.* +import java.awt.datatransfer.* import java.awt.event.* import javax.swing.* +import kotlin.concurrent.* + +val AwtAGOpenglCanvas.gameWindow: AwtCanvasGameWindow by Extra.PropertyThis { AwtCanvasGameWindow(this) } + +open class AwtCanvasGameWindow( + val canvas: AwtAGOpenglCanvas +) : GameWindow(), ClipboardOwner { + override val devicePixelRatio: Double by TimedCache(ttl = .1.seconds) { canvas.devicePixelRatio } + override val pixelsPerInch: Double by TimedCache(ttl = .1.seconds) { canvas.pixelsPerInch } + override val pixelsPerLogicalInchRatio: Double by TimedCache(ttl = .1.seconds) { pixelsPerInch / AG.defaultPixelsPerInch } + + override val dialogInterface: DialogInterface = DialogInterfaceAwt { canvas } + override val ag: AG get() = canvas.ag + private var _window: Window? = null + open val window: Window? get() { + if (_window == null) { + _window = SwingUtilities.getWindowAncestor(canvas) + } + return _window + } + + val thread = nativeThread(name = "NewAwtGameWindow") { + eventLoop.runTasksForever() + } + + override val width: Int get() = canvas.width + override val height: Int get() = canvas.height + override var backgroundColor: RGBA + get() = canvas.background.toRgba() + set(value) { canvas.background = value.toAwt() } + + override var cursor: ICursor = Cursor.DEFAULT + set(value) { + if (field == value) return + field = value + canvas.cursor = value.jvmCursor + } + + init { + onUpdateEvent { + DesktopGamepadUpdater.updateGamepads(this) + } + canvas.doRender = { + //it.clear(it.mainFrameBuffer, Colors.RED) + dispatchNewRenderEvent() + } + canvas.canvas.registerMouseEvents(this) + canvas.canvas.registerKeyEvents(this) + canvas.registerGestureListeners(this) + } + + override fun showContextMenu(items: List) { + val popupMenu = JPopupMenu() + for (item in items) { + popupMenu.add(item.toJMenuItem()) + } + //println("showContextMenu: $items") + popupMenu.setLightWeightPopupEnabled(false) + popupMenu.show(canvas, canvas.mousePosition.x, canvas.mousePosition.y) + } + + override fun close(exitCode: Int) { + super.close(exitCode) + coroutineDispatcher.close() + } + + override fun lostOwnership(clipboard: Clipboard?, contents: Transferable?) { + } + + override val hapticFeedbackGenerateSupport: Boolean get() = true + override fun hapticFeedbackGenerate(kind: HapticFeedbackKind) = canvas.hapticFeedbackGenerate(kind) -class AwtGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig.DEFAULT) : BaseAwtGameWindow(AGOpenglAWT(config)) { - override var ctx: BaseOpenglContext? = null + val clipboard: Clipboard by lazy { Toolkit.getDefaultToolkit().systemClipboard } + + override suspend fun clipboardWrite(data: ClipboardData) { + awtEventQueueLater { + when (data) { + is TextClipboardData -> { + clipboard.setContents(StringSelection(data.text), this) + } + } + } + } + + override suspend fun clipboardRead(): ClipboardData? { + return awtEventQueueLater { + val str = clipboard.getData(DataFlavor.stringFlavor) as? String? + str?.let { TextClipboardData(it) } + } + } - override fun ensureContext() { - if (ctx == null) ctx = glContextFromComponent(frame, this) + suspend fun awtEventQueueLater(block: () -> T): T { + val deferred = CompletableDeferred() + EventQueue.invokeLater { + deferred.completeWith(runCatching(block)) + } + return deferred.await() } - val frame: JFrame = object : JFrame("Korgw") { - val frame = this + override fun computeDisplayRefreshRate(): Int { + return window?.getScreenDevice()?.cachedRefreshRate?.takeIf { it > 0 } ?: 60 + } +} +class AwtGameWindow( + val config: GameWindowCreationConfig = GameWindowCreationConfig() +) : AwtCanvasGameWindow(run { + System.setProperty("apple.laf.useScreenMenuBar", "true") + System.setProperty("apple.awt.application.name", config.title) + System.setProperty("com.apple.mrj.application.apple.menu.about.name", config.title) + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) + AwtAGOpenglCanvas() +}) { + val frame = object : JFrame() { init { isVisible = false ignoreRepaint = true - //background = Color.black - setBounds(0, 0, 640, 480) - frame.setLocationRelativeTo(null) - frame.setKorgeDropTarget(this@AwtGameWindow) - frame.setIconIncludingTaskbarFromResource("@appicon.png") - this.initTools() - + defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE + contentPane.layout = GridLayout(1, 1) + contentPane.add(canvas) + preferredSize = Dimension(640, 480) + //setBounds(0, 0, 640, 480) + pack() + setLocationRelativeTo(null) addWindowListener(object : WindowAdapter() { - override fun windowClosing(e: WindowEvent?) { - debugFrame.isVisible = false - debugFrame.dispose() - running = false - } - }) - addComponentListener(object : ComponentAdapter() { - override fun componentMoved(e: ComponentEvent?) { - synchronizeDebugFrameCoordinates() - } - - override fun componentResized(e: ComponentEvent?) { - synchronizeDebugFrameCoordinates() + override fun windowClosed(e: WindowEvent) { + this@AwtGameWindow.close() } }) + initTools() } - - override fun paintComponents(g: Graphics?) = Unit - - override fun paint(g: Graphics) { - try { - framePaint(g) - } catch (e: Throwable) { - e.printStackTrace() + override fun paintComponents(g: Graphics?) { + } + } + override val window: Window get() = frame + + override fun setMainMenu(items: List) { + //Dispatchers.Unconfined.launchUnscoped { + thread { + val bar = JMenuBar() + for (item in items) { + val mit = item.toJMenuItem() + if (mit is JMenu) bar.add(mit) } + frame.jMenuBar = bar + frame.doLayout() + frame.repaint() + println("GameWindow.setMainMenu: component=$frame, bar=$bar") } } override var alwaysOnTop: Boolean by frame::_isAlwaysOnTop - override var title: String by frame::title + override var title: String + get() = frame.title + set(value) { + frame.title = value + System.setProperty("apple.awt.application.name", value) + } + override var visible: Boolean by frame::visible override var icon: Bitmap? = null set(value) { field = value @@ -69,51 +180,23 @@ class AwtGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig. } override var fullscreen: Boolean by frame::isFullScreen - val debugFrame = JFrame("Debug").apply { + override var backgroundColor: RGBA + get() = frame.background.toRgba() + set(value) { + frame.background = value.toAwt() + canvas.background = value.toAwt() + canvas.canvas.background = value.toAwt() + } + + override fun close(exitCode: Int) { try { - this.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE - this.setSize(280, 256) - this.type = Window.Type.UTILITY - this.isAlwaysOnTop = true + super.close(exitCode) } catch (e: Throwable) { e.printStackTrace() } - val debugFrame = this - this@AwtGameWindow.onDebugChanged.add { - EventQueue.invokeLater { - debugFrame.isVisible = it - synchronizeDebugFrameCoordinates() - if (debugFrame.isVisible) { - //frame.isVisible = false - frame.isVisible = true - } - } + println("exitProcessOnClose=$exitProcessOnClose") + if (exitProcessOnClose) { + System.exit(exitCode) } } - - override val debugComponent: Any? = debugFrame - - private fun synchronizeDebugFrameCoordinates() { - val displayMode = frame.getScreenDevice().displayMode - //println("frame.location=${frame.location}, frame.size=${frame.size}, debugFrame.width=${debugFrame.width}, displayMode=${displayMode.width}x${displayMode.height}") - val frameBounds = RectangleInt(frame.location.x, frame.location.y, frame.size.width, frame.size.height) - debugFrame.setLocation(frameBounds.right.clamp(0, (displayMode.width - debugFrame.width * 1.0).toInt()), frameBounds.top) - debugFrame.setSize(debugFrame.width.coerceAtLeast(64), frameBounds.height) - } - - override fun setSize(width: Int, height: Int) { - contentComponent.setSize(width, height) - contentComponent.preferredSize = Dimension(width, height) - frame.pack() - val component = this.component - if (component is Window) component.setLocationRelativeTo(null) - } - - override val component: Component get() = frame - override val contentComponent: Component get() = frame.contentPane - - override fun frameDispose() { - frame.dispose() - } } -*/ diff --git a/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt deleted file mode 100644 index a363d657f3..0000000000 --- a/korge/src/jvm/korlibs/render/awt/BaseAwtGameWindow.kt +++ /dev/null @@ -1,576 +0,0 @@ -package korlibs.render.awt - -/* -import korlibs.datastructure.* -import korlibs.event.* -import korlibs.graphics.* -import korlibs.graphics.gl.* -import korlibs.image.color.* -import korlibs.io.async.* -import korlibs.kgl.* -import korlibs.memory.* -import korlibs.platform.* -import korlibs.render.* -import korlibs.render.osx.* -import korlibs.render.platform.* -import korlibs.render.win32.* -import korlibs.render.x11.* -import korlibs.time.* -import kotlinx.coroutines.* -import java.awt.* -import java.awt.datatransfer.* -import java.awt.event.* -import java.awt.event.KeyEvent -import java.awt.event.MouseEvent -import javax.swing.* -import kotlin.system.* - -abstract class BaseAwtGameWindow( - override val ag: AGOpengl -) : GameWindow(), ClipboardOwner { - override val devicePixelRatio: Double get() = component.devicePixelRatio - override val pixelsPerInch: Double get() = component.pixelsPerInch - override val pixelsPerLogicalInchRatio: Double by lazy(LazyThreadSafetyMode.PUBLICATION) { - pixelsPerInch / AG.defaultPixelsPerInch - } - - //val fvsync get() = vsync - val fvsync get() = false - open val ctx: BaseOpenglContext? = null - - abstract val component: Component - abstract val contentComponent: Component - private var lastFactor = 0f - - override val dialogInterface: DialogInterface = DialogInterfaceAwt { component } - - private var _window: Window? = null - val window: Window? get() { - if (_window == null) { - _window = SwingUtilities.getWindowAncestor(component) ?: (component as? Window?) - } - return _window - } - val windowOrComponent get() = window ?: component - - override var cursor: ICursor = Cursor.DEFAULT - set(value) { - if (field == value) return - field = value - component.cursor = value.jvmCursor - } - - override fun setMainMenu(items: List) { - val component = this.component - if (component !is JFrame) { - println("GameWindow.setMainMenu: component=$component") - return - } - - val bar = JMenuBar() - for (item in items) { - val mit = item.toJMenuItem() - if (mit is JMenu) { - bar.add(mit) - } - } - component.jMenuBar = bar - component.doLayout() - component.repaint() - println("GameWindow.setMainMenu: component=$component, bar=$bar") - } - - override fun showContextMenu(items: List) { - val popupMenu = JPopupMenu() - for (item in items) { - popupMenu.add(item.toJMenuItem()) - } - //println("showContextMenu: $items") - popupMenu.setLightWeightPopupEnabled(false) - popupMenu.show(contentComponent, mouseX, mouseY) - } - - protected open fun ensureContext() { - } - - fun framePaint(g: Graphics) { - //println("framePaint") - - if (fvsync) { - EventQueue.invokeLater { - //println("repaint!") - component.repaint() - } - } - - ensureContext() - - ctx?.useContext(g, ag, paintInContextDelegate) - //Toolkit.getDefaultToolkit().sync(); - } - - val paintInContextDelegate: (Graphics, BaseOpenglContext.ContextInfo) -> Unit = { g, info -> - paintInContext(g, info) - } - - val state: KmlGlState by lazy { ag.createGlState() } - - fun paintInContext(g: Graphics, info: BaseOpenglContext.ContextInfo) { - // We only have to keep the state on mac, in linux and windows, the context/state is not shared - if (Platform.isMac) { - state.keep { - paintInContextInternal(g, info) - } - } else { - paintInContextInternal(g, info) - } - } - - fun paintInContextInternal(g: Graphics, info: BaseOpenglContext.ContextInfo) { - run { - //run { - ctx?.swapInterval(1) - - val g = g as Graphics2D - val gl = ag.gl - val factor = frameScaleFactor - if (lastFactor != factor) { - lastFactor = factor - reshaped = true - } - - //println("RENDER[1]") - - val viewport = info.viewport - val scissor = info.scissors - - if (component is JFrame) { - //println("component.width: ${contentComponent.width}x${contentComponent.height}") - ag.mainFrameBuffer.setSize( - 0, 0, (contentComponent.width * factor).toInt(), (contentComponent.height * factor).toInt(), - ) - } else { - ag.mainFrameBuffer.scissor(scissor) - if (viewport != null) { - //val window = SwingUtilities.getWindowAncestor(contentComponent) - //println("window=${window.width}x${window.height} : factor=$factor") - - val frameOrComponent = (window as? JFrame)?.contentPane ?: windowOrComponent - - ag.mainFrameBuffer.setSize( - viewport.x, viewport.y, viewport.width, viewport.height, - (frameOrComponent.width * factor).toInt(), - (frameOrComponent.height * factor).toInt(), - ) - } else { - ag.mainFrameBuffer.setSize( - 0, 0, (component.width * factor).toInt(), (component.height * factor).toInt(), - ) - } - } - - //println(gl.getString(gl.VERSION)) - //println(gl.versionString) - if (reshaped) { - reshaped = false - //println("RESHAPED!") - dispatchReshapeEventEx( - ag.mainFrameBuffer.x, - ag.mainFrameBuffer.y, - ag.mainFrameBuffer.width, - ag.mainFrameBuffer.height, - ag.mainFrameBuffer.fullWidth, - ag.mainFrameBuffer.fullHeight, - ) - } - - //gl.clearColor(1f, 1f, 1f, 1f) - //gl.clear(gl.COLOR_BUFFER_BIT) - var gamePadTime: TimeSpan = 0.milliseconds - var frameTime: TimeSpan = 0.milliseconds - var finishTime: TimeSpan = 0.milliseconds - val totalTime = measureTime { - frameTime = measureTime { - frame() - } - finishTime = measureTime { - gl.flush() - gl.finish() - } - } - - //println("totalTime=$totalTime, gamePadTime=$gamePadTime, finishTime=$finishTime, frameTime=$frameTime, timedTasksTime=${coroutineDispatcher.timedTasksTime}, tasksTime=${coroutineDispatcher.tasksTime}, renderTime=${renderTime}, updateTime=${updateTime}") - } - } - - override fun updateGamepads() { - when { - Platform.isWindows -> xinputEventAdapter.updateGamepads(this.gamepadEmitter) - Platform.isLinux -> linuxJoyEventAdapter.updateGamepads(this.gamepadEmitter) - Platform.isMac -> macosGamepadEventAdapter.updateGamepads(this) - else -> Unit //println("undetected OS: ${OS.rawName}") - } - } - - private val xinputEventAdapter by lazy { XInputGamepadEventAdapter() } - private val linuxJoyEventAdapter by lazy { LinuxJoyEventAdapter() } - private val macosGamepadEventAdapter by lazy { MacosGamepadEventAdapter() } - - val frameScaleFactor: Float get() = getDisplayScalingFactor(component) - - val nonScaledWidth get() = contentComponent.width.toDouble() - val nonScaledHeight get() = contentComponent.height.toDouble() - - val scaledWidth get() = contentComponent.width * frameScaleFactor - val scaledHeight get() = contentComponent.height * frameScaleFactor - - override val width: Int get() = (scaledWidth).toInt() - override val height: Int get() = (scaledHeight).toInt() - override var visible: Boolean by LazyDelegate { component::visible } - override var backgroundColor: RGBA - get() = component.background.toRgba() - set(value) { - component.background = value.toAwt() - } - override var quality: Quality = Quality.AUTOMATIC - - fun dispatchReshapeEvent() { - val factor = frameScaleFactor - dispatchReshapeEvent( - component.x, - component.y, - (contentComponent.width * factor).toInt().coerceAtLeast(1), - (contentComponent.height * factor).toInt().coerceAtLeast(1) - ) - } - - var displayLinkLock: java.lang.Object? = null - private val dl = OSXDisplayLink { - displayLinkLock?.let { displayLock -> - synchronized(displayLock) { - displayLock.notify() - } - } - } - - open fun frameDispose() { - } - - var reshaped = false - - protected var mouseX: Int = 0 - protected var mouseY: Int = 0 - - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - launchImmediately(getCoroutineDispatcherWithCurrentContext()) { - entry() - } - -//frame.setBounds(0, 0, width, height) - - //val timer= Timer(40, ActionListener { - //}) - - component.addComponentListener(object : ComponentAdapter() { - override fun componentResized(e: ComponentEvent) { - //window.invalidate() - //SwingUtilities.updateComponentTreeUI(window) - reshaped = true - //window.repaint() - queue { - //window.revalidate() - //println("repaint") - component.repaint() - //EventQueue.invokeLater { SwingUtilities.updateComponentTreeUI(window) } - } - } - }) - - var lastMouseX: Int = 0 - var lastMouseY: Int = 0 - var lockingX: Int = 0 - var lockingY: Int = 0 - var locking = false - - mouseEvent.requestLock = { - val location = MouseInfo.getPointerInfo().location - lockingX = location.x - lockingY = location.y - locking = true - } - - fun handleMouseEvent(e: MouseEvent) { - val ev = when (e.id) { - MouseEvent.MOUSE_DRAGGED -> korlibs.event.MouseEvent.Type.DRAG - MouseEvent.MOUSE_MOVED -> korlibs.event.MouseEvent.Type.MOVE - MouseEvent.MOUSE_CLICKED -> korlibs.event.MouseEvent.Type.CLICK - MouseEvent.MOUSE_PRESSED -> korlibs.event.MouseEvent.Type.DOWN - MouseEvent.MOUSE_RELEASED -> korlibs.event.MouseEvent.Type.UP - MouseEvent.MOUSE_ENTERED -> korlibs.event.MouseEvent.Type.ENTER - MouseEvent.MOUSE_EXITED -> korlibs.event.MouseEvent.Type.EXIT - else -> korlibs.event.MouseEvent.Type.MOVE - } - //println("MOUSE EVENT: $ev : ${e.button} : ${MouseButton[e.button - 1]}") - queue { - val button = if (e.button == 0) MouseButton.NONE else MouseButton[e.button - 1] - val factor = frameScaleFactor - - if (locking) { - Robot().mouseMove(lockingX, lockingY) - if (ev == korlibs.event.MouseEvent.Type.UP) { - locking = false - } - } - - lastMouseX = e.x - lastMouseY = e.y - val sx = e.x * factor - val sy = e.y * factor - val modifiers = e.modifiersEx - mouseX = e.x - mouseY = e.y - dispatchMouseEvent( - type = ev, - id = 0, - x = sx.toInt(), - y = sy.toInt(), - button = button, - buttons = 0, - scrollDeltaX = 0f, - scrollDeltaY = 0f, - scrollDeltaZ = 0f, - scrollDeltaMode = korlibs.event.MouseEvent.ScrollDeltaMode.PIXEL, - isShiftDown = modifiers hasFlags MouseEvent.SHIFT_DOWN_MASK, - isCtrlDown = modifiers hasFlags MouseEvent.CTRL_DOWN_MASK, - isAltDown = modifiers hasFlags MouseEvent.ALT_DOWN_MASK, - isMetaDown = modifiers hasFlags MouseEvent.META_DOWN_MASK, - scaleCoords = false, - simulateClickOnUp = false - ) - } - } - - fun handleMouseWheelEvent(e: MouseWheelEvent) { - queue { - val ev = korlibs.event.MouseEvent.Type.SCROLL - val button = MouseButton[8] - val factor = frameScaleFactor - val sx = e.x * factor - val sy = e.y * factor - val modifiers = e.modifiersEx - //TODO: check this on linux and macos - //val scrollDelta = e.scrollAmount * e.preciseWheelRotation // * e.unitsToScroll - val osfactor = when { - Platform.isMac -> 0.25f - else -> 1.0f - } - val scrollDelta = e.preciseWheelRotation.toFloat() * osfactor - - val isShiftDown = modifiers hasFlags MouseEvent.SHIFT_DOWN_MASK - dispatchMouseEvent( - type = ev, - id = 0, - x = sx.toInt(), - y = sy.toInt(), - button = button, - buttons = 0, - scrollDeltaX = if (isShiftDown) scrollDelta else 0f, - scrollDeltaY = if (isShiftDown) 0f else scrollDelta, - scrollDeltaZ = 0f, - scrollDeltaMode = korlibs.event.MouseEvent.ScrollDeltaMode.PIXEL, - isShiftDown = isShiftDown, - isCtrlDown = modifiers hasFlags MouseEvent.CTRL_DOWN_MASK, - isAltDown = modifiers hasFlags MouseEvent.ALT_DOWN_MASK, - isMetaDown = modifiers hasFlags MouseEvent.META_DOWN_MASK, - scaleCoords = false, - simulateClickOnUp = false - ) - } - } - - fun handleKeyEvent(e: KeyEvent) { - queue { - val ev = when (e.id) { - KeyEvent.KEY_TYPED -> korlibs.event.KeyEvent.Type.TYPE - KeyEvent.KEY_PRESSED -> korlibs.event.KeyEvent.Type.DOWN - KeyEvent.KEY_RELEASED -> korlibs.event.KeyEvent.Type.UP - else -> korlibs.event.KeyEvent.Type.TYPE - } - val id = 0 - val char = e.keyChar - val keyCode = e.keyCode - val key = awtKeyCodeToKey(e.keyCode) - - dispatchKeyEventEx(ev, id, char, key, keyCode, e.isShiftDown, e.isControlDown, e.isAltDown, e.isMetaDown) - } - } - - component.addMouseWheelListener { e -> handleMouseWheelEvent(e) } - - component.focusTraversalKeysEnabled = false - component.addKeyListener(object : KeyAdapter() { - override fun keyTyped(e: KeyEvent) = handleKeyEvent(e) - override fun keyPressed(e: KeyEvent) = handleKeyEvent(e) - override fun keyReleased(e: KeyEvent) = handleKeyEvent(e) - }) - - queue { - dispatchInitEvent() - dispatchReshapeEvent() - } - EventQueue.invokeLater { - component.isVisible = true - component.repaint() - //fullscreen = true - - // keys.up(Key.ENTER) { if (it.alt) gameWindow.toggleFullScreen() } - - // @TODO: HACK so the windows grabs focus on Windows 10 when launching on gradle daemon - val useRobotHack = Platform.isWindows - - if (useRobotHack) { - (component as? Frame?)?.apply { - val frame = this - val insets = frame.insets - frame.isAlwaysOnTop = true - try { - val robot = Robot() - val pos = MouseInfo.getPointerInfo().location - val bounds = frame.bounds - bounds.setFrameFromDiagonal(bounds.minX + insets.left, bounds.minY + insets.top, bounds.maxX - insets.right, bounds.maxY - insets.bottom) - - //println("frame.bounds: ${frame.bounds}") - //println("frame.bounds: ${bounds}") - //println("frame.insets: ${insets}") - //println(frame.contentPane.bounds) - //println("START ROBOT") - robot.mouseMove(bounds.centerX.toInt(), bounds.centerY.toInt()) - robot.mousePress(InputEvent.BUTTON3_MASK) - robot.mouseRelease(InputEvent.BUTTON3_MASK) - robot.mouseMove(pos.x, pos.y) - //println("END ROBOT") - } catch (e: Throwable) { - } - frame.isAlwaysOnTop = false - } - } - - EventQueue.invokeLater { - // Here all the robot events have been already processed so they won't be processed - //println("END ROBOT2") - - contentComponent.addMouseMotionListener(object : MouseMotionAdapter() { - override fun mouseMoved(e: MouseEvent) = handleMouseEvent(e) - override fun mouseDragged(e: MouseEvent) = handleMouseEvent(e) - }) - - contentComponent.addMouseListener(object : MouseAdapter() { - override fun mouseReleased(e: MouseEvent) = handleMouseEvent(e) - override fun mouseMoved(e: MouseEvent) = handleMouseEvent(e) - override fun mouseEntered(e: MouseEvent) = handleMouseEvent(e) - override fun mouseDragged(e: MouseEvent) = handleMouseEvent(e) - override fun mouseClicked(e: MouseEvent) = handleMouseEvent(e) - override fun mouseExited(e: MouseEvent) = handleMouseEvent(e) - override fun mousePressed(e: MouseEvent) = handleMouseEvent(e) - }) - } - } - - if (Platform.isMac) dl.start() - val displayLock = this.displayLinkLock - logger.info { if (displayLock != null) "Using DisplayLink" else "NOT Using DisplayLink" } - logger.info { "running: ${Thread.currentThread()}, fvsync=$fvsync" } - contentComponent.registerGestureListeners(this@BaseAwtGameWindow) - - while (running) { - if (fvsync) { - Thread.sleep(1L) - } else { - //println("running[a]") - component.repaint() - //executePending() - //println("running[b]") - when { - //ctx?.supportsSwapInterval() == true -> { - false -> { - //println("running[ba]") - Unit - } // Do nothing. Already waited for vsync - displayLock != null -> { - //println("running[bc]") - synchronized(displayLock) { displayLock.wait(100L) } - } - else -> { - //println("running[bb]") - val nanos = System.nanoTime() - val frameTimeNanos = (1.0 / fps.toDouble()).seconds.nanosecondsInt - val delayNanos = frameTimeNanos - (nanos % frameTimeNanos) - if (delayNanos > 0) { - //println(delayNanos / 1_000_000) - Thread.sleep(delayNanos / 1_000_000, (delayNanos % 1_000_000).toInt()) - } - //println("[2] currentFrameCount=$currentFrameCount, frameCount=$frameCount") - - //println(System.nanoTime()) - } - } - //println("running[c]") - //val end = PerformanceCounter.hr - //println((end - start).timeSpan) - } - } - logger.info { "completed.running=$running" } - //timer.stop() - - if (Platform.isMac) { - dl.stop() - } - - dispatchDestroyEvent() - - component.isVisible = false - frameDispose() - - if (exitProcessOnClose) { // Don't do this since we might continue in the e2e test - exitProcess(this.exitCode) - } - } - - override fun computeDisplayRefreshRate(): Int { - return window?.getScreenDevice()?.cachedRefreshRate?.takeIf { it > 0 } ?: 60 - } - - val clipboard: Clipboard by lazy { Toolkit.getDefaultToolkit().systemClipboard } - - suspend fun eventQueueLater(block: () -> T): T { - val deferred = CompletableDeferred() - EventQueue.invokeLater { - deferred.completeWith(runCatching(block)) - } - return deferred.await() - } - - override suspend fun clipboardWrite(data: ClipboardData) { - eventQueueLater { - when (data) { - is TextClipboardData -> { - clipboard.setContents(StringSelection(data.text), this) - } - } - } - } - - override suspend fun clipboardRead(): ClipboardData? { - return eventQueueLater { - val str = clipboard.getData(DataFlavor.stringFlavor) as? String? - str?.let { TextClipboardData(it) } - } - } - - override fun lostOwnership(clipboard: Clipboard?, contents: Transferable?) { - } - - override val hapticFeedbackGenerateSupport: Boolean get() = true - override fun hapticFeedbackGenerate(kind: HapticFeedbackKind) = component.hapticFeedbackGenerate(kind) -} -*/ diff --git a/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt b/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt new file mode 100644 index 0000000000..eabb538efe --- /dev/null +++ b/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt @@ -0,0 +1,20 @@ +package korlibs.render.awt + +import korlibs.event.gamepad.* +import korlibs.platform.* +import korlibs.render.* + +internal object DesktopGamepadUpdater { + fun updateGamepads(window: GameWindow) { + when { + Platform.isWindows -> xinputEventAdapter.updateGamepads(window.gamepadEmitter) + Platform.isLinux -> linuxJoyEventAdapter.updateGamepads(window.gamepadEmitter) + Platform.isMac -> macosGamepadEventAdapter.updateGamepads(window) + else -> Unit //println("undetected OS: ${OS.rawName}") + } + } + + private val xinputEventAdapter by lazy { XInputGamepadEventAdapter() } + private val linuxJoyEventAdapter by lazy { LinuxJoyEventAdapter() } + private val macosGamepadEventAdapter by lazy { MacosGamepadEventAdapter() } +} diff --git a/korge/src/jvm/korlibs/render/awt/GLCanvas.kt b/korge/src/jvm/korlibs/render/awt/GLCanvas.kt deleted file mode 100644 index 086acde15e..0000000000 --- a/korge/src/jvm/korlibs/render/awt/GLCanvas.kt +++ /dev/null @@ -1,101 +0,0 @@ -package korlibs.render.awt - -import korlibs.graphics.* -import korlibs.graphics.gl.* -import korlibs.kgl.* -import korlibs.render.* -import korlibs.render.platform.* -import java.awt.* -import java.io.* - -open class GLCanvas constructor(checkGl: Boolean = true, val logGl: Boolean = false, cacheGl: Boolean = false) : Canvas(), GameWindowConfig, Closeable { - val ag: AGOpengl = AGOpenglAWT(checkGl, logGl, cacheGl) - var ctx: BaseOpenglContext? = null - val gl = ag.gl - private var doContextLost = false - - override fun getGraphicsConfiguration(): GraphicsConfiguration? { - return super.getGraphicsConfiguration() - } - - override fun addNotify() { - super.addNotify() - close() - } - - override fun removeNotify() { - super.removeNotify() - close() - } - - override fun reshape(x: Int, y: Int, width: Int, height: Int) { - super.reshape(x, y, width, height) - repaint() - } - - override fun update(g: Graphics) { - paint(g) - } - - override fun paint(g: Graphics) { - //val componentId = Native.getComponentID(this) - //if (ctxComponentId != componentId) { - // close() - //} - if (logGl) { - //println("+++++++++++++++++++++++++++++") - } - if (ctx == null) { - //println("--------------------------------------") - //ctxComponentId = componentId - ctx = glContextFromComponent(this, this) - doContextLost = true - } - //println("--------------") - ctx?.useContext(g, ag) { g, info -> - if (doContextLost) { - doContextLost = false - ag.contextLost() - } - render(gl, g, info) - } - } - - - override fun close() { - ctx?.dispose() - ctx = null - } - - var defaultRendererAG: (ag: AG) -> Unit = { - } - - var defaultRenderer: (gl: KmlGl, g: Graphics) -> Unit = { gl, g -> - /* - ctx?.useContext(g, ag) { - gl.clearColor(0f, 0f, 0f, 1f) - gl.clear(gl.COLOR_BUFFER_BIT) - } - */ - } - - val getCurrent: () -> Any? = { ctx?.getCurrent() } - - open fun render(gl: KmlGl, g: Graphics, info: BaseOpenglContext.ContextInfo) { - //ctx?.makeCurrent() - gl.info.current = getCurrent - ag.startEndFrame { - val viewport = info.viewport - if (viewport != null) { - gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height) - gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height) - gl.enable(KmlGl.SCISSOR_TEST) - //println("viewport=$viewport") - } - defaultRenderer(gl, g) - defaultRendererAG(ag) - } - } - - override var quality: GameWindowQuality = GameWindowQuality.AUTOMATIC -} diff --git a/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt deleted file mode 100644 index c5cc93c7a6..0000000000 --- a/korge/src/jvm/korlibs/render/awt/NewAwtGameWindow.kt +++ /dev/null @@ -1,71 +0,0 @@ -package korlibs.render.awt - -import korlibs.datastructure.* -import korlibs.datastructure.event.* -import korlibs.datastructure.thread.* -import korlibs.graphics.* -import korlibs.image.awt.* -import korlibs.image.bitmap.* -import korlibs.render.* -import korlibs.time.* -import java.awt.* -import java.awt.event.* -import javax.swing.* - -val AwtAGOpenglCanvas.gameWindow: NewAwtCanvasGameWindow by Extra.PropertyThis { NewAwtCanvasGameWindow(this) } - -open class NewAwtCanvasGameWindow(val canvas: AwtAGOpenglCanvas) : GameWindow() { - override val ag: AG get() = canvas.ag - - val thread = nativeThread(name = "NewAwtGameWindow") { - eventLoop.runTasksForever() - } - - init { - canvas.doRender = { - dispatchNewRenderEvent() - } - coroutineDispatcher.eventLoop.setInterval(60.hz) { - dispatchUpdateEvent() - } - } - - override fun close(exitCode: Int) { - super.close(exitCode) - coroutineDispatcher.close() - } -} - -class NewAwtGameWindow(val config: GameWindowCreationConfig = GameWindowCreationConfig()) : NewAwtCanvasGameWindow(AwtAGOpenglCanvas()) { - val frame = object : JFrame() { - init { - isVisible = false - ignoreRepaint = true - defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE - contentPane.layout = GridLayout(1, 1) - contentPane.add(canvas) - preferredSize = Dimension(640, 480) - //setBounds(0, 0, 640, 480) - pack() - setLocationRelativeTo(null) - addWindowListener(object : WindowAdapter() { - override fun windowClosed(e: WindowEvent) { - this@NewAwtGameWindow.close() - } - }) - initTools() - } - override fun paintComponents(g: Graphics?) { - } - } - - override var alwaysOnTop: Boolean by frame::_isAlwaysOnTop - override var title: String by frame::title - override var visible: Boolean by frame::visible - override var icon: Bitmap? = null - set(value) { - field = value - frame.setIconIncludingTaskbarFromImage(value?.toAwt()) - } - override var fullscreen: Boolean by frame::isFullScreen -} diff --git a/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt new file mode 100644 index 0000000000..6a57857566 --- /dev/null +++ b/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt @@ -0,0 +1,409 @@ +package korlibs.render.awt + +/* +import korlibs.datastructure.* +import korlibs.event.* +import korlibs.graphics.* +import korlibs.graphics.gl.* +import korlibs.image.color.* +import korlibs.io.async.* +import korlibs.kgl.* +import korlibs.memory.* +import korlibs.platform.* +import korlibs.render.* +import korlibs.render.osx.* +import korlibs.render.platform.* +import korlibs.render.win32.* +import korlibs.render.x11.* +import korlibs.time.* +import kotlinx.coroutines.* +import java.awt.* +import java.awt.datatransfer.* +import java.awt.event.* +import java.awt.event.KeyEvent +import java.awt.event.MouseEvent +import javax.swing.* +import kotlin.system.* + +import korlibs.image.awt.* +import korlibs.image.bitmap.* +import korlibs.math.* +import korlibs.math.geom.* +import korlibs.render.* +import korlibs.render.platform.* +import java.awt.* +import java.awt.event.* +import javax.swing.* + + +open class GLCanvas constructor(checkGl: Boolean = true, val logGl: Boolean = false, cacheGl: Boolean = false) : Canvas(), GameWindowConfig, Closeable { + val ag: AGOpengl = AGOpenglAWT(checkGl, logGl, cacheGl) + var ctx: BaseOpenglContext? = null + val gl = ag.gl + private var doContextLost = false + + override fun getGraphicsConfiguration(): GraphicsConfiguration? { + return super.getGraphicsConfiguration() + } + + override fun addNotify() { + super.addNotify() + close() + } + + override fun removeNotify() { + super.removeNotify() + close() + } + + override fun reshape(x: Int, y: Int, width: Int, height: Int) { + super.reshape(x, y, width, height) + repaint() + } + + override fun update(g: Graphics) { + paint(g) + } + + override fun paint(g: Graphics) { + //val componentId = Native.getComponentID(this) + //if (ctxComponentId != componentId) { + // close() + //} + if (logGl) { + //println("+++++++++++++++++++++++++++++") + } + if (ctx == null) { + //println("--------------------------------------") + //ctxComponentId = componentId + ctx = glContextFromComponent(this, this) + doContextLost = true + } + //println("--------------") + ctx?.useContext(g, ag) { g, info -> + if (doContextLost) { + doContextLost = false + ag.contextLost() + } + render(gl, g, info) + } + } + + + override fun close() { + ctx?.dispose() + ctx = null + } + + var defaultRendererAG: (ag: AG) -> Unit = { + } + + var defaultRenderer: (gl: KmlGl, g: Graphics) -> Unit = { gl, g -> + /* + ctx?.useContext(g, ag) { + gl.clearColor(0f, 0f, 0f, 1f) + gl.clear(gl.COLOR_BUFFER_BIT) + } + */ + } + + val getCurrent: () -> Any? = { ctx?.getCurrent() } + + open fun render(gl: KmlGl, g: Graphics, info: BaseOpenglContext.ContextInfo) { + //ctx?.makeCurrent() + gl.info.current = getCurrent + ag.startEndFrame { + val viewport = info.viewport + if (viewport != null) { + gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height) + gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height) + gl.enable(KmlGl.SCISSOR_TEST) + //println("viewport=$viewport") + } + defaultRenderer(gl, g) + defaultRendererAG(ag) + } + } + + override var quality: GameWindowQuality = GameWindowQuality.AUTOMATIC +} + +class AwtGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig.DEFAULT) : BaseAwtGameWindow(AGOpenglAWT(config)) { + override var ctx: BaseOpenglContext? = null + + override fun ensureContext() { + if (ctx == null) ctx = glContextFromComponent(frame, this) + } + + val frame: JFrame = object : JFrame("Korgw") { + val frame = this + + init { + isVisible = false + ignoreRepaint = true + //background = Color.black + setBounds(0, 0, 640, 480) + frame.setLocationRelativeTo(null) + frame.setKorgeDropTarget(this@AwtGameWindow) + frame.setIconIncludingTaskbarFromResource("@appicon.png") + this.initTools() + + addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent?) { + debugFrame.isVisible = false + debugFrame.dispose() + running = false + } + }) + addComponentListener(object : ComponentAdapter() { + override fun componentMoved(e: ComponentEvent?) { + synchronizeDebugFrameCoordinates() + } + + override fun componentResized(e: ComponentEvent?) { + synchronizeDebugFrameCoordinates() + } + }) + } + + override fun paintComponents(g: Graphics?) = Unit + + override fun paint(g: Graphics) { + try { + framePaint(g) + } catch (e: Throwable) { + e.printStackTrace() + } + } + } + + override var alwaysOnTop: Boolean by frame::_isAlwaysOnTop + override var title: String by frame::title + override var icon: Bitmap? = null + set(value) { + field = value + frame.setIconIncludingTaskbarFromImage(value?.toAwt()) + } + override var fullscreen: Boolean by frame::isFullScreen + + val debugFrame = JFrame("Debug").apply { + try { + this.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE + this.setSize(280, 256) + this.type = Window.Type.UTILITY + this.isAlwaysOnTop = true + } catch (e: Throwable) { + e.printStackTrace() + } + val debugFrame = this + this@AwtGameWindow.onDebugChanged.add { + EventQueue.invokeLater { + debugFrame.isVisible = it + synchronizeDebugFrameCoordinates() + if (debugFrame.isVisible) { + //frame.isVisible = false + frame.isVisible = true + } + } + } + } + + override val debugComponent: Any? = debugFrame + + private fun synchronizeDebugFrameCoordinates() { + val displayMode = frame.getScreenDevice().displayMode + //println("frame.location=${frame.location}, frame.size=${frame.size}, debugFrame.width=${debugFrame.width}, displayMode=${displayMode.width}x${displayMode.height}") + val frameBounds = RectangleInt(frame.location.x, frame.location.y, frame.size.width, frame.size.height) + debugFrame.setLocation(frameBounds.right.clamp(0, (displayMode.width - debugFrame.width * 1.0).toInt()), frameBounds.top) + debugFrame.setSize(debugFrame.width.coerceAtLeast(64), frameBounds.height) + } + + override fun setSize(width: Int, height: Int) { + contentComponent.setSize(width, height) + contentComponent.preferredSize = Dimension(width, height) + frame.pack() + val component = this.component + if (component is Window) component.setLocationRelativeTo(null) + } + + override val component: Component get() = frame + override val contentComponent: Component get() = frame.contentPane + + override fun frameDispose() { + frame.dispose() + } +} +abstract class BaseAwtGameWindow( + override val ag: AGOpengl +) : GameWindow(), ClipboardOwner { + + fun paintInContextInternal(g: Graphics, info: BaseOpenglContext.ContextInfo) { + run { + //run { + ctx?.swapInterval(1) + + val g = g as Graphics2D + val gl = ag.gl + val factor = frameScaleFactor + if (lastFactor != factor) { + lastFactor = factor + reshaped = true + } + + //println("RENDER[1]") + + val viewport = info.viewport + val scissor = info.scissors + + if (component is JFrame) { + //println("component.width: ${contentComponent.width}x${contentComponent.height}") + ag.mainFrameBuffer.setSize( + 0, 0, (contentComponent.width * factor).toInt(), (contentComponent.height * factor).toInt(), + ) + } else { + ag.mainFrameBuffer.scissor(scissor) + if (viewport != null) { + //val window = SwingUtilities.getWindowAncestor(contentComponent) + //println("window=${window.width}x${window.height} : factor=$factor") + + val frameOrComponent = (window as? JFrame)?.contentPane ?: windowOrComponent + + ag.mainFrameBuffer.setSize( + viewport.x, viewport.y, viewport.width, viewport.height, + (frameOrComponent.width * factor).toInt(), + (frameOrComponent.height * factor).toInt(), + ) + } else { + ag.mainFrameBuffer.setSize( + 0, 0, (component.width * factor).toInt(), (component.height * factor).toInt(), + ) + } + } + + //println(gl.getString(gl.VERSION)) + //println(gl.versionString) + if (reshaped) { + reshaped = false + //println("RESHAPED!") + dispatchReshapeEventEx( + ag.mainFrameBuffer.x, + ag.mainFrameBuffer.y, + ag.mainFrameBuffer.width, + ag.mainFrameBuffer.height, + ag.mainFrameBuffer.fullWidth, + ag.mainFrameBuffer.fullHeight, + ) + } + + //gl.clearColor(1f, 1f, 1f, 1f) + //gl.clear(gl.COLOR_BUFFER_BIT) + var gamePadTime: TimeSpan = 0.milliseconds + var frameTime: TimeSpan = 0.milliseconds + var finishTime: TimeSpan = 0.milliseconds + val totalTime = measureTime { + frameTime = measureTime { + frame() + } + finishTime = measureTime { + gl.flush() + gl.finish() + } + } + + //println("totalTime=$totalTime, gamePadTime=$gamePadTime, finishTime=$finishTime, frameTime=$frameTime, timedTasksTime=${coroutineDispatcher.timedTasksTime}, tasksTime=${coroutineDispatcher.tasksTime}, renderTime=${renderTime}, updateTime=${updateTime}") + } + } + + + val frameScaleFactor: Float get() = getDisplayScalingFactor(component) + + val nonScaledWidth get() = contentComponent.width.toDouble() + val nonScaledHeight get() = contentComponent.height.toDouble() + + val scaledWidth get() = contentComponent.width * frameScaleFactor + val scaledHeight get() = contentComponent.height * frameScaleFactor + + override val width: Int get() = (scaledWidth).toInt() + override val height: Int get() = (scaledHeight).toInt() + override var visible: Boolean by LazyDelegate { component::visible } + override var quality: Quality = Quality.AUTOMATIC + + fun dispatchReshapeEvent() { + val factor = frameScaleFactor + dispatchReshapeEvent( + component.x, + component.y, + (contentComponent.width * factor).toInt().coerceAtLeast(1), + (contentComponent.height * factor).toInt().coerceAtLeast(1) + ) + } + + var displayLinkLock: java.lang.Object? = null + private val dl = OSXDisplayLink { + displayLinkLock?.let { displayLock -> + synchronized(displayLock) { + displayLock.notify() + } + } + } + + open fun frameDispose() { + } + + var reshaped = false + + override suspend fun loop(entry: suspend GameWindow.() -> Unit) { + launchImmediately(getCoroutineDispatcherWithCurrentContext()) { + entry() + } + +//frame.setBounds(0, 0, width, height) + + //val timer= Timer(40, ActionListener { + //}) + + queue { + dispatchInitEvent() + dispatchReshapeEvent() + } + EventQueue.invokeLater { + component.isVisible = true + component.repaint() + //fullscreen = true + + // keys.up(Key.ENTER) { if (it.alt) gameWindow.toggleFullScreen() } + + // @TODO: HACK so the windows grabs focus on Windows 10 when launching on gradle daemon + val useRobotHack = Platform.isWindows + + if (useRobotHack) { + (component as? Frame?)?.apply { + val frame = this + val insets = frame.insets + frame.isAlwaysOnTop = true + try { + val robot = Robot() + val pos = MouseInfo.getPointerInfo().location + val bounds = frame.bounds + bounds.setFrameFromDiagonal(bounds.minX + insets.left, bounds.minY + insets.top, bounds.maxX - insets.right, bounds.maxY - insets.bottom) + + //println("frame.bounds: ${frame.bounds}") + //println("frame.bounds: ${bounds}") + //println("frame.insets: ${insets}") + //println(frame.contentPane.bounds) + //println("START ROBOT") + robot.mouseMove(bounds.centerX.toInt(), bounds.centerY.toInt()) + robot.mousePress(InputEvent.BUTTON3_MASK) + robot.mouseRelease(InputEvent.BUTTON3_MASK) + robot.mouseMove(pos.x, pos.y) + //println("END ROBOT") + } catch (e: Throwable) { + } + frame.isAlwaysOnTop = false + } + } + } + } + +} +*/ diff --git a/korge/src/jvm/korlibs/render/osx/Cocoa.kt b/korge/src/jvm/korlibs/render/osx/Cocoa.kt index 95971ee156..6af4343a19 100644 --- a/korge/src/jvm/korlibs/render/osx/Cocoa.kt +++ b/korge/src/jvm/korlibs/render/osx/Cocoa.kt @@ -277,9 +277,6 @@ inline class NSMenu(val id: Long) { } } -@Deprecated("", ReplaceWith("korlibs.memory.dyn.osx.autoreleasePool(body)", "korlibs")) -inline fun autoreleasePool(body: () -> Unit) = korlibs.memory.dyn.osx.autoreleasePool(body) - internal interface GL : Library { fun glViewport(x: Int, y: Int, width: Int, height: Int) fun glClearColor(r: Float, g: Float, b: Float, a: Float) diff --git a/korge/src/jvm/korlibs/render/osx/MacGL.kt b/korge/src/jvm/korlibs/render/osx/MacGL.kt index 42fb90a243..b727c78e51 100644 --- a/korge/src/jvm/korlibs/render/osx/MacGL.kt +++ b/korge/src/jvm/korlibs/render/osx/MacGL.kt @@ -4,8 +4,7 @@ import com.sun.jna.* import korlibs.memory.dyn.* import korlibs.render.platform.* -//open class MacKmlGL : NativeKgl(MacGL) -open class MacKmlGL : NativeKgl(DirectGL) +open class MacKmlGl : NativeKgl(DirectGL) interface MacGL : INativeGL, Library { companion object : MacGL by NativeLoad(nativeOpenGLLibraryPath) { diff --git a/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt b/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt index 35d885bcdc..bbcc5d12ed 100644 --- a/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt +++ b/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt @@ -208,39 +208,43 @@ class MacAWTOpenglContext(val gwconfig: GameWindowConfig, val c: Component, var override val scaleFactor: Float get() = getDisplayScalingFactor(c) override fun useContext(g: Graphics, ag: AG, action: (Graphics, BaseOpenglContext.ContextInfo) -> Unit) { - invokeWithOGLContextCurrentMethod.invoke(null, g, Runnable { - - //if (!(isQueueFlusherThread.invoke(null) as Boolean)) error("Can't render on another thread") - try { - val factor = getDisplayScalingFactor(c) - //val window = SwingUtilities.getWindowAncestor(c) - val viewport = getOGLViewport.invoke(null, g, (c.width * factor).toInt(), (c.height * factor).toInt()) as java.awt.Rectangle - //val viewport = getOGLViewport.invoke(null, g, window.width.toInt(), window.height.toInt()) as java.awt.Rectangle - val scissorBox = getOGLScissorBox(null, g) as? java.awt.Rectangle? - ///println("factor=$factor, scissorBox: $scissorBox, viewport: $viewport") - //info.scissors?.setTo(scissorBox.x, scissorBox.y, scissorBox.width, scissorBox.height) - if (scissorBox != null) { - info.scissors = RectangleInt(scissorBox.x, scissorBox.y, scissorBox.width, scissorBox.height) - //info.viewport?.setTo(viewport.x, viewport.y, viewport.width, viewport.height) - info.viewport = RectangleInt(scissorBox.x, scissorBox.y, scissorBox.width, scissorBox.height) - } else { - System.err.println("ERROR !! scissorBox = $scissorBox, viewport = $viewport") + try { + invokeWithOGLContextCurrentMethod.invoke(null, g, Runnable { + + //if (!(isQueueFlusherThread.invoke(null) as Boolean)) error("Can't render on another thread") + try { + val factor = getDisplayScalingFactor(c) + //val window = SwingUtilities.getWindowAncestor(c) + val viewport = getOGLViewport.invoke(null, g, (c.width * factor).toInt(), (c.height * factor).toInt()) as java.awt.Rectangle + //val viewport = getOGLViewport.invoke(null, g, window.width.toInt(), window.height.toInt()) as java.awt.Rectangle + val scissorBox = getOGLScissorBox(null, g) as? java.awt.Rectangle? + ///println("factor=$factor, scissorBox: $scissorBox, viewport: $viewport") + //info.scissors?.setTo(scissorBox.x, scissorBox.y, scissorBox.width, scissorBox.height) + if (scissorBox != null) { + info.scissors = RectangleInt(scissorBox.x, scissorBox.y, scissorBox.width, scissorBox.height) + //info.viewport?.setTo(viewport.x, viewport.y, viewport.width, viewport.height) + info.viewport = RectangleInt(scissorBox.x, scissorBox.y, scissorBox.width, scissorBox.height) + } else { + System.err.println("ERROR !! scissorBox = $scissorBox, viewport = $viewport") + } + //info.viewport?.setTo(scissorBox.x, scissorBox.y) + //println("viewport: $viewport, $scissorBox") + //println(g.clipBounds) + if (other != null) { + val oldContext = NSClass("NSOpenGLContext").msgSend("currentContext") + other!!.useContext(g, ag) { g, info -> action(g, info) } + oldContext.msgSend("makeCurrentContext") + } else { + action(g, info) + } + + } catch (e: Throwable) { + e.printStackTrace() } - //info.viewport?.setTo(scissorBox.x, scissorBox.y) - //println("viewport: $viewport, $scissorBox") - //println(g.clipBounds) - if (other != null) { - val oldContext = NSClass("NSOpenGLContext").msgSend("currentContext") - other!!.useContext(g, ag) { g, info -> action(g, info) } - oldContext.msgSend("makeCurrentContext") - } else { - action(g, info) - } - - } catch (e: Throwable) { - e.printStackTrace() - } - }) + }) + } catch (e: Throwable) { + e.printStackTrace() + } } override fun makeCurrent() = Unit diff --git a/korge/test/common/korlibs/render/x11/LinuxJoyEventAdapterTest.kt b/korge/test/common/korlibs/render/x11/LinuxJoyEventAdapterTest.kt index 56a20636cd..a490f55c6e 100644 --- a/korge/test/common/korlibs/render/x11/LinuxJoyEventAdapterTest.kt +++ b/korge/test/common/korlibs/render/x11/LinuxJoyEventAdapterTest.kt @@ -1,6 +1,7 @@ package korlibs.render.x11 import korlibs.event.* +import korlibs.event.gamepad.* import korlibs.io.async.* import korlibs.io.file.sync.* import korlibs.io.lang.* diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt index 57ab2b591d..98603878a3 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt @@ -5,7 +5,6 @@ import korlibs.datastructure.lock.* import korlibs.datastructure.thread.* import korlibs.image.bitmap.* import korlibs.image.color.* -import korlibs.kgl.* import korlibs.korge.render.* import korlibs.platform.* import korlibs.time.* @@ -16,7 +15,7 @@ import kotlin.test.* class AwtGameCanvasTest { @Test - @Ignore + //@Ignore fun test() = korlibs.memory.dyn.osx.autoreleasePool { //System.setProperty("sun.java2d.metal", "true") //System.setProperty("sun.java2d.opengl", "false") @@ -86,12 +85,12 @@ class AwtGameCanvasTest { fpsLabel = it it.isOpaque = true; it.background = Color.YELLOW }) frame.contentPane.add(JLabel().also { it.isOpaque = true; it.background = Color.GREEN }) - frame.contentPane.add(GLCanvas().also { - it.defaultRenderer = { gl, g -> - gl.clearColor(1f, .5f, 1f, 1f) - gl.clear(KmlGl.COLOR_BUFFER_BIT) - } - }) + //frame.contentPane.add(GLCanvas().also { + // it.defaultRenderer = { gl, g -> + // gl.clearColor(1f, .5f, 1f, 1f) + // gl.clear(KmlGl.COLOR_BUFFER_BIT) + // } + //}) /* frame.contentPane.add(AwtAGOpenglCanvas().also { val renderContext = RenderContext(it.ag, it) diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt index d9d90170d4..8aba459c33 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt @@ -1,12 +1,15 @@ package korlibs.render.awt import korlibs.datastructure.thread.* +import korlibs.event.* import korlibs.image.color.* import korlibs.image.format.* import korlibs.io.file.std.* import korlibs.korge.* +import korlibs.korge.input.* import korlibs.korge.view.* import korlibs.logger.* +import korlibs.render.* import korlibs.time.* import kotlin.test.* @@ -15,13 +18,45 @@ class AwtGameWindowTest { //@Ignore fun test() { Korge.logger.level = Logger.Level.DEBUG - val gameWindow = NewAwtGameWindow() + val gameWindow = AwtGameWindow(GameWindowCreationConfig.DEFAULT.copy(title = "hello")) - gameWindow.configureKorge { - image(resourcesVfs["korge.png"].readBitmap()) + gameWindow.bgcolor = Colors.BLACK + + gameWindow.configureKorge(KorgeConfig(backgroundColor = Colors.BLACK)) { + println(measureTime { + image(resourcesVfs["korge.png"].readBitmap()) + }) solidRect(100, 100, Colors.RED).addUpdater { this.x++ } + mouse.click { + println("CLICK!") + showContextMenu { + item("Hello") + item("World") + } + + } + mouse.scroll { + println("SCROLL: $it") + } + gestures { + magnify { println("MAGNITIFY! $it") } + } + onEvent(DestroyEvent) { + println("DESTROY!") + } + + setMainMenu { + item("Hello") { + item("&Exit") + } + item("World") { + item("Hello") + } + } + + //mouse { moveAnywhere { println("MOUSE MOVE $it") } } } gameWindow.show() diff --git a/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt b/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt index e117ba1fc8..b3f9f8ef06 100644 --- a/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt +++ b/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt @@ -133,7 +133,7 @@ class AGMetalTest { private fun macTestWithAutoreleasePool(block: () -> Unit) { if (!Platform.isMac) return - nsAutoreleasePool { + autoreleasePool { //run { block() } From c92dac05f62cd8768a357b6e8d85b0dc77e66bc5 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 22:53:36 +0200 Subject: [PATCH 04/66] Make common FFI support ObjectiveC code, and moved DarwinMacosGameController to common code --- .../src/android/korlibs/ffi/FFILib.android.kt | 4 + korge-core/src/common/korlibs/ffi/FFILib.kt | 24 +- .../src/common/korlibs/ffi/osx/FFIObjc.kt | 510 ++++++++++++++++++ .../common/korlibs/ffi/osx/ObjcStandard.kt | 66 +++ .../src/darwin/korlibs/ffi/FFILib.wasm.kt | 4 + korge-core/src/js/korlibs/ffi/FFILib.js.kt | 8 +- korge-core/src/jvm/korlibs/ffi/FFILib.jvm.kt | 52 +- .../common/korlibs/ffi/osx/FFIObjcTest.kt | 22 + .../src/jvm/korlibs/memory/dyn/osx/Cocoa.kt | 9 +- .../gamepad/DarwinMacosGameController.kt} | 148 ++--- 10 files changed, 744 insertions(+), 103 deletions(-) create mode 100644 korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt create mode 100644 korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt create mode 100644 korge-core/test/common/korlibs/ffi/osx/FFIObjcTest.kt rename korge/src/{jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt => common/korlibs/event/gamepad/DarwinMacosGameController.kt} (65%) diff --git a/korge-core/src/android/korlibs/ffi/FFILib.android.kt b/korge-core/src/android/korlibs/ffi/FFILib.android.kt index 7755d8af15..6588eb7efc 100644 --- a/korge-core/src/android/korlibs/ffi/FFILib.android.kt +++ b/korge-core/src/android/korlibs/ffi/FFILib.android.kt @@ -7,6 +7,10 @@ actual fun FFILibSym(lib: FFILib): FFILibSym { } } +actual fun FFICreateProxyFunction(type: KType, handler: (args: Array) -> Any?): T { + TODO() +} + actual class FFIPointer actual class FFIMemory diff --git a/korge-core/src/common/korlibs/ffi/FFILib.kt b/korge-core/src/common/korlibs/ffi/FFILib.kt index 7b7654030d..aa5533eb96 100644 --- a/korge-core/src/common/korlibs/ffi/FFILib.kt +++ b/korge-core/src/common/korlibs/ffi/FFILib.kt @@ -4,9 +4,14 @@ import korlibs.datastructure.* import korlibs.io.file.sync.* import korlibs.io.lang.* import korlibs.memory.* +import kotlin.jvm.* import kotlin.properties.* import kotlin.reflect.* +expect fun FFICreateProxyFunction(type: KType, handler: (args: Array) -> Any?): T + +inline fun FFICreateProxyFunction(noinline handler: (args: Array) -> Any?): T = FFICreateProxyFunction(typeOf(), handler) + expect class FFIPointer expect class FFIMemory @@ -69,6 +74,19 @@ fun FFIPointer.setFFIPointer(value: FFIPointer?, byteOffset: Int = 0) { fun FFIPointer.getFFIPointer(byteOffset: Int = 0): FFIPointer? = if (FFI_POINTER_SIZE == 8) CreateFFIPointer(getS64(byteOffset)) else CreateFFIPointer(getS32(byteOffset).toLong()) +@JvmInline +value class FFIVarargs(val args: List) { + constructor(vararg args: Any?) : this(args.toList()) + override fun toString(): String = "FFIVarargs(${args.joinToString(", ")})" +} + +interface FFICallback + +/** Might be 32-bit or 64-bit depending on the OS */ +class FFINativeLong(val value: Long) { + +} + fun FFIPointer.getAlignedS16(offset: Int = 0): Short = getS16(offset * 2) fun FFIPointer.getAlignedS32(offset: Int = 0): Int = getS32(offset * 4) fun FFIPointer.getAlignedS64(offset: Int = 0): Long = getS64(offset * 8) @@ -191,14 +209,14 @@ open class FFILib(val paths: List, val lazyCreate: Boolean = true) { } } - class FuncDelegate(val base: FFILib, val name: String, val type: KType, val config: FFIFuncConfig) : ReadOnlyProperty { + class FuncDelegate(val base: FFILib, val bname: String, val name: String, val type: KType, val config: FFIFuncConfig) : ReadOnlyProperty { val parts = extractTypeFunc(type) //val generics = type.arguments.map { it.type?.classifier } val params = parts.paramsClass val ret = parts.retClass var cached: T? = null override fun getValue(thisRef: FFILib, property: KProperty<*>): T { - if (cached == null) cached = base.sym.get(name, type) + if (cached == null) cached = base.sym.get(bname, type) return cached.fastCastTo() } } @@ -207,7 +225,7 @@ open class FFILib(val paths: List, val lazyCreate: Boolean = true) { operator fun provideDelegate( thisRef: FFILib, prop: KProperty<*> - ): ReadOnlyProperty = FuncDelegate(thisRef, extraName ?: prop.name, type, config).also { + ): ReadOnlyProperty = FuncDelegate(thisRef, prop.name, extraName ?: prop.name, type, config).also { thisRef.functions.add(it) if (!thisRef.lazyCreate) it.getValue(thisRef, prop) } diff --git a/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt b/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt new file mode 100644 index 0000000000..bb938d76b3 --- /dev/null +++ b/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt @@ -0,0 +1,510 @@ +package korlibs.ffi.osx + +import korlibs.annotations.* +import korlibs.datastructure.lock.* +import korlibs.ffi.* +import kotlin.properties.* +import kotlin.reflect.* + +typealias ID = Long + +@KeepNames +object FFIObjc : FFILib("objc") { + val objc_copyProtocolList: (outCount: IntArray) -> FFIPointer? by func() + val protocol_getName: (protocol: Long) -> String by func() + + val objc_getClass: (name: String) -> Long by func() + + val objc_getClassList: (buffer: FFIPointer?, bufferCount: Int) -> Int by func() + + val objc_getProtocol: (name: String) -> Long by func() + + val class_addProtocol: (a: Long, b: Long) -> Long by func() + val class_copyMethodList: (clazz: Long, items: IntArray) -> FFIPointer? by func() + + // typedef struct objc_method_description { + // SEL name; // The name of the method + // char *types; // The types of the method arguments + // } MethodDescription; + val protocol_copyMethodDescriptionList: (proto: Long, isRequiredMethod: Boolean, isInstanceMethod: Boolean, outCount: IntArray) -> FFIPointer? by func() + + val objc_registerClassPair: (cls: Long) -> Unit by func() + val objc_lookUpClass: (name: String) -> Long by func() + + val objc_msgSend: (args: FFIVarargs) -> Long by func("objc_msgSend") + val objc_msgSendInt: (args: FFIVarargs) -> Int by func("objc_msgSend") + val objc_msgSendVoid: (args: FFIVarargs) -> Unit by func("objc_msgSend") + //val objc_msgSendCGFloat(args: FFIVarargs) -> CGFloat by func("objc_msgSend") + val objc_msgSendFloat: (args: FFIVarargs) -> Float by func("objc_msgSend") + val objc_msgSendDouble: (args: FFIVarargs) -> Double by func("objc_msgSend") + //val objc_msgSendNSPoint(args: FFIVarargs): NSPointRes by func("objc_msgSend") + //val objc_msgSendNSRect(args: FFIVarargs): NSRectRes by func("objc_msgSend") + + // @TODO: Check: Error looking up function 'objc_msgSend_stret': dlsym(RTLD_DEFAULT, objc_msgSend_stret): symbol not found + // Was only available on Intel macs? + val objc_msgSend_stret: (structPtr: Any?, args: FFIVarargs) -> Unit by func("objc_msgSend_stret") + + /* + fun objc_msgSend(a: Long, b: Long): Long + fun objc_msgSend(a: Long, b: Long, c: Long): Long + fun objc_msgSend(a: Long, b: Long, c: String): Long + fun objc_msgSend(a: Long, b: Long, c: ByteArray, d: Int, e: Int): Long + fun objc_msgSend(a: Long, b: Long, c: ByteArray, len: Int): Long + fun objc_msgSend(a: Long, b: Long, c: CharArray, len: Int): Long + */ + val method_getName: (m: Long) -> Long by func() + val method_getNameString: (m: Long) -> String by func("method_getName") + + val sel_registerName: (name: String) -> Long by func() + val sel_getName: (sel: Long) -> String by func() + val sel_getNameString: (sel: String) -> String by func("sel_getName") + + val objc_allocateClassPair: (clazz: Long, name: String, extraBytes: Int) -> Long by func() + val object_getIvar: (obj: Long, ivar: Long) -> Long by func() + + val class_getInstanceVariable: (clazz: Long, name: String) -> Long by func() + val class_getProperty: (clazz: Long, name: String) -> Long by func() + + @OptIn(ExperimentalStdlibApi::class) + val class_addMethod: (cls: Long, name: Long, imp: FFICallback, types: String) -> Long by func() + val class_conformsToProtocol: (cls: Long, protocol: Long) -> Boolean by func() + + val object_getClass: (obj: Long) -> Long by func() + val class_getName: (clazz: Long) -> String by func() + + val object_getClassName: (obj: Long) -> String by func() + val class_getImageName: (obj: Long) -> String by func() + + val property_getName: (prop: Long) -> String by func() + val property_getAttributes: (prop: Long) -> String by func() + + val class_getInstanceMethod: (cls: Long, id: Long) -> Long by func() + + val method_getReturnType: (id: Long, dst: FFIPointer?, dst_length: Long) -> Unit by func() + val method_getTypeEncoding: (ptr: FFIPointer?) -> String by func() + + val class_createInstance: (cls: Long, extraBytes: Long) -> Long by func() + val class_copyPropertyList: (cls: Long, outCountPtr: IntArray) -> FFIPointer? by func() + val class_copyIvarList: (cls: Long, outCountPtr: IntArray) -> FFIPointer? by func() + val ivar_getName: (ivar: FFIPointer?) -> String? by func() + val ivar_getTypeEncoding: (ivar: FFIPointer?) -> String? by func() +} + +fun FFIObjc.getClassByName(name: String): ObjcClassRef? { + val id = FFIObjc.objc_lookUpClass(name) + return if (id != 0L) ObjcClassRef(id) else null +} + +fun FFIObjc.getAllClassIDs(): List = ffiScoped { + val total = objc_getClassList(null, 0) + val data = allocBytes((total * 8)) + //println(data.getLong(0L)) + val total2 = objc_getClassList(data, total) + return (0 until total2).map { ObjcClassRef(data.getS64((it * 8))) } +} + +interface ObjcProtocolClassBaseRef { + val name: String +} + +data class ObjcMethodRef(val objcClass: ObjcProtocolClassBaseRef, val ptr: FFIPointer?) { + val name: String by lazy { FFIObjc.method_getNameString(ptr.address) } + val selName: String by lazy { FFIObjc.sel_getNameString(name) } + val types: String by lazy { FFIObjc.method_getTypeEncoding(ptr) } + val parsedTypes: ObjcMethodDesc by lazy { ObjcTypeParser.parse(types) } + + override fun toString(): String = "${objcClass.name}.$name" +} + +data class ObjcClassRef(val ref: Long) : ObjcProtocolClassBaseRef { + companion object { + fun listAll(): List = FFIObjc.getAllClassIDs() + fun fromName(name: String): ObjcClassRef? = FFIObjc.getClassByName(name) + } + + fun createInstance(extraBytes: Int = 0): Long { + return FFIObjc.class_createInstance(ref, extraBytes.toLong()) + } + + fun dumpKotlin() { + println("class $name(id: Long) : NSObject(id) {") + listIVars() + listProperties() + for (method in listMethods()) { + println(" " + createKotlinMethod(method.name, method.parsedTypes)) + } + println("}") + } + + override val name: String by lazy { FFIObjc.object_getClassName(ref) } + val imageName: String by lazy { FFIObjc.class_getImageName(ref) } + + fun listMethods(): List { + val nitemsPtr = IntArray(1) + val items2 = FFIObjc.class_copyMethodList(ref, nitemsPtr)!! + val nitems = nitemsPtr[0] + return (0 until nitems).map { + ObjcMethodRef(this, items2.getFFIPointer(Long.SIZE_BYTES * it)) + } + } + + fun listProperties() { + val outCountPtr = IntArray(1) + val properties = FFIObjc.class_copyPropertyList(ref, outCountPtr)!! + val outCount = outCountPtr[0] + for (n in 0 until outCount) { + val prop = properties.getFFIPointer(n * 8) + val propName = FFIObjc.property_getName(prop.address) + val attributes = FFIObjc.property_getAttributes(prop.address) + println(" // PROP: * $propName : $attributes") + } + //TODO("listProperties. outCount=$outCount") + } + + fun listIVars() { + val outCountPtr = IntArray(1) + val ivars = FFIObjc.class_copyIvarList(ref, outCountPtr) + val outCount = outCountPtr[0] + for (n in 0 until outCount) { + val ivar = ivars!!.getFFIPointer(n * 8) + val ivarName = FFIObjc.ivar_getName(ivar) + val encoding = FFIObjc.ivar_getTypeEncoding(ivar) + println(" // IVAR: * ivar=$ivarName : $encoding") + } + //TODO("listIVars. outCount=$outCount") + } + + override fun toString(): String = "ObjcClass[$name]" +} + +class ObjcProtocol(val ref: Long) : ObjcProtocolClassBaseRef { + override val name: String by lazy { FFIObjc.protocol_getName(ref) } + + fun dumpKotlin() { + println("interface $name : ObjcDynamicInterface {") + for (method in listMethods()) { + method.dumpKotlin() + } + println("}") + } + + fun listMethods(): List { + val nitemsPtr = IntArray(1) + val items2 = FFIObjc.protocol_copyMethodDescriptionList(ref, true, true, nitemsPtr)!! + val nitems = nitemsPtr[0] + val out = ArrayList(nitems) + //println("nitems=$nitems") + return (0 until nitems).map { n -> + val namePtr = items2.getS64(Long.SIZE_BYTES * (n * 2 + 0)) + val typesPtr = items2.getS64(Long.SIZE_BYTES * (n * 2 + 1)) + val typesStr = CreateFFIPointer(typesPtr)!!.getStringz() + ObjcMethodDescription(this, namePtr, typesStr) + //println("$selName: $typesStr") + //val selName = ObjectiveC.sel_getName(mname) + } + } + override fun toString(): String = "ObjcProtocolRef(${FFIObjc.protocol_getName(ref)})" + + companion object { + fun fromName(name: String): ObjcProtocol? = + FFIObjc.objc_getProtocol(name).takeIf { it != 0L }?.let { ObjcProtocol(it) } + + fun listAll(): List { + val sizePtr = IntArray(1) + val ptr = FFIObjc.objc_copyProtocolList(sizePtr)!! + val size = sizePtr[0] + return (0 until size).map { ObjcProtocol(ptr.getFFIPointer(FFI_POINTER_SIZE * it).address) } + } + } +} + +data class ObjcMethodDescription( + val protocol: ObjcProtocol, + val id: Long, + val types: String +) { + val method by lazy { FFIObjc.class_getInstanceMethod(protocol.ref, id) } + val name: String by lazy { FFIObjc.sel_getName(id) } + val parsedTypes: ObjcMethodDesc by lazy { ObjcTypeParser.parse(types) } + + override fun toString(): String = "ObjcMethodDescription[$name$parsedTypes]" + fun dumpKotlin() { + println(" " + createKotlinMethod(name, parsedTypes)) + } +} + +private fun String.keywordQuotedIfRequired(): String = if (this in KotlinKeywords) "`$this`" else this + +private fun createKotlinMethod( + name: String, parsedTypes: ObjcMethodDesc, + setName: String? = null, setParsedTypes: ObjcMethodDesc? = null, +): String { + val types = parsedTypes.desc + val parts = name.split(":") + val fullName = parts[0] + val firstArg = fullName.substringAfterLast("With").replaceFirstChar { it.lowercase() } + val baseName = fullName.substringBeforeLast("With") + val argNames = listOf(firstArg) + parts.drop(1) + val paramsWithNames = argNames.zip(parsedTypes.params.drop(2)) + val paramsStr = paramsWithNames.joinToString(", ") { "${it.first.keywordQuotedIfRequired()}: ${it.second.type.toKotlinString()}" } + val returnTypeStr = parsedTypes.returnType.type.toKotlinString() + return when { + paramsWithNames.isEmpty() && parsedTypes.returnType.type != PrimitiveObjcType.VOID -> { + "val $baseName: $returnTypeStr by objProp(\"$name\", \"$types\")" + } + else -> { + //return "@ObjcDesc(\"$name\", \"$types\") fun $baseName($paramsStr): $returnTypeStr" + "val $baseName: ($paramsStr) -> $returnTypeStr by objFunc(\"$name\", \"$types\")" + } + } +} + +data class ObjcMethodDesc(val desc: String, val returnType: ObjcParam, val params: List) { + constructor(desc: String, all: List) : this(desc, all.first(), all.drop(1)) +} +data class ObjcParam(val offset: Int, val type: ObjcType) + +interface ObjcType { + fun toKotlinString(): String +} +data class ConstObjcType(val base: ObjcType) : ObjcType { + override fun toKotlinString(): String = base.toKotlinString() +} +data class PointerObjcType(val base: ObjcType) : ObjcType { + override fun toKotlinString(): String = "Pointer" +} +data class StructObjcType(val strName: String, val types: List) : ObjcType { + override fun toKotlinString(): String = "Struct" +} +data class FixedArrayObjcType(val count: Int, val type: ObjcType) : ObjcType { + override fun toKotlinString(): String = "FixedArray[$count]" +} + +val KotlinKeywords = setOf("object", "val", "class") + +enum class PrimitiveObjcType : ObjcType { + VOID, BOOL, ID, SEL, BYTE, INT, UINT, NINT, NUINT, FLOAT, DOUBLE, BLOCK; + + override fun toKotlinString(): String = when (this) { + VOID -> "Unit" + BOOL -> "Boolean" + ID -> "ID" + SEL -> "SEL" + BYTE -> "Byte" + INT -> "Int" + UINT -> "UInt" + NINT -> "FFINativeLong" + NUINT -> "FFINativeLong" + FLOAT -> "Float" + DOUBLE -> "Double" + BLOCK -> "(() -> Unit)" + } +} + +object ObjcTypeParser { + data class StrReader(val str: String, var pos: Int = 0, var end: Int = str.length) { + val hasMore: Boolean get() = pos < end + + fun peekChar(): Char = str.getOrElse(pos) { '\u0000' } + fun readChar(): Char = peekChar().also { skip() } + fun skip(count: Int = 1) { + pos += count + } + fun readUntil(cond: (Char) -> Boolean): String { + var out = "" + while (true) { + val c = peekChar() + if (!cond(c)) break + skip() + out += c + } + return out + } + } + + fun parseInt(str: StrReader): Int { + var out = "" + while (str.peekChar().isDigit()) { + out += str.readChar() + } + return out.toIntOrNull() ?: -1 + } + + fun parseType(str: StrReader): ObjcType { + val c = str.readChar() + return when (c) { + 'V' -> PrimitiveObjcType.VOID // Class initializer + 'v' -> PrimitiveObjcType.VOID + 'B' -> PrimitiveObjcType.BOOL + '@' -> { + if (str.peekChar() == '?') { + str.skip() + PrimitiveObjcType.BLOCK + } else { + PrimitiveObjcType.ID + } + } + '{' -> { + val name = str.readUntil { it != '=' } + if (str.readChar() != '=') error("Invalid $str") + val out = arrayListOf() + while (str.peekChar() != '}') { + out += parseType(str) + } + str.skip() + StructObjcType(name, out) + } + '[' -> { + val count = parseInt(str) + val type = parseType(str) + if (str.readChar() != ']') error("Invalid $str") + FixedArrayObjcType(count, type) + } + '#' -> PrimitiveObjcType.ID + ':' -> PrimitiveObjcType.SEL + 'C' -> PrimitiveObjcType.BYTE + 'i' -> PrimitiveObjcType.INT + 'I' -> PrimitiveObjcType.UINT + 'q' -> PrimitiveObjcType.NINT + 'Q' -> PrimitiveObjcType.NUINT + '^' -> PointerObjcType(parseType(str)) + '?' -> PrimitiveObjcType.BLOCK + '*' -> PointerObjcType(PrimitiveObjcType.BYTE) + 'r' -> ConstObjcType(parseType(str)) + 'f' -> PrimitiveObjcType.FLOAT + 'd' -> PrimitiveObjcType.DOUBLE + else -> TODO("Not implemented '$c' in $str") + } + } + fun parseParam(str: StrReader): ObjcParam { + val type = parseType(str) + val offset = parseInt(str) + return ObjcParam(offset, type) + } + fun parse(str: StrReader): ObjcMethodDesc { + val out = arrayListOf() + while (str.hasMore) { + out += parseParam(str) + } + return ObjcMethodDesc(str.str, out) + } + fun parse(str: String): ObjcMethodDesc = parse(StrReader(str)) +} + +open class NSClass(id: Long) : NSObject(id) { + constructor(name: String) : this(FFIObjc.objc_getClass(name)) + + val OBJ_CLASS = ObjcRef(id) + + val name by lazy { FFIObjc.class_getName(id) } + + override fun toString(): String = "NSClass[$id]($name)" +} + +//open class NSObject(val id: Long) : FFILib() { +open class NSObject(val id: Long) { + constructor(id: ObjcRef, unit: Unit = Unit) : this(id.id) + + init { + if (id == 0L) { + println("NSObject[${this::class}]: id=$id") + } + } + + val ref = ObjcRef(id) + + class FuncInfo>(val type: KType, val selector: String, val def: String) { + operator fun provideDelegate( + thisRef: NSObject, + prop: KProperty<*> + ): ReadOnlyProperty = NSObject.FuncDelegate(thisRef, selector, def, type).also { + //thisRef.functions.add(it) + //if (!thisRef.lazyCreate) it.getValue(thisRef, prop) + } + } + + class FuncDelegate>(val base: NSObject, val selector: String, val def: String, val type: KType) : ReadOnlyProperty { + val parts = FFILib.extractTypeFunc(type) + //val generics = type.arguments.map { it.type?.classifier } + val params = parts.paramsClass + val ret = parts.retClass + var cached: T? = null + override fun getValue(thisRef: NSObject, property: KProperty<*>): T { + return FFICreateProxyFunction(type) { args -> + thisRef.msgSend(selector, *args) + } + } + } + + inline fun > objFunc(selector: String, def: String): FuncInfo { + return FuncInfo(typeOf(), selector, def) + } + + class PropInfo(val type: KType, val selector: String, val def: String) { + val info = FuncInfo<() -> T>(type, selector, def) + + operator fun getValue(thisRef: NSObject, property: KProperty<*>): T { + return info.provideDelegate(thisRef, property).getValue(thisRef, property).invoke() + } + } + + inline fun objProp(selector: String, def: String): PropInfo { + return PropInfo(typeOf<() -> T>(), selector, def) + } + + fun msgSend(sel: String, vararg args: Any?): Long = ref.msgSend(sel, *args) + fun msgSendRef(sel: String, vararg args: Any?): ObjcRef = ref.msgSendRef(sel, *args) + fun msgSendVoid(sel: String, vararg args: Any?): Unit = ref.msgSendVoid(sel, *args) + fun msgSendInt(sel: String, vararg args: Any?): Int = ref.msgSendInt(sel, *args) + fun msgSendFloat(sel: String, vararg args: Any?): Float = ref.msgSendFloat(sel, *args) + fun msgSendDouble(sel: String, vararg args: Any?): Double = ref.msgSendDouble(sel, *args) + //fun msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = FFIObjc.objc_msgSendCGFloat(id, sel(sel), *args) + //fun msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = FFIObjc.objc_msgSendNSPoint(id, sel(sel), *args) + //fun msgSend_stret(sel: String, vararg args: Any?): Unit = FFIObjc.objc_msgSend_stret(id, sel(sel), *args) + + fun alloc(): NSObject = NSObject(msgSend("alloc")) + + companion object : NSClass("NSObject") { + fun sel(name: String): Long = ObjcSel(name).id + } + + val objcClass: NSClass get() = NSClass(msgSend("class")) + + override fun toString(): String = "NSObject(${objcClass})" +} + +inline class ObjcRef(val id: Long) { + constructor(ref: ObjcRef, unit: Unit = Unit) : this(ref.id) + + fun msgSend(sel: ObjcSel, vararg args: Any?): Long = FFIObjc.objc_msgSend(FFIVarargs(id, (sel.id), *args)) + fun msgSendRef(sel: ObjcSel, vararg args: Any?): ObjcRef = ObjcRef(msgSend(sel, *args)) + fun msgSendVoid(sel: ObjcSel, vararg args: Any?): Unit = FFIObjc.objc_msgSendVoid(FFIVarargs(id, (sel.id), *args)) + fun msgSendInt(sel: ObjcSel, vararg args: Any?): Int = FFIObjc.objc_msgSendInt(FFIVarargs(id, (sel.id), *args)) + fun msgSendFloat(sel: ObjcSel, vararg args: Any?): Float = FFIObjc.objc_msgSendFloat(FFIVarargs(id, (sel.id), *args)) + fun msgSendDouble(sel: ObjcSel, vararg args: Any?): Double = FFIObjc.objc_msgSendDouble(FFIVarargs(id, (sel.id), *args)) + + fun msgSend(sel: String, vararg args: Any?): Long = msgSend(ObjcSel(sel), *args) + fun msgSendRef(sel: String, vararg args: Any?): ObjcRef = msgSendRef(ObjcSel(sel), *args) + fun msgSendVoid(sel: String, vararg args: Any?): Unit = msgSendVoid(ObjcSel(sel), *args) + fun msgSendInt(sel: String, vararg args: Any?): Int = msgSendInt(ObjcSel(sel), *args) + fun msgSendFloat(sel: String, vararg args: Any?): Float = msgSendFloat(ObjcSel(sel), *args) + fun msgSendDouble(sel: String, vararg args: Any?): Double = msgSendDouble(ObjcSel(sel), *args) + //fun msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = FFIObjc.objc_msgSendCGFloat(id, sel(sel), *args) + //fun msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = FFIObjc.objc_msgSendNSPoint(id, sel(sel), *args) + //fun msgSend_stret(sel: String, vararg args: Any?): Unit = FFIObjc.objc_msgSend_stret(id, sel(sel), *args) +} + +inline class ObjcSel(val id: Long) { + companion object { + private val lock = Lock() + private val selectors = HashMap() + + operator fun invoke(name: String): ObjcSel = lock { + selectors.getOrPut(name) { + ObjcSel(FFIObjc.sel_registerName(name).also { + if (it == 0L) error("Invalid selector '$name'") + }) + } + } + } +} diff --git a/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt b/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt new file mode 100644 index 0000000000..11acb8e386 --- /dev/null +++ b/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt @@ -0,0 +1,66 @@ +package korlibs.ffi.osx + +import korlibs.io.lang.* +import kotlin.text.toCharArray + +fun NSObject.Companion.cast(value: Any): NSObject { + return when (value) { + is NSObject -> value + //is String -> NSString(value) + is Int -> NSNumber(value) + is Long -> NSNumber(value) + is Double -> NSNumber(value) + //is Boolean -> CFBoolean(value) + else -> TODO("Unsupported value '$value' to be cast to NSObject") + } +} + +open class NSString(id: Long) : NSObject(id) { + constructor() : this("") + constructor(id: Long?) : this(id ?: 0L) + constructor(str: String) : this(OBJ_CLASS.msgSendRef("alloc").msgSend("initWithCharacters:length:", str.toCharArray(), str.length)) + + //val length: Int get() = ObjectiveC.object_getIvar(this.id, LENGTH_ivar).toInt() + val length: Int get() = this.msgSend("length").toInt() + + val cString: String + get() { + val length = this.length + val ba = ByteArray(length + 1) + msgSend("getCString:maxLength:encoding:", ba, length + 1, 4) + val str = ba.toString(Charsets.UTF8) + return str.substring(0, str.length - 1) + } + + override fun toString(): String = cString + + companion object : NSClass("NSString") { + val LENGTH_ivar = FFIObjc.class_getProperty(OBJ_CLASS.id, "length") + } +} + +open class NSDictionary(id: Long) : NSObject(id) { + constructor() : this(NSClass("NSDictionary").alloc().msgSend("init")) + val count: Long by objProp("count", "Q16@0:8") + val objectForKey: (objectForKey: ID) -> ID by objFunc("objectForKey:", "@24@0:8@16") +} + +class NSMutableDictionary(id: Long) : NSDictionary(id) { + constructor() : this(NSClass("NSMutableDictionary").alloc().msgSend("init")) + fun setValue(value: NSObject, forKey: NSObject) = msgSendVoid("setValue:forKey:", value.id, forKey.id) + fun getValue(key: NSObject): NSObject = NSObject(msgSend("valueForKey:", key.id)) + operator fun set(key: NSObject, value: NSObject) = setValue(value, key) + operator fun set(key: Any, value: Any) = run { this[NSObject.cast(key)] = NSObject.cast(value) } + operator fun get(key: Any): NSObject = getValue(NSObject.cast(key)) +} + +class NSNumber private constructor(id: Long) : NSObject(id) { + constructor(value: Int) : this(NSClass("NSNumber").alloc().msgSend("initWithInt:", value)) + constructor(value: Double) : this(NSClass("NSNumber").alloc().msgSend("initWithDouble:", value)) + constructor(value: Long, unit: Unit = Unit) : this(NSClass("NSNumber").alloc().msgSend("initWithLong:", value)) + val boolValue: Boolean get() = msgSendInt("boolValue") != 0 + val intValue: Int get() = msgSendInt("intValue") + val longValue: Long get() = msgSend("longValue") + //val doubleValue: Double get() = msgSendDouble("doubleValue") +} + diff --git a/korge-core/src/darwin/korlibs/ffi/FFILib.wasm.kt b/korge-core/src/darwin/korlibs/ffi/FFILib.wasm.kt index 10a38c560b..df8b777897 100644 --- a/korge-core/src/darwin/korlibs/ffi/FFILib.wasm.kt +++ b/korge-core/src/darwin/korlibs/ffi/FFILib.wasm.kt @@ -7,6 +7,10 @@ actual fun FFILibSym(lib: FFILib): FFILibSym { } } +actual fun FFICreateProxyFunction(type: KType, handler: (args: Array) -> Any?): T { + TODO() +} + actual class FFIPointer actual class FFIMemory diff --git a/korge-core/src/js/korlibs/ffi/FFILib.js.kt b/korge-core/src/js/korlibs/ffi/FFILib.js.kt index 74dfbcd68b..4de4f81333 100644 --- a/korge-core/src/js/korlibs/ffi/FFILib.js.kt +++ b/korge-core/src/js/korlibs/ffi/FFILib.js.kt @@ -69,7 +69,7 @@ actual fun FFILibSym(lib: FFILib): FFILibSym { } class FFILibSymJS(val lib: FFILib) : FFILibSym { - val symbolsByName: Map> by lazy { lib.functions.associateBy { it.name } } + val symbolsByName: Map> by lazy { lib.functions.associateBy { it.bname } } val syms: dynamic by lazy { lib as FFILib @@ -78,7 +78,7 @@ class FFILibSymJS(val lib: FFILib) : FFILibSym { Deno.dlopen( path, jsObject( *lib.functions.map { - it.name to it.type.funcToDenoDef() + it.bname to it.type.funcToDenoDef() }.toTypedArray() ) ).symbols @@ -180,6 +180,10 @@ actual class FFIArena actual constructor() { } } +actual fun FFICreateProxyFunction(type: KType, handler: (args: Array) -> Any?): T { + TODO() +} + actual typealias FFIPointer = DenoPointer actual val FFI_POINTER_SIZE: Int = 8 diff --git a/korge-core/src/jvm/korlibs/ffi/FFILib.jvm.kt b/korge-core/src/jvm/korlibs/ffi/FFILib.jvm.kt index cd8354db9c..07e5f91cda 100644 --- a/korge-core/src/jvm/korlibs/ffi/FFILib.jvm.kt +++ b/korge-core/src/jvm/korlibs/ffi/FFILib.jvm.kt @@ -16,6 +16,13 @@ actual fun FFILibSym(lib: FFILib): FFILibSym { return FFILibSymJVM(lib) } +actual fun FFICreateProxyFunction(type: KType, handler: (args: Array) -> Any?): T = Proxy.newProxyInstance( + FFILibSym::class.java.classLoader, + arrayOf((type.classifier as KClass<*>).java) +) { _, _, args -> + handler(args ?: arrayOf()) +} as T + actual typealias FFIPointer = Pointer actual typealias FFIMemory = Memory @@ -62,29 +69,34 @@ actual class FFIArena actual constructor() { } actual fun FFIPointer.castToFunc(type: KType, config: FFIFuncConfig): T = - createJNAFunctionToPlainFunc(Function.getFunction(this), type, config) + createJNAFunctionToPlainFunc(Function.getFunction(this), type, config, null) -fun > createJNAFunctionToPlainFunc(func: Function, type: KType, config: FFIFuncConfig): T { +fun > createJNAFunctionToPlainFunc(func: Function?, type: KType, config: FFIFuncConfig, name: String?): T { val ftype = FFILib.extractTypeFunc(type) + var ret = ftype.retClass as KClass<*> + val isDeferred = ret == Deferred::class + if (isDeferred) { + ret = ftype.ret?.arguments?.first()?.type?.classifier as KClass<*> + } return Proxy.newProxyInstance( FFILibSymJVM::class.java.classLoader, arrayOf((type.classifier as KClass<*>).java) ) { proxy, method, args -> - val targs = (args ?: emptyArray()).map { + if (func == null) error("Function not available") + val inputArgs = args ?: emptyArray() + val targsl = ArrayList(inputArgs.size) + for (it in inputArgs) { when (it) { - is FFIPointerArray -> it.data - is Buffer -> it.buffer - is String -> if (config.wideString) com.sun.jna.WString(it) else it - else -> it + is FFIVarargs -> targsl.addAll(it.args) + is FFIPointerArray -> targsl.add(it.data) + is Buffer -> targsl.add(it.buffer) + is String -> targsl.add(if (config.wideString) com.sun.jna.WString(it) else it) + else -> targsl.add(it) } - }.toTypedArray() - - var ret = ftype.retClass - val isDeferred = ret == Deferred::class - if (isDeferred) { - ret = ftype.ret?.arguments?.first()?.type?.classifier } + val targs = targsl.toTypedArray() + //println("name=$name, targsl=$targsl") fun call(): Any? { return when (ret) { @@ -93,6 +105,8 @@ fun > createJNAFunctionToPlainFunc(func: Function, type: Float::class -> func.invokeFloat(targs) Double::class -> func.invokeDouble(targs) else -> func.invoke((ftype.retClass as KClass<*>).java, targs) + }.also { + //println(" -> ret=$it [${ret.simpleName}]") } } @@ -103,17 +117,18 @@ fun > createJNAFunctionToPlainFunc(func: Function, type: } } else { call() + }.also { + //println(" -> $it") } } as T } inline fun > createJNAFunctionToPlainFunc(func: Function, config: FFIFuncConfig): T = - createJNAFunctionToPlainFunc(func, typeOf(), config) + createJNAFunctionToPlainFunc(func, typeOf(), config, null) class FFILibSymJVM(val lib: FFILib) : FFILibSym { @OptIn(SyncIOAPI::class) val nlib by lazy { - lib as FFILib val resolvedPaths = listOf(LibraryResolver.resolve(*lib.paths.toTypedArray())) resolvedPaths.firstNotNullOfOrNull { NativeLibrary.getInstance(it) @@ -121,14 +136,15 @@ class FFILibSymJVM(val lib: FFILib) : FFILibSym { } fun > createFunction(funcName: String, type: KType, config: FFIFuncConfig): T { - val func: Function = nlib!!.getFunction(funcName) ?: error("Can't find function ${funcName}") - return createJNAFunctionToPlainFunc(func, type, config) + val func = runCatching { nlib!!.getFunction(funcName) ?: error("Can't find function ${funcName}") } + func.exceptionOrNull()?.let { println("WARNING: ${it.message}") } + return createJNAFunctionToPlainFunc(func.getOrNull(), type, config, funcName) } val functions: Map> by lazy { lib.functions.associate { nfunc -> //val lib = NativeLibrary.getInstance("") - nfunc.name to createFunction(nfunc.name, nfunc.type, nfunc.config) + nfunc.bname to createFunction(nfunc.name, nfunc.type, nfunc.config) } } diff --git a/korge-core/test/common/korlibs/ffi/osx/FFIObjcTest.kt b/korge-core/test/common/korlibs/ffi/osx/FFIObjcTest.kt new file mode 100644 index 0000000000..3f81a54a1c --- /dev/null +++ b/korge-core/test/common/korlibs/ffi/osx/FFIObjcTest.kt @@ -0,0 +1,22 @@ +package korlibs.ffi.osx + +import korlibs.ffi.* +import korlibs.io.async.* +import korlibs.platform.* +import kotlin.test.* + +class FFIObjcTest { + @Test + fun test() = suspendTest({ Platform.isMac && FFILib.isFFISupported }){ + //println(NSDictionary().objcClass) + val dict = NSMutableDictionary() + dict[10] = 10 + //dict[10] = 20 + dict[20] = 30 + println(dict.count) + //println("dict=$dict") + //println(ObjcClassRef.listAll()) + //ObjcClassRef.fromName("NSDictionary")?.dumpKotlin() + //ObjcProtocol.fromName("NSDictionary")?.dumpKotlin() + } +} diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt index bbabf3cd94..0d8157b9df 100644 --- a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt +++ b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt @@ -233,6 +233,7 @@ object ObjcTypeParser { if (str.readChar() != ']') error("Invalid $str") FixedArrayObjcType(count, type) } + '#' -> PrimitiveObjcType.ID ':' -> PrimitiveObjcType.SEL 'C' -> PrimitiveObjcType.BYTE 'i' -> PrimitiveObjcType.INT @@ -866,13 +867,9 @@ public class NativeNSRect { pointer = Pointer(memory); } - fun free() { - Native.free(Pointer.nativeValue(pointer)); - } + fun free() = Native.free(Pointer.nativeValue(pointer)) - fun getPointer(): Pointer { - return pointer; - } + fun getPointer(): Pointer = pointer var a: Int get() = pointer.getInt(0L); set(value) { pointer.setInt(0L, value) } var b: Int get() = pointer.getInt(4L); set(value) { pointer.setInt(4L, value) } diff --git a/korge/src/jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt b/korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt similarity index 65% rename from korge/src/jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt rename to korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt index 0b38f0a17a..14e6379d6e 100644 --- a/korge/src/jvm/korlibs/event/gamepad/DarwinMacosGameControllerJvm.kt +++ b/korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt @@ -1,26 +1,24 @@ package korlibs.event.gamepad -import com.sun.jna.* +import korlibs.annotations.* import korlibs.datastructure.iterators.* import korlibs.event.* -import korlibs.io.annotations.* +import korlibs.ffi.* +import korlibs.ffi.osx.* import korlibs.math.geom.* -import korlibs.memory.dyn.osx.* import korlibs.number.* import korlibs.render.* import kotlin.reflect.* -internal interface FrameworkInt : Library - @PublishedApi -internal inline fun getValueOrNull(obj: ObjcRef, property: KProperty<*>, gen: (Long) -> T): T? = - obj.id.msgSend(property.name).takeIf { it != 0L }?.let { gen(it) } +internal inline fun getValueOrNull(obj: NSObject, property: KProperty<*>, gen: (Long) -> T): T? = + obj.msgSend(property.name).takeIf { it != 0L }?.let { gen(it) } @PublishedApi -internal inline fun getValue(obj: ObjcRef, property: KProperty<*>, gen: (Long) -> T): T = - obj.id.msgSend(property.name).let { gen(it) } +internal inline fun getValue(obj: NSObject, property: KProperty<*>, gen: (Long) -> T): T = + obj.msgSend(property.name).let { gen(it) } -internal inline class GCControllerButtonInput(val id: Long) { +internal inline class GCControllerButtonInput(val id: ObjcRef) { val analog: Boolean get() = id.msgSendInt(sel_isAnalog) != 0 val touched: Boolean get() = id.msgSendInt(sel_isTouched) != 0 val pressed: Boolean get() = id.msgSendInt(sel_isPressed) != 0 @@ -38,28 +36,29 @@ internal inline class GCControllerButtonInput(val id: Long) { val sel_isPressed = ObjcSel("isPressed") val sel_value = ObjcSel("value") - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCControllerButtonInput = - getValue(obj, property) { GCControllerButtonInput(it) } + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCControllerButtonInput = + getValue(obj, property) { GCControllerButtonInput(ObjcRef(it)) } } } -class GCControllerAxisInput(id: Long) : ObjcRef(id) { - val value: Float get() = id.msgSendFloat("value").toFloat() +class GCControllerAxisInput(id: ObjcRef) : NSObject(id) { + val value: Float get() = ref.msgSendFloat("value") companion object { - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCControllerAxisInput = GCControllerAxisInput(obj.id.msgSend(property.name)) + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCControllerAxisInput = GCControllerAxisInput(obj.msgSendRef(property.name)) } override fun toString(): String = value.niceStr(2) } -internal class GCControllerDirectionPad(id: Long) : ObjcRef(id) { - @Keep val right by GCControllerButtonInput - @Keep val left by GCControllerButtonInput - @Keep val up by GCControllerButtonInput - @Keep val down by GCControllerButtonInput +@KeepNames +internal class GCControllerDirectionPad(id: ObjcRef) : NSObject(id) { + val right by GCControllerButtonInput + val left by GCControllerButtonInput + val up by GCControllerButtonInput + val down by GCControllerButtonInput - @Keep val xAxis by GCControllerAxisInput - @Keep val yAxis by GCControllerAxisInput + val xAxis by GCControllerAxisInput + val yAxis by GCControllerAxisInput val x: Float get() = xAxis.value val y: Float get() = yAxis.value @@ -68,50 +67,53 @@ internal class GCControllerDirectionPad(id: Long) : ObjcRef(id) { val point: Point get() = _point companion object { - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCControllerDirectionPad = - getValue(obj, property) { GCControllerDirectionPad(it) } + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCControllerDirectionPad = + getValue(obj, property) { GCControllerDirectionPad(ObjcRef(it)) } } override fun toString(): String = "DPad(${up.nice}, ${right.nice}, ${down.nice}, ${left.nice})" } -internal open class GCMicroGamepad(id: Long) : ObjcRef(id) { - @Keep val buttonA by GCControllerButtonInput - @Keep val buttonX by GCControllerButtonInput - @Keep val dpad by GCControllerDirectionPad - @Keep val buttonMenu by GCControllerButtonInput +@KeepNames +internal open class GCMicroGamepad(id: ObjcRef) : NSObject(id) { + val buttonA by GCControllerButtonInput + val buttonX by GCControllerButtonInput + val dpad by GCControllerDirectionPad + val buttonMenu by GCControllerButtonInput companion object { - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCMicroGamepad? = - getValueOrNull(obj, property) { GCMicroGamepad(it) } + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCMicroGamepad? = + getValueOrNull(obj, property) { GCMicroGamepad(ObjcRef(it)) } } } -internal open class GCGamepad(id: Long) : GCMicroGamepad(id) { - @Keep val leftShoulder by GCControllerButtonInput - @Keep val rightShoulder by GCControllerButtonInput - @Keep val buttonB by GCControllerButtonInput - @Keep val buttonY by GCControllerButtonInput +@KeepNames +internal open class GCGamepad(id: ObjcRef) : GCMicroGamepad(id) { + val leftShoulder by GCControllerButtonInput + val rightShoulder by GCControllerButtonInput + val buttonB by GCControllerButtonInput + val buttonY by GCControllerButtonInput companion object { - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCGamepad? = - getValueOrNull(obj, property) { GCGamepad(it) } + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCGamepad? = + getValueOrNull(obj, property) { GCGamepad(ObjcRef(it)) } } } -internal class GCExtendedGamepad(id: Long) : GCGamepad(id) { - @Keep val leftTrigger by GCControllerButtonInput - @Keep val rightTrigger by GCControllerButtonInput - @Keep val buttonOptions by GCControllerButtonInput - @Keep val buttonHome by GCControllerButtonInput - @Keep val leftThumbstick by GCControllerDirectionPad - @Keep val rightThumbstick by GCControllerDirectionPad - @Keep val leftThumbstickButton by GCControllerButtonInput - @Keep val rightThumbstickButton by GCControllerButtonInput +@KeepNames +internal class GCExtendedGamepad(id: ObjcRef) : GCGamepad(id) { + val leftTrigger by GCControllerButtonInput + val rightTrigger by GCControllerButtonInput + val buttonOptions by GCControllerButtonInput + val buttonHome by GCControllerButtonInput + val leftThumbstick by GCControllerDirectionPad + val rightThumbstick by GCControllerDirectionPad + val leftThumbstickButton by GCControllerButtonInput + val rightThumbstickButton by GCControllerButtonInput companion object { - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCExtendedGamepad? = - getValueOrNull(obj, property) { GCExtendedGamepad(it) } + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCExtendedGamepad? = + getValueOrNull(obj, property) { GCExtendedGamepad(ObjcRef(it)) } } override fun toString(): String = "GCExtendedGamepad(dpad=$dpad, LR=[${leftThumbstick.point.niceStr(2)}, ${rightThumbstick.point.niceStr(2)}] [A=${buttonA.nice}, B=${buttonB.nice}, X=${buttonX.nice}, Y=${buttonY.nice}], L=[${leftShoulder.nice}, ${leftTrigger.nice}, ${leftThumbstickButton.nice}], R=[${rightShoulder.nice}, ${rightTrigger.nice}, ${rightThumbstickButton.nice}], SYS=[${buttonMenu.nice}, ${buttonOptions.nice}, ${buttonHome.nice}])" @@ -134,22 +136,22 @@ internal class GCExtendedGamepad(id: Long) : GCGamepad(id) { // } //} -class GCDeviceBattery(id: Long) : ObjcRef(id) { +class GCDeviceBattery(val id: ObjcRef) { val batteryLevel: Float get() = id.msgSendFloat("batteryLevel") val batteryState: Int get() = id.msgSendInt("batteryState") companion object { - inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCDeviceBattery? = - getValueOrNull(obj, property) { GCDeviceBattery(it) } + inline operator fun getValue(obj: NSObject, property: KProperty<*>): GCDeviceBattery? = + getValueOrNull(obj, property) { GCDeviceBattery(ObjcRef(it)) } } } /** * https://developer.apple.com/documentation/gamecontroller/gccontroller?language=objc */ -internal class GCController(id: Long) : ObjcRef(id) { - val isAttachedToDevice: Boolean get() = id.msgSendInt("isAttachedToDevice") != 0 - val playerIndex: Int get() = id.msgSendInt("playerIndex") +internal class GCController(id: ObjcRef) : NSObject(id) { + val isAttachedToDevice: Boolean get() = ref.msgSendInt("isAttachedToDevice") != 0 + val playerIndex: Int get() = ref.msgSendInt("playerIndex") //val physicalInputProfile: GCPhysicalInputProfile by GCPhysicalInputProfile val extendedGamepad: GCExtendedGamepad? by GCExtendedGamepad val gamepad: GCGamepad? by GCGamepad @@ -163,35 +165,33 @@ internal class GCController(id: Long) : ObjcRef(id) { val productCategory: String by lazy { NSString(id.msgSend("productCategory")).toString() } companion object { - fun controllers(): NSArray = NSArray(NSClass("GCController").msgSend("controllers")) + fun controllers(): NSArray = NSArray(NSClass("GCController").msgSendRef("controllers")) } } -class NSArray(val id: Long) : AbstractList() { +class NSArray(val id: ObjcRef) : AbstractList() { val count: Int get() = id.msgSendInt("count") override val size: Int get() = count override operator fun get(index: Int): Long = id.msgSend("objectAtIndex:", index) override fun toString(): String = "NSArray(${toList()})" } -class MacosGameController { - companion object { - @JvmStatic - fun main(args: Array) { - val gamepad = MacosGamepadEventAdapter() - val events = GameWindow() - events.onEvent(GamePadUpdateEvent) { print("$it\r") } - events.onEvents(*GamePadConnectionEvent.Type.ALL) { println(it) } - while (true) { - gamepad.updateGamepads(events) - Thread.sleep(10L) - } - } - } -} +//@JvmStatic +//fun main(args: Array) { +// val gamepad = MacosGamepadEventAdapter() +// val events = GameWindow() +// events.onEvent(GamePadUpdateEvent) { print("$it\r") } +// events.onEvents(*GamePadConnectionEvent.Type.ALL) { println(it) } +// while (true) { +// gamepad.updateGamepads(events) +// NativeThread.sleep(10.milliseconds) +// } +//} internal class MacosGamepadEventAdapter { - val lib by lazy { Native.load("/System/Library/Frameworks/GameController.framework/Versions/A/GameController", FrameworkInt::class.java) } + internal object FrameworkInt : FFILib("/System/Library/Frameworks/GameController.framework/Versions/A/GameController") + + val lib by lazy { FrameworkInt } private fun GamepadInfo.set(button: GameButton, value: Float, deadRange: Boolean = false) { rawButtons[button.index] = GamepadInfo.withoutDeadRange(value.toFloat(), apply = deadRange) } private fun GamepadInfo.set(button: GameButton, cbutton: GCControllerButtonInput, deadRange: Boolean = false) { @@ -207,7 +207,7 @@ internal class MacosGamepadEventAdapter { for (n in allControllers.indices) allControllers[n] = null GCController.controllers() //.sortedBy { it } - .map { GCController(it) } + .map { GCController(ObjcRef(it)) } .fastForEachWithIndex { index, it -> //println("index=$index") if (index in allControllers.indices) allControllers[index] = it From db35c6221e33593efab5f6a9992dc9c4624e01fc Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 23:11:30 +0200 Subject: [PATCH 05/66] Remove memory.dyn package --- korge-core/build.gradle.kts | 11 +- .../audio/sound/backend/CoreAudioImpl.kt | 3 +- .../format/CoreGraphicsImageFormatProvider.kt | 34 +- .../korlibs/internal/osx/CoreFoundation.kt | 32 + korge-foundation/build.gradle.kts | 1 - .../src/jvm/korlibs/memory/dyn/JNAExt.kt | 18 - .../src/jvm/korlibs/memory/dyn/NativeLoad.kt | 16 - .../src/jvm/korlibs/memory/dyn/osx/Cocoa.kt | 1111 ----------------- korge/build.gradle.kts | 7 +- .../jvm/korlibs/kgl/KmlGlContextDefault.kt | 1 - .../jvm/korlibs/render/awt/AwtFrameTools.kt | 7 +- korge/src/jvm/korlibs/render/osx/Cocoa.kt | 837 ++++++++++++- korge/src/jvm/korlibs/render/osx/MacGL.kt | 1 - .../jvm/korlibs/render/osx/MacosGLContext.kt | 2 - korge/src/jvm/korlibs/render/osx/metal/MTL.kt | 1 - .../jvm/korlibs/render/platform/AwtTools.kt | 2 +- .../jvm/korlibs/render/platform/INativeGL.kt | 2 +- .../korlibs/render/awt/AwtGameCanvasTest.kt | 3 +- .../korlibs/render/osx/metal/AGMetalTest.kt | 2 +- 19 files changed, 896 insertions(+), 1195 deletions(-) create mode 100644 korge-core/src/jvm/korlibs/internal/osx/CoreFoundation.kt delete mode 100644 korge-foundation/src/jvm/korlibs/memory/dyn/JNAExt.kt delete mode 100644 korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt delete mode 100644 korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt diff --git a/korge-core/build.gradle.kts b/korge-core/build.gradle.kts index 9bb9bca187..dad7b69d1f 100644 --- a/korge-core/build.gradle.kts +++ b/korge-core/build.gradle.kts @@ -5,13 +5,14 @@ dependencies { //add("androidMainApi", "androidx.javascriptengine:javascriptengine:1.0.0-alpha05") //add("androidMainApi", "com.google.guava:guava:31.0.1-android") } - add("commonMainApi", project(":korge-foundation")) - add("commonMainApi", libs.kotlinx.coroutines.core) + commonMainApi(project(":korge-foundation")) + commonMainApi(libs.kotlinx.coroutines.core) //add("commonMainApi", libs.kotlinx.atomicfu) //add("commonTestApi", project(":korge-test")) - add("commonTestApi", libs.kotlinx.coroutines.test) - add("jvmMainApi", libs.asm.core) - add("jvmTestApi", libs.asm.util) + commonTestApi(libs.kotlinx.coroutines.test) + jvmMainImplementation(libs.bundles.jna) + jvmMainImplementation(libs.asm.core) + jvmMainImplementation(libs.asm.util) } korlibs.NativeTools.configureAndroidDependency(project, libs.kotlinx.coroutines.android) diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index 6ac92c1edf..5725ea0857 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -3,9 +3,8 @@ package korlibs.audio.sound.backend import com.sun.jna.* import korlibs.audio.sound.* import korlibs.ffi.* +import korlibs.internal.osx.* import korlibs.io.annotations.* -import korlibs.memory.dyn.* -import korlibs.memory.dyn.osx.* import java.util.concurrent.* import java.util.concurrent.atomic.* import kotlin.coroutines.* diff --git a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt index a430f5102e..9e2d785f35 100644 --- a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt +++ b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt @@ -1,12 +1,13 @@ package korlibs.image.format import com.sun.jna.* +import korlibs.ffi.osx.* import korlibs.image.awt.* import korlibs.image.color.* +import korlibs.internal.osx.* import korlibs.io.annotations.* import korlibs.io.file.* import korlibs.io.file.std.* -import korlibs.memory.dyn.osx.* import korlibs.time.* import kotlinx.coroutines.* import java.awt.image.* @@ -15,29 +16,6 @@ import java.io.* open class CoreGraphicsImageFormatProvider : AwtNativeImageFormatProvider() { companion object : CoreGraphicsImageFormatProvider() - @Keep - object CoreFoundation { - // CFNumberGetValue(number: platform.CoreFoundation.CFNumberRef? /* = kotlinx.cinterop.CPointer? */, theType: platform.CoreFoundation.CFNumberType /* = kotlin.Int */, valuePtr: kotlinx.cinterop.CValuesRef<*>?): kotlin.Boolean { /* compiled code */ } - val LIB = "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation" - val lib: NativeLibrary = NativeLibrary.getInstance("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation") - val kCFBooleanFalse: Pointer? get() = lib.getGlobalVariableAddress("kCFBooleanFalse") - val kCFBooleanTrue: Pointer? get() = lib.getGlobalVariableAddress("kCFBooleanTrue") - //val kCFNumberIntType: Pointer? get() = lib.getGlobalVariableAddress("kCFNumberIntType") - val kCFNumberIntType: Int get() = 9 - - @JvmStatic external fun CFDataCreate(allocator: Pointer?, bytes: Pointer?, length: Int): Pointer? - @JvmStatic external fun CFDataCreate(allocator: Pointer?, bytes: ByteArray, length: Int): Pointer? - @JvmStatic external fun CFDataGetBytePtr(data: Pointer?): Pointer? - @JvmStatic external fun CFDictionaryCreateMutable(allocator: Pointer?, capacity: Int, keyCallbacks: Pointer?, valueCallbacks: Pointer?): Pointer? - @JvmStatic external fun CFDictionaryAddValue(theDict: Pointer?, key: Pointer?, value: Pointer?): Unit - @JvmStatic external fun CFDictionaryGetValue(dict: Pointer?, key: Pointer?): Pointer? - @JvmStatic external fun CFNumberGetValue(number: Pointer?, type: Int, holder: Pointer?) - - init { - Native.register(LIB) - } - } - @Keep object CoreGraphics { val LIB = "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics" @@ -231,3 +209,11 @@ open class CoreGraphicsImageFormatProvider : AwtNativeImageFormatProvider() { } } +private inline fun autoreleasePool(body: () -> T): T { + val autoreleasePool = if (Platform.isMac()) NSClass("NSAutoreleasePool").alloc().msgSendRef("init") else null + try { + return body() + } finally { + autoreleasePool?.msgSendVoid("drain") + } +} diff --git a/korge-core/src/jvm/korlibs/internal/osx/CoreFoundation.kt b/korge-core/src/jvm/korlibs/internal/osx/CoreFoundation.kt new file mode 100644 index 0000000000..18668ce8fd --- /dev/null +++ b/korge-core/src/jvm/korlibs/internal/osx/CoreFoundation.kt @@ -0,0 +1,32 @@ +package korlibs.internal.osx + +import com.sun.jna.* +import korlibs.annotations.* + +// @TODO: Change to a FFILib +@KeepNames +object CoreFoundation { + // CFNumberGetValue(number: platform.CoreFoundation.CFNumberRef? /* = kotlinx.cinterop.CPointer? */, theType: platform.CoreFoundation.CFNumberType /* = kotlin.Int */, valuePtr: kotlinx.cinterop.CValuesRef<*>?): kotlin.Boolean { /* compiled code */ } + val LIB = "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation" + val lib: NativeLibrary = NativeLibrary.getInstance("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation") + val kCFBooleanFalse: Pointer? get() = lib.getGlobalVariableAddress("kCFBooleanFalse") + val kCFBooleanTrue: Pointer? get() = lib.getGlobalVariableAddress("kCFBooleanTrue") + //val kCFNumberIntType: Pointer? get() = lib.getGlobalVariableAddress("kCFNumberIntType") + val kCFNumberIntType: Int get() = 9 + + @JvmStatic val kCFRunLoopCommonModes: Pointer? = lib.getGlobalVariableAddress("kCFRunLoopCommonModes").getPointer(0L) + @JvmStatic external fun CFRunLoopGetCurrent(): Pointer? + @JvmStatic external fun CFRunLoopGetMain(): Pointer? + @JvmStatic external fun CFRunLoopRun(): Void + @JvmStatic external fun CFDataCreate(allocator: Pointer?, bytes: Pointer?, length: Int): Pointer? + @JvmStatic external fun CFDataCreate(allocator: Pointer?, bytes: ByteArray, length: Int): Pointer? + @JvmStatic external fun CFDataGetBytePtr(data: Pointer?): Pointer? + @JvmStatic external fun CFDictionaryCreateMutable(allocator: Pointer?, capacity: Int, keyCallbacks: Pointer?, valueCallbacks: Pointer?): Pointer? + @JvmStatic external fun CFDictionaryAddValue(theDict: Pointer?, key: Pointer?, value: Pointer?): Unit + @JvmStatic external fun CFDictionaryGetValue(dict: Pointer?, key: Pointer?): Pointer? + @JvmStatic external fun CFNumberGetValue(number: Pointer?, type: Int, holder: Pointer?) + + init { + Native.register(LIB) + } +} diff --git a/korge-foundation/build.gradle.kts b/korge-foundation/build.gradle.kts index 00998b7556..748a009f42 100644 --- a/korge-foundation/build.gradle.kts +++ b/korge-foundation/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { //add("jvmTestApi", "org.powermock:powermock-mockito-release-full:1.6.4") //add("jvmTestApi", "org.fuin:units4j:0.8.4") //add("jvmTestApi", "org.ow2.asm:asm:8.0.1") - add("jvmMainApi", libs.bundles.jna) add("commonMainApi", libs.kotlinx.coroutines.core) //add("commonMainApi", libs.kotlinx.atomicfu) //add("commonTestApi", project(":korge-test")) diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/JNAExt.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/JNAExt.kt deleted file mode 100644 index 35ad2b288f..0000000000 --- a/korge-foundation/src/jvm/korlibs/memory/dyn/JNAExt.kt +++ /dev/null @@ -1,18 +0,0 @@ -package korlibs.memory.dyn - -import com.sun.jna.* - -fun Memory(data: IntArray): Memory { - val out = Memory(data.size.toLong() * 4) - for (n in data.indices) out.setInt((n * 4).toLong(), data[n]) - return out -} - -fun Memory(data: LongArray): Memory { - val out = Memory(data.size.toLong() * 8) - for (n in data.indices) out.setLong((n * 8).toLong(), data[n]) - return out -} - -val Pointer.address: Long get() = Pointer.nativeValue(this) -val Pointer?.addressNotNull: Long get() = Pointer.nativeValue(this) diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt deleted file mode 100644 index cf52b28e7a..0000000000 --- a/korge-foundation/src/jvm/korlibs/memory/dyn/NativeLoad.kt +++ /dev/null @@ -1,16 +0,0 @@ -package korlibs.memory.dyn - -import com.sun.jna.* - -annotation class NativeName(val name: String) { - companion object { - val OPTIONS = mapOf( - Library.OPTION_FUNCTION_MAPPER to FunctionMapper { _, method -> - method.getAnnotation(NativeName::class.java)?.name ?: method.name - } - ) - } -} - -inline fun NativeLoad(name: String): T = Native.load(name, T::class.java, NativeName.OPTIONS) as T - diff --git a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt b/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt deleted file mode 100644 index 0d8157b9df..0000000000 --- a/korge-foundation/src/jvm/korlibs/memory/dyn/osx/Cocoa.kt +++ /dev/null @@ -1,1111 +0,0 @@ -package korlibs.memory.dyn.osx - -import com.sun.jna.* -import korlibs.annotations.* -import korlibs.memory.dyn.* -import java.lang.reflect.* -import java.util.* -import java.util.concurrent.* - -//inline class ID(val id: Long) -typealias ID = Long - -typealias NSRectPtr = Pointer - -// https://developer.apple.com/documentation/objectivec/objective-c_runtime -interface ObjectiveC : Library { - fun objc_copyProtocolList(outCount: IntArray): Pointer - fun protocol_getName(protocol: Long): String - - fun objc_getClass(name: String): Long - - fun objc_getClassList(buffer: Pointer?, bufferCount: Int): Int - - fun objc_getProtocol(name: String): Long - - fun class_addProtocol(a: Long, b: Long): Long - fun class_copyMethodList(clazz: Long, items: IntArray): Pointer - - // typedef struct objc_method_description { - // SEL name; // The name of the method - // char *types; // The types of the method arguments - // } MethodDescription; - fun protocol_copyMethodDescriptionList(proto: Long, isRequiredMethod: Boolean, isInstanceMethod: Boolean, outCount: IntArray): Pointer - - fun objc_registerClassPair(cls: Long) - fun objc_lookUpClass(name: String): Long - - fun objc_msgSend(vararg args: Any?): Long - @NativeName("objc_msgSend") - fun objc_msgSendInt(vararg args: Any?): Int - @NativeName("objc_msgSend") - fun objc_msgSendVoid(vararg args: Any?): Unit - @NativeName("objc_msgSend") - fun objc_msgSendCGFloat(vararg args: Any?): CGFloat - @NativeName("objc_msgSend") - fun objc_msgSendFloat(vararg args: Any?): Float - @NativeName("objc_msgSend") - fun objc_msgSendDouble(vararg args: Any?): Double - @NativeName("objc_msgSend") - fun objc_msgSendNSPoint(vararg args: Any?): NSPointRes - @NativeName("objc_msgSend") - fun objc_msgSendNSRect(vararg args: Any?): NSRectRes - @NativeName("objc_msgSend_stret") - fun objc_msgSend_stret(structPtr: Any?, vararg args: Any?): Unit - - /* - fun objc_msgSend(a: Long, b: Long): Long - fun objc_msgSend(a: Long, b: Long, c: Long): Long - fun objc_msgSend(a: Long, b: Long, c: String): Long - fun objc_msgSend(a: Long, b: Long, c: ByteArray, d: Int, e: Int): Long - fun objc_msgSend(a: Long, b: Long, c: ByteArray, len: Int): Long - fun objc_msgSend(a: Long, b: Long, c: CharArray, len: Int): Long - */ - fun method_getName(m: Long): Long - @NativeName("method_getName") - fun method_getNameString(m: Long): String - - fun sel_registerName(name: String): Long - fun sel_getName(sel: Long): String - @NativeName("sel_getName") - fun sel_getNameString(sel: String): String - - fun objc_allocateClassPair(clazz: Long, name: String, extraBytes: Int): Long - fun object_getIvar(obj: Long, ivar: Long): Long - - fun class_getInstanceVariable(clazz: ID, name: String): ID - fun class_getProperty(clazz: ID, name: String): ID - - fun class_addMethod(cls: Long, name: Long, imp: Callback, types: String): Long - fun class_conformsToProtocol(cls: Long, protocol: Long): Boolean - - fun object_getClass(obj: ID): ID - fun class_getName(clazz: ID): String - - fun object_getClassName(obj: ID): String - fun class_getImageName(obj: ID): String - - fun property_getName(prop: ID): String - fun property_getAttributes(prop: ID): String - - fun class_getInstanceMethod(cls: ID, id: NativeLong): NativeLong - - fun method_getReturnType(id: NativeLong, dst: Pointer, dst_length: NativeLong) - fun method_getTypeEncoding(ptr: Pointer): String - - fun class_createInstance(cls: ID, extraBytes: NativeLong): ID - fun class_copyPropertyList(cls: ID, outCountPtr: IntArray): Pointer - fun class_copyIvarList(cls: ID, outCountPtr: IntArray): Pointer - fun ivar_getName(ivar: Pointer?): String? - fun ivar_getTypeEncoding(ivar: Pointer?): String? - - companion object : ObjectiveC by NativeLoad("objc") { - //val NATIVE = NativeLibrary.getInstance("objc") - } -} - -data class ObjcMethodRef(val objcClass: ObjcProtocolClassBaseRef, val ptr: Pointer) { - val name: String by lazy { ObjectiveC.method_getNameString(ptr.address) } - val selName: String by lazy { ObjectiveC.sel_getNameString(name) } - val types: String by lazy { ObjectiveC.method_getTypeEncoding(ptr) } - val parsedTypes: ObjcMethodDesc by lazy { ObjcTypeParser.parse(types) } - - override fun toString(): String = "${objcClass.name}.$name" -} - -interface ObjcProtocolClassBaseRef { - val name: String -} - -annotation class ObjcDesc(val name: String, val types: String = "") - -data class ObjcMethodDescription( - val protocol: ObjcProtocolRef, - val id: NativeLong, - val types: String -) { - val method by lazy { ObjectiveC.class_getInstanceMethod(protocol.ref, id) } - val name: String by lazy { ObjectiveC.sel_getName(id.toLong()) } - val parsedTypes: ObjcMethodDesc by lazy { ObjcTypeParser.parse(types) } - - override fun toString(): String = "ObjcMethodDescription[$name$parsedTypes]" - fun dumpKotlin() { - println(" " + createKotlinMethod(name, parsedTypes)) - } -} - -data class ObjcMethodDesc(val desc: String, val returnType: ObjcParam, val params: List) { - constructor(desc: String, all: List) : this(desc, all.first(), all.drop(1)) -} -data class ObjcParam(val offset: Int, val type: ObjcType) - -interface ObjcType { - fun toKotlinString(): String -} -data class ConstObjcType(val base: ObjcType) : ObjcType { - override fun toKotlinString(): String = base.toKotlinString() -} -data class PointerObjcType(val base: ObjcType) : ObjcType { - override fun toKotlinString(): String = "Pointer" -} -data class StructObjcType(val strName: String, val types: List) : ObjcType { - override fun toKotlinString(): String = "Struct" -} -data class FixedArrayObjcType(val count: Int, val type: ObjcType) : ObjcType { - override fun toKotlinString(): String = "FixedArray[$count]" -} - -enum class PrimitiveObjcType : ObjcType { - VOID, BOOL, ID, SEL, BYTE, INT, UINT, NINT, NUINT, FLOAT, DOUBLE, BLOCK; - - override fun toKotlinString(): String = when (this) { - VOID -> "Unit" - BOOL -> "Boolean" - ID -> "ID" - SEL -> "SEL" - BYTE -> "Byte" - INT -> "Int" - UINT -> "UInt" - NINT -> "NativeLong" - NUINT -> "NativeLong" - FLOAT -> "Float" - DOUBLE -> "Double" - BLOCK -> "(() -> Unit)" - } -} - -data class StrReader(val str: String, var pos: Int = 0, var end: Int = str.length) { - val hasMore: Boolean get() = pos < end - - fun peekChar(): Char = str.getOrElse(pos) { '\u0000' } - fun readChar(): Char = peekChar().also { skip() } - fun skip(count: Int = 1) { - pos += count - } - fun readUntil(cond: (Char) -> Boolean): String { - var out = "" - while (true) { - val c = peekChar() - if (!cond(c)) break - skip() - out += c - } - return out - } -} - -object ObjcTypeParser { - fun parseInt(str: StrReader): Int { - var out = "" - while (str.peekChar().isDigit()) { - out += str.readChar() - } - return out.toIntOrNull() ?: -1 - } - - fun parseType(str: StrReader): ObjcType { - val c = str.readChar() - return when (c) { - 'V' -> PrimitiveObjcType.VOID // Class initializer - 'v' -> PrimitiveObjcType.VOID - 'B' -> PrimitiveObjcType.BOOL - '@' -> { - if (str.peekChar() == '?') { - str.skip() - PrimitiveObjcType.BLOCK - } else { - PrimitiveObjcType.ID - } - } - '{' -> { - val name = str.readUntil { it != '=' } - if (str.readChar() != '=') error("Invalid $str") - val out = arrayListOf() - while (str.peekChar() != '}') { - out += parseType(str) - } - str.skip() - StructObjcType(name, out) - } - '[' -> { - val count = parseInt(str) - val type = parseType(str) - if (str.readChar() != ']') error("Invalid $str") - FixedArrayObjcType(count, type) - } - '#' -> PrimitiveObjcType.ID - ':' -> PrimitiveObjcType.SEL - 'C' -> PrimitiveObjcType.BYTE - 'i' -> PrimitiveObjcType.INT - 'I' -> PrimitiveObjcType.UINT - 'q' -> PrimitiveObjcType.NINT - 'Q' -> PrimitiveObjcType.NUINT - '^' -> PointerObjcType(parseType(str)) - 'r' -> ConstObjcType(parseType(str)) - 'f' -> PrimitiveObjcType.FLOAT - 'd' -> PrimitiveObjcType.DOUBLE - else -> TODO("Not implemented '$c' in $str") - } - } - fun parseParam(str: StrReader): ObjcParam { - val type = parseType(str) - val offset = parseInt(str) - return ObjcParam(offset, type) - } - fun parse(str: StrReader): ObjcMethodDesc { - val out = arrayListOf() - while (str.hasMore) { - out += parseParam(str) - } - return ObjcMethodDesc(str.str, out) - } - fun parse(str: String): ObjcMethodDesc = parse(StrReader(str)) -} - -// @TODO: Optimize this to be as fast as possible -@Suppress("NewApi") -interface ObjcDynamicInterface { - val __id: Long get() = TODO() - - @ObjcDesc("dealloc", "v16@0:8") fun dealloc(): Unit - - companion object { - inline fun createNew(init: String = "init", vararg args: Any?): T = createNew(T::class.java, init, *args) - fun createNew(clazz: Class, init: String = "init", vararg args: Any?): T { - val name = clazz.getDeclaredAnnotation(ObjcDesc::class.java)?.name ?: clazz.simpleName - return proxy(NSClass(name).alloc().msgSend(init, *args), clazz) - //return proxy(NSClass(name).alloc(), clazz) - //return proxy(ObjcClassRef.fromName(name)!!.createInstance(), clazz) - } - - fun proxy(instance: Pointer?, clazz: Class): T { - return proxy(instance?.address ?: 0L, clazz) - } - fun proxy(instance: Long, clazz: Class): T { - return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz) - ) { proxy, method, args -> - if (method.name == "get__id") { - return@newProxyInstance instance - } - if (method.name == "toString") { - val classInstance = ObjectiveC.object_getClass(instance) - val className = ObjectiveC.class_getName(classInstance) - //val className = NSString(ObjectiveC.object_getClass(instance).msgSend("name")).cString - //val className = "$classInstance" - return@newProxyInstance "ObjcDynamicInterface[$className]($instance)" - } - val nargs = (args ?: emptyArray()).map { - when (it) { - null -> 0L - is ObjcDynamicInterface -> it.__id - else -> it - } - }.toTypedArray() - val name = method.getDeclaredAnnotation(ObjcDesc::class.java)?.name ?: method.name - val returnType = method.returnType - //println(":: $clazz[$instance].$name : ${nargs.toList()}") - //if (returnType == Void::class.javaPrimitiveType) { - // val res = instance.msgSendVoid(name, *nargs) - // Unit - //} - val res = instance.msgSend(name, *nargs) - if (ObjcDynamicInterface::class.java.isAssignableFrom(returnType)) { - ObjcDynamicInterface.proxy(res, returnType as Class) - } else { - when (returnType) { - String::class.java -> NSString(res).cString - Boolean::class.java -> res != 0L - Long::class.java -> res - NativeLong::class.java -> NativeLong(res) - Pointer::class.java -> Pointer(res) - Void::class.javaPrimitiveType -> Unit - Unit::class.java -> Unit - else -> TODO() - } - } - } as T - } - } -} - -inline fun Long.asObjcDynamicInterface(): T = ObjcDynamicInterface.proxy(this, T::class.java) -inline fun Pointer.asObjcDynamicInterface(): T = ObjcDynamicInterface.proxy(this, T::class.java) - -data class ObjcProtocolRef(val ref: ID) : ObjcProtocolClassBaseRef { - override val name: String by lazy { ObjectiveC.protocol_getName(ref) } - - fun dumpKotlin() { - println("interface $name : ObjcDynamicInterface {") - for (method in listMethods()) { - method.dumpKotlin() - } - println("}") - } - - fun listMethods(): List { - val nitemsPtr = IntArray(1) - val items2 = ObjectiveC.protocol_copyMethodDescriptionList(ref, true, true, nitemsPtr) - val nitems = nitemsPtr[0] - val out = ArrayList(nitems) - //println("nitems=$nitems") - return (0 until nitems).map { n -> - val namePtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + 0).toLong()) - val typesPtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + Native.LONG_SIZE).toLong()) - val typesStr = typesPtr.toLong().toPointer().getString(0L) - ObjcMethodDescription(this, namePtr, typesStr) - //println("$selName: $typesStr") - //val selName = ObjectiveC.sel_getName(mname) - } - } - - companion object { - fun fromName(name: String): ObjcProtocolRef? = - ObjectiveC.objc_getProtocol(name).takeIf { it != 0L }?.let { ObjcProtocolRef(it) } - - fun listAll(): List { - val countPtr = IntArray(1) - val ptr = ObjectiveC.objc_copyProtocolList(countPtr) - val count = countPtr[0] - return (0 until count).map { - ObjcProtocolRef(ptr.getPointer((Native.LONG_SIZE * it).toLong()).address) - } - } - } - - override fun toString(): String = "ObjcProtocol[$name]" -} - -private fun createKotlinMethod( - name: String, parsedTypes: ObjcMethodDesc, - setName: String? = null, setParsedTypes: ObjcMethodDesc? = null, -): String { - val types = parsedTypes.desc - val parts = name.split(":") - val fullName = parts[0] - val firstArg = fullName.substringAfterLast("With").decapitalize(Locale.ENGLISH) - val baseName = fullName.substringBeforeLast("With") - val argNames = listOf(firstArg) + parts.drop(1) - val paramsWithNames = argNames.zip(parsedTypes.params.drop(2)) - val paramsStr = paramsWithNames.joinToString(", ") { "${it.first}: ${it.second.type.toKotlinString()}" } - val returnTypeStr = parsedTypes.returnType.type.toKotlinString() - if (paramsWithNames.isEmpty() && parsedTypes.returnType.type != PrimitiveObjcType.VOID) { - return "@get:ObjcDesc(\"$name\", \"$types\") val $baseName: $returnTypeStr" - } else { - return "@ObjcDesc(\"$name\", \"$types\") fun $baseName($paramsStr): $returnTypeStr" - } -} - -data class ObjcClassRef(val ref: ID) : ObjcProtocolClassBaseRef { - companion object { - fun listAll(): List = ObjectiveC.getAllClassIDs() - fun fromName(name: String): ObjcClassRef? = ObjectiveC.getClassByName(name) - } - - fun createInstance(extraBytes: Int = 0): ID { - return ObjectiveC.class_createInstance(ref, NativeLong(extraBytes.toLong())) - } - - fun dumpKotlin() { - println("class $name : ObjcDynamicInterface {") - listIVars() - listProperties() - for (method in listMethods()) { - println(" " + createKotlinMethod(method.name, method.parsedTypes)) - } - println("}") - } - - override val name: String by lazy { ObjectiveC.object_getClassName(ref) } - val imageName: String by lazy { ObjectiveC.class_getImageName(ref) } - - fun listMethods(): List { - val nitemsPtr = IntArray(1) - val items2 = ObjectiveC.class_copyMethodList(ref, nitemsPtr) - val nitems = nitemsPtr[0] - return (0 until nitems).map { - ObjcMethodRef(this, items2.getPointer((Native.LONG_SIZE * it).toLong())) - } - } - - fun listProperties() { - val outCountPtr = IntArray(1) - val properties = ObjectiveC.class_copyPropertyList(ref, outCountPtr) - val outCount = outCountPtr[0] - for (n in 0 until outCount) { - val prop = properties.getPointer(n * 8L) - val propName = ObjectiveC.property_getName(prop.address) - val attributes = ObjectiveC.property_getAttributes(prop.address) - println("* $propName : $attributes") - } - //TODO("listProperties. outCount=$outCount") - } - - fun listIVars() { - val outCountPtr = IntArray(1) - val ivars = ObjectiveC.class_copyIvarList(ref, outCountPtr) - val outCount = outCountPtr[0] - for (n in 0 until outCount) { - val ivar = ivars.getPointer(n * 8L) - val ivarName = ObjectiveC.ivar_getName(ivar) - val encoding = ObjectiveC.ivar_getTypeEncoding(ivar) - println("* ivar=$ivarName : $encoding") - } - //TODO("listIVars. outCount=$outCount") - } - - override fun toString(): String = "ObjcClass[$name]" -} - -fun ObjectiveC.getClassByName(name: String): ObjcClassRef? { - val id = ObjectiveC.objc_lookUpClass(name) - return if (id != 0L) ObjcClassRef(id) else null -} - -fun ObjectiveC.getAllClassIDs(): List { - val total = objc_getClassList(null, 0) - val data = Memory((total * 8).toLong()).also { it.clear() } - //println(data.getLong(0L)) - val total2 = objc_getClassList(data, total) - return (0 until total2).map { ObjcClassRef(data.getLong((it * 8).toLong())) } -} - -@PublishedApi -internal fun __AllocateClass(name: String, base: String, vararg protocols: String): Long { - val clazz = ObjectiveC.objc_allocateClassPair(ObjectiveC.objc_getClass(base), name, 0) - for (protocol in protocols) { - val protocolId = ObjectiveC.objc_getProtocol(protocol) - if (protocolId != 0L) { - ObjectiveC.class_addProtocol(clazz, protocolId) - } - } - return clazz -} - -inline fun AllocateClassAndRegister(name: String, base: String, vararg protocols: String, configure: AllocateClassMethodRegister.() -> Unit = {}): Long { - val clazz = __AllocateClass(name, base, *protocols) - try { - configure(AllocateClassMethodRegister(clazz)) - } finally { - ObjectiveC.objc_registerClassPair(clazz) - } - return clazz -} - -inline class AllocateClassMethodRegister(val clazz: Long) { - fun addMethod(sel: String, callback: Callback, types: String) { - ObjectiveC.class_addMethod(clazz, sel(sel), callback, types) - } -} - -interface Foundation : Library { - fun NSLog(msg: Long): Unit - fun NSMakeRect(x: CGFloat, y: CGFloat, w: CGFloat, h: CGFloat): NSRect - - //companion object : Foundation by Native.load("/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation", Foundation::class.java) as Foundation - companion object : Foundation by Native.load("Foundation", Foundation::class.java, NativeName.OPTIONS) as Foundation { - val NATIVE = NativeLibrary.getInstance("Foundation") - } -} - -interface Cocoa : Library { - companion object : Cocoa by Native.load("Cocoa", Cocoa::class.java, NativeName.OPTIONS) as Cocoa { - val NATIVE = NativeLibrary.getInstance("Cocoa") - } -} - -interface AppKit : Library { - companion object : AppKit by Native.load("AppKit", AppKit::class.java, NativeName.OPTIONS) as AppKit { - val NATIVE = NativeLibrary.getInstance("AppKit") - val NSApp = NATIVE.getGlobalVariableAddress("NSApp").getLong(0L) - } -} - -fun Foundation.NSLog(msg: NSString) = NSLog(msg.id) -fun Foundation.NSLog(msg: String) = NSLog(NSString(msg)) - -//typealias NSPointRes = Long -typealias NSPointRes = MyNativeNSPoint.ByValue -typealias NSRectRes = MyNativeNSRect.ByValue - -private val isArm64 = System.getProperty("os.arch") == "aarch64" - -// @TODO: Move Long to ObjcRef to not pollute Long scope -open class ObjcRef(val id: Long) { -} - -inline class ObjcSel(val id: Long) { - companion object { - private val selectors = ConcurrentHashMap() - - operator fun invoke(name: String): ObjcSel = - selectors.getOrPut(name) { ObjcSel(ObjectiveC.sel_registerName(name)) } - } -} - -fun sel(name: String): Long { - val value = ObjectiveC.sel_registerName(name) - if (value == 0L) error("Invalid selector '$name'") - return value -} -fun sel(name: ObjcSel): Long = name.id -fun Long.msgSend(sel: ObjcSel, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args) -fun Long.msgSend(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args) -fun Long.msgSendInt(sel: ObjcSel, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(this, sel(sel), *args) -fun Long.msgSendInt(sel: String, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(this, sel(sel), *args) -fun Long.msgSendVoid(sel: String, vararg args: Any?): Unit = ObjectiveC.objc_msgSendVoid(this, sel(sel), *args) - -fun Long.msgSendFloat(sel: ObjcSel, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(this, sel(sel), *args) -fun Long.msgSendFloat(sel: String, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(this, sel(sel), *args) -fun Long.msgSendDouble(sel: ObjcSel, vararg args: Any?): Double = ObjectiveC.objc_msgSendDouble(this, sel(sel), *args) -fun Long.msgSendDouble(sel: String, vararg args: Any?): Double = ObjectiveC.objc_msgSendDouble(this, sel(sel), *args) - -fun Long.msgSendCGFloat(sel: ObjcSel, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(this, sel(sel), *args) -fun Long.msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(this, sel(sel), *args) - -fun Long.msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = ObjectiveC.objc_msgSendNSPoint(this, sel(sel), *args) -fun Long.msgSendNSRect(sel: String, vararg args: Any?): NSRectRes { - if (isArm64) { - return ObjectiveC.objc_msgSendNSRect(this, sel(sel), *args) - } else { - val rect = Memory(32) - val out = NSRectRes() - this.msgSend_stret(rect, sel, *args) - out.x = rect.getDouble(0L) - out.y = rect.getDouble(8L) - out.width = rect.getDouble(16L) - out.height = rect.getDouble(24L) - return out - } -} - -fun Long.msgSend_stret(output: Any?, sel: String, vararg args: Any?) { - if (isArm64) error("Not available on arm64") - ObjectiveC.objc_msgSend_stret(output, this, sel(sel), *args) -} -fun Long.toPointer(): Pointer = Pointer(this) - -/* -open class NSRECT : Structure { - var x: Double = 0.0 - var y: Double = 0.0 - var width: Double = 0.0 - var height: Double = 0.0 - - constructor() : super() {} - constructor(peer: Pointer?) : super(peer) {} - - override fun getFieldOrder() = listOf("x", "y", "width", "height") - - class ByReference : NSRECT(), Structure.ByReference - class ByValue : NSRECT(), Structure.ByValue -} - */ - -operator fun Long.invoke(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args) - -open class NSObject(val id: Long) : IntegerType(8, id, false), NativeMapped { - val ptr get() = id.toPointer() - - fun msgSend(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(id, sel(sel), *args) - fun msgSendInt(sel: String, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(id, sel(sel), *args) - fun msgSendFloat(sel: String, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(id, sel(sel), *args) - fun msgSendDouble(sel: String, vararg args: Any?): Double = ObjectiveC.objc_msgSendDouble(id, sel(sel), *args) - fun msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(id, sel(sel), *args) - fun msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = ObjectiveC.objc_msgSendNSPoint(id, sel(sel), *args) - fun msgSend_stret(sel: String, vararg args: Any?): Unit = ObjectiveC.objc_msgSend_stret(id, sel(sel), *args) - - fun alloc(): Long = msgSend("alloc") - - companion object : NSClass("NSObject") { - } - - override fun toByte(): Byte = id.toByte() - override fun toChar(): Char = id.toChar() - override fun toShort(): Short = id.toShort() - override fun toInt(): Int = id.toInt() - override fun toLong(): Long = id - - override fun toNative(): Any = this.id - - override fun fromNative(nativeValue: Any, context: FromNativeContext?): Any = NSObject((nativeValue as Number).toLong()) - override fun nativeType(): Class<*> = Long::class.javaPrimitiveType!! - - val objcClass: NSClass get() = NSClass(msgSend("class")) - - override fun toString(): String = "NSObject($id)" -} - -open class NSString(id: Long) : NSObject(id) { - constructor() : this("") - constructor(id: Long?) : this(id ?: 0L) - constructor(str: String) : this(OBJ_CLASS.msgSend("alloc").msgSend("initWithCharacters:length:", str.toCharArray(), str.length)) - - //val length: Int get() = ObjectiveC.object_getIvar(this.id, LENGTH_ivar).toInt() - val length: Int get() = this.msgSend("length").toInt() - - val cString: String - get() { - val length = this.length - val ba = ByteArray(length + 1) - msgSend("getCString:maxLength:encoding:", ba, length + 1, 4) - val str = ba.toString(Charsets.UTF_8) - return str.substring(0, str.length - 1) - } - - override fun toString(): String = cString - - companion object : NSClass("NSString") { - val LENGTH_ivar = ObjectiveC.class_getProperty(OBJ_CLASS, "length") - } -} - -open class NSClass(id: Long) : NSObject(id) { - constructor(name: String) : this(ObjectiveC.objc_getClass(name)) - - val OBJ_CLASS = id - - val name by lazy { ObjectiveC.class_getName(id) } - - override fun toString(): String = "NSClass[$id]($name)" -} - -open class ObjcProtocol(val name: String) : NSObject(ObjectiveC.objc_getProtocol(name)) { - val OBJ_PROTOCOL = id -} - -fun NSClass.listClassMethods(): List = ObjC_listMethods(ObjectiveC.object_getClass(this.id)) -fun NSClass.listInstanceMethods(): List = ObjC_listMethods(this.id) - -fun ObjC_listMethods(clazz: Long): List { - val nitemsPtr = IntArray(1) - val items2 = ObjectiveC.class_copyMethodList(clazz, nitemsPtr) - val nitems = nitemsPtr[0] - val out = ArrayList(nitems) - for (n in 0 until nitems) { - val ptr = items2.getNativeLong((Native.LONG_SIZE * n).toLong()) - val mname = ObjectiveC.method_getName(ptr.toLong()) - val selName = ObjectiveC.sel_getName(mname) - out.add(selName) - } - return out -} - -fun ObjcProtocol.listMethods(): List = ObjC_listProtocolMethods(this.id) - -fun ObjC_listProtocolMethods(protocol: Long): List { - val nitemsPtr = IntArray(1) - val items2 = ObjectiveC.protocol_copyMethodDescriptionList(protocol, true, true, nitemsPtr) - val nitems = nitemsPtr[0] - val out = ArrayList(nitems) - //println("nitems=$nitems") - for (n in 0 until nitems) { - val namePtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + 0).toLong()) - val typesPtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + 1).toLong()) - val selName = ObjectiveC.sel_getName(namePtr.toLong()) - //val typesStr = Pointer(typesPtr.toLong()).getString(0L) - //println("$selName: $typesStr") - //val selName = ObjectiveC.sel_getName(mname) - out.add(selName) - } - return out -} - -class NSApplication(id: Long) : NSObject(id) { - fun setActivationPolicy(value: Int) = id.msgSend("setActivationPolicy:", value.toLong()) - - companion object : NSClass("NSApplication") { - fun sharedApplication(): NSApplication = NSApplication(OBJ_CLASS.msgSend("sharedApplication")) - } -} - -class NSWindow(id: Long) : NSObject(id) { - companion object : NSClass("NSWindow") { - operator fun invoke() { - val res = OBJ_CLASS.msgSend("alloc").msgSend("init(contentRect:styleMask:backing:defer:)") - } - - fun sharedApplication(): NSApplication = NSApplication(OBJ_CLASS.msgSend("sharedApplication")) - } -} - -interface ApplicationShouldTerminateCallback : Callback { - operator fun invoke(self: Long, _sel: Long, sender: Long): Long -} - -var running = true - -val applicationShouldTerminateCallback = object : ApplicationShouldTerminateCallback { - override fun invoke(self: Long, _sel: Long, sender: Long): Long { - println("applicationShouldTerminateCallback") - running = false - System.exit(0) - return 0L - } -} - -interface ObjcCallback : Callback { - operator fun invoke(self: Long, _sel: Long, sender: Long): Long -} - -interface ObjcCallbackVoid : Callback { - operator fun invoke(self: Long, _sel: Long, sender: Long): Unit -} - -fun ObjcCallback(callback: (self: Long, _sel: Long, sender: Long) -> Long): ObjcCallback { - return object : ObjcCallback { - override fun invoke(self: Long, _sel: Long, sender: Long): Long = callback(self, _sel, sender) - } -} - -fun ObjcCallbackVoid(callback: (self: Long, _sel: Long, sender: Long) -> Unit): ObjcCallbackVoid { - return object : ObjcCallbackVoid { - override fun invoke(self: Long, _sel: Long, sender: Long): Unit = callback(self, _sel, sender) - } -} - -fun ObjcCallbackVoidEmpty(callback: () -> Unit): ObjcCallbackVoid { - return object : ObjcCallbackVoid { - override fun invoke(self: Long, _sel: Long, sender: Long): Unit = callback() - } -} - -interface WindowWillCloseCallback : Callback { - operator fun invoke(self: Long, _sel: Long, sender: Long): Long -} - -val windowWillClose = object : WindowWillCloseCallback { - override fun invoke(self: Long, _sel: Long, sender: Long): Long { - running = false - System.exit(0) - return 0L - } -} - -fun Long.alloc(): Long = this.msgSend("alloc") -fun Long.autorelease(): Long = this.apply { this.msgSend("autorelease") } -fun T.autorelease(): T = this.apply { this.msgSend("autorelease") } - -@Structure.FieldOrder("value") -class CGFloat(val value: Double) : Number(), NativeMapped { - constructor() : this(0.0) - constructor(value: Float) : this(value.toDouble()) - constructor(value: Number) : this(value.toDouble()) - - companion object { - @JvmStatic - val SIZE = Native.LONG_SIZE - } - - override fun toByte(): Byte = value.toInt().toByte() - override fun toChar(): Char = value.toChar() - override fun toDouble(): Double = value.toDouble() - override fun toFloat(): Float = value.toFloat() - override fun toInt(): Int = value.toInt() - override fun toLong(): Long = value.toLong() - override fun toShort(): Short = value.toInt().toShort() - override fun nativeType(): Class<*> = when (SIZE) { - 4 -> Float::class.java - 8 -> Double::class.java - else -> TODO() - } - - override fun toNative(): Any = when (SIZE) { - 4 -> this.toFloat() - 8 -> this.toDouble() - else -> TODO() - } - - override fun fromNative(nativeValue: Any, context: FromNativeContext?): Any = CGFloat((nativeValue as Number).toDouble()) - - override fun toString(): String = "$value" -} - -@Structure.FieldOrder("x", "y") -public class NSPoint(@JvmField var x: CGFloat, @JvmField var y: CGFloat) : Structure(), Structure.ByValue { - constructor(x: Double, y: Double) : this(CGFloat(x), CGFloat(y)) - constructor() : this(0.0, 0.0) - - override fun toString(): String = "($x, $y)" - - companion object { - inline operator fun invoke(x: Number, y: Number) = NSPoint(x.toDouble(), y.toDouble()) - } -} - -@Structure.FieldOrder("width", "height") -public class NSSize(@JvmField var width: CGFloat, @JvmField var height: CGFloat) : Structure(), Structure.ByValue { - constructor(width: Double, height: Double) : this(CGFloat(width), CGFloat(height)) - constructor(width: Number, height: Number) : this(CGFloat(width), CGFloat(height)) - constructor() : this(0.0, 0.0) - - override fun toString(): String = "($width, $height)" - - companion object { - inline operator fun invoke(width: Number, height: Number) = NSSize(width.toDouble(), height.toDouble()) - } -} - -@Structure.FieldOrder("origin", "size") -public class NSRect( - @JvmField var origin: NSPoint, - @JvmField var size: NSSize -) : Structure(), Structure.ByValue { - constructor() : this(NSPoint(), NSSize()) { - allocateMemory() - autoWrite() - } - constructor(x: Number, y: Number, width: Number, height: Number) : this(NSPoint(x, y), NSSize(width, height)) - - override fun toString(): String = "NSRect($origin, $size)" -} - -public class NativeNSRect { - private var pointer: Pointer - - constructor() { - val memory = Native.malloc(32); - pointer = Pointer(memory); - } - - fun free() = Native.free(Pointer.nativeValue(pointer)) - - fun getPointer(): Pointer = pointer - - var a: Int get() = pointer.getInt(0L); set(value) { pointer.setInt(0L, value) } - var b: Int get() = pointer.getInt(4L); set(value) { pointer.setInt(4L, value) } - var c: Int get() = pointer.getInt(8L); set(value) { pointer.setInt(8L, value) } - var d: Int get() = pointer.getInt(12L); set(value) { pointer.setInt(12L, value) } - var e: Int get() = pointer.getInt(16L); set(value) { pointer.setInt(16L, value) } - var f: Int get() = pointer.getInt(20L); set(value) { pointer.setInt(20L, value) } - var g: Int get() = pointer.getInt(24L); set(value) { pointer.setInt(24L, value) } - var h: Int get() = pointer.getInt(28L); set(value) { pointer.setInt(28L, value) } - - override fun toString(): String = "NativeNSRect($a, $b, $c, $d, $e, $f, $g, $h)" -} - -@Structure.FieldOrder("x", "y", "width", "height") -open class MyNativeNSRect : Structure { - @JvmField var x: Double = 0.0 - @JvmField var y: Double = 0.0 - @JvmField var width: Double = 0.0 - @JvmField var height: Double = 0.0 - - constructor() { - allocateMemory() - autoWrite() - } - constructor(x: Number, y: Number, width: Number, height: Number) : this() { - this.x = x.toDouble() - this.y = y.toDouble() - this.width = width.toDouble() - this.height = height.toDouble() - } - - class ByReference() : MyNativeNSRect(), Structure.ByReference { - constructor(x: Number, y: Number, width: Number, height: Number) : this() { - this.x = x.toDouble() - this.y = y.toDouble() - this.width = width.toDouble() - this.height = height.toDouble() - } - } - class ByValue() : MyNativeNSRect(), Structure.ByValue { - constructor(x: Number, y: Number, width: Number, height: Number) : this() { - this.x = x.toDouble() - this.y = y.toDouble() - this.width = width.toDouble() - this.height = height.toDouble() - } - } - - override fun toString(): String = "NSRect($x, $y, $width, $height)" -} - -@Structure.FieldOrder("x", "y") -open class MyNativeNSPoint() : Structure() { - @JvmField var x: Double = 0.0 - @JvmField var y: Double = 0.0 - - constructor(x: Number, y: Number) : this() { - this.x = x.toDouble() - this.y = y.toDouble() - } - - init { - allocateMemory() - autoWrite() - } - - class ByReference() : MyNativeNSPoint(), Structure.ByReference { - constructor(x: Number, y: Number) : this() { - this.x = x.toDouble() - this.y = y.toDouble() - } - } - class ByValue() : MyNativeNSPoint(), Structure.ByValue { - constructor(x: Number, y: Number) : this() { - this.x = x.toDouble() - this.y = y.toDouble() - } - } - - override fun toString(): String = "NSPoint($x, $y)" -} - -@Structure.FieldOrder("x", "y") -open class MyNativeNSPointLong() : Structure() { - @JvmField var x: Long = 0L - @JvmField var y: Long = 0L - - init { - allocateMemory() - autoWrite() - } - - class ByReference : MyNativeNSPoint(), Structure.ByReference - class ByValue : MyNativeNSPoint(), Structure.ByValue - - override fun toString(): String = "NSPoint($x, $y)" -} - -inline class NSMenuItem(val id: Long) { - constructor() : this(NSClass("NSMenuItem").alloc().msgSend("init")) - constructor(text: String, sel: String, keyEquivalent: String) : this( - NSClass("NSMenuItem").alloc().msgSend( - "initWithTitle:action:keyEquivalent:", - NSString(text).id, - sel(sel), - NSString(keyEquivalent).id - ).autorelease() - ) - - companion object { - operator fun invoke(callback: NSMenuItem.() -> Unit) = NSMenuItem().apply(callback) - } - - fun setSubmenu(menu: NSMenu) { - id.msgSend("setSubmenu:", menu.id) - } -} - -inline class NSMenu(val id: Long) { - constructor() : this(NSClass("NSMenu").alloc().msgSend("init")) - - companion object { - operator fun invoke(callback: NSMenu.() -> Unit) = NSMenu().apply(callback) - } - - fun addItem(menuItem: NSMenuItem) { - id.msgSend("addItem:", menuItem.id) - } -} - -inline fun autoreleasePool(body: () -> T): T { - val autoreleasePool = if (Platform.isMac()) NSClass("NSAutoreleasePool").alloc().msgSend("init") else null - try { - return body() - } finally { - autoreleasePool?.msgSend("drain") - } -} - -@Keep -object CoreFoundation { - val library = NativeLibrary.getInstance("CoreFoundation") - @JvmStatic val kCFRunLoopCommonModes: Pointer? = library.getGlobalVariableAddress("kCFRunLoopCommonModes").getPointer(0L) - @JvmStatic val kCFBooleanTrue: Pointer? = library.getGlobalVariableAddress("kCFBooleanTrue").getPointer(0L) - @JvmStatic val kCFBooleanFalse: Pointer? = library.getGlobalVariableAddress("kCFBooleanFalse").getPointer(0L) - @JvmStatic external fun CFRunLoopGetCurrent(): Pointer? - @JvmStatic external fun CFRunLoopGetMain(): Pointer? - @JvmStatic external fun CFRunLoopRun(): Void - - init { - Native.register("CoreFoundation") - } -} - -class CFBoolean private constructor(id: Long) : NSObject(id) { - val value: Boolean get() = this.id == TRUE.id - - companion object { - val TRUE = CFBoolean(CoreFoundation.kCFBooleanTrue.addressNotNull) - val FALSE = CFBoolean(CoreFoundation.kCFBooleanFalse.addressNotNull) - - operator fun invoke(value: Boolean): CFBoolean = if (value) TRUE else FALSE - } -} - -class NSNumber private constructor(id: Long) : NSObject(id) { - constructor(value: Int) : this(NSClass("NSNumber").alloc().msgSend("initWithInt:", value)) - constructor(value: Double) : this(NSClass("NSNumber").alloc().msgSend("initWithDouble:", value)) - constructor(value: Long, unit: Unit = Unit) : this(NSClass("NSNumber").alloc().msgSend("initWithLong:", value)) - val boolValue: Boolean get() = id.msgSendInt("boolValue") != 0 - val intValue: Int get() = id.msgSendInt("intValue") - val longValue: Long get() = id.msgSend("longValue") - val doubleValue: Double get() = id.msgSendDouble("doubleValue") -} - -fun NSObject.Companion.cast(value: Any): NSObject { - return when (value) { - is NSObject -> value - is String -> NSString(value) - is Int -> NSNumber(value) - is Long -> NSNumber(value) - is Double -> NSNumber(value) - is Boolean -> CFBoolean(value) - else -> TODO("Unsupported value '$value' to be cast to NSObject") - } -} - -class NSMutableDictionary(id: Long) : NSObject(id) { - constructor() : this(NSClass("NSMutableDictionary").alloc().msgSend("init")) - val count: Int get() = id.msgSendInt("count") - fun setValue(value: NSObject, forKey: NSObject) { - id.msgSend("setValue:forKey:", value.id, forKey.id) - } - fun getValue(key: NSObject): NSObject { - return NSObject(id.msgSend("valueForKey:", key.id)) - } - operator fun set(key: NSObject, value: NSObject) { - setValue(value, key) - } - operator fun set(key: Any, value: Any) { - this[NSObject.cast(key)] = NSObject.cast(value) - } - - operator fun get(key: Any): NSObject { - return getValue(NSObject.cast(key)) - } -} - -class NSDictionary(id: Long) : NSObject(id) { - constructor() : this(NSClass("NSDictionary").alloc().msgSend("init")) - val count: Int get() = id.msgSendInt("count") -} - -/* -interface DisplayLinkCallback : Callback { - fun callback(displayLink: Pointer?, inNow: Pointer?, inOutputTime: Pointer?, flagsIn: Pointer?, flagsOut: Pointer?, userInfo: Pointer?): Int -} - -interface CoreGraphics : Library { - fun CGMainDisplayID(): Int - companion object : CoreGraphics by NativeLoad("/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics") -} - -interface CoreVideo : Library { - fun CVDisplayLinkCreateWithCGDisplay(displayId: Int, ptr: Pointer?): Int - fun CVDisplayLinkSetOutputCallback(displayLinkValue: Pointer?, callback: Callback?, userInfo: Pointer?): Int - fun CVDisplayLinkStart(displayLinkValue: Pointer?): Int - fun CVDisplayLinkStop(displayLinkValue: Pointer?): Int - companion object : CoreVideo by NativeLoad("/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",) -} -*/ - -fun JnaMemory(array: IntArray): Memory { - val mem = Memory((array.size * 4).toLong()) - for (n in 0 until array.size) { - mem.setInt((n * 4).toLong(), array[n]) - } - return mem -} diff --git a/korge/build.gradle.kts b/korge/build.gradle.kts index 66b499ffc0..61bede0257 100644 --- a/korge/build.gradle.kts +++ b/korge/build.gradle.kts @@ -1,4 +1,4 @@ -import korlibs.applyProjectProperties +import korlibs.* description = "Multiplatform Game Engine written in Kotlin" @@ -15,8 +15,9 @@ dependencies { commonMainApi(project(":korge-foundation")) //commonTestApi(project(":korge-test")) jvmMainApi("org.jetbrains.kotlin:kotlin-reflect") - jvmMainApi(libs.jackson.databind) - jvmMainApi(libs.jackson.module.kotlin) + jvmMainImplementation(libs.jackson.databind) + jvmMainImplementation(libs.jackson.module.kotlin) + jvmMainImplementation(libs.bundles.jna) //commonTestApi(testFixtures(project(":korma"))) diff --git a/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt b/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt index 86795e93ba..0bbfecda8c 100644 --- a/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt +++ b/korge/src/jvm/korlibs/kgl/KmlGlContextDefault.kt @@ -5,7 +5,6 @@ import com.sun.jna.platform.win32.* import korlibs.io.lang.* import korlibs.logger.* import korlibs.platform.Platform -import korlibs.memory.dyn.* import korlibs.render.* import korlibs.render.osx.* import korlibs.render.platform.* diff --git a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt index a531ecd067..0ccb6e2923 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt @@ -3,6 +3,7 @@ package korlibs.render.awt import korlibs.datastructure.* import korlibs.event.* import korlibs.event.MouseEvent +import korlibs.ffi.osx.* import korlibs.graphics.* import korlibs.image.awt.* import korlibs.io.dynamic.* @@ -11,7 +12,6 @@ import korlibs.io.file.std.* import korlibs.math.* import korlibs.math.geom.* import korlibs.memory.* -import korlibs.memory.dyn.osx.* import korlibs.platform.* import korlibs.render.* import korlibs.render.MenuItem @@ -236,8 +236,8 @@ fun Component.hapticFeedbackGenerate(kind: HapticFeedbackKind) { val performanceTime = PERFORMANCE_TIME_NOW NSClass("NSHapticFeedbackManager") - .msgSend("defaultPerformer") - .msgSend("performFeedbackPattern:performanceTime:", kindInt.toLong(), performanceTime.toLong()) + .msgSendRef("defaultPerformer") + .msgSendVoid("performFeedbackPattern:performanceTime:", kindInt.toLong(), performanceTime.toLong()) } else -> { Unit @@ -506,5 +506,4 @@ fun Component.registerKeyEvents(gameWindow: GameWindow) { override fun keyPressed(e: JKeyEvent) = handleKeyEvent(e) override fun keyReleased(e: JKeyEvent) = handleKeyEvent(e) }) - } diff --git a/korge/src/jvm/korlibs/render/osx/Cocoa.kt b/korge/src/jvm/korlibs/render/osx/Cocoa.kt index 6af4343a19..ac23f51204 100644 --- a/korge/src/jvm/korlibs/render/osx/Cocoa.kt +++ b/korge/src/jvm/korlibs/render/osx/Cocoa.kt @@ -1,9 +1,11 @@ package korlibs.render.osx import com.sun.jna.* -import korlibs.memory.dyn.* -import korlibs.memory.dyn.osx.* +import korlibs.annotations.* import korlibs.render.platform.* +import java.lang.reflect.* +import java.util.* +import java.util.concurrent.* class NSApplication(id: Long) : NSObject(id) { fun setActivationPolicy(value: Int) = id.msgSend("setActivationPolicy:", value.toLong()) @@ -396,3 +398,834 @@ fun JnaMemory(array: IntArray): Memory { } return mem } +//inline class ID(val id: Long) +typealias ID = Long + +typealias NSRectPtr = Pointer + +// https://developer.apple.com/documentation/objectivec/objective-c_runtime +interface ObjectiveC : Library { + fun objc_copyProtocolList(outCount: IntArray): Pointer + fun protocol_getName(protocol: Long): String + + fun objc_getClass(name: String): Long + + fun objc_getClassList(buffer: Pointer?, bufferCount: Int): Int + + fun objc_getProtocol(name: String): Long + + fun class_addProtocol(a: Long, b: Long): Long + fun class_copyMethodList(clazz: Long, items: IntArray): Pointer + + // typedef struct objc_method_description { + // SEL name; // The name of the method + // char *types; // The types of the method arguments + // } MethodDescription; + fun protocol_copyMethodDescriptionList(proto: Long, isRequiredMethod: Boolean, isInstanceMethod: Boolean, outCount: IntArray): Pointer + + fun objc_registerClassPair(cls: Long) + fun objc_lookUpClass(name: String): Long + + fun objc_msgSend(vararg args: Any?): Long + @NativeName("objc_msgSend") + fun objc_msgSendInt(vararg args: Any?): Int + @NativeName("objc_msgSend") + fun objc_msgSendVoid(vararg args: Any?): Unit + @NativeName("objc_msgSend") + fun objc_msgSendCGFloat(vararg args: Any?): CGFloat + @NativeName("objc_msgSend") + fun objc_msgSendFloat(vararg args: Any?): Float + @NativeName("objc_msgSend") + fun objc_msgSendDouble(vararg args: Any?): Double + @NativeName("objc_msgSend") + fun objc_msgSendNSPoint(vararg args: Any?): NSPointRes + @NativeName("objc_msgSend") + fun objc_msgSendNSRect(vararg args: Any?): NSRectRes + @NativeName("objc_msgSend_stret") + fun objc_msgSend_stret(structPtr: Any?, vararg args: Any?): Unit + + /* + fun objc_msgSend(a: Long, b: Long): Long + fun objc_msgSend(a: Long, b: Long, c: Long): Long + fun objc_msgSend(a: Long, b: Long, c: String): Long + fun objc_msgSend(a: Long, b: Long, c: ByteArray, d: Int, e: Int): Long + fun objc_msgSend(a: Long, b: Long, c: ByteArray, len: Int): Long + fun objc_msgSend(a: Long, b: Long, c: CharArray, len: Int): Long + */ + fun method_getName(m: Long): Long + @NativeName("method_getName") + fun method_getNameString(m: Long): String + + fun sel_registerName(name: String): Long + fun sel_getName(sel: Long): String + @NativeName("sel_getName") + fun sel_getNameString(sel: String): String + + fun objc_allocateClassPair(clazz: Long, name: String, extraBytes: Int): Long + fun object_getIvar(obj: Long, ivar: Long): Long + + fun class_getInstanceVariable(clazz: ID, name: String): ID + fun class_getProperty(clazz: ID, name: String): ID + + fun class_addMethod(cls: Long, name: Long, imp: Callback, types: String): Long + fun class_conformsToProtocol(cls: Long, protocol: Long): Boolean + + fun object_getClass(obj: ID): ID + fun class_getName(clazz: ID): String + + fun object_getClassName(obj: ID): String + fun class_getImageName(obj: ID): String + + fun property_getName(prop: ID): String + fun property_getAttributes(prop: ID): String + + fun class_getInstanceMethod(cls: ID, id: NativeLong): NativeLong + + fun method_getReturnType(id: NativeLong, dst: Pointer, dst_length: NativeLong) + fun method_getTypeEncoding(ptr: Pointer): String + + fun class_createInstance(cls: ID, extraBytes: NativeLong): ID + fun class_copyPropertyList(cls: ID, outCountPtr: IntArray): Pointer + fun class_copyIvarList(cls: ID, outCountPtr: IntArray): Pointer + fun ivar_getName(ivar: Pointer?): String? + fun ivar_getTypeEncoding(ivar: Pointer?): String? + + companion object : ObjectiveC by NativeLoad("objc") { + //val NATIVE = NativeLibrary.getInstance("objc") + } +} + +data class ObjcMethodRef(val objcClass: ObjcProtocolClassBaseRef, val ptr: Pointer) { + val name: String by lazy { ObjectiveC.method_getNameString(ptr.address) } + val selName: String by lazy { ObjectiveC.sel_getNameString(name) } + val types: String by lazy { ObjectiveC.method_getTypeEncoding(ptr) } + val parsedTypes: ObjcMethodDesc by lazy { ObjcTypeParser.parse(types) } + + override fun toString(): String = "${objcClass.name}.$name" +} + +interface ObjcProtocolClassBaseRef { + val name: String +} + +annotation class ObjcDesc(val name: String, val types: String = "") + +data class ObjcMethodDescription( + val protocol: ObjcProtocolRef, + val id: NativeLong, + val types: String +) { + val method by lazy { ObjectiveC.class_getInstanceMethod(protocol.ref, id) } + val name: String by lazy { ObjectiveC.sel_getName(id.toLong()) } + val parsedTypes: ObjcMethodDesc by lazy { ObjcTypeParser.parse(types) } + + override fun toString(): String = "ObjcMethodDescription[$name$parsedTypes]" + fun dumpKotlin() { + println(" " + createKotlinMethod(name, parsedTypes)) + } +} + +data class ObjcMethodDesc(val desc: String, val returnType: ObjcParam, val params: List) { + constructor(desc: String, all: List) : this(desc, all.first(), all.drop(1)) +} +data class ObjcParam(val offset: Int, val type: ObjcType) + +interface ObjcType { + fun toKotlinString(): String +} +data class ConstObjcType(val base: ObjcType) : ObjcType { + override fun toKotlinString(): String = base.toKotlinString() +} +data class PointerObjcType(val base: ObjcType) : ObjcType { + override fun toKotlinString(): String = "Pointer" +} +data class StructObjcType(val strName: String, val types: List) : ObjcType { + override fun toKotlinString(): String = "Struct" +} +data class FixedArrayObjcType(val count: Int, val type: ObjcType) : ObjcType { + override fun toKotlinString(): String = "FixedArray[$count]" +} + +enum class PrimitiveObjcType : ObjcType { + VOID, BOOL, ID, SEL, BYTE, INT, UINT, NINT, NUINT, FLOAT, DOUBLE, BLOCK; + + override fun toKotlinString(): String = when (this) { + VOID -> "Unit" + BOOL -> "Boolean" + ID -> "ID" + SEL -> "SEL" + BYTE -> "Byte" + INT -> "Int" + UINT -> "UInt" + NINT -> "NativeLong" + NUINT -> "NativeLong" + FLOAT -> "Float" + DOUBLE -> "Double" + BLOCK -> "(() -> Unit)" + } +} + +data class StrReader(val str: String, var pos: Int = 0, var end: Int = str.length) { + val hasMore: Boolean get() = pos < end + + fun peekChar(): Char = str.getOrElse(pos) { '\u0000' } + fun readChar(): Char = peekChar().also { skip() } + fun skip(count: Int = 1) { + pos += count + } + fun readUntil(cond: (Char) -> Boolean): String { + var out = "" + while (true) { + val c = peekChar() + if (!cond(c)) break + skip() + out += c + } + return out + } +} + +object ObjcTypeParser { + fun parseInt(str: StrReader): Int { + var out = "" + while (str.peekChar().isDigit()) { + out += str.readChar() + } + return out.toIntOrNull() ?: -1 + } + + fun parseType(str: StrReader): ObjcType { + val c = str.readChar() + return when (c) { + 'V' -> PrimitiveObjcType.VOID // Class initializer + 'v' -> PrimitiveObjcType.VOID + 'B' -> PrimitiveObjcType.BOOL + '@' -> { + if (str.peekChar() == '?') { + str.skip() + PrimitiveObjcType.BLOCK + } else { + PrimitiveObjcType.ID + } + } + '{' -> { + val name = str.readUntil { it != '=' } + if (str.readChar() != '=') error("Invalid $str") + val out = arrayListOf() + while (str.peekChar() != '}') { + out += parseType(str) + } + str.skip() + StructObjcType(name, out) + } + '[' -> { + val count = parseInt(str) + val type = parseType(str) + if (str.readChar() != ']') error("Invalid $str") + FixedArrayObjcType(count, type) + } + '#' -> PrimitiveObjcType.ID + ':' -> PrimitiveObjcType.SEL + 'C' -> PrimitiveObjcType.BYTE + 'i' -> PrimitiveObjcType.INT + 'I' -> PrimitiveObjcType.UINT + 'q' -> PrimitiveObjcType.NINT + 'Q' -> PrimitiveObjcType.NUINT + '^' -> PointerObjcType(parseType(str)) + 'r' -> ConstObjcType(parseType(str)) + 'f' -> PrimitiveObjcType.FLOAT + 'd' -> PrimitiveObjcType.DOUBLE + else -> TODO("Not implemented '$c' in $str") + } + } + fun parseParam(str: StrReader): ObjcParam { + val type = parseType(str) + val offset = parseInt(str) + return ObjcParam(offset, type) + } + fun parse(str: StrReader): ObjcMethodDesc { + val out = arrayListOf() + while (str.hasMore) { + out += parseParam(str) + } + return ObjcMethodDesc(str.str, out) + } + fun parse(str: String): ObjcMethodDesc = parse(StrReader(str)) +} + +// @TODO: Optimize this to be as fast as possible +@Suppress("NewApi") +interface ObjcDynamicInterface { + val __id: Long get() = TODO() + + @ObjcDesc("dealloc", "v16@0:8") fun dealloc(): Unit + + companion object { + inline fun createNew(init: String = "init", vararg args: Any?): T = createNew(T::class.java, init, *args) + fun createNew(clazz: Class, init: String = "init", vararg args: Any?): T { + val name = clazz.getDeclaredAnnotation(ObjcDesc::class.java)?.name ?: clazz.simpleName + return proxy(NSClass(name).alloc().msgSend(init, *args), clazz) + //return proxy(NSClass(name).alloc(), clazz) + //return proxy(ObjcClassRef.fromName(name)!!.createInstance(), clazz) + } + + fun proxy(instance: Pointer?, clazz: Class): T { + return proxy(instance?.address ?: 0L, clazz) + } + fun proxy(instance: Long, clazz: Class): T { + return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz) + ) { proxy, method, args -> + if (method.name == "get__id") { + return@newProxyInstance instance + } + if (method.name == "toString") { + val classInstance = ObjectiveC.object_getClass(instance) + val className = ObjectiveC.class_getName(classInstance) + //val className = NSString(ObjectiveC.object_getClass(instance).msgSend("name")).cString + //val className = "$classInstance" + return@newProxyInstance "ObjcDynamicInterface[$className]($instance)" + } + val nargs = (args ?: emptyArray()).map { + when (it) { + null -> 0L + is ObjcDynamicInterface -> it.__id + else -> it + } + }.toTypedArray() + val name = method.getDeclaredAnnotation(ObjcDesc::class.java)?.name ?: method.name + val returnType = method.returnType + //println(":: $clazz[$instance].$name : ${nargs.toList()}") + //if (returnType == Void::class.javaPrimitiveType) { + // val res = instance.msgSendVoid(name, *nargs) + // Unit + //} + val res = instance.msgSend(name, *nargs) + if (ObjcDynamicInterface::class.java.isAssignableFrom(returnType)) { + ObjcDynamicInterface.proxy(res, returnType as Class) + } else { + when (returnType) { + String::class.java -> NSString(res).cString + Boolean::class.java -> res != 0L + Long::class.java -> res + NativeLong::class.java -> NativeLong(res) + Pointer::class.java -> Pointer(res) + Void::class.javaPrimitiveType -> Unit + Unit::class.java -> Unit + else -> TODO() + } + } + } as T + } + } +} + +inline fun Long.asObjcDynamicInterface(): T = ObjcDynamicInterface.proxy(this, T::class.java) +inline fun Pointer.asObjcDynamicInterface(): T = ObjcDynamicInterface.proxy(this, T::class.java) + +data class ObjcProtocolRef(val ref: ID) : ObjcProtocolClassBaseRef { + override val name: String by lazy { ObjectiveC.protocol_getName(ref) } + + fun dumpKotlin() { + println("interface $name : ObjcDynamicInterface {") + for (method in listMethods()) { + method.dumpKotlin() + } + println("}") + } + + fun listMethods(): List { + val nitemsPtr = IntArray(1) + val items2 = ObjectiveC.protocol_copyMethodDescriptionList(ref, true, true, nitemsPtr) + val nitems = nitemsPtr[0] + val out = ArrayList(nitems) + //println("nitems=$nitems") + return (0 until nitems).map { n -> + val namePtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + 0).toLong()) + val typesPtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + Native.LONG_SIZE).toLong()) + val typesStr = typesPtr.toLong().toPointer().getString(0L) + ObjcMethodDescription(this, namePtr, typesStr) + //println("$selName: $typesStr") + //val selName = ObjectiveC.sel_getName(mname) + } + } + + companion object { + fun fromName(name: String): ObjcProtocolRef? = + ObjectiveC.objc_getProtocol(name).takeIf { it != 0L }?.let { ObjcProtocolRef(it) } + + fun listAll(): List { + val countPtr = IntArray(1) + val ptr = ObjectiveC.objc_copyProtocolList(countPtr) + val count = countPtr[0] + return (0 until count).map { + ObjcProtocolRef(ptr.getPointer((Native.LONG_SIZE * it).toLong()).address) + } + } + } + + override fun toString(): String = "ObjcProtocol[$name]" +} + +private fun createKotlinMethod( + name: String, parsedTypes: ObjcMethodDesc, + setName: String? = null, setParsedTypes: ObjcMethodDesc? = null, +): String { + val types = parsedTypes.desc + val parts = name.split(":") + val fullName = parts[0] + val firstArg = fullName.substringAfterLast("With").decapitalize(Locale.ENGLISH) + val baseName = fullName.substringBeforeLast("With") + val argNames = listOf(firstArg) + parts.drop(1) + val paramsWithNames = argNames.zip(parsedTypes.params.drop(2)) + val paramsStr = paramsWithNames.joinToString(", ") { "${it.first}: ${it.second.type.toKotlinString()}" } + val returnTypeStr = parsedTypes.returnType.type.toKotlinString() + if (paramsWithNames.isEmpty() && parsedTypes.returnType.type != PrimitiveObjcType.VOID) { + return "@get:ObjcDesc(\"$name\", \"$types\") val $baseName: $returnTypeStr" + } else { + return "@ObjcDesc(\"$name\", \"$types\") fun $baseName($paramsStr): $returnTypeStr" + } +} + +data class ObjcClassRef(val ref: ID) : ObjcProtocolClassBaseRef { + companion object { + fun listAll(): List = ObjectiveC.getAllClassIDs() + fun fromName(name: String): ObjcClassRef? = ObjectiveC.getClassByName(name) + } + + fun createInstance(extraBytes: Int = 0): ID { + return ObjectiveC.class_createInstance(ref, NativeLong(extraBytes.toLong())) + } + + fun dumpKotlin() { + println("class $name : ObjcDynamicInterface {") + listIVars() + listProperties() + for (method in listMethods()) { + println(" " + createKotlinMethod(method.name, method.parsedTypes)) + } + println("}") + } + + override val name: String by lazy { ObjectiveC.object_getClassName(ref) } + val imageName: String by lazy { ObjectiveC.class_getImageName(ref) } + + fun listMethods(): List { + val nitemsPtr = IntArray(1) + val items2 = ObjectiveC.class_copyMethodList(ref, nitemsPtr) + val nitems = nitemsPtr[0] + return (0 until nitems).map { + ObjcMethodRef(this, items2.getPointer((Native.LONG_SIZE * it).toLong())) + } + } + + fun listProperties() { + val outCountPtr = IntArray(1) + val properties = ObjectiveC.class_copyPropertyList(ref, outCountPtr) + val outCount = outCountPtr[0] + for (n in 0 until outCount) { + val prop = properties.getPointer(n * 8L) + val propName = ObjectiveC.property_getName(prop.address) + val attributes = ObjectiveC.property_getAttributes(prop.address) + println("* $propName : $attributes") + } + //TODO("listProperties. outCount=$outCount") + } + + fun listIVars() { + val outCountPtr = IntArray(1) + val ivars = ObjectiveC.class_copyIvarList(ref, outCountPtr) + val outCount = outCountPtr[0] + for (n in 0 until outCount) { + val ivar = ivars.getPointer(n * 8L) + val ivarName = ObjectiveC.ivar_getName(ivar) + val encoding = ObjectiveC.ivar_getTypeEncoding(ivar) + println("* ivar=$ivarName : $encoding") + } + //TODO("listIVars. outCount=$outCount") + } + + override fun toString(): String = "ObjcClass[$name]" +} + +fun ObjectiveC.getClassByName(name: String): ObjcClassRef? { + val id = ObjectiveC.objc_lookUpClass(name) + return if (id != 0L) ObjcClassRef(id) else null +} + +fun ObjectiveC.getAllClassIDs(): List { + val total = objc_getClassList(null, 0) + val data = Memory((total * 8).toLong()).also { it.clear() } + //println(data.getLong(0L)) + val total2 = objc_getClassList(data, total) + return (0 until total2).map { ObjcClassRef(data.getLong((it * 8).toLong())) } +} + +@PublishedApi +internal fun __AllocateClass(name: String, base: String, vararg protocols: String): Long { + val clazz = ObjectiveC.objc_allocateClassPair(ObjectiveC.objc_getClass(base), name, 0) + for (protocol in protocols) { + val protocolId = ObjectiveC.objc_getProtocol(protocol) + if (protocolId != 0L) { + ObjectiveC.class_addProtocol(clazz, protocolId) + } + } + return clazz +} + +inline fun AllocateClassAndRegister(name: String, base: String, vararg protocols: String, configure: AllocateClassMethodRegister.() -> Unit = {}): Long { + val clazz = __AllocateClass(name, base, *protocols) + try { + configure(AllocateClassMethodRegister(clazz)) + } finally { + ObjectiveC.objc_registerClassPair(clazz) + } + return clazz +} + +inline class AllocateClassMethodRegister(val clazz: Long) { + fun addMethod(sel: String, callback: Callback, types: String) { + ObjectiveC.class_addMethod(clazz, sel(sel), callback, types) + } +} + +//typealias NSPointRes = Long +typealias NSPointRes = MyNativeNSPoint.ByValue +typealias NSRectRes = MyNativeNSRect.ByValue + +private val isArm64 = System.getProperty("os.arch") == "aarch64" + +// @TODO: Move Long to ObjcRef to not pollute Long scope +open class ObjcRef(val id: Long) { +} + +inline class ObjcSel(val id: Long) { + companion object { + private val selectors = ConcurrentHashMap() + + operator fun invoke(name: String): ObjcSel = + selectors.getOrPut(name) { ObjcSel(ObjectiveC.sel_registerName(name)) } + } +} + +fun sel(name: String): Long { + val value = ObjectiveC.sel_registerName(name) + if (value == 0L) error("Invalid selector '$name'") + return value +} +fun sel(name: ObjcSel): Long = name.id +fun Long.msgSend(sel: ObjcSel, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args) +fun Long.msgSend(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args) +fun Long.msgSendInt(sel: ObjcSel, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(this, sel(sel), *args) +fun Long.msgSendInt(sel: String, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(this, sel(sel), *args) +fun Long.msgSendVoid(sel: String, vararg args: Any?): Unit = ObjectiveC.objc_msgSendVoid(this, sel(sel), *args) + +fun Long.msgSendFloat(sel: ObjcSel, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(this, sel(sel), *args) +fun Long.msgSendFloat(sel: String, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(this, sel(sel), *args) +fun Long.msgSendDouble(sel: ObjcSel, vararg args: Any?): Double = ObjectiveC.objc_msgSendDouble(this, sel(sel), *args) +fun Long.msgSendDouble(sel: String, vararg args: Any?): Double = ObjectiveC.objc_msgSendDouble(this, sel(sel), *args) + +fun Long.msgSendCGFloat(sel: ObjcSel, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(this, sel(sel), *args) +fun Long.msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(this, sel(sel), *args) + +fun Long.msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = ObjectiveC.objc_msgSendNSPoint(this, sel(sel), *args) +fun Long.msgSendNSRect(sel: String, vararg args: Any?): NSRectRes { + if (isArm64) { + return ObjectiveC.objc_msgSendNSRect(this, sel(sel), *args) + } else { + val rect = Memory(32) + val out = NSRectRes() + this.msgSend_stret(rect, sel, *args) + out.x = rect.getDouble(0L) + out.y = rect.getDouble(8L) + out.width = rect.getDouble(16L) + out.height = rect.getDouble(24L) + return out + } +} + +fun Long.msgSend_stret(output: Any?, sel: String, vararg args: Any?) { + if (isArm64) error("Not available on arm64") + ObjectiveC.objc_msgSend_stret(output, this, sel(sel), *args) +} +fun Long.toPointer(): Pointer = Pointer(this) + +/* +open class NSRECT : Structure { + var x: Double = 0.0 + var y: Double = 0.0 + var width: Double = 0.0 + var height: Double = 0.0 + + constructor() : super() {} + constructor(peer: Pointer?) : super(peer) {} + + override fun getFieldOrder() = listOf("x", "y", "width", "height") + + class ByReference : NSRECT(), Structure.ByReference + class ByValue : NSRECT(), Structure.ByValue +} + */ + +operator fun Long.invoke(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args) + +open class NSObject(val id: Long) : IntegerType(8, id, false), NativeMapped { + val ptr get() = id.toPointer() + + fun msgSend(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(id, sel(sel), *args) + fun msgSendInt(sel: String, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(id, sel(sel), *args) + fun msgSendFloat(sel: String, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(id, sel(sel), *args) + fun msgSendDouble(sel: String, vararg args: Any?): Double = ObjectiveC.objc_msgSendDouble(id, sel(sel), *args) + fun msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(id, sel(sel), *args) + fun msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = ObjectiveC.objc_msgSendNSPoint(id, sel(sel), *args) + fun msgSend_stret(sel: String, vararg args: Any?): Unit = ObjectiveC.objc_msgSend_stret(id, sel(sel), *args) + + fun alloc(): Long = msgSend("alloc") + + companion object : NSClass("NSObject") { + } + + override fun toByte(): Byte = id.toByte() + override fun toChar(): Char = id.toChar() + override fun toShort(): Short = id.toShort() + override fun toInt(): Int = id.toInt() + override fun toLong(): Long = id + + override fun toNative(): Any = this.id + + override fun fromNative(nativeValue: Any, context: FromNativeContext?): Any = NSObject((nativeValue as Number).toLong()) + override fun nativeType(): Class<*> = Long::class.javaPrimitiveType!! + + val objcClass: NSClass get() = NSClass(msgSend("class")) + + override fun toString(): String = "NSObject($id)" +} + +open class NSString(id: Long) : NSObject(id) { + constructor() : this("") + constructor(id: Long?) : this(id ?: 0L) + constructor(str: String) : this(OBJ_CLASS.msgSend("alloc").msgSend("initWithCharacters:length:", str.toCharArray(), str.length)) + + //val length: Int get() = ObjectiveC.object_getIvar(this.id, LENGTH_ivar).toInt() + val length: Int get() = this.msgSend("length").toInt() + + val cString: String + get() { + val length = this.length + val ba = ByteArray(length + 1) + msgSend("getCString:maxLength:encoding:", ba, length + 1, 4) + val str = ba.toString(Charsets.UTF_8) + return str.substring(0, str.length - 1) + } + + override fun toString(): String = cString + + companion object : NSClass("NSString") { + val LENGTH_ivar = ObjectiveC.class_getProperty(OBJ_CLASS, "length") + } +} + +open class NSClass(id: Long) : NSObject(id) { + constructor(name: String) : this(ObjectiveC.objc_getClass(name)) + + val OBJ_CLASS = id + + val name by lazy { ObjectiveC.class_getName(id) } + + override fun toString(): String = "NSClass[$id]($name)" +} + +open class ObjcProtocol(val name: String) : NSObject(ObjectiveC.objc_getProtocol(name)) { + val OBJ_PROTOCOL = id +} + +fun NSClass.listClassMethods(): List = ObjC_listMethods(ObjectiveC.object_getClass(this.id)) +fun NSClass.listInstanceMethods(): List = ObjC_listMethods(this.id) + +fun ObjC_listMethods(clazz: Long): List { + val nitemsPtr = IntArray(1) + val items2 = ObjectiveC.class_copyMethodList(clazz, nitemsPtr) + val nitems = nitemsPtr[0] + val out = ArrayList(nitems) + for (n in 0 until nitems) { + val ptr = items2.getNativeLong((Native.LONG_SIZE * n).toLong()) + val mname = ObjectiveC.method_getName(ptr.toLong()) + val selName = ObjectiveC.sel_getName(mname) + out.add(selName) + } + return out +} + +fun ObjcProtocol.listMethods(): List = ObjC_listProtocolMethods(this.id) + +fun ObjC_listProtocolMethods(protocol: Long): List { + val nitemsPtr = IntArray(1) + val items2 = ObjectiveC.protocol_copyMethodDescriptionList(protocol, true, true, nitemsPtr) + val nitems = nitemsPtr[0] + val out = ArrayList(nitems) + //println("nitems=$nitems") + for (n in 0 until nitems) { + val namePtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + 0).toLong()) + val typesPtr = items2.getNativeLong((Native.LONG_SIZE * n * 2 + 1).toLong()) + val selName = ObjectiveC.sel_getName(namePtr.toLong()) + //val typesStr = Pointer(typesPtr.toLong()).getString(0L) + //println("$selName: $typesStr") + //val selName = ObjectiveC.sel_getName(mname) + out.add(selName) + } + return out +} + +interface ObjcCallback : Callback { + operator fun invoke(self: Long, _sel: Long, sender: Long): Long +} + +interface ObjcCallbackVoid : Callback { + operator fun invoke(self: Long, _sel: Long, sender: Long): Unit +} + +fun ObjcCallback(callback: (self: Long, _sel: Long, sender: Long) -> Long): ObjcCallback { + return object : ObjcCallback { + override fun invoke(self: Long, _sel: Long, sender: Long): Long = callback(self, _sel, sender) + } +} + +fun ObjcCallbackVoid(callback: (self: Long, _sel: Long, sender: Long) -> Unit): ObjcCallbackVoid { + return object : ObjcCallbackVoid { + override fun invoke(self: Long, _sel: Long, sender: Long): Unit = callback(self, _sel, sender) + } +} + +fun ObjcCallbackVoidEmpty(callback: () -> Unit): ObjcCallbackVoid { + return object : ObjcCallbackVoid { + override fun invoke(self: Long, _sel: Long, sender: Long): Unit = callback() + } +} + +inline fun autoreleasePool(body: () -> T): T { + val autoreleasePool = if (Platform.isMac()) NSClass("NSAutoreleasePool").alloc().msgSend("init") else null + try { + return body() + } finally { + autoreleasePool?.msgSend("drain") + } +} + +@KeepNames +object CoreFoundation { + val library = NativeLibrary.getInstance("CoreFoundation") + @JvmStatic val kCFRunLoopCommonModes: Pointer? = library.getGlobalVariableAddress("kCFRunLoopCommonModes").getPointer(0L) + @JvmStatic val kCFBooleanTrue: Pointer? = library.getGlobalVariableAddress("kCFBooleanTrue").getPointer(0L) + @JvmStatic val kCFBooleanFalse: Pointer? = library.getGlobalVariableAddress("kCFBooleanFalse").getPointer(0L) + @JvmStatic external fun CFRunLoopGetCurrent(): Pointer? + @JvmStatic external fun CFRunLoopGetMain(): Pointer? + @JvmStatic external fun CFRunLoopRun(): Void + + init { + Native.register("CoreFoundation") + } +} + +class CFBoolean private constructor(id: Long) : NSObject(id) { + val value: Boolean get() = this.id == TRUE.id + + companion object { + val TRUE = CFBoolean(CoreFoundation.kCFBooleanTrue.addressNotNull) + val FALSE = CFBoolean(CoreFoundation.kCFBooleanFalse.addressNotNull) + + operator fun invoke(value: Boolean): CFBoolean = if (value) TRUE else FALSE + } +} + +class NSNumber private constructor(id: Long) : NSObject(id) { + constructor(value: Int) : this(NSClass("NSNumber").alloc().msgSend("initWithInt:", value)) + constructor(value: Double) : this(NSClass("NSNumber").alloc().msgSend("initWithDouble:", value)) + constructor(value: Long, unit: Unit = Unit) : this(NSClass("NSNumber").alloc().msgSend("initWithLong:", value)) + val boolValue: Boolean get() = id.msgSendInt("boolValue") != 0 + val intValue: Int get() = id.msgSendInt("intValue") + val longValue: Long get() = id.msgSend("longValue") + val doubleValue: Double get() = id.msgSendDouble("doubleValue") +} + +fun NSObject.Companion.cast(value: Any): NSObject { + return when (value) { + is NSObject -> value + is String -> NSString(value) + is Int -> NSNumber(value) + is Long -> NSNumber(value) + is Double -> NSNumber(value) + is Boolean -> CFBoolean(value) + else -> TODO("Unsupported value '$value' to be cast to NSObject") + } +} + +class NSMutableDictionary(id: Long) : NSObject(id) { + constructor() : this(NSClass("NSMutableDictionary").alloc().msgSend("init")) + val count: Int get() = id.msgSendInt("count") + fun setValue(value: NSObject, forKey: NSObject) { + id.msgSend("setValue:forKey:", value.id, forKey.id) + } + fun getValue(key: NSObject): NSObject { + return NSObject(id.msgSend("valueForKey:", key.id)) + } + operator fun set(key: NSObject, value: NSObject) { + setValue(value, key) + } + operator fun set(key: Any, value: Any) { + this[NSObject.cast(key)] = NSObject.cast(value) + } + + operator fun get(key: Any): NSObject { + return getValue(NSObject.cast(key)) + } +} + +class NSDictionary(id: Long) : NSObject(id) { + constructor() : this(NSClass("NSDictionary").alloc().msgSend("init")) + val count: Int get() = id.msgSendInt("count") +} + +/* +interface DisplayLinkCallback : Callback { + fun callback(displayLink: Pointer?, inNow: Pointer?, inOutputTime: Pointer?, flagsIn: Pointer?, flagsOut: Pointer?, userInfo: Pointer?): Int +} + +interface CoreGraphics : Library { + fun CGMainDisplayID(): Int + companion object : CoreGraphics by NativeLoad("/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics") +} + +interface CoreVideo : Library { + fun CVDisplayLinkCreateWithCGDisplay(displayId: Int, ptr: Pointer?): Int + fun CVDisplayLinkSetOutputCallback(displayLinkValue: Pointer?, callback: Callback?, userInfo: Pointer?): Int + fun CVDisplayLinkStart(displayLinkValue: Pointer?): Int + fun CVDisplayLinkStop(displayLinkValue: Pointer?): Int + companion object : CoreVideo by NativeLoad("/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",) +} +*/ + +fun Memory(data: IntArray): Memory { + val out = Memory(data.size.toLong() * 4) + for (n in data.indices) out.setInt((n * 4).toLong(), data[n]) + return out +} + +fun Memory(data: LongArray): Memory { + val out = Memory(data.size.toLong() * 8) + for (n in data.indices) out.setLong((n * 8).toLong(), data[n]) + return out +} + +val Pointer.address: Long get() = Pointer.nativeValue(this) +val Pointer?.addressNotNull: Long get() = Pointer.nativeValue(this) + +annotation class NativeName(val name: String) { + companion object { + val OPTIONS = mapOf( + Library.OPTION_FUNCTION_MAPPER to FunctionMapper { _, method -> + method.getAnnotation(NativeName::class.java)?.name ?: method.name + } + ) + } +} + +inline fun NativeLoad(name: String): T = Native.load(name, T::class.java, NativeName.OPTIONS) as T diff --git a/korge/src/jvm/korlibs/render/osx/MacGL.kt b/korge/src/jvm/korlibs/render/osx/MacGL.kt index b727c78e51..acf5290e91 100644 --- a/korge/src/jvm/korlibs/render/osx/MacGL.kt +++ b/korge/src/jvm/korlibs/render/osx/MacGL.kt @@ -1,7 +1,6 @@ package korlibs.render.osx import com.sun.jna.* -import korlibs.memory.dyn.* import korlibs.render.platform.* open class MacKmlGl : NativeKgl(DirectGL) diff --git a/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt b/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt index bbcc5d12ed..3319ccac33 100644 --- a/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt +++ b/korge/src/jvm/korlibs/render/osx/MacosGLContext.kt @@ -7,8 +7,6 @@ import korlibs.graphics.gl.* import korlibs.io.dynamic.* import korlibs.kgl.* import korlibs.math.geom.* -import korlibs.memory.dyn.* -import korlibs.memory.dyn.osx.* import korlibs.render.* import korlibs.render.platform.* import java.awt.* diff --git a/korge/src/jvm/korlibs/render/osx/metal/MTL.kt b/korge/src/jvm/korlibs/render/osx/metal/MTL.kt index d5b86362a6..01dcc28563 100644 --- a/korge/src/jvm/korlibs/render/osx/metal/MTL.kt +++ b/korge/src/jvm/korlibs/render/osx/metal/MTL.kt @@ -4,7 +4,6 @@ import com.sun.jna.* import korlibs.io.dynamic.* import korlibs.io.lang.* import korlibs.memory.* -import korlibs.memory.dyn.osx.* import korlibs.render.osx.* import korlibs.render.platform.* import javax.swing.* diff --git a/korge/src/jvm/korlibs/render/platform/AwtTools.kt b/korge/src/jvm/korlibs/render/platform/AwtTools.kt index cf0e923bfb..5412c56341 100644 --- a/korge/src/jvm/korlibs/render/platform/AwtTools.kt +++ b/korge/src/jvm/korlibs/render/platform/AwtTools.kt @@ -2,7 +2,7 @@ package korlibs.render.platform import com.sun.jna.* import korlibs.io.dynamic.* -import korlibs.memory.dyn.* +import korlibs.render.osx.* import java.awt.* fun Component.awtGetPeer(): Any { diff --git a/korge/src/jvm/korlibs/render/platform/INativeGL.kt b/korge/src/jvm/korlibs/render/platform/INativeGL.kt index d84fa5d087..933932968c 100644 --- a/korge/src/jvm/korlibs/render/platform/INativeGL.kt +++ b/korge/src/jvm/korlibs/render/platform/INativeGL.kt @@ -3,11 +3,11 @@ package korlibs.render.platform import com.sun.jna.* +import korlibs.ffi.* import korlibs.io.annotations.* import korlibs.io.lang.* import korlibs.io.time.* import korlibs.logger.* -import korlibs.memory.dyn.* import korlibs.platform.Platform import korlibs.render.win32.* import java.io.* diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt index 98603878a3..9900314d44 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt @@ -7,6 +7,7 @@ import korlibs.image.bitmap.* import korlibs.image.color.* import korlibs.korge.render.* import korlibs.platform.* +import korlibs.render.osx.* import korlibs.time.* import java.awt.* import javax.swing.* @@ -16,7 +17,7 @@ import kotlin.test.* class AwtGameCanvasTest { @Test //@Ignore - fun test() = korlibs.memory.dyn.osx.autoreleasePool { + fun test() = autoreleasePool { //System.setProperty("sun.java2d.metal", "true") //System.setProperty("sun.java2d.opengl", "false") System.setProperty("sun.java2d.opengl", "true") diff --git a/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt b/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt index b3f9f8ef06..614695db44 100644 --- a/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt +++ b/korge/test/jvm/korlibs/render/osx/metal/AGMetalTest.kt @@ -3,8 +3,8 @@ package korlibs.render.osx.metal import korlibs.image.color.* import korlibs.image.format.* import korlibs.memory.* -import korlibs.memory.dyn.osx.* import korlibs.platform.* +import korlibs.render.osx.* import kotlinx.coroutines.* import org.junit.Test import kotlin.test.* From 0034073e8e81399a0ee60919be5a327386d0f25b Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 23:30:48 +0200 Subject: [PATCH 06/66] Fixed DarwinMacosGameController --- .../src/common/korlibs/ffi/osx/FFIObjc.kt | 4 +- .../common/korlibs/ffi/osx/ObjcStandard.kt | 11 ++ .../format/CoreGraphicsImageFormatProvider.kt | 2 +- .../gamepad/DarwinMacosGameController.kt | 124 ++++++++---------- .../render/awt/DesktopGamepadUpdater.kt | 26 +++- 5 files changed, 90 insertions(+), 77 deletions(-) diff --git a/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt b/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt index bb938d76b3..7754a8dbad 100644 --- a/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt +++ b/korge-core/src/common/korlibs/ffi/osx/FFIObjc.kt @@ -406,9 +406,7 @@ open class NSObject(val id: Long) { constructor(id: ObjcRef, unit: Unit = Unit) : this(id.id) init { - if (id == 0L) { - println("NSObject[${this::class}]: id=$id") - } + check(id != 0L) { "NSObject is null" } } val ref = ObjcRef(id) diff --git a/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt b/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt index 11acb8e386..907ec94de0 100644 --- a/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt +++ b/korge-core/src/common/korlibs/ffi/osx/ObjcStandard.kt @@ -1,5 +1,6 @@ package korlibs.ffi.osx +import korlibs.ffi.* import korlibs.io.lang.* import kotlin.text.toCharArray @@ -64,3 +65,13 @@ class NSNumber private constructor(id: Long) : NSObject(id) { //val doubleValue: Double get() = msgSendDouble("doubleValue") } +class NSArray(val id: ObjcRef) : AbstractList() { + val count: Int get() = id.msgSendInt("count") + override val size: Int get() = count + override operator fun get(index: Int): Long = id.msgSend("objectAtIndex:", index) + override fun toString(): String = "NSArray(${toList()})" +} + +object CoreFoundation : FFILib("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation") { + val dlopen: (path: String, mode: Int) -> FFIPointer? by func() +} diff --git a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt index 9e2d785f35..1502fabf50 100644 --- a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt +++ b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt @@ -4,7 +4,7 @@ import com.sun.jna.* import korlibs.ffi.osx.* import korlibs.image.awt.* import korlibs.image.color.* -import korlibs.internal.osx.* +import korlibs.internal.osx.CoreFoundation import korlibs.io.annotations.* import korlibs.io.file.* import korlibs.io.file.std.* diff --git a/korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt b/korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt index 14e6379d6e..84da9cc6bb 100644 --- a/korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt +++ b/korge/src/common/korlibs/event/gamepad/DarwinMacosGameController.kt @@ -3,7 +3,6 @@ package korlibs.event.gamepad import korlibs.annotations.* import korlibs.datastructure.iterators.* import korlibs.event.* -import korlibs.ffi.* import korlibs.ffi.osx.* import korlibs.math.geom.* import korlibs.number.* @@ -169,13 +168,6 @@ internal class GCController(id: ObjcRef) : NSObject(id) { } } -class NSArray(val id: ObjcRef) : AbstractList() { - val count: Int get() = id.msgSendInt("count") - override val size: Int get() = count - override operator fun get(index: Int): Long = id.msgSend("objectAtIndex:", index) - override fun toString(): String = "NSArray(${toList()})" -} - //@JvmStatic //fun main(args: Array) { // val gamepad = MacosGamepadEventAdapter() @@ -189,9 +181,9 @@ class NSArray(val id: ObjcRef) : AbstractList() { //} internal class MacosGamepadEventAdapter { - internal object FrameworkInt : FFILib("/System/Library/Frameworks/GameController.framework/Versions/A/GameController") - - val lib by lazy { FrameworkInt } + val lib by lazy { + CoreFoundation.dlopen("/System/Library/Frameworks/GameController.framework/Versions/A/GameController", 0) + } private fun GamepadInfo.set(button: GameButton, value: Float, deadRange: Boolean = false) { rawButtons[button.index] = GamepadInfo.withoutDeadRange(value.toFloat(), apply = deadRange) } private fun GamepadInfo.set(button: GameButton, cbutton: GCControllerButtonInput, deadRange: Boolean = false) { @@ -202,64 +194,60 @@ internal class MacosGamepadEventAdapter { private val gamepad = GamepadInfo() fun updateGamepads(gameWindow: GameWindow) { - try { - lib - for (n in allControllers.indices) allControllers[n] = null - GCController.controllers() - //.sortedBy { it } - .map { GCController(ObjcRef(it)) } - .fastForEachWithIndex { index, it -> - //println("index=$index") - if (index in allControllers.indices) allControllers[index] = it - } - - gameWindow.dispatchGamepadUpdateStart() - for (index in allControllers.indices) { - val ctrl = allControllers[index] ?: continue - val ex: GCExtendedGamepad? = ctrl.extendedGamepad - val base: GCGamepad? = ctrl.gamepad ?: ctrl.extendedGamepad - val micro: GCMicroGamepad? = ctrl.microGamepad ?: ctrl.gamepad ?: ctrl.extendedGamepad - if (micro != null) { - gamepad.set(GameButton.LEFT, micro.dpad.left) - gamepad.set(GameButton.RIGHT, micro.dpad.right) - gamepad.set(GameButton.UP, micro.dpad.up) - gamepad.set(GameButton.DOWN, micro.dpad.down) - gamepad.set(GameButton.XBOX_A, micro.buttonA) - gamepad.set(GameButton.XBOX_X, micro.buttonX) - gamepad.set(GameButton.START, micro.buttonMenu) - } - if (base != null) { - gamepad.set(GameButton.XBOX_B, base.buttonB) - gamepad.set(GameButton.XBOX_Y, base.buttonY) - gamepad.set(GameButton.L1, base.leftShoulder) - gamepad.set(GameButton.R1, base.rightShoulder) - } - if (ex != null) { - gamepad.set(GameButton.SYSTEM, ex.buttonHome) - gamepad.set(GameButton.SELECT, ex.buttonOptions) - gamepad.set(GameButton.L3, ex.leftThumbstickButton) - gamepad.set(GameButton.R3, ex.rightThumbstickButton) - gamepad.set(GameButton.L2, ex.leftTrigger) - gamepad.set(GameButton.R2, ex.rightTrigger) - gamepad.set(GameButton.LX, ex.leftThumbstick.x, deadRange = true) - gamepad.set(GameButton.LY, ex.leftThumbstick.y, deadRange = true) - gamepad.set(GameButton.RX, ex.rightThumbstick.x, deadRange = true) - gamepad.set(GameButton.RY, ex.rightThumbstick.y, deadRange = true) - } - gamepad.name = ctrl.vendorName - gamepad.batteryLevel = ctrl.battery?.batteryLevel?.toFloat() ?: 1f - //println("ctrl.battery?.batteryState=${ctrl.battery?.batteryState}") - gamepad.batteryStatus = when (ctrl.battery?.batteryState) { - 0 -> GamepadInfo.BatteryStatus.DISCHARGING - 1 -> GamepadInfo.BatteryStatus.CHARGING - 2 -> GamepadInfo.BatteryStatus.FULL - else -> GamepadInfo.BatteryStatus.UNKNOWN - } - gameWindow.dispatchGamepadUpdateAdd(gamepad) + lib + for (n in allControllers.indices) allControllers[n] = null + GCController.controllers() + //.sortedBy { it } + .map { GCController(ObjcRef(it)) } + .fastForEachWithIndex { index, it -> + //println("index=$index") + if (index in allControllers.indices) allControllers[index] = it + } + + gameWindow.dispatchGamepadUpdateStart() + for (index in allControllers.indices) { + val ctrl = allControllers[index] ?: continue + val ex: GCExtendedGamepad? = ctrl.extendedGamepad + val base: GCGamepad? = ctrl.gamepad ?: ctrl.extendedGamepad + val micro: GCMicroGamepad? = ctrl.microGamepad ?: ctrl.gamepad ?: ctrl.extendedGamepad + if (micro != null) { + gamepad.set(GameButton.LEFT, micro.dpad.left) + gamepad.set(GameButton.RIGHT, micro.dpad.right) + gamepad.set(GameButton.UP, micro.dpad.up) + gamepad.set(GameButton.DOWN, micro.dpad.down) + gamepad.set(GameButton.XBOX_A, micro.buttonA) + gamepad.set(GameButton.XBOX_X, micro.buttonX) + gamepad.set(GameButton.START, micro.buttonMenu) + } + if (base != null) { + gamepad.set(GameButton.XBOX_B, base.buttonB) + gamepad.set(GameButton.XBOX_Y, base.buttonY) + gamepad.set(GameButton.L1, base.leftShoulder) + gamepad.set(GameButton.R1, base.rightShoulder) + } + if (ex != null) { + gamepad.set(GameButton.SYSTEM, ex.buttonHome) + gamepad.set(GameButton.SELECT, ex.buttonOptions) + gamepad.set(GameButton.L3, ex.leftThumbstickButton) + gamepad.set(GameButton.R3, ex.rightThumbstickButton) + gamepad.set(GameButton.L2, ex.leftTrigger) + gamepad.set(GameButton.R2, ex.rightTrigger) + gamepad.set(GameButton.LX, ex.leftThumbstick.x, deadRange = true) + gamepad.set(GameButton.LY, ex.leftThumbstick.y, deadRange = true) + gamepad.set(GameButton.RX, ex.rightThumbstick.x, deadRange = true) + gamepad.set(GameButton.RY, ex.rightThumbstick.y, deadRange = true) + } + gamepad.name = ctrl.vendorName + gamepad.batteryLevel = ctrl.battery?.batteryLevel?.toFloat() ?: 1f + //println("ctrl.battery?.batteryState=${ctrl.battery?.batteryState}") + gamepad.batteryStatus = when (ctrl.battery?.batteryState) { + 0 -> GamepadInfo.BatteryStatus.DISCHARGING + 1 -> GamepadInfo.BatteryStatus.CHARGING + 2 -> GamepadInfo.BatteryStatus.FULL + else -> GamepadInfo.BatteryStatus.UNKNOWN } - gameWindow.dispatchGamepadUpdateEnd() - } catch (e: Throwable) { - e.printStackTrace() + gameWindow.dispatchGamepadUpdateAdd(gamepad) } + gameWindow.dispatchGamepadUpdateEnd() } } diff --git a/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt b/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt index eabb538efe..21a7cb0aec 100644 --- a/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt +++ b/korge/src/jvm/korlibs/render/awt/DesktopGamepadUpdater.kt @@ -3,14 +3,30 @@ package korlibs.render.awt import korlibs.event.gamepad.* import korlibs.platform.* import korlibs.render.* +import korlibs.time.* internal object DesktopGamepadUpdater { + private var exceptionStopwatch: Stopwatch? = null + fun updateGamepads(window: GameWindow) { - when { - Platform.isWindows -> xinputEventAdapter.updateGamepads(window.gamepadEmitter) - Platform.isLinux -> linuxJoyEventAdapter.updateGamepads(window.gamepadEmitter) - Platform.isMac -> macosGamepadEventAdapter.updateGamepads(window) - else -> Unit //println("undetected OS: ${OS.rawName}") + if (exceptionStopwatch != null && exceptionStopwatch!!.elapsed < 1.seconds) { + return + } + if (exceptionStopwatch == null) { + exceptionStopwatch = Stopwatch().start() + } + //println("exceptionStopwatch.elapsed=${exceptionStopwatch?.elapsed}") + try { + when { + Platform.isWindows -> xinputEventAdapter.updateGamepads(window.gamepadEmitter) + Platform.isLinux -> linuxJoyEventAdapter.updateGamepads(window.gamepadEmitter) + Platform.isMac -> macosGamepadEventAdapter.updateGamepads(window) + else -> Unit //println("undetected OS: ${OS.rawName}") + } + } catch (e: Throwable) { + exceptionStopwatch?.restart() + //println("exceptionStopwatch.restart()") + e.printStackTrace() } } From 517e0655a5132e1b579f78195e75412a27cb31fc Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 23:31:09 +0200 Subject: [PATCH 07/66] Improve Stopwatch internal namings --- korge-foundation/src/common/korlibs/time/Stopwatch.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/korge-foundation/src/common/korlibs/time/Stopwatch.kt b/korge-foundation/src/common/korlibs/time/Stopwatch.kt index df2e046bb0..4b7e66d36a 100644 --- a/korge-foundation/src/common/korlibs/time/Stopwatch.kt +++ b/korge-foundation/src/common/korlibs/time/Stopwatch.kt @@ -2,9 +2,9 @@ package korlibs.time class Stopwatch(val nanosecondProvider: () -> Double = { PerformanceCounter.nanoseconds }) { private var running = false - private var nanoseconds = 0.0 - private val clock get() = nanosecondProvider() - private fun setStart() { nanoseconds = clock } + private var startNano = 0.0 + private val currentNano get() = nanosecondProvider() + private fun setStart() { startNano = currentNano } init { setStart() } @@ -14,10 +14,10 @@ class Stopwatch(val nanosecondProvider: () -> Double = { PerformanceCounter.nano } fun restart() = start() fun stop() = this.apply { - nanoseconds = elapsedNanoseconds + startNano = elapsedNanoseconds running = false } - val elapsedNanoseconds get() = if (running) clock - nanoseconds else nanoseconds + val elapsedNanoseconds get() = if (running) currentNano - startNano else startNano val elapsedMicroseconds get() = elapsedNanoseconds * 1000 val elapsed: TimeSpan get() = elapsedNanoseconds.nanoseconds fun getElapsedAndRestart(): TimeSpan = elapsed.also { restart() } From 842b96675beed48bc90520de02973f72de57ae76 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 21 Oct 2023 23:31:23 +0200 Subject: [PATCH 08/66] [WIP] More work on New Game Window --- .../datastructure/_Datastructure_event.kt | 2 +- korge/src/common/korlibs/korge/Korge.kt | 9 ++++ .../korlibs/korge/tests/ViewsForTesting.kt | 11 +---- korge/src/common/korlibs/korge/view/Views.kt | 30 +++++------- korge/src/common/korlibs/render/GameWindow.kt | 12 ++--- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 46 ++++++++++++++++++- .../korlibs/korge/ViewsForTestingTest.kt | 20 +++----- 7 files changed, 82 insertions(+), 48 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index b47fbbca4f..ccb6632675 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -87,8 +87,8 @@ class SyncEventLoop( try { // Run pending tasks including pending timers, but won't allow to add new tasks because running=false immediateRun = true - running = false runAvailableNextTasks() + running = false } finally { immediateRun = oldImmediateRun } diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index 278b2b3f15..9b7626345f 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -184,6 +184,7 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen Dyn.global["views"] = it } } + views.setVirtualSize(config.virtualSize) gameWindow.coroutineContext += InjectorContext(config.injector) Korge.logger.logTime("configureGameWindow") { gameWindow.configure(windowSize, config.title, null, config.fullscreen, config.backgroundColor ?: Colors.BLACK) @@ -226,10 +227,18 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen } } + var cachedFrameSize = SizeInt(0, 0) + gameWindow.onRenderEvent { if (initialized) { //println("RENDER") //println("gameWindow.size=${gameWindow.frameSize}") + val frameSize = gameWindow.frameSize + if (cachedFrameSize != frameSize) { + cachedFrameSize = frameSize + views.resized(frameSize.width, frameSize.height) + views.update(0.milliseconds) + } views.renderNew() } } diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index 280cffcb61..b416d7bb6c 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -334,18 +334,11 @@ open class ViewsForTesting( frameTime: TimeSpan = this.frameTime, crossinline block: suspend S.() -> Unit ): AsyncEntryPointResult = viewsTest(timeout, frameTime) { - config?.apply { - injector.configInjector() - } - + config?.apply { injector.configInjector() } injector.configureInjector() - val container = sceneContainer(views) container.changeTo() - - with(container.currentScene as S) { - block() - } + with(container.currentScene as S) { block() } } diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 000d5ada36..54d4c73366 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.* import kotlin.collections.set import kotlin.coroutines.* -//@Singleton /** * Heavyweight singleton object within the application that contains information about the Views. * It contains information about the [coroutineContext], the [gameWindow], the [injector], the [input] @@ -248,6 +247,9 @@ class Views( this.virtualHeight = height resized() } + fun setVirtualSize(size: Size) { + setVirtualSize(size.width.toInt(), size.height.toInt()) + } override fun dispatch( type: EventType, @@ -358,16 +360,10 @@ class Views( //println("localMouse: (${stage.localMouseX}, ${stage.localMouseY}), inputMouse: (${input.mouse.x}, ${input.mouse.y})") } - fun resized(width: Int, height: Int) { - val actualWidth = width - val actualHeight = height - //println("RESIZED: $width, $height") - actualSize = SizeInt(actualWidth, actualHeight) - resized() - } - + fun resized(width: Int, height: Int) = resized(SizeInt(width, height)) - fun resized() { + fun resized(actualSize: SizeInt = this.actualSize) { + this.actualSize = actualSize //println("$e : ${views.ag.backWidth}x${views.ag.backHeight}") bp.setBoundsInfo( Size(virtualWidth, virtualHeight), @@ -592,15 +588,13 @@ interface BoundsProvider { //@KorgeExperimental var actualVirtualWidth = DefaultViewport.WIDTH; private set //@KorgeExperimental var actualVirtualHeight = DefaultViewport.HEIGHT; private set - val virtualLeft: Double get() = actualVirtualBounds.left.toDouble() - val virtualTop: Double get() = actualVirtualBounds.top.toDouble() - val virtualRight: Double get() = actualVirtualBounds.right.toDouble() - val virtualBottom: Double get() = actualVirtualBounds.bottom.toDouble() + val virtualLeft: Double get() = actualVirtualBounds.left + val virtualTop: Double get() = actualVirtualBounds.top + val virtualRight: Double get() = actualVirtualBounds.right + val virtualBottom: Double get() = actualVirtualBounds.bottom - @KorgeExperimental - val actualVirtualRight: Double get() = actualVirtualBounds.right.toDouble() - @KorgeExperimental - val actualVirtualBottom: Double get() = actualVirtualBounds.bottom.toDouble() + @KorgeExperimental val actualVirtualRight: Double get() = actualVirtualBounds.right + @KorgeExperimental val actualVirtualBottom: Double get() = actualVirtualBounds.bottom fun globalToWindowBounds(bounds: Rectangle): Rectangle = bounds.transformed(globalToWindowMatrix) diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index f9bfb577ef..34bd6a68b4 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -177,7 +177,7 @@ open class GameWindow : open var title: String get() = ""; set(value) = Unit open val width: Int = 0 open val height: Int = 0 - val frameSize get() = Size(width, height) + val frameSize get() = SizeInt(width, height) open fun setSize(width: Int, height: Int): Unit = Unit open var vsync: Boolean = true open var icon: Bitmap? = null @@ -219,17 +219,17 @@ open class GameWindow : open fun close(exitCode: Int = 0) { if (closing) return closing = true - println("[1]") + //println("[1]") queue { dispatchDestroyEvent() } - println("[2]") + //println("[2]") running = false this.exitCode = exitCode - println("[3]") + //println("[3]") logger.info { "GameWindow.close" } coroutineDispatcher.close() - println("[4]") + //println("[4]") coroutineDispatcher.cancelChildren() - println("[5]") + //println("[5]") } @Deprecated("") diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 636bd193c6..1f9564890b 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -6,6 +6,7 @@ import korlibs.graphics.* import korlibs.image.awt.* import korlibs.image.bitmap.* import korlibs.image.color.* +import korlibs.platform.* import korlibs.render.* import korlibs.render.MenuItem import korlibs.time.* @@ -172,7 +173,50 @@ class AwtGameWindow( frame.title = value System.setProperty("apple.awt.application.name", value) } - override var visible: Boolean by frame::visible + override var visible: Boolean + get() = frame.visible + set(value) { + val shown = !frame.visible && value + frame.visible = value + if (shown) { + EventQueue.invokeLater { + frame.repaint() + //fullscreen = true + + // keys.up(Key.ENTER) { if (it.alt) gameWindow.toggleFullScreen() } + + // @TODO: HACK so the windows grabs focus on Windows 10 when launching on gradle daemon + val useRobotHack = Platform.isWindows + + if (useRobotHack) { + (frame as? Frame?)?.apply { + val frame = this + val insets = frame.insets + frame.isAlwaysOnTop = true + try { + val robot = Robot() + val pos = MouseInfo.getPointerInfo().location + val bounds = frame.bounds + bounds.setFrameFromDiagonal(bounds.minX + insets.left, bounds.minY + insets.top, bounds.maxX - insets.right, bounds.maxY - insets.bottom) + + //println("frame.bounds: ${frame.bounds}") + //println("frame.bounds: ${bounds}") + //println("frame.insets: ${insets}") + //println(frame.contentPane.bounds) + //println("START ROBOT") + robot.mouseMove(bounds.centerX.toInt(), bounds.centerY.toInt()) + robot.mousePress(InputEvent.BUTTON3_MASK) + robot.mouseRelease(InputEvent.BUTTON3_MASK) + robot.mouseMove(pos.x, pos.y) + //println("END ROBOT") + } catch (e: Throwable) { + } + frame.isAlwaysOnTop = false + } + } + } + } + } override var icon: Bitmap? = null set(value) { field = value diff --git a/korge/test/common/korlibs/korge/ViewsForTestingTest.kt b/korge/test/common/korlibs/korge/ViewsForTestingTest.kt index ebab93017b..41447115b1 100644 --- a/korge/test/common/korlibs/korge/ViewsForTestingTest.kt +++ b/korge/test/common/korlibs/korge/ViewsForTestingTest.kt @@ -1,12 +1,10 @@ package korlibs.korge -import korlibs.korge.scene.Scene -import korlibs.korge.tests.ViewsForTesting -import korlibs.korge.view.SContainer -import korlibs.inject.Injector -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue +import korlibs.inject.* +import korlibs.korge.scene.* +import korlibs.korge.tests.* +import korlibs.korge.view.* +import kotlin.test.* class ViewsForTestingTest : ViewsForTesting() { open class Dependency { @@ -21,12 +19,8 @@ class ViewsForTestingTest : ViewsForTesting() { } fun Injector.mapCommon() { - mapSingleton { - Dependency() - } - mapPrototype { - DummyScene(get()) - } + mapSingleton { Dependency() } + mapPrototype { DummyScene(get()) } } @Test From 2c5d738ff05d5cbef97092c3b3fe7bdeeb110dcc Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 00:14:58 +0200 Subject: [PATCH 09/66] [WIP] More work on New Game Window --- .../korlibs/graphics/AGTextureDrawer.kt | 15 +++++++++++ .../korlibs/korge/render/RenderContext.kt | 7 +++--- korge/src/common/korlibs/korge/view/Views.kt | 6 ++--- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 8 +++--- .../jvm/korlibs/render/awt/AwtFrameTools.kt | 5 ++++ .../jvm/korlibs/render/awt/AwtGameWindow.kt | 8 +++++- .../korlibs/render/awt/AwtGameWindowTest.kt | 25 ++++++++----------- 7 files changed, 49 insertions(+), 25 deletions(-) diff --git a/korge/src/common/korlibs/graphics/AGTextureDrawer.kt b/korge/src/common/korlibs/graphics/AGTextureDrawer.kt index 11caf9e9ce..9e40e85dbe 100644 --- a/korge/src/common/korlibs/graphics/AGTextureDrawer.kt +++ b/korge/src/common/korlibs/graphics/AGTextureDrawer.kt @@ -2,6 +2,7 @@ package korlibs.graphics import korlibs.datastructure.* import korlibs.graphics.shader.* +import korlibs.math.* import korlibs.memory.* val AG.textureDrawer by Extra.PropertyThis { @@ -39,6 +40,20 @@ class AGTextureDrawer(val ag: AG) { verticesDataF32[offset + 3] = ty } + private fun Float.convertToM1P1(): Float { + return this.convertRange(0f, 1f, -1f, +1f) + } + + fun drawXY(frameBuffer: AGFrameBuffer, tex: AGTexture, x: Int, y: Int, width: Int, height: Int) { + val fwidth = frameBuffer.fullWidth.toFloat() + val fheight = frameBuffer.fullHeight.toFloat() + val left = x.toFloat() / fwidth + val top = y.toFloat() / fheight + val right = (x + width).toFloat() / fwidth + val bottom = (y + height).toFloat() / fheight + return draw(frameBuffer, tex, left.convertToM1P1(), top.convertToM1P1(), right.convertToM1P1(), bottom.convertToM1P1()) + } + fun draw(frameBuffer: AGFrameBuffer, tex: AGTexture, left: Float = -1f, top: Float = +1f, right: Float = +1f, bottom: Float = -1f) { //tex.upload(Bitmap32(32, 32) { x, y -> Colors.RED }) //uniforms.set(DefaultShaders.u_Tex, tex) diff --git a/korge/src/common/korlibs/korge/render/RenderContext.kt b/korge/src/common/korlibs/korge/render/RenderContext.kt index 328da965da..6932322dac 100644 --- a/korge/src/common/korlibs/korge/render/RenderContext.kt +++ b/korge/src/common/korlibs/korge/render/RenderContext.kt @@ -69,8 +69,8 @@ class RenderContext( @KorgeInternal var viewMat2D: Matrix = Matrix() - var flipRenderTexture = true - //var flipRenderTexture = false + //var flipRenderTexture = true + var flipRenderTexture = false val tempTexturePool: Pool = Pool { AGTexture() } @@ -83,6 +83,7 @@ class RenderContext( fun updateStandardUniforms() { //println("updateStandardUniforms: ag.currentSize(${ag.currentWidth}, ${ag.currentHeight}) : ${ag.currentFrameBuffer}") + //if (flipRenderTexture && currentFrameBuffer.isTexture) { if (flipRenderTexture && currentFrameBuffer.isTexture) { projMat = Matrix4.ortho(Rectangle.fromBounds(0, currentFrameBuffer.height, currentFrameBuffer.width, 0), -1f, 1f) } else { @@ -215,6 +216,7 @@ class RenderContext( var currentFrameBuffer: AGFrameBuffer = ag.mainFrameBuffer val currentFrameBufferOrMain: AGFrameBuffer get() = currentFrameBuffer ?: mainFrameBuffer val isRenderingToWindow: Boolean get() = currentFrameBufferOrMain === mainFrameBuffer + //val isRenderingToWindow: Boolean get() = false val isRenderingToTexture: Boolean get() = !isRenderingToWindow // On macOS components, this will be the size of the component @@ -485,7 +487,6 @@ class RenderContext( currentFrameBuffer = frameBufferStack.removeAt(frameBufferStack.size - 1) } - val textureDrawer get() = ag.textureDrawer fun drawTexture(frameBuffer: AGFrameBuffer, tex: AGTexture, left: Float = -1f, top: Float = +1f, right: Float = +1f, bottom: Float = -1f) { textureDrawer.draw(frameBuffer, tex, left, top, right, bottom) diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 54d4c73366..abd0aa87ab 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -147,9 +147,7 @@ class Views( */ fun onClose(callback: suspend () -> Unit) { closeables += object : AsyncCloseable { - override suspend fun close() { - callback() - } + override suspend fun close() = callback() } } @@ -376,6 +374,8 @@ class Views( //println("RESIZED: $virtualSize, $actualSize, $targetSize") + //println("bp.globalToWindowMatrix=${bp.globalToWindowMatrix}") + renderContext.projectionMatrixTransform = bp.globalToWindowMatrix renderContext.projectionMatrixTransformInv = bp.windowToGlobalMatrix diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index 39a29ee048..2e121c4039 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -95,6 +95,8 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP var doRender: (AG) -> Unit = { ag -> } + val frameBuffer = AGFrameBuffer() + val paintInContextDelegate: (Graphics, BaseOpenglContext.ContextInfo) -> Unit = { g, info -> state.keep { //run { @@ -112,6 +114,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP val scaledWidth = (frameOrComponent.width * factor).toInt() val scaledHeight = (frameOrComponent.height * factor).toInt() val viewportScale = 1.0 + //println("factor=$factor, viewportScale=$viewportScale, viewport=$viewport") if (contextLost) { contextLost = false @@ -123,7 +126,6 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP if (viewport != null) { //if (false) { viewport!! - val frameBuffer = AGFrameBuffer() mainFrameBuffer.setSize( (viewport.x * viewportScale).toInt(), (viewport.y * viewportScale).toInt(), @@ -132,7 +134,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP scaledWidth, scaledHeight, ) - frameBuffer.setSize(scaledWidth, scaledHeight) + frameBuffer.setSize(viewport.width, viewport.height) //frameBuffer.scissor = RectangleInt(0, 0, 50, 50) //ag.clear(frameBuffer) @@ -140,7 +142,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP ag.setMainFrameBufferTemporarily(frameBuffer) { doRender(ag) } - ag.textureDrawer.draw(mainFrameBuffer, frameBuffer.tex) + ag.textureDrawer.drawXY(mainFrameBuffer, frameBuffer.tex, viewport.x, viewport.y, viewport.width, viewport.height) } else { mainFrameBuffer.setSize(scaledWidth, scaledHeight) doRender(ag) diff --git a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt index 0ccb6e2923..557b51171d 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt @@ -507,3 +507,8 @@ fun Component.registerKeyEvents(gameWindow: GameWindow) { override fun keyReleased(e: JKeyEvent) = handleKeyEvent(e) }) } + +fun JFrame.computeDimensionsForSize(width: Int, height: Int): Dimension { + val insets = this.insets + return Dimension(width + insets.left + insets.right, height + insets.top + insets.bottom) +} diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 1f9564890b..9e43d171b5 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -135,7 +135,7 @@ class AwtGameWindow( defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE contentPane.layout = GridLayout(1, 1) contentPane.add(canvas) - preferredSize = Dimension(640, 480) + preferredSize = computeDimensionsForSize(640, 480) //setBounds(0, 0, 640, 480) pack() setLocationRelativeTo(null) @@ -151,6 +151,12 @@ class AwtGameWindow( } override val window: Window get() = frame + override fun setSize(width: Int, height: Int): Unit { + frame.preferredSize = frame.computeDimensionsForSize(width, height) + frame.pack() + frame.setLocationRelativeTo(null) + } + override fun setMainMenu(items: List) { //Dispatchers.Unconfined.launchUnscoped { thread { diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt index 8aba459c33..8a617f9e7b 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt @@ -11,6 +11,7 @@ import korlibs.korge.view.* import korlibs.logger.* import korlibs.render.* import korlibs.time.* +import kotlinx.coroutines.* import kotlin.test.* class AwtGameWindowTest { @@ -22,20 +23,23 @@ class AwtGameWindowTest { gameWindow.bgcolor = Colors.BLACK + gameWindow.icon = runBlocking { resourcesVfs["korge.png"].readBitmap() } gameWindow.configureKorge(KorgeConfig(backgroundColor = Colors.BLACK)) { + solidRect(width, height, Colors.SADDLEBROWN.withAd(0.2)) println(measureTime { image(resourcesVfs["korge.png"].readBitmap()) }) - solidRect(100, 100, Colors.RED).addUpdater { + solidRect(100, 100, Colors.RED) { + mouse { + over { alpha = 0.5 } + out { alpha = 1.0 } + } + }.addUpdater { + //println(views.globalToWindowMatrix) this.x++ } mouse.click { println("CLICK!") - showContextMenu { - item("Hello") - item("World") - } - } mouse.scroll { println("SCROLL: $it") @@ -47,15 +51,6 @@ class AwtGameWindowTest { println("DESTROY!") } - setMainMenu { - item("Hello") { - item("&Exit") - } - item("World") { - item("Hello") - } - } - //mouse { moveAnywhere { println("MOUSE MOVE $it") } } } From 65b7d7bd1c71f530e0905699ed876d089cb1a884 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 00:32:54 +0200 Subject: [PATCH 10/66] [WIP] More work on New Game Window --- .../common/korlibs/datastructure/_Datastructure_event.kt | 3 ++- korge/src/common/korlibs/korge/render/RenderContext.kt | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index ccb6632675..7a88ebb520 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -19,7 +19,8 @@ fun EventLoop.setInterval(time: Frequency, task: () -> Unit): Closeable = setInt class SyncEventLoop( /** precise=true will have better precision at the cost of more CPU-usage (busy waiting) */ - var precise: Boolean = true, + //var precise: Boolean = true, + var precise: Boolean = false, /** Execute timers immediately instead of waiting. Useful for testing. */ var immediateRun: Boolean = false, ) : EventLoop { diff --git a/korge/src/common/korlibs/korge/render/RenderContext.kt b/korge/src/common/korlibs/korge/render/RenderContext.kt index 6932322dac..d0a6d239a4 100644 --- a/korge/src/common/korlibs/korge/render/RenderContext.kt +++ b/korge/src/common/korlibs/korge/render/RenderContext.kt @@ -69,8 +69,8 @@ class RenderContext( @KorgeInternal var viewMat2D: Matrix = Matrix() - //var flipRenderTexture = true - var flipRenderTexture = false + var flipRenderTexture = true + //var flipRenderTexture = false val tempTexturePool: Pool = Pool { AGTexture() } @@ -84,7 +84,7 @@ class RenderContext( fun updateStandardUniforms() { //println("updateStandardUniforms: ag.currentSize(${ag.currentWidth}, ${ag.currentHeight}) : ${ag.currentFrameBuffer}") //if (flipRenderTexture && currentFrameBuffer.isTexture) { - if (flipRenderTexture && currentFrameBuffer.isTexture) { + if (flipRenderTexture && currentFrameBuffer.isTexture && currentFrameBuffer != ag.mainFrameBuffer) { projMat = Matrix4.ortho(Rectangle.fromBounds(0, currentFrameBuffer.height, currentFrameBuffer.width, 0), -1f, 1f) } else { projMat = Matrix4.ortho(Rectangle.fromBounds(0, 0, currentFrameBuffer.width, currentFrameBuffer.height), -1f, 1f) From 3d6fa3e21d97ff1b6bde7322daa0b4a47794139e Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 00:36:44 +0200 Subject: [PATCH 11/66] [WIP] More work on New Game Window --- korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt | 2 +- korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index 2e121c4039..4e0f3fe51f 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -142,7 +142,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP ag.setMainFrameBufferTemporarily(frameBuffer) { doRender(ag) } - ag.textureDrawer.drawXY(mainFrameBuffer, frameBuffer.tex, viewport.x, viewport.y, viewport.width, viewport.height) + ag.textureDrawer.draw(mainFrameBuffer, frameBuffer.tex, -1f, -1f, +1f, +1f) } else { mainFrameBuffer.setSize(scaledWidth, scaledHeight) doRender(ag) diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt index 9900314d44..07e7f813d6 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt @@ -16,7 +16,7 @@ import kotlin.test.* class AwtGameCanvasTest { @Test - //@Ignore + @Ignore fun test() = autoreleasePool { //System.setProperty("sun.java2d.metal", "true") //System.setProperty("sun.java2d.opengl", "false") @@ -63,6 +63,7 @@ class AwtGameCanvasTest { } fpsLabel?.text = "FPS: ${(canvas as AwtAGOpenglCanvas).renderFps}" } + it.visible = true }) /* frame.contentPane.add(GLCanvas().also { From d3d9d7ca5e9a0fdf4474c8197e0505b6f80cb1aa Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 02:04:26 +0200 Subject: [PATCH 12/66] Some fixes --- .../datastructure/_Datastructure_event.kt | 32 +++++++-- .../src/common/korlibs/time/Stopwatch.kt | 1 + .../datastructure/event/SyncEventLoopTest.kt | 2 +- .../commonMain/kotlin/samples/MainClipping.kt | 18 +++-- .../kotlin/samples/MainColorPicker.kt | 29 ++++---- korge/src/common/korlibs/korge/Korge.kt | 55 ++-------------- .../korlibs/korge/KorgeViewsConfigureInput.kt | 44 ++++++++++++- .../korlibs/korge/tests/ViewsForTesting.kt | 66 +++++++++++++------ korge/src/common/korlibs/korge/view/Views.kt | 32 ++++----- korge/src/common/korlibs/render/GameWindow.kt | 21 +++++- .../common/korlibs/render/GameWindowExt.kt | 1 + .../korge/testing/KorgeOffscreenTest.kt | 5 +- .../korlibs/korge/testing/testing_utils.kt | 3 + .../korlibs/korge/animate/AnimatorTest.kt | 1 + .../korlibs/render/awt/AwtGameWindowTest.kt | 2 +- 15 files changed, 188 insertions(+), 124 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 7a88ebb520..06259d3fae 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -47,7 +47,7 @@ class SyncEventLoop( private val tasks = ArrayDeque<() -> Unit>() private val timedTasks = TGenPriorityQueue { a, b -> a.compareTo(b) } - fun queueFirst(task: () -> Unit) { + fun setImmediateFirst(task: () -> Unit) { lock { tasks.addFirst(task) lock.notify() @@ -55,6 +55,7 @@ class SyncEventLoop( } override fun setImmediate(task: () -> Unit) { + //println("setImmediate: task=$task") lock { tasks.addLast(task) lock.notify() @@ -70,6 +71,7 @@ class SyncEventLoop( } private fun _queueAfter(time: TimeSpan, interval: Boolean, task: () -> Unit): Closeable { + //println("_queueAfter: time=$time, interval=$interval, task=$task") return lock { val task = TimedTask(this, now, time, interval, task) if (running) { @@ -105,9 +107,9 @@ class SyncEventLoop( lock.wait(waitTime, precise) } - fun runAvailableNextTasks(): Int { + fun runAvailableNextTasks(runTimers: Boolean = true): Int { var count = 0 - while (runAvailableNextTask()) { + while (runAvailableNextTask(runTimers)) { count++ } return count @@ -123,13 +125,20 @@ class SyncEventLoop( } } - fun runAvailableNextTask(): Boolean { + fun runAvailableNextTask(maxCount: Int): Boolean { + for (n in 0 until maxCount) { + if (!runAvailableNextTask()) return false + } + return true + } + + fun runAvailableNextTask(runTimers: Boolean = true): Boolean { val timedTask = lock { - if (timedTasks.isNotEmpty() && shouldTimedTaskRun(timedTasks.head)) timedTasks.removeHead() else null + if (runTimers) if (timedTasks.isNotEmpty() && shouldTimedTaskRun(timedTasks.head)) timedTasks.removeHead() else null else null } if (timedTask != null) { runCatchingExceptions { timedTask.callback() } - if (timedTask.interval) { + if (timedTask.interval && !immediateRun) { timedTask.timeMark = maxOf(timedTask.timeMark + timedTask.time, now) //println("READDED: timedTask.now=${timedTask.now}") timedTasks.add(timedTask) @@ -138,7 +147,10 @@ class SyncEventLoop( val task = lock { if (tasks.isNotEmpty()) tasks.removeFirst() else null } - runCatchingExceptions { task?.invoke() } + runCatchingExceptions { + //println("RUN TASK $task") + task?.invoke() + } return task != null || timedTask != null } @@ -165,12 +177,18 @@ class SyncEventLoop( fun runTasksUntilEmpty() { //Thread.currentThread().priority = Thread.MAX_PRIORITY // Timed tasks + val stopwatch = Stopwatch().start() while (running) { val somethingExecuted = waitAndRunNextTask() if (lock { !somethingExecuted && tasks.isEmpty() && timedTasks.isEmpty() }) { break } + + if (stopwatch.elapsed >= 0.1.seconds) { + stopwatch.restart() + NativeThread.sleep(10.milliseconds) + } } } diff --git a/korge-foundation/src/common/korlibs/time/Stopwatch.kt b/korge-foundation/src/common/korlibs/time/Stopwatch.kt index 4b7e66d36a..d3449a06d5 100644 --- a/korge-foundation/src/common/korlibs/time/Stopwatch.kt +++ b/korge-foundation/src/common/korlibs/time/Stopwatch.kt @@ -1,6 +1,7 @@ package korlibs.time class Stopwatch(val nanosecondProvider: () -> Double = { PerformanceCounter.nanoseconds }) { + constructor(timeProvider: TimeProvider) : this({ timeProvider.now().unixMillis.milliseconds.nanoseconds }) private var running = false private var startNano = 0.0 private val currentNano get() = nanosecondProvider() diff --git a/korge-foundation/test/common/korlibs/datastructure/event/SyncEventLoopTest.kt b/korge-foundation/test/common/korlibs/datastructure/event/SyncEventLoopTest.kt index 4f30c2f6dd..bc251eb3d3 100644 --- a/korge-foundation/test/common/korlibs/datastructure/event/SyncEventLoopTest.kt +++ b/korge-foundation/test/common/korlibs/datastructure/event/SyncEventLoopTest.kt @@ -26,7 +26,7 @@ class SyncEventLoopTest { } ep.setImmediate { log("hello") } ep.setImmediate { log("world") } - ep.queueFirst { log("hi, ") } + ep.setImmediateFirst { log("hi, ") } if (NativeThread.isSupported) { nativeThread { NativeThread.sleepExact(10.milliseconds) diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainClipping.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainClipping.kt index 850cbc2229..a4b42b9b99 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainClipping.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainClipping.kt @@ -2,6 +2,7 @@ package samples import korlibs.image.color.* import korlibs.image.format.* +import korlibs.io.async.* import korlibs.io.file.std.* import korlibs.korge.scene.* import korlibs.korge.view.* @@ -64,13 +65,16 @@ class MainClipping : Scene() { // This shouldn't be needed since Stage.localMatrix is always the identity onStageResized { width, height -> //println("resized=$width, $height") - container2.removeChildren() - container2.addChild(image(container.unsafeRenderToBitmapSync(views.renderContext).also { - it.updateColors { if (it.a == 0) Colors.RED else it } - }).also { - it.x = 300.0 - it.y = views.virtualHeightDouble - it.bitmap.height - 50 * escale - }) + launch { + val bitmap = container.renderToBitmap(views).also { + it.updateColors { if (it.a == 0) Colors.RED else it } + } + container2.removeChildren() + container2.addChild(image(bitmap).also { + it.x = 300.0 + it.y = views.virtualHeightDouble - it.bitmap.height - 50 * escale + }) + } } } } diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainColorPicker.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainColorPicker.kt index 57354d834f..2984009601 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainColorPicker.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainColorPicker.kt @@ -1,17 +1,12 @@ package samples -import korlibs.korge.input.mouse -import korlibs.korge.scene.Scene -import korlibs.korge.view.SContainer -import korlibs.korge.view.addUpdater -import korlibs.korge.view.image -import korlibs.korge.view.scale -import korlibs.korge.view.unsafeRenderToBitmapSync -import korlibs.korge.view.xy -import korlibs.image.bitmap.Bitmaps -import korlibs.image.bitmap.slice -import korlibs.image.format.readBitmap -import korlibs.io.file.std.resourcesVfs +import korlibs.image.bitmap.* +import korlibs.image.format.* +import korlibs.io.async.* +import korlibs.io.file.std.* +import korlibs.korge.input.* +import korlibs.korge.scene.* +import korlibs.korge.view.* import korlibs.math.geom.* class MainColorPicker : Scene() { @@ -22,9 +17,13 @@ class MainColorPicker : Scene() { mouse { move { - val bmp = stage!!.unsafeRenderToBitmapSync(views!!.renderContext, Rectangle(views.stage.mousePos.x - 5.0, views.stage.mousePos.y - 5.0, 10.0, 10.0), views!!.globalToWindowScaleAvg) - magnifier.bitmap = bmp.slice() - invalidateRender() + + //val bmp = stage!!.unsafeRenderToBitmapSync(views!!.renderContext, Rectangle(views.stage.mousePos.x - 5.0, views.stage.mousePos.y - 5.0, 10.0, 10.0), views!!.globalToWindowScaleAvg) + launch { + val bmp = stage!!.renderToBitmap(views!!, Rectangle(views.stage.mousePos.x - 5.0, views.stage.mousePos.y - 5.0, 10.0, 10.0), views!!.globalToWindowScaleAvg) + magnifier.bitmap = bmp.slice() + invalidateRender() + } } } addUpdater { diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index 9b7626345f..d5c3d4f509 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -6,11 +6,9 @@ import korlibs.image.format.* import korlibs.inject.* import korlibs.io.dynamic.* import korlibs.io.file.std.* -import korlibs.io.resources.* import korlibs.korge.internal.* import korlibs.korge.logger.* import korlibs.korge.render.* -import korlibs.korge.resources.* import korlibs.korge.scene.* import korlibs.korge.view.* import korlibs.logger.* @@ -192,22 +190,11 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen gameWindow.quality = quality gameWindow.backgroundColor = config.backgroundColor ?: Colors.BLACK - RegisteredImageFormats.register(config.imageFormats) val injector = views.injector config.configInjector(injector) - injector.mapInstance(views) - injector.mapInstance(views.ag) + RegisteredImageFormats.register(config.imageFormats) injector.mapInstance(KorgeArgs(config.args)) - injector.mapInstance(Resources::class, views.globalResources) - injector.mapSingleton(ResourcesRoot::class) { ResourcesRoot() } - injector.mapInstance(views.input) - injector.mapInstance(views.stats) - injector.mapInstance(CoroutineContext::class, views.coroutineContext) - injector.mapPrototype(EmptyScene::class) { EmptyScene() } - injector.mapInstance(TimeProvider::class, views.timeProvider) - injector.mapInstance(GameWindow::class, gameWindow) injector.mapInstance(KorgeConfig::class, config) - views.debugViews = gameWindow.debug views.debugFontExtraScale = config.debugFontExtraScale views.debugFontColor = config.debugFontColor views.virtualWidth = config.virtualSize.width.toInt() @@ -217,31 +204,10 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen views.clipBorders = config.displayMode.clipBorders views.targetFps = config.targetFps - var initialized = false - val stopwatch = Stopwatch() - gameWindow.onUpdateEvent { - if (initialized) { - //println("UPDATE") - views.update(stopwatch.getElapsedAndRestart()) - } - } - - var cachedFrameSize = SizeInt(0, 0) - - gameWindow.onRenderEvent { - if (initialized) { - //println("RENDER") - //println("gameWindow.size=${gameWindow.frameSize}") - val frameSize = gameWindow.frameSize - if (cachedFrameSize != frameSize) { - cachedFrameSize = frameSize - views.resized(frameSize.width, frameSize.height) - views.update(0.milliseconds) - } - views.renderNew() - } - } + KorgeReload.registerEventDispatcher(gameWindow) + @Suppress("OPT_IN_USAGE") + views.prepareViewsBase(gameWindow, true, gameWindow.bgcolor, config.forceRenderEveryFrame, config.configInjector) gameWindow.queueSuspend { Korge.logger.info { "Initializing..." } @@ -262,20 +228,8 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen e.printStackTrace() } } - - Korge.logger.logTime("prepareViews") { - KorgeReload.registerEventDispatcher(gameWindow) - @Suppress("OPT_IN_USAGE") - views.prepareViewsBase(gameWindow, true, gameWindow.bgcolor, TimeSpan.NIL, config.forceRenderEveryFrame, config.configInjector) - } - - Korge.logger.logTime("completeViews") { - // Here we can install a debugger, etc. - completeViews(views) - } } finally { Korge.logger.info { "Initialized" } - initialized = true } config.main(views.stage) @@ -284,5 +238,4 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen views.stage.sceneContainer().changeTo(config.mainSceneClass) } } - } diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index 86b7806b07..4bca5b5c76 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -6,22 +6,39 @@ import korlibs.event.* import korlibs.image.color.* import korlibs.inject.* import korlibs.io.async.* +import korlibs.io.resources.* import korlibs.korge.internal.* +import korlibs.korge.resources.* +import korlibs.korge.scene.* import korlibs.korge.view.* import korlibs.math.geom.* +import korlibs.render.* import korlibs.time.* +import kotlin.coroutines.* @KorgeInternal internal fun Views.prepareViewsBase( eventDispatcher: EventListener, clearEachFrame: Boolean = true, bgcolor: RGBA = Colors.TRANSPARENT, - fixedSizeStep: TimeSpan = TimeSpan.NIL, forceRenderEveryFrame: Boolean = true, configInjector: Injector.() -> Unit = {}, ) { val views = this + val injector = views.injector + injector.mapInstance(views) + injector.mapInstance(views.ag) + injector.mapInstance(Resources::class, views.globalResources) + injector.mapSingleton(ResourcesRoot::class) { ResourcesRoot() } + injector.mapInstance(views.input) + injector.mapInstance(views.stats) + injector.mapInstance(CoroutineContext::class, views.coroutineContext) + injector.mapPrototype(EmptyScene::class) { EmptyScene() } + injector.mapInstance(TimeProvider::class, views.timeProvider) + injector.mapInstance(GameWindow::class, gameWindow) + views.debugViews = gameWindow.debug + configInjector(views.injector) val input = views.input @@ -242,4 +259,29 @@ internal fun Views.prepareViewsBase( views.mouseUpdated() } } + + val stopwatch = Stopwatch(views.timeProvider) + gameWindow.onUpdateEvent { + //println("stopwatch.elapsed=${stopwatch.elapsed}") + views.update(stopwatch.getElapsedAndRestart()) + } + + var cachedFrameSize = SizeInt(0, 0) + + gameWindow.onRenderEvent { + //println("RENDER") + //println("gameWindow.size=${gameWindow.frameSize}") + val frameSize = gameWindow.frameSize + if (cachedFrameSize != frameSize) { + cachedFrameSize = frameSize + views.resized(frameSize.width, frameSize.height) + views.update(0.milliseconds) + } + views.renderNew() + } + + Korge.logger.logTime("completeViews") { + // Here we can install a debugger, etc. + completeViews(views) + } } diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index b416d7bb6c..7aef5cac54 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -42,6 +42,7 @@ open class ViewsForTesting( eventLoop.immediateRun = true eventLoop.nowProvider = { time.unixMillisDouble.milliseconds } } + override val autoUpdateInterval = false override var androidContextAny: Any? = null override val devicePixelRatio: Double get() = this@ViewsForTesting.devicePixelRatio override var width: Int = initialSize.width.toInt() @@ -280,6 +281,8 @@ open class ViewsForTesting( return bounds.intersects(visibleBounds) } + var currentFrameTime = this.frameTime + fun viewsTest( timeout: TimeSpan? = DEFAULT_SUSPEND_TEST_TIMEOUT, frameTime: TimeSpan = this.frameTime, @@ -288,10 +291,11 @@ open class ViewsForTesting( forceRenderEveryFrame: Boolean = true, block: suspend Stage.() -> Unit ): AsyncEntryPointResult = suspendTest(timeout = timeout, cond = cond) { + currentFrameTime = frameTime viewsLog.init() this@ViewsForTesting.devicePixelRatio = devicePixelRatio //suspendTest(timeout = timeout, cond = { !OS.isAndroid && !OS.isJs && !OS.isNative }) { - views.prepareViewsBase(gameWindow, fixedSizeStep = frameTime, forceRenderEveryFrame = forceRenderEveryFrame) + views.prepareViewsBase(gameWindow, forceRenderEveryFrame = forceRenderEveryFrame) injector.mapInstance(KorgeConfig( title = "KorgeViewsForTesting", @@ -313,16 +317,37 @@ open class ViewsForTesting( } //println("[a0]") - withTimeoutNullable(timeout ?: TimeSpan.NIL) { - //println("[a1]") - while (!completed) { - //println("FRAME") - simulateFrame() - } - - //println("[a2]") - if (completedException != null) throw completedException!! - } + try { + withTimeoutNullable(timeout ?: TimeSpan.NIL) { + //println("[a1]") + var nframes = 0 + while (!completed) { + //println("[1]") + simulateFrame() + //println("[2]") + //val ntasks = gameWindow.eventLoop.runAvailableNextTask(10) + val ntasks = gameWindow.eventLoop.runAvailableNextTasks() + //println("[3]") + + if (ntasks == 0) { + nframes++ + if (nframes >= 10000) { + completedException = Exception("No tasks ran during frames=$nframes") + break + } + } else { + nframes = 0 + } + //if (ntasks > 0) println("Ran ntasks=$ntasks") + //println("Ran ntasks=$ntasks") + } + + //println("[a2]") + if (completedException != null) throw completedException!! + } + } finally { + gameWindow.close() + } //println("[a3]") } @@ -334,12 +359,12 @@ open class ViewsForTesting( frameTime: TimeSpan = this.frameTime, crossinline block: suspend S.() -> Unit ): AsyncEntryPointResult = viewsTest(timeout, frameTime) { - config?.apply { injector.configInjector() } - injector.configureInjector() - val container = sceneContainer(views) - container.changeTo() - with(container.currentScene as S) { block() } - } + config?.apply { injector.configInjector() } + injector.configureInjector() + val container = sceneContainer(views) + container.changeTo() + with(container.currentScene as S) { block() } + } private var simulatedFrames = 0 @@ -347,14 +372,15 @@ open class ViewsForTesting( private suspend fun simulateFrame(count: Int = 1) { repeat(count) { //println("SIMULATE: $frameTime") - time += frameTime - gameWindow.dispatchRenderEvent() + time += currentFrameTime + gameWindow.dispatchUpdateEvent() + gameWindow.dispatchNewRenderEvent() simulatedFrames++ val now = PerformanceCounter.reference val elapsedSinceLastDelay = now - lastDelay if (elapsedSinceLastDelay >= 1.seconds) { lastDelay = now - delay(1) + delay(1.milliseconds) } } } diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index abd0aa87ab..e717978f89 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -281,24 +281,26 @@ class Views( fun renderNew(frameBuffer: AGFrameBuffer = ag.mainFrameBuffer) { renderContext.currentFrameBuffer = frameBuffer - if (clearEachFrame) renderContext.clear(clearColor, stencil = 0, depth = 1f, clearColor = true, clearStencil = true, clearDepth = true) - onBeforeRender(renderContext) - renderContext.flush() - stage.render(renderContext) - renderContext.flush() - stage.renderDebug(renderContext) - - if (debugViews) { - //renderContext.setTemporalProjectionMatrixTransform(Matrix()) { - run { - debugHandlers.fastForEach { debugHandler -> - this.debugHandler(renderContext) + renderContext.doRenderNew { + if (clearEachFrame) renderContext.clear(clearColor, stencil = 0, depth = 1f, clearColor = true, clearStencil = true, clearDepth = true) + onBeforeRender(renderContext) + renderContext.flush() + stage.render(renderContext) + renderContext.flush() + stage.renderDebug(renderContext) + + if (debugViews) { + //renderContext.setTemporalProjectionMatrixTransform(Matrix()) { + run { + debugHandlers.fastForEach { debugHandler -> + this.debugHandler(renderContext) + } } } - } - onAfterRender(renderContext) - renderContext.flush() + onAfterRender(renderContext) + renderContext.flush() + } } fun render() { diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 34bd6a68b4..e393b86441 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -13,6 +13,7 @@ import korlibs.io.async.* import korlibs.korge.view.* import korlibs.logger.* import korlibs.math.geom.* +import korlibs.platform.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* @@ -94,6 +95,7 @@ open class GameWindow : override val ag: AG = AGDummy() val eventLoop = SyncEventLoop() + val renderEventLoop = SyncEventLoop(immediateRun = true) open val coroutineDispatcher = EventLoopCoroutineDispatcher(eventLoop) var coroutineContext: CoroutineContext = EmptyCoroutineContext @@ -104,7 +106,7 @@ open class GameWindow : @PublishedApi internal val _updateRenderLock = Lock() inline fun updateRenderLock(block: () -> Unit) = _updateRenderLock(block) fun queueSuspend(callback: suspend () -> Unit) { - launch(coroutineDispatcher + coroutineContext) { + launchAsap(coroutineDispatcher + coroutineContext) { callback() } } @@ -118,6 +120,18 @@ open class GameWindow : } return runBlockingNoJs { deferred.await() } } + fun queueRender(callback: () -> Unit) = renderEventLoop.setImmediate(callback) + fun queueRenderSync(callback: () -> Unit) { + if (Platform.isJs) { + callback() + } else { + val done = CompletableDeferred() + renderEventLoop.setImmediate { + try { callback() } finally { done.complete(Unit) } + } + runBlockingNoJs { done.await() } + } + } internal val events = GameWindowEventInstances() internal val gamepadEmitter: GamepadInfoEmitter = GamepadInfoEmitter(this) @@ -153,10 +167,13 @@ open class GameWindow : field = value updateUpdateInterval() } + + open val autoUpdateInterval = true + private var currentUpdateFps: Frequency = 0.hz private var currentUpdateInterval: Closeable? = null private fun updateUpdateInterval(fps: Frequency = this.updateFps) { - if (currentUpdateFps != fps) { + if (autoUpdateInterval && currentUpdateFps != fps) { currentUpdateFps = fps currentUpdateInterval?.close() currentUpdateInterval = eventLoop.setInterval(fps) { diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index 49ae5c788a..b7471cc714 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -160,6 +160,7 @@ fun GameWindow.dispatchUpdateEvent() { } fun GameWindow.dispatchRenderEvent(update: Boolean = true, render: Boolean = true) { updateRenderLock { + renderEventLoop.runAvailableNextTasks(runTimers = false) dispatch(events.renderEvent.reset { this.update = update this.render = render diff --git a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt index 0c8ef1a65e..cec1969617 100644 --- a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt +++ b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt @@ -8,6 +8,7 @@ import korlibs.kgl.* import korlibs.korge.* import korlibs.korge.view.* import korlibs.math.geom.* +import korlibs.render.awt.* import kotlinx.coroutines.* internal fun createKmlGlContext(fboWidth: Int, fboHeight: Int): KmlGlContext { @@ -44,7 +45,6 @@ internal fun createKmlGlContext(fboWidth: Int, fboHeight: Int): KmlGlContext { } fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: Boolean = false, callback: suspend CoroutineScope.(ag: AG) -> Unit) = suspendTest { - /* val fboWidth = fboSize.width.toInt() val fboHeight = fboSize.height.toInt() @@ -61,9 +61,6 @@ fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: B ag.contextsToFree.forEach { it?.unset(); it?.close() } ag.contextsToFree.clear() } - - */ - TODO() } class OffscreenContext(val testClassName: String, val testMethodName: String) { diff --git a/korge/src/jvm/korlibs/korge/testing/testing_utils.kt b/korge/src/jvm/korlibs/korge/testing/testing_utils.kt index 99a49638e2..0748e4dbaa 100644 --- a/korge/src/jvm/korlibs/korge/testing/testing_utils.kt +++ b/korge/src/jvm/korlibs/korge/testing/testing_utils.kt @@ -11,6 +11,8 @@ import korlibs.korge.view.* import korlibs.korge.view.Container import korlibs.korge.view.align.* import korlibs.math.geom.* +import korlibs.time.* +import kotlinx.coroutines.* import kotlinx.coroutines.sync.* import java.awt.* @@ -54,6 +56,7 @@ inline fun korgeScreenshotTestV2( while (testingLock.isLocked) { println("Waiting for test to end...") + delay(100.milliseconds) } val resultsWithErrors = results.results.filter { result -> diff --git a/korge/test/common/korlibs/korge/animate/AnimatorTest.kt b/korge/test/common/korlibs/korge/animate/AnimatorTest.kt index 5a2321ca3b..33f6be1ca1 100644 --- a/korge/test/common/korlibs/korge/animate/AnimatorTest.kt +++ b/korge/test/common/korlibs/korge/animate/AnimatorTest.kt @@ -15,6 +15,7 @@ class AnimatorTest : ViewsForTesting() { fun test() = viewsTest { val view = solidRect(100, 100, Colors.RED) val log = arrayListOf() + //delay(1.seconds) animate(completeOnCancel = false) { moveTo(view, 100, 0) moveBy(view, y = +100.0) diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt index 8a617f9e7b..c74fc7f0e9 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameWindowTest.kt @@ -16,7 +16,7 @@ import kotlin.test.* class AwtGameWindowTest { @Test - //@Ignore + @Ignore fun test() { Korge.logger.level = Logger.Level.DEBUG val gameWindow = AwtGameWindow(GameWindowCreationConfig.DEFAULT.copy(title = "hello")) From f8a71711820b04f794ce8902c45b9c7cb2853016 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 03:22:37 +0200 Subject: [PATCH 13/66] Optimize UIButton.textColor cache --- korge/src/common/korlibs/korge/ui/UIButton.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/korge/src/common/korlibs/korge/ui/UIButton.kt b/korge/src/common/korlibs/korge/ui/UIButton.kt index bbdf745a72..4371f473cb 100644 --- a/korge/src/common/korlibs/korge/ui/UIButton.kt +++ b/korge/src/common/korlibs/korge/ui/UIButton.kt @@ -148,7 +148,9 @@ open class UIButton( var textColor: RGBA get() = richText.defaultStyle.color ?: Colors.WHITE set(value) { - richText = richText.withStyle(style = richText.defaultStyle.copy(color = value)) + if (textColor != value) { + richText = richText.withStyle(style = richText.defaultStyle.copy(color = value)) + } } private fun setInitialState() { From 67434d212778f76aa2e220768e454f5d9d216608 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 03:23:22 +0200 Subject: [PATCH 14/66] Run gl.finish outside the lock --- korge/src/common/korlibs/korge/render/RenderContext.kt | 1 - korge/src/common/korlibs/render/GameWindowExt.kt | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/korge/src/common/korlibs/korge/render/RenderContext.kt b/korge/src/common/korlibs/korge/render/RenderContext.kt index d0a6d239a4..e9e8e7fb58 100644 --- a/korge/src/common/korlibs/korge/render/RenderContext.kt +++ b/korge/src/common/korlibs/korge/render/RenderContext.kt @@ -293,7 +293,6 @@ class RenderContext( * Finishes the drawing and flips the screen. Called by the KorGe engine at the end of the frame. */ fun finish() { - ag.finish() frameBuffers.free(frameFrameBuffers) if (frameFrameBuffers.isNotEmpty()) frameFrameBuffers.clear() } diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index b7471cc714..6fe681141c 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -160,12 +160,13 @@ fun GameWindow.dispatchUpdateEvent() { } fun GameWindow.dispatchRenderEvent(update: Boolean = true, render: Boolean = true) { updateRenderLock { - renderEventLoop.runAvailableNextTasks(runTimers = false) + //renderEventLoop.runAvailableNextTasks(runTimers = false) dispatch(events.renderEvent.reset { this.update = update this.render = render }) } + ag.finish() } fun GameWindow.dispatchNewRenderEvent() { dispatchRenderEvent(update = false, render = true) From 3c61df9f102a76ec07366ea209fa0ffbff883dee Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 03:23:42 +0200 Subject: [PATCH 15/66] Minor --- .../audio/sound/backend/CoreAudioImpl.kt | 1 - korge/src/common/korlibs/render/GameWindow.kt | 29 ++++--- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 79 ++++++++++++------- 3 files changed, 66 insertions(+), 43 deletions(-) diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index 5725ea0857..9677ef4f1b 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -144,7 +144,6 @@ private class JvmCoreAudioPlatformAudioOutput( CoreAudioKit.AudioQueueStart(queue, null).also { if (it != 0) println("CoreAudioKit.AudioQueueStart -> $it") } - } override fun stop() { diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index e393b86441..155ba55522 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -13,7 +13,6 @@ import korlibs.io.async.* import korlibs.korge.view.* import korlibs.logger.* import korlibs.math.geom.* -import korlibs.platform.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* @@ -94,8 +93,8 @@ open class GameWindow : //override val ag: AG = LogAG() override val ag: AG = AGDummy() - val eventLoop = SyncEventLoop() - val renderEventLoop = SyncEventLoop(immediateRun = true) + val eventLoop = SyncEventLoop(precise = false) + //val renderEventLoop = SyncEventLoop(precise = false, immediateRun = true) open val coroutineDispatcher = EventLoopCoroutineDispatcher(eventLoop) var coroutineContext: CoroutineContext = EmptyCoroutineContext @@ -120,18 +119,18 @@ open class GameWindow : } return runBlockingNoJs { deferred.await() } } - fun queueRender(callback: () -> Unit) = renderEventLoop.setImmediate(callback) - fun queueRenderSync(callback: () -> Unit) { - if (Platform.isJs) { - callback() - } else { - val done = CompletableDeferred() - renderEventLoop.setImmediate { - try { callback() } finally { done.complete(Unit) } - } - runBlockingNoJs { done.await() } - } - } + //fun queueRender(callback: () -> Unit) = renderEventLoop.setImmediate(callback) + //fun queueRenderSync(callback: () -> Unit) { + // if (Platform.isJs) { + // callback() + // } else { + // val done = CompletableDeferred() + // renderEventLoop.setImmediate { + // try { callback() } finally { done.complete(Unit) } + // } + // runBlockingNoJs { done.await() } + // } + //} internal val events = GameWindowEventInstances() internal val gamepadEmitter: GamepadInfoEmitter = GamepadInfoEmitter(this) diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index 4e0f3fe51f..f0dc40f1cf 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -1,6 +1,7 @@ package korlibs.render.awt import korlibs.datastructure.* +import korlibs.datastructure.lock.* import korlibs.graphics.* import korlibs.graphics.gl.* import korlibs.kgl.* @@ -24,7 +25,7 @@ import javax.swing.* //open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setProperty("sun.java2d.opengl", "true") }), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { val canvas = object : Canvas() { - override fun paint(g: Graphics) { + private fun renderGraphics(g: Graphics) { counter.add() //super.paint(g) if (ctx == null) { @@ -36,15 +37,56 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP } //g.fillRect(0, 0, 100, 100) val ctx = this@AwtAGOpenglCanvas.ctx + if (isUsingMetalPipeline) { + println("!!! ERROR: Using Metal pipeline ${this::class} won't work") + } + //println("CTX: $ctx") + ctx?.useContext(g, ag, paintInContextDelegate) if (ctx != null) { if (autoRepaint && ctx.supportsSwapInterval()) { - SwingUtilities.invokeLater { requestFrame() } + //if (true) { + SwingUtilities.invokeLater { + //vsyncLock { vsyncLock.wait(0.5.seconds) } + requestFrame() + } } - if (isUsingMetalPipeline) { - println("!!! ERROR: Using Metal pipeline ${this::class} won't work") + } + } + + override fun paint(g: Graphics) { + //if (!createdBufferStrategy) { + // createdBufferStrategy = true + // val buf = createBufferStrategy(2) + //} + renderGraphics(g) + } + + //var createdBufferStrategy = false + + private val dl: OSXDisplayLink? = if (!Platform.isMac) null else OSXDisplayLink { + //private val dl: OSXDisplayLink? = if (true) null else OSXDisplayLink { + //if (autoRepaint && ctx?.supportsSwapInterval() != true && createdBufferStrategy) { + if (autoRepaint && ctx?.supportsSwapInterval() != true) { + //val bufferStrat = bufferStrategy + //val g = bufferStrat.drawGraphics + //renderGraphics(g) + //bufferStrategy.show() + + //requestFrame() + SwingUtilities.invokeLater { requestFrame() } + //vsyncLock { vsyncLock.notify() } + } + //println("FRAME!") + } + + init { + addHierarchyListener { + if (dl == null) return@addHierarchyListener + val added = getContainerFrame()?.isVisible == true + if (dl.running != added) { + if (added) dl.start() else dl.stop() + //println("!!! processHierarchyEvent: added=$added") } - //println("CTX: $ctx") - ctx.useContext(g, ag, paintInContextDelegate) } } }.also { layout = GridLayout(1, 1) }.also { add(it) } @@ -55,6 +97,8 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP var autoRepaint = true + val vsyncLock = Lock() + private val counter = TimeSampler(1.seconds) val renderFps: Int get() = counter.count @@ -64,30 +108,11 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP contextLost = true } - private val dl = if (!Platform.isMac) null else OSXDisplayLink { - if (autoRepaint && ctx?.supportsSwapInterval() != true) { - requestFrame() - //SwingUtilities.invokeLater { repaint() } - } - //println("FRAME!") - } - fun requestFrame() { canvas.repaint() //repaint() } - init { - addHierarchyListener { - if (dl == null) return@addHierarchyListener - val added = getContainerFrame()?.isVisible == true - if (dl.running != added) { - if (added) dl.start() else dl.stop() - //println("!!! processHierarchyEvent: added=$added") - } - } - } - val state: KmlGlState by lazy { (ag as AGOpengl).createGlState() } override var actualVirtualBounds: Rectangle = Rectangle(0, 0, 200, 200) @@ -123,8 +148,8 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP //println("factor=$factor") - if (viewport != null) { - //if (false) { + //if (viewport != null) { + if (false) { viewport!! mainFrameBuffer.setSize( (viewport.x * viewportScale).toInt(), From a581e445d7a95c1c9c80f306ab1f945db126fe06 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 03:23:55 +0200 Subject: [PATCH 16/66] Optimize MainTiledBackground --- .../commonMain/kotlin/samples/MainTiledBackground.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainTiledBackground.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainTiledBackground.kt index 5477370bc7..456d1d73dc 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainTiledBackground.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainTiledBackground.kt @@ -1,7 +1,7 @@ package samples import korlibs.datastructure.* -import korlibs.image.bitmap.* +import korlibs.image.atlas.* import korlibs.image.format.* import korlibs.image.tiles.* import korlibs.io.file.std.* @@ -12,9 +12,13 @@ import korlibs.korge.view.tiles.* class MainTiledBackground : Scene() { override suspend fun SContainer.sceneMain() { + val atlas = MutableAtlasUnit() + val korgePng = atlas.add(bitmap("korge.png").toBMP32().scaleLinear(0.5, 0.5)) + val korimPng = atlas.add(bitmap("korim.png").toBMP32().scaleLinear(0.5, 0.5)) + val tileset = TileSet( - TileSetTileInfo(0, bitmap("korge.png").toBMP32().scaleLinear(0.5, 0.5).slice()), - TileSetTileInfo(1, bitmap("korim.png").toBMP32().scaleLinear(0.5, 0.5).slice()), + TileSetTileInfo(0, korgePng.slice), + TileSetTileInfo(1, korimPng.slice), //TileSetTileInfo(1, Bitmap32(256, 256, Colors.MEDIUMAQUAMARINE).premultipliedIfRequired().slice()) ) val tilemap = tileMap(IntArray2(2, 2, intArrayOf(0, 1, 1, 0)), repeatX = TileMapRepeat.REPEAT, repeatY = TileMapRepeat.REPEAT, tileset = tileset) From 6198f8c2734caefd803af1aba7a3c29f42db81b4 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 03:25:25 +0200 Subject: [PATCH 17/66] Do not GC on event loop. Fixes performance issues --- .../korlibs/datastructure/_Datastructure_event.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 06259d3fae..26548267fc 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -160,10 +160,10 @@ class SyncEventLoop( lock { if (tasks.isEmpty() && timedTasks.isNotEmpty()) { val head = timedTasks.head - if ((head.timeMark - now) >= 16.milliseconds) { - //println("GC") - NativeThread.gc(full = false) - } + //if ((head.timeMark - now) >= 16.milliseconds) { + // //println("GC") + // //NativeThread.gc(full = false) + //} val waitTime = head.timeMark - now if (waitTime >= 0.seconds) { wait(waitTime) @@ -196,7 +196,7 @@ class SyncEventLoop( running = true while (running) { runTasksUntilEmpty() - NativeThread.gc(full = true) + //NativeThread.gc(full = true) NativeThread.sleep(1.milliseconds) } } From 39b2f346ffc1dd128168e82a2ea025b0d402239c Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 04:03:42 +0200 Subject: [PATCH 18/66] Some fixes and improvements --- .../korlibs/korge/KorgeViewsConfigureInput.kt | 2 +- korge/src/common/korlibs/render/GameWindow.kt | 3 +- korge/src/jvm/korlibs/korge/KorgeExtJvm.kt | 5 +- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 7 +- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 4 ++ .../render/awt/AwtGameWindowDebugger.kt | 65 +++++++++++++++++++ 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index 4bca5b5c76..aeafcce368 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -271,7 +271,7 @@ internal fun Views.prepareViewsBase( gameWindow.onRenderEvent { //println("RENDER") //println("gameWindow.size=${gameWindow.frameSize}") - val frameSize = gameWindow.frameSize + val frameSize = gameWindow.scaledFrameSize if (cachedFrameSize != frameSize) { cachedFrameSize = frameSize views.resized(frameSize.width, frameSize.height) diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 155ba55522..3a27588c80 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -194,6 +194,7 @@ open class GameWindow : open val width: Int = 0 open val height: Int = 0 val frameSize get() = SizeInt(width, height) + val scaledFrameSize get() = SizeInt((width * devicePixelRatio).toInt(), (height * devicePixelRatio).toInt()) open fun setSize(width: Int, height: Int): Unit = Unit open var vsync: Boolean = true open var icon: Bitmap? = null @@ -212,8 +213,8 @@ open class GameWindow : open var debug: Boolean = false set(value) { field = value - onDebugChanged(value) if (value) onDebugEnabled() + onDebugChanged(value) } var exitProcessOnClose: Boolean = true diff --git a/korge/src/jvm/korlibs/korge/KorgeExtJvm.kt b/korge/src/jvm/korlibs/korge/KorgeExtJvm.kt index 19422965ed..96fbe9e4ee 100644 --- a/korge/src/jvm/korlibs/korge/KorgeExtJvm.kt +++ b/korge/src/jvm/korlibs/korge/KorgeExtJvm.kt @@ -2,15 +2,16 @@ package korlibs.korge import korlibs.image.bitmap.* import korlibs.image.color.* -import korlibs.time.* import korlibs.korge.awt.* import korlibs.korge.time.* import korlibs.korge.view.* import korlibs.korge.view.Ellipse +import korlibs.korge.view.Image import korlibs.math.geom.* +import korlibs.time.* import kotlinx.coroutines.* import java.awt.Container -import java.util.ServiceLoader +import java.util.* interface ViewsCompleter { fun completeViews(views: Views) diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index f0dc40f1cf..fda5277703 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -102,6 +102,8 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP private val counter = TimeSampler(1.seconds) val renderFps: Int get() = counter.count + + private var contextLost: Boolean = false fun contextLost() { @@ -135,6 +137,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP val frameOrComponent = this val transform = this.graphicsConfiguration.defaultTransform val factor = transform.scaleX + //val factor = 1.0 val scaledWidth = (frameOrComponent.width * factor).toInt() val scaledHeight = (frameOrComponent.height * factor).toInt() @@ -148,8 +151,8 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP //println("factor=$factor") - //if (viewport != null) { - if (false) { + if (viewport != null) { + //if (false) { viewport!! mainFrameBuffer.setSize( (viewport.x * viewportScale).toInt(), diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 9e43d171b5..c0d0f76256 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -42,6 +42,7 @@ open class AwtCanvasGameWindow( override val width: Int get() = canvas.width override val height: Int get() = canvas.height + override var backgroundColor: RGBA get() = canvas.background.toRgba() set(value) { canvas.background = value.toAwt() } @@ -249,4 +250,7 @@ class AwtGameWindow( System.exit(exitCode) } } + + val awtGameWindowDebugger = AwtGameWindowDebugger(this, frame) + override val debugComponent: Any? = awtGameWindowDebugger.debugFrame } diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt new file mode 100644 index 0000000000..166c79cdf7 --- /dev/null +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt @@ -0,0 +1,65 @@ +package korlibs.render.awt + +import korlibs.math.* +import korlibs.math.geom.* +import korlibs.render.* +import java.awt.* +import java.awt.event.* +import javax.swing.* + +class AwtGameWindowDebugger(val gameWindow: GameWindow, val mainFrame: JFrame) { + + val debugFrame = JFrame("Debug").apply { + try { + this.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE + this.setSize(280, 256) + this.type = Window.Type.UTILITY + this.isAlwaysOnTop = true + } catch (e: Throwable) { + e.printStackTrace() + } + val debugFrame = this + gameWindow.onDebugChanged.add { + EventQueue.invokeLater { + debugFrame.isVisible = it + synchronizeDebugFrameCoordinates() + if (debugFrame.isVisible) { + //frame.isVisible = false + mainFrame.isVisible = true + } + } + } + } + + init { + + mainFrame.addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent?) { + debugFrame.isVisible = false + debugFrame.dispose() + gameWindow.close() + } + }) + mainFrame.addComponentListener(object : ComponentAdapter() { + override fun componentMoved(e: ComponentEvent?) { + synchronizeDebugFrameCoordinates() + } + + override fun componentResized(e: ComponentEvent?) { + synchronizeDebugFrameCoordinates() + } + }) + } + + + private fun synchronizeDebugFrameCoordinates() { + val displayMode = mainFrame.getScreenDevice().displayMode + //println("frame.location=${frame.location}, frame.size=${frame.size}, debugFrame.width=${debugFrame.width}, displayMode=${displayMode.width}x${displayMode.height}") + val frameBounds = RectangleInt(mainFrame.location.x, mainFrame.location.y, mainFrame.size.width, mainFrame.size.height) + debugFrame.setLocation(frameBounds.right.clamp(0, (displayMode.width - debugFrame.width * 1.0).toInt()), frameBounds.top) + debugFrame.setSize(debugFrame.width.coerceAtLeast(64), frameBounds.height) + //debugFrame.pack() + //debugFrame.doLayout() + //debugFrame.repaint() + } +} From 095ec760d060a5f137107b02c4f301f4a87cfcd2 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 05:02:14 +0200 Subject: [PATCH 19/66] Added arraycopyStride --- .../src/common/korlibs/memory/Arrays.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/korge-foundation/src/common/korlibs/memory/Arrays.kt b/korge-foundation/src/common/korlibs/memory/Arrays.kt index aa7ae88b83..f97e858f85 100644 --- a/korge-foundation/src/common/korlibs/memory/Arrays.kt +++ b/korge-foundation/src/common/korlibs/memory/Arrays.kt @@ -236,6 +236,21 @@ public fun FloatArray.lastIndexOf(sub: FloatArray, starting: Int = 0): Int = arr public fun DoubleArray.lastIndexOf(sub: DoubleArray, starting: Int = 0): Int = array_lastIndexOf(starting, size, sub.size) { n, m -> this[n] == sub[m] } public fun Array.lastIndexOf(sub: Array, starting: Int = 0): Int = array_lastIndexOf(starting, size, sub.size) { n, m -> this[n] == sub[m] } +public fun arraycopyStride(src: ByteArray, srcPos: Int, srcStride: Int, dst: ByteArray, dstPos: Int, dstStride: Int, size: Int) { + for (n in 0 until size) dst[dstPos + dstStride * n] = src[srcPos + srcStride * n] +} + +public fun arraycopyStride(src: ShortArray, srcPos: Int, srcStride: Int, dst: ShortArray, dstPos: Int, dstStride: Int, size: Int) { + for (n in 0 until size) dst[dstPos + dstStride * n] = src[srcPos + srcStride * n] +} + +public fun arraycopyStride(src: IntArray, srcPos: Int, srcStride: Int, dst: IntArray, dstPos: Int, dstStride: Int, size: Int) { + for (n in 0 until size) dst[dstPos + dstStride * n] = src[srcPos + srcStride * n] +} + +public fun arraycopyStride(src: FloatArray, srcPos: Int, srcStride: Int, dst: FloatArray, dstPos: Int, dstStride: Int, size: Int) { + for (n in 0 until size) dst[dstPos + dstStride * n] = src[srcPos + srcStride * n] +} public fun arrayinterleave( out: ByteArray, outPos: Int, From 9ec074ede74e51e1d7711450559ded36606d1675 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 05:02:26 +0200 Subject: [PATCH 20/66] Added NewPlatformAudioOutput callback-based --- .../audio/sound/PlatformAudioOutput.kt | 26 ++++ .../src/common/korlibs/audio/sound/Sound.kt | 41 +++--- .../src/common/korlibs/io/async/AsyncExt.kt | 12 ++ .../audio/sound/CoreAudioSoundProvider.kt | 85 +++++------ .../audio/sound/backend/CoreAudioImpl.kt | 137 ++++++++++++++++++ korge-sandbox/src/commonMain/kotlin/Main.kt | 18 +-- .../kotlin/samples/MainPolyphonic.kt | 28 ++-- 7 files changed, 251 insertions(+), 96 deletions(-) diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index 04f7a9dd9f..24f99aff34 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -8,6 +8,32 @@ import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* +open class NewPlatformAudioOutput( + val coroutineContext: CoroutineContext, + val buffer: AudioSamples, + val frequency: Int, + val gen: (AudioSamples) -> Unit, +) : Disposable, SoundProps { + private val lock = Lock() + fun safeGen(buffer: AudioSamples) { + lock { + try { + gen(buffer) + } catch (e: Throwable) { + e.printStackTrace() + } + } + } + + val nchannels get() = buffer.channels + override var pitch: Double = 1.0 + override var volume: Double = 1.0 + override var panning: Double = 0.0 + open fun start() = Unit + open fun stop() = Unit + final override fun dispose() = stop() +} + open class PlatformAudioOutput( val coroutineContext: CoroutineContext, val frequency: Int diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 359f822bd0..02542312ff 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -1,28 +1,15 @@ package korlibs.audio.sound -import korlibs.datastructure.Extra -import korlibs.time.DateTime -import korlibs.time.TimeSpan -import korlibs.time.milliseconds -import korlibs.time.seconds -import korlibs.audio.format.AudioDecodingProps -import korlibs.audio.format.AudioFormats -import korlibs.audio.format.WAV -import korlibs.audio.format.defaultAudioFormats -import korlibs.io.async.Signal -import korlibs.io.async.delay -import korlibs.io.concurrent.atomic.korAtomic -import korlibs.io.file.FinalVfsFile -import korlibs.io.file.Vfs -import korlibs.io.file.VfsFile -import korlibs.io.file.baseName -import korlibs.io.lang.Disposable -import korlibs.io.lang.unsupported -import korlibs.io.stream.AsyncStream -import korlibs.io.stream.openAsync +import korlibs.audio.format.* +import korlibs.datastructure.* +import korlibs.io.async.* +import korlibs.io.file.* +import korlibs.io.lang.* +import korlibs.io.stream.* +import korlibs.time.* import kotlinx.coroutines.* -import kotlin.coroutines.CoroutineContext -import kotlin.native.concurrent.ThreadLocal +import kotlin.coroutines.* +import kotlin.native.concurrent.* import kotlin.coroutines.coroutineContext as coroutineContextKt @ThreadLocal @@ -34,6 +21,8 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS override val target: String get() = parent.target override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = parent.createPlatformAudioOutput(coroutineContext, freq) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = + parent.createNewPlatformAudioOutput(coroutineContext, buffer, freq, gen) override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = parent.createSound(data, streaming, props, name) @@ -57,10 +46,16 @@ open class NativeSoundProvider : Disposable { open var paused: Boolean = false open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput = PlatformAudioOutput(coroutineContext, freq) + open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + println("createNewPlatformAudioOutput: ${this::class}") + return NewPlatformAudioOutput(coroutineContext, buffer, freq, gen) + } suspend fun createPlatformAudioOutput(freq: Int = 44100): PlatformAudioOutput = createPlatformAudioOutput(coroutineContextKt, freq) - open suspend fun createSound(data: ByteArray, streaming: Boolean = false, props: AudioDecodingProps = AudioDecodingProps.DEFAULT, name: String = "Unknown"): Sound { + suspend fun createNewPlatformAudioOutput(buffer: AudioSamples, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = createNewPlatformAudioOutput(coroutineContextKt, buffer, freq, gen) + + open suspend fun createSound(data: ByteArray, streaming: Boolean = false, props: AudioDecodingProps = AudioDecodingProps.DEFAULT, name: String = "Unknown"): Sound { val format = props.formats ?: audioFormats val stream = format.decodeStreamOrError(data.openAsync(), props) return if (streaming) { diff --git a/korge-core/src/common/korlibs/io/async/AsyncExt.kt b/korge-core/src/common/korlibs/io/async/AsyncExt.kt index 2a3ec9d64f..ec6cfa57cf 100644 --- a/korge-core/src/common/korlibs/io/async/AsyncExt.kt +++ b/korge-core/src/common/korlibs/io/async/AsyncExt.kt @@ -22,6 +22,18 @@ suspend fun CoroutineContext.launchUnscopedAndWait(block: suspend () -> T): return deferred.await() } +fun CoroutineContext.onCancel(block: () -> Unit): Cancellable { + var running = true + launchUnscoped { + try { + while (running) kotlinx.coroutines.delay(1.seconds) + } catch (e: CancellationException) { + if (running) block() + } + } + return Cancellable { running = false } +} + fun CoroutineContext.launchUnscoped(block: suspend () -> Unit) { block.startCoroutine(object : Continuation { override val context: CoroutineContext = this@launchUnscoped diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index 4c594b08c8..5d55ca7f71 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -2,62 +2,23 @@ package korlibs.audio.sound //import mystdio.* import cnames.structs.OpaqueAudioQueue -import kotlinx.cinterop.Arena +import kotlinx.cinterop.* import kotlinx.cinterop.COpaquePointer -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.CPointerVarOf import kotlinx.cinterop.ShortVar -import kotlinx.cinterop.StableRef -import kotlinx.cinterop.alloc -import kotlinx.cinterop.allocArray -import kotlinx.cinterop.asStableRef import kotlinx.cinterop.convert import kotlinx.cinterop.get -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.plus -import kotlinx.cinterop.pointed -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret import kotlinx.cinterop.set -import kotlinx.cinterop.staticCFunction -import kotlinx.cinterop.value -import platform.AudioToolbox.AudioQueueAllocateBuffer -import platform.AudioToolbox.AudioQueueBuffer +import platform.AudioToolbox.* import platform.AudioToolbox.AudioQueueBufferRef -import platform.AudioToolbox.AudioQueueDispose -import platform.AudioToolbox.AudioQueueEnqueueBuffer -import platform.AudioToolbox.AudioQueueNewOutput import platform.AudioToolbox.AudioQueueRef -import platform.AudioToolbox.AudioQueueStart -import platform.AudioToolbox.kAudioFormatUnsupportedDataFormatError -import platform.AudioToolbox.kAudioQueueErr_BufferEmpty -import platform.AudioToolbox.kAudioQueueErr_BufferInQueue -import platform.AudioToolbox.kAudioQueueErr_CannotStart -import platform.AudioToolbox.kAudioQueueErr_CodecNotFound -import platform.AudioToolbox.kAudioQueueErr_DisposalPending -import platform.AudioToolbox.kAudioQueueErr_EnqueueDuringReset -import platform.AudioToolbox.kAudioQueueErr_InvalidBuffer -import platform.AudioToolbox.kAudioQueueErr_InvalidCodecAccess -import platform.AudioToolbox.kAudioQueueErr_InvalidDevice -import platform.AudioToolbox.kAudioQueueErr_InvalidOfflineMode -import platform.AudioToolbox.kAudioQueueErr_InvalidParameter -import platform.AudioToolbox.kAudioQueueErr_InvalidProperty -import platform.AudioToolbox.kAudioQueueErr_InvalidPropertySize -import platform.AudioToolbox.kAudioQueueErr_InvalidPropertyValue -import platform.AudioToolbox.kAudioQueueErr_InvalidQueueType -import platform.AudioToolbox.kAudioQueueErr_InvalidRunState -import platform.AudioToolbox.kAudioQueueErr_Permissions -import platform.AudioToolbox.kAudioQueueErr_PrimeTimedOut -import platform.AudioToolbox.kAudioQueueErr_QueueInvalidated -import platform.AudioToolbox.kAudioQueueErr_RecordUnderrun -import platform.CoreAudioTypes.AudioStreamBasicDescription -import platform.CoreAudioTypes.kAudioFormatFlagIsPacked -import platform.CoreAudioTypes.kAudioFormatLinearPCM -import platform.CoreAudioTypes.kLinearPCMFormatFlagIsSignedInteger -import platform.CoreFoundation.CFRunLoopGetCurrent -import platform.CoreFoundation.kCFRunLoopCommonModes +import platform.CoreAudioTypes.* +import platform.CoreFoundation.* import platform.darwin.OSStatus -import kotlin.coroutines.CoroutineContext +import kotlin.Int +import kotlin.String +import kotlin.Unit +import kotlin.coroutines.* +import kotlin.native.ThreadLocal actual val nativeSoundProvider: NativeSoundProvider get() = CORE_AUDIO_NATIVE_SOUND_PROVIDER expect fun appleInitAudio() @@ -73,6 +34,34 @@ class CoreAudioNativeSoundProvider : NativeSoundProvider() { //override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps): NativeSound = AVFoundationNativeSoundNoStream(CoroutineScope(coroutineContext), audioFormats.decode(data)) override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = CoreAudioPlatformAudioOutput(coroutineContext, freq) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, freq, buffer, gen) +} + +class CoreAudioNewPlatformAudioOutput( + coroutineContext: CoroutineContext, + freq: Int, + buffer: AudioSamples, + gen: (AudioSamples) -> Unit, +) : NewPlatformAudioOutput(coroutineContext, freq, buffer, gen) { + val generator = CoreAudioGenerator(freq, buffer.channels, coroutineContext = coroutineContext) { data, dataSize -> + val samples = AudioSamples(buffer.channels, dataSize) + gen(samples) + + println("GEN") + for (m in 0 until nchannels) { + for (n in 0 until dataSize / nchannels) { + data[n * nchannels + m] = samples[m, n] + } + } + } + override fun start() { + println("GEN.start") + generator.start() + } + override fun stop() { + println("GEN.stop") + generator.dispose() + } } class CoreAudioPlatformAudioOutput( diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index 9677ef4f1b..8a3c0527b4 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -5,6 +5,9 @@ import korlibs.audio.sound.* import korlibs.ffi.* import korlibs.internal.osx.* import korlibs.io.annotations.* +import korlibs.io.async.* +import korlibs.io.lang.* +import korlibs.memory.* import java.util.concurrent.* import java.util.concurrent.atomic.* import kotlin.coroutines.* @@ -20,6 +23,9 @@ val jvmCoreAudioNativeSoundProvider: JvmCoreAudioNativeSoundProvider? by lazy { class JvmCoreAudioNativeSoundProvider : NativeSoundProvider() { override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = JvmCoreAudioPlatformAudioOutput(coroutineContext, freq) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + return JvmCoreAudioNewPlatformAudioOutput(coroutineContext, buffer, freq, gen) + } } private val audioOutputsById = ConcurrentHashMap() @@ -77,6 +83,137 @@ private val jnaCoreAudioCallback by lazy { } } +private val newAudioOutputsById = ConcurrentHashMap() +private val ctiNew by lazy { CallbackThreadInitializer() } +private val jnaNewCoreAudioCallback by lazy { + AudioQueueNewOutputCallback { inUserData, inAQ, inBuffer -> + try { + val output = newAudioOutputsById[(inUserData?.address ?: 0L).toLong()] ?: return@AudioQueueNewOutputCallback 0 + val nchannels = output.nchannels + + //val tone = AudioTone.generate(1.seconds, 41000.0) + val queue = AudioQueueBuffer(inBuffer) + val ptr = queue.mAudioData + val samplesCount = (queue.mAudioDataByteSize / Short.SIZE_BYTES) + //println("samplesCount=$samplesCount") + + if (ptr != null) { + val samples = AudioSamples(nchannels, samplesCount) + output.safeGen(samples) + val buffer = ShortArray(samplesCount * nchannels) + + for (m in 0 until nchannels) { + arraycopyStride( + samples.data[m], 0, 1, + buffer, m, nchannels, + samplesCount + ) + } + for (n in 0 until samplesCount * nchannels) { + ptr[n] = buffer[n] + } + } + //println("queue.mAudioData=${queue.mAudioData}") + + if (!output.completed) { + CoreAudioKit.AudioQueueEnqueueBuffer(inAQ, queue.ptr, 0, null).also { + if (it != 0) println("CoreAudioKit.AudioQueueEnqueueBuffer -> $it") + } + } else { + Unit + } + } catch (e: Throwable) { + e.printStackTrace() + } + 0 + }.also { + Native.setCallbackThreadInitializer(it, ctiNew) + } +} + +private class JvmCoreAudioNewPlatformAudioOutput( + coroutineContext: CoroutineContext, + buffer: AudioSamples, + freq: Int, + gen: (AudioSamples) -> Unit, +) : NewPlatformAudioOutput(coroutineContext, buffer, freq, gen) { + val id = lastId.incrementAndGet() + companion object { + private var lastId = AtomicLong(0L) + const val bufferSizeInBytes = 2048 + const val numBuffers = 3 + } + + var onCancel: Cancellable? = null + + init { + } + + internal var completed = false + + var queue: Pointer? = null + + override fun start() { + stop() + + onCancel = coroutineContext.onCancel { stop() } + newAudioOutputsById[id] = this + completed = false + val queueRef = Memory(16).also { it.clear() } + val format = AudioStreamBasicDescription(Memory(40).also { it.clear() }) + + format.mSampleRate = frequency.toDouble() + format.mFormatID = CoreAudioKit.kAudioFormatLinearPCM + format.mFormatFlags = CoreAudioKit.kLinearPCMFormatFlagIsSignedInteger or CoreAudioKit.kAudioFormatFlagIsPacked + format.mBitsPerChannel = (8 * Short.SIZE_BYTES) + format.mChannelsPerFrame = nchannels + format.mBytesPerFrame = (Short.SIZE_BYTES * format.mChannelsPerFrame) + format.mFramesPerPacket = 1 + format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket + format.mReserved = 0 + + val userDefinedPtr = Pointer(id.toLong()) + + CoreAudioKit.AudioQueueNewOutput( + format.ptr, jnaNewCoreAudioCallback, userDefinedPtr, + //CoreFoundation.CFRunLoopGetCurrent(), + CoreFoundation.CFRunLoopGetMain(), // @TODO: Use other Loop to avoid issues + CoreFoundation.kCFRunLoopCommonModes, 0, queueRef + ).also { + if (it != 0) println("CoreAudioKit.AudioQueueNewOutput -> $it") + } + queue = queueRef.getPointer(0L) + //println("result=$result, queue=$queue") + val buffersArray = Memory((8 * numBuffers).toLong()).also { it.clear() } + for (buf in 0 until numBuffers) { + val bufferPtr = Pointer(buffersArray.address + 8 * buf) + CoreAudioKit.AudioQueueAllocateBuffer(queue, bufferSizeInBytes, bufferPtr).also { + if (it != 0) println("CoreAudioKit.AudioQueueAllocateBuffer -> $it") + } + val ptr = AudioQueueBuffer(bufferPtr.getPointer(0)) + //println("AudioQueueAllocateBuffer=$res, ptr.pointer=${ptr.pointer}") + ptr.mAudioDataByteSize = bufferSizeInBytes + jnaNewCoreAudioCallback.callback(userDefinedPtr, queue, ptr.ptr) + } + CoreAudioKit.AudioQueueStart(queue, null).also { + if (it != 0) println("CoreAudioKit.AudioQueueStart -> $it") + } + } + + override fun stop() { + completed = true + + onCancel?.cancel() + onCancel = null + + if (queue != null) { + CoreAudioKit.AudioQueueDispose(queue, false) + queue = null + } + newAudioOutputsById.remove(id) + } +} + private class JvmCoreAudioPlatformAudioOutput( coroutineContext: CoroutineContext, frequency: Int diff --git a/korge-sandbox/src/commonMain/kotlin/Main.kt b/korge-sandbox/src/commonMain/kotlin/Main.kt index 5eea3b0024..100b6d1029 100644 --- a/korge-sandbox/src/commonMain/kotlin/Main.kt +++ b/korge-sandbox/src/commonMain/kotlin/Main.kt @@ -1,27 +1,14 @@ -import korlibs.audio.sound.* import korlibs.event.* import korlibs.image.color.* -import korlibs.image.font.* -import korlibs.image.format.* import korlibs.image.text.* -import korlibs.image.vector.* -import korlibs.image.vector.format.SVG import korlibs.io.async.* -import korlibs.io.file.std.* -import korlibs.io.lang.* import korlibs.korge.* import korlibs.korge.input.* -import korlibs.korge.scene.* import korlibs.korge.time.* import korlibs.korge.tween.* -import korlibs.korge.ui.* import korlibs.korge.view.* -import korlibs.korge.view.align.alignBottomToBottomOf -import korlibs.korge.view.align.alignLeftToLeftOf -import korlibs.korge.view.align.alignTopToTopOf -import korlibs.korge.view.align.centerXOn -import korlibs.math.geom.* +import korlibs.korge.view.align.* import korlibs.math.interpolation.* import korlibs.time.* import samples.* @@ -135,7 +122,8 @@ suspend fun main() = Korge( //Demo(::MainColorTransformFilter), //Demo(::MainMasks), //Demo(::MainShape2dScene), - Demo(::MainUIStacks), + //Demo(::MainUIStacks), + Demo(::MainPolyphonic), //Demo(::MainSprites10k), //Demo(::MainStressMatrixMultiplication), //Demo(::MainSDF), diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt index 32bb30737d..fb281d59ee 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt @@ -1,14 +1,11 @@ package samples -import korlibs.memory.arraycopy -import korlibs.math.clamp -import korlibs.audio.sound.AudioSamples -import korlibs.audio.sound.nativeSoundProvider -import korlibs.korge.scene.Scene -import korlibs.korge.ui.uiVerticalStack -import korlibs.korge.view.SContainer -import korlibs.korge.view.text -import korlibs.io.async.launchImmediately +import korlibs.audio.sound.* +import korlibs.korge.scene.* +import korlibs.korge.ui.* +import korlibs.korge.view.* +import korlibs.math.* +import korlibs.memory.* import kotlin.math.* class MainPolyphonic : Scene() { @@ -38,6 +35,15 @@ class MainPolyphonic : Scene() { channelStates[1].noteIndex = 0; nextNote(1) for (nchannel in 0 until 2) { + val stream2 = nativeSoundProvider.createNewPlatformAudioOutput(AudioSamples(1, 4410), 44100) { samples -> + audioOutCallback(nchannel, samples.data[0], samples.data[0].size) + for (n in 1 until samples.channels) { + arraycopy(samples.data[0], 0, samples.data[n], 0, samples.data[0].size) + } + samples.scaleVolume(.05f) + } + stream2.start() + /* //for (nchannel in 0 until 1) { launchImmediately { //AudioTone.generate(0.25.seconds, 440.0).playAndWait() @@ -58,11 +64,13 @@ class MainPolyphonic : Scene() { stream.add(samples) } } + + */ } } companion object { - const val SAMPLE_COUNT = 0x10000 + const val SAMPLE_COUNT = 0x20000 val sample = FloatArray(SAMPLE_COUNT) const val SAMPLE_RATE = 44100 From 31217f3073c4f479e4212b5ac05ecccb357798be Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 05:19:10 +0200 Subject: [PATCH 21/66] Super stable audio on MacOS by using the NewPlatformAudioOutput --- .../audio/sound/PlatformAudioOutput.kt | 18 +++ .../audio/sound/backend/CoreAudioImpl.kt | 150 +----------------- 2 files changed, 24 insertions(+), 144 deletions(-) diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index 24f99aff34..33eb577d8c 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -34,6 +34,24 @@ open class NewPlatformAudioOutput( final override fun dispose() = stop() } +open class PlatformAudioOutputBasedOnNew( + val soundProvider: NativeSoundProvider, + coroutineContext: CoroutineContext, + frequency: Int, +) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { + val new = soundProvider.createNewPlatformAudioOutput(coroutineContext, AudioSamples(2, 4410), frequency) { buffer -> + readShorts(buffer.data) + } + + override fun start() { + new.start() + } + + override fun stop() { + new.stop() + } +} + open class PlatformAudioOutput( val coroutineContext: CoroutineContext, val frequency: Int diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index 8a3c0527b4..ac0c6177e6 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -3,7 +3,6 @@ package korlibs.audio.sound.backend import com.sun.jna.* import korlibs.audio.sound.* import korlibs.ffi.* -import korlibs.internal.osx.* import korlibs.io.annotations.* import korlibs.io.async.* import korlibs.io.lang.* @@ -22,69 +21,15 @@ val jvmCoreAudioNativeSoundProvider: JvmCoreAudioNativeSoundProvider? by lazy { } class JvmCoreAudioNativeSoundProvider : NativeSoundProvider() { - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = JvmCoreAudioPlatformAudioOutput(coroutineContext, freq) + //override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = JvmCoreAudioPlatformAudioOutput(coroutineContext, freq) + override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { return JvmCoreAudioNewPlatformAudioOutput(coroutineContext, buffer, freq, gen) } } -private val audioOutputsById = ConcurrentHashMap() - -private val cti by lazy { CallbackThreadInitializer() } -private val jnaCoreAudioCallback by lazy { - AudioQueueNewOutputCallback { inUserData, inAQ, inBuffer -> - val output = audioOutputsById[(inUserData?.address ?: 0L).toInt()] ?: return@AudioQueueNewOutputCallback 0 - - //val tone = AudioTone.generate(1.seconds, 41000.0) - val queue = AudioQueueBuffer(inBuffer) - val ptr = queue.mAudioData - val samplesCount = (queue.mAudioDataByteSize / Short.SIZE_BYTES) / 2 - - if (output.left.size != samplesCount) output.left = ShortArray(samplesCount) - if (output.right.size != samplesCount) output.right = ShortArray(samplesCount) - - val left: ShortArray = output.left - val right: ShortArray = output.right - - //val availableRead = this@JvmCoreAudioPlatformAudioOutput.availableRead - output._readShorts(0, left) - output._readShorts(1, right) - - //println("callback: availableRead=$availableRead, completed=$completed, inUserData=$inUserData, inAQ=$inAQ, inBuffer=$inBuffer, thread=${Thread.currentThread()}") - - //println(queue.mAudioDataByteSize) - if (ptr != null) { - for (n in 0 until samplesCount) { - ptr[n * DequeBasedPlatformAudioOutput.nchannels + 0] = left[n] - ptr[n * DequeBasedPlatformAudioOutput.nchannels + 1] = right[n] - } - } - //println("queue.mAudioData=${queue.mAudioData}") - - if (!output.completed) { - CoreAudioKit.AudioQueueEnqueueBuffer(inAQ, queue.ptr, 0, null).also { - if (it != 0) println("CoreAudioKit.AudioQueueEnqueueBuffer -> $it") - } - } else { - Unit - //println("COMPLETED!") - } - - //initRuntimeIfNeeded() - //val output = custom_data?.asStableRef() ?: return println("outputCallback null[0]") - //val buf = buffer?.pointed ?: return println("outputCallback null[1]") - //val dat = buf.mAudioDataByteSize.toInt() / Short.SIZE_BYTES - //val shortBuf = buf.mAudioData?.reinterpret() ?: return println("outputCallback null[2]") - //output.get().generateOutput(shortBuf, dat) - //AudioQueueEnqueueBuffer(queue, buffer, 0.convert(), null).checkError("AudioQueueEnqueueBuffer") - 0 - }.also { - Native.setCallbackThreadInitializer(it, cti) - } -} - private val newAudioOutputsById = ConcurrentHashMap() -private val ctiNew by lazy { CallbackThreadInitializer() } private val jnaNewCoreAudioCallback by lazy { AudioQueueNewOutputCallback { inUserData, inAQ, inBuffer -> try { @@ -94,7 +39,7 @@ private val jnaNewCoreAudioCallback by lazy { //val tone = AudioTone.generate(1.seconds, 41000.0) val queue = AudioQueueBuffer(inBuffer) val ptr = queue.mAudioData - val samplesCount = (queue.mAudioDataByteSize / Short.SIZE_BYTES) + val samplesCount = (queue.mAudioDataByteSize / Short.SIZE_BYTES) / nchannels //println("samplesCount=$samplesCount") if (ptr != null) { @@ -127,7 +72,7 @@ private val jnaNewCoreAudioCallback by lazy { } 0 }.also { - Native.setCallbackThreadInitializer(it, ctiNew) + Native.setCallbackThreadInitializer(it, CallbackThreadInitializer(false, false)) } } @@ -146,9 +91,6 @@ private class JvmCoreAudioNewPlatformAudioOutput( var onCancel: Cancellable? = null - init { - } - internal var completed = false var queue: Pointer? = null @@ -175,10 +117,7 @@ private class JvmCoreAudioNewPlatformAudioOutput( val userDefinedPtr = Pointer(id.toLong()) CoreAudioKit.AudioQueueNewOutput( - format.ptr, jnaNewCoreAudioCallback, userDefinedPtr, - //CoreFoundation.CFRunLoopGetCurrent(), - CoreFoundation.CFRunLoopGetMain(), // @TODO: Use other Loop to avoid issues - CoreFoundation.kCFRunLoopCommonModes, 0, queueRef + format.ptr, jnaNewCoreAudioCallback, userDefinedPtr, null, null, 0, queueRef ).also { if (it != 0) println("CoreAudioKit.AudioQueueNewOutput -> $it") } @@ -214,83 +153,6 @@ private class JvmCoreAudioNewPlatformAudioOutput( } } -private class JvmCoreAudioPlatformAudioOutput( - coroutineContext: CoroutineContext, - frequency: Int -) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { - val id = lastId.incrementAndGet() - companion object { - private var lastId = AtomicInteger(0) - const val bufferSizeInBytes = 2048 - const val numBuffers = 3 - } - - init { - audioOutputsById[id] = this - } - - internal var completed = false - - var queue: Pointer? = null - - var left: ShortArray = ShortArray(0) - var right: ShortArray = ShortArray(0) - - internal fun _readShorts(channel: Int, out: ShortArray, offset: Int = 0, count: Int = out.size - offset) { - readShorts(channel, out, offset, count) - } - - override fun start() { - completed = false - val queueRef = Memory(16).also { it.clear() } - val format = AudioStreamBasicDescription(Memory(40).also { it.clear() }) - - format.mSampleRate = frequency.toDouble() - format.mFormatID = CoreAudioKit.kAudioFormatLinearPCM - format.mFormatFlags = CoreAudioKit.kLinearPCMFormatFlagIsSignedInteger or CoreAudioKit.kAudioFormatFlagIsPacked - format.mBitsPerChannel = (8 * Short.SIZE_BYTES) - format.mChannelsPerFrame = nchannels - format.mBytesPerFrame = (Short.SIZE_BYTES * format.mChannelsPerFrame) - format.mFramesPerPacket = 1 - format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket - format.mReserved = 0 - - val userDefinedPtr = Pointer(id.toLong()) - - CoreAudioKit.AudioQueueNewOutput( - format.ptr, jnaCoreAudioCallback, userDefinedPtr, - //CoreFoundation.CFRunLoopGetCurrent(), - CoreFoundation.CFRunLoopGetMain(), - CoreFoundation.kCFRunLoopCommonModes, 0, queueRef - ).also { - if (it != 0) println("CoreAudioKit.AudioQueueNewOutput -> $it") - } - queue = queueRef.getPointer(0L) - //println("result=$result, queue=$queue") - val buffersArray = Memory((8 * numBuffers).toLong()).also { it.clear() } - for (buf in 0 until numBuffers) { - val bufferPtr = Pointer(buffersArray.address + 8 * buf) - CoreAudioKit.AudioQueueAllocateBuffer(queue, bufferSizeInBytes, bufferPtr).also { - if (it != 0) println("CoreAudioKit.AudioQueueAllocateBuffer -> $it") - } - val ptr = AudioQueueBuffer(bufferPtr.getPointer(0)) - //println("AudioQueueAllocateBuffer=$res, ptr.pointer=${ptr.pointer}") - ptr.mAudioDataByteSize = bufferSizeInBytes - jnaCoreAudioCallback.callback(userDefinedPtr, queue, ptr.ptr) - } - CoreAudioKit.AudioQueueStart(queue, null).also { - if (it != 0) println("CoreAudioKit.AudioQueueStart -> $it") - } - } - - override fun stop() { - completed = true - CoreAudioKit.AudioQueueDispose(queue, false) - audioOutputsById.remove(id) - } -} - - private class AudioQueueBuffer(p: FFIPointer? = null) : FFIStructure(p) { var mAudioDataBytesCapacity by int() var mAudioData by pointer() From 585c25342bef8e073121aed5fdc123cf97b5969d Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 05:28:07 +0200 Subject: [PATCH 22/66] "-XX:+IgnoreUnrecognizedVMOptions", "-XX:+UseZGC", "-XX:+ZGenerational" --- .../kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt index f5ab7dde62..128b585842 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt @@ -166,6 +166,8 @@ open class KorgeJavaExec : JavaExec() { || System.getenv("KORGW_JVM_ENGINE") == "sdl" //|| project.findProperty("korgw.jvm.engine") == "sdl" ) + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions", "-XX:+UseZGC", "-XX:+ZGenerational") + //jvmArgs("-XX:+UseZGC") if (firstThread && isMacos) { jvmArgs("-XstartOnFirstThread") } From 35f3dd48b3c40ceda64fbd3d3a866a6159e6412b Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 06:27:48 +0200 Subject: [PATCH 23/66] Fixes forceRenderEveryFrame=false and fixes some TextBlock & UICheckBox re-setting properties --- korge-sandbox/src/commonMain/kotlin/Main.kt | 2 +- .../korlibs/render/KorgwSurfaceView.kt | 3 +- .../src/common/korlibs/korge/ui/UICheckBox.kt | 9 ++--- .../common/korlibs/korge/view/TextBlock.kt | 26 ++++++------ korge/src/common/korlibs/korge/view/Views.kt | 40 ++----------------- korge/src/common/korlibs/render/GameWindow.kt | 14 +++++-- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 14 ++++++- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 3 ++ 8 files changed, 48 insertions(+), 63 deletions(-) diff --git a/korge-sandbox/src/commonMain/kotlin/Main.kt b/korge-sandbox/src/commonMain/kotlin/Main.kt index 100b6d1029..2c63ec3bfc 100644 --- a/korge-sandbox/src/commonMain/kotlin/Main.kt +++ b/korge-sandbox/src/commonMain/kotlin/Main.kt @@ -59,7 +59,7 @@ suspend fun main() = Korge( backgroundColor = DEFAULT_KORGE_BG_COLOR, displayMode = KorgeDisplayMode.CENTER_NO_CLIP, debug = false, - //forceRenderEveryFrame = false + forceRenderEveryFrame = false ) { //sceneContainer().changeTo({MainSprites10k()}); return@start //sceneContainer().changeTo({MainGraphicsText()}); return@start diff --git a/korge/src/android/korlibs/render/KorgwSurfaceView.kt b/korge/src/android/korlibs/render/KorgwSurfaceView.kt index 847d62f698..fbef6a2728 100644 --- a/korge/src/android/korlibs/render/KorgwSurfaceView.kt +++ b/korge/src/android/korlibs/render/KorgwSurfaceView.kt @@ -11,6 +11,7 @@ import korlibs.datastructure.* import korlibs.datastructure.lock.* import korlibs.event.* import korlibs.io.async.* +import korlibs.io.concurrent.atomic.* import korlibs.math.geom.* import korlibs.memory.* import korlibs.time.* @@ -140,7 +141,7 @@ open class KorgwSurfaceView constructor( val frameStartTime = runPreFrame() try { if (!continuousRenderMode) { - gameWindow.updatedSinceFrame++ + gameWindow.updatedSinceFrame.incrementAndGet() } gameWindow.frame(frameStartTime = frameStartTime, doUpdate = continuousRenderMode, doRender = true) onDraw(Unit) diff --git a/korge/src/common/korlibs/korge/ui/UICheckBox.kt b/korge/src/common/korlibs/korge/ui/UICheckBox.kt index e8c6dc7fee..b2494c1a51 100644 --- a/korge/src/common/korlibs/korge/ui/UICheckBox.kt +++ b/korge/src/common/korlibs/korge/ui/UICheckBox.kt @@ -40,6 +40,10 @@ open class UIBaseCheckBox>( ) : UIFocusableView(size), ViewLeaf { open class Kind + init { + simpleAnimator.autoInvalidateView = true + } + val thisAsT: T get() = this.fastCastTo() val onChange: Signal = Signal() @@ -70,11 +74,6 @@ open class UIBaseCheckBox>( private var textBounds = Rectangle() - override fun renderInternal(ctx: RenderContext) { - updateState() - super.renderInternal(ctx) - } - override fun onSizeChanged() { super.onSizeChanged() updateState() diff --git a/korge/src/common/korlibs/korge/view/TextBlock.kt b/korge/src/common/korlibs/korge/view/TextBlock.kt index e7a9be531b..0399151e69 100644 --- a/korge/src/common/korlibs/korge/view/TextBlock.kt +++ b/korge/src/common/korlibs/korge/view/TextBlock.kt @@ -26,38 +26,38 @@ class TextBlock( private var dirty = true @ViewProperty - var text: RichTextData = text; set(value) { field = value; invalidateText() } + var text: RichTextData = text; set(value) { if (field != value) { field = value; invalidateText() } } @ViewProperty @ViewPropertyProvider(TextAlignmentProvider::class) - var align: TextAlignment = align; set(value) { field = value; invalidProps() } + var align: TextAlignment = align; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var includePartialLines: Boolean = false; set(value) { field = value; invalidProps() } + var includePartialLines: Boolean = false; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var includeFirstLineAlways: Boolean = true; set(value) { field = value; invalidProps() } + var includeFirstLineAlways: Boolean = true; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var fill: Paint? = colorMul; set(value) { field = value; invalidProps() } + var fill: Paint? = colorMul; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var stroke: Stroke? = null; set(value) { field = value; invalidProps() } + var stroke: Stroke? = null; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var wordWrap: Boolean = true; set(value) { field = value; invalidProps() } + var wordWrap: Boolean = true; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var ellipsis: String? = "..."; set(value) { field = value; invalidProps() } + var ellipsis: String? = "..."; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var padding: Margin = Margin.ZERO; set(value) { field = value; invalidProps() } + var padding: Margin = Margin.ZERO; set(value) { if (field != value) { field = value; invalidProps() } } @ViewProperty - var autoSize: Boolean = false; set(value) { field = value; invalidateText() } + var autoSize: Boolean = false; set(value) { if (field != value) { field = value; invalidateText() } } //@ViewProperty(min = 0.0, max = 10.0, clampMin = true) //var textRange: IntRange = ALL_TEXT_RANGE; set(value) { field = value; invalidateText() } @ViewProperty(min = 0.0, max = 10.0, clampMin = true) - var textRangeStart: Int = 0; set(value) { field = value; invalidateText() } + var textRangeStart: Int = 0; set(value) { if (field != value) { field = value; invalidateText() } } @ViewProperty(min = 0.0, max = 10.0, clampMin = true) - var textRangeEnd: Int = Int.MAX_VALUE; set(value) { field = value; invalidateText() } + var textRangeEnd: Int = Int.MAX_VALUE; set(value) { if (field != value) { field = value; invalidateText() } } var plainText: String get() = text.text set(value) { - text = RichTextData(value, style = text.defaultStyle) + if (plainText != value) text = RichTextData(value, style = text.defaultStyle) } private var image: Image? = null private var allBitmap: Boolean? = null diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index e717978f89..512db1a303 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -11,6 +11,7 @@ import korlibs.image.format.* import korlibs.inject.* import korlibs.io.* import korlibs.io.async.* +import korlibs.io.concurrent.atomic.* import korlibs.io.file.* import korlibs.io.file.std.* import korlibs.io.lang.* @@ -308,42 +309,6 @@ class Views( renderNew() } - fun frameUpdateAndRender( - fixedSizeStep: TimeSpan = TimeSpan.NIL, - forceRender: Boolean = false, - doUpdate: Boolean = true, - doRender: Boolean = true, - ) { - val currentTime = timeProvider.now() - views.stats.startFrame() - Korge.logger.trace { "ag.onRender" } - //println("Render") - //println("currentTime: $currentTime") - val delta = (currentTime - lastTime) - val adelta = if (delta > views.clampElapsedTimeTo) views.clampElapsedTimeTo else delta - //println("delta: $delta") - //println("Render($lastTime -> $currentTime): $delta") - lastTime = currentTime - if (doUpdate) { - if (fixedSizeStep != TimeSpan.NIL) { - update(fixedSizeStep) - } else { - update(adelta) - } - } - val doRender2 = doRender && (forceRender || updatedSinceFrame > 0) - if (doRender2) { - if (printRendering) { - println("Views.frameUpdateAndRender[${DateTime.nowUnixMillisLong()}]: doRender=$doRender2 -> [forceRender=$forceRender, updatedSinceFrame=$updatedSinceFrame]") - } - render() - startFrame() - } - } - - //var printRendering: Boolean = true - var printRendering: Boolean = Environment["SHOW_FRAME_UPDATE_AND_RENDER"] == "true" - private val eventResults = EventResult() fun update(elapsed: TimeSpan) { @@ -455,7 +420,7 @@ class Views( debugSaveView(action, view) } - var updatedSinceFrame: Int by gameWindow::updatedSinceFrame + val updatedSinceFrame: KorAtomicInt by gameWindow::updatedSinceFrame fun startFrame() { gameWindow.startFrame() @@ -464,6 +429,7 @@ class Views( override fun invalidatedView(view: BaseView?) { //println("invalidatedView: $view") gameWindow.invalidatedView() + //printStackTrace() } //var viewExtraBuildDebugComponent = arrayListOf<(views: Views, view: View, container: UiContainer) -> Unit>() diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 3a27588c80..541a6b11d3 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -10,6 +10,7 @@ import korlibs.graphics.log.* import korlibs.image.bitmap.* import korlibs.image.color.* import korlibs.io.async.* +import korlibs.io.concurrent.atomic.* import korlibs.korge.view.* import korlibs.logger.* import korlibs.math.geom.* @@ -136,8 +137,8 @@ open class GameWindow : internal val gamepadEmitter: GamepadInfoEmitter = GamepadInfoEmitter(this) internal val gameWindowInputState = GameWindowInputState() - var updatedSinceFrame: Int = 0 - val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceFrame > 0 + open val updatedSinceFrame = KorAtomicInt(0) + val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceFrame.value > 0 var onContinuousRenderModeUpdated: ((Boolean) -> Unit)? = null open var continuousRenderMode: Boolean by Delegates.observable(true) { prop, old, new -> onContinuousRenderModeUpdated?.invoke(new) } @@ -255,9 +256,14 @@ open class GameWindow : override fun repaint(): Unit = Unit @Deprecated("") - fun startFrame() = run { updatedSinceFrame = 0 } + fun startFrame() = run { + //updatedSinceFrame = 0 + } @Deprecated("") - fun invalidatedView() = run { updatedSinceFrame++ } + fun invalidatedView() = run { + //println("invalidatedView") + updatedSinceFrame.incrementAndGet() + } @Deprecated("") fun handleContextLost() = run { gameWindowInputState.contextLost = true } diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index fda5277703..027ceb69aa 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -4,6 +4,7 @@ import korlibs.datastructure.* import korlibs.datastructure.lock.* import korlibs.graphics.* import korlibs.graphics.gl.* +import korlibs.io.concurrent.atomic.* import korlibs.kgl.* import korlibs.korge.view.* import korlibs.math.geom.Rectangle @@ -24,6 +25,9 @@ import javax.swing.* // https://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html //open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setProperty("sun.java2d.opengl", "true") }), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { + var continuousRenderMode: Boolean = true + var updatedSinceFrame = KorAtomicInt(0) + val canvas = object : Canvas() { private fun renderGraphics(g: Graphics) { counter.add() @@ -62,7 +66,6 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP } //var createdBufferStrategy = false - private val dl: OSXDisplayLink? = if (!Platform.isMac) null else OSXDisplayLink { //private val dl: OSXDisplayLink? = if (true) null else OSXDisplayLink { //if (autoRepaint && ctx?.supportsSwapInterval() != true && createdBufferStrategy) { @@ -73,7 +76,14 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP //bufferStrategy.show() //requestFrame() - SwingUtilities.invokeLater { requestFrame() } + //if (gameWindow.continuousRenderMode || gameWindow.updatedSinceFrame > 0) { + if (continuousRenderMode || updatedSinceFrame.value > 0) { + //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") + updatedSinceFrame.value = 0 + SwingUtilities.invokeLater { + requestFrame() + } + } //vsyncLock { vsyncLock.notify() } } //println("FRAME!") diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index c0d0f76256..46bf5f7069 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -43,6 +43,9 @@ open class AwtCanvasGameWindow( override val width: Int get() = canvas.width override val height: Int get() = canvas.height + override var continuousRenderMode: Boolean by canvas::continuousRenderMode + override val updatedSinceFrame = canvas.updatedSinceFrame + override var backgroundColor: RGBA get() = canvas.background.toRgba() set(value) { canvas.background = value.toAwt() } From 98fda7ef1083b14777b163100a66ed329fdd350f Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 06:27:57 +0200 Subject: [PATCH 24/66] Stage.GC action --- korge/src/common/korlibs/korge/view/Stage.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/korge/src/common/korlibs/korge/view/Stage.kt b/korge/src/common/korlibs/korge/view/Stage.kt index d6d3ab7f40..0fe615b5d8 100644 --- a/korge/src/common/korlibs/korge/view/Stage.kt +++ b/korge/src/common/korlibs/korge/view/Stage.kt @@ -1,5 +1,6 @@ package korlibs.korge.view +import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.inject.* import korlibs.io.resources.* @@ -63,6 +64,11 @@ open class Stage internal constructor(override val views: Views) : FixedSizeCont // } //} + @ViewProperty + fun GC() { + NativeThread.gc(full = true) + } + @Suppress("unused") @ViewProperty(min = 0.0, max = 2000.0, groupName = "Stage") private var virtualSize: Point From 6148692cd8d953e0d3ed991142bbfc36e7163706 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 06:28:07 +0200 Subject: [PATCH 25/66] Add DitheringFilter to the MainFilterScale sample --- .../commonMain/kotlin/samples/MainFilterScale.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainFilterScale.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainFilterScale.kt index 9904df482d..efb6f7198a 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainFilterScale.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainFilterScale.kt @@ -10,13 +10,24 @@ import korlibs.korge.view.filter.* class MainFilterScale : Scene() { override suspend fun SContainer.sceneMain() { - val image = image(resourcesVfs["korge.png"].readBitmap()).xy(100, 100) + val bmp = resourcesVfs["korge.png"].readBitmap() + val image = image(bmp).xy(100, 100) .filterScale(1.0) .filters(Convolute3Filter(Convolute3Filter.KERNEL_EDGE_DETECTION)) + val dithering = DitheringFilter(4.0) + val image2 = image(bmp).xy(600, 100) + .filterScale(1.0) + .filters(dithering) //.filters(BlurFilter()) //.filters(WaveFilter()) - val combo = uiSlider(value = 1.0, max = 1.0, step = 0.01).xy(400, 100).changed { image.filterScale = it } + val combo = uiSlider(value = 1.0, max = 1.0, step = 0.01).xy(400, 100).changed { + image.filterScale = it + image2.filterScale = it + } + val combo2 = uiSlider(value = 4.0, min = 1.0, max = 8.0, step = 0.1).xy(400, 50).changed { + dithering.levels = it + } //val combo = uiComboBox(items = listOf(0.0, 0.01, 0.05, 0.075, 0.125, 0.25, 0.44, 0.5, 0.75, 0.95, 0.99, 1.0)).xy(400, 100).onSelectionUpdate { image.filterScale = it.selectedItem ?: 1.0 } // This reproduces a bug (black right and bottom border) at least on macOS with M1 From 1d0d3f03c573ea5799899f2053b3be7e9c00e65f Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 22 Oct 2023 06:39:56 +0200 Subject: [PATCH 26/66] Fix some tests --- korge/src/jvm/korlibs/korge/testing/testing_utils.kt | 5 +++++ korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/korge/src/jvm/korlibs/korge/testing/testing_utils.kt b/korge/src/jvm/korlibs/korge/testing/testing_utils.kt index 0748e4dbaa..0b0bf3b8d8 100644 --- a/korge/src/jvm/korlibs/korge/testing/testing_utils.kt +++ b/korge/src/jvm/korlibs/korge/testing/testing_utils.kt @@ -54,9 +54,14 @@ inline fun korgeScreenshotTestV2( return@suspendTest } + var n = 0 + while (testingLock.isLocked) { println("Waiting for test to end...") delay(100.milliseconds) + if (n++ >= 50) { + error("Test didn't finish in time") + } } val resultsWithErrors = results.results.filter { result -> diff --git a/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt b/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt index 5767402485..fcd90569f9 100644 --- a/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt +++ b/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt @@ -55,12 +55,12 @@ class ViewUpdatedTest { } fun assertUpdatedOnce(name: String = "block()", expectedCount: Int = 1, block: () -> Unit) { - views.startFrame() + views.updatedSinceFrame.value = 0 block() - assertEquals(expectedCount, views.updatedSinceFrame, message = "Should update view::${name}") - views.startFrame() + assertEquals(expectedCount, views.updatedSinceFrame.value, message = "Should update view::${name}") + views.updatedSinceFrame.value = 0 block() - assertEquals(0, views.updatedSinceFrame, message = "Shouldn't re-update view::${name}") + assertEquals(0, views.updatedSinceFrame.value, message = "Shouldn't re-update view::${name}") } } } From 2382d03cf019635c40921d52a9744d1b4f4c650e Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 09:54:39 +0200 Subject: [PATCH 27/66] Added KorAtomicFloat & KorAtomicBase.update --- .../korlibs/io/concurrent/atomic/KorAtomic.kt | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/korge-core/src/common/korlibs/io/concurrent/atomic/KorAtomic.kt b/korge-core/src/common/korlibs/io/concurrent/atomic/KorAtomic.kt index 5a20eb017a..c1ce261381 100644 --- a/korge-core/src/common/korlibs/io/concurrent/atomic/KorAtomic.kt +++ b/korge-core/src/common/korlibs/io/concurrent/atomic/KorAtomic.kt @@ -1,6 +1,6 @@ package korlibs.io.concurrent.atomic -import kotlin.reflect.KProperty +import kotlin.reflect.* expect fun korAtomic(initial: T): KorAtomicRef expect fun korAtomic(initial: Boolean): KorAtomicBoolean @@ -22,6 +22,14 @@ interface KorAtomicBase { fun compareAndSet(expect: T, update: T): Boolean } +inline fun KorAtomicBase.update(transform: (T) -> T): T { + while (true) { + val value = this.value + val next = transform(value) + if (compareAndSet(value, next)) return next + } +} + interface KorAtomicNumber : KorAtomicBase { fun addAndGet(delta: T): T } @@ -68,11 +76,31 @@ open class KorAtomicInt internal constructor(initial: Int, dummy: Boolean) : Kor } } - override fun addAndGet(delta: Int): Int { - this.value += delta - return this.value - } + override fun addAndGet(delta: Int): Int = update { it + delta } + fun addAndGetMod(delta: Int, modulo: Int): Int = update { (it + delta) % modulo } + + override fun toString(): String = "$value" +} +class KorAtomicFloat(initial: Float) : KorAtomicNumber { + private val atomic = KorAtomicInt(initial.toRawBits()) + override var value: Float + get() = Float.fromBits(atomic.value) + set(value) { + atomic.value = value.toRawBits() + } + + override fun compareAndSet(expect: Float, update: Float): Boolean { + return if (value == expect) { + value = update + true + } else { + false + } + } + + override fun addAndGet(delta: Float): Float = update { it + delta } + fun addAndGetMod(delta: Float, modulo: Float): Float = update { (it + delta) % modulo } override fun toString(): String = "$value" } From 6f64d8464f4ff194a889c7e5ce86d5e64ce571f0 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 09:54:56 +0200 Subject: [PATCH 28/66] Added generic arraycopyStride --- korge-foundation/src/common/korlibs/memory/Arrays.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/korge-foundation/src/common/korlibs/memory/Arrays.kt b/korge-foundation/src/common/korlibs/memory/Arrays.kt index f97e858f85..51d5d4b6d8 100644 --- a/korge-foundation/src/common/korlibs/memory/Arrays.kt +++ b/korge-foundation/src/common/korlibs/memory/Arrays.kt @@ -236,6 +236,10 @@ public fun FloatArray.lastIndexOf(sub: FloatArray, starting: Int = 0): Int = arr public fun DoubleArray.lastIndexOf(sub: DoubleArray, starting: Int = 0): Int = array_lastIndexOf(starting, size, sub.size) { n, m -> this[n] == sub[m] } public fun Array.lastIndexOf(sub: Array, starting: Int = 0): Int = array_lastIndexOf(starting, size, sub.size) { n, m -> this[n] == sub[m] } +public inline fun arraycopyStride(src: (Int) -> T, srcPos: Int, srcStride: Int, dst: (Int, T) -> Unit, dstPos: Int, dstStride: Int, size: Int) { + for (n in 0 until size) dst(dstPos + dstStride * n, src(srcPos + srcStride * n)) +} + public fun arraycopyStride(src: ByteArray, srcPos: Int, srcStride: Int, dst: ByteArray, dstPos: Int, dstStride: Int, size: Int) { for (n in 0 until size) dst[dstPos + dstStride * n] = src[srcPos + srcStride * n] } From d1ee1580e611dd116c1da21c4cd854d9496654d8 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 09:55:29 +0200 Subject: [PATCH 29/66] Make createNewPlatformAudioOutput accept the number of channels instead of an AudioSamples instance so the implementation decices the size of the AudioSamples instance --- .../audio/sound/PlatformAudioOutput.kt | 5 ++--- .../src/common/korlibs/audio/sound/Sound.kt | 12 +++++------ .../audio/sound/CoreAudioSoundProvider.kt | 20 +++++++++---------- .../audio/sound/backend/CoreAudioImpl.kt | 8 ++++---- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index 33eb577d8c..d94886d213 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -10,7 +10,7 @@ import kotlin.coroutines.* open class NewPlatformAudioOutput( val coroutineContext: CoroutineContext, - val buffer: AudioSamples, + val nchannels: Int, val frequency: Int, val gen: (AudioSamples) -> Unit, ) : Disposable, SoundProps { @@ -25,7 +25,6 @@ open class NewPlatformAudioOutput( } } - val nchannels get() = buffer.channels override var pitch: Double = 1.0 override var volume: Double = 1.0 override var panning: Double = 0.0 @@ -39,7 +38,7 @@ open class PlatformAudioOutputBasedOnNew( coroutineContext: CoroutineContext, frequency: Int, ) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { - val new = soundProvider.createNewPlatformAudioOutput(coroutineContext, AudioSamples(2, 4410), frequency) { buffer -> + val new = soundProvider.createNewPlatformAudioOutput(coroutineContext, 2, frequency) { buffer -> readShorts(buffer.data) } diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 02542312ff..0ea1576308 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -21,8 +21,8 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS override val target: String get() = parent.target override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = parent.createPlatformAudioOutput(coroutineContext, freq) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = - parent.createNewPlatformAudioOutput(coroutineContext, buffer, freq, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = + parent.createNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = parent.createSound(data, streaming, props, name) @@ -46,14 +46,14 @@ open class NativeSoundProvider : Disposable { open var paused: Boolean = false open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput = PlatformAudioOutput(coroutineContext, freq) - open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { - println("createNewPlatformAudioOutput: ${this::class}") - return NewPlatformAudioOutput(coroutineContext, buffer, freq, gen) + open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + //println("createNewPlatformAudioOutput: ${this::class}") + return NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) } suspend fun createPlatformAudioOutput(freq: Int = 44100): PlatformAudioOutput = createPlatformAudioOutput(coroutineContextKt, freq) - suspend fun createNewPlatformAudioOutput(buffer: AudioSamples, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = createNewPlatformAudioOutput(coroutineContextKt, buffer, freq, gen) + suspend fun createNewPlatformAudioOutput(nchannels: Int, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = createNewPlatformAudioOutput(coroutineContextKt, nchannels, freq, gen) open suspend fun createSound(data: ByteArray, streaming: Boolean = false, props: AudioDecodingProps = AudioDecodingProps.DEFAULT, name: String = "Unknown"): Sound { val format = props.formats ?: audioFormats diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index 5d55ca7f71..9ee6d901ce 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -12,7 +12,6 @@ import platform.AudioToolbox.* import platform.AudioToolbox.AudioQueueBufferRef import platform.AudioToolbox.AudioQueueRef import platform.CoreAudioTypes.* -import platform.CoreFoundation.* import platform.darwin.OSStatus import kotlin.Int import kotlin.String @@ -34,32 +33,31 @@ class CoreAudioNativeSoundProvider : NativeSoundProvider() { //override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps): NativeSound = AVFoundationNativeSoundNoStream(CoroutineScope(coroutineContext), audioFormats.decode(data)) override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = CoreAudioPlatformAudioOutput(coroutineContext, freq) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, freq, buffer, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, freq, nchannels, gen) } class CoreAudioNewPlatformAudioOutput( coroutineContext: CoroutineContext, freq: Int, - buffer: AudioSamples, + nchannels: Int, gen: (AudioSamples) -> Unit, -) : NewPlatformAudioOutput(coroutineContext, freq, buffer, gen) { - val generator = CoreAudioGenerator(freq, buffer.channels, coroutineContext = coroutineContext) { data, dataSize -> - val samples = AudioSamples(buffer.channels, dataSize) +) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { + val generator = CoreAudioGenerator(freq, nchannels, coroutineContext = coroutineContext) { data, dataSize -> + val nchannels = this.nchannels + val samples = AudioSamples(nchannels, dataSize) gen(samples) - println("GEN") for (m in 0 until nchannels) { + val input = samples[m] for (n in 0 until dataSize / nchannels) { - data[n * nchannels + m] = samples[m, n] + data[n * nchannels + m] = input[n] } } } override fun start() { - println("GEN.start") generator.start() } override fun stop() { - println("GEN.stop") generator.dispose() } } @@ -174,7 +172,7 @@ class CoreAudioGenerator( AudioQueueNewOutput( format.ptr, staticCFunction(::coreAudioOutputCallback), thisStableRef!!.asCPointer(), - CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0.convert(), queue!!.ptr + null, null, 0.convert(), queue!!.ptr ).also { if (it != 0) error("Error in AudioQueueNewOutput") } diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index ac0c6177e6..f3b1567fda 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -24,8 +24,8 @@ class JvmCoreAudioNativeSoundProvider : NativeSoundProvider() { //override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = JvmCoreAudioPlatformAudioOutput(coroutineContext, freq) override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, buffer: AudioSamples, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { - return JvmCoreAudioNewPlatformAudioOutput(coroutineContext, buffer, freq, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + return JvmCoreAudioNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) } } @@ -78,10 +78,10 @@ private val jnaNewCoreAudioCallback by lazy { private class JvmCoreAudioNewPlatformAudioOutput( coroutineContext: CoroutineContext, - buffer: AudioSamples, + nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit, -) : NewPlatformAudioOutput(coroutineContext, buffer, freq, gen) { +) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { val id = lastId.incrementAndGet() companion object { private var lastId = AtomicLong(0L) From 8413167495967f937adbef3cde478f767d3d4bd0 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 10:01:32 +0200 Subject: [PATCH 30/66] Improve atomics for JnaNewCoreAudio --- .../jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index f3b1567fda..2b3aae3131 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -5,6 +5,7 @@ import korlibs.audio.sound.* import korlibs.ffi.* import korlibs.io.annotations.* import korlibs.io.async.* +import korlibs.io.concurrent.atomic.* import korlibs.io.lang.* import korlibs.memory.* import java.util.concurrent.* @@ -43,9 +44,12 @@ private val jnaNewCoreAudioCallback by lazy { //println("samplesCount=$samplesCount") if (ptr != null) { - val samples = AudioSamples(nchannels, samplesCount) + // Reuse instances as much as possible + if (output.samples.totalSamples != samplesCount) output.samples = AudioSamples(nchannels, samplesCount) + if (output.buffer.totalSamples != samplesCount) output.buffer = AudioSamplesInterleaved(nchannels, samplesCount) + val samples = output.samples output.safeGen(samples) - val buffer = ShortArray(samplesCount * nchannels) + val buffer = output.buffer.data for (m in 0 until nchannels) { arraycopyStride( @@ -93,6 +97,10 @@ private class JvmCoreAudioNewPlatformAudioOutput( internal var completed = false + internal var samples by KorAtomicRef(AudioSamples(nchannels, 0)) + internal var buffer by KorAtomicRef(AudioSamplesInterleaved(nchannels, 0)) + + var queue: Pointer? = null override fun start() { From e135e2718de53a0e07462258321f691abf6e0ac8 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 10:01:50 +0200 Subject: [PATCH 31/66] Make MainPolyphonic work with atomic fields to avoid concurrency issues --- .../kotlin/samples/MainPolyphonic.kt | 103 +++++++----------- 1 file changed, 40 insertions(+), 63 deletions(-) diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt index fb281d59ee..0f243c859b 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt @@ -1,6 +1,7 @@ package samples import korlibs.audio.sound.* +import korlibs.io.concurrent.atomic.* import korlibs.korge.scene.* import korlibs.korge.ui.* import korlibs.korge.view.* @@ -19,23 +20,11 @@ class MainPolyphonic : Scene() { text("by Yann Tiersen") } - val maxAt = SAMPLE_COUNT / 16 - for (i in 0 until SAMPLE_COUNT) { - sample[i] = when { - i < maxAt -> (i.toFloat() / maxAt.toFloat() * 2f - 1f) - else -> (1f - (i - maxAt).toFloat() / (SAMPLE_COUNT - maxAt).toFloat() * 2f) - } - } - var base = 40.0f - for (i in 0 until OCTAVE_COUNT) { - createPitches(base, octaves[i]) - base *= 2f - } - channelStates[0].noteIndex = 0; nextNote(0) - channelStates[1].noteIndex = 0; nextNote(1) + channelStates[0].noteIndex.value = 0; nextNote(0) + channelStates[1].noteIndex.value = 0; nextNote(1) for (nchannel in 0 until 2) { - val stream2 = nativeSoundProvider.createNewPlatformAudioOutput(AudioSamples(1, 4410), 44100) { samples -> + val stream2 = nativeSoundProvider.createNewPlatformAudioOutput(1, 44100) { samples -> audioOutCallback(nchannel, samples.data[0], samples.data[0].size) for (n in 1 until samples.channels) { arraycopy(samples.data[0], 0, samples.data[n], 0, samples.data[0].size) @@ -43,52 +32,41 @@ class MainPolyphonic : Scene() { samples.scaleVolume(.05f) } stream2.start() - /* - //for (nchannel in 0 until 1) { - launchImmediately { - //AudioTone.generate(0.25.seconds, 440.0).playAndWait() - val stream = nativeSoundProvider.createPlatformAudioOutput(44100) - stream.start() - while (true) { - //val samples = AudioSamples(1, 44100 * 6) - val samples = AudioSamples(1, 4410) - //val samples = AudioSamples(2, 44100) - //val samples = AudioSamples(1, 44100) - audioOutCallback(nchannel, samples.data[0], samples.data[0].size) - for (n in 1 until samples.channels) { - arraycopy(samples.data[0], 0, samples.data[n], 0, samples.data[0].size) - } - samples.scaleVolume(.05f) - //MemorySyncStream().apply { writeShortArrayLE(samples.data[0]) }.toByteArray().writeToFile("/tmp/data.raw") - //for (n in 0 until 44100) println(samples.data[0][n]) - stream.add(samples) - } - } - - */ } } companion object { - const val SAMPLE_COUNT = 0x20000 - val sample = FloatArray(SAMPLE_COUNT) + const val SAMPLE_COUNT = 0x1000 + val SAMPLE = FloatArray(SAMPLE_COUNT).also { SAMPLE -> + val maxAt = SAMPLE_COUNT / 16 + for (i in 0 until SAMPLE_COUNT) { + SAMPLE[i] = when { + i < maxAt -> (i.toFloat() / maxAt.toFloat() * 2f - 1f) + else -> (1f - (i - maxAt).toFloat() / (SAMPLE_COUNT - maxAt).toFloat() * 2f) + } + } + } const val SAMPLE_RATE = 44100 - const val OCTAVE_COUNT = 6 - - val octaves = Array(6) { FloatArray(12) } + val OCTAVES = Array(6) { FloatArray(12) }.also { OCTAVES -> + var base = 40.0f + for (element in OCTAVES) { + createPitches(base, element) + base *= 2f + } + } data class Note_t(val note: Int, val octave: Int, val duration: Int) data class ChannelState_t( - var currentNote: Note_t = Note_t(0, 0, 0), - var noteIndex: Int = 0, - var currentTime: Int = 0, - var currentsampleIndex: Float = 0f, - var currentsampleIncrement: Float = 0f + val currentNote: KorAtomicRef = KorAtomicRef(Note_t(0, 0, 0)), + val noteIndex: KorAtomicInt = KorAtomicInt(0), + val currentTime: KorAtomicInt = KorAtomicInt(0), + val currentsampleIndex: KorAtomicFloat = KorAtomicFloat(0f), + val currentsampleIncrement: KorAtomicFloat = KorAtomicFloat(0f) ) - val channelStates = Array(3) { ChannelState_t() } + val channelStates = Array(2) { ChannelState_t() } // "S" means "#" const val NOTE_END = -2 @@ -367,18 +345,18 @@ class MainPolyphonic : Scene() { fun nextNote(channel: Int) { val state = channelStates[channel] - state.currentNote = channels[channel][state.noteIndex] - state.currentTime = 0 - state.currentsampleIndex = 0f - val note = state.currentNote.note + state.currentNote.value = channels[channel][state.noteIndex.value] + state.currentTime.value = 0 + state.currentsampleIndex.value = 0f + val note = state.currentNote.value.note if (note == NOTE_PAUSE) { - state.currentsampleIncrement = 0f + state.currentsampleIncrement.value = 0f } else { - state.currentsampleIncrement = octaves[state.currentNote.octave][note] * (SAMPLE_COUNT.toFloat()) / (SAMPLE_RATE.toFloat()) + state.currentsampleIncrement.value = OCTAVES[state.currentNote.value.octave][note] * (SAMPLE_COUNT.toFloat()) / (SAMPLE_RATE.toFloat()) } - state.noteIndex++ - if (channels[channel][state.noteIndex].note == NOTE_END) state.noteIndex = 0 + state.noteIndex.incrementAndGet() + if (channels[channel][state.noteIndex.value].note == NOTE_END) state.noteIndex.value = 0 } // calculate current value of attack/delay/sustain/release envelope @@ -405,18 +383,17 @@ class MainPolyphonic : Scene() { val state = channelStates[channel] var bufn = bufn for (i in 0 until reqn) { - val time = (state.currentTime.toFloat()) / (SAMPLE_RATE.toFloat()) - if (state.currentTime++ == state.currentNote.duration) { + val time = (state.currentTime.value.toFloat()) / (SAMPLE_RATE.toFloat()) + if (state.currentTime.getAndIncrement() == state.currentNote.value.duration) { nextNote(channel) } var value: Float - if (state.currentsampleIncrement == 0.0f) { + if (state.currentsampleIncrement.value == 0.0f) { value = 0.0f } else { - value = sample[state.currentsampleIndex.toInt()] * adsr(time, (state.currentNote.duration.toFloat()) / (SAMPLE_RATE.toFloat())) + value = SAMPLE[state.currentsampleIndex.value.toInt()] * adsr(time, (state.currentNote.value.duration.toFloat()) / (SAMPLE_RATE.toFloat())) value *= 0x7000f - state.currentsampleIndex += state.currentsampleIncrement - if (state.currentsampleIndex >= SAMPLE_COUNT) state.currentsampleIndex -= SAMPLE_COUNT.toFloat() + state.currentsampleIndex.addAndGetMod(state.currentsampleIncrement.value, SAMPLE_COUNT.toFloat()) } val rvalue = value.clamp(Short.MIN_VALUE.toFloat(), Short.MAX_VALUE.toInt().toFloat()).toInt().toShort() //for (n in 0 until nchannels) buf[bufn++] = value.toShort() From fd9bdb1e178ad6bc2f351f7fa737eb4ef68ab27e Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 12:06:58 +0200 Subject: [PATCH 32/66] Some android GameWindow adjustments --- .../render/DefaultGameWindowAndroid.kt | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt index 961147d3ff..d615863715 100644 --- a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt +++ b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt @@ -8,8 +8,10 @@ import android.view.inputmethod.* import korlibs.event.* import korlibs.graphics.* import korlibs.image.bitmap.* +import korlibs.io.android.* +import korlibs.io.lang.TimedCache +import korlibs.time.* import kotlinx.coroutines.* -import kotlin.coroutines.* actual fun CreateDefaultGameWindow(config: GameWindowCreationConfig): GameWindow = TODO() @@ -19,27 +21,26 @@ interface AndroidContextHolder { } abstract class BaseAndroidGameWindow( + final override val androidContext: Context, val config: GameWindowCreationConfig = GameWindowCreationConfig(), ) : GameWindow(), AndroidContextHolder { - abstract override val androidContext: Context + final override val dialogInterface = DialogInterfaceAndroid { androidContext } + override val androidContextAny: Any? = androidContext abstract val androidView: View + override val pixelsPerInch: Double by TimedCache(1.seconds) { androidContext.resources.displayMetrics.xdpi.toDouble() } + val _activity: Activity? get() = androidContext.activity - override val androidContextAny: Any? get() = androidContext - - // @TODO: Cache somehow? - override val pixelsPerInch: Double get() = androidContext.resources.displayMetrics.xdpi.toDouble() + init { + coroutineContext += AndroidCoroutineContext(androidContext) + } - val context get() = androidContext - var coroutineContext: CoroutineContext? = null val Context?.activity: Activity? get() = when (this) { is Activity -> this is ContextWrapper -> this.baseContext.activity else -> null } - val _activity: Activity? get() = context.activity val inputMethodManager get() = androidContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - override val dialogInterface = DialogInterfaceAndroid { androidContext } override var isSoftKeyboardVisible: Boolean = false override var keepScreenOn: Boolean @@ -115,8 +116,7 @@ suspend fun runAndroidOnUiThreadSuspend(context: Context? = null, block: () return deferred.await() } -class AndroidGameWindow(val activity: KorgwActivity, config: GameWindowCreationConfig = activity.config) : BaseAndroidGameWindow(config) { - override val androidContext get() = activity +class AndroidGameWindow(val activity: KorgwActivity, config: GameWindowCreationConfig = activity.config) : BaseAndroidGameWindow(activity, config) { override val androidView: View get() = activity.mGLView ?: error("Can't find mGLView") val mainHandler by lazy { android.os.Handler(androidContext.mainLooper) } @@ -135,12 +135,6 @@ class AndroidGameWindow(val activity: KorgwActivity, config: GameWindowCreationC field = value activity.makeFullscreen(value) } - override var visible: Boolean - get() = super.visible - set(value) {} - override var quality: Quality - get() = super.quality - set(value) {} fun initializeAndroid() { fullscreen = true @@ -148,25 +142,16 @@ class AndroidGameWindow(val activity: KorgwActivity, config: GameWindowCreationC override fun setSize(width: Int, height: Int) { } - - - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - this.coroutineContext = kotlin.coroutines.coroutineContext - //println("CONTEXT: ${kotlin.coroutines.coroutineContext[AndroidCoroutineContext.Key]?.context}") - entry(this) - } } class AndroidGameWindowNoActivity( override val width: Int, override val height: Int, override val ag: AG, - override val androidContext: Context, + androidContext: Context, config: GameWindowCreationConfig = GameWindowCreationConfig(), val getView: () -> View -) : BaseAndroidGameWindow(config) { - override val dialogInterface = DialogInterfaceAndroid { androidContext } - +) : BaseAndroidGameWindow(androidContext, config) { override val androidView: View get() = getView() override var title: String = "Korge" @@ -181,13 +166,4 @@ class AndroidGameWindowNoActivity( override var visible: Boolean get() = super.visible set(value) {} - - override var quality: Quality - get() = super.quality - set(value) {} - - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - this.coroutineContext = kotlin.coroutines.coroutineContext - entry(this) - } } From 94b3b7480da51908e48467c0d4205fdb68938b0e Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 12:07:32 +0200 Subject: [PATCH 33/66] Auto-configure a JVM logger if no handlers are configured --- .../src/jvm/korlibs/logger/Logger.jvm.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt b/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt index 017f822e6a..d200ed7940 100644 --- a/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt +++ b/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt @@ -1,6 +1,6 @@ package korlibs.logger -import java.util.logging.Level +import java.util.logging.* actual object Console : BaseConsole() { override fun logInternal(kind: Kind, vararg msg: Any?) { @@ -24,10 +24,24 @@ internal actual val miniEnvironmentVariables: Map by lazy { Syst actual object DefaultLogOutput : Logger.Output { override fun output(logger: Logger, level: Logger.Level, msg: Any?) { if (logger.nativeLogger == null) { - logger.nativeLogger = java.util.logging.Logger.getLogger(logger.name) + logger.nativeLogger = java.util.logging.Logger.getLogger(logger.name).also { nativeLogger -> + nativeLogger.useParentHandlers = true + if (nativeLogger.handlers.isEmpty()) { + nativeLogger.addHandler(object : Handler() { + override fun publish(record: LogRecord) { + println("${record.instant}: ${record.loggerName} - ${record.message}") + } + override fun flush() = Unit + override fun close() = Unit + }) + } + } } + val nativeLogger = logger.nativeLogger as java.util.logging.Logger + nativeLogger.level = logger.level.toJava() + //println("logger.level=${logger.level}, nativeLogger.level=${nativeLogger.level}, level=$level") //println("logger=$logger, level=$level, msg=$msg") - (logger.nativeLogger as java.util.logging.Logger).log(level.toJava(), msg.toString()) + nativeLogger.log(level.toJava(), msg.toString()) } } From 9ec4db7f09d87876a87318d51b0267fb668cf14a Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 12:07:50 +0200 Subject: [PATCH 34/66] Check in sandbox that injector is available in the coroutine context --- korge-sandbox/src/commonMain/kotlin/Main.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/korge-sandbox/src/commonMain/kotlin/Main.kt b/korge-sandbox/src/commonMain/kotlin/Main.kt index 2c63ec3bfc..f9b68fdb1d 100644 --- a/korge-sandbox/src/commonMain/kotlin/Main.kt +++ b/korge-sandbox/src/commonMain/kotlin/Main.kt @@ -2,6 +2,7 @@ import korlibs.event.* import korlibs.image.color.* import korlibs.image.text.* +import korlibs.inject.* import korlibs.io.async.* import korlibs.korge.* import korlibs.korge.input.* @@ -65,6 +66,9 @@ suspend fun main() = Korge( //sceneContainer().changeTo({MainGraphicsText()}); return@start //sceneContainer().changeTo({MainUI()}); return@start + val injector = injector() + println("Injector: $injector") // Ensure injector is available as a manual test + var lastBackTime = DateTime.EPOCH keys { this.down(Key.BACK) { From c1e2c7918280c3e5d55b1ab2fb6c66ac66d61d6b Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 12:08:18 +0200 Subject: [PATCH 35/66] Make GameWindow Closeable --- korge/src/common/korlibs/render/GameWindow.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 541a6b11d3..fbe45b2fda 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -57,6 +57,7 @@ expect fun CreateDefaultGameWindow(config: GameWindowCreationConfig = GameWindow */ open class GameWindow : BaseEventListener(), + Closeable, DialogInterfaceProvider, MenuInterface, DeviceDimensionsProvider, @@ -234,7 +235,11 @@ open class GameWindow : @Deprecated("Use close instead") fun exit(exitCode: Int = 0): Unit = close(exitCode) - open fun close(exitCode: Int = 0) { + override fun close() { + close(0) + } + + open fun close(exitCode: Int) { if (closing) return closing = true //println("[1]") From be87a46ccee7ee236d356a5ad926990930cba3d8 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 12:08:29 +0200 Subject: [PATCH 36/66] Small cleanup --- korge/src/common/korlibs/render/GameWindowExt.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index 6fe681141c..67f4eb05a3 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -11,7 +11,6 @@ import korlibs.korge.view.* import korlibs.math.geom.* import korlibs.memory.* import korlibs.render.GameWindowQuality.* -import kotlin.native.concurrent.* class GameWindowEventInstances { val pauseEvent = PauseEvent() @@ -207,9 +206,6 @@ fun GameWindow.handleReshapeEventIfRequired(x: Int, y: Int, width: Int, height: gameWindowInputState.surfaceHeight = height } -@ThreadLocal -var GLOBAL_CHECK_GL = false - data class GameWindowCreationConfig( val multithreaded: Boolean? = null, val hdr: Boolean? = null, From 686aff8133f9f205669145f1bc53a45f41c89821 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 12:08:53 +0200 Subject: [PATCH 37/66] Try to fix headless tests --- korge/src/common/korlibs/korge/Korge.kt | 1 - korge/src/common/korlibs/korge/KorgeHeadless.kt | 14 +++++++++++++- .../korlibs/korge/KorgeViewsConfigureInput.kt | 7 ++++--- .../korlibs/korge/testing/KorgeOffscreenTest.kt | 17 +++++++++-------- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index d5c3d4f509..2d9dfd9358 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -183,7 +183,6 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen } } views.setVirtualSize(config.virtualSize) - gameWindow.coroutineContext += InjectorContext(config.injector) Korge.logger.logTime("configureGameWindow") { gameWindow.configure(windowSize, config.title, null, config.fullscreen, config.backgroundColor ?: Colors.BLACK) } diff --git a/korge/src/common/korlibs/korge/KorgeHeadless.kt b/korge/src/common/korlibs/korge/KorgeHeadless.kt index e6db6971f4..dc24a50745 100644 --- a/korge/src/common/korlibs/korge/KorgeHeadless.kt +++ b/korge/src/common/korlibs/korge/KorgeHeadless.kt @@ -1,13 +1,17 @@ package korlibs.korge +import korlibs.datastructure.thread.* import korlibs.graphics.* +import korlibs.graphics.gl.* import korlibs.graphics.log.* import korlibs.image.format.* import korlibs.korge.view.* import korlibs.math.geom.* import korlibs.render.* +import korlibs.time.* object KorgeHeadless { + @OptIn(ExperimentalStdlibApi::class) class HeadlessGameWindow( val size: Size = Size(640, 480), val draw: Boolean = false, @@ -22,10 +26,19 @@ object KorgeHeadless { this.exitProcessOnClose = exitProcessOnClose } + init { + nativeThread { eventLoop.runTasksForever() } + eventLoop.setInterval(60.hz.timeSpan) { + (ag as? AGOpengl?)?.context?.set() + this@HeadlessGameWindow.dispatchNewRenderEvent() + } + } + //override val ag: AG = if (draw) AGSoftware(width, height) else DummyAG(width, height) //override val ag: AG = AGDummy(width, height) } + @OptIn(ExperimentalStdlibApi::class) suspend operator fun invoke( config: KorgeConfig, ag: AG = AGDummy(config.windowSize), @@ -37,7 +50,6 @@ object KorgeHeadless { val gameWindow = HeadlessGameWindow(config.windowSize, draw = draw, ag = ag, devicePixelRatio = devicePixelRatio) gameWindow.exitProcessOnClose = false config.copy(gameWindow = gameWindow).start { - //config.main?.invoke(this) entry() } return gameWindow diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index aeafcce368..73008e0bd9 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -14,16 +14,17 @@ import korlibs.korge.view.* import korlibs.math.geom.* import korlibs.render.* import korlibs.time.* -import kotlin.coroutines.* @KorgeInternal internal fun Views.prepareViewsBase( - eventDispatcher: EventListener, + eventDispatcher: GameWindow, clearEachFrame: Boolean = true, bgcolor: RGBA = Colors.TRANSPARENT, forceRenderEveryFrame: Boolean = true, configInjector: Injector.() -> Unit = {}, ) { + eventDispatcher.coroutineContext += InjectorContext(injector) + val views = this val injector = views.injector @@ -33,7 +34,7 @@ internal fun Views.prepareViewsBase( injector.mapSingleton(ResourcesRoot::class) { ResourcesRoot() } injector.mapInstance(views.input) injector.mapInstance(views.stats) - injector.mapInstance(CoroutineContext::class, views.coroutineContext) + //injector.mapInstance(CoroutineContext::class, views.coroutineContext) // Maybe we shouldn't include this injector.mapPrototype(EmptyScene::class) { EmptyScene() } injector.mapInstance(TimeProvider::class, views.timeProvider) injector.mapInstance(GameWindow::class, gameWindow) diff --git a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt index cec1969617..e1cd182aab 100644 --- a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt +++ b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt @@ -44,7 +44,7 @@ internal fun createKmlGlContext(fboWidth: Int, fboHeight: Int): KmlGlContext { return ctx } -fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: Boolean = false, callback: suspend CoroutineScope.(ag: AG) -> Unit) = suspendTest { +fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: Boolean = false, setContext: Boolean = true, callback: suspend CoroutineScope.(ag: AG) -> Unit) = suspendTest { val fboWidth = fboSize.width.toInt() val fboHeight = fboSize.height.toInt() @@ -54,7 +54,7 @@ fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: B try { ag.contextsToFree += ctx ag.mainFrameBuffer.setSize(fboWidth, fboHeight) - ctx.set() + if (setContext) ctx.set() callback(ag) } finally { @@ -93,12 +93,10 @@ inline fun korgeScreenshotTest( } var exception: Throwable? = null - suspendTestWithOffscreenAG(windowSize, checkGl = checkGl, logGl = logGl) { ag -> - KorgeHeadless(KorgeConfig( - windowSize = windowSize, virtualSize = virtualSize, - backgroundColor = bgcolor, - stageBuilder = { OffscreenStage(it) } - ), + suspendTestWithOffscreenAG(windowSize, checkGl = checkGl, logGl = logGl, setContext = false) { ag -> + val deferred = CompletableDeferred() + val gameWindow = KorgeHeadless( + config = KorgeConfig(windowSize = windowSize, virtualSize = virtualSize, backgroundColor = bgcolor, stageBuilder = { OffscreenStage(it) }), ag = ag, devicePixelRatio = devicePixelRatio, ) { injector.mapInstance(offscreenContext) @@ -108,8 +106,11 @@ inline fun korgeScreenshotTest( exception = e } finally { gameWindow.close() + deferred.complete(Unit) } } + deferred.await() + gameWindow.close() } exception?.let { throw it } } From 715934f4be0fe5e67290e299d81868ae85223e60 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 15:36:29 +0200 Subject: [PATCH 38/66] More work --- .../audio/sound/PlatformAudioOutput.kt | 7 + .../src/common/korlibs/audio/sound/Sound.kt | 11 +- .../audio/sound/backend/JvmWaveOutImpl.kt | 151 ++++++----- korge-core/src/common/korlibs/io/file/Vfs.kt | 2 +- .../audio/sound/CoreAudioSoundProvider.kt | 3 +- .../audio/sound/NativeSoundProviderJvm.kt | 10 +- .../audio/sound/backend/CoreAudioImpl.kt | 11 +- .../sound/impl/awt/AwtNativeSoundProvider.kt | 254 +++--------------- .../datastructure/_Datastructure_thread.kt | 5 +- .../datastructure/thread/NativeThread.kt | 3 +- .../src/jvm/korlibs/logger/Logger.jvm.kt | 11 +- .../datastructure/thread/NativeThread.kt | 9 +- korge-sandbox/src/commonMain/kotlin/Main.kt | 4 +- .../commonMain/kotlin/samples/MainSound.kt | 1 - .../korlibs/korge/android/KorgeAndroidView.kt | 2 +- .../common/korlibs/korge/ui/UIVerticalList.kt | 2 +- korge/src/common/korlibs/render/GameWindow.kt | 11 +- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 139 +++++++--- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 21 +- .../render/awt/AwtGameWindowDebugger.kt | 1 - .../jvm/korlibs/render/win32/Win32Tools.kt | 3 +- 21 files changed, 276 insertions(+), 385 deletions(-) diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index d94886d213..8e58eafc55 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -2,6 +2,7 @@ package korlibs.audio.sound import korlibs.datastructure.lock.* import korlibs.datastructure.thread.* +import korlibs.io.async.* import korlibs.io.lang.* import korlibs.math.* import korlibs.time.* @@ -14,6 +15,8 @@ open class NewPlatformAudioOutput( val frequency: Int, val gen: (AudioSamples) -> Unit, ) : Disposable, SoundProps { + val onCancel = coroutineContext.onCancel { stop() } + private val lock = Lock() fun safeGen(buffer: AudioSamples) { lock { @@ -38,6 +41,10 @@ open class PlatformAudioOutputBasedOnNew( coroutineContext: CoroutineContext, frequency: Int, ) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { + init{ + println("PlatformAudioOutputBasedOnNew[$frequency] = $soundProvider") + } + val new = soundProvider.createNewPlatformAudioOutput(coroutineContext, 2, frequency) { buffer -> readShorts(buffer.data) } diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 0ea1576308..c323b4c0be 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -40,12 +40,19 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS override fun dispose() = parent.dispose() } -open class NativeSoundProvider : Disposable { +open class NativeSoundProviderNew : NativeSoundProvider() { + final override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = + PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) +} + +open class NativeSoundProvider() : Disposable { open val target: String = "unknown" open var paused: Boolean = false - open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput = PlatformAudioOutput(coroutineContext, freq) + open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput { + return PlatformAudioOutput(coroutineContext, freq) + } open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { //println("createNewPlatformAudioOutput: ${this::class}") return NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) diff --git a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt index 2265c345ed..39db31087f 100644 --- a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt +++ b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt @@ -3,20 +3,21 @@ package korlibs.audio.sound.backend import korlibs.audio.sound.* import korlibs.datastructure.thread.* import korlibs.ffi.* -import korlibs.io.async.* import korlibs.io.lang.* import korlibs.memory.* -import korlibs.time.* -import kotlinx.coroutines.* import kotlin.coroutines.* val jvmWaveOutNativeSoundProvider: NativeSoundProvider? by lazy { JvmWaveOutNativeSoundProvider() } -class JvmWaveOutNativeSoundProvider : NativeSoundProvider() { - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = - JvmWaveOutPlatformAudioOutput(this, coroutineContext, freq) +class JvmWaveOutNativeSoundProvider : NativeSoundProviderNew() { + override fun createNewPlatformAudioOutput( + coroutineContext: CoroutineContext, + nchannels: Int, + freq: Int, + gen: (AudioSamples) -> Unit + ): NewPlatformAudioOutput = JvmWaveOutNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) } /* @@ -68,34 +69,35 @@ class JvmWaveOutPlatformAudioOutputNew( } */ -class JvmWaveOutPlatformAudioOutput( - val provider: JvmWaveOutNativeSoundProvider, +class JvmWaveOutNewPlatformAudioOutput( coroutineContext: CoroutineContext, - frequency: Int -) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { + nchannels: Int, + freq: Int, + gen: (AudioSamples) -> Unit +) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { val samplesLock = korlibs.datastructure.lock.NonRecursiveLock() var nativeThread: NativeThread? = null var running = false var totalEmittedSamples = 0L - override suspend fun wait() { - // @TODO: Get samples not reproduced - //println("WAITING...") - for (n in 0 until 1000) { - var currentPositionInSamples: Long = 0L - var totalEmittedSamples: Long = 0L - var availableRead = 0 - samplesLock { - currentPositionInSamples = WINMM.waveOutGetPositionInSamples(handle) - availableRead = this.availableRead - totalEmittedSamples = this.totalEmittedSamples - } - //println("availableRead=$availableRead, waveOutGetPosition=$currentPositionInSamples, totalEmittedSamples=$totalEmittedSamples") - if (availableRead <= 0 && currentPositionInSamples >= totalEmittedSamples) break - delay(1.milliseconds) - } - //println("DONE WAITING") - } + //override suspend fun wait() { + // // @TODO: Get samples not reproduced + // //println("WAITING...") + // for (n in 0 until 1000) { + // var currentPositionInSamples: Long = 0L + // var totalEmittedSamples: Long = 0L + // var availableRead = 0 + // samplesLock { + // currentPositionInSamples = WINMM.waveOutGetPositionInSamples(handle) + // availableRead = this.availableRead + // totalEmittedSamples = this.totalEmittedSamples + // } + // //println("availableRead=$availableRead, waveOutGetPosition=$currentPositionInSamples, totalEmittedSamples=$totalEmittedSamples") + // if (availableRead <= 0 && currentPositionInSamples >= totalEmittedSamples) break + // delay(1.milliseconds) + // } + // //println("DONE WAITING") + //} private var handle: FFIPointer? = null private var headers = emptyArray() @@ -126,28 +128,26 @@ class JvmWaveOutPlatformAudioOutput( handle = handlePtr[0] //println("handle=$handle") - headers = Array(3) { WaveHeader(it, handle, 1024, nchannels, arena) } + headers = Array(4) { WaveHeader(it, handle, 1024, nchannels, arena) } try { - while (running || availableRead > 0) { + while (running) { + var queued = 0 for (header in headers) { if (!header.hdr.isInQueue) { + safeGen(header.samples) + header.prepareAndWrite() + queued++ //println("Sending running=$running, availableRead=$availableRead, header=${header}") - if (availableRead > 0) { - samplesLock { - totalEmittedSamples += header.totalSamples - readShortsFully(header.data) - } - header.prepareAndWrite() - } } - Thread_sleep(1L) } + if (queued == 0) Thread_sleep(1L) } } finally { - runBlockingNoJs { - wait() - } + for (header in headers) header.dispose() + //runBlockingNoJs { + // wait() + //} WINMM.waveOutReset(handle) WINMM.waveOutClose(handle) handle = null @@ -164,47 +164,52 @@ class JvmWaveOutPlatformAudioOutput( running = false //println("STOPPING") } +} - private class WaveHeader( - val id: Int, - val handle: FFIPointer?, - val totalSamples: Int, - val nchannels: Int, - val arena: FFIArena, - ) { - val data = Array(nchannels) { ShortArray(totalSamples) } - val totalBytes = (totalSamples * nchannels * Short.SIZE_BYTES) - val dataMem = arena.allocBytes(totalBytes).typed() - val hdr = WAVEHDR(arena.allocBytes(WAVEHDR().size)).also { hdr -> - hdr.lpData = dataMem.reinterpret() - hdr.dwBufferLength = totalBytes - hdr.dwFlags = 0 - } - fun prepareAndWrite(totalSamples: Int = this.totalSamples) { - //println(data[0].toList()) +private class WaveHeader( + val id: Int, + val handle: FFIPointer?, + val totalSamples: Int, + val nchannels: Int, + val arena: FFIArena, +) { + val samples = AudioSamples(nchannels, totalSamples) + val data = samples.data + + val totalBytes = (totalSamples * nchannels * Short.SIZE_BYTES) + val dataMem = arena.allocBytes(totalBytes).typed() + val hdr = WAVEHDR(arena.allocBytes(WAVEHDR().size)).also { hdr -> + hdr.lpData = dataMem.reinterpret() + hdr.dwBufferLength = totalBytes + hdr.dwFlags = 0 + } - hdr.dwBufferLength = (totalSamples * nchannels * Short.SIZE_BYTES) + fun prepareAndWrite(totalSamples: Int = this.totalSamples) { + //println(data[0].toList()) - for (ch in 0 until nchannels) { - for (n in 0 until totalSamples) { - dataMem[n * nchannels + ch] = data[ch][n] - } - } - //if (hdr.isPrepared) dispose() - if (!hdr.isPrepared) { - //println("-> prepare") - WINMM.waveOutPrepareHeader(handle, hdr.ptr, hdr.size) + val nchannels = this.nchannels + hdr.dwBufferLength = (totalSamples * nchannels * Short.SIZE_BYTES) + + for (ch in 0 until nchannels) { + val inputCh = data[ch] + for (n in 0 until totalSamples) { + dataMem[n * nchannels + ch] = inputCh[n] } - WINMM.waveOutWrite(handle, hdr.ptr, hdr.size) } - - fun dispose() { - WINMM.waveOutUnprepareHeader(handle, hdr.ptr, hdr.size) + //if (hdr.isPrepared) dispose() + if (!hdr.isPrepared) { + //println("-> prepare") + WINMM.waveOutPrepareHeader(handle, hdr.ptr, hdr.size) } + WINMM.waveOutWrite(handle, hdr.ptr, hdr.size) + } - override fun toString(): String = "WaveHeader(id=$id, totalSamples=$totalSamples, nchannels=$nchannels, hdr=$hdr)" + fun dispose() { + WINMM.waveOutUnprepareHeader(handle, hdr.ptr, hdr.size) } + + override fun toString(): String = "WaveHeader(id=$id, totalSamples=$totalSamples, nchannels=$nchannels, hdr=$hdr)" } internal typealias LPHWAVEOUT = FFIPointer diff --git a/korge-core/src/common/korlibs/io/file/Vfs.kt b/korge-core/src/common/korlibs/io/file/Vfs.kt index fb8a30096b..6d7b8f1cc6 100644 --- a/korge-core/src/common/korlibs/io/file/Vfs.kt +++ b/korge-core/src/common/korlibs/io/file/Vfs.kt @@ -215,7 +215,7 @@ abstract class Vfs : AsyncCloseable { FinalVfsFile(this, path) abstract class Proxy : Vfs() { - private val logger = Logger("Proxy") + private val logger = Logger("Vfs.Proxy") protected abstract suspend fun access(path: String): VfsFile protected open suspend fun VfsFile.transform(): VfsFile = file(this.path) //suspend protected fun transform2_f(f: VfsFile): VfsFile = transform(f) diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index 9ee6d901ce..c9b4d0f6e8 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -25,14 +25,13 @@ expect fun appleInitAudio() @ThreadLocal val CORE_AUDIO_NATIVE_SOUND_PROVIDER: CoreAudioNativeSoundProvider by lazy { CoreAudioNativeSoundProvider() } -class CoreAudioNativeSoundProvider : NativeSoundProvider() { +class CoreAudioNativeSoundProvider : NativeSoundProviderNew() { init { appleInitAudio() } //override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps): NativeSound = AVFoundationNativeSoundNoStream(CoroutineScope(coroutineContext), audioFormats.decode(data)) - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = CoreAudioPlatformAudioOutput(coroutineContext, freq) override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, freq, nchannels, gen) } diff --git a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt index 38b77a42ad..1785953b3c 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt @@ -15,10 +15,12 @@ private val nativeSoundProviderDeferred: NativeSoundProvider by lazy { traceTime("SoundProvider") { when { //Platform.isLinux -> FFIALSANativeSoundProvider - Platform.isLinux -> AwtNativeSoundProvider - Platform.isApple -> jvmCoreAudioNativeSoundProvider - Platform.isWindows -> jvmWaveOutNativeSoundProvider - else -> JnaOpenALNativeSoundProvider() + //Platform.isLinux -> AwtNativeSoundProvider + //Platform.isApple -> jvmCoreAudioNativeSoundProvider + //Platform.isWindows -> jvmWaveOutNativeSoundProvider + //Platform.isWindows -> AwtNativeSoundProvider + //else -> JnaOpenALNativeSoundProvider() + else -> AwtNativeSoundProvider } ?: AwtNativeSoundProvider } } catch (e: UnsatisfiedLinkError) { diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index 2b3aae3131..b626c05da5 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -21,10 +21,7 @@ val jvmCoreAudioNativeSoundProvider: JvmCoreAudioNativeSoundProvider? by lazy { } } -class JvmCoreAudioNativeSoundProvider : NativeSoundProvider() { - //override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = JvmCoreAudioPlatformAudioOutput(coroutineContext, freq) - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) - +class JvmCoreAudioNativeSoundProvider : NativeSoundProviderNew() { override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { return JvmCoreAudioNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) } @@ -93,8 +90,6 @@ private class JvmCoreAudioNewPlatformAudioOutput( const val numBuffers = 3 } - var onCancel: Cancellable? = null - internal var completed = false internal var samples by KorAtomicRef(AudioSamples(nchannels, 0)) @@ -106,7 +101,6 @@ private class JvmCoreAudioNewPlatformAudioOutput( override fun start() { stop() - onCancel = coroutineContext.onCancel { stop() } newAudioOutputsById[id] = this completed = false val queueRef = Memory(16).also { it.clear() } @@ -150,9 +144,6 @@ private class JvmCoreAudioNewPlatformAudioOutput( override fun stop() { completed = true - onCancel?.cancel() - onCancel = null - if (queue != null) { CoreAudioKit.AudioQueueDispose(queue, false) queue = null diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt index 76bca059e9..f28b83a4ad 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt @@ -10,142 +10,25 @@ import javax.sound.sampled.* import kotlin.coroutines.* import kotlin.time.* -// AudioSystem.getMixerInfo() -private val mixer by lazy { AudioSystem.getMixer(null) } - -object AwtNativeSoundProvider : NativeSoundProvider() { - val format = AudioFormat(44100f, 16, 2, true, false) - - val linePool = ConcurrentPool { (mixer.getLine(DataLine.Info(SourceDataLine::class.java, format)) as SourceDataLine).also { it.open() } } - - init { - // warming and preparing - mixer.mixerInfo - val info = DataLine.Info(SourceDataLine::class.java, format) - val line = AudioSystem.getLine(info) as SourceDataLine - line.open(format, 4096) - line.start() - line.write(ByteArray(4), 0, 4) - line.drain() - line.stop() - line.close() +object AwtNativeSoundProvider : NativeSoundProviderNew() { + override fun createNewPlatformAudioOutput( + coroutineContext: CoroutineContext, + nchannels: Int, + freq: Int, + gen: (AudioSamples) -> Unit + ): NewPlatformAudioOutput { + return JvmNewPlatformAudioOutput(this, coroutineContext, nchannels, freq, gen) } - - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = - JvmPlatformAudioOutput(this, coroutineContext, freq) } -data class SampleBuffer(val timestamp: Long, val data: AudioSamples) - -/* -private class JvmCoreAudioPlatformAudioOutput( - coroutineContext: CoroutineContext, - frequency: Int -) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { - val id = lastId.incrementAndGet() - companion object { - private var lastId = AtomicInteger(0) - const val bufferSizeInBytes = 2048 - const val numBuffers = 3 - } - - init { - audioOutputsById[id] = this - } - - internal var completed = false - - var queue: Pointer? = null - - var left: ShortArray = ShortArray(0) - var right: ShortArray = ShortArray(0) - - internal fun _readShorts(channel: Int, out: ShortArray, offset: Int = 0, count: Int = out.size - offset) { - readShorts(channel, out, offset, count) - } - - override fun start() { - completed = false - val queueRef = Memory(16).also { it.clear() } - val format = AudioStreamBasicDescription(Memory(40).also { it.clear() }) - - format.mSampleRate = frequency.toDouble() - format.mFormatID = CoreAudioKit.kAudioFormatLinearPCM - format.mFormatFlags = CoreAudioKit.kLinearPCMFormatFlagIsSignedInteger or CoreAudioKit.kAudioFormatFlagIsPacked - format.mBitsPerChannel = (8 * Short.SIZE_BYTES) - format.mChannelsPerFrame = nchannels - format.mBytesPerFrame = (Short.SIZE_BYTES * format.mChannelsPerFrame) - format.mFramesPerPacket = 1 - format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket - format.mReserved = 0 - - val userDefinedPtr = Pointer(id.toLong()) - - CoreAudioKit.AudioQueueNewOutput( - format.ptr, jnaCoreAudioCallback, userDefinedPtr, - //CoreFoundation.CFRunLoopGetCurrent(), - CoreFoundation.CFRunLoopGetMain(), - CoreFoundation.kCFRunLoopCommonModes, 0, queueRef - ).also { - if (it != 0) println("CoreAudioKit.AudioQueueNewOutput -> $it") - } - queue = queueRef.getPointer(0L) - //println("result=$result, queue=$queue") - val buffersArray = Memory((8 * numBuffers).toLong()).also { it.clear() } - for (buf in 0 until numBuffers) { - val bufferPtr = Pointer(buffersArray.address + 8 * buf) - CoreAudioKit.AudioQueueAllocateBuffer(queue, bufferSizeInBytes, bufferPtr).also { - if (it != 0) println("CoreAudioKit.AudioQueueAllocateBuffer -> $it") - } - val ptr = AudioQueueBuffer(bufferPtr.getPointer(0)) - //println("AudioQueueAllocateBuffer=$res, ptr.pointer=${ptr.pointer}") - ptr.mAudioDataByteSize = bufferSizeInBytes - jnaCoreAudioCallback.callback(userDefinedPtr, queue, ptr.ptr) - } - CoreAudioKit.AudioQueueStart(queue, null).also { - if (it != 0) println("CoreAudioKit.AudioQueueStart -> $it") - } - - } - - override fun stop() { - completed = true - CoreAudioKit.AudioQueueDispose(queue, false) - audioOutputsById.remove(id) - } -} -*/ - - -class JvmPlatformAudioOutput( +class JvmNewPlatformAudioOutput( val provider: AwtNativeSoundProvider, coroutineContext: CoroutineContext, - frequency: Int -) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { - val samplesLock = korlibs.datastructure.lock.NonRecursiveLock() + nchannels: Int, + freq: Int, + gen: (AudioSamples) -> Unit +) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { var nativeThread: NativeThread? = null - var running = false - var totalEmittedSamples = 0L - - override suspend fun wait() { - if (line == null) return - for (n in 0 until 1000) { - var currentPositionInSamples: Long = 0L - var totalEmittedSamples: Long = 0L - var availableRead = 0 - samplesLock { - currentPositionInSamples = line?.longFramePosition ?: 0L - availableRead = this.availableRead - totalEmittedSamples = this.totalEmittedSamples - } - //println("availableRead=$availableRead, waveOutGetPosition=$currentPositionInSamples, totalEmittedSamples=$totalEmittedSamples") - if (availableRead <= 0 && currentPositionInSamples >= totalEmittedSamples) break - delay(1.milliseconds) - } - } - - val format = provider.format - var line: SourceDataLine? = null val BYTES_PER_SAMPLE = nchannels * Short.SIZE_BYTES @@ -154,109 +37,38 @@ class JvmPlatformAudioOutput( override fun start() { //println("TRYING TO START") - if (running) return + if (nativeThread?.threadSuggestRunning == true) return + //println("STARTED") - running = true nativeThread = nativeThread(isDaemon = true) { + it.threadSuggestRunning = true + val nchannels = this.nchannels + //val format = AudioFormat(frequency.toFloat(), Short.SIZE_BITS * nchannels, nchannels, true, false) + val format = AudioFormat(44100.toFloat(), Short.SIZE_BITS * nchannels, nchannels, true, false) + val line = AudioSystem.getSourceDataLine(format) + line.open() + line.start() try { - var timesWithoutBuffers = 0 - while (running || availableRead > 0) { - while (availableRead > 0) { - timesWithoutBuffers = 0 - while (availableRead > 0) { - if (line == null) { - val prepareLineTime = measureTimedValue { - line = provider.linePool.alloc() - //println("OPEN LINE: $line") - line!!.stop() - line!!.flush() - line!!.start() - } - //println("prepareLineTime=$prepareLineTime") - } - val availableBytes = line!!.available() - //val availableSamples = minOf(availableRead, bytesToSamples(availableBytes)) - val availableSamples = minOf(441, minOf(availableRead, bytesToSamples(availableBytes))) - - val info = AudioSamplesInterleaved(nchannels, availableSamples) - val readCount = readShortsInterleaved(info) - val bytes = ByteArray(samplesToBytes(readCount)) - bytes.setArrayLE(0, info.data) - //println(bytes.hex) - val (written, time) = measureTimedValue { line!!.write(bytes, 0, bytes.size) } - if (written != bytes.size) { - println("NOT FULLY WRITTEN $written != ${bytes.size}") - } - //println("written=$written, write time=$time") - samplesLock { - this.totalEmittedSamples += readCount - } - } - //println(bytes.hex) - Thread.sleep(1L) - } - //println("SHUT($id)!") - //Thread.sleep(500L) // 0.5 seconds of grace before shutting down this thread! - Thread.sleep(50L) // 0.5 seconds of grace before shutting down this thread! - timesWithoutBuffers++ - if (timesWithoutBuffers >= 10) break + val info = AudioSamples(nchannels, 1024) + val bytes = ByteArray(samplesToBytes(1024)) + while (it.threadSuggestRunning) { + safeGen(info) + bytes.setArrayLE(0, info.interleaved().data) + line.write(bytes, 0, bytes.size) } } catch (e: Throwable) { e.printStackTrace() } finally { - //println("CLOSED_LINE: $line running=$running!") - if (line != null) { - //line?.drain() - //line?.stop() - //line?.close() - provider.linePool.free(line!!) - line = null - } + line.drain() + line.stop() + line.close() } } } override fun stop() { - running = false + nativeThread?.threadSuggestRunning = false + nativeThread = null //println("STOPPING") } - - /* - val line by lazy { mixer.getLine(DataLine.Info(SourceDataLine::class.java, format)) as SourceDataLine } - line.open() - line.start() - //println("OPENED_LINE($id)!") - try { - var timesWithoutBuffers = 0 - while (running) { - while (availableBuffers > 0) { - timesWithoutBuffers = 0 - val buf = synchronized(buffers) { buffers.dequeue() } - synchronized(buffers) { totalShorts -= buf.data.totalSamples * buf.data.channels } - val bdata = convertFromShortToByte(buf.data.interleaved().data) - - val msChunk = (((bdata.size / 2) * 1000.0) / frequency.toDouble()).toInt() - - _msElapsed += msChunk - val now = System.currentTimeMillis() - val latency = now - buf.timestamp - //val drop = latency >= 150 - val start = System.currentTimeMillis() - line.write(bdata, 0, bdata.size) - //line.drain() - val end = System.currentTimeMillis() - //println("LINE($id): ${end - start} :: msChunk=$msChunk :: start=$start, end=$end :: available=${line.available()} :: framePosition=${line.framePosition} :: availableBuffers=$availableBuffers") - } - //println("SHUT($id)!") - //Thread.sleep(500L) // 0.5 seconds of grace before shutting down this thread! - Thread.sleep(50L) // 0.5 seconds of grace before shutting down this thread! - timesWithoutBuffers++ - if (timesWithoutBuffers >= 10) break - } - } finally { - //println("CLOSED_LINE($id)!") - line.stop() - line.close() - } - */ } diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt index 7d739b8226..c73e647344 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt @@ -2,10 +2,11 @@ package korlibs.datastructure.thread +import korlibs.datastructure.* import korlibs.time.* import kotlin.time.* -expect class NativeThread(code: () -> Unit) { +expect class NativeThread(code: (NativeThread) -> Unit) : Extra { companion object { val isSupported: Boolean val currentThreadId: Long @@ -27,7 +28,7 @@ public fun nativeThread( isDaemon: Boolean = false, name: String? = null, priority: Int = -1, - block: () -> Unit + block: (NativeThread) -> Unit ): NativeThread { val thread = NativeThread(block) if (isDaemon) thread.isDaemon = true diff --git a/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt b/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt index 9fa9b49528..e03d479ef1 100644 --- a/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt +++ b/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt @@ -1,9 +1,10 @@ package korlibs.datastructure.thread +import korlibs.datastructure.* import korlibs.time.* import kotlin.time.* -actual class NativeThread actual constructor(val code: () -> Unit) { +actual class NativeThread actual constructor(val code: (NativeThread) -> Unit) : Extra by Extra.Mixin() { actual var isDaemon: Boolean = false actual fun start() { diff --git a/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt b/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt index d200ed7940..d2b9ef3e75 100644 --- a/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt +++ b/korge-foundation/src/jvm/korlibs/logger/Logger.jvm.kt @@ -1,5 +1,7 @@ package korlibs.logger +import korlibs.logger.Console.color +import korlibs.time.* import java.util.logging.* actual object Console : BaseConsole() { @@ -29,7 +31,14 @@ actual object DefaultLogOutput : Logger.Output { if (nativeLogger.handlers.isEmpty()) { nativeLogger.addHandler(object : Handler() { override fun publish(record: LogRecord) { - println("${record.instant}: ${record.loggerName} - ${record.message}") + val out = if (record.level.intValue() >= Level.WARNING.intValue()) System.err else System.out + val color = when { + record.level.intValue() >= Level.SEVERE.intValue() -> AnsiEscape.Color.RED + record.level.intValue() >= Level.WARNING.intValue() -> AnsiEscape.Color.YELLOW + else -> AnsiEscape.Color.WHITE + } + val time = DateTime.fromUnixMillis(record.millis).format(DateFormat.FORMAT2) + out.println("$time[${Thread.currentThread()}]: ${record.level}: ${record.loggerName} - ${record.message}".color(color)) } override fun flush() = Unit override fun close() = Unit diff --git a/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt b/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt index 4a488d2361..c3270e7aba 100644 --- a/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt +++ b/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt @@ -1,5 +1,6 @@ package korlibs.datastructure.thread +import korlibs.datastructure.* import korlibs.time.* private fun TimeSpan.toMillisNanos(): Pair { @@ -9,8 +10,10 @@ private fun TimeSpan.toMillisNanos(): Pair { return millis to nanos } -actual class NativeThread actual constructor(val code: () -> Unit) { - val thread = Thread(code) +actual class NativeThread actual constructor(val code: (NativeThread) -> Unit) : Extra by Extra.Mixin() { + val thread = Thread { code(this) } + + var threadSuggestRunning = true actual var priority: Int by thread::priority actual var name: String? by thread::name @@ -20,10 +23,12 @@ actual class NativeThread actual constructor(val code: () -> Unit) { set(value) { thread.isDaemon = value } actual fun start() { + threadSuggestRunning = true thread.start() } actual fun interrupt() { + threadSuggestRunning = false // No operation thread.interrupt() } diff --git a/korge-sandbox/src/commonMain/kotlin/Main.kt b/korge-sandbox/src/commonMain/kotlin/Main.kt index f9b68fdb1d..c82aeb7b66 100644 --- a/korge-sandbox/src/commonMain/kotlin/Main.kt +++ b/korge-sandbox/src/commonMain/kotlin/Main.kt @@ -127,7 +127,9 @@ suspend fun main() = Korge( //Demo(::MainMasks), //Demo(::MainShape2dScene), //Demo(::MainUIStacks), - Demo(::MainPolyphonic), + //Demo(::MainPolyphonic), + //Demo(::MainSound), + Demo(::MainTiledBackground), //Demo(::MainSprites10k), //Demo(::MainStressMatrixMultiplication), //Demo(::MainSDF), diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt index 710f283c76..2acbc5bec4 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt @@ -8,7 +8,6 @@ import korlibs.time.* class MainSound : Scene() { override suspend fun SContainer.sceneMain() { - val music = resourcesVfs["sounds/Snowland.mp3"].readMusic() //val music = resourcesVfs["sounds/click.wav"].readSound() //val music = resourcesVfs["sounds/click.wav"].readMusic() diff --git a/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt b/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt index f9dac9d071..a9268fda4e 100644 --- a/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt +++ b/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt @@ -32,7 +32,7 @@ open class KorgeAndroidView @JvmOverloads constructor( if (!moduleLoaded) return gameWindow?.dispatchDestroyEvent() - gameWindow?.coroutineContext = null + //gameWindow?.coroutineContext = null gameWindow?.close() gameWindow?.exit() mGLView = null diff --git a/korge/src/common/korlibs/korge/ui/UIVerticalList.kt b/korge/src/common/korlibs/korge/ui/UIVerticalList.kt index 1e6729001a..b2d2c7670e 100644 --- a/korge/src/common/korlibs/korge/ui/UIVerticalList.kt +++ b/korge/src/common/korlibs/korge/ui/UIVerticalList.kt @@ -135,7 +135,7 @@ open class UIVerticalList(provider: Provider, width: Double = 200.0) : UIView(DE val removeIndices = viewsByIndex.keys.filter { it !in fromIndex .. toIndex }.toSet() - viewsByIndex.forEach { (index, view) -> + viewsByIndex.toList().forEach { (index, view) -> if (index in removeIndices) { view.removeFromParent() } diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index fbe45b2fda..95406ad618 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -101,13 +101,13 @@ open class GameWindow : var coroutineContext: CoroutineContext = EmptyCoroutineContext fun getCoroutineDispatcherWithCurrentContext(coroutineContext: CoroutineContext): CoroutineContext = coroutineContext + coroutineDispatcher - suspend fun getCoroutineDispatcherWithCurrentContext(): CoroutineContext = getCoroutineDispatcherWithCurrentContext(coroutineContext) + fun getCoroutineDispatcherWithCurrentContext(): CoroutineContext = getCoroutineDispatcherWithCurrentContext(coroutineContext) // Event Loop @PublishedApi internal val _updateRenderLock = Lock() inline fun updateRenderLock(block: () -> Unit) = _updateRenderLock(block) fun queueSuspend(callback: suspend () -> Unit) { - launchAsap(coroutineDispatcher + coroutineContext) { + getCoroutineDispatcherWithCurrentContext().launchUnscoped { callback() } } @@ -242,17 +242,12 @@ open class GameWindow : open fun close(exitCode: Int) { if (closing) return closing = true - //println("[1]") queue { dispatchDestroyEvent() } - //println("[2]") running = false this.exitCode = exitCode - //println("[3]") logger.info { "GameWindow.close" } + coroutineDispatcher.cancel() coroutineDispatcher.close() - //println("[4]") - coroutineDispatcher.cancelChildren() - //println("[5]") } @Deprecated("") diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index 027ceb69aa..db1cb2bda3 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -2,11 +2,13 @@ package korlibs.render.awt import korlibs.datastructure.* import korlibs.datastructure.lock.* +import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.graphics.gl.* import korlibs.io.concurrent.atomic.* import korlibs.kgl.* import korlibs.korge.view.* +import korlibs.logger.* import korlibs.math.geom.Rectangle import korlibs.platform.* import korlibs.render.* @@ -16,6 +18,7 @@ import korlibs.render.platform.* import korlibs.time.* import java.awt.* import java.awt.Graphics +import java.lang.IllegalStateException import javax.swing.* // @TODO: Use Metal, OpenGL or whatever required depending on what's AWT is using @@ -25,11 +28,88 @@ import javax.swing.* // https://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html //open class AwtAGOpenglCanvas : Canvas(), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setProperty("sun.java2d.opengl", "true") }), BoundsProvider by BoundsProvider.Base(), Extra by Extra.Mixin() { + companion object { + val LOGGER = Logger("AwtAGOpenglCanvas") + } + var continuousRenderMode: Boolean = true var updatedSinceFrame = KorAtomicInt(0) - val canvas = object : Canvas() { - private fun renderGraphics(g: Graphics) { + interface RendererThread { + val running: Boolean + fun start() + fun stop() + } + + class DisplayLinkRenderThread(val canvas: AwtAGOpenglCanvas) : RendererThread { + val dl = OSXDisplayLink { + //private val dl: OSXDisplayLink? = if (true) null else OSXDisplayLink { + //if (autoRepaint && ctx?.supportsSwapInterval() != true && createdBufferStrategy) { + if (canvas.autoRepaint && canvas.ctx?.supportsSwapInterval() != true) { + //val bufferStrat = bufferStrategy + //val g = bufferStrat.drawGraphics + //renderGraphics(g) + //bufferStrategy.show() + + //requestFrame() + //if (gameWindow.continuousRenderMode || gameWindow.updatedSinceFrame > 0) { + if (canvas.continuousRenderMode || canvas.updatedSinceFrame.value > 0) { + //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") + canvas.updatedSinceFrame.value = 0 + SwingUtilities.invokeLater { + canvas.requestFrame() + } + } + //vsyncLock { vsyncLock.notify() } + } + //println("FRAME!") + } + override val running: Boolean by dl::running + override fun start() = dl.start() + override fun stop() = dl.stop() + } + + class VsyncRenderThread(val canvas: AwtAGOpenglCanvas) : RendererThread { + var createdBufferStrategy = false + + var thread: NativeThread? = null + override val running: Boolean get() = thread?.threadSuggestRunning == true + + override fun start() { + stop() + thread = nativeThread(start = true, name = "VsyncRenderThread") { thread -> + val fcanvas = canvas.canvas + try { + while (thread.threadSuggestRunning) { + if (!createdBufferStrategy) { + try { + fcanvas.createBufferStrategy(2) + //println("buf=$buf") + createdBufferStrategy = true + AwtAGOpenglCanvas.LOGGER.info { "createdBufferStrategy = true" } + } catch (e: IllegalStateException) { + Thread.sleep(1L) + continue + } + } + fcanvas.renderGraphics(fcanvas.bufferStrategy.drawGraphics) + } + } catch (e: Throwable) { + e.printStackTrace() + } + } + } + + override fun stop() { + thread?.threadSuggestRunning = false + thread = null + } + } + + inner class GLCanvas : Canvas() { + internal fun renderGraphics(g: Graphics) { + if (!visible || !SwingUtilities.getWindowAncestor(this).visible) return + //println("renderGraphics") counter.add() //super.paint(g) if (ctx == null) { @@ -45,53 +125,32 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP println("!!! ERROR: Using Metal pipeline ${this::class} won't work") } //println("CTX: $ctx") + //if (ctx != null) { + // if (autoRepaint && ctx.supportsSwapInterval()) { + // //if (true) { + // SwingUtilities.invokeLater { + // //vsyncLock { vsyncLock.wait(0.5.seconds) } + // requestFrame() + // } + // } + //} ctx?.useContext(g, ag, paintInContextDelegate) - if (ctx != null) { - if (autoRepaint && ctx.supportsSwapInterval()) { - //if (true) { - SwingUtilities.invokeLater { - //vsyncLock { vsyncLock.wait(0.5.seconds) } - requestFrame() - } - } - } + //ctx?.swapBuffers() } override fun paint(g: Graphics) { - //if (!createdBufferStrategy) { - // createdBufferStrategy = true - // val buf = createBufferStrategy(2) - //} - renderGraphics(g) + + //renderGraphics(g) } - //var createdBufferStrategy = false - private val dl: OSXDisplayLink? = if (!Platform.isMac) null else OSXDisplayLink { - //private val dl: OSXDisplayLink? = if (true) null else OSXDisplayLink { - //if (autoRepaint && ctx?.supportsSwapInterval() != true && createdBufferStrategy) { - if (autoRepaint && ctx?.supportsSwapInterval() != true) { - //val bufferStrat = bufferStrategy - //val g = bufferStrat.drawGraphics - //renderGraphics(g) - //bufferStrategy.show() - //requestFrame() - //if (gameWindow.continuousRenderMode || gameWindow.updatedSinceFrame > 0) { - if (continuousRenderMode || updatedSinceFrame.value > 0) { - //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") - updatedSinceFrame.value = 0 - SwingUtilities.invokeLater { - requestFrame() - } - } - //vsyncLock { vsyncLock.notify() } - } - //println("FRAME!") + private val dl: RendererThread = when { + Platform.isMac -> DisplayLinkRenderThread(this@AwtAGOpenglCanvas) + else -> VsyncRenderThread(this@AwtAGOpenglCanvas) } init { addHierarchyListener { - if (dl == null) return@addHierarchyListener val added = getContainerFrame()?.isVisible == true if (dl.running != added) { if (added) dl.start() else dl.stop() @@ -99,7 +158,9 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP } } } - }.also { layout = GridLayout(1, 1) }.also { add(it) } + } + + val canvas = GLCanvas().also { layout = GridLayout(1, 1) }.also { add(it) } //override val ag: AGOpengl = AGOpenglAWT(checkGl = true, logGl = true) val ag: AG = AGOpenglAWT() diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 46bf5f7069..2a2b60df0f 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -6,10 +6,12 @@ import korlibs.graphics.* import korlibs.image.awt.* import korlibs.image.bitmap.* import korlibs.image.color.* +import korlibs.io.lang.* import korlibs.platform.* import korlibs.render.* import korlibs.render.MenuItem import korlibs.time.* +import korlibs.time.TimedCache import kotlinx.coroutines.* import java.awt.* import java.awt.datatransfer.* @@ -80,11 +82,6 @@ open class AwtCanvasGameWindow( popupMenu.show(canvas, canvas.mousePosition.x, canvas.mousePosition.y) } - override fun close(exitCode: Int) { - super.close(exitCode) - coroutineDispatcher.close() - } - override fun lostOwnership(clipboard: Clipboard?, contents: Transferable?) { } @@ -150,8 +147,8 @@ class AwtGameWindow( }) initTools() } - override fun paintComponents(g: Graphics?) { - } + //override fun paintComponents(g: Graphics?) { + //} } override val window: Window get() = frame @@ -245,12 +242,10 @@ class AwtGameWindow( override fun close(exitCode: Int) { try { super.close(exitCode) - } catch (e: Throwable) { - e.printStackTrace() - } - println("exitProcessOnClose=$exitProcessOnClose") - if (exitProcessOnClose) { - System.exit(exitCode) + } finally { + if (exitProcessOnClose) { + System.exit(exitCode) + } } } diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt index 166c79cdf7..e019655f02 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindowDebugger.kt @@ -37,7 +37,6 @@ class AwtGameWindowDebugger(val gameWindow: GameWindow, val mainFrame: JFrame) { override fun windowClosing(e: WindowEvent?) { debugFrame.isVisible = false debugFrame.dispose() - gameWindow.close() } }) mainFrame.addComponentListener(object : ComponentAdapter() { diff --git a/korge/src/jvm/korlibs/render/win32/Win32Tools.kt b/korge/src/jvm/korlibs/render/win32/Win32Tools.kt index f122c73145..be8dbd3eb9 100644 --- a/korge/src/jvm/korlibs/render/win32/Win32Tools.kt +++ b/korge/src/jvm/korlibs/render/win32/Win32Tools.kt @@ -363,7 +363,6 @@ class Win32OpenglContext(val gwconfig: GameWindowConfig, val hWnd: WinDef.HWND, GameWindowQuality.PERFORMANCE, GameWindowQuality.AUTOMATIC -> DirectGL.glDisable(GL_MULTISAMPLE) } - } override fun releaseCurrent() { @@ -375,9 +374,11 @@ class Win32OpenglContext(val gwconfig: GameWindowConfig, val hWnd: WinDef.HWND, val error = Win32.GetLastError() logger.error { "WGL.wglMakeCurrent($hDC, $hRC).error = $error" } } + //println("makeCurrent") } override fun swapBuffers() { + //println("swapBuffers") //println("swapBuffers") Win32.glFlush() Win32.SwapBuffers(hDC) From 1da13838273199b173f5f8f0fefc7fba82583af1 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 16:48:27 +0200 Subject: [PATCH 39/66] More work --- .../common/korlibs/audio/sound/AudioData.kt | 21 ++---- .../audio/sound/PlatformAudioOutput.kt | 9 ++- .../src/common/korlibs/audio/sound/Sound.kt | 26 +++++++- .../korlibs/audio/sound/SoundAudioStream.kt | 66 +++++++++++++++++++ .../audio/sound/NativeSoundProviderJvm.kt | 7 +- .../sound/impl/awt/AwtNativeSoundProvider.kt | 30 +++++---- korge-sandbox/src/commonMain/kotlin/Main.kt | 8 +-- .../commonMain/kotlin/samples/MainSound.kt | 3 +- .../korlibs/graphics/gl/GLShaderCompiler.kt | 2 +- korge/src/common/korlibs/korge/Korge.kt | 12 +++- .../korlibs/korge/KorgeViewsConfigureInput.kt | 2 +- korge/src/common/korlibs/render/GameWindow.kt | 2 +- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 17 +++-- 13 files changed, 156 insertions(+), 49 deletions(-) diff --git a/korge-core/src/common/korlibs/audio/sound/AudioData.kt b/korge-core/src/common/korlibs/audio/sound/AudioData.kt index d7e3b5201d..0e89dd4ee4 100644 --- a/korge-core/src/common/korlibs/audio/sound/AudioData.kt +++ b/korge-core/src/common/korlibs/audio/sound/AudioData.kt @@ -1,19 +1,11 @@ package korlibs.audio.sound -import korlibs.time.TimeSpan -import korlibs.time.seconds -import korlibs.memory.arraycopy -import korlibs.audio.format.AudioDecodingProps -import korlibs.audio.format.AudioEncodingProps -import korlibs.audio.format.AudioFormat -import korlibs.audio.format.AudioFormats -import korlibs.audio.format.defaultAudioFormats -import korlibs.io.file.VfsFile -import korlibs.io.file.VfsOpenMode -import korlibs.io.file.baseName -import korlibs.io.lang.invalidOp -import korlibs.io.stream.openUse -import kotlin.math.min +import korlibs.audio.format.* +import korlibs.io.file.* +import korlibs.io.lang.* +import korlibs.memory.* +import korlibs.time.* +import kotlin.math.* class AudioData( val rate: Int, @@ -32,6 +24,7 @@ class AudioData( val totalSamples: Int get() = samples.totalSamples val totalTime: TimeSpan get() = timeAtSample(totalSamples) fun timeAtSample(sample: Int): TimeSpan = ((sample).toDouble() / rate.toDouble()).seconds + fun sampleAtTime(time: TimeSpan): Int = (time.seconds * rate.toDouble()).toInt() operator fun get(channel: Int): ShortArray = samples.data[channel] operator fun get(channel: Int, sample: Int): Short = samples.data[channel][sample] diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index 8e58eafc55..abee56065f 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -16,12 +16,14 @@ open class NewPlatformAudioOutput( val gen: (AudioSamples) -> Unit, ) : Disposable, SoundProps { val onCancel = coroutineContext.onCancel { stop() } + var paused: Boolean = false private val lock = Lock() fun safeGen(buffer: AudioSamples) { lock { try { gen(buffer) + applyPropsTo(buffer) } catch (e: Throwable) { e.printStackTrace() } @@ -46,7 +48,10 @@ open class PlatformAudioOutputBasedOnNew( } val new = soundProvider.createNewPlatformAudioOutput(coroutineContext, 2, frequency) { buffer -> + //println("availableRead=$availableRead") + //if (availableRead >= buffer.data.size) { readShorts(buffer.data) + //} } override fun start() { @@ -251,9 +256,7 @@ open class DequeBasedPlatformAudioOutput( override val availableSamples: Int get() = lock { deque.availableRead } final override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - while (deque.availableRead >= 441 * 4) { - delay(10.milliseconds) - } + while (deque.availableRead >= 1024 * 16) delay(1.milliseconds) lock { deque.write(samples, offset, size) } } } diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index c323b4c0be..46268a8a69 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -6,6 +6,7 @@ import korlibs.io.async.* import korlibs.io.file.* import korlibs.io.lang.* import korlibs.io.stream.* +import korlibs.math.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* @@ -43,6 +44,10 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS open class NativeSoundProviderNew : NativeSoundProvider() { final override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) + + override suspend fun createNonStreamingSound(data: AudioData, name: String): Sound { + return super.createNonStreamingSound(data, name) + } } open class NativeSoundProvider() : Disposable { @@ -96,7 +101,8 @@ open class NativeSoundProvider() : Disposable { open suspend fun createNonStreamingSound( data: AudioData, name: String = "Unknown" - ): Sound = createStreamingSound(data.toStream(), true, name) + //): Sound = createStreamingSound(data.toStream(), true, name) + ): Sound = SoundAudioData(coroutineContextKt, data, this, true, name) open suspend fun createSound( data: AudioData, @@ -188,6 +194,24 @@ fun SoundProps.copySoundPropsFrom(other: ReadonlySoundProps) { this.panning = other.panning } +fun SoundProps.volumeForChannel(channel: Int): Double { + return when (channel) { + 0 -> panning.convertRangeClamped(-1.0, 0.0, 0.0, 1.0) + else -> 1.0 - panning.convertRangeClamped(0.0, 1.0, 0.0, 1.0) + } +} + +fun SoundProps.applyPropsTo(samples: AudioSamples) { + for (ch in 0 until samples.channels) { + val volume01 = volumeForChannel(ch) + for (n in 0 until samples.totalSamples) { + var sample = samples[ch, n] + sample = (sample * volume01).toInt().toShort() + samples[ch, n] = sample + } + } +} + fun SoundProps.copySoundPropsFromCombined(l: ReadonlySoundProps, r: ReadonlySoundProps) { this.volume = l.volume * r.volume this.pitch = l.pitch * r.pitch diff --git a/korge-core/src/common/korlibs/audio/sound/SoundAudioStream.kt b/korge-core/src/common/korlibs/audio/sound/SoundAudioStream.kt index 09751d1cbb..10a6d83fc5 100644 --- a/korge-core/src/common/korlibs/audio/sound/SoundAudioStream.kt +++ b/korge-core/src/common/korlibs/audio/sound/SoundAudioStream.kt @@ -10,6 +10,72 @@ import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.coroutines.cancellation.CancellationException +@OptIn(ExperimentalCoroutinesApi::class) +class SoundAudioData( + coroutineContext: CoroutineContext, + val audioData: AudioData, + var soundProvider: NativeSoundProvider, + val closeStream: Boolean = false, + override val name: String = "Unknown", + val onComplete: (suspend () -> Unit)? = null +) : Sound(coroutineContext) { + override suspend fun decode(maxSamples: Int): AudioData = audioData + + override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { + var pos = 0 + var paused = false + var times = params.times + var nas: NewPlatformAudioOutput? = null + nas = soundProvider.createNewPlatformAudioOutput(coroutineContext, audioData.channels, audioData.rate) { it -> + if (paused) { + // @TODO: paused should not even call this right? + for (ch in 0 until it.channels) { + audioData[ch].fill(0) + } + return@createNewPlatformAudioOutput + } + loop@for (ch in 0 until it.channels) { + val audioDataCh = audioData[ch] + for (n in 0 until it.totalSamples) { + val audioDataPos = pos + n + val sample = if (audioDataPos < audioDataCh.size) audioDataCh[audioDataPos] else 0 + it[ch, n] = sample + } + } + pos += it.totalSamples + if (pos >= audioData.totalSamples) { + pos = 0 + times = times.oneLess + + if (times == PlaybackTimes.ZERO) { + nas?.stop() + } + } + } + nas.copySoundPropsFromCombined(params, this) + nas.start() + return object : SoundChannel(this) { + override var volume: Double by nas::volume + override var pitch: Double by nas::pitch + override var panning: Double by nas::panning + override var current: TimeSpan + get() = audioData.timeAtSample(pos) + set(value) { + pos = audioData.sampleAtTime(value) + } + override val total: TimeSpan get() = audioData.totalTime + override val state: SoundChannelState get() = when { + paused -> SoundChannelState.PAUSED + playing -> SoundChannelState.PLAYING + else -> SoundChannelState.STOPPED + } + override fun pause() { nas.paused = true } + override fun resume() { nas.paused = false } + override fun stop() { nas.stop() } + } + } +} + @OptIn(ExperimentalCoroutinesApi::class) class SoundAudioStream( coroutineContext: CoroutineContext, diff --git a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt index 1785953b3c..d964912c27 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt @@ -2,7 +2,6 @@ package korlibs.audio.sound import korlibs.audio.sound.backend.* import korlibs.audio.sound.impl.awt.* -import korlibs.audio.sound.impl.jna.* import korlibs.audio.sound.impl.jna.OpenALException import korlibs.io.time.* import korlibs.logger.* @@ -15,10 +14,8 @@ private val nativeSoundProviderDeferred: NativeSoundProvider by lazy { traceTime("SoundProvider") { when { //Platform.isLinux -> FFIALSANativeSoundProvider - //Platform.isLinux -> AwtNativeSoundProvider - //Platform.isApple -> jvmCoreAudioNativeSoundProvider - //Platform.isWindows -> jvmWaveOutNativeSoundProvider - //Platform.isWindows -> AwtNativeSoundProvider + Platform.isApple -> jvmCoreAudioNativeSoundProvider + Platform.isWindows -> jvmWaveOutNativeSoundProvider //else -> JnaOpenALNativeSoundProvider() else -> AwtNativeSoundProvider } ?: AwtNativeSoundProvider diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt index f28b83a4ad..8f94b67d9e 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt @@ -1,14 +1,12 @@ package korlibs.audio.sound.impl.awt import korlibs.audio.sound.* -import korlibs.datastructure.* import korlibs.datastructure.thread.* import korlibs.memory.* -import korlibs.time.* -import kotlinx.coroutines.* import javax.sound.sampled.* import kotlin.coroutines.* -import kotlin.time.* + +private val mixer by lazy { AudioSystem.getMixer(null) } object AwtNativeSoundProvider : NativeSoundProviderNew() { override fun createNewPlatformAudioOutput( @@ -32,29 +30,37 @@ class JvmNewPlatformAudioOutput( val BYTES_PER_SAMPLE = nchannels * Short.SIZE_BYTES - fun bytesToSamples(bytes: Int): Int = bytes / BYTES_PER_SAMPLE - fun samplesToBytes(samples: Int): Int = samples * BYTES_PER_SAMPLE + private fun bytesToSamples(bytes: Int): Int = bytes / BYTES_PER_SAMPLE + private fun samplesToBytes(samples: Int): Int = samples * BYTES_PER_SAMPLE override fun start() { //println("TRYING TO START") if (nativeThread?.threadSuggestRunning == true) return //println("STARTED") + + // SAMPLE -> Short, FRAME -> nchannels * SAMPLE nativeThread = nativeThread(isDaemon = true) { it.threadSuggestRunning = true val nchannels = this.nchannels - //val format = AudioFormat(frequency.toFloat(), Short.SIZE_BITS * nchannels, nchannels, true, false) - val format = AudioFormat(44100.toFloat(), Short.SIZE_BITS * nchannels, nchannels, true, false) - val line = AudioSystem.getSourceDataLine(format) + val format = AudioFormat(frequency.toFloat(), Short.SIZE_BITS, nchannels, true, false) + //val format = AudioFormat(44100.toFloat(), Short.SIZE_BITS, nchannels, true, false) + //val line = AudioSystem.getSourceDataLine(format) + val line = (mixer.getLine(DataLine.Info(SourceDataLine::class.java, format)) as SourceDataLine) line.open() line.start() try { val info = AudioSamples(nchannels, 1024) val bytes = ByteArray(samplesToBytes(1024)) while (it.threadSuggestRunning) { - safeGen(info) - bytes.setArrayLE(0, info.interleaved().data) - line.write(bytes, 0, bytes.size) + if (paused) { + Thread.sleep(10L) + } else { + safeGen(info) + bytes.setArrayLE(0, info.interleaved().data) + //println(bytes.count { it == 0.toByte() }) + line.write(bytes, 0, bytes.size) + } } } catch (e: Throwable) { e.printStackTrace() diff --git a/korge-sandbox/src/commonMain/kotlin/Main.kt b/korge-sandbox/src/commonMain/kotlin/Main.kt index c82aeb7b66..c3952c4715 100644 --- a/korge-sandbox/src/commonMain/kotlin/Main.kt +++ b/korge-sandbox/src/commonMain/kotlin/Main.kt @@ -2,7 +2,6 @@ import korlibs.event.* import korlibs.image.color.* import korlibs.image.text.* -import korlibs.inject.* import korlibs.io.async.* import korlibs.korge.* import korlibs.korge.input.* @@ -66,8 +65,7 @@ suspend fun main() = Korge( //sceneContainer().changeTo({MainGraphicsText()}); return@start //sceneContainer().changeTo({MainUI()}); return@start - val injector = injector() - println("Injector: $injector") // Ensure injector is available as a manual test + //println("Injector: ${injector()}") // Ensure injector is available as a manual test var lastBackTime = DateTime.EPOCH keys { @@ -128,8 +126,8 @@ suspend fun main() = Korge( //Demo(::MainShape2dScene), //Demo(::MainUIStacks), //Demo(::MainPolyphonic), - //Demo(::MainSound), - Demo(::MainTiledBackground), + Demo(::MainSound), + //Demo(::MainTiledBackground), //Demo(::MainSprites10k), //Demo(::MainStressMatrixMultiplication), //Demo(::MainSDF), diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt index 2acbc5bec4..26e29fad74 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainSound.kt @@ -8,7 +8,8 @@ import korlibs.time.* class MainSound : Scene() { override suspend fun SContainer.sceneMain() { - val music = resourcesVfs["sounds/Snowland.mp3"].readMusic() + //val music = resourcesVfs["sounds/Snowland.mp3"].readMusic() + val music = resourcesVfs["sounds/Snowland.mp3"].readSound() //val music = resourcesVfs["sounds/click.wav"].readSound() //val music = resourcesVfs["sounds/click.wav"].readMusic() diff --git a/korge/src/common/korlibs/graphics/gl/GLShaderCompiler.kt b/korge/src/common/korlibs/graphics/gl/GLShaderCompiler.kt index 579291293a..d5d2ee56ef 100644 --- a/korge/src/common/korlibs/graphics/gl/GLShaderCompiler.kt +++ b/korge/src/common/korlibs/graphics/gl/GLShaderCompiler.kt @@ -89,7 +89,7 @@ internal object GLShaderCompiler { //println("!!! PROGRAM SUCCESSFULLY COMPILED: config=$config\n$vertexString\n$fragmentString" ) return Triple(fragmentShaderId, vertexShaderId, config) } catch (e: AGOpengl.ShaderException) { - logger.info { e.stackTraceToString() } + logger.debug { e.stackTraceToString() } //e.printStackTrace() errors += e diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index 2d9dfd9358..8053fe31b0 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -203,12 +203,22 @@ fun GameWindow.configureKorge(config: KorgeConfig = KorgeConfig(), block: suspen views.clipBorders = config.displayMode.clipBorders views.targetFps = config.targetFps - KorgeReload.registerEventDispatcher(gameWindow) @Suppress("OPT_IN_USAGE") views.prepareViewsBase(gameWindow, true, gameWindow.bgcolor, config.forceRenderEveryFrame, config.configInjector) gameWindow.queueSuspend { + // @TODO: ResourcesVfs seems to require an access here? If ResourcesVfs is accessed in a scene, it is Cancelled later. + resourcesVfs["klogger.properties"].exists() + + if (!Platform.isJsBrowser) { + try { + configureLoggerFromProperties(localCurrentDirVfs["klogger.properties"]) + } catch (e: Throwable) { + + } + } + Korge.logger.info { "Initializing..." } runCatching { views.init() }.exceptionOrNull()?.let { it.stackTraceToString() } diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index 73008e0bd9..918e6d083b 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -31,7 +31,7 @@ internal fun Views.prepareViewsBase( injector.mapInstance(views) injector.mapInstance(views.ag) injector.mapInstance(Resources::class, views.globalResources) - injector.mapSingleton(ResourcesRoot::class) { ResourcesRoot() } + injector.mapInstance(ResourcesRoot()) injector.mapInstance(views.input) injector.mapInstance(views.stats) //injector.mapInstance(CoroutineContext::class, views.coroutineContext) // Maybe we shouldn't include this diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 95406ad618..1a865a5a10 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -107,7 +107,7 @@ open class GameWindow : @PublishedApi internal val _updateRenderLock = Lock() inline fun updateRenderLock(block: () -> Unit) = _updateRenderLock(block) fun queueSuspend(callback: suspend () -> Unit) { - getCoroutineDispatcherWithCurrentContext().launchUnscoped { + launchAsap(getCoroutineDispatcherWithCurrentContext()) { callback() } } diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index db1cb2bda3..d22d1bcdb9 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -18,7 +18,6 @@ import korlibs.render.platform.* import korlibs.time.* import java.awt.* import java.awt.Graphics -import java.lang.IllegalStateException import javax.swing.* // @TODO: Use Metal, OpenGL or whatever required depending on what's AWT is using @@ -36,12 +35,14 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP var updatedSinceFrame = KorAtomicInt(0) interface RendererThread { + val doRenderInUIThread: Boolean val running: Boolean fun start() fun stop() } class DisplayLinkRenderThread(val canvas: AwtAGOpenglCanvas) : RendererThread { + override val doRenderInUIThread: Boolean = true val dl = OSXDisplayLink { //private val dl: OSXDisplayLink? = if (true) null else OSXDisplayLink { //if (autoRepaint && ctx?.supportsSwapInterval() != true && createdBufferStrategy) { @@ -70,6 +71,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP } class VsyncRenderThread(val canvas: AwtAGOpenglCanvas) : RendererThread { + override val doRenderInUIThread: Boolean = false var createdBufferStrategy = false var thread: NativeThread? = null @@ -92,7 +94,11 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP continue } } - fcanvas.renderGraphics(fcanvas.bufferStrategy.drawGraphics) + if (canvas.continuousRenderMode || canvas.updatedSinceFrame.value > 0) { + //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") + canvas.updatedSinceFrame.value = 0 + fcanvas.renderGraphics(fcanvas.bufferStrategy.drawGraphics) + } } } catch (e: Throwable) { e.printStackTrace() @@ -139,8 +145,11 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP } override fun paint(g: Graphics) { - - //renderGraphics(g) + if (dl.doRenderInUIThread) { + renderGraphics(g) + } else { + updatedSinceFrame.incrementAndGet() + } } From f4291f533df04b30624d728c5ec66716bdccdeae Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 17:42:50 +0200 Subject: [PATCH 40/66] More work --- .../korge/gradle/targets/js/JavaScriptRun.kt | 3 +- .../korge/gradle/targets/jvm/KorgeJavaExec.kt | 2 +- .../datastructure/_Datastructure_closeable.kt | 6 +++- .../datastructure/_Datastructure_event.kt | 4 ++- .../datastructure/event/JsEventLoop.kt | 22 ++++++++++++++ .../src/common/korlibs/korge/KorgeHeadless.kt | 5 +++- .../korlibs/korge/tests/ViewsForTesting.kt | 8 +++-- korge/src/common/korlibs/korge/view/Views.kt | 1 + korge/src/common/korlibs/render/GameWindow.kt | 29 ++++++++++++++----- .../js/korlibs/render/DefaultGameWindowJs.kt | 22 ++++++-------- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 7 ++--- 11 files changed, 76 insertions(+), 33 deletions(-) create mode 100644 korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/JavaScriptRun.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/JavaScriptRun.kt index 6f82fe8cde..2d03bfc147 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/JavaScriptRun.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/JavaScriptRun.kt @@ -75,7 +75,8 @@ fun Project.configureJavascriptRun() { // @TODO: jsBrowserProductionRun is much faster than jsBrowserDevelopmentRun at runtime. Why is that?? val runJs = project.tasks.createThis("runJs") { group = GROUP_KORGE_RUN - dependsOn(runJsRelease) + //dependsOn(runJsRelease) + dependsOn(runJsDebug) } /* diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt index 128b585842..0fe978a36f 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/jvm/KorgeJavaExec.kt @@ -166,7 +166,7 @@ open class KorgeJavaExec : JavaExec() { || System.getenv("KORGW_JVM_ENGINE") == "sdl" //|| project.findProperty("korgw.jvm.engine") == "sdl" ) - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions", "-XX:+UseZGC", "-XX:+ZGenerational") + jvmArgs("-XX:+UnlockExperimentalVMOptions", "-XX:+IgnoreUnrecognizedVMOptions", "-XX:+UseZGC", "-XX:+ZGenerational") //jvmArgs("-XX:+UseZGC") if (firstThread && isMacos) { jvmArgs("-XstartOnFirstThread") diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_closeable.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_closeable.kt index 162d96f6c6..6124b96f40 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_closeable.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_closeable.kt @@ -3,7 +3,11 @@ package korlibs.datastructure.closeable @OptIn(ExperimentalStdlibApi::class) -interface Closeable : AutoCloseable { +//interface Closeable : AutoCloseable { +//interface Closeable : AutoCloseable { +interface Closeable { + fun close(): Unit + companion object { operator fun invoke(callback: () -> Unit) = object : Closeable { override fun close() = callback() diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 26548267fc..69e1a18ff6 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -17,13 +17,15 @@ interface EventLoop : Closeable { } fun EventLoop.setInterval(time: Frequency, task: () -> Unit): Closeable = setInterval(time.timeSpan, task) +abstract class BaseEventLoop : EventLoop + class SyncEventLoop( /** precise=true will have better precision at the cost of more CPU-usage (busy waiting) */ //var precise: Boolean = true, var precise: Boolean = false, /** Execute timers immediately instead of waiting. Useful for testing. */ var immediateRun: Boolean = false, -) : EventLoop { +) : BaseEventLoop() { private val lock = NonRecursiveLock() private var running = true protected class TimedTask(val eventLoop: SyncEventLoop, var now: Duration, val time: Duration, var interval: Boolean, val callback: () -> Unit) : Comparable, Closeable { diff --git a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt new file mode 100644 index 0000000000..bca582b391 --- /dev/null +++ b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt @@ -0,0 +1,22 @@ +package korlibs.datastructure.event + +import korlibs.datastructure.closeable.* +import korlibs.platform.* +import korlibs.time.* + +object JsEventLoop : BaseEventLoop() { + override fun close() = Unit + override fun setImmediate(task: () -> Unit) { + jsGlobalThis.setTimeout({ task() }, 0) + } + + override fun setTimeout(time: TimeSpan, task: () -> Unit): Closeable { + val id = jsGlobalThis.setTimeout({ task() }, time.millisecondsInt) + return Closeable { jsGlobalThis.clearTimeout(id) } + } + + override fun setInterval(time: TimeSpan, task: () -> Unit): Closeable { + val id = jsGlobalThis.setInterval({ task() }, time.millisecondsInt) + return Closeable { jsGlobalThis.clearInterval(id) } + } +} diff --git a/korge/src/common/korlibs/korge/KorgeHeadless.kt b/korge/src/common/korlibs/korge/KorgeHeadless.kt index dc24a50745..7ca105b2c5 100644 --- a/korge/src/common/korlibs/korge/KorgeHeadless.kt +++ b/korge/src/common/korlibs/korge/KorgeHeadless.kt @@ -1,5 +1,6 @@ package korlibs.korge +import korlibs.datastructure.event.* import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.graphics.gl.* @@ -19,6 +20,8 @@ object KorgeHeadless { exitProcessOnClose: Boolean = false, override val devicePixelRatio: Double = 1.0, ) : GameWindow() { + val syncEventLoop by lazy { eventLoop as SyncEventLoop } + override val width: Int = size.width.toInt() override val height: Int = size.height.toInt() @@ -27,7 +30,7 @@ object KorgeHeadless { } init { - nativeThread { eventLoop.runTasksForever() } + nativeThread(name = "HeadlessGameWindow-syncEventLoop") { syncEventLoop.runTasksForever() } eventLoop.setInterval(60.hz.timeSpan) { (ag as? AGOpengl?)?.context?.set() this@HeadlessGameWindow.dispatchNewRenderEvent() diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index 7aef5cac54..2491efc6c9 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -1,5 +1,6 @@ package korlibs.korge.tests +import korlibs.datastructure.event.* import korlibs.event.* import korlibs.graphics.* import korlibs.graphics.log.* @@ -38,9 +39,10 @@ open class ViewsForTesting( override fun now(): DateTime = time } inner class TestGameWindow(initialSize: Size) : GameWindowLog() { + val syncEventLoop by lazy { eventLoop as SyncEventLoop } init { - eventLoop.immediateRun = true - eventLoop.nowProvider = { time.unixMillisDouble.milliseconds } + syncEventLoop.immediateRun = true + syncEventLoop.nowProvider = { time.unixMillisDouble.milliseconds } } override val autoUpdateInterval = false override var androidContextAny: Any? = null @@ -326,7 +328,7 @@ open class ViewsForTesting( simulateFrame() //println("[2]") //val ntasks = gameWindow.eventLoop.runAvailableNextTask(10) - val ntasks = gameWindow.eventLoop.runAvailableNextTasks() + val ntasks = gameWindow.syncEventLoop.runAvailableNextTasks() //println("[3]") if (ntasks == 0) { diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 512db1a303..0a20ca7270 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -1,6 +1,7 @@ package korlibs.korge.view import korlibs.datastructure.* +import korlibs.datastructure.event.* import korlibs.datastructure.iterators.* import korlibs.event.* import korlibs.graphics.* diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 1a865a5a10..7e69854f17 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -95,24 +95,41 @@ open class GameWindow : //override val ag: AG = LogAG() override val ag: AG = AGDummy() - val eventLoop = SyncEventLoop(precise = false) + open fun createEventLoop(): BaseEventLoop = SyncEventLoop(precise = false) + + val eventLoop: BaseEventLoop by lazy { createEventLoop() } //val renderEventLoop = SyncEventLoop(precise = false, immediateRun = true) - open val coroutineDispatcher = EventLoopCoroutineDispatcher(eventLoop) + open val coroutineDispatcher by lazy { + EventLoopCoroutineDispatcher(eventLoop).also { + ensureInitialized() + } + } var coroutineContext: CoroutineContext = EmptyCoroutineContext fun getCoroutineDispatcherWithCurrentContext(coroutineContext: CoroutineContext): CoroutineContext = coroutineContext + coroutineDispatcher fun getCoroutineDispatcherWithCurrentContext(): CoroutineContext = getCoroutineDispatcherWithCurrentContext(coroutineContext) + private fun ensureInitialized() { + updateUpdateInterval() + } + // Event Loop @PublishedApi internal val _updateRenderLock = Lock() inline fun updateRenderLock(block: () -> Unit) = _updateRenderLock(block) fun queueSuspend(callback: suspend () -> Unit) { + coroutineDispatcher launchAsap(getCoroutineDispatcherWithCurrentContext()) { callback() } } - fun queue(callback: () -> Unit) = eventLoop.setImmediate(callback) - fun queue(callback: Runnable) = eventLoop.setImmediate { callback.run() } + fun queue(callback: () -> Unit) { + coroutineDispatcher + eventLoop.setImmediate(callback) + } + fun queue(callback: Runnable) { + coroutineDispatcher + eventLoop.setImmediate { callback.run() } + } @Deprecated("") fun queueBlocking(callback: () -> T): T { val deferred = CompletableDeferred() @@ -183,10 +200,6 @@ open class GameWindow : } } - init { - updateUpdateInterval() - } - // PROPS open var visible: Boolean = false fun hide() = run { visible = false } diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index dac0104f49..183276e0f6 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -1,5 +1,6 @@ package korlibs.render +import korlibs.datastructure.event.* import korlibs.event.* import korlibs.event.Touch import korlibs.graphics.* @@ -20,6 +21,7 @@ import org.w3c.dom.events.MouseEvent private external val navigator: dynamic open class JsGameWindow : GameWindow() { + override fun createEventLoop(): EventLoop = JsEventLoop } open class BrowserCanvasJsGameWindow( @@ -109,7 +111,7 @@ open class BrowserCanvasJsGameWindow( } } - override var quality: Quality = Quality.AUTOMATIC + override var quality: GameWindowQuality = GameWindowQuality.AUTOMATIC set(value) { if (field != value) { field = value @@ -207,7 +209,7 @@ open class BrowserCanvasJsGameWindow( } } } - dispatch(keyEvent { + dispatch(events.keyEvent { this.type = when (me.type) { "keydown" -> KeyEvent.Type.DOWN "keyup" -> KeyEvent.Type.UP @@ -257,7 +259,7 @@ open class BrowserCanvasJsGameWindow( val tx = transformEventX(e.clientX.toFloat() - canvasBounds.left.toFloat()).toInt() val ty = transformEventY(e.clientY.toFloat() - canvasBounds.top.toFloat()).toInt() //console.log("mouseEvent", type.toString(), e.clientX, e.clientY, tx, ty) - mouseEvent { + events.mouseEvent { this.type = if (e.buttons.toInt() != 0) pressingType else type this.scaleCoords = false this.id = 0 @@ -294,7 +296,7 @@ open class BrowserCanvasJsGameWindow( // If we are in a touch device, touch events will be dispatched, and then we don't want to emit mouse events, that would be duplicated if (!is_touch_device() || type == korlibs.event.MouseEvent.Type.SCROLL) { - dispatch(mouseEvent) + dispatch(events.mouseEvent) } } @@ -310,7 +312,7 @@ open class BrowserCanvasJsGameWindow( set(value) { field = value canvas.style.cursor = when (value) { - is Cursor -> { + is korlibs.render.Cursor -> { when (value) { Cursor.DEFAULT -> "default" Cursor.CROSSHAIR -> "crosshair" @@ -380,13 +382,6 @@ open class BrowserCanvasJsGameWindow( loopJob = null } - override suspend fun loop(entry: suspend GameWindow.() -> Unit) { - loopJob = launchImmediately(getCoroutineDispatcherWithCurrentContext()) { - entry() - } - jsFrame(0.0) - } - private lateinit var jsFrame: (Double) -> Unit init { @@ -455,8 +450,9 @@ open class BrowserCanvasJsGameWindow( jsFrame = { step: Double -> window.requestAnimationFrame(jsFrame) // Execute first to prevent exceptions breaking the loop, not triggering again - frame() + dispatchNewRenderEvent() } + jsFrame(0.0) } diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 2a2b60df0f..b938ce2d78 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -1,17 +1,16 @@ package korlibs.render.awt import korlibs.datastructure.* +import korlibs.datastructure.event.* import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.image.awt.* import korlibs.image.bitmap.* import korlibs.image.color.* -import korlibs.io.lang.* import korlibs.platform.* import korlibs.render.* import korlibs.render.MenuItem import korlibs.time.* -import korlibs.time.TimedCache import kotlinx.coroutines.* import java.awt.* import java.awt.datatransfer.* @@ -21,7 +20,7 @@ import kotlin.concurrent.* val AwtAGOpenglCanvas.gameWindow: AwtCanvasGameWindow by Extra.PropertyThis { AwtCanvasGameWindow(this) } -open class AwtCanvasGameWindow( +open class AwtCanvasGameWindow constructor( val canvas: AwtAGOpenglCanvas ) : GameWindow(), ClipboardOwner { override val devicePixelRatio: Double by TimedCache(ttl = .1.seconds) { canvas.devicePixelRatio } @@ -39,7 +38,7 @@ open class AwtCanvasGameWindow( } val thread = nativeThread(name = "NewAwtGameWindow") { - eventLoop.runTasksForever() + (this.eventLoop as SyncEventLoop).runTasksForever() } override val width: Int get() = canvas.width From 0f54c2b168d009bda1eee05af4cd88cbf104bae5 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 17:44:12 +0200 Subject: [PATCH 41/66] More work --- korge/src/js/korlibs/render/DefaultGameWindowJs.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index 183276e0f6..19c84f8cb9 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -21,7 +21,7 @@ import org.w3c.dom.events.MouseEvent private external val navigator: dynamic open class JsGameWindow : GameWindow() { - override fun createEventLoop(): EventLoop = JsEventLoop + override fun createEventLoop(): BaseEventLoop = JsEventLoop } open class BrowserCanvasJsGameWindow( From 5761e6790b768a2e74f4ce83335d16fbb64d8129 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 18:23:17 +0200 Subject: [PATCH 42/66] More work --- .../audio/sound/PlatformAudioOutput.kt | 4 +- .../audio/sound/backend/JvmWaveOutImpl.kt | 2 +- .../audio/sound/HtmlNativeSoundProvider.kt | 29 +++-- .../audio/sound/NativeAudioStreamJs.kt | 114 +++++------------- .../audio/sound/backend/CoreAudioImpl.kt | 4 +- .../sound/impl/awt/AwtNativeSoundProvider.kt | 2 +- korge/src/common/korlibs/korge/view/Views.kt | 6 +- korge/src/common/korlibs/render/GameWindow.kt | 33 ++++- .../js/korlibs/render/DefaultGameWindowJs.kt | 4 +- .../korlibs/render/awt/AwtAGOpenglCanvas.kt | 15 +-- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 3 +- .../korlibs/korge/view/ViewUpdatedTest.kt | 8 +- 12 files changed, 89 insertions(+), 135 deletions(-) diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index abee56065f..fe6b1367a6 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -13,13 +13,13 @@ open class NewPlatformAudioOutput( val coroutineContext: CoroutineContext, val nchannels: Int, val frequency: Int, - val gen: (AudioSamples) -> Unit, + private val gen: (AudioSamples) -> Unit, ) : Disposable, SoundProps { val onCancel = coroutineContext.onCancel { stop() } var paused: Boolean = false private val lock = Lock() - fun safeGen(buffer: AudioSamples) { + fun genSafe(buffer: AudioSamples) { lock { try { gen(buffer) diff --git a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt index 39db31087f..60b7c60b54 100644 --- a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt +++ b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt @@ -135,7 +135,7 @@ class JvmWaveOutNewPlatformAudioOutput( var queued = 0 for (header in headers) { if (!header.hdr.isInQueue) { - safeGen(header.samples) + genSafe(header.samples) header.prepareAndWrite() queued++ //println("Sending running=$running, availableRead=$availableRead, header=${header}") diff --git a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt index c194b7e27a..23114cb0d5 100644 --- a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt +++ b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt @@ -1,24 +1,23 @@ package korlibs.audio.sound -import korlibs.time.TimeSpan -import korlibs.time.seconds -import korlibs.audio.format.AudioDecodingProps -import korlibs.audio.internal.SampleConvert -import korlibs.io.file.Vfs -import korlibs.io.file.std.LocalVfs -import korlibs.io.file.std.UrlVfs -import korlibs.io.lang.invalidOp -import kotlinx.coroutines.CompletableDeferred -import org.w3c.dom.HTMLAudioElement -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext - -class HtmlNativeSoundProvider : NativeSoundProvider() { +import korlibs.audio.format.* +import korlibs.audio.internal.* +import korlibs.io.file.* +import korlibs.io.file.std.* +import korlibs.io.lang.* +import korlibs.time.* +import kotlinx.coroutines.* +import org.w3c.dom.* +import kotlin.coroutines.* + +class HtmlNativeSoundProvider : NativeSoundProviderNew() { init { HtmlSimpleSound.ensureUnlockStart() } - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = JsPlatformAudioOutput(coroutineContext, freq) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + return JsNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) + } override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = AudioBufferSound(AudioBufferOrHTMLMediaElement(HtmlSimpleSound.loadSound(data)), "#bytes", coroutineContext, name) diff --git a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt index b2094b4ac0..c2f982ace5 100644 --- a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt +++ b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt @@ -1,17 +1,9 @@ package korlibs.audio.sound -import korlibs.datastructure.FloatArrayDeque -import korlibs.time.milliseconds -import korlibs.time.seconds -import korlibs.memory.* -import korlibs.audio.internal.SampleConvert -import korlibs.audio.internal.write -import korlibs.io.async.delay -import korlibs.io.lang.Cancellable -import korlibs.io.lang.cancel +import korlibs.audio.internal.* +import korlibs.io.lang.* import korlibs.platform.* -import kotlinx.browser.document -import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.* actual val nativeSoundProvider: NativeSoundProvider by lazy { if (Platform.isJsBrowser) { @@ -21,60 +13,47 @@ actual val nativeSoundProvider: NativeSoundProvider by lazy { } } -class JsPlatformAudioOutput(coroutineContext: CoroutineContext, val freq: Int) : PlatformAudioOutput(coroutineContext, freq) { - val id = lastId++ - +class JsNewPlatformAudioOutput( + coroutineContext: CoroutineContext, + nchannels: Int, + freq: Int, + gen: (AudioSamples) -> Unit +) : NewPlatformAudioOutput( + coroutineContext, nchannels, freq, gen +) { init { nativeSoundProvider // Ensure it is created } - companion object { - var lastId = 0 - } - var missingDataCount = 0 var nodeRunning = false var node: ScriptProcessorNode? = null - private val nchannels = 2 - private val deques = Array(nchannels) { FloatArrayDeque() } - - private fun process(e: AudioProcessingEvent) { - //val outChannels = Array(e.outputBuffer.numberOfChannels) { e.outputBuffer.getChannelData(it) } - val outChannels = Array(e.outputBuffer.numberOfChannels) { e.outputBuffer.getChannelData(it) } - var hasData = true - - if (!document.asDynamic().hidden) { - for (channel in 0 until nchannels) { - val deque = deques[channel] - val outChannel = outChannels[channel] - val read = deque.read(outChannel) - if (read < outChannel.size) hasData = false - } - } - - if (!hasData) { - missingDataCount++ - } - - if (missingDataCount >= 500) { - stop() - } - } - - private fun ensureInit() { node } - private var startPromise: Cancellable? = null override fun start() { if (nodeRunning) return startPromise = HtmlSimpleSound.callOnUnlocked { - node = HtmlSimpleSound.ctx?.createScriptProcessor(1024, 2, 2) - node?.onaudioprocess = { process(it) } + node = HtmlSimpleSound.ctx?.createScriptProcessor(1024, nchannels, 2) + node?.onaudioprocess = { e -> + val nchannels = e.outputBuffer.numberOfChannels + val outChannels = Array(nchannels) { e.outputBuffer.getChannelData(it) } + val nsamples = e.outputBuffer.getChannelData(0).size + val samples = AudioSamples(nchannels, nsamples) + genSafe(samples) + for (ch in 0 until nchannels) { + val samplesCh = samples[ch] + val outCh = outChannels[ch] + for (n in 0 until nsamples) { + outCh[n] = SampleConvert.shortToFloat(samplesCh[n]) + } + } + + } if (HtmlSimpleSound.ctx != null) this.node?.connect(HtmlSimpleSound.ctx.destination) } + nodeRunning = true missingDataCount = 0 - nodeRunning = true } override fun stop() { @@ -83,41 +62,4 @@ class JsPlatformAudioOutput(coroutineContext: CoroutineContext, val freq: Int) : this.node?.disconnect() nodeRunning = false } - - fun ensureRunning() { - ensureInit() - if (!nodeRunning) { - start() - } - } - - var totalShorts = 0 - override val availableSamples get() = totalShorts - - override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - //println("addSamples: $available, $size") - //println(samples.sliceArray(offset until offset + size).toList()) - totalShorts += size - if (!HtmlSimpleSound.available) { - // Delay simulating consuming samples - val sampleCount = (size / 2) - val timeSeconds = sampleCount.toDouble() / 41_000.0 - coroutineContext.delay(timeSeconds.seconds) - } else { - ensureRunning() - - val schannels = samples.channels - for (channel in 0 until nchannels) { - val sample = samples[channel % schannels] - val deque = deques[channel] - for (n in 0 until size) { - deque.write(SampleConvert.shortToFloat(sample[offset + n])) - } - } - - while (deques[0].availableRead > samples.totalSamples * 4) { - coroutineContext.delay(4.milliseconds) - } - } - } } diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index b626c05da5..58d262cbd2 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -4,9 +4,7 @@ import com.sun.jna.* import korlibs.audio.sound.* import korlibs.ffi.* import korlibs.io.annotations.* -import korlibs.io.async.* import korlibs.io.concurrent.atomic.* -import korlibs.io.lang.* import korlibs.memory.* import java.util.concurrent.* import java.util.concurrent.atomic.* @@ -45,7 +43,7 @@ private val jnaNewCoreAudioCallback by lazy { if (output.samples.totalSamples != samplesCount) output.samples = AudioSamples(nchannels, samplesCount) if (output.buffer.totalSamples != samplesCount) output.buffer = AudioSamplesInterleaved(nchannels, samplesCount) val samples = output.samples - output.safeGen(samples) + output.genSafe(samples) val buffer = output.buffer.data for (m in 0 until nchannels) { diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt index 8f94b67d9e..8b097002d3 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt @@ -56,7 +56,7 @@ class JvmNewPlatformAudioOutput( if (paused) { Thread.sleep(10L) } else { - safeGen(info) + genSafe(info) bytes.setArrayLE(0, info.interleaved().data) //println(bytes.count { it == 0.toByte() }) line.write(bytes, 0, bytes.size) diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 0a20ca7270..9dd671b407 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -1,7 +1,6 @@ package korlibs.korge.view import korlibs.datastructure.* -import korlibs.datastructure.event.* import korlibs.datastructure.iterators.* import korlibs.event.* import korlibs.graphics.* @@ -69,7 +68,8 @@ class Views( override val views = this var rethrowRenderError = false - var forceRenderEveryFrame: Boolean by gameWindow::continuousRenderMode + val continuousRenderMode = gameWindow.continuousRenderMode + var forceRenderEveryFrame: Boolean by continuousRenderMode::continuousRenderMode val virtualPixelsPerInch: Double get() = pixelsPerInch / globalToWindowScaleAvg val virtualPixelsPerCm: Double get() = virtualPixelsPerInch / DeviceDimensionsProvider.INCH_TO_CM @@ -421,7 +421,7 @@ class Views( debugSaveView(action, view) } - val updatedSinceFrame: KorAtomicInt by gameWindow::updatedSinceFrame + //val updatedSinceFrame: KorAtomicInt by gameWindow::updatedSinceFrame fun startFrame() { gameWindow.startFrame() diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 7e69854f17..e0ccd97c5e 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -17,11 +17,35 @@ import korlibs.math.geom.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* -import kotlin.properties.* /** Creates the default [GameWindow] for this platform. Typically, a window/frame or a fullscreen screen depending on the OS */ expect fun CreateDefaultGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig()): GameWindow +class ContinuousRenderMode { + var continuousRenderMode: Boolean = true + var updatedSinceRender = KorAtomicInt(0) + + val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceRender.value > 0 + + fun updated() { + updatedSinceRender.incrementAndGet() + } + + fun restart() { + updatedSinceRender.value = 0 + } + + fun shouldRender(): Boolean { + if (mustTriggerRender) { + //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") + restart() + return true + } else { + return false + } + } +} + /** * A [GameWindow] represents a window, canvas or headless virtual frame where a game is displayed and can receive user events, it provides: * @@ -155,10 +179,7 @@ open class GameWindow : internal val gamepadEmitter: GamepadInfoEmitter = GamepadInfoEmitter(this) internal val gameWindowInputState = GameWindowInputState() - open val updatedSinceFrame = KorAtomicInt(0) - val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceFrame.value > 0 - var onContinuousRenderModeUpdated: ((Boolean) -> Unit)? = null - open var continuousRenderMode: Boolean by Delegates.observable(true) { prop, old, new -> onContinuousRenderModeUpdated?.invoke(new) } + open val continuousRenderMode = ContinuousRenderMode() /** Happens on the updater thread */ fun onUpdateEvent(block: (UpdateEvent) -> Unit): Closeable = onEvent(UpdateEvent, block) @@ -275,7 +296,7 @@ open class GameWindow : @Deprecated("") fun invalidatedView() = run { //println("invalidatedView") - updatedSinceFrame.incrementAndGet() + continuousRenderMode.updated() } @Deprecated("") fun handleContextLost() = run { gameWindowInputState.contextLost = true } diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index 19c84f8cb9..3aad4b020f 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -450,7 +450,9 @@ open class BrowserCanvasJsGameWindow( jsFrame = { step: Double -> window.requestAnimationFrame(jsFrame) // Execute first to prevent exceptions breaking the loop, not triggering again - dispatchNewRenderEvent() + if (continuousRenderMode.shouldRender()) { + dispatchNewRenderEvent() + } } jsFrame(0.0) } diff --git a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt index d22d1bcdb9..06ca939703 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtAGOpenglCanvas.kt @@ -5,7 +5,6 @@ import korlibs.datastructure.lock.* import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.graphics.gl.* -import korlibs.io.concurrent.atomic.* import korlibs.kgl.* import korlibs.korge.view.* import korlibs.logger.* @@ -31,8 +30,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP val LOGGER = Logger("AwtAGOpenglCanvas") } - var continuousRenderMode: Boolean = true - var updatedSinceFrame = KorAtomicInt(0) + val continuousRenderMode = ContinuousRenderMode() interface RendererThread { val doRenderInUIThread: Boolean @@ -54,9 +52,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP //requestFrame() //if (gameWindow.continuousRenderMode || gameWindow.updatedSinceFrame > 0) { - if (canvas.continuousRenderMode || canvas.updatedSinceFrame.value > 0) { - //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") - canvas.updatedSinceFrame.value = 0 + if (canvas.continuousRenderMode.shouldRender()) { SwingUtilities.invokeLater { canvas.requestFrame() } @@ -94,9 +90,8 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP continue } } - if (canvas.continuousRenderMode || canvas.updatedSinceFrame.value > 0) { + if (canvas.continuousRenderMode.shouldRender()) { //println("continuousRenderMode=$continuousRenderMode, updatedSinceFrame.value=${updatedSinceFrame.value}") - canvas.updatedSinceFrame.value = 0 fcanvas.renderGraphics(fcanvas.bufferStrategy.drawGraphics) } } @@ -148,7 +143,7 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP if (dl.doRenderInUIThread) { renderGraphics(g) } else { - updatedSinceFrame.incrementAndGet() + continuousRenderMode.updated() } } @@ -182,8 +177,6 @@ open class AwtAGOpenglCanvas : JPanel(GridLayout(1, 1), false.also { System.setP private val counter = TimeSampler(1.seconds) val renderFps: Int get() = counter.count - - private var contextLost: Boolean = false fun contextLost() { diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index b938ce2d78..50bfb0c9c8 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -44,8 +44,7 @@ open class AwtCanvasGameWindow constructor( override val width: Int get() = canvas.width override val height: Int get() = canvas.height - override var continuousRenderMode: Boolean by canvas::continuousRenderMode - override val updatedSinceFrame = canvas.updatedSinceFrame + override val continuousRenderMode = canvas.continuousRenderMode override var backgroundColor: RGBA get() = canvas.background.toRgba() diff --git a/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt b/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt index fcd90569f9..866da811a4 100644 --- a/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt +++ b/korge/test/common/korlibs/korge/view/ViewUpdatedTest.kt @@ -55,12 +55,12 @@ class ViewUpdatedTest { } fun assertUpdatedOnce(name: String = "block()", expectedCount: Int = 1, block: () -> Unit) { - views.updatedSinceFrame.value = 0 + views.continuousRenderMode.restart() block() - assertEquals(expectedCount, views.updatedSinceFrame.value, message = "Should update view::${name}") - views.updatedSinceFrame.value = 0 + assertEquals(expectedCount, views.continuousRenderMode.updatedSinceRender.value, message = "Should update view::${name}") + views.continuousRenderMode.restart() block() - assertEquals(0, views.updatedSinceFrame.value, message = "Shouldn't re-update view::${name}") + assertEquals(0, views.continuousRenderMode.updatedSinceRender.value, message = "Shouldn't re-update view::${name}") } } } From be5f121169ac09de3f77ba4c67be92af8323d686 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 18:39:49 +0200 Subject: [PATCH 43/66] More work --- .../audio/sound/AndroidNativeSoundProvider.kt | 1 + .../datastructure/_Datastructure_event.kt | 4 +- .../android/korlibs/render/KorgwActivity.kt | 7 +- .../korlibs/render/KorgwSurfaceView.kt | 82 +++---------------- korge/src/common/korlibs/korge/view/Views.kt | 1 - korge/src/common/korlibs/render/GameWindow.kt | 6 +- 6 files changed, 21 insertions(+), 80 deletions(-) diff --git a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt index 0986b99ba2..19b3dae8df 100644 --- a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt +++ b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt @@ -14,6 +14,7 @@ import kotlin.coroutines.cancellation.CancellationException actual val nativeSoundProvider: NativeSoundProvider by lazy { AndroidNativeSoundProvider() } +//class AndroidNativeSoundProvider : NativeSoundProviderNew() { class AndroidNativeSoundProvider : NativeSoundProvider() { companion object { val MAX_CHANNELS = 16 diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 69e1a18ff6..ae69026dd0 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -194,9 +194,9 @@ class SyncEventLoop( } } - fun runTasksForever() { + fun runTasksForever(runWhile: () -> Boolean = { true }) { running = true - while (running) { + while (running && runWhile()) { runTasksUntilEmpty() //NativeThread.gc(full = true) NativeThread.sleep(1.milliseconds) diff --git a/korge/src/android/korlibs/render/KorgwActivity.kt b/korge/src/android/korlibs/render/KorgwActivity.kt index 39a5b66d81..80c0dc3125 100644 --- a/korge/src/android/korlibs/render/KorgwActivity.kt +++ b/korge/src/android/korlibs/render/KorgwActivity.kt @@ -6,7 +6,7 @@ import android.os.* import android.util.* import android.view.* import android.view.KeyEvent -import android.widget.RelativeLayout +import android.widget.* import korlibs.event.* import korlibs.graphics.gl.* import korlibs.io.file.std.* @@ -41,9 +41,8 @@ abstract class KorgwActivity( init { activityWithResult.activity = this - gameWindow.onContinuousRenderModeUpdated = { - mGLView?.continuousRenderMode = it - } + //mGLView?.continuousRenderMode = true + gameWindow.continuousRenderMode.onContinuousRenderModeUpdated = { mGLView?.continuousRenderMode = it } } fun Bundle.toMap(): Map = this.keySet().associateWith { this.get(it) } diff --git a/korge/src/android/korlibs/render/KorgwSurfaceView.kt b/korge/src/android/korlibs/render/KorgwSurfaceView.kt index fbef6a2728..071f381105 100644 --- a/korge/src/android/korlibs/render/KorgwSurfaceView.kt +++ b/korge/src/android/korlibs/render/KorgwSurfaceView.kt @@ -8,18 +8,16 @@ import android.os.* import android.view.* import android.view.KeyEvent import korlibs.datastructure.* -import korlibs.datastructure.lock.* +import korlibs.datastructure.event.* +import korlibs.datastructure.thread.* import korlibs.event.* import korlibs.io.async.* -import korlibs.io.concurrent.atomic.* import korlibs.math.geom.* import korlibs.memory.* -import korlibs.time.* import javax.microedition.khronos.egl.* import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.egl.EGLDisplay import javax.microedition.khronos.opengles.* -import kotlin.concurrent.* // https://github.com/aosp-mirror/platform_frameworks_base/blob/e4df5d375df945b0f53a9c7cca83d37970b7ce64/opengl/java/android/opengl/GLSurfaceView.java open class KorgwSurfaceView constructor( @@ -53,10 +51,7 @@ open class KorgwSurfaceView constructor( //renderMode = RENDERMODE_WHEN_DIRTY } - var firstRender = false - private val renderLock = NonRecursiveLock() - - var updateTimerThread: Thread? = null + var updateTimerThread: NativeThread? = null override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -64,53 +59,14 @@ open class KorgwSurfaceView constructor( val display: Display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).getDefaultDisplay() val refreshRating: Float = display.refreshRate - updateTimerThread = thread(start = true, isDaemon = true, name = "korgw-updater") { - try { - while (true) { - val startTime = System.currentTimeMillis() - try { - if (!firstRender) { - requestRender() - } else { - //queueEvent { - renderLock { - //println("onAttachedToWindow.timer: continuousRenderMode=$continuousRenderMode") - if (!continuousRenderMode) { - val frameStartTime = runPreFrame() - try { - gameWindow.frame( - frameStartTime = frameStartTime, - doUpdate = true, - doRender = false - ) - //println(" --> gameWindow.mustTriggerRender=${gameWindow.mustTriggerRender}") - if (gameWindow.mustTriggerRender) { - requestRender() - } - } catch (e: Throwable) { - e.printStackTrace() - } - } - } - } - //} - } finally { - val endTime = System.currentTimeMillis() - val elapsedTime = (endTime - startTime).toInt() - // @TODO: Ideally this shouldn't be a timer, but a vsync-based callback, or at least use screen's hz) - //Thread.sleep(maxOf(4L, (1000L / refreshRating).toLong() - elapsedTime)) - Thread.sleep(1L) - } - } - } catch (e: InterruptedException) { - // Do nothing, just finish the loop - } + updateTimerThread = nativeThread(start = true, isDaemon = true, name = "korgw-updater") { thread -> + (gameWindow.eventLoop as SyncEventLoop).runTasksForever { thread.threadSuggestRunning } } } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - updateTimerThread?.interrupt() + updateTimerThread?.threadSuggestRunning = false updateTimerThread = null } @@ -127,30 +83,12 @@ open class KorgwSurfaceView constructor( println("OpenGL ES Version (actual): $clientVersion") } - private fun runPreFrame(): TimeSpan { - val frameStartTime = PerformanceCounter.reference - gameWindow.handleInitEventIfRequired() - gameWindow.handleReshapeEventIfRequired(0, 0, view.width, view.height) - gameController.runPreFrame(gameWindow) - return frameStartTime - } - override fun onDrawFrame(unused: GL10) { - renderLock { - try { - val frameStartTime = runPreFrame() - try { - if (!continuousRenderMode) { - gameWindow.updatedSinceFrame.incrementAndGet() - } - gameWindow.frame(frameStartTime = frameStartTime, doUpdate = continuousRenderMode, doRender = true) - onDraw(Unit) - } catch (e: Throwable) { - e.printStackTrace() - } - } finally { - firstRender = true + if (gameWindow.continuousRenderMode.shouldRender()) { + gameWindow.updateRenderLock { + gameWindow.dispatchNewRenderEvent() } + onDraw(Unit) } } diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 9dd671b407..2913cdf816 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -11,7 +11,6 @@ import korlibs.image.format.* import korlibs.inject.* import korlibs.io.* import korlibs.io.async.* -import korlibs.io.concurrent.atomic.* import korlibs.io.file.* import korlibs.io.file.std.* import korlibs.io.lang.* diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index e0ccd97c5e..4ae71313b5 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -17,12 +17,16 @@ import korlibs.math.geom.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* +import kotlin.properties.* /** Creates the default [GameWindow] for this platform. Typically, a window/frame or a fullscreen screen depending on the OS */ expect fun CreateDefaultGameWindow(config: GameWindowCreationConfig = GameWindowCreationConfig()): GameWindow class ContinuousRenderMode { - var continuousRenderMode: Boolean = true + var onContinuousRenderModeUpdated: ((Boolean) -> Unit)? = null + var continuousRenderMode: Boolean by Delegates.observable(true) { prop, old, new -> + onContinuousRenderModeUpdated?.invoke(new) + } var updatedSinceRender = KorAtomicInt(0) val mustTriggerRender: Boolean get() = continuousRenderMode || updatedSinceRender.value > 0 From 91c3af805273a66c7bfb79d98d44fafbe9871826 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 18:49:31 +0200 Subject: [PATCH 44/66] More work --- .../datastructure/_Datastructure_thread.kt | 1 + .../datastructure/thread/NativeThread.kt | 9 +++++-- .../datastructure/thread/NativeThread.kt | 3 +++ .../datastructure/thread/NativeThread.kt | 2 +- .../korlibs/render/DefaultGameWindowIos.kt | 24 ++++++++++++------- .../render/DefaultGameWindowIosTools.kt | 8 +++---- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt index c73e647344..39cc88c49b 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt @@ -16,6 +16,7 @@ expect class NativeThread(code: (NativeThread) -> Unit) : Extra { fun sleep(time: TimeSpan): Unit inline fun spinWhile(cond: () -> Boolean): Unit } + var threadSuggestRunning: Boolean var priority: Int var name: String? var isDaemon: Boolean diff --git a/korge-foundation/src/darwin/korlibs/datastructure/thread/NativeThread.kt b/korge-foundation/src/darwin/korlibs/datastructure/thread/NativeThread.kt index b41964ab12..f9288b3127 100644 --- a/korge-foundation/src/darwin/korlibs/datastructure/thread/NativeThread.kt +++ b/korge-foundation/src/darwin/korlibs/datastructure/thread/NativeThread.kt @@ -1,24 +1,28 @@ +@file:Suppress("PackageDirectoryMismatch") package korlibs.datastructure.thread +import korlibs.datastructure.* import korlibs.time.* import kotlinx.cinterop.* import platform.Foundation.* import kotlin.native.concurrent.* import kotlin.native.runtime.* -actual class NativeThread actual constructor(val code: () -> Unit) { +actual class NativeThread actual constructor(val code: (NativeThread) -> Unit) : Extra by Extra.Mixin() { actual var isDaemon: Boolean = false + actual var threadSuggestRunning: Boolean = true var worker: Worker? = null actual var priority: Int = 0 actual var name: String? = null actual fun start() { + threadSuggestRunning = true worker = Worker.start() worker?.executeAfter { try { - code() + code(this) } finally { worker?.requestTermination() worker = null @@ -28,6 +32,7 @@ actual class NativeThread actual constructor(val code: () -> Unit) { actual fun interrupt() { // No operation + threadSuggestRunning = false worker = null } diff --git a/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt b/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt index e03d479ef1..3c01c34862 100644 --- a/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt +++ b/korge-foundation/src/js/korlibs/datastructure/thread/NativeThread.kt @@ -6,12 +6,15 @@ import kotlin.time.* actual class NativeThread actual constructor(val code: (NativeThread) -> Unit) : Extra by Extra.Mixin() { actual var isDaemon: Boolean = false + actual var threadSuggestRunning = true actual fun start() { + threadSuggestRunning = true TODO() } actual fun interrupt() { + threadSuggestRunning = false TODO() } diff --git a/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt b/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt index c3270e7aba..0ff2acca63 100644 --- a/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt +++ b/korge-foundation/src/jvmAndroid/korlibs/datastructure/thread/NativeThread.kt @@ -13,7 +13,7 @@ private fun TimeSpan.toMillisNanos(): Pair { actual class NativeThread actual constructor(val code: (NativeThread) -> Unit) : Extra by Extra.Mixin() { val thread = Thread { code(this) } - var threadSuggestRunning = true + actual var threadSuggestRunning = true actual var priority: Int by thread::priority actual var name: String? by thread::name diff --git a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt index 30911b92c5..6a2e4768b1 100644 --- a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt +++ b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt @@ -1,6 +1,8 @@ package korlibs.render import korlibs.datastructure.* +import korlibs.datastructure.event.* +import korlibs.datastructure.thread.* import korlibs.event.* import korlibs.graphics.* import korlibs.graphics.gl.* @@ -33,7 +35,7 @@ open class IosTvosToolsImpl { open fun viewDidLoad(view: GLKView?) { } - open fun hapticFeedbackGenerate(kind: GameWindow.HapticFeedbackKind) { + open fun hapticFeedbackGenerate(kind: HapticFeedbackKind) { } } @@ -187,8 +189,6 @@ class ViewController( pressesHandler(KeyEvent.Type.UP, presses, withEvent) } - - //override fun pressesCancelled(presses: Set<*>, withEvent: UIPressesEvent?) { // super.pressesBegan(presses, withEvent) // pressesHandler(KeyEvent.Type.UP, presses, withEvent) @@ -216,6 +216,7 @@ class MyGLKViewController( var lastWidth = 0 var lastHeight = 0 val darwinGamePad = DarwinGameControllerNative() + var eventLoopThread: NativeThread? = null override fun viewDidLoad() { val view = this.view as? GLKView? @@ -228,6 +229,14 @@ class MyGLKViewController( lastHeight = 0 } + override fun viewWillAppear(animated: Boolean) { + eventLoopThread = nativeThread { thread -> (gameWindow.eventLoop as SyncEventLoop).runTasksForever { thread.threadSuggestRunning } } + } + + override fun viewWillDisappear(animated: Boolean) { + eventLoopThread?.threadSuggestRunning = false + } + override fun glkView(view: GLKView, drawInRect: CValue) { if (!initialized) { initialized = true @@ -246,10 +255,7 @@ class MyGLKViewController( logger.info { "dispatchInitEvent" } gameWindow.dispatchInitEvent() - gameWindow.queue { - - } - gameWindow.entry { + gameWindow.queueSuspend { logger.info { "Executing entry..." } this.entry() } @@ -274,7 +280,9 @@ class MyGLKViewController( } darwinGamePad.updateGamepads(gameWindow) - gameWindow.frame() + if (gameWindow.continuousRenderMode.shouldRender()) { + gameWindow.dispatchNewRenderEvent() + } } override fun didReceiveMemoryWarning() { diff --git a/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt b/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt index c6ed41cd63..3ac0283c6c 100644 --- a/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt +++ b/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt @@ -15,11 +15,11 @@ actual val iosTvosTools: IosTvosToolsImpl = object : IosTvosToolsImpl() { view?.multipleTouchEnabled = true } - override fun hapticFeedbackGenerate(kind: GameWindow.HapticFeedbackKind) { + override fun hapticFeedbackGenerate(kind: HapticFeedbackKind) { when (kind) { - GameWindow.HapticFeedbackKind.GENERIC -> uiSelectionFeedbackGenerator.selectionChanged() - GameWindow.HapticFeedbackKind.ALIGNMENT -> uiSelectionFeedbackGenerator.selectionChanged() - GameWindow.HapticFeedbackKind.LEVEL_CHANGE -> uiImpactFeedbackGenerator.impactOccurred() + HapticFeedbackKind.GENERIC -> uiSelectionFeedbackGenerator.selectionChanged() + HapticFeedbackKind.ALIGNMENT -> uiSelectionFeedbackGenerator.selectionChanged() + HapticFeedbackKind.LEVEL_CHANGE -> uiImpactFeedbackGenerator.impactOccurred() } } } From 7901b7e6a76130e202cc7249218bd80de2ec7aa3 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 23 Oct 2023 19:45:26 +0200 Subject: [PATCH 45/66] More work --- .../gradle/targets/android/AndroidConfig.kt | 4 +- .../gradle/targets/android/AndroidRun.kt | 15 +++++-- .../android/korlibs/io/file/std/VfsAndroid.kt | 10 +++-- .../src/common/korlibs/audio/sound/Sound.kt | 1 + .../render/DefaultGameWindowAndroid.kt | 5 ++- .../android/korlibs/render/KorgwActivity.kt | 35 ++++++++------- .../korlibs/render/KorgwSurfaceView.kt | 9 ++-- korge/src/common/korlibs/korge/Korge.kt | 1 + .../korlibs/korge/KorgeViewsConfigureInput.kt | 43 ++++++++++--------- korge/src/common/korlibs/korge/scene/Scene.kt | 2 +- korge/src/common/korlibs/korge/view/Views.kt | 2 +- korge/src/common/korlibs/render/GameWindow.kt | 4 ++ 12 files changed, 79 insertions(+), 52 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt index b75b433777..017a4eb4fb 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt @@ -36,7 +36,7 @@ class AndroidConfig( fun Project.toAndroidGenerated(isKorge: Boolean, info: AndroidInfo = AndroidInfo(null)): AndroidGenerated = AndroidGenerated( icons = if (isKorge) korge.iconProvider else KorgeIconProvider(File(korgeGradlePluginResources, "icons/korge.png"), File(korgeGradlePluginResources, "banners/korge.png")), ifNotExists = if (isKorge) korge.overwriteAndroidFiles else true, - androidPackageName = korge.id, + androidPackageName = AndroidGenerated.getAppId(this, isKorge), androidInit = korge.plugins.pluginExts.getAndroidInit() + info.androidInit, androidMsaa = if (isKorge) korge.androidMsaa else 4, fullscreen = if (isKorge) korge.fullscreen else true, @@ -52,7 +52,7 @@ fun Project.toAndroidGenerated(isKorge: Boolean, info: AndroidInfo = AndroidInfo buildDir = project.buildDir, ) -class AndroidGenerated constructor( +data class AndroidGenerated constructor( val icons: KorgeIconProvider, val ifNotExists: Boolean, val androidPackageName: String, diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt index ea213f7ccd..3da6d2706c 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt @@ -11,6 +11,7 @@ import java.io.* fun Project.installAndroidRun(dependsOnList: List, direct: Boolean, isKorge: Boolean) { val createAndroidManifest = tasks.createThis("createAndroidManifest") { + this.isKorge = isKorge } val hasKotlinMultiplatformExtension = project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null @@ -89,7 +90,11 @@ fun Project.installAndroidRun(dependsOnList: List, direct: Boolean, isKo val onlyRunAndroid = tasks.createTyped("onlyRunAndroid$suffixDevice$suffixDebug") { this.extra = extra - this.androidApplicationId = androidApplicationId + //this.androidApplicationId = androidApplicationId + } + + afterEvaluate { + onlyRunAndroid.androidApplicationId = AndroidGenerated.getAppId(project, isKorge) } tasks.createTyped("runAndroid$suffixDevice$suffixDebug") { @@ -131,6 +136,7 @@ open class AndroidCreateAndroidManifest : DefaultTask() { } @TaskAction fun run() { + //println("this.generated=${this.generated} : isKorge=$isKorge") val mainDir = this.generated.getAndroidManifestFile(isKorge).parentFile generated.writeResources(this.generated.getAndroidResFolder(isKorge)) generated.writeMainActivity(this.generated.getAndroidSrcFolder(isKorge)) @@ -150,19 +156,22 @@ open class OnlyRunAndroidTask : DefaultAndroidTask() { "-e", "sleepBeforeStart", "300", "-n", "$androidApplicationId/$androidApplicationId.MainActivity" ) - val pid = run { + val pid: String = run { val startTime = System.currentTimeMillis() while (true) { val currentTime = System.currentTimeMillis() val elapsedTime = currentTime - startTime try { - return@run execOutput(androidAdbPath, *extra, "shell", "pidof", androidApplicationId).trim() + val res = execOutput(androidAdbPath, *extra, "shell", "pidof", androidApplicationId).trim() + if (res.isEmpty()) error("PID not found") + return@run res } catch (e: Throwable) { //e.printStackTrace() Thread.sleep(10L) if (elapsedTime >= 5000L) throw e } } + error("Unexpected") } execLogger(androidAdbPath, *extra, "logcat", "--pid=$pid") } diff --git a/korge-core/src/android/korlibs/io/file/std/VfsAndroid.kt b/korge-core/src/android/korlibs/io/file/std/VfsAndroid.kt index cba1eadf33..f4770fc6ad 100644 --- a/korge-core/src/android/korlibs/io/file/std/VfsAndroid.kt +++ b/korge-core/src/android/korlibs/io/file/std/VfsAndroid.kt @@ -18,10 +18,14 @@ private var _vfsInitWithAndroidContextOnce: Boolean = false var absoluteCwd = File(".").absolutePath val tmpdir: String by lazy { System.getProperty("java.io.tmpdir") } -fun vfsInitWithAndroidContextOnce(context: Context) { - if (_vfsInitWithAndroidContextOnce) return +fun vfsInitWithAndroidContextOnce(context: Context?) { + if (_vfsInitWithAndroidContextOnce || context == null) return _vfsInitWithAndroidContextOnce = true - absoluteCwd = context.applicationInfo.dataDir + try { + absoluteCwd = context.applicationInfo.dataDir + } catch (e: Throwable) { + e.printStackTrace() + } } class AndroidDeferredVfs(private val generate: (Context) -> VfsFile) : Vfs.Proxy() { diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 46268a8a69..60bcae00eb 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -81,6 +81,7 @@ open class NativeSoundProvider() : Disposable { //open val audioFormats: AudioFormats = AudioFormats(WAV, MP3Decoder, OGG) open suspend fun createSound(vfs: Vfs, path: String, streaming: Boolean = false, props: AudioDecodingProps = AudioDecodingProps.DEFAULT): Sound { + println("createSound.coroutineContext: $coroutineContextKt") return if (streaming) { //val stream = vfs.file(path).open() //createStreamingSound(audioFormats.decodeStreamOrError(stream, props)) { diff --git a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt index d615863715..2ce3a04f74 100644 --- a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt +++ b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt @@ -21,16 +21,17 @@ interface AndroidContextHolder { } abstract class BaseAndroidGameWindow( - final override val androidContext: Context, + androidContext: Context, val config: GameWindowCreationConfig = GameWindowCreationConfig(), ) : GameWindow(), AndroidContextHolder { final override val dialogInterface = DialogInterfaceAndroid { androidContext } + final override val androidContext = androidContext override val androidContextAny: Any? = androidContext abstract val androidView: View override val pixelsPerInch: Double by TimedCache(1.seconds) { androidContext.resources.displayMetrics.xdpi.toDouble() } val _activity: Activity? get() = androidContext.activity - init { + override fun enrichCoroutineContext() { coroutineContext += AndroidCoroutineContext(androidContext) } diff --git a/korge/src/android/korlibs/render/KorgwActivity.kt b/korge/src/android/korlibs/render/KorgwActivity.kt index 80c0dc3125..fbb8f47d92 100644 --- a/korge/src/android/korlibs/render/KorgwActivity.kt +++ b/korge/src/android/korlibs/render/KorgwActivity.kt @@ -9,10 +9,10 @@ import android.view.KeyEvent import android.widget.* import korlibs.event.* import korlibs.graphics.gl.* +import korlibs.io.android.* import korlibs.io.file.std.* import korlibs.kgl.* import korlibs.memory.* -import kotlin.coroutines.* abstract class KorgwActivity( private val activityWithResult: ActivityWithResult.Mixin = ActivityWithResult.Mixin(), @@ -20,7 +20,10 @@ abstract class KorgwActivity( ) : Activity(), ActivityWithResult by activityWithResult //, DialogInterface.OnKeyListener { - var gameWindow: AndroidGameWindow = AndroidGameWindow(this, config) + val gameWindow: AndroidGameWindow by lazy { AndroidGameWindow(this, config).also { + //it.continuousRenderMode.onContinuousRenderModeUpdated = { mGLView?.continuousRenderMode = it } + mGLView?.continuousRenderMode = true + } } var mGLView: KorgwSurfaceView? = null var layout: RelativeLayout? = null lateinit var ag: AGOpengl @@ -42,7 +45,6 @@ abstract class KorgwActivity( init { activityWithResult.activity = this //mGLView?.continuousRenderMode = true - gameWindow.continuousRenderMode.onContinuousRenderModeUpdated = { mGLView?.continuousRenderMode = it } } fun Bundle.toMap(): Map = this.keySet().associateWith { this.get(it) } @@ -69,6 +71,7 @@ abstract class KorgwActivity( ag = AGOpengl(KmlGlAndroid { mGLView?.clientVersion ?: -1 }.checkedIf(checked = agCheck).logIf(log = false)) gameWindow.initializeAndroid() + gameWindow.coroutineContext += AndroidCoroutineContext(this) + gameWindow layout = RelativeLayout(this).also { layout -> layout.addView( @@ -81,20 +84,20 @@ abstract class KorgwActivity( } setContentView(layout) - mGLView!!.onDraw.once { - suspend { - activityMain() - }.startCoroutine(object : Continuation { - override val context: CoroutineContext get() = korlibs.io.android.AndroidCoroutineContext(this@KorgwActivity) + gameWindow - - override fun resumeWith(result: Result) { - println("KorgwActivity.activityMain completed! result=$result") - if (result.isFailure) { - result.exceptionOrNull()?.printStackTrace() - } - } - }) + gameWindow.queueSuspend { + activityMain() } + //suspend { + //}.startCoroutine(object : Continuation { + // override val context: CoroutineContext get() = korlibs.io.android.AndroidCoroutineContext(this@KorgwActivity) + gameWindow +// + // override fun resumeWith(result: Result) { + // println("KorgwActivity.activityMain completed! result=$result") + // if (result.isFailure) { + // result.exceptionOrNull()?.printStackTrace() + // } + // } + //}) } override fun onSaveInstanceState(outState: Bundle) { diff --git a/korge/src/android/korlibs/render/KorgwSurfaceView.kt b/korge/src/android/korlibs/render/KorgwSurfaceView.kt index 071f381105..418a1d92dd 100644 --- a/korge/src/android/korlibs/render/KorgwSurfaceView.kt +++ b/korge/src/android/korlibs/render/KorgwSurfaceView.kt @@ -84,10 +84,13 @@ open class KorgwSurfaceView constructor( } override fun onDrawFrame(unused: GL10) { - if (gameWindow.continuousRenderMode.shouldRender()) { - gameWindow.updateRenderLock { + //GLES20.glClearColor(1f, 0f, 1f, 1f) + //GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + //if (gameWindow.continuousRenderMode.shouldRender()) { + run { + //gameWindow.updateRenderLock { gameWindow.dispatchNewRenderEvent() - } + //} onDraw(Unit) } } diff --git a/korge/src/common/korlibs/korge/Korge.kt b/korge/src/common/korlibs/korge/Korge.kt index 8053fe31b0..8fec59cd30 100644 --- a/korge/src/common/korlibs/korge/Korge.kt +++ b/korge/src/common/korlibs/korge/Korge.kt @@ -140,6 +140,7 @@ data class Korge( } val config = this val creationConfig = GameWindowCreationConfig(multithreaded = config.multithreaded, fullscreen = config.fullscreen, title = config.title) + println("Korge.start: coroutineContext=$coroutineContext") val gameWindow = (config.gameWindow ?: coroutineContext[GameWindow] ?: CreateDefaultGameWindow(creationConfig)) gameWindow.configureKorge(config) { entry() diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index 918e6d083b..e805b1324d 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -17,13 +17,14 @@ import korlibs.time.* @KorgeInternal internal fun Views.prepareViewsBase( - eventDispatcher: GameWindow, + gameWindow: GameWindow, clearEachFrame: Boolean = true, bgcolor: RGBA = Colors.TRANSPARENT, forceRenderEveryFrame: Boolean = true, configInjector: Injector.() -> Unit = {}, ) { - eventDispatcher.coroutineContext += InjectorContext(injector) + gameWindow.coroutineContext += InjectorContext(injector) + gameWindow.enrichCoroutineContext() val views = this @@ -37,8 +38,8 @@ internal fun Views.prepareViewsBase( //injector.mapInstance(CoroutineContext::class, views.coroutineContext) // Maybe we shouldn't include this injector.mapPrototype(EmptyScene::class) { EmptyScene() } injector.mapInstance(TimeProvider::class, views.timeProvider) - injector.mapInstance(GameWindow::class, gameWindow) - views.debugViews = gameWindow.debug + injector.mapInstance(GameWindow::class, this.gameWindow) + views.debugViews = this.gameWindow.debug configInjector(views.injector) @@ -110,7 +111,7 @@ internal fun Views.prepareViewsBase( views.dispatch(mouseTouchEvent) } - eventDispatcher.onEvents(*MouseEvent.Type.ALL) { e -> + gameWindow.onEvents(*MouseEvent.Type.ALL) { e -> //println("MOUSE: $e") Korge.logger.trace { "eventDispatcher.addEventListener:$e" } val p = getRealXY(e.x.toDouble(), e.y.toDouble(), e.scaleCoords) @@ -142,26 +143,26 @@ internal fun Views.prepareViewsBase( views.dispatch(e) } - eventDispatcher.onEvents(*KeyEvent.Type.ALL) { e -> + gameWindow.onEvents(*KeyEvent.Type.ALL) { e -> Korge.logger.trace { "eventDispatcher.addEventListener:$e" } views.dispatch(e) } - eventDispatcher.onEvents(*GestureEvent.Type.ALL) { e -> + gameWindow.onEvents(*GestureEvent.Type.ALL) { e -> Korge.logger.trace { "eventDispatcher.addEventListener:$e" } views.dispatch(e) } - eventDispatcher.onEvents(*DropFileEvent.Type.ALL) { e -> views.dispatch(e) } - eventDispatcher.onEvent(ResumeEvent) { e -> + gameWindow.onEvents(*DropFileEvent.Type.ALL) { e -> views.dispatch(e) } + gameWindow.onEvent(ResumeEvent) { e -> views.dispatch(e) nativeSoundProvider.paused = false } - eventDispatcher.onEvent(PauseEvent) { e -> + gameWindow.onEvent(PauseEvent) { e -> views.dispatch(e) nativeSoundProvider.paused = true } - eventDispatcher.onEvent(StopEvent) { e -> views.dispatch(e) } - eventDispatcher.onEvent(DestroyEvent) { e -> + gameWindow.onEvent(StopEvent) { e -> views.dispatch(e) } + gameWindow.onEvent(DestroyEvent) { e -> try { views.dispatch(e) } finally { @@ -172,7 +173,7 @@ internal fun Views.prepareViewsBase( } val touchMouseEvent = MouseEvent() - eventDispatcher.onEvents(*TouchEvent.Type.ALL) { e -> + gameWindow.onEvents(*TouchEvent.Type.ALL) { e -> Korge.logger.trace { "eventDispatcher.addEventListener:$e" } input.updateTouches(e) @@ -226,17 +227,17 @@ internal fun Views.prepareViewsBase( input.updateConnectedGamepads() } - eventDispatcher.onEvents(*GamePadConnectionEvent.Type.ALL) { e -> + gameWindow.onEvents(*GamePadConnectionEvent.Type.ALL) { e -> Korge.logger.trace { "eventDispatcher.addEventListener:$e" } views.dispatch(e) } - eventDispatcher.onEvent(GamePadUpdateEvent) { e -> + gameWindow.onEvent(GamePadUpdateEvent) { e -> gamepadUpdated(e) views.dispatch(e) } - eventDispatcher.onEvent(ReshapeEvent) { e -> + gameWindow.onEvent(ReshapeEvent) { e -> //try { throw Exception() } catch (e: Throwable) { e.printStackTrace() } //println("eventDispatcher.addEventListener: ${ag.backWidth}x${ag.backHeight} : ${e.width}x${e.height}") //println("resized. ${ag.backWidth}, ${ag.backHeight}") @@ -244,9 +245,9 @@ internal fun Views.prepareViewsBase( } //println("eventDispatcher.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) : ${views.nativeWidth}x${views.nativeHeight}") - eventDispatcher.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) + gameWindow.dispatch(ReshapeEvent(0, 0, views.nativeWidth, views.nativeHeight)) - eventDispatcher.onEvent(ReloadEvent) { views.dispatch(it) } + gameWindow.onEvent(ReloadEvent) { views.dispatch(it) } views.clearEachFrame = clearEachFrame views.clearColor = bgcolor @@ -262,17 +263,17 @@ internal fun Views.prepareViewsBase( } val stopwatch = Stopwatch(views.timeProvider) - gameWindow.onUpdateEvent { + this.gameWindow.onUpdateEvent { //println("stopwatch.elapsed=${stopwatch.elapsed}") views.update(stopwatch.getElapsedAndRestart()) } var cachedFrameSize = SizeInt(0, 0) - gameWindow.onRenderEvent { + this.gameWindow.onRenderEvent { //println("RENDER") //println("gameWindow.size=${gameWindow.frameSize}") - val frameSize = gameWindow.scaledFrameSize + val frameSize = this.gameWindow.scaledFrameSize if (cachedFrameSize != frameSize) { cachedFrameSize = frameSize views.resized(frameSize.width, frameSize.height) diff --git a/korge/src/common/korlibs/korge/scene/Scene.kt b/korge/src/common/korlibs/korge/scene/Scene.kt index ec497e8775..21ed6db80e 100644 --- a/korge/src/common/korlibs/korge/scene/Scene.kt +++ b/korge/src/common/korlibs/korge/scene/Scene.kt @@ -54,7 +54,7 @@ abstract class Scene : InjectorAsyncDependency, ViewsContainer, CoroutineScope, val root get() = _sceneViewContainer protected val cancellables = CancellableGroup() - override val coroutineContext by lazy { views.coroutineContext + InjectorContext(injector) + Job(views.coroutineContext[Job.Key]) } + override val coroutineContext by lazy { views.coroutineContext + gameWindow.coroutineContext + Job(views.coroutineContext[Job.Key]) } val sceneView: SContainer by lazy { createSceneView(sceneContainer.size).apply { _sceneViewContainer += this diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index 2913cdf816..f3f9b0dbf6 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -37,7 +37,7 @@ import kotlin.coroutines.* * It contains information about the [coroutineContext], the [gameWindow], the [injector], the [input] * and contains a reference to the [root] [Stage] view. */ -class Views( +class Views constructor( val gameWindow: GameWindow, override val coroutineContext: CoroutineContext = gameWindow.coroutineDispatcher, val ag: AG = gameWindow.ag, diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index 4ae71313b5..baa151d8e1 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -132,8 +132,12 @@ open class GameWindow : ensureInitialized() } } + var coroutineContext: CoroutineContext = EmptyCoroutineContext + open fun enrichCoroutineContext() { + } + fun getCoroutineDispatcherWithCurrentContext(coroutineContext: CoroutineContext): CoroutineContext = coroutineContext + coroutineDispatcher fun getCoroutineDispatcherWithCurrentContext(): CoroutineContext = getCoroutineDispatcherWithCurrentContext(coroutineContext) From af55612ab82fd22be1ebada9a252d865672a982a Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 08:44:48 +0200 Subject: [PATCH 46/66] More work --- .../korge/gradle/util/SpawnExtension.kt | 7 +- .../audio/sound/AndroidNativeSoundProvider.kt | 259 +++++++----------- .../audio/sound/PlatformAudioOutput.kt | 20 +- .../src/common/korlibs/audio/sound/Sound.kt | 8 +- .../audio/sound/backend/JvmWaveOutImpl.kt | 18 +- .../audio/sound/CoreAudioSoundProvider.kt | 6 +- .../audio/sound/HtmlNativeSoundProvider.kt | 4 +- .../audio/sound/NativeAudioStreamJs.kt | 6 +- .../audio/sound/backend/CoreAudioImpl.kt | 8 +- .../sound/impl/awt/AwtNativeSoundProvider.kt | 6 +- .../datastructure/_Datastructure_event.kt | 9 +- .../korlibs/render/KorgwSurfaceView.kt | 17 +- korge/src/common/korlibs/event/Events.kt | 9 +- .../korlibs/graphics/AGTextureDrawer.kt | 19 +- .../src/common/korlibs/korge/KorgeHeadless.kt | 2 +- .../korlibs/korge/tests/ViewsForTesting.kt | 2 +- .../common/korlibs/render/GameWindowExt.kt | 14 +- .../korlibs/render/DefaultGameWindowIos.kt | 2 +- .../js/korlibs/render/DefaultGameWindowJs.kt | 2 +- .../jvm/korlibs/render/awt/AwtGameWindow.kt | 2 +- 20 files changed, 184 insertions(+), 236 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt index 2d405fbe01..c9b22abd44 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt @@ -5,11 +5,14 @@ import java.io.* open class SpawnExtension { open fun spawn(dir: File, command: List) { - ProcessBuilder(command).redirectErrorStream(true).directory(dir).start() + ProcessBuilder(command).inheritIO().redirectErrorStream(true).directory(dir).start() } open fun execLogger(projectDir: File, vararg params: String) { println("EXEC: ${params.joinToString(" ")}") - ProcessBuilder(*params).redirectErrorStream(true).directory(projectDir).start().waitFor() + val process = ProcessBuilder(*params).redirectErrorStream(true).directory(projectDir).start() + val reader = process.inputStream.reader() + reader.forEachLine { println(it) } + process.waitFor() } open fun execOutput(projectDir: File, vararg params: String): String { diff --git a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt index 19b3dae8df..4fa4186af4 100644 --- a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt +++ b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt @@ -3,23 +3,13 @@ package korlibs.audio.sound import android.content.* import android.media.* import android.os.* -import korlibs.datastructure.* -import korlibs.datastructure.lock.* +import korlibs.datastructure.thread.* import korlibs.io.android.* -import korlibs.io.async.* -import korlibs.time.* -import kotlinx.coroutines.* import kotlin.coroutines.* -import kotlin.coroutines.cancellation.CancellationException actual val nativeSoundProvider: NativeSoundProvider by lazy { AndroidNativeSoundProvider() } -//class AndroidNativeSoundProvider : NativeSoundProviderNew() { -class AndroidNativeSoundProvider : NativeSoundProvider() { - companion object { - val MAX_CHANNELS = 16 - } - +class AndroidNativeSoundProvider : NativeSoundProviderNew() { override val target: String = "android" private var audioManager: AudioManager? = null @@ -27,12 +17,10 @@ class AndroidNativeSoundProvider : NativeSoundProvider() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) audioManager!!.generateAudioSessionId() else -1 } - //val audioSessionId get() = audioManager!!.generateAudioSessionId() - //private val activeOutputs = LinkedHashSet() - private val threadPool = Pool { id -> - //Console.info("Creating AudioThread[$id]") - AudioThread(this, id = id).also { it.isDaemon = true }.also { it.start() } + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + ensureAudioManager(coroutineContext) + return AndroidNewPlatformAudioOutput(this, coroutineContext, channels, frequency, gen) } override var paused: Boolean = false @@ -43,177 +31,116 @@ class AndroidNativeSoundProvider : NativeSoundProvider() { } } - class AudioThread(val provider: AndroidNativeSoundProvider, var freq: Int = 44100, val id: Int = -1) : Thread() { - var props: SoundProps = DummySoundProps - val deque = AudioSamplesDeque(2) - val lock = Lock() - @Volatile - var running = true - - init { - this.isDaemon = true + fun ensureAudioManager(coroutineContext: CoroutineContext) { + if (audioManager == null) { + val ctx = coroutineContext[AndroidCoroutineContext.Key]?.context ?: error("Can't find the Android Context on the CoroutineContext. Must call withAndroidContext first") + audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager } + } + + class AndroidNewPlatformAudioOutput( + val provider: AndroidNativeSoundProvider, + coroutineContext: CoroutineContext, + channels: Int, + frequency: Int, + gen: (AudioSamples) -> Unit + ) : NewPlatformAudioOutput(coroutineContext, channels, frequency, gen) { + var thread: NativeThread? = null + + override fun internalStart() { + thread = nativeThread(isDaemon = true) { thread -> + //val bufferSamples = 4096 + val bufferSamples = 1024 + + val atChannelSize = Short.SIZE_BYTES * channels * bufferSamples + val atChannel = if (channels >= 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO + val atMode = AudioTrack.MODE_STREAM + val at = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + AudioTrack( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME) + //.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .build(), + AudioFormat.Builder() + .setChannelMask(atChannel) + .setSampleRate(frequency) + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .build(), + atChannelSize, + atMode, + provider.audioSessionId + ) + } else { + @Suppress("DEPRECATION") + AudioTrack( + AudioManager.STREAM_MUSIC, + frequency, + atChannel, + AudioFormat.ENCODING_PCM_16BIT, + atChannelSize, + atMode + ) + } + if (at.state == AudioTrack.STATE_UNINITIALIZED) { + System.err.println("Audio track was not initialized correctly frequency=$frequency, bufferSamples=$bufferSamples") + } + + val buffer = AudioSamples(channels, bufferSamples) + at.play() + + var lastVolL = Float.NaN + var lastVolR = Float.NaN - override fun run() { - val bufferSamples = 4096 - - val at = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - AudioTrack( - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_GAME) - //.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .build(), - AudioFormat.Builder() - .setChannelMask(AudioFormat.CHANNEL_IN_STEREO) - .setSampleRate(freq) - .setEncoding(AudioFormat.ENCODING_PCM_16BIT) - .build(), - 2 * 2 * bufferSamples, - AudioTrack.MODE_STREAM, - provider.audioSessionId - ) - } else { - @Suppress("DEPRECATION") - AudioTrack( - AudioManager.STREAM_MUSIC, - freq, - AudioFormat.CHANNEL_OUT_STEREO, - AudioFormat.ENCODING_PCM_16BIT, - 2 * 2 * bufferSamples, - AudioTrack.MODE_STREAM - ) - } - if (at.state == AudioTrack.STATE_UNINITIALIZED) { - System.err.println("Audio track was not initialized correctly freq=$freq, bufferSamples=$bufferSamples") - } - //if (at.state == AudioTrack.STATE_INITIALIZED) at.play() - while (running) { try { - val temp = AudioSamplesInterleaved(2, bufferSamples) - //val tempEmpty = ShortArray(1024) - var paused = true - var lastVolume = Float.NaN - while (running) { - //println("Android sound thread running = ${currentThreadId} ${currentThreadName}") - - if (provider.paused) { - at.stop() - //at.pause() - //at.flush() - while (provider.paused && running) { - //(provider as java.lang.Object).wait(10_000L) - Thread.sleep(250L) - } + while (thread.threadSuggestRunning) { + if (this.paused) { + at.pause() + Thread.sleep(20L) + continue + } else { at.play() } - val readCount = lock { deque.read(temp) } - if (at.state == AudioTrack.STATE_UNINITIALIZED) { - Thread.sleep(50L) - continue - } - if (readCount > 0) { - if (paused) { - //println("[KORAU] Resume $id") - paused = false - at.play() + when (at.state) { + AudioTrack.STATE_UNINITIALIZED -> { + Thread.sleep(20L) } - //println("AUDIO CHUNK: $readCount : ${temp.data.toList()}") - if (at.state == AudioTrack.STATE_INITIALIZED) { - at.playbackRate = freq + AudioTrack.STATE_INITIALIZED -> { + at.playbackRate = frequency if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - at.playbackParams.speed = props.pitch.toFloat() + at.playbackParams.speed = this.pitch.toFloat() } - val vol = props.volume.toFloat() - if (lastVolume != vol) { + val volL = this.volumeForChannel(0).toFloat() + val volR = this.volumeForChannel(1).toFloat() + if (lastVolL != volL || lastVolR != volR) { + lastVolL = volL + lastVolR = volR if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - at.setVolume(vol) + at.setVolume(volL) } else { - @Suppress("DEPRECATION") - at.setStereoVolume(vol, vol) + at.setStereoVolume(volL, volR) } } - lastVolume = vol - at.write(temp.data, 0, readCount * 2) - } - } else { - //at.write(tempEmpty, 0, tempEmpty.size) - if (!paused) { - //println("[KORAU] Stop $id") - //at.flush() - at.stop() - paused = true + + genSafe(buffer) + val bufferInterleaved = buffer.interleaved() + at.write(bufferInterleaved.data, 0, bufferInterleaved.data.size) } - Thread.sleep(2L) } } - } catch (e: Throwable) { - e.printStackTrace() } finally { - //println("[KORAU] Completed $id") - try { - at.stop() - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - e.printStackTrace() - } + at.flush() + at.stop() + at.release() } - } - at.release() - } - } - - fun ensureAudioManager(coroutineContext: CoroutineContext) { - if (audioManager == null) { - val ctx = coroutineContext[AndroidCoroutineContext.Key]?.context ?: error("Can't find the Android Context on the CoroutineContext. Must call withAndroidContext first") - audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager - } - } - - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { - ensureAudioManager(coroutineContext) - return AndroidPlatformAudioOutput(coroutineContext, freq, this) - } - - class AndroidPlatformAudioOutput(coroutineContext: CoroutineContext, frequency: Int, val provider: AndroidNativeSoundProvider) : PlatformAudioOutput(coroutineContext, frequency) { - private var started = false - internal var thread: AudioThread? = null - private val threadDeque get() = thread?.deque - - override val availableSamples: Int get() = threadDeque?.availableRead ?: 0 - override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - //println("AndroidPlatformAudioOutput.add") - while (thread == null) delay(10.milliseconds) - while (threadDeque!!.availableRead >= 44100) delay(1.milliseconds) - - thread!!.lock { threadDeque!!.write(samples, offset, size) } - } - - override fun start() { - if (started) return - started = true - launchImmediately(coroutineContext) { - while (provider.threadPool.totalItemsInUse >= MAX_CHANNELS) { - delay(10.milliseconds) - } - thread = provider.threadPool.alloc() - thread?.props = this - thread?.freq = frequency - threadDeque?.clear() + //val temp = AudioSamplesInterleaved(2, bufferSamples) } - //provider.activeOutputs += this } - override fun stop() { - if (!started) return - //provider.activeOutputs -= this - started = false - if (thread != null) { - provider.threadPool.free(thread!!) - } + override fun internalStop() { + thread?.threadSuggestRunning = false thread = null } } diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index fe6b1367a6..22450a020d 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -11,11 +11,11 @@ import kotlin.coroutines.* open class NewPlatformAudioOutput( val coroutineContext: CoroutineContext, - val nchannels: Int, + val channels: Int, val frequency: Int, private val gen: (AudioSamples) -> Unit, ) : Disposable, SoundProps { - val onCancel = coroutineContext.onCancel { stop() } + var onCancel: Cancellable? = null var paused: Boolean = false private val lock = Lock() @@ -33,8 +33,20 @@ open class NewPlatformAudioOutput( override var pitch: Double = 1.0 override var volume: Double = 1.0 override var panning: Double = 0.0 - open fun start() = Unit - open fun stop() = Unit + + protected open fun internalStart() = Unit + protected open fun internalStop() = Unit + + fun start() { + stop() + onCancel = coroutineContext.onCancel { stop() } + internalStart() + } + fun stop() { + onCancel?.cancel() + onCancel = null + internalStop() + } final override fun dispose() = stop() } diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 60bcae00eb..2757486dd8 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -22,8 +22,8 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS override val target: String get() = parent.target override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = parent.createPlatformAudioOutput(coroutineContext, freq) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = - parent.createNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = + parent.createNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = parent.createSound(data, streaming, props, name) @@ -58,9 +58,9 @@ open class NativeSoundProvider() : Disposable { open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput { return PlatformAudioOutput(coroutineContext, freq) } - open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { //println("createNewPlatformAudioOutput: ${this::class}") - return NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) + return NewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } suspend fun createPlatformAudioOutput(freq: Int = 44100): PlatformAudioOutput = createPlatformAudioOutput(coroutineContextKt, freq) diff --git a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt index 60b7c60b54..fe1786dbf6 100644 --- a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt +++ b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt @@ -14,10 +14,10 @@ val jvmWaveOutNativeSoundProvider: NativeSoundProvider? by lazy { class JvmWaveOutNativeSoundProvider : NativeSoundProviderNew() { override fun createNewPlatformAudioOutput( coroutineContext: CoroutineContext, - nchannels: Int, - freq: Int, + channels: Int, + frequency: Int, gen: (AudioSamples) -> Unit - ): NewPlatformAudioOutput = JvmWaveOutNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) + ): NewPlatformAudioOutput = JvmWaveOutNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } /* @@ -102,7 +102,7 @@ class JvmWaveOutNewPlatformAudioOutput( private var handle: FFIPointer? = null private var headers = emptyArray() - override fun start() { + override fun internalStart() { //println("TRYING TO START") if (running) return //println("STARTED") @@ -112,13 +112,13 @@ class JvmWaveOutNewPlatformAudioOutput( val arena = this val handlePtr = allocBytes(8).typed() val freq = frequency - val blockAlign = (nchannels * Short.SIZE_BYTES) + val blockAlign = (channels * Short.SIZE_BYTES) val format = WAVEFORMATEX(allocBytes(WAVEFORMATEX().size)).also { format -> format.wFormatTag = WINMM.WAVE_FORMAT_PCM.toShort() - format.nChannels = nchannels.toShort() // 2? + format.nChannels = channels.toShort() // 2? format.nSamplesPerSec = freq.toInt() format.wBitsPerSample = Short.SIZE_BITS.toShort() // 16 - format.nBlockAlign = ((nchannels * Short.SIZE_BYTES).toShort()) + format.nBlockAlign = ((channels * Short.SIZE_BYTES).toShort()) format.nAvgBytesPerSec = freq * blockAlign format.cbSize = format.size.toShort() } @@ -128,7 +128,7 @@ class JvmWaveOutNewPlatformAudioOutput( handle = handlePtr[0] //println("handle=$handle") - headers = Array(4) { WaveHeader(it, handle, 1024, nchannels, arena) } + headers = Array(4) { WaveHeader(it, handle, 1024, channels, arena) } try { while (running) { @@ -160,7 +160,7 @@ class JvmWaveOutNewPlatformAudioOutput( } } - override fun stop() { + override fun internalStop() { running = false //println("STOPPING") } diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index c9b4d0f6e8..d738fc28b2 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -32,7 +32,7 @@ class CoreAudioNativeSoundProvider : NativeSoundProviderNew() { //override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps): NativeSound = AVFoundationNativeSoundNoStream(CoroutineScope(coroutineContext), audioFormats.decode(data)) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, freq, nchannels, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, frequency, channels, gen) } class CoreAudioNewPlatformAudioOutput( @@ -53,10 +53,10 @@ class CoreAudioNewPlatformAudioOutput( } } } - override fun start() { + override fun internalStart() { generator.start() } - override fun stop() { + override fun internalStop() { generator.dispose() } } diff --git a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt index 23114cb0d5..3334291c8f 100644 --- a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt +++ b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt @@ -15,8 +15,8 @@ class HtmlNativeSoundProvider : NativeSoundProviderNew() { HtmlSimpleSound.ensureUnlockStart() } - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { - return JsNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + return JsNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = diff --git a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt index c2f982ace5..73effb9fc3 100644 --- a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt +++ b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt @@ -31,10 +31,10 @@ class JsNewPlatformAudioOutput( private var startPromise: Cancellable? = null - override fun start() { + override fun internalStart() { if (nodeRunning) return startPromise = HtmlSimpleSound.callOnUnlocked { - node = HtmlSimpleSound.ctx?.createScriptProcessor(1024, nchannels, 2) + node = HtmlSimpleSound.ctx?.createScriptProcessor(1024, channels, 2) node?.onaudioprocess = { e -> val nchannels = e.outputBuffer.numberOfChannels val outChannels = Array(nchannels) { e.outputBuffer.getChannelData(it) } @@ -56,7 +56,7 @@ class JsNewPlatformAudioOutput( missingDataCount = 0 } - override fun stop() { + override fun internalStop() { if (!nodeRunning) return startPromise?.cancel() this.node?.disconnect() diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt index 58d262cbd2..1cb67191c2 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt @@ -30,7 +30,7 @@ private val jnaNewCoreAudioCallback by lazy { AudioQueueNewOutputCallback { inUserData, inAQ, inBuffer -> try { val output = newAudioOutputsById[(inUserData?.address ?: 0L).toLong()] ?: return@AudioQueueNewOutputCallback 0 - val nchannels = output.nchannels + val nchannels = output.channels //val tone = AudioTone.generate(1.seconds, 41000.0) val queue = AudioQueueBuffer(inBuffer) @@ -96,7 +96,7 @@ private class JvmCoreAudioNewPlatformAudioOutput( var queue: Pointer? = null - override fun start() { + override fun internalStart() { stop() newAudioOutputsById[id] = this @@ -108,7 +108,7 @@ private class JvmCoreAudioNewPlatformAudioOutput( format.mFormatID = CoreAudioKit.kAudioFormatLinearPCM format.mFormatFlags = CoreAudioKit.kLinearPCMFormatFlagIsSignedInteger or CoreAudioKit.kAudioFormatFlagIsPacked format.mBitsPerChannel = (8 * Short.SIZE_BYTES) - format.mChannelsPerFrame = nchannels + format.mChannelsPerFrame = channels format.mBytesPerFrame = (Short.SIZE_BYTES * format.mChannelsPerFrame) format.mFramesPerPacket = 1 format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket @@ -139,7 +139,7 @@ private class JvmCoreAudioNewPlatformAudioOutput( } } - override fun stop() { + override fun internalStop() { completed = true if (queue != null) { diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt index 8b097002d3..a7361902e8 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt @@ -33,7 +33,7 @@ class JvmNewPlatformAudioOutput( private fun bytesToSamples(bytes: Int): Int = bytes / BYTES_PER_SAMPLE private fun samplesToBytes(samples: Int): Int = samples * BYTES_PER_SAMPLE - override fun start() { + override fun internalStart() { //println("TRYING TO START") if (nativeThread?.threadSuggestRunning == true) return @@ -42,7 +42,7 @@ class JvmNewPlatformAudioOutput( // SAMPLE -> Short, FRAME -> nchannels * SAMPLE nativeThread = nativeThread(isDaemon = true) { it.threadSuggestRunning = true - val nchannels = this.nchannels + val nchannels = this.channels val format = AudioFormat(frequency.toFloat(), Short.SIZE_BITS, nchannels, true, false) //val format = AudioFormat(44100.toFloat(), Short.SIZE_BITS, nchannels, true, false) //val line = AudioSystem.getSourceDataLine(format) @@ -72,7 +72,7 @@ class JvmNewPlatformAudioOutput( } } - override fun stop() { + override fun internalStop() { nativeThread?.threadSuggestRunning = false nativeThread = null //println("STOPPING") diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index ae69026dd0..65d9736708 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -17,7 +17,9 @@ interface EventLoop : Closeable { } fun EventLoop.setInterval(time: Frequency, task: () -> Unit): Closeable = setInterval(time.timeSpan, task) -abstract class BaseEventLoop : EventLoop +abstract class BaseEventLoop : EventLoop { + val runLock = Lock() +} class SyncEventLoop( /** precise=true will have better precision at the cost of more CPU-usage (busy waiting) */ @@ -121,7 +123,10 @@ class SyncEventLoop( private inline fun runCatchingExceptions(block: () -> Unit) { try { - block() + //runLock { + run { + block() + } } catch (e: Throwable) { uncatchedExceptionHandler(e) } diff --git a/korge/src/android/korlibs/render/KorgwSurfaceView.kt b/korge/src/android/korlibs/render/KorgwSurfaceView.kt index 418a1d92dd..79415dd274 100644 --- a/korge/src/android/korlibs/render/KorgwSurfaceView.kt +++ b/korge/src/android/korlibs/render/KorgwSurfaceView.kt @@ -11,7 +11,6 @@ import korlibs.datastructure.* import korlibs.datastructure.event.* import korlibs.datastructure.thread.* import korlibs.event.* -import korlibs.io.async.* import korlibs.math.geom.* import korlibs.memory.* import javax.microedition.khronos.egl.* @@ -32,7 +31,6 @@ open class KorgwSurfaceView constructor( val view = this - val onDraw = Signal() val requestedClientVersion by lazy { getVersionFromPackageManager(context) } var clientVersion = -1 var continuousRenderMode: Boolean @@ -84,15 +82,22 @@ open class KorgwSurfaceView constructor( } override fun onDrawFrame(unused: GL10) { - //GLES20.glClearColor(1f, 0f, 1f, 1f) - //GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + val ag = gameWindow.ag + + ag.mainFrameBuffer.setSize(width, height) + + //GLES20.glClearColor(1f, 0f, .5f, 1f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) //if (gameWindow.continuousRenderMode.shouldRender()) { run { //gameWindow.updateRenderLock { - gameWindow.dispatchNewRenderEvent() + gameWindow.dispatchRenderEvent() //} - onDraw(Unit) } + + //println("gameWindow.bufferWidth=${gameWindow.bufferWidth},${gameWindow.bufferHeight}, ag.mainFrameBuffer=${ag.mainFrameBuffer.width}, ${ag.mainFrameBuffer.height}, ${ag.mainFrameBuffer.fullWidth}, ${ag.mainFrameBuffer.fullHeight}") + + //ag.textureDrawer.drawXY(ag.mainFrameBuffer, null, 20, 20, 100, 100) + } override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) { diff --git a/korge/src/common/korlibs/event/Events.kt b/korge/src/common/korlibs/event/Events.kt index 021fa2ce6a..8e0374371d 100644 --- a/korge/src/common/korlibs/event/Events.kt +++ b/korge/src/common/korlibs/event/Events.kt @@ -487,14 +487,7 @@ data class FullScreenEvent(var fullscreen: Boolean = false) : TypedEvent(RenderEvent) { companion object : RenderEvent(), EventType - var update: Boolean = true - var render: Boolean = true - fun copyFrom(other: RenderEvent) { - this.update = other.update - this.render = other.render - } - - override fun toString(): String = "RenderEvent(update=$update, render=$render)" + override fun toString(): String = "RenderEvent" } open class InitEvent : TypedEvent(InitEvent) { diff --git a/korge/src/common/korlibs/graphics/AGTextureDrawer.kt b/korge/src/common/korlibs/graphics/AGTextureDrawer.kt index 9e40e85dbe..80afa2ae8f 100644 --- a/korge/src/common/korlibs/graphics/AGTextureDrawer.kt +++ b/korge/src/common/korlibs/graphics/AGTextureDrawer.kt @@ -29,6 +29,15 @@ class AGTextureDrawer(val ag: AG) { SET(out, texture2D(u_Tex, v_Tex["xy"])) } }) + val PROGRAM_DEBUG = Program(VertexShader { + DefaultShaders { + SET(out, vec4(a_Pos, 0f.lit, 1f.lit)) + } + }, FragmentShader { + DefaultShaders { + SET(out, vec4(1f, 0f, 1f, 1f)) + } + }) } val ref = AGProgramWithUniforms(PROGRAM) @@ -44,7 +53,7 @@ class AGTextureDrawer(val ag: AG) { return this.convertRange(0f, 1f, -1f, +1f) } - fun drawXY(frameBuffer: AGFrameBuffer, tex: AGTexture, x: Int, y: Int, width: Int, height: Int) { + fun drawXY(frameBuffer: AGFrameBuffer, tex: AGTexture?, x: Int, y: Int, width: Int, height: Int) { val fwidth = frameBuffer.fullWidth.toFloat() val fheight = frameBuffer.fullHeight.toFloat() val left = x.toFloat() / fwidth @@ -54,10 +63,12 @@ class AGTextureDrawer(val ag: AG) { return draw(frameBuffer, tex, left.convertToM1P1(), top.convertToM1P1(), right.convertToM1P1(), bottom.convertToM1P1()) } - fun draw(frameBuffer: AGFrameBuffer, tex: AGTexture, left: Float = -1f, top: Float = +1f, right: Float = +1f, bottom: Float = -1f) { + fun draw(frameBuffer: AGFrameBuffer, tex: AGTexture?, left: Float = -1f, top: Float = +1f, right: Float = +1f, bottom: Float = -1f) { //tex.upload(Bitmap32(32, 32) { x, y -> Colors.RED }) //uniforms.set(DefaultShaders.u_Tex, tex) - textureUnits.set(DefaultShaders.u_Tex, tex) + if (tex != null) { + textureUnits.set(DefaultShaders.u_Tex, tex) + } val texLeft = 0f val texRight = +1f @@ -78,7 +89,7 @@ class AGTextureDrawer(val ag: AG) { ag.draw( frameBuffer, vertexData = vertexData, - program = PROGRAM, + program = if (tex != null) PROGRAM else PROGRAM_DEBUG, drawType = AGDrawType.TRIANGLE_STRIP, indexType = AGIndexType.NONE, vertexCount = 4, diff --git a/korge/src/common/korlibs/korge/KorgeHeadless.kt b/korge/src/common/korlibs/korge/KorgeHeadless.kt index 7ca105b2c5..a6b311fc63 100644 --- a/korge/src/common/korlibs/korge/KorgeHeadless.kt +++ b/korge/src/common/korlibs/korge/KorgeHeadless.kt @@ -33,7 +33,7 @@ object KorgeHeadless { nativeThread(name = "HeadlessGameWindow-syncEventLoop") { syncEventLoop.runTasksForever() } eventLoop.setInterval(60.hz.timeSpan) { (ag as? AGOpengl?)?.context?.set() - this@HeadlessGameWindow.dispatchNewRenderEvent() + this@HeadlessGameWindow.dispatchRenderEvent() } } diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index 2491efc6c9..df77a2b1f8 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -376,7 +376,7 @@ open class ViewsForTesting( //println("SIMULATE: $frameTime") time += currentFrameTime gameWindow.dispatchUpdateEvent() - gameWindow.dispatchNewRenderEvent() + gameWindow.dispatchRenderEvent() simulatedFrames++ val now = PerformanceCounter.reference val elapsedSinceLastDelay = now - lastDelay diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index 67f4eb05a3..75e3a117e1 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -157,19 +157,11 @@ fun GameWindow.dispatchDisposeEvent() = dispatch(events.disposeEvent.reset()) fun GameWindow.dispatchUpdateEvent() { updateRenderLock { dispatch(events.updateEvent) } } -fun GameWindow.dispatchRenderEvent(update: Boolean = true, render: Boolean = true) { - updateRenderLock { - //renderEventLoop.runAvailableNextTasks(runTimers = false) - dispatch(events.renderEvent.reset { - this.update = update - this.render = render - }) - } + +fun GameWindow.dispatchRenderEvent() { + updateRenderLock { dispatch(events.renderEvent) } ag.finish() } -fun GameWindow.dispatchNewRenderEvent() { - dispatchRenderEvent(update = false, render = true) -} fun GameWindow.dispatchDropfileEvent(type: DropFileEvent.Type, files: List?) = dispatch(events.dropFileEvent.reset { this.type = type this.files = files diff --git a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt index 6a2e4768b1..f248e94afd 100644 --- a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt +++ b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt @@ -281,7 +281,7 @@ class MyGLKViewController( darwinGamePad.updateGamepads(gameWindow) if (gameWindow.continuousRenderMode.shouldRender()) { - gameWindow.dispatchNewRenderEvent() + gameWindow.dispatchRenderEvent() } } diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index 3aad4b020f..390de5e57e 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -451,7 +451,7 @@ open class BrowserCanvasJsGameWindow( jsFrame = { step: Double -> window.requestAnimationFrame(jsFrame) // Execute first to prevent exceptions breaking the loop, not triggering again if (continuousRenderMode.shouldRender()) { - dispatchNewRenderEvent() + dispatchRenderEvent() } } jsFrame(0.0) diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 50bfb0c9c8..03fc16df31 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -63,7 +63,7 @@ open class AwtCanvasGameWindow constructor( } canvas.doRender = { //it.clear(it.mainFrameBuffer, Colors.RED) - dispatchNewRenderEvent() + dispatchRenderEvent() } canvas.canvas.registerMouseEvents(this) canvas.canvas.registerKeyEvents(this) From 4a8a90c41abbe279e4ff856d1f9da88bc668d3e0 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 09:12:18 +0200 Subject: [PATCH 47/66] More work --- .../korge/gradle/targets/android/Android.kt | 4 ++-- .../gradle/targets/android/AndroidRun.kt | 21 ++++++++++++++++++- .../korge/gradle/util/SpawnExtension.kt | 12 ++++++++--- .../render/DefaultGameWindowAndroid.kt | 5 +++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/Android.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/Android.kt index 88dabce98d..080c9a0064 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/Android.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/Android.kt @@ -10,8 +10,8 @@ interface AndroidSdkProvider { val androidSdkPath: String val spawnExt: SpawnExtension - fun execLogger(vararg params: String) { - spawnExt.execLogger(projectDir, *params) + fun execLogger(vararg params: String, filter: Process.(line: String) -> String? = { it }) { + spawnExt.execLogger(projectDir, *params, filter = filter) } fun execOutput(vararg params: String): String { diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt index 3da6d2706c..24f23beec3 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidRun.kt @@ -3,6 +3,7 @@ package korlibs.korge.gradle.targets.android import korlibs.korge.gradle.* import korlibs.korge.gradle.targets.* import korlibs.korge.gradle.util.* +import korlibs.korge.gradle.util.AnsiEscape.Companion.color import org.gradle.api.* import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.dsl.* @@ -173,7 +174,25 @@ open class OnlyRunAndroidTask : DefaultAndroidTask() { } error("Unexpected") } - execLogger(androidAdbPath, *extra, "logcat", "--pid=$pid") + val EXIT_MESSAGE = "InputTransport: Input channel destroyed:" + execLogger(androidAdbPath, *extra, "logcat", "--pid=$pid") { + if (it.contains(EXIT_MESSAGE)) { + println("Found EXIT_MESSAGE=$EXIT_MESSAGE") + this.destroy() + } + val parts = it.split(" ", limit = 5) + val end = parts.getOrElse(4) { " " }.trimStart() + val color = when { + end.startsWith('V') -> null + end.startsWith('D') -> AnsiEscape.Color.BLUE + end.startsWith('I') -> AnsiEscape.Color.GREEN + end.startsWith('W') -> AnsiEscape.Color.YELLOW + end.startsWith('E') -> AnsiEscape.Color.RED + else -> AnsiEscape.Color.WHITE + } + //println("parts=$parts") + if (color != null) it.color(color) else it + } } } diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt index c9b22abd44..1aa9dd7c41 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/util/SpawnExtension.kt @@ -7,11 +7,17 @@ open class SpawnExtension { open fun spawn(dir: File, command: List) { ProcessBuilder(command).inheritIO().redirectErrorStream(true).directory(dir).start() } - open fun execLogger(projectDir: File, vararg params: String) { + open fun execLogger(projectDir: File, vararg params: String, filter: Process.(line: String) -> String? = { it }) { println("EXEC: ${params.joinToString(" ")}") val process = ProcessBuilder(*params).redirectErrorStream(true).directory(projectDir).start() - val reader = process.inputStream.reader() - reader.forEachLine { println(it) } + try { + val reader = process.inputStream.reader() + reader.forEachLine { + filter(process, it)?.let { println(it) } + } + } catch (e: IOException) { + // Steam closed is fine if the filter closed the process + } process.waitFor() } diff --git a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt index 2ce3a04f74..13b404e8b5 100644 --- a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt +++ b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt @@ -89,6 +89,11 @@ abstract class BaseAndroidGameWindow( } } + override fun close(exitCode: Int) { + super.close(exitCode) + androidContext.activity?.finishAffinity() + } + //fun queueAndWait(callback: () -> T): T { // var result: Result? = null // val semaphore = java.util.concurrent.Semaphore(0) From a8aaf77a894e24974f679e00df7223b607c0f877 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 09:18:46 +0200 Subject: [PATCH 48/66] Some cleanups --- .../src/common/korlibs/audio/sound/Sound.kt | 14 +- .../audio/sound/NativeSoundProviderJvm.kt | 2 - .../sound/{impl/alsa => backend}/Alsa.kt | 13 +- .../awt => backend}/AwtNativeSoundProvider.kt | 2 +- .../jvm/korlibs/audio/sound/impl/jna/AL.kt | 281 ------------ .../audio/sound/impl/jna/JnaSoundProvider.kt | 423 ------------------ .../sound/impl/jogamp/JogampSoundProvider.kt | 182 -------- .../audio/sound/internal/jvm/JvmTools.kt | 17 - 8 files changed, 11 insertions(+), 923 deletions(-) rename korge-core/src/jvm/korlibs/audio/sound/{impl/alsa => backend}/Alsa.kt (99%) rename korge-core/src/jvm/korlibs/audio/sound/{impl/awt => backend}/AwtNativeSoundProvider.kt (98%) delete mode 100644 korge-core/src/jvm/korlibs/audio/sound/impl/jna/AL.kt delete mode 100644 korge-core/src/jvm/korlibs/audio/sound/impl/jna/JnaSoundProvider.kt delete mode 100644 korge-core/src/jvm/korlibs/audio/sound/impl/jogamp/JogampSoundProvider.kt delete mode 100644 korge-core/src/jvm/korlibs/audio/sound/internal/jvm/JvmTools.kt diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 2757486dd8..dee44fce92 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -44,10 +44,6 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS open class NativeSoundProviderNew : NativeSoundProvider() { final override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) - - override suspend fun createNonStreamingSound(data: AudioData, name: String): Sound { - return super.createNonStreamingSound(data, name) - } } open class NativeSoundProvider() : Disposable { @@ -55,16 +51,16 @@ open class NativeSoundProvider() : Disposable { open var paused: Boolean = false - open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput { - return PlatformAudioOutput(coroutineContext, freq) - } + @Deprecated("") + open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput = PlatformAudioOutput(coroutineContext, freq) + @Deprecated("") + suspend fun createPlatformAudioOutput(freq: Int = 44100): PlatformAudioOutput = createPlatformAudioOutput(coroutineContextKt, freq) + open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { //println("createNewPlatformAudioOutput: ${this::class}") return NewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } - suspend fun createPlatformAudioOutput(freq: Int = 44100): PlatformAudioOutput = createPlatformAudioOutput(coroutineContextKt, freq) - suspend fun createNewPlatformAudioOutput(nchannels: Int, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = createNewPlatformAudioOutput(coroutineContextKt, nchannels, freq, gen) open suspend fun createSound(data: ByteArray, streaming: Boolean = false, props: AudioDecodingProps = AudioDecodingProps.DEFAULT, name: String = "Unknown"): Sound { diff --git a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt index d964912c27..a85477f4fd 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt @@ -1,8 +1,6 @@ package korlibs.audio.sound import korlibs.audio.sound.backend.* -import korlibs.audio.sound.impl.awt.* -import korlibs.audio.sound.impl.jna.OpenALException import korlibs.io.time.* import korlibs.logger.* import korlibs.platform.* diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/alsa/Alsa.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt similarity index 99% rename from korge-core/src/jvm/korlibs/audio/sound/impl/alsa/Alsa.kt rename to korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt index b085b254f3..ee88a52bc8 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/alsa/Alsa.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt @@ -1,15 +1,12 @@ -package korlibs.audio.sound.impl.alsa +package korlibs.audio.sound.backend -import korlibs.datastructure.lock.* -import korlibs.time.* +import com.sun.jna.* import korlibs.audio.sound.* -import korlibs.io.async.* -import korlibs.io.file.std.* -import com.sun.jna.Memory -import com.sun.jna.Native -import com.sun.jna.Pointer +import korlibs.datastructure.lock.* import korlibs.io.annotations.* +import korlibs.io.file.std.* import korlibs.math.* +import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt similarity index 98% rename from korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt rename to korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt index a7361902e8..f89a7cbb11 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/awt/AwtNativeSoundProvider.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt @@ -1,4 +1,4 @@ -package korlibs.audio.sound.impl.awt +package korlibs.audio.sound.backend import korlibs.audio.sound.* import korlibs.datastructure.thread.* diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/jna/AL.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/jna/AL.kt deleted file mode 100644 index fe84c6b76b..0000000000 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/jna/AL.kt +++ /dev/null @@ -1,281 +0,0 @@ -package korlibs.audio.sound.impl.jna - -import com.sun.jna.* -import korlibs.io.annotations.* -import korlibs.io.lang.* -import korlibs.io.time.* -import korlibs.logger.* -import korlibs.platform.Platform -import java.io.* -import java.net.* -import java.nio.* - -@Suppress("unused") -@Keep -object AL { - private val logger = Logger("AL") - - @JvmStatic external fun alDopplerFactor(value: Float) - @JvmStatic external fun alDopplerVelocity(value: Float) - @JvmStatic external fun alSpeedOfSound(value: Float) - @JvmStatic external fun alDistanceModel(distanceModel: Int) - @JvmStatic external fun alEnable(capability: Int) - @JvmStatic external fun alDisable(capability: Int) - @JvmStatic external fun alIsEnabled(capability: Int): Boolean - @JvmStatic external fun alGetString(param: Int): String - @JvmStatic external fun alGetBooleanv(param: Int, values: BooleanArray) - @JvmStatic external fun alGetIntegerv(param: Int, values: IntArray) - @JvmStatic external fun alGetFloatv(param: Int, values: FloatArray) - @JvmStatic external fun alGetDoublev(param: Int, values: DoubleArray) - @JvmStatic external fun alGetBoolean(param: Int): Boolean - @JvmStatic external fun alGetInteger(param: Int): Int - @JvmStatic external fun alGetFloat(param: Int): Float - @JvmStatic external fun alGetDouble(param: Int): Double - @JvmStatic external fun alGetError(): Int - @JvmStatic external fun alIsExtensionPresent(extname: String): Boolean - @JvmStatic external fun alGetProcAddress(fname: String): Pointer - @JvmStatic external fun alGetEnumValue(ename: String): Int - @JvmStatic external fun alListenerf(param: Int, value: Float) - @JvmStatic external fun alListener3f(param: Int, value1: Float, value2: Float, value3: Float) - @JvmStatic external fun alListenerfv(param: Int, values: FloatArray) - @JvmStatic external fun alListeneri(param: Int, value: Int) - @JvmStatic external fun alListener3i(param: Int, value1: Int, value2: Int, value3: Int) - @JvmStatic external fun alListeneriv(param: Int, values: IntArray) - @JvmStatic external fun alGetListenerf(param: Int, value: FloatArray) - @JvmStatic external fun alGetListener3f(param: Int, value1: FloatArray, value2: FloatArray, value3: FloatArray) - @JvmStatic external fun alGetListenerfv(param: Int, values: FloatArray) - @JvmStatic external fun alGetListeneri(param: Int, value: IntArray) - @JvmStatic external fun alGetListener3i(param: Int, value1: IntArray, value2: IntArray, value3: IntArray) - @JvmStatic external fun alGetListeneriv(param: Int, values: IntArray) - @JvmStatic external fun alGenSources(n: Int, sources: IntArray) - @JvmStatic external fun alDeleteSources(n: Int, sources: IntArray) - @JvmStatic external fun alIsSource(source: Int): Boolean - @JvmStatic external fun alSourcef(source: Int, param: Int, value: Float) - @JvmStatic external fun alSource3f(source: Int, param: Int, value1: Float, value2: Float, value3: Float) - @JvmStatic external fun alSourcefv(source: Int, param: Int, values: FloatArray) - @JvmStatic external fun alSourcei(source: Int, param: Int, value: Int) - @JvmStatic external fun alSource3i(source: Int, param: Int, value1: Int, value2: Int, value3: Int) - @JvmStatic external fun alSourceiv(source: Int, param: Int, values: IntArray) - @JvmStatic external fun alGetSourcef(source: Int, param: Int, value: FloatArray) - @JvmStatic external fun alGetSource3f(source: Int, param: Int, value1: FloatArray, value2: FloatArray, value3: FloatArray) - @JvmStatic external fun alGetSourcefv(source: Int, param: Int, values: FloatArray) - @JvmStatic external fun alGetSourcei(source: Int, param: Int, value: IntArray) - @JvmStatic external fun alGetSource3i(source: Int, param: Int, value1: IntArray, value2: IntArray, value3: IntArray) - @JvmStatic external fun alGetSourceiv(source: Int, param: Int, values: IntArray) - @JvmStatic external fun alSourcePlayv(n: Int, sources: IntArray) - @JvmStatic external fun alSourceStopv(n: Int, sources: IntArray) - @JvmStatic external fun alSourceRewindv(n: Int, sources: IntArray) - @JvmStatic external fun alSourcePausev(n: Int, sources: IntArray) - @JvmStatic external fun alSourcePlay(source: Int) - @JvmStatic external fun alSourceStop(source: Int) - @JvmStatic external fun alSourceRewind(source: Int) - @JvmStatic external fun alSourcePause(source: Int) - @JvmStatic external fun alSourceQueueBuffers(source: Int, nb: Int, buffers: IntArray) - @JvmStatic external fun alSourceUnqueueBuffers(source: Int, nb: Int, buffers: IntArray) - @JvmStatic external fun alGenBuffers(n: Int, buffers: IntArray) - @JvmStatic external fun alDeleteBuffers(n: Int, buffers: IntArray) - @JvmStatic external fun alIsBuffer(buffer: Int): Boolean - @JvmStatic external fun alBufferData(buffer: Int, format: Int, data: Buffer?, size: Int, freq: Int) - @JvmStatic external fun alBufferf(buffer: Int, param: Int, value: Float) - @JvmStatic external fun alBuffer3f(buffer: Int, param: Int, value1: Float, value2: Float, value3: Float) - @JvmStatic external fun alBufferfv(buffer: Int, param: Int, values: FloatArray) - @JvmStatic external fun alBufferi(buffer: Int, param: Int, value: Int) - @JvmStatic external fun alBuffer3i(buffer: Int, param: Int, value1: Int, value2: Int, value3: Int) - @JvmStatic external fun alBufferiv(buffer: Int, param: Int, values: IntArray) - @JvmStatic external fun alGetBufferf(buffer: Int, param: Int, value: FloatArray) - @JvmStatic external fun alGetBuffer3f(buffer: Int, param: Int, value1: FloatArray, value2: FloatArray, value3: FloatArray) - @JvmStatic external fun alGetBufferfv(buffer: Int, param: Int, values: FloatArray) - @JvmStatic external fun alGetBufferi(buffer: Int, param: Int, value: IntArray) - @JvmStatic external fun alGetBuffer3i(buffer: Int, param: Int, value1: IntArray, value2: IntArray, value3: IntArray) - @JvmStatic external fun alGetBufferiv(buffer: Int, param: Int, values: IntArray) - - private val tempF = FloatArray(1) - private val tempI = IntArray(1) - - fun alGenBuffer(): Int = tempI.also { alGenBuffers(1, it) }[0] - fun alGenSource(): Int = tempI.also { alGenSources(1, it) }[0] - fun alDeleteBuffer(buffer: Int) { alDeleteBuffers(1, tempI.also { it[0] = buffer }) } - fun alDeleteSource(buffer: Int) { alDeleteSources(1, tempI.also { it[0] = buffer }) } - fun alGetSourcef(source: Int, param: Int): Float = tempF.also { alGetSourcef(source, param, it) }[0] - fun alGetSourcei(source: Int, param: Int): Int = tempI.also { alGetSourcei(source, param, it) }[0] - fun alGetSourceState(source: Int): Int = alGetSourcei(source, AL.AL_SOURCE_STATE) - - const val AL_NONE = 0 - const val AL_FALSE = 0 - const val AL_TRUE = 1 - const val AL_SOURCE_RELATIVE = 0x202 - const val AL_CONE_INNER_ANGLE = 0x1001 - const val AL_CONE_OUTER_ANGLE = 0x1002 - const val AL_PITCH = 0x1003 - const val AL_POSITION = 0x1004 - const val AL_DIRECTION = 0x1005 - const val AL_VELOCITY = 0x1006 - const val AL_LOOPING = 0x1007 - const val AL_BUFFER = 0x1009 - const val AL_GAIN = 0x100A - const val AL_MIN_GAIN = 0x100D - const val AL_MAX_GAIN = 0x100E - const val AL_ORIENTATION = 0x100F - const val AL_SOURCE_STATE = 0x1010 - const val AL_INITIAL = 0x1011 - const val AL_PLAYING = 0x1012 - const val AL_PAUSED = 0x1013 - const val AL_STOPPED = 0x1014 - const val AL_BUFFERS_QUEUED = 0x1015 - const val AL_BUFFERS_PROCESSED = 0x1016 - const val AL_REFERENCE_DISTANCE = 0x1020 - const val AL_ROLLOFF_FACTOR = 0x1021 - const val AL_CONE_OUTER_GAIN = 0x1022 - const val AL_MAX_DISTANCE = 0x1023 - const val AL_SEC_OFFSET = 0x1024 - const val AL_SAMPLE_OFFSET = 0x1025 - const val AL_BYTE_OFFSET = 0x1026 - const val AL_SOURCE_TYPE = 0x1027 - const val AL_STATIC = 0x1028 - const val AL_STREAMING = 0x1029 - const val AL_UNDETERMINED = 0x1030 - const val AL_FORMAT_MONO8 = 0x1100 - const val AL_FORMAT_MONO16 = 0x1101 - const val AL_FORMAT_STEREO8 = 0x1102 - const val AL_FORMAT_STEREO16 = 0x1103 - const val AL_FREQUENCY = 0x2001 - const val AL_BITS = 0x2002 - const val AL_CHANNELS = 0x2003 - const val AL_SIZE = 0x2004 - const val AL_UNUSED = 0x2010 - const val AL_PENDING = 0x2011 - const val AL_PROCESSED = 0x2012 - const val AL_NO_ERROR = 0 - const val AL_INVALID_NAME = 0xA001 - const val AL_INVALID_ENUM = 0xA002 - const val AL_INVALID_VALUE = 0xA003 - const val AL_INVALID_OPERATION = 0xA004 - const val AL_OUT_OF_MEMORY = 0xA005 - const val AL_VENDOR = 0xB001 - const val AL_VERSION = 0xB002 - const val AL_RENDERER = 0xB003 - const val AL_EXTENSIONS = 0xB004 - const val AL_DOPPLER_FACTOR = 0xC000 - const val AL_DOPPLER_VELOCITY = 0xC001 - const val AL_SPEED_OF_SOUND = 0xC003 - const val AL_DISTANCE_MODEL = 0xD000 - const val AL_INVERSE_DISTANCE = 0xD001 - const val AL_INVERSE_DISTANCE_CLAMPED = 0xD002 - const val AL_LINEAR_DISTANCE = 0xD003 - const val AL_LINEAR_DISTANCE_CLAMPED = 0xD004 - const val AL_EXPONENT_DISTANCE = 0xD005 - const val AL_EXPONENT_DISTANCE_CLAMPED = 0xD006 - - // ALC - - @JvmStatic external fun alcCreateContext(device: Pointer, attrlist: IntArray?): Pointer? - @JvmStatic external fun alcMakeContextCurrent(context: Pointer?): Boolean - @JvmStatic external fun alcProcessContext(context: Pointer) - @JvmStatic external fun alcSuspendContext(context: Pointer) - @JvmStatic external fun alcDestroyContext(context: Pointer) - @JvmStatic external fun alcGetCurrentContext(): Pointer - @JvmStatic external fun alcGetContextsDevice(context: Pointer): Pointer - @JvmStatic external fun alcOpenDevice(devicename: String?): Pointer? - @JvmStatic external fun alcCloseDevice(device: Pointer): Boolean - @JvmStatic external fun alcGetError(device: Pointer): Int - @JvmStatic external fun alcIsExtensionPresent(device: Pointer, extname: String): Boolean - @JvmStatic external fun alcGetProcAddress(device: Pointer, funcname: String): Pointer - @JvmStatic external fun alcGetEnumValue(device: Pointer, enumname: String): Int - @JvmStatic external fun alcGetString(device: Pointer, param: Int): String - @JvmStatic external fun alcGetIntegerv(device: Pointer, param: Int, size: Int, values: IntArray) - @JvmStatic external fun alcCaptureOpenDevice(devicename: String, frequency: Int, format: Int, buffersize: Int): Pointer - @JvmStatic external fun alcCaptureCloseDevice(device: Pointer): Boolean - @JvmStatic external fun alcCaptureStart(device: Pointer) - @JvmStatic external fun alcCaptureStop(device: Pointer) - @JvmStatic external fun alcCaptureSamples(device: Pointer, buffer: Buffer, samples: Int) - - const val ALC_FALSE = 0 - const val ALC_TRUE = 1 - const val ALC_FREQUENCY = 0x1007 - const val ALC_REFRESH = 0x1008 - const val ALC_SYNC = 0x1009 - const val ALC_MONO_SOURCES = 0x1010 - const val ALC_STEREO_SOURCES = 0x1011 - const val ALC_NO_ERROR = 0 - const val ALC_INVALID_DEVICE = 0xA001 - const val ALC_INVALID_CONTEXT = 0xA002 - const val ALC_INVALID_ENUM = 0xA003 - const val ALC_INVALID_VALUE = 0xA004 - const val ALC_OUT_OF_MEMORY = 0xA005 - const val ALC_MAJOR_VERSION = 0x1000 - const val ALC_MINOR_VERSION = 0x1001 - const val ALC_ATTRIBUTES_SIZE = 0x1002 - const val ALC_ALL_ATTRIBUTES = 0x1003 - const val ALC_DEFAULT_DEVICE_SPECIFIER = 0x1004 - const val ALC_DEVICE_SPECIFIER = 0x1005 - const val ALC_EXTENSIONS = 0x1006 - const val ALC_EXT_CAPTURE = 1 - const val ALC_CAPTURE_DEVICE_SPECIFIER = 0x310 - const val ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER = 0x311 - const val ALC_CAPTURE_SAMPLES = 0x312 - const val ALC_ENUMERATE_ALL_EXT = 1 - const val ALC_DEFAULT_ALL_DEVICES_SPECIFIER = 0x1012 - const val ALC_ALL_DEVICES_SPECIFIER = 0x1013 - - internal var loaded = false - - init { - try { - if (nativeOpenALLibraryPath == null) error("Can't get OpenAL library") - traceTime("OpenAL Native.register") { - Native.register(nativeOpenALLibraryPath) - } - loaded = true - } catch (e: Throwable) { - logger.error { "Failed to initialize OpenAL: arch=$arch, OS.rawName=${Platform.rawOsName}, nativeOpenALLibraryPath=$nativeOpenALLibraryPath, message=${e.message}" } - //e.printStackTrace() - } - } -} - -val nativeOpenALLibraryPath: String? by lazy { - Environment["OPENAL_LIB_PATH"]?.let { path -> - return@lazy path - } - if (Environment["KORAU_JVM_DUMMY_SOUND"] == "true") { - return@lazy null - } - when { - Platform.isMac -> "OpenAL" // Mac already includes the OpenAL library - Platform.isLinux -> "libopenal.so.1" - Platform.isWindows -> "soft_oal.dll" - else -> { - println(" - Unknown/Unsupported OS") - null - } - } -} - -private val arch by lazy { System.getProperty("os.arch").toLowerCase() } -private val alClassLoader by lazy { AL::class.java.classLoader } -private fun getNativeFileURL(path: String): URL? = alClassLoader.getResource(path) -private fun getNativeFile(path: String): ByteArray = getNativeFileURL(path)?.readBytes() ?: error("Can't find '$path'") -private fun getNativeFileLocalPath(path: String): String { - val tempDir = File(System.getProperty("java.io.tmpdir")) - //val tempFile = File.createTempFile("libopenal_", ".${File(path).extension}") - val tempFile = File(tempDir, "korau_openal.${File(path).extension}") - - val expectedSize = getNativeFileURL(path)?.openStream()?.use { it.available().toLong() } - - if (!tempFile.exists() || tempFile.length() != expectedSize) { - try { - tempFile.writeBytes(getNativeFile(path)) - } catch (e: Throwable) { - e.printStackTrace() - } - } - return tempFile.absolutePath -} - -internal inline fun runCatchingAl(block: () -> T): T? { - val result = runCatching { block() } - if (result.isFailure) { - result.exceptionOrNull()?.printStackTrace() - } - return result.getOrNull() -} diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/jna/JnaSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/jna/JnaSoundProvider.kt deleted file mode 100644 index c143e20980..0000000000 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/jna/JnaSoundProvider.kt +++ /dev/null @@ -1,423 +0,0 @@ -package korlibs.audio.sound.impl.jna - -import com.sun.jna.* -import korlibs.audio.internal.* -import korlibs.audio.sound.* -import korlibs.datastructure.* -import korlibs.io.async.* -import korlibs.logger.* -import korlibs.math.* -import korlibs.time.* -import kotlinx.coroutines.* -import java.nio.* -import kotlin.coroutines.* -import kotlin.math.* - -class OpenALException(message: String) : RuntimeException(message) - -class JnaOpenALNativeSoundProvider : NativeSoundProvider() { - companion object { - val MAX_AVAILABLE_SOURCES = 100 - } - - val device = (AL.alcOpenDevice(null) ?: throw OpenALException("Can't open OpenAL device")) - val context = (AL.alcCreateContext(device, null) ?: throw OpenALException("Can't get OpenAL context")) - - val sourcePool = Pool { - alGenSourceAndInitialize() - //.also { println("CREATED OpenAL source $it") } - } - val bufferPool = Pool { - AL.alGenBuffer() - //.also { println("CREATED OpenAL buffer $it") } - } - - fun makeCurrent() { - AL.alcMakeContextCurrent(context) - } - - fun unmakeCurrent() { - AL.alcMakeContextCurrent(Pointer.NULL) - } - - init { - makeCurrent() - - AL.alListener3f(AL.AL_POSITION, 0f, 0f, 1.0f) - checkAlErrors("alListener3f", 0) - AL.alListener3f(AL.AL_VELOCITY, 0f, 0f, 0f) - checkAlErrors("alListener3f", 0) - AL.alListenerfv(AL.AL_ORIENTATION, floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f)) - checkAlErrors("alListenerfv", 0) - - java.lang.Runtime.getRuntime().addShutdownHook(Thread { - unmakeCurrent() - AL.alcDestroyContext(context) - AL.alcCloseDevice(device) - }) - } - - override suspend fun createNonStreamingSound(data: AudioData, name: String): Sound { - if (!AL.loaded) return super.createNonStreamingSound(data, name) - return OpenALSoundNoStream(this, coroutineContext, data, name = name) - } - - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { - if (!AL.loaded) return super.createPlatformAudioOutput(coroutineContext, freq) - return OpenALPlatformAudioOutput(this, coroutineContext, freq) - } -} - -class OpenALPlatformAudioOutput( - val provider: JnaOpenALNativeSoundProvider, - coroutineContext: CoroutineContext, - freq: Int, -) : PlatformAudioOutput(coroutineContext, freq) { - var source = 0 - val sourceProv = JnaSoundPropsProvider { source } - override var availableSamples: Int = 0 - - override var pitch: Double by sourceProv::pitch - override var volume: Double by sourceProv::volume - override var panning: Double by sourceProv::panning - - //val source - - //alSourceQueueBuffers - - //val buffersPool = Pool(6) { all.alGenBuffer() } - //val buffers = IntArray(32) - //val buffers = IntArray(6) - - init { - start() - } - - override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - //println("OpenALPlatformAudioOutput.add") - availableSamples += samples.totalSamples - try { - provider.makeCurrent() - val tempBuffers = IntArray(1) - ensureSource() - while (true) { - //val buffer = al.alGetSourcei(source, AL.AL_BUFFER) - //val sampleOffset = al.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - val processed = AL.alGetSourcei(source, AL.AL_BUFFERS_PROCESSED) - val queued = AL.alGetSourcei(source, AL.AL_BUFFERS_QUEUED) - val total = processed + queued - val state = AL.alGetSourceState(source) - val playing = state == AL.AL_PLAYING - - //println("buffer=$buffer, processed=$processed, queued=$queued, state=$state, playing=$playing, sampleOffset=$sampleOffset") - //println("Samples.add") - - if (processed <= 0 && total >= 6) { - delay(10.milliseconds) - continue - } - - if (total < 6) { - AL.alGenBuffers(1, tempBuffers) - checkAlErrors("alGenBuffers", tempBuffers[0]) - //println("alGenBuffers: ${tempBuffers[0]}") - } else { - AL.alSourceUnqueueBuffers(source, 1, tempBuffers) - checkAlErrors("alSourceUnqueueBuffers", source) - //println("alSourceUnqueueBuffers: ${tempBuffers[0]}") - } - //println("samples: $samples - $offset, $size") - //al.alBufferData(tempBuffers[0], samples.copyOfRange(offset, offset + size), frequency, panning, volume) - AL.alBufferData(tempBuffers[0], samples.copyOfRange(offset, offset + size), frequency, panning) - checkAlErrors("alBufferData", tempBuffers[0]) - AL.alSourceQueueBuffers(source, 1, tempBuffers) - checkAlErrors("alSourceQueueBuffers", tempBuffers[0]) - - //val gain = al.alGetSourcef(source, AL.AL_GAIN) - //val pitch = al.alGetSourcef(source, AL.AL_PITCH) - //println("gain=$gain, pitch=$pitch") - if (!playing) { - AL.alSourcePlay(source) - } - break - } - } finally { - availableSamples -= samples.totalSamples - } - } - - fun ensureSource() { - if (source != 0) return - provider.makeCurrent() - - source = alGenSourceAndInitialize() - //al.alGenBuffers(buffers.size, buffers) - } - - override fun start() { - ensureSource() - AL.alSourcePlay(source) - checkAlErrors("alSourcePlay", source) - //checkAlErrors() - } - - //override fun pause() { - // al.alSourcePause(source) - //} - - override fun stop() { - provider.makeCurrent() - - AL.alSourceStop(source) - if (source != 0) { - AL.alDeleteSource(source) - source = 0 - } - //for (n in buffers.indices) { - // if (buffers[n] != 0) { - // al.alDeleteBuffer(buffers[n]) - // buffers[n] = 0 - // } - //} - } -} - -private class MyStopwatch { - private var running = false - private var ns = 0L - private val now get() = System.nanoTime() - - fun resume() { - if (running) return - toggle() - } - - fun pause() { - if (!running) return - toggle() - } - - fun toggle() { - running = !running - ns = now - ns - } - - val elapsedNanoseconds: Long get() = if (running) now - ns else ns -} - -// https://ffainelli.github.io/openal-example/ -class OpenALSoundNoStream( - val provider: JnaOpenALNativeSoundProvider, - coroutineContext: CoroutineContext, - val data: AudioData?, - override val name: String = "Unknown" -) : Sound(coroutineContext), SoundProps { - private val logger = Logger("OpenALSoundNoStream") - - override suspend fun decode(maxSamples: Int): AudioData = data ?: AudioData.DUMMY - - override var volume: Double = 1.0 - override var pitch: Double = 1.0 - override var panning: Double = 0.0 - - override val length: TimeSpan get() = data?.totalTime ?: 0.seconds - override val nchannels: Int get() = data?.channels ?: 1 - - override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { - val data = data ?: return DummySoundChannel(this) - //println("provider.sourcePool.totalItemsInUse=${provider.sourcePool.totalItemsInUse}, provider.sourcePool.totalAllocatedItems=${provider.sourcePool.totalAllocatedItems}, provider.sourcePool.itemsInPool=${provider.sourcePool.itemsInPool}") - if (provider.sourcePool.totalItemsInUse >= JnaOpenALNativeSoundProvider.MAX_AVAILABLE_SOURCES) { - error("OpenAL too many sources in use") - } - provider.makeCurrent() - var buffer = provider.bufferPool.alloc() - var source = provider.sourcePool.alloc() - if (source == -1) logger.warn { "UNEXPECTED[0] source=-1" } - - AL.alBufferData(buffer, data, panning, volume) - - AL.alSourcei(source, AL.AL_BUFFER, buffer) - checkAlErrors("alSourcei", source) - - var stopped = false - - val sourceProvider: () -> Int = { source } - - val channel = object : SoundChannel(this), SoundProps by JnaSoundPropsProvider(sourceProvider) { - private val stopWatch = MyStopwatch() - val totalSamples get() = data.totalSamples - var currentSampleOffset: Int - get() { - if (source < 0) return 0 - return AL.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - } - set(value) { - if (source < 0) return - AL.alSourcei(source, AL.AL_SAMPLE_OFFSET, value) - } - - val estimatedTotalNanoseconds: Long - get() = total.nanoseconds.toLong() - val estimatedCurrentNanoseconds: Long - get() = stopWatch.elapsedNanoseconds - - override var current: TimeSpan - get() = data.timeAtSample(currentSampleOffset) - set(value) { - if (source < 0) return - AL.alSourcef(source, AL.AL_SEC_OFFSET, value.seconds.toFloat()) - } - override val total: TimeSpan get() = data.totalTime - - override val state: SoundChannelState get() { - if (source < 0) return SoundChannelState.STOPPED - val result = AL.alGetSourceState(source) - checkAlErrors("alGetSourceState", source) - return when (result) { - AL.AL_INITIAL -> SoundChannelState.INITIAL - AL.AL_PLAYING -> SoundChannelState.PLAYING - AL.AL_PAUSED -> SoundChannelState.PAUSED - AL.AL_STOPPED -> SoundChannelState.STOPPED - else -> SoundChannelState.STOPPED - } - } - - override fun stop() { - if (stopped) return - stopped = true - if (source == -1) logger.warn { "UNEXPECTED[1] source=-1" } - AL.alSourceStop(source) - AL.alSourcei(source, AL.AL_BUFFER, 0) - provider.sourcePool.free(source) - provider.bufferPool.free(buffer) - source = -1 - buffer = -1 - stopWatch.pause() - // We reuse them from the pool - //AL.alDeleteSource(source) - //AL.alDeleteBuffer(buffer) - } - - override fun pause() { - AL.alSourcePause(source) - stopWatch.pause() - } - - override fun resume() { - AL.alSourcePlay(source) - stopWatch.resume() - } - }.also { - it.copySoundPropsFromCombined(this@OpenALSoundNoStream, params) - } - launchImmediately(coroutineContext[ContinuationInterceptor] ?: coroutineContext) { - var times = params.times - var startTime = params.startTime - try { - while (times.hasMore && !stopped) { - times = times.oneLess - channel.reset() - AL.alSourcef(source, AL.AL_SEC_OFFSET, startTime.seconds.toFloat()) - channel.resume() - //checkAlErrors("alSourcePlay") - startTime = 0.seconds - while (channel.playingOrPaused) delay(10L) - } - } catch (e: CancellationException) { - params.onCancel?.invoke() - } catch (e: Throwable) { - e.printStackTrace() - } finally { - channel.stop() - params.onFinish?.invoke() - } - } - return channel - } -} - -class JnaSoundPropsProvider(val sourceProvider: () -> Int) : SoundProps { - val source get() = sourceProvider() - - private val temp1 = FloatArray(3) - private val temp2 = FloatArray(3) - private val temp3 = FloatArray(3) - - override var pitch: Double - get() = if (source < 0) 1.0 else AL.alGetSourcef(source, AL.AL_PITCH).toDouble() - set(value) { - if (source < 0) return - AL.alSourcef(source, AL.AL_PITCH, value.toFloat()) - } - override var volume: Double - get() = if (source < 0) 1.0 else AL.alGetSourcef(source, AL.AL_GAIN).toDouble() - set(value) { - if (source < 0) return - AL.alSourcef(source, AL.AL_GAIN, value.toFloat()) - } - override var panning: Double - get() { - if (source < 0) return 0.0 - AL.alGetSource3f(source, AL.AL_POSITION, temp1, temp2, temp3) - return temp1[0].toDouble() - } - set(value) { - if (source < 0) return - val pan = value.toFloat() - AL.alSourcef(source, AL.AL_ROLLOFF_FACTOR, 0.0f) - AL.alSourcei(source, AL.AL_SOURCE_RELATIVE, 1) - AL.alSource3f(source, AL.AL_POSITION, pan, 0f, -sqrt(1.0f - pan * pan)) - //println("SET PANNING: source=$source, pan=$pan") - } -} - -private fun AL.alBufferData(buffer: Int, data: AudioSamples, freq: Int, panning: Double = 0.0, volume: Double = 1.0) { - alBufferData(buffer, AudioData(freq, data), panning, volume) -} - -private fun applyStereoPanningInline(interleaved: ShortArray, panning: Double = 0.0, volume: Double = 1.0) { - if (panning == 0.0 || volume != 1.0) return - val vvolume = volume.clamp01() - val rratio = (((panning + 1.0) / 2.0).clamp01() * vvolume).toFloat() - val lratio = ((1.0 - rratio) * vvolume).toFloat() - //println("panning=$panning, lratio=$lratio, rratio=$rratio, vvolume=$vvolume") - for (n in interleaved.indices step 2) { - interleaved[n + 0] = (interleaved[n + 0] * lratio).coerceToShort() - interleaved[n + 1] = (interleaved[n + 1] * rratio).coerceToShort() - } -} - -private fun AL.alBufferData(buffer: Int, data: AudioData, panning: Double = 0.0, volume: Double = 1.0) { - val samples = data.samplesInterleaved.data - if (data.stereo && panning != 0.0) applyStereoPanningInline(samples, panning, volume) - val bufferData = ShortBuffer.wrap(samples) - val format = if (data.stereo) AL.AL_FORMAT_STEREO16 else AL.AL_FORMAT_MONO16 - val samplesData = if (samples.isNotEmpty()) bufferData else null - val bytesSize = samples.size * 2 - val rate = data.rate - AL.alBufferData(buffer, format, samplesData, bytesSize, rate) - checkAlErrors("alBufferData", buffer) -} - -private fun alGenSourceAndInitialize() = AL.alGenSource().also { source -> - AL.alSourcef(source, AL.AL_PITCH, 1f) - AL.alSourcef(source, AL.AL_GAIN, 1f) - AL.alSource3f(source, AL.AL_POSITION, 0f, 0f, 0f) - AL.alSource3f(source, AL.AL_VELOCITY, 0f, 0f, 0f) - AL.alSourcei(source, AL.AL_LOOPING, AL.AL_FALSE) - AL.alSourceStop(source) -} - -fun ALerrorToString(value: Int): String = when (value) { - AL.AL_INVALID_NAME -> "AL_INVALID_NAME" - AL.AL_INVALID_ENUM -> "AL_INVALID_ENUM" - AL.AL_INVALID_VALUE -> "AL_INVALID_VALUE" - AL.AL_INVALID_OPERATION -> "AL_INVALID_OPERATION" - AL.AL_OUT_OF_MEMORY -> "AL_OUT_OF_MEMORY" - else -> "UNKNOWN" -} - -//fun checkAlErrors(name: String, value: Int = -1) { -fun checkAlErrors(name: String, value: Int) { - //AL.alGetError().also { error -> if (error != AL.AL_NO_ERROR) Console.error("OpenAL error ${error.shex} (${ALerrorToString(error)}) '$name' (value=$value)") } -} diff --git a/korge-core/src/jvm/korlibs/audio/sound/impl/jogamp/JogampSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/impl/jogamp/JogampSoundProvider.kt deleted file mode 100644 index a354f9de95..0000000000 --- a/korge-core/src/jvm/korlibs/audio/sound/impl/jogamp/JogampSoundProvider.kt +++ /dev/null @@ -1,182 +0,0 @@ -package korlibs.audio.sound.impl.jogamp - -/* -import com.jogamp.openal.* -import com.jogamp.openal.util.* -import korlibs.time.* -import korlibs.audio.format.* -import korlibs.audio.sound.* -import korlibs.io.async.* -import korlibs.io.file.* -import kotlinx.coroutines.* -import java.nio.* -import kotlin.coroutines.* - -internal inline fun runCatchingAl(block: () -> T): T? { - val result = runCatching { block() } - if (result.isFailure) { - result.exceptionOrNull()?.printStackTrace() - } - return result.getOrNull() -} - -val al: AL? by lazy { - runCatchingAl { - ALFactory.getAL().also { al -> - //val error = al.alGetError() - //if (error != AL.AL_NO_ERROR) error("Error initializing OpenAL ${error.shex}") - } - } -} - -class JogampNativeSoundProvider : NativeSoundProvider() { - init { - //println("ALut.alutInit: ${Thread.currentThread()}") - runCatchingAl { - ALut.alutInit() - } - //alc.alcMakeContextCurrent(context) - al?.alListener3f(AL.AL_POSITION, 0f, 0f, 1.0f) - checkAlErrors() - al?.alListener3f(AL.AL_VELOCITY, 0f, 0f, 0f) - checkAlErrors() - al?.alListenerfv(AL.AL_ORIENTATION, floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f), 0) - checkAlErrors() - } - - override suspend fun createSound(data: ByteArray, streaming: Boolean): NativeSound { - return OpenALNativeSoundNoStream(coroutineContext, nativeAudioFormats.decode(data)) - } - - override suspend fun createSound(vfs: Vfs, path: String, streaming: Boolean): NativeSound { - return super.createSound(vfs, path, streaming) - } - - override suspend fun createSound(data: AudioData, formats: AudioFormats, streaming: Boolean): NativeSound { - return super.createSound(data, formats, streaming) - } -} - -// https://ffainelli.github.io/openal-example/ -class OpenALNativeSoundNoStream(val coroutineContext: CoroutineContext, val data: AudioData?) : NativeSound() { - override suspend fun decode(): AudioData = data ?: AudioData.DUMMY - - override fun play(): NativeSoundChannel { - //if (openalNativeSoundProvider.device == null || openalNativeSoundProvider.context == null) return DummyNativeSoundChannel(this, data) - //println("OpenALNativeSoundNoStream.play : $data") - //alc.alcMakeContextCurrent(context) - val data = data ?: return DummyNativeSoundChannel(this) - - val buffer = alGenBuffer() - alBufferData(buffer, data) - - val source = alGenSource() - al?.alSourcef(source, AL.AL_PITCH, 1f) - al?.alSourcef(source, AL.AL_GAIN, 1f) - al?.alSource3f(source, AL.AL_POSITION, 0f, 0f, 0f) - al?.alSource3f(source, AL.AL_VELOCITY, 0f, 0f, 0f) - al?.alSourcei(source, AL.AL_LOOPING, AL.AL_FALSE) - al?.alSourcei(source, AL.AL_BUFFER, buffer) - checkAlErrors() - - al?.alSourcePlay(source) - checkAlErrors() - - var stopped = false - - val channel = object : NativeSoundChannel(this) { - val totalSamples get() = data.totalSamples - val currentSampleOffset get() = alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - - override var volume: Double - get() = run { alGetSourcef(source, AL.AL_GAIN).toDouble() } - set(value) = run { al?.alSourcef(source, AL.AL_GAIN, value.toFloat()) } - override var pitch: Double - get() = run { alGetSourcef(source, AL.AL_PITCH).toDouble() } - set(value) = run { al?.alSourcef(source, AL.AL_PITCH, value.toFloat()) } - override var panning: Double = 0.0 - set(value) = run { - field = value - al?.alSource3f(source, AL.AL_POSITION, panning.toFloat(), 0f, 0f) - } - - override val current: TimeSpan get() = data.timeAtSample(currentSampleOffset) - override val total: TimeSpan get() = data.totalTime - override val playing: Boolean get() { - val result = alGetSourceState(source) == AL.AL_PLAYING - checkAlErrors() - return result - } - - override fun stop() { - if (!stopped) { - stopped = true - alDeleteSource(source) - alDeleteBuffer(buffer) - } - } - } - launchImmediately(coroutineContext[ContinuationInterceptor] ?: coroutineContext) { - try { - do { - delay(1L) - } while (channel.playing) - } catch (e: Throwable) { - e.printStackTrace() - } finally { - channel.stop() - } - } - return channel - - } -} - -private val tempF = FloatArray(1) -private val tempI = IntArray(1) -private fun alGetSourcef(source: Int, param: Int): Float = tempF.apply { al?.alGetSourcef(source, param, this, 0) }[0] -private fun alGetSourcei(source: Int, param: Int): Int = tempI.apply { al?.alGetSourcei(source, param, this, 0) }[0] -private fun alGetSourceState(source: Int): Int = alGetSourcei(source, AL.AL_SOURCE_STATE) - -private fun alBufferData(buffer: Int, data: AudioData) { - val samples = data.samplesInterleaved.data - - val bufferData = ShortBuffer.wrap(samples) - //val bufferData = ByteBuffer.allocateDirect(samples.size * 2).order(ByteOrder.nativeOrder()) - //bufferData.asShortBuffer().put(samples) - - al?.alBufferData( - buffer, - if (data.channels == 1) AL.AL_FORMAT_MONO16 else AL.AL_FORMAT_STEREO16, - if (samples.isNotEmpty()) bufferData else null, - samples.size * 2, - data.rate - ) - checkAlErrors() -} - -private fun alGenBuffer(): Int = tempI.apply { al?.alGenBuffers(1, this, 0) }[0] -private fun alGenSource(): Int = tempI.apply { al?.alGenSources(1, this, 0) }[0] -private fun alDeleteBuffer(buffer: Int): Unit = run { al?.alDeleteBuffers(1, tempI.also { it[0] = buffer }, 0) } -private fun alDeleteSource(buffer: Int): Unit = run { al?.alDeleteSources(1, tempI.also { it[0] = buffer }, 0) } - -/* -val alc by lazy { - ALFactory.getALC().also { alc -> - //val error = alc.alcGetError() - //if (error != AL.AL_NO_ERROR) error("Error initializing OpenAL ${error.shex}") - } } - -private val device by lazy { alc.alcOpenDevice(null).also { - println("alc.alcOpenDevice: $it") -} } -private val context by lazy { alc.alcCreateContext(device, null).also { - println("alc.alcCreateContext: $it with device=$device") -} } -*/ - -fun checkAlErrors() { -// val error = al.alGetError() -// if (error != AL.AL_NO_ERROR) error("OpenAL error ${error.shex}") -} -*/ diff --git a/korge-core/src/jvm/korlibs/audio/sound/internal/jvm/JvmTools.kt b/korge-core/src/jvm/korlibs/audio/sound/internal/jvm/JvmTools.kt deleted file mode 100644 index 79260eb2e0..0000000000 --- a/korge-core/src/jvm/korlibs/audio/sound/internal/jvm/JvmTools.kt +++ /dev/null @@ -1,17 +0,0 @@ -package korlibs.audio.sound.internal.jvm - -import korlibs.datastructure.ByteArrayDeque -import java.io.InputStream - -internal fun ByteArrayDeque.inputStream() = object : InputStream() { - override fun read(b: ByteArray, off: Int, len: Int): Int { - val out = this@inputStream.read(b, off, len) - //if (out <= 0) return -1 - if (out <= 0) return 0 - return out - } - - override fun read(): Int { - return this@inputStream.readByte() - } -} From 0060eaad983810320de2dd75af7678933d9f8fdb Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 09:48:52 +0200 Subject: [PATCH 49/66] More work --- .../audio/sound/backend => archive}/AL.kt | 2 + .../audio/sound/AndroidNativeSoundProvider.kt | 9 +- .../korlibs/audio/sound/AudioSamples.kt | 45 ++- .../audio/sound/PlatformAudioOutput.kt | 20 +- .../src/common/korlibs/audio/sound/Sound.kt | 17 +- .../korlibs/audio/sound/backend/ALSA.kt | 132 +++---- .../audio/sound/backend/JvmWaveOutImpl.kt | 25 +- .../audio/sound/CoreAudioSoundProvider.kt | 17 +- .../audio/sound/HtmlNativeSoundProvider.kt | 2 +- .../audio/sound/NativeAudioStreamJs.kt | 7 +- .../audio/sound/NativeSoundProviderJvm.kt | 9 +- .../jvm/korlibs/audio/sound/backend/Alsa.kt | 374 ------------------ .../sound/backend/AwtNativeSoundProvider.kt | 8 +- ....kt => JvmCoreAudioNativeSoundProvider.kt} | 25 +- .../korlibs/korge/gradle/AndroidTest.kt | 2 +- .../kotlin/samples/MainPolyphonic.kt | 6 +- 16 files changed, 151 insertions(+), 549 deletions(-) rename {korge-core/src/common/korlibs/audio/sound/backend => archive}/AL.kt (99%) delete mode 100644 korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt rename korge-core/src/jvm/korlibs/audio/sound/backend/{CoreAudioImpl.kt => JvmCoreAudioNativeSoundProvider.kt} (90%) diff --git a/korge-core/src/common/korlibs/audio/sound/backend/AL.kt b/archive/AL.kt similarity index 99% rename from korge-core/src/common/korlibs/audio/sound/backend/AL.kt rename to archive/AL.kt index f465138aff..5480634734 100644 --- a/korge-core/src/common/korlibs/audio/sound/backend/AL.kt +++ b/archive/AL.kt @@ -1,5 +1,6 @@ package korlibs.audio.sound.backend +/* import korlibs.audio.internal.* import korlibs.audio.sound.* import korlibs.datastructure.* @@ -685,3 +686,4 @@ internal inline fun runCatchingAl(block: () -> T): T? { return result.getOrNull() } */ +*/ diff --git a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt index 4fa4186af4..cb680a1e3d 100644 --- a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt +++ b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt @@ -18,7 +18,7 @@ class AndroidNativeSoundProvider : NativeSoundProviderNew() { audioManager!!.generateAudioSessionId() else -1 } - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput { ensureAudioManager(coroutineContext) return AndroidNewPlatformAudioOutput(this, coroutineContext, channels, frequency, gen) } @@ -43,7 +43,7 @@ class AndroidNativeSoundProvider : NativeSoundProviderNew() { coroutineContext: CoroutineContext, channels: Int, frequency: Int, - gen: (AudioSamples) -> Unit + gen: (AudioSamplesInterleaved) -> Unit ) : NewPlatformAudioOutput(coroutineContext, channels, frequency, gen) { var thread: NativeThread? = null @@ -86,7 +86,7 @@ class AndroidNativeSoundProvider : NativeSoundProviderNew() { System.err.println("Audio track was not initialized correctly frequency=$frequency, bufferSamples=$bufferSamples") } - val buffer = AudioSamples(channels, bufferSamples) + val buffer = AudioSamplesInterleaved(channels, bufferSamples) at.play() var lastVolL = Float.NaN @@ -124,8 +124,7 @@ class AndroidNativeSoundProvider : NativeSoundProviderNew() { } genSafe(buffer) - val bufferInterleaved = buffer.interleaved() - at.write(bufferInterleaved.data, 0, bufferInterleaved.data.size) + at.write(buffer.data, 0, buffer.data.size) } } } diff --git a/korge-core/src/common/korlibs/audio/sound/AudioSamples.kt b/korge-core/src/common/korlibs/audio/sound/AudioSamples.kt index 90fb524b9c..2679050911 100644 --- a/korge-core/src/common/korlibs/audio/sound/AudioSamples.kt +++ b/korge-core/src/common/korlibs/audio/sound/AudioSamples.kt @@ -1,15 +1,11 @@ package korlibs.audio.sound -import korlibs.memory.arraycopy -import korlibs.memory.arrayinterleave -import korlibs.math.clamp01 -import korlibs.audio.internal.SampleConvert -import korlibs.audio.internal.coerceToShort +import korlibs.audio.internal.* import korlibs.datastructure.iterators.* -import korlibs.io.lang.assert -import kotlin.math.absoluteValue -import kotlin.math.max -import kotlin.math.min +import korlibs.io.lang.* +import korlibs.math.* +import korlibs.memory.* +import kotlin.math.* interface IAudioSamples { val channels: Int @@ -19,11 +15,19 @@ interface IAudioSamples { operator fun get(channel: Int, sample: Int): Short operator fun set(channel: Int, sample: Int, value: Short): Unit fun getFloat(channel: Int, sample: Int): Float = SampleConvert.shortToFloat(this[channel, sample]) - fun setFloat(channel: Int, sample: Int, value: Float) { this[channel, sample] = SampleConvert.floatToShort(value) } + fun setFloat(channel: Int, sample: Int, value: Float) { + this[channel, sample] = SampleConvert.floatToShort(value) + } + fun setFloatStereo(sample: Int, l: Float, r: Float) { setFloat(0, sample, l) setFloat(1, sample, r) } + + fun scaleVolume(scale: Double): IAudioSamples = scaleVolume(scale.toFloat()) + fun scaleVolume(channelScales: DoubleArray): IAudioSamples = scaleVolume(FloatArray(channelScales.size) { channelScales[it].toFloat() }) + fun scaleVolume(scale: Float): IAudioSamples + fun scaleVolume(channelScales: FloatArray): IAudioSamples } internal fun AudioSamples.resample(scale: Double, totalSamples: Int = (this.totalSamples * scale).toInt(), out: AudioSamples = AudioSamples(channels, totalSamples)): AudioSamples { @@ -98,10 +102,8 @@ class AudioSamples(override val channels: Int, override val totalSamples: Int, v this[1, sample] = valueRight } - fun scaleVolume(scale: Double): AudioSamples = scaleVolume(scale.toFloat()) - fun scaleVolume(channelScales: DoubleArray): AudioSamples = scaleVolume(FloatArray(channelScales.size) { channelScales[it].toFloat() }) - fun scaleVolume(scale: Float): AudioSamples { + override fun scaleVolume(scale: Float): AudioSamples { data.fastForEach { channel -> for (n in channel.indices) { channel[n] = (channel[n] * scale).toInt().coerceToShort() @@ -109,7 +111,7 @@ class AudioSamples(override val channels: Int, override val totalSamples: Int, v } return this } - fun scaleVolume(channelScales: FloatArray): AudioSamples { + override fun scaleVolume(channelScales: FloatArray): AudioSamples { data.fastForEachWithIndex { ch, channel -> for (n in channel.indices) { channel[n] = (channel[n] * channelScales[ch]).toInt().coerceToShort() @@ -146,6 +148,21 @@ class AudioSamplesInterleaved(override val channels: Int, override val totalSamp override operator fun get(channel: Int, sample: Int): Short = data[index(channel, sample)] override operator fun set(channel: Int, sample: Int, value: Short) { data[index(channel, sample)] = value } + override fun scaleVolume(scale: Float): AudioSamplesInterleaved { + for (n in data.indices) data[n] = (data[n] * scale).toInt().coerceToShort() + return this + } + override fun scaleVolume(channelScales: FloatArray): AudioSamplesInterleaved { + for (ch in 0 until channels) { + val chVolume = channelScales[ch] + for (n in 0 until totalSamples) { + val i = n * channels + ch + data[i] = (data[i] * chVolume).toInt().coerceToShort() + } + } + return this + } + override fun toString(): String = "AudioSamplesInterleaved(channels=$channels, totalSamples=$totalSamples)" } diff --git a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt index 22450a020d..f3bb3a4d74 100644 --- a/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt +++ b/korge-core/src/common/korlibs/audio/sound/PlatformAudioOutput.kt @@ -13,13 +13,13 @@ open class NewPlatformAudioOutput( val coroutineContext: CoroutineContext, val channels: Int, val frequency: Int, - private val gen: (AudioSamples) -> Unit, + private val gen: (AudioSamplesInterleaved) -> Unit, ) : Disposable, SoundProps { var onCancel: Cancellable? = null var paused: Boolean = false private val lock = Lock() - fun genSafe(buffer: AudioSamples) { + fun genSafe(buffer: AudioSamplesInterleaved) { lock { try { gen(buffer) @@ -62,7 +62,7 @@ open class PlatformAudioOutputBasedOnNew( val new = soundProvider.createNewPlatformAudioOutput(coroutineContext, 2, frequency) { buffer -> //println("availableRead=$availableRead") //if (availableRead >= buffer.data.size) { - readShorts(buffer.data) + readSamplesInterleaved(buffer, fully = true) //} } @@ -262,6 +262,20 @@ open class DequeBasedPlatformAudioOutput( } } + protected fun readSamplesInterleaved(out: IAudioSamples, offset: Int = 0, count: Int = out.totalSamples - offset, nchannels: Int = out.channels, fully: Boolean): Int { + lock { + val totalRead = if (fully) count else minOf(availableRead, count) + + for (n in 0 until totalRead) { + for (ch in 0 until nchannels) { + out[ch, offset + n] = _readShort(ch) + } + } + + return totalRead + } + } + protected fun readSamples(samples: AudioSamples, offset: Int = 0, count: Int = samples.totalSamples - offset, fully: Boolean = false): Int { return _readShorts(samples.data, offset, count, fully = fully) } diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index dee44fce92..4cf186d2d4 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -22,7 +22,7 @@ open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeS override val target: String get() = parent.target override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput = parent.createPlatformAudioOutput(coroutineContext, freq) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput = parent.createNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = @@ -56,12 +56,12 @@ open class NativeSoundProvider() : Disposable { @Deprecated("") suspend fun createPlatformAudioOutput(freq: Int = 44100): PlatformAudioOutput = createPlatformAudioOutput(coroutineContextKt, freq) - open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + open fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int = 44100, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput { //println("createNewPlatformAudioOutput: ${this::class}") return NewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } - suspend fun createNewPlatformAudioOutput(nchannels: Int, freq: Int = 44100, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput = createNewPlatformAudioOutput(coroutineContextKt, nchannels, freq, gen) + suspend fun createNewPlatformAudioOutput(nchannels: Int, freq: Int = 44100, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput = createNewPlatformAudioOutput(coroutineContextKt, nchannels, freq, gen) open suspend fun createSound(data: ByteArray, streaming: Boolean = false, props: AudioDecodingProps = AudioDecodingProps.DEFAULT, name: String = "Unknown"): Sound { val format = props.formats ?: audioFormats @@ -198,6 +198,17 @@ fun SoundProps.volumeForChannel(channel: Int): Double { } } +fun SoundProps.applyPropsTo(samples: AudioSamplesInterleaved) { + for (ch in 0 until samples.channels) { + val volume01 = volumeForChannel(ch) + for (n in 0 until samples.totalSamples) { + var sample = samples[ch, n] + sample = (sample * volume01).toInt().toShort() + samples[ch, n] = sample + } + } +} + fun SoundProps.applyPropsTo(samples: AudioSamples) { for (ch in 0 until samples.channels) { val volume01 = volumeForChannel(ch) diff --git a/korge-core/src/common/korlibs/audio/sound/backend/ALSA.kt b/korge-core/src/common/korlibs/audio/sound/backend/ALSA.kt index 95d713969a..1da6f5680b 100644 --- a/korge-core/src/common/korlibs/audio/sound/backend/ALSA.kt +++ b/korge-core/src/common/korlibs/audio/sound/backend/ALSA.kt @@ -4,120 +4,78 @@ import korlibs.audio.sound.* import korlibs.datastructure.thread.* import korlibs.ffi.* import korlibs.io.lang.* -import korlibs.time.* -import kotlinx.coroutines.* import kotlin.coroutines.* object FFIALSANativeSoundProvider : NativeSoundProvider() { - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput { //println("ALSANativeSoundProvider.createPlatformAudioOutput(freq=$freq)") - return ALSAPlatformAudioOutput(this, coroutineContext, freq) + return ALSAPlatformAudioOutput(this, coroutineContext, channels, frequency, gen) } } class ALSAPlatformAudioOutput( val soundProvider: FFIALSANativeSoundProvider, coroutineContext: CoroutineContext, + channels: Int, frequency: Int, -) : DequeBasedPlatformAudioOutput(coroutineContext, frequency) { + gen: (AudioSamplesInterleaved) -> Unit, +) : NewPlatformAudioOutput(coroutineContext, channels, frequency, gen) { //var nativeThread: Job? = null var nativeThread: NativeThread? = null - var running = false - - var pcm: FFIPointer? = null - - override suspend fun wait() { - running = false - //println("WAITING") - val time = measureTime { - while (pcm.address != 0L) { - delay(10.milliseconds) - } - } - //println("WAITED: time=$time") - //super.wait() - } - override fun start() { - if (running) return - running = true + override fun internalStart() { //nativeThread = launchImmediately(coroutineContext) { - nativeThread = nativeThread(isDaemon = true) { - // @TODO: + nativeThread = nativeThread(isDaemon = true) { thread -> + val buffer = AudioSamplesInterleaved(channels, 1024) + val pcm = A2.snd_pcm_open("default", A2.SND_PCM_STREAM_PLAYBACK, 0) + if (pcm.address == 0L) { + error("Can't initialize ALSA") + //running = false + //return@nativeThread + } - val temp = AudioSamplesInterleaved(nchannels, 1024) + //val latency = 8 * 4096 + val latency = 32 * 4096 + A2.snd_pcm_set_params( + pcm, + A2.SND_PCM_FORMAT_S16_LE, + A2.SND_PCM_ACCESS_RW_INTERLEAVED, + channels, + frequency, + 1, + latency + ) try { - while (running || availableRead > 0) { - val readCount = readShortsInterleaved(temp) - - if (readCount == 0) { + while (thread.threadSuggestRunning) { + genSafe(buffer) + val written = A2.snd_pcm_writei(pcm, buffer.data, 0, buffer.totalSamples * channels, buffer.totalSamples) + //println("offset=$offset, pending=$pending, written=$written") + if (written == -A2.EPIPE) { + //println("ALSA: EPIPE error") + //A2.snd_pcm_prepare(pcm) + A2.snd_pcm_recover(pcm, written, 0) + continue + //blockingSleep(1.milliseconds) + } else if (written < 0) { + println("ALSA: OTHER error: $written") //delay(1.milliseconds) Thread_sleep(1L) - continue - } - - //println("readCount=$readCount") - var offset = 0 - var pending = readCount - while (pending > 0) { - if (pcm == null) { - pcm = A2.snd_pcm_open("default", A2.SND_PCM_STREAM_PLAYBACK, 0) - if (pcm.address == 0L) { - error("Can't initialize ALSA") - //running = false - //return@nativeThread - } - - //val latency = 8 * 4096 - val latency = 32 * 4096 - A2.snd_pcm_set_params( - pcm, - A2.SND_PCM_FORMAT_S16_LE, - A2.SND_PCM_ACCESS_RW_INTERLEAVED, - nchannels, - frequency, - 1, - latency - ) - } - - val written = A2.snd_pcm_writei(pcm, temp.data, offset * nchannels, pending * nchannels, pending) - //println("offset=$offset, pending=$pending, written=$written") - if (written == -A2.EPIPE) { - //println("ALSA: EPIPE error") - //A2.snd_pcm_prepare(pcm) - A2.snd_pcm_recover(pcm, written, 0) - offset = 0 - pending = readCount - continue - //blockingSleep(1.milliseconds) - } else if (written < 0) { - println("ALSA: OTHER error: $written") - //delay(1.milliseconds) - Thread_sleep(1L) - break - } else { - offset += written - pending -= written - } + break } } } finally { //println("!!COMPLETED : pcm=$pcm") - if (pcm.address != 0L) { - A2.snd_pcm_wait(pcm, 1000) - A2.snd_pcm_drain(pcm) - A2.snd_pcm_close(pcm) - pcm = null - //println("!!CLOSED = $pcm") - } + A2.snd_pcm_wait(pcm, 1000) + A2.snd_pcm_drain(pcm) + A2.snd_pcm_close(pcm) + //println("!!CLOSED = $pcm") } } } - override fun stop() { - running = false - super.stop() + override fun internalStop() { + nativeThread?.threadSuggestRunning = false + nativeThread = null } } diff --git a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt index fe1786dbf6..2d2a3e82e9 100644 --- a/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt +++ b/korge-core/src/common/korlibs/audio/sound/backend/JvmWaveOutImpl.kt @@ -16,7 +16,7 @@ class JvmWaveOutNativeSoundProvider : NativeSoundProviderNew() { coroutineContext: CoroutineContext, channels: Int, frequency: Int, - gen: (AudioSamples) -> Unit + gen: (AudioSamplesInterleaved) -> Unit ): NewPlatformAudioOutput = JvmWaveOutNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } @@ -73,7 +73,7 @@ class JvmWaveOutNewPlatformAudioOutput( coroutineContext: CoroutineContext, nchannels: Int, freq: Int, - gen: (AudioSamples) -> Unit + gen: (AudioSamplesInterleaved) -> Unit ) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { val samplesLock = korlibs.datastructure.lock.NonRecursiveLock() var nativeThread: NativeThread? = null @@ -171,13 +171,12 @@ private class WaveHeader( val id: Int, val handle: FFIPointer?, val totalSamples: Int, - val nchannels: Int, + val channels: Int, val arena: FFIArena, ) { - val samples = AudioSamples(nchannels, totalSamples) - val data = samples.data + val samples = AudioSamplesInterleaved(channels, totalSamples) - val totalBytes = (totalSamples * nchannels * Short.SIZE_BYTES) + val totalBytes = (totalSamples * channels * Short.SIZE_BYTES) val dataMem = arena.allocBytes(totalBytes).typed() val hdr = WAVEHDR(arena.allocBytes(WAVEHDR().size)).also { hdr -> hdr.lpData = dataMem.reinterpret() @@ -188,14 +187,12 @@ private class WaveHeader( fun prepareAndWrite(totalSamples: Int = this.totalSamples) { //println(data[0].toList()) - val nchannels = this.nchannels - hdr.dwBufferLength = (totalSamples * nchannels * Short.SIZE_BYTES) + val channels = this.channels + hdr.dwBufferLength = (totalSamples * channels * Short.SIZE_BYTES) - for (ch in 0 until nchannels) { - val inputCh = data[ch] - for (n in 0 until totalSamples) { - dataMem[n * nchannels + ch] = inputCh[n] - } + val samplesData = samples.data + for (n in 0 until channels * totalSamples) { + dataMem[n] = samplesData[n] } //if (hdr.isPrepared) dispose() if (!hdr.isPrepared) { @@ -209,7 +206,7 @@ private class WaveHeader( WINMM.waveOutUnprepareHeader(handle, hdr.ptr, hdr.size) } - override fun toString(): String = "WaveHeader(id=$id, totalSamples=$totalSamples, nchannels=$nchannels, hdr=$hdr)" + override fun toString(): String = "WaveHeader(id=$id, totalSamples=$totalSamples, nchannels=$channels, hdr=$hdr)" } internal typealias LPHWAVEOUT = FFIPointer diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index d738fc28b2..d9450559d2 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -32,26 +32,21 @@ class CoreAudioNativeSoundProvider : NativeSoundProviderNew() { //override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps): NativeSound = AVFoundationNativeSoundNoStream(CoroutineScope(coroutineContext), audioFormats.decode(data)) - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, frequency, channels, gen) + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamplesInterleaved) -> Unit): CoreAudioNewPlatformAudioOutput = CoreAudioNewPlatformAudioOutput(coroutineContext, frequency, channels, gen) } class CoreAudioNewPlatformAudioOutput( coroutineContext: CoroutineContext, freq: Int, nchannels: Int, - gen: (AudioSamples) -> Unit, + gen: (AudioSamplesInterleaved) -> Unit, ) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { val generator = CoreAudioGenerator(freq, nchannels, coroutineContext = coroutineContext) { data, dataSize -> val nchannels = this.nchannels - val samples = AudioSamples(nchannels, dataSize) - gen(samples) - - for (m in 0 until nchannels) { - val input = samples[m] - for (n in 0 until dataSize / nchannels) { - data[n * nchannels + m] = input[n] - } - } + val samples = AudioSamplesInterleaved(nchannels, dataSize) + genSafe(samples) + val samplesData = samples.data + for (n in 0 until dataSize) data[n] = samplesData[n] } override fun internalStart() { generator.start() diff --git a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt index 3334291c8f..20ef9375f6 100644 --- a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt +++ b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt @@ -15,7 +15,7 @@ class HtmlNativeSoundProvider : NativeSoundProviderNew() { HtmlSimpleSound.ensureUnlockStart() } - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, channels: Int, frequency: Int, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput { return JsNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } diff --git a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt index 73effb9fc3..d97d4ac127 100644 --- a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt +++ b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt @@ -17,7 +17,7 @@ class JsNewPlatformAudioOutput( coroutineContext: CoroutineContext, nchannels: Int, freq: Int, - gen: (AudioSamples) -> Unit + gen: (AudioSamplesInterleaved) -> Unit ) : NewPlatformAudioOutput( coroutineContext, nchannels, freq, gen ) { @@ -39,13 +39,12 @@ class JsNewPlatformAudioOutput( val nchannels = e.outputBuffer.numberOfChannels val outChannels = Array(nchannels) { e.outputBuffer.getChannelData(it) } val nsamples = e.outputBuffer.getChannelData(0).size - val samples = AudioSamples(nchannels, nsamples) + val samples = AudioSamplesInterleaved(nchannels, nsamples) genSafe(samples) for (ch in 0 until nchannels) { - val samplesCh = samples[ch] val outCh = outChannels[ch] for (n in 0 until nsamples) { - outCh[n] = SampleConvert.shortToFloat(samplesCh[n]) + outCh[n] = SampleConvert.shortToFloat(samples[ch, n]) } } diff --git a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt index a85477f4fd..305ac73711 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/NativeSoundProviderJvm.kt @@ -1,6 +1,7 @@ package korlibs.audio.sound import korlibs.audio.sound.backend.* +import korlibs.datastructure.thread.* import korlibs.io.time.* import korlibs.logger.* import korlibs.platform.* @@ -20,9 +21,9 @@ private val nativeSoundProviderDeferred: NativeSoundProvider by lazy { } } catch (e: UnsatisfiedLinkError) { DummyNativeSoundProvider - } catch (e: OpenALException) { - logger.error { "OpenALException: ${e.message}" } - DummyNativeSoundProvider + //} catch (e: OpenALException) { + // logger.error { "OpenALException: ${e.message}" } + // DummyNativeSoundProvider } catch (e: Throwable) { e.printStackTrace() DummyNativeSoundProvider @@ -30,7 +31,7 @@ private val nativeSoundProviderDeferred: NativeSoundProvider by lazy { } actual val nativeSoundProvider: NativeSoundProvider by lazy { - Thread { nativeSoundProviderDeferred }.apply { isDaemon = true }.start() + nativeThread(isDaemon = true, start = true) { nativeSoundProviderDeferred } LazyNativeSoundProvider { nativeSoundProviderDeferred } } //actual val nativeSoundProvider: NativeSoundProvider by lazy { JogampNativeSoundProvider() } diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt deleted file mode 100644 index ee88a52bc8..0000000000 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/Alsa.kt +++ /dev/null @@ -1,374 +0,0 @@ -package korlibs.audio.sound.backend - -import com.sun.jna.* -import korlibs.audio.sound.* -import korlibs.datastructure.lock.* -import korlibs.io.annotations.* -import korlibs.io.file.std.* -import korlibs.math.* -import korlibs.time.* -import kotlinx.coroutines.* -import kotlin.coroutines.* - -object ALSAExample { - @JvmStatic - fun main(args: Array) { - runBlocking { - val sp = ALSANativeSoundProvider() - //val sp = JnaOpenALNativeSoundProvider() - val job1 = launch(coroutineContext) { - //sp.playAndWait(AudioTone.generate(10.seconds, 400.0).toStream()) - sp.playAndWait(resourcesVfs["Snowland.mp3"].readMusic().toStream()) - } - val job2 = launch(coroutineContext) { - //sp.playAndWait(AudioTone.generate(10.seconds, 200.0).toStream()) - } - println("Waiting...") - job1.join() - job2.join() - println("Done") - } - } -} - -class ALSANativeSoundProvider : NativeSoundProvider() { - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { - return ALSAPlatformAudioOutput(this, coroutineContext, freq) - } -} - -class ALSAPlatformAudioOutput( - val soundProvider: ALSANativeSoundProvider, - coroutineContext: CoroutineContext, - frequency: Int, -) : PlatformAudioOutput(coroutineContext, frequency) { - val channels = 2 - val cmpPtr = Memory(1024L).also { it.clear() } - val params = Memory(1024L).also { it.clear() } - val temp = Memory(1024L).also { it.clear() } - var pcm: Pointer? = Pointer.NULL - private val lock = Lock() - val sdeque = AudioSamplesDeque(channels) - var running = true - var thread: Thread? = null - - init { - start() - } - - override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - if (!ASound2.initialized) return super.add(samples, offset, size) - - while (running && lock { sdeque.availableRead > 4 * 1024 }) { - delay(10.milliseconds) - } - lock { sdeque.write(samples, offset, size) } - } - - override fun start() { - sdeque.clear() - running = true - - if (!ASound2.initialized) return - - //cmpPtr.clear() - //cmpPtr.setLong(0L, 0L) - //println("test") - ASound2.snd_pcm_open(cmpPtr, "default", ASound2.SND_PCM_STREAM_PLAYBACK, 0).also { - if (it != 0) error("Can't initialize ALSA") - } - pcm = cmpPtr.getPointer(0L) - //println("pcm=$pcm") - ASound2.snd_pcm_hw_params_any(pcm, params) - ASound2.snd_pcm_hw_params_set_access(pcm, params, ASound2.SND_PCM_ACCESS_RW_INTERLEAVED).also { - if (it != 0) error("Error calling snd_pcm_hw_params_set_access=$it") - } - ASound2.snd_pcm_hw_params_set_format(pcm, params, ASound2.SND_PCM_FORMAT_S16_LE).also { - if (it != 0) error("Error calling snd_pcm_hw_params_set_format=$it") - } - ASound2.snd_pcm_hw_params_set_channels(pcm, params, channels).also { - if (it != 0) error("Error calling snd_pcm_hw_params_set_channels=$it") - } - ASound2.snd_pcm_hw_params_set_rate(pcm, params, frequency, +1).also { - if (it != 0) error("Error calling snd_pcm_hw_params_set_rate=$it") - } - ASound2.snd_pcm_hw_params(pcm, params).also { - if (it != 0) error("Error calling snd_pcm_hw_params=$it") - } - - //println(ASound2.snd_pcm_name(pcm)) - //println(ASound2.snd_pcm_state_name(ASound2.snd_pcm_state(pcm))) - ASound2.snd_pcm_hw_params_get_channels(params, temp).also { - if (it != 0) error("Error calling snd_pcm_hw_params_get_channels=$it") - } - val cchannels = temp.getInt(0L) - ASound2.snd_pcm_hw_params_get_rate(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_rate=$it") } - val crate = temp.getInt(0L) - ASound2.snd_pcm_hw_params_get_period_size(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } - val frames = temp.getInt(0L) - //println("cchannels: $cchannels, rate=$crate, frames=$frames") - val buff = Memory((frames * channels * 2).toLong()).also { it.clear() } - ASound2.snd_pcm_hw_params_get_period_time(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } - //val random = Random(0L) - thread = Thread { - val samples = AudioSamplesInterleaved(channels, frames) - try { - mainLoop@ while (running) { - while (lock { sdeque.availableRead < frames }) { - if (!running) break@mainLoop - Thread.sleep(1L) - } - val readCount = lock { sdeque.read(samples, 0, frames) } - //println("readCount=$readCount") - val panning = this.panning.toFloat() - //val panning = -1f - //val panning = +0f - //val panning = +1f - val volume = this.volume.toFloat().clamp01() - for (ch in 0 until channels) { - val pan = (if (ch == 0) -panning else +panning) + 1f - val npan = pan.clamp01() - val rscale: Float = npan * volume - //println("panning=$panning, volume=$volume, pan=$pan, npan=$npan, rscale=$rscale") - for (n in 0 until readCount) { - buff.setShort( - ((n * channels + ch) * 2).toLong(), - (samples[ch, n] * rscale).toInt().toShort() - ) - } - } - val result = ASound2.snd_pcm_writei(pcm, buff, frames) - //println("result=$result") - if (result == -ASound2.EPIPE) { - ASound2.snd_pcm_prepare(pcm) - } - } - } catch (e: InterruptedException) { - // Done - } - }.also { - it.isDaemon = true - it.start() - } - } - - override fun stop() { - running = false - thread?.interrupt() - if (!ASound2.initialized) return - - ASound2.snd_pcm_drain(pcm) - ASound2.snd_pcm_close(pcm) - } -} - -object AlsaTest { - @JvmStatic fun main(args: Array) { - /* - val data = AudioTone.generate(1.seconds, 400.0) - - var nn = 0 - while (true) { - for (n in 0 until frames * channels) { - val value = data[0, nn] - buff.setShort((n * 2).toLong(), value) - nn++ - if (nn >= data.totalSamples) nn = 0 - } - val result = ASound2.snd_pcm_writei(pcm, buff, frames) - println("result=$result") - if (result == -ASound2.EPIPE) { - ASound2.snd_pcm_prepare(pcm) - } - } - */ - - } -} - -@Keep -object ASound2 { - var initialized = false - - @JvmStatic external fun snd_pcm_open(pcmPtr: Pointer?, name: String, stream: Int, mode: Int): Int - @JvmStatic external fun snd_pcm_hw_params_any(pcm: Pointer?, params: Pointer): Int - @JvmStatic external fun snd_pcm_hw_params_set_access(pcm: Pointer?, params: Pointer, access: Int): Int - @JvmStatic external fun snd_pcm_hw_params_set_format(pcm: Pointer?, params: Pointer, format: Int): Int - @JvmStatic external fun snd_pcm_hw_params_set_channels(pcm: Pointer?, params: Pointer, channels: Int): Int - @JvmStatic external fun snd_pcm_hw_params_set_rate(pcm: Pointer?, params: Pointer, rate: Int, dir: Int): Int - @JvmStatic external fun snd_pcm_hw_params(pcm: Pointer?, params: Pointer): Int - @JvmStatic external fun snd_pcm_name(pcm: Pointer?): String - @JvmStatic external fun snd_pcm_state(pcm: Pointer?): Int - @JvmStatic external fun snd_pcm_state_name(state: Int): String - @JvmStatic external fun snd_pcm_hw_params_get_channels(params: Pointer, out: Pointer): Int - @JvmStatic external fun snd_pcm_hw_params_get_rate(params: Pointer?, value: Pointer?, dir: Pointer?): Int - @JvmStatic external fun snd_pcm_hw_params_get_period_size(params: Pointer?, value: Pointer?, dir: Pointer?): Int - @JvmStatic external fun snd_pcm_hw_params_get_period_time(params: Pointer?, value: Pointer?, dir: Pointer?): Int - @JvmStatic external fun snd_pcm_writei(pcm: Pointer?, buffer: Pointer, size: Int): Int - @JvmStatic external fun snd_pcm_prepare(pcm: Pointer?): Int - @JvmStatic external fun snd_pcm_drain(pcm: Pointer?): Int - @JvmStatic external fun snd_pcm_close(pcm: Pointer?): Int - - const val EPIPE = 32 // Broken pipe - const val EBADFD = 77 // File descriptor in bad state - const val ESTRPIPE = 86 // Streams pipe error - - const val SND_PCM_STREAM_PLAYBACK = 0 - const val SND_PCM_STREAM_CAPTURE = 1 - - const val SND_PCM_ACCESS_MMAP_INTERLEAVED = 0 // mmap access with simple interleaved channels - const val SND_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 // mmap access with simple non interleaved channels - const val SND_PCM_ACCESS_MMAP_COMPLEX = 2 // mmap access with complex placement - const val SND_PCM_ACCESS_RW_INTERLEAVED = 3 // snd_pcm_readi/snd_pcm_writei access - const val SND_PCM_ACCESS_RW_NONINTERLEAVED = 4 // /snd_pcm_writen access - - const val SND_PCM_FORMAT_S16_LE = 2 - - const val SND_PCM_STATE_OPEN = 0 // Open - const val SND_PCM_STATE_SETUP = 1 // Setup installed - const val SND_PCM_STATE_PREPARED = 2 // Ready to start - const val SND_PCM_STATE_RUNNING = 3 // Running - const val SND_PCM_STATE_XRUN = 4 // Stopped: underrun (playback) or overrun (capture) detected - const val SND_PCM_STATE_DRAINING = 5 // Draining: running (playback) or stopped (capture) - const val SND_PCM_STATE_PAUSED = 6 // Paused - const val SND_PCM_STATE_SUSPENDED = 7 // Hardware is suspended - const val SND_PCM_STATE_DISCONNECTED = 8 // Hardware is disconnected - - init { - try { - Native.register("libasound.so.2") - initialized = true - } catch (e: Throwable) { - e.printStackTrace() - } - } -} - -/* -âžœ korge git:(main) ✗ cat ~/alsatest.c -/* - * Simple sound playback using ALSA API and libasound. - * - * Compile: - * $ cc -o play sound_playback.c -lasound - * - * Usage: - * $ ./play < - * - * Examples: - * $ ./play 44100 2 5 < /dev/urandom - * $ ./play 22050 1 8 < /path/to/file.wav - * - * Copyright (C) 2009 Alessandro Ghedini - * -------------------------------------------------------------- - * "THE BEER-WARE LICENSE" (Revision 42): - * Alessandro Ghedini wrote this file. As long as you retain this - * notice you can do whatever you want with this stuff. If we - * meet some day, and you think this stuff is worth it, you can - * buy me a beer in return. - * -------------------------------------------------------------- - */ - -#include -#include - -#define PCM_DEVICE "default" - -int main(int argc, char **argv) { - unsigned int pcm, tmp, dir; - int rate, channels, seconds; - snd_pcm_t *pcm_handle; - snd_pcm_hw_params_t *params; - snd_pcm_uframes_t frames; - char *buff; - int buff_size, loops; - - if (argc < 4) { - printf("Usage: %s \n", - argv[0]); - return -1; - } - - rate = atoi(argv[1]); - channels = atoi(argv[2]); - seconds = atoi(argv[3]); - - /* Open the PCM device in playback mode */ - if (pcm = snd_pcm_open(&pcm_handle, PCM_DEVICE, - SND_PCM_STREAM_PLAYBACK, 0) < 0) - printf("ERROR: Can't open \"%s\" PCM device. %s\n", - PCM_DEVICE, snd_strerror(pcm)); - - /* Allocate parameters object and fill it with default values*/ - snd_pcm_hw_params_alloca(¶ms); - - snd_pcm_hw_params_any(pcm_handle, params); - - /* Set parameters */ - if (pcm = snd_pcm_hw_params_set_access(pcm_handle, params, - SND_PCM_ACCESS_RW_INTERLEAVED) < 0) - printf("ERROR: Can't set interleaved mode. %s\n", snd_strerror(pcm)); - - if (pcm = snd_pcm_hw_params_set_format(pcm_handle, params, - SND_PCM_FORMAT_S16_LE) < 0) - printf("ERROR: Can't set format. %s\n", snd_strerror(pcm)); - - if (pcm = snd_pcm_hw_params_set_channels(pcm_handle, params, channels) < 0) - printf("ERROR: Can't set channels number. %s\n", snd_strerror(pcm)); - - if (pcm = snd_pcm_hw_params_set_rate_near(pcm_handle, params, &rate, 0) < 0) - printf("ERROR: Can't set rate. %s\n", snd_strerror(pcm)); - - /* Write parameters */ - if (pcm = snd_pcm_hw_params(pcm_handle, params) < 0) - printf("ERROR: Can't set harware parameters. %s\n", snd_strerror(pcm)); - - /* Resume information */ - printf("PCM name: '%s'\n", snd_pcm_name(pcm_handle)); - - printf("PCM state: %s\n", snd_pcm_state_name(snd_pcm_state(pcm_handle))); - - snd_pcm_hw_params_get_channels(params, &tmp); - printf("channels: %i ", tmp); - - if (tmp == 1) - printf("(mono)\n"); - else if (tmp == 2) - printf("(stereo)\n"); - - snd_pcm_hw_params_get_rate(params, &tmp, 0); - printf("rate: %d bps\n", tmp); - - printf("seconds: %d\n", seconds); - - /* Allocate buffer to hold single period */ - snd_pcm_hw_params_get_period_size(params, &frames, 0); - - buff_size = frames * channels * 2 /* 2 -> sample size */; - buff = (char *) malloc(buff_size); - - snd_pcm_hw_params_get_period_time(params, &tmp, NULL); - - for (loops = (seconds * 1000000) / tmp; loops > 0; loops--) { - - if (pcm = read(0, buff, buff_size) == 0) { - printf("Early end of file.\n"); - return 0; - } - - if (pcm = snd_pcm_writei(pcm_handle, buff, frames) == -EPIPE) { - printf("XRUN.\n"); - snd_pcm_prepare(pcm_handle); - } else if (pcm < 0) { - printf("ERROR. Can't write to PCM device. %s\n", snd_strerror(pcm)); - } - - } - - snd_pcm_drain(pcm_handle); - snd_pcm_close(pcm_handle); - free(buff); - - return 0; -}% - */ diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt index f89a7cbb11..4e786ed2e5 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/AwtNativeSoundProvider.kt @@ -13,7 +13,7 @@ object AwtNativeSoundProvider : NativeSoundProviderNew() { coroutineContext: CoroutineContext, nchannels: Int, freq: Int, - gen: (AudioSamples) -> Unit + gen: (AudioSamplesInterleaved) -> Unit ): NewPlatformAudioOutput { return JvmNewPlatformAudioOutput(this, coroutineContext, nchannels, freq, gen) } @@ -24,7 +24,7 @@ class JvmNewPlatformAudioOutput( coroutineContext: CoroutineContext, nchannels: Int, freq: Int, - gen: (AudioSamples) -> Unit + gen: (AudioSamplesInterleaved) -> Unit ) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { var nativeThread: NativeThread? = null @@ -50,14 +50,14 @@ class JvmNewPlatformAudioOutput( line.open() line.start() try { - val info = AudioSamples(nchannels, 1024) + val info = AudioSamplesInterleaved(nchannels, 1024) val bytes = ByteArray(samplesToBytes(1024)) while (it.threadSuggestRunning) { if (paused) { Thread.sleep(10L) } else { genSafe(info) - bytes.setArrayLE(0, info.interleaved().data) + bytes.setArrayLE(0, info.data) //println(bytes.count { it == 0.toByte() }) line.write(bytes, 0, bytes.size) } diff --git a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt b/korge-core/src/jvm/korlibs/audio/sound/backend/JvmCoreAudioNativeSoundProvider.kt similarity index 90% rename from korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt rename to korge-core/src/jvm/korlibs/audio/sound/backend/JvmCoreAudioNativeSoundProvider.kt index 1cb67191c2..694704452b 100644 --- a/korge-core/src/jvm/korlibs/audio/sound/backend/CoreAudioImpl.kt +++ b/korge-core/src/jvm/korlibs/audio/sound/backend/JvmCoreAudioNativeSoundProvider.kt @@ -5,7 +5,6 @@ import korlibs.audio.sound.* import korlibs.ffi.* import korlibs.io.annotations.* import korlibs.io.concurrent.atomic.* -import korlibs.memory.* import java.util.concurrent.* import java.util.concurrent.atomic.* import kotlin.coroutines.* @@ -20,7 +19,7 @@ val jvmCoreAudioNativeSoundProvider: JvmCoreAudioNativeSoundProvider? by lazy { } class JvmCoreAudioNativeSoundProvider : NativeSoundProviderNew() { - override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamples) -> Unit): NewPlatformAudioOutput { + override fun createNewPlatformAudioOutput(coroutineContext: CoroutineContext, nchannels: Int, freq: Int, gen: (AudioSamplesInterleaved) -> Unit): NewPlatformAudioOutput { return JvmCoreAudioNewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) } } @@ -40,21 +39,13 @@ private val jnaNewCoreAudioCallback by lazy { if (ptr != null) { // Reuse instances as much as possible - if (output.samples.totalSamples != samplesCount) output.samples = AudioSamples(nchannels, samplesCount) if (output.buffer.totalSamples != samplesCount) output.buffer = AudioSamplesInterleaved(nchannels, samplesCount) - val samples = output.samples + val samples = output.buffer output.genSafe(samples) - val buffer = output.buffer.data - - for (m in 0 until nchannels) { - arraycopyStride( - samples.data[m], 0, 1, - buffer, m, nchannels, - samplesCount - ) - } + + val samplesData = samples.data for (n in 0 until samplesCount * nchannels) { - ptr[n] = buffer[n] + ptr[n] = samplesData[n] } } //println("queue.mAudioData=${queue.mAudioData}") @@ -79,7 +70,7 @@ private class JvmCoreAudioNewPlatformAudioOutput( coroutineContext: CoroutineContext, nchannels: Int, freq: Int, - gen: (AudioSamples) -> Unit, + gen: (AudioSamplesInterleaved) -> Unit, ) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { val id = lastId.incrementAndGet() companion object { @@ -90,15 +81,11 @@ private class JvmCoreAudioNewPlatformAudioOutput( internal var completed = false - internal var samples by KorAtomicRef(AudioSamples(nchannels, 0)) internal var buffer by KorAtomicRef(AudioSamplesInterleaved(nchannels, 0)) - var queue: Pointer? = null override fun internalStart() { - stop() - newAudioOutputsById[id] = this completed = false val queueRef = Memory(16).also { it.clear() } diff --git a/korge-gradle-plugin/src/test/kotlin/korlibs/korge/gradle/AndroidTest.kt b/korge-gradle-plugin/src/test/kotlin/korlibs/korge/gradle/AndroidTest.kt index 7c2d80fc1e..ac02aa0ab0 100644 --- a/korge-gradle-plugin/src/test/kotlin/korlibs/korge/gradle/AndroidTest.kt +++ b/korge-gradle-plugin/src/test/kotlin/korlibs/korge/gradle/AndroidTest.kt @@ -19,7 +19,7 @@ class AndroidTest : AbstractGradleIntegrationTest() { spawnResult.add(dir to command) } - override fun execLogger(projectDir: File, vararg params: String) { + override fun execLogger(projectDir: File, vararg params: String, filter: Process.(line: String) -> String?) { project.exec { it.workingDir(projectDir) it.commandLine(*params) diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt index 0f243c859b..4513d2e948 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainPolyphonic.kt @@ -6,7 +6,6 @@ import korlibs.korge.scene.* import korlibs.korge.ui.* import korlibs.korge.view.* import korlibs.math.* -import korlibs.memory.* import kotlin.math.* class MainPolyphonic : Scene() { @@ -25,10 +24,7 @@ class MainPolyphonic : Scene() { for (nchannel in 0 until 2) { val stream2 = nativeSoundProvider.createNewPlatformAudioOutput(1, 44100) { samples -> - audioOutCallback(nchannel, samples.data[0], samples.data[0].size) - for (n in 1 until samples.channels) { - arraycopy(samples.data[0], 0, samples.data[n], 0, samples.data[0].size) - } + audioOutCallback(nchannel, samples.data, samples.data.size) samples.scaleVolume(.05f) } stream2.start() From cb9d79375095a8d6f8cdb8b2b8026ea70d94b28d Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 11:12:27 +0200 Subject: [PATCH 50/66] More work --- .../korge/gradle/targets/js/Esbuild.kt | 62 ++++---- .../audio/sound/HtmlNativeSoundProvider.kt | 150 ++++++++---------- .../audio/sound/NativeAudioStreamJs.kt | 64 -------- .../src/common/korlibs/memory/Arrays.kt | 19 +++ .../test/common/korlibs/memory/ArraysTest.kt | 17 +- 5 files changed, 133 insertions(+), 179 deletions(-) delete mode 100644 korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/Esbuild.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/Esbuild.kt index 222b58b1e6..ff24040332 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/Esbuild.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/js/Esbuild.kt @@ -99,38 +99,36 @@ fun Project.configureErrorableEsbuild() { else -> browserPrepareEsbuildRelease } - //for (run in listOf(false, true)) { - for (run in listOf(false)) { - val runSuffix = if (run) "Run" else "" - - // browserDebugEsbuild - // browserReleaseEsbuild - tasks.createThis("browser${debugPrefix}Esbuild${runSuffix}") { - group = "kotlin browser" - dependsOn(browserPrepareEsbuild) - - val compileExecutableKotlinJs = tasks.getByName("compile${productionInfix}ExecutableKotlinJs") as KotlinJsIrLink - //println("compileExecutableKotlinJs:" + compileExecutableKotlinJs::class) - val jsPath = compileExecutableKotlinJs.outputFileProperty.get() - - val output = File(wwwFolder, "${project.name}.js") - inputs.file(jsPath) - outputs.file(output) - environment("PATH", ENV_PATH) - commandLine(ArrayList().apply { - addAll(esbuildCmd) - //add("--watch",) - add("--bundle") - if (!debug) { - add("--minify") - add("--sourcemap=external") - } - add(jsPath) - add("--outfile=$output") - // @TODO: Close this command on CTRL+C - //if (run) add("--servedir=$wwwFolder") - }) - } + // browserDebugEsbuild + // browserReleaseEsbuild + tasks.createThis("browser${debugPrefix}Esbuild") { + group = "kotlin browser" + val compileExecutableKotlinJs = tasks.getByName("compile${productionInfix}ExecutableKotlinJs") as KotlinJsIrLink + dependsOn(browserPrepareEsbuild) + dependsOn(compileExecutableKotlinJs) + + //println("compileExecutableKotlinJs:" + compileExecutableKotlinJs::class) + val jsPath = compileExecutableKotlinJs.outputFileProperty.get() + val output = File(wwwFolder, "${project.name}.js") + //println("jsPath=$jsPath") + //println("jsPath.parentFile=${jsPath.parentFile}") + //println("outputs=${compileExecutableKotlinJs.outputs.files.toList()}") + inputs.files(compileExecutableKotlinJs.outputs.files) + outputs.file(output) + environment("PATH", ENV_PATH) + commandLine(buildList { + addAll(esbuildCmd) + //add("--watch",) + add("--bundle") + if (!debug) { + add("--minify") + add("--sourcemap=external") + } + add(jsPath) + add("--outfile=$output") + // @TODO: Close this command on CTRL+C + //if (run) add("--servedir=$wwwFolder") + }) } } } diff --git a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt index 20ef9375f6..48938f4e23 100644 --- a/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt +++ b/korge-core/src/js/korlibs/audio/sound/HtmlNativeSoundProvider.kt @@ -5,11 +5,19 @@ import korlibs.audio.internal.* import korlibs.io.file.* import korlibs.io.file.std.* import korlibs.io.lang.* +import korlibs.memory.* +import korlibs.platform.* import korlibs.time.* -import kotlinx.coroutines.* -import org.w3c.dom.* import kotlin.coroutines.* +actual val nativeSoundProvider: NativeSoundProvider by lazy { + if (Platform.isJsBrowser) { + HtmlNativeSoundProvider() + } else { + DummyNativeSoundProvider + } +} + class HtmlNativeSoundProvider : NativeSoundProviderNew() { init { HtmlSimpleSound.ensureUnlockStart() @@ -19,103 +27,83 @@ class HtmlNativeSoundProvider : NativeSoundProviderNew() { return JsNewPlatformAudioOutput(coroutineContext, channels, frequency, gen) } - override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = + override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound = AudioBufferSound(AudioBufferOrHTMLMediaElement(HtmlSimpleSound.loadSound(data)), "#bytes", coroutineContext, name) - override suspend fun createSound(vfs: Vfs, path: String, streaming: Boolean, props: AudioDecodingProps): Sound = when (vfs) { - is LocalVfs, is UrlVfs -> { + override suspend fun createSound(vfs: Vfs, path: String, streaming: Boolean, props: AudioDecodingProps): Sound = when (vfs) { + is LocalVfs, is UrlVfs -> { //println("createSound[1]") - val url = when (vfs) { - is LocalVfs -> path - is UrlVfs -> vfs.getFullUrl(path) - else -> invalidOp - } + val url = when (vfs) { + is LocalVfs -> path + is UrlVfs -> vfs.getFullUrl(path) + else -> invalidOp + } if (streaming) { AudioBufferSound(AudioBufferOrHTMLMediaElement(HtmlSimpleSound.loadSoundBuffer(url)), url, coroutineContext) //HtmlElementAudio(url) } else { AudioBufferSound(AudioBufferOrHTMLMediaElement(HtmlSimpleSound.loadSound(url)), url, coroutineContext) } - } - else -> { + } + else -> { //println("createSound[2]") - super.createSound(vfs, path) - } - } + super.createSound(vfs, path) + } + } } -class HtmlElementAudio( - val audio: HTMLAudioElement, +class JsNewPlatformAudioOutput( coroutineContext: CoroutineContext, -) : Sound(coroutineContext) { - override val length: TimeSpan get() = audio.duration.seconds - - override suspend fun decode(maxSamples: Int): AudioData = - AudioBufferSound(AudioBufferOrHTMLMediaElement(HtmlSimpleSound.loadSound(audio.src)), audio.src, defaultCoroutineContext).decode() - - companion object { - suspend operator fun invoke(url: String): HtmlElementAudio { - val audio = createAudioElement(url) - val promise = CompletableDeferred() - audio.oncanplay = { promise.complete(Unit) } - audio.oncanplaythrough = { promise.complete(Unit) } - promise.await() - //HtmlSimpleSound.waitUnlocked() - return HtmlElementAudio(audio, coroutineContext) - } + nchannels: Int, + frequency: Int, + gen: (AudioSamplesInterleaved) -> Unit +) : NewPlatformAudioOutput( + coroutineContext, nchannels, frequency, gen +) { + init { + nativeSoundProvider // Ensure it is created } - override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { - val audioCopy = audio.clone() - audioCopy.volume = params.volume - HtmlSimpleSound.callOnUnlocked { - audioCopy.play() - } - audioCopy.oncancel = { - params.onCancel?.invoke() - } - audioCopy.onended = { - params.onFinish?.invoke() - } - return object : SoundChannel(this@HtmlElementAudio) { - override var volume: Double - get() = audioCopy.volume - set(value) { - audioCopy.volume = value - } - - override var pitch: Double - get() = 1.0 - set(value) {} - - override var panning: Double - get() = 0.0 - set(value) {} - - override val total: TimeSpan get() = audioCopy.duration.seconds - override var current: TimeSpan - get() = audioCopy.currentTime.seconds - set(value) { audioCopy.currentTime = value.seconds } - - override val state: SoundChannelState get() = when { - audioCopy.paused -> SoundChannelState.PAUSED - audioCopy.ended -> SoundChannelState.STOPPED - else -> SoundChannelState.PLAYING - } - - override fun pause() { - audioCopy.pause() - } - - override fun resume() { - audioCopy.play() - } + var missingDataCount = 0 + var nodeRunning = false + var node: ScriptProcessorNode? = null + + private var startPromise: Cancellable? = null + + override fun internalStart() { + if (nodeRunning) return + startPromise = HtmlSimpleSound.callOnUnlocked { + val ctx = HtmlSimpleSound.ctx + if (ctx != null) { + val bufferSize = 1024 + val scale = (frequency / ctx.sampleRate).toFloat() + val samples = AudioSamplesInterleaved(channels, (bufferSize * scale).toInt()) + node = ctx.createScriptProcessor(bufferSize, channels, channels) + //Console.log("sampleRate", ctx.sampleRate, "bufferSize", bufferSize, "totalSamples", samples.totalSamples, "scale", scale) + node?.onaudioprocess = { e -> + genSafe(samples) + val separated = samples.separated() + for (ch in 0 until channels) { + val outCh = e.outputBuffer.getChannelData(ch) + val data = separated[ch] + for (n in 0 until bufferSize) { + outCh[n] = SampleConvert.shortToFloat(data.getSampled(n * scale)) + } + } - override fun stop() { - audioCopy.pause() - current = 0.seconds + } + this.node?.connect(ctx.destination) } } + nodeRunning = true + missingDataCount = 0 + } + + override fun internalStop() { + if (!nodeRunning) return + startPromise?.cancel() + this.node?.disconnect() + nodeRunning = false } } diff --git a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt b/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt deleted file mode 100644 index d97d4ac127..0000000000 --- a/korge-core/src/js/korlibs/audio/sound/NativeAudioStreamJs.kt +++ /dev/null @@ -1,64 +0,0 @@ -package korlibs.audio.sound - -import korlibs.audio.internal.* -import korlibs.io.lang.* -import korlibs.platform.* -import kotlin.coroutines.* - -actual val nativeSoundProvider: NativeSoundProvider by lazy { - if (Platform.isJsBrowser) { - HtmlNativeSoundProvider() - } else { - DummyNativeSoundProvider - } -} - -class JsNewPlatformAudioOutput( - coroutineContext: CoroutineContext, - nchannels: Int, - freq: Int, - gen: (AudioSamplesInterleaved) -> Unit -) : NewPlatformAudioOutput( - coroutineContext, nchannels, freq, gen -) { - init { - nativeSoundProvider // Ensure it is created - } - - var missingDataCount = 0 - var nodeRunning = false - var node: ScriptProcessorNode? = null - - private var startPromise: Cancellable? = null - - override fun internalStart() { - if (nodeRunning) return - startPromise = HtmlSimpleSound.callOnUnlocked { - node = HtmlSimpleSound.ctx?.createScriptProcessor(1024, channels, 2) - node?.onaudioprocess = { e -> - val nchannels = e.outputBuffer.numberOfChannels - val outChannels = Array(nchannels) { e.outputBuffer.getChannelData(it) } - val nsamples = e.outputBuffer.getChannelData(0).size - val samples = AudioSamplesInterleaved(nchannels, nsamples) - genSafe(samples) - for (ch in 0 until nchannels) { - val outCh = outChannels[ch] - for (n in 0 until nsamples) { - outCh[n] = SampleConvert.shortToFloat(samples[ch, n]) - } - } - - } - if (HtmlSimpleSound.ctx != null) this.node?.connect(HtmlSimpleSound.ctx.destination) - } - nodeRunning = true - missingDataCount = 0 - } - - override fun internalStop() { - if (!nodeRunning) return - startPromise?.cancel() - this.node?.disconnect() - nodeRunning = false - } -} diff --git a/korge-foundation/src/common/korlibs/memory/Arrays.kt b/korge-foundation/src/common/korlibs/memory/Arrays.kt index 51d5d4b6d8..adc3289195 100644 --- a/korge-foundation/src/common/korlibs/memory/Arrays.kt +++ b/korge-foundation/src/common/korlibs/memory/Arrays.kt @@ -368,3 +368,22 @@ public inline class FloatArrayFromIntArray(public val base: IntArray) { public fun IntArray.asFloatArray(): FloatArrayFromIntArray = FloatArrayFromIntArray(this) /** Gets the underlying array of [this] */ public fun FloatArrayFromIntArray.asIntArray(): IntArray = base + +internal inline fun getSampledGeneric(index: Float, get: (Int) -> T, scale: (T, Float) -> Float, convert: (Float) -> T, unit: Unit = Unit): T { + val index0 = index.toInt() + val v0 = get(index0) + if (index0.toFloat() == index) return v0 + val v1 = get(index0 + 1) + val ratio = index % 1f + val o0 = scale(v0, 1f - ratio) + val o1 = scale(v1, ratio) + return convert(o0 + o1) +} + + +fun ByteArray.getSampled(index: Float): Byte = getSampledGeneric(index, get = { this[it] }, scale = { value, scale -> value * scale }, convert = { it.toInt().toByte() }) +fun UByteArray.getSampled(index: Float): UByte = getSampledGeneric(index, get = { this[it] }, scale = { value, scale -> value.toInt() * scale }, convert = { it.toInt().toUByte() }) +fun ShortArray.getSampled(index: Float): Short = getSampledGeneric(index, get = { this[it] }, scale = { value, scale -> value * scale }, convert = { it.toInt().toShort() }) +fun UShortArray.getSampled(index: Float): UShort = getSampledGeneric(index, get = { this[it] }, scale = { value, scale -> value.toInt() * scale }, convert = { it.toInt().toUShort() }) +fun CharArray.getSampled(index: Float): Char = getSampledGeneric(index, get = { this[it] }, scale = { value, scale -> value.toInt() * scale }, convert = { it.toInt().toChar() }) +fun FloatArray.getSampled(index: Float): Float = getSampledGeneric(index, get = { this[it] }, scale = { value, scale -> value * scale }, convert = { it }) diff --git a/korge-foundation/test/common/korlibs/memory/ArraysTest.kt b/korge-foundation/test/common/korlibs/memory/ArraysTest.kt index 3db98c280c..20dcbd42a6 100644 --- a/korge-foundation/test/common/korlibs/memory/ArraysTest.kt +++ b/korge-foundation/test/common/korlibs/memory/ArraysTest.kt @@ -1,7 +1,6 @@ package korlibs.memory -import kotlin.test.Test -import kotlin.test.assertEquals +import kotlin.test.* class ArraysTest { @Test @@ -24,4 +23,18 @@ class ArraysTest { assertEquals(-1, bytes.indexOf(byteArrayOf(2, 3, 4, 5))) assertEquals(-1, bytes.lastIndexOf(byteArrayOf(2, 3, 4, 5))) } + + @Test + fun testGetSampled() { + val array = byteArrayOf(0, 127, 64) + assertEquals(0, array.getSampled(0f)) + assertEquals(15, array.getSampled(.125f)) + assertEquals(63, array.getSampled(.5f)) + assertEquals(111, array.getSampled(.875f)) + assertEquals(127, array.getSampled(1f)) + assertEquals(119, array.getSampled(1.125f)) + assertEquals(95, array.getSampled(1.5f)) + assertEquals(71, array.getSampled(1.875f)) + assertEquals(64, array.getSampled(2f)) + } } From e5f274d71c2fc013b927b4665bef6c22d056e965 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 14:08:16 +0200 Subject: [PATCH 51/66] More work --- .../gradle/targets/android/AndroidConfig.kt | 4 +-- .../korge/gradle/targets/ios/IosXcodegen.kt | 12 +++----- .../audio/sound/AndroidNativeSoundProvider.kt | 12 ++++---- .../src/common/korlibs/audio/sound/Sound.kt | 5 ++-- .../datastructure/_Datastructure_event.kt | 28 ++++++++++++++++--- .../korlibs/render/KorgwSurfaceView.kt | 2 +- .../korlibs/korge/KorgeViewsConfigureInput.kt | 4 +++ korge/src/common/korlibs/render/GameWindow.kt | 2 -- .../common/korlibs/render/GameWindowExt.kt | 1 - 9 files changed, 43 insertions(+), 27 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt index 017a4eb4fb..c16914e988 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/android/AndroidConfig.kt @@ -113,11 +113,11 @@ data class AndroidGenerated constructor( fun getAndroidResFolder(isKorge: Boolean): File { //return File(project.projectDir, "src/androidMain/res") - return File(buildDir, "androidres") + return File(buildDir, "platforms/android/androires").ensureParents() } fun getAndroidSrcFolder(isKorge: Boolean): File { //return File(project.projectDir, "src/androidMain/kotlin") - return File(buildDir, "androidsrc") + return File(buildDir, "platforms/android/androisrc").ensureParents() } companion object { diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosXcodegen.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosXcodegen.kt index d1f3726fff..b56504cd3e 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosXcodegen.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosXcodegen.kt @@ -1,19 +1,15 @@ package korlibs.korge.gradle.targets.ios -import korlibs.korge.gradle.util.FileList -import korlibs.korge.gradle.util.execLogger -import korlibs.korge.gradle.util.projectExtension -import korlibs.korge.gradle.util.takeIfExists -import org.gradle.api.Project -import java.io.File +import korlibs.korge.gradle.util.* +import org.gradle.api.* +import java.io.* val Project.iosXcodegenExt by projectExtension { IosXcodegen(this) } class IosXcodegen(val project: Project) { - //val xcodeGenGitTag = "2.25.0" - val xcodeGenGitTag = "2.35.0" + val xcodeGenGitTag = "2.37.0" val korlibsFolder = File(System.getProperty("user.home") + "/.korlibs").apply { mkdirs() } val xcodeGenFolder = File(korlibsFolder, "XcodeGen-$xcodeGenGitTag") val xcodeGenLocalExecutable = File("/usr/local/bin/xcodegen") diff --git a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt index cb680a1e3d..a670fe0b7d 100644 --- a/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt +++ b/korge-core/src/android/korlibs/audio/sound/AndroidNativeSoundProvider.kt @@ -3,6 +3,7 @@ package korlibs.audio.sound import android.content.* import android.media.* import android.os.* +import korlibs.datastructure.event.* import korlibs.datastructure.thread.* import korlibs.io.android.* import kotlin.coroutines.* @@ -23,13 +24,8 @@ class AndroidNativeSoundProvider : NativeSoundProviderNew() { return AndroidNewPlatformAudioOutput(this, coroutineContext, channels, frequency, gen) } - override var paused: Boolean = false - set(value) { - if (field != value) { - field = value - //(this as java.lang.Object).notifyAll() - } - } + private val pauseable = SyncPauseable() + override var paused: Boolean by pauseable::paused fun ensureAudioManager(coroutineContext: CoroutineContext) { if (audioManager == null) { @@ -94,6 +90,8 @@ class AndroidNativeSoundProvider : NativeSoundProviderNew() { try { while (thread.threadSuggestRunning) { + provider.pauseable.checkPaused() + if (this.paused) { at.pause() Thread.sleep(20L) diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 4cf186d2d4..86635c0be3 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -2,6 +2,7 @@ package korlibs.audio.sound import korlibs.audio.format.* import korlibs.datastructure.* +import korlibs.datastructure.event.* import korlibs.io.async.* import korlibs.io.file.* import korlibs.io.lang.* @@ -46,10 +47,10 @@ open class NativeSoundProviderNew : NativeSoundProvider() { PlatformAudioOutputBasedOnNew(this, coroutineContext, freq) } -open class NativeSoundProvider() : Disposable { +open class NativeSoundProvider() : Disposable, Pauseable { open val target: String = "unknown" - open var paused: Boolean = false + override var paused: Boolean = false @Deprecated("") open fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int = 44100): PlatformAudioOutput = PlatformAudioOutput(coroutineContext, freq) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 65d9736708..fd5f319b8c 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -10,15 +10,32 @@ import korlibs.logger.* import korlibs.time.* import kotlin.time.* -interface EventLoop : Closeable { +interface Pauseable { + var paused: Boolean +} + +interface EventLoop : Pauseable, Closeable { fun setImmediate(task: () -> Unit) fun setTimeout(time: TimeSpan, task: () -> Unit): Closeable fun setInterval(time: TimeSpan, task: () -> Unit): Closeable } fun EventLoop.setInterval(time: Frequency, task: () -> Unit): Closeable = setInterval(time.timeSpan, task) -abstract class BaseEventLoop : EventLoop { - val runLock = Lock() +class SyncPauseable : Pauseable { + val pausedLock = Lock() + override var paused: Boolean = false + set(value) { + if (field != value) { + field = value + pausedLock { pausedLock.notify() } + } + } + fun checkPaused() { + while (paused) { pausedLock { pausedLock.wait(60.seconds) } } + } +} + +abstract class BaseEventLoop : EventLoop, Pauseable { } class SyncEventLoop( @@ -27,7 +44,9 @@ class SyncEventLoop( var precise: Boolean = false, /** Execute timers immediately instead of waiting. Useful for testing. */ var immediateRun: Boolean = false, -) : BaseEventLoop() { +) : BaseEventLoop(), Pauseable { + private val pauseable = SyncPauseable() + override var paused: Boolean by pauseable::paused private val lock = NonRecursiveLock() private var running = true protected class TimedTask(val eventLoop: SyncEventLoop, var now: Duration, val time: Duration, var interval: Boolean, val callback: () -> Unit) : Comparable, Closeable { @@ -186,6 +205,7 @@ class SyncEventLoop( // Timed tasks val stopwatch = Stopwatch().start() while (running) { + pauseable.checkPaused() val somethingExecuted = waitAndRunNextTask() if (lock { !somethingExecuted && tasks.isEmpty() && timedTasks.isEmpty() }) { diff --git a/korge/src/android/korlibs/render/KorgwSurfaceView.kt b/korge/src/android/korlibs/render/KorgwSurfaceView.kt index 79415dd274..de3ac544b9 100644 --- a/korge/src/android/korlibs/render/KorgwSurfaceView.kt +++ b/korge/src/android/korlibs/render/KorgwSurfaceView.kt @@ -70,7 +70,7 @@ open class KorgwSurfaceView constructor( override fun onSurfaceCreated(unused: GL10, config: EGLConfig) { //GLES20.glClearColor(0.0f, 0.4f, 0.7f, 1.0f) - gameWindow.handleContextLost() + gameWindow.ag.contextLost() clientVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { val out = IntArray(1) eglQueryContext(eglGetCurrentDisplay(), eglGetCurrentContext(), EGL_CONTEXT_CLIENT_VERSION, out, 0) diff --git a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt index e805b1324d..c880f3b0e4 100644 --- a/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt +++ b/korge/src/common/korlibs/korge/KorgeViewsConfigureInput.kt @@ -2,6 +2,7 @@ package korlibs.korge import korlibs.audio.sound.* import korlibs.datastructure.iterators.* +import korlibs.datastructure.lock.* import korlibs.event.* import korlibs.image.color.* import korlibs.inject.* @@ -155,11 +156,14 @@ internal fun Views.prepareViewsBase( gameWindow.onEvents(*DropFileEvent.Type.ALL) { e -> views.dispatch(e) } gameWindow.onEvent(ResumeEvent) { e -> views.dispatch(e) + Lock() nativeSoundProvider.paused = false + gameWindow.eventLoop.paused = false } gameWindow.onEvent(PauseEvent) { e -> views.dispatch(e) nativeSoundProvider.paused = true + gameWindow.eventLoop.paused = true } gameWindow.onEvent(StopEvent) { e -> views.dispatch(e) } gameWindow.onEvent(DestroyEvent) { e -> diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index baa151d8e1..e900657e74 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -306,8 +306,6 @@ open class GameWindow : //println("invalidatedView") continuousRenderMode.updated() } - @Deprecated("") - fun handleContextLost() = run { gameWindowInputState.contextLost = true } open fun updateGamepads() { } diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index 75e3a117e1..9ef87c0a53 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -49,7 +49,6 @@ class GameWindowInputState { var surfaceHeight = -1 var doInitialize = false var initialized = false - var contextLost = false } fun GameWindow.dispatchKeyEvent(type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { From 964ef56ed6728a94fc7b34180e791a1edc30d50d Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 14:31:17 +0200 Subject: [PATCH 52/66] More work --- .../korlibs/korge/gradle/targets/ios/Ios.kt | 2 +- .../korge/gradle/targets/ios/IosSdk.kt | 41 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt index 4bf7dc376c..9115e97967 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt @@ -227,7 +227,7 @@ fun Project.configureNativeIosTvosRun(targetName: String) { //} workingDir(xcodeProjDir) doFirst { - commandLine("xcrun", "xcodebuild", "-scheme", "app-$arch-$debugSuffix", "-project", ".", "-configuration", debugSuffix, "-derivedDataPath", "build", "-arch", arch2, "-sdk", iosSdkExt.appleFindSdk(sdkName)) + commandLine("xcrun", "xcodebuild", "-allowProvisioningUpdates", "-scheme", "app-$arch-$debugSuffix", "-project", ".", "-configuration", debugSuffix, "-derivedDataPath", "build", "-arch", arch2, "-sdk", iosSdkExt.appleFindSdk(sdkName)) println("COMMAND: ${commandLine.joinToString(" ")}") } } diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosSdk.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosSdk.kt index b8e8518a0c..ad43391519 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosSdk.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosSdk.kt @@ -1,13 +1,11 @@ package korlibs.korge.gradle.targets.ios import korlibs.korge.gradle.util.* -import org.gradle.api.Project -import java.io.ByteArrayInputStream -import java.io.IOException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate +import org.gradle.api.* +import java.io.* +import java.security.cert.* import java.util.* -import javax.security.auth.x500.X500Principal +import javax.security.auth.x500.* val Project.iosSdkExt by projectExtension { IosSdk(this) @@ -43,20 +41,37 @@ class IosSdk(val project: Project) { // https://gist.github.com/luckman212/ec52e9291f27bc39c2eecee07e7a9aa7 fun appleGetDefaultDeveloperCertificateTeamId(): String? { @Throws(IOException::class) - fun execCmd(cmd: String?): String { + fun execCmd(vararg cmd: String?): String { return Runtime.getRuntime().exec(cmd).inputStream.reader().readText() } - val certB64 = execCmd("security find-certificate -p") - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .lines() - .joinToString("") + fun certFromFilter(filter: String?): String? { + return execCmd(*buildList { + add("security") + add("find-certificate") + if (filter != null) { + add("-c") + add("Apple Development:") + } + add("-p") + }.toTypedArray()) + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .lines() + .joinToString("").trim().takeIf { it.isNotEmpty() } + } + + val certB64 = certFromFilter("Apple Development:") ?: certFromFilter(null) val cert = CertificateFactory.getInstance("X.509").generateCertificate(ByteArrayInputStream(Base64.getDecoder().decode(certB64))) as X509Certificate val subjectStr = cert.subjectX500Principal.getName(X500Principal.RFC2253) + val teamId = Regex("OU=([^,]+)").find(subjectStr)?.groups?.get(1)?.value + + //println("CERT=$cert") + //println("subjectStr=$subjectStr") + //println("teamId=$teamId") - return Regex("OU=(\\w+)").find(subjectStr)?.groups?.get(1)?.value + return teamId } fun appleGetDevices(os: String = "iOS"): List { From f8cad85fb324aba91c09afb24076518b1e21ab74 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 14:48:00 +0200 Subject: [PATCH 53/66] Update ios-deploy to 1.12.2 --- .../korge/gradle/targets/ios/IosDeploy.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt index c7033bd37c..ba8afe4359 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt @@ -1,9 +1,8 @@ package korlibs.korge.gradle.targets.ios -import org.gradle.api.Project -import korlibs.korge.gradle.util.projectExtension -import korlibs.korge.gradle.util.execLogger -import java.io.File +import korlibs.korge.gradle.util.* +import org.gradle.api.* +import java.io.* val Project.iosTvosDeployExt by projectExtension { IosDeploy(this) @@ -12,7 +11,10 @@ val Project.iosTvosDeployExt by projectExtension { val korgeCacheDir get() = File(System.getProperty("user.home"), ".korge").apply { mkdirs() } class IosDeploy(val project: Project) { - val iosDeployDir = File(korgeCacheDir, "ios-deploy") + val iosDeployVersion = "1.12.2" + //val iosDeployRepo = "https://github.com/korlibs/ios-deploy.git" + val iosDeployRepo = "https://github.com/ios-control/ios-deploy.git" + val iosDeployDir = File(korgeCacheDir, "ios-deploy-$iosDeployVersion") val iosDeployCmd = File(iosDeployDir, "build/Release/ios-deploy") val isInstalled get() = iosDeployCmd.exists() @@ -36,7 +38,12 @@ class IosDeploy(val project: Project) { fun clone() { iosDeployDir.mkdirs() project.execLogger { - it.commandLine("git", "clone", "https://github.com/korlibs/ios-deploy.git", iosDeployDir.absolutePath) + it.workingDir(iosDeployDir.absolutePath) + it.commandLine("git", "clone", iosDeployRepo, iosDeployDir.absolutePath) + } + project.execLogger { + it.workingDir(iosDeployDir.absolutePath) + it.commandLine("git", "checkout", iosDeployVersion) } } From c5629b7ec2fad525561c44f4e958d809f5bc1f04 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 16:55:13 +0200 Subject: [PATCH 54/66] Prepare for using xcrun devicectl device --- .../kotlin/korlibs/korge/gradle/targets/ios/Ios.kt | 4 ++-- .../korlibs/korge/gradle/targets/ios/IosDeploy.kt | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt index 9115e97967..06764e2786 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/Ios.kt @@ -252,7 +252,7 @@ fun Project.configureNativeIosTvosRun(targetName: String) { dependsOn(installIosTvosDeploy, buildTaskName) doLast { val appFolder = tasks.getByName(buildTaskName).outputs.files.first().parentFile - iosTvosDeployExt.command("--bundle", appFolder.absolutePath) + iosTvosDeployExt.install(appFolder.absolutePath) } } @@ -262,7 +262,7 @@ fun Project.configureNativeIosTvosRun(targetName: String) { dependsOn(installIosTvosDeploy, buildTaskName) doFirst { val appFolder = tasks.getByName(buildTaskName).outputs.files.first().parentFile - iosTvosDeployExt.command("--noninteractive", "-d", "--bundle", appFolder.absolutePath) + iosTvosDeployExt.installAndRun(appFolder.absolutePath) } } diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt index ba8afe4359..b2411d1daa 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosDeploy.kt @@ -19,6 +19,20 @@ class IosDeploy(val project: Project) { val isInstalled get() = iosDeployCmd.exists() + // https://github.com/ios-control/ios-deploy/issues/588 + // no ios-deploy required anymore? + // xcrun devicectl list devices -j /tmp/devices.json + // xcrun devicectl device install app --device 00008110-001XXXXXXXXXX ./korge-sandbox/build/platforms/ios/app.xcodeproj/build/Build/Products/Debug-iphoneos/unnamed.app + // crun devicectl device process launch --device 00008110-001XXXXXXXXXX file:///private/var/containers/Bundle/Application/1604D2D5-35F3-4E43-8B47-1DEF5D778480/nilo.app + + fun install(localAppPath: String) { + command("--bundle", localAppPath) + } + + fun installAndRun(localAppPath: String) { + command("--noninteractive", "-d", "--bundle", localAppPath) + } + fun command(vararg cmds: String) { project.execLogger { it.commandLine(iosDeployCmd, *cmds) From f72a6c68b91d26e797f18233fb39c6b04bf19881 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 17:39:09 +0200 Subject: [PATCH 55/66] More work --- .../gradle/targets/ios/IosProjectTools.kt | 3 +- .../korlibs/audio/format/AudioFormat.kt | 1 - .../src/common/korlibs/audio/sound/Sound.kt | 1 - .../audio/sound/fade/SoundChannelFade.kt | 3 -- .../src/common/korlibs/image/bitmap/Bitmap.kt | 1 - .../korlibs/image/font/DefaultTtfFont.kt | 6 +-- .../common/korlibs/image/font/FontRegistry.kt | 5 +- .../image/font/NativeSystemFontProvider.kt | 2 - .../src/common/korlibs/image/format/DDS.kt | 14 ++---- .../common/korlibs/image/format/ImageData.kt | 1 - .../korlibs/image/format/ImageOrientation.kt | 6 +-- .../src/common/korlibs/image/format/KRA.kt | 2 - .../image/format/NativeImageFormatProvider.kt | 1 - .../image/format/RegisteredImageFormats.kt | 1 - .../src/common/korlibs/image/style/CSS.kt | 12 ++--- .../src/common/korlibs/io/async/AsyncExt.kt | 2 - .../src/common/korlibs/io/dynamic/DynApi.kt | 3 -- .../common/korlibs/io/internal/TempBytes.kt | 7 +-- .../src/common/korlibs/io/lang/Charset.kt | 13 ++--- .../src/common/korlibs/io/lang/Environment.kt | 2 - .../src/common/korlibs/io/util/RangeExt.kt | 1 - .../common/korlibs/template/KorteDefaults.kt | 1 - .../audio/sound/CoreAudioSoundProvider.kt | 1 - .../korlibs/io/file/std/LocalVfsNative.kt | 19 ++----- .../io/file/std/StandardBasePathsIos.kt | 2 - .../io/lang/NativeThreadLocalNative.kt | 10 ++-- .../common/korlibs/image/bitmap/DemoTest.kt | 1 - .../datastructure/_Datastructure_random.kt | 8 +-- .../korlibs/datastructure/_Delegates.kt | 13 ++++- .../src/common/korlibs/logger/Console.kt | 1 - .../korlibs/math/geom/_MathGeom.bezier.kt | 1 - .../math/geom/_MathGeom.vector.VectorPath.kt | 9 ++-- .../src/common/korlibs/time/KlockLocale.kt | 1 - .../time/locale/ExtendedTimezoneNames.kt | 6 +-- .../kotlin/samples/MainRotatedAtlas.kt | 20 +++----- korge/src/common/korlibs/kgl/KmlGlExt.kt | 4 -- .../korge/bitmapfont/DebugBitmapFont.kt | 2 - .../common/korlibs/korge/input/KeysEvents.kt | 1 - .../common/korlibs/korge/input/MouseEvents.kt | 1 - .../korlibs/korge/render/LineRenderBatcher.kt | 1 - .../korge/service/haptic/HapticFeedback.kt | 1 - .../korge/service/storage/NativeStorage.kt | 2 - .../service/vibration/NativeVibration.kt | 1 - .../common/korlibs/korge/tween/tweenbase.kt | 1 - .../src/common/korlibs/korge/ui/UIComboBox.kt | 1 - .../common/korlibs/korge/ui/UIFocusManager.kt | 1 - .../korlibs/korge/view/filter/Filter.kt | 2 - .../korlibs/korge/view/filter/ViewFilter.kt | 2 - .../korge/view/filter/ViewFilterExt.kt | 1 - .../korlibs/korge/view/mask/ViewMask.kt | 4 -- .../korlibs/render/DefaultGameWindowIos.kt | 49 ++++++++++++------- .../render/DefaultGameWindowIosTools.kt | 2 + 52 files changed, 91 insertions(+), 165 deletions(-) diff --git a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosProjectTools.kt b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosProjectTools.kt index 2770330d7b..fc616ff065 100644 --- a/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosProjectTools.kt +++ b/buildSrc/src/main/kotlin/korlibs/korge/gradle/targets/ios/IosProjectTools.kt @@ -1,14 +1,13 @@ package korlibs.korge.gradle.targets.ios -import java.io.File import korlibs.korge.gradle.util.* import org.gradle.configurationcache.extensions.* +import java.io.* object IosProjectTools { fun genBootstrapKt(entrypoint: String): String = """ import $entrypoint - @ThreadLocal object NewAppDelegate : korlibs.render.KorgwBaseNewAppDelegate() { override fun applicationDidFinishLaunching(app: platform.UIKit.UIApplication) { applicationDidFinishLaunching(app) { ${entrypoint}() } } } diff --git a/korge-core/src/common/korlibs/audio/format/AudioFormat.kt b/korge-core/src/common/korlibs/audio/format/AudioFormat.kt index 1211cce273..8e697fc6d7 100644 --- a/korge-core/src/common/korlibs/audio/format/AudioFormat.kt +++ b/korge-core/src/common/korlibs/audio/format/AudioFormat.kt @@ -85,7 +85,6 @@ open class InvalidAudioFormatException(message: String) : RuntimeException(messa fun invalidAudioFormat(message: String = "invalid audio format"): Nothing = throw InvalidAudioFormatException(message) -@ThreadLocal val defaultAudioFormats by lazy { standardAudioFormats() } class AudioFormats : AudioFormat() { diff --git a/korge-core/src/common/korlibs/audio/sound/Sound.kt b/korge-core/src/common/korlibs/audio/sound/Sound.kt index 86635c0be3..9eaed7e8d1 100644 --- a/korge-core/src/common/korlibs/audio/sound/Sound.kt +++ b/korge-core/src/common/korlibs/audio/sound/Sound.kt @@ -14,7 +14,6 @@ import kotlin.coroutines.* import kotlin.native.concurrent.* import kotlin.coroutines.coroutineContext as coroutineContextKt -@ThreadLocal expect val nativeSoundProvider: NativeSoundProvider open class LazyNativeSoundProvider(val gen: () -> NativeSoundProvider) : NativeSoundProvider() { diff --git a/korge-core/src/common/korlibs/audio/sound/fade/SoundChannelFade.kt b/korge-core/src/common/korlibs/audio/sound/fade/SoundChannelFade.kt index aab794a717..93bc9ccc6e 100644 --- a/korge-core/src/common/korlibs/audio/sound/fade/SoundChannelFade.kt +++ b/korge-core/src/common/korlibs/audio/sound/fade/SoundChannelFade.kt @@ -7,14 +7,11 @@ import korlibs.math.* import korlibs.math.interpolation.* import korlibs.time.* import kotlinx.coroutines.* -import kotlin.native.concurrent.* val DEFAULT_FADE_TIME get() = 0.5.seconds val DEFAULT_FADE_EASING get() = Easing.LINEAR -@ThreadLocal private val SoundChannel.fadeThread by extraProperty { AsyncThread() } -@ThreadLocal private var SoundChannel.changing by extraProperty { false } private inline fun SoundChannel.changing(block: () -> T): T { changing = true diff --git a/korge-core/src/common/korlibs/image/bitmap/Bitmap.kt b/korge-core/src/common/korlibs/image/bitmap/Bitmap.kt index 03c9266228..7537b7cc91 100644 --- a/korge-core/src/common/korlibs/image/bitmap/Bitmap.kt +++ b/korge-core/src/common/korlibs/image/bitmap/Bitmap.kt @@ -37,7 +37,6 @@ abstract class Bitmap( //override fun getOrNull() = this //override suspend fun get() = this - //@ThreadLocal protected val tempInts: IntArray by lazy { IntArray(width * 2) } protected val tempRgba: RgbaArray get() = RgbaArray(tempInts) diff --git a/korge-core/src/common/korlibs/image/font/DefaultTtfFont.kt b/korge-core/src/common/korlibs/image/font/DefaultTtfFont.kt index ba08a15a61..d34a85b5f3 100644 --- a/korge-core/src/common/korlibs/image/font/DefaultTtfFont.kt +++ b/korge-core/src/common/korlibs/image/font/DefaultTtfFont.kt @@ -1,10 +1,9 @@ package korlibs.image.font -import korlibs.time.* +import korlibs.encoding.* import korlibs.io.compression.* import korlibs.io.compression.deflate.* -import korlibs.encoding.* -import kotlin.native.concurrent.* +import korlibs.time.* // Sani Trixie Sans // 1.6.1 - December 3, 2014 @@ -18,7 +17,6 @@ private fun DefaultTtfFontBytes() = "eNq0kwO8HFsSxv/Vmr4T20k/xZzYtu1J7maSp6vYtm3 // Do not use by lazy because that would be included in JS (because global property delegates are included in the output because they can have side effects) -@ThreadLocal private var DefaultTtfFontOrNull: TtfFont? = null val DefaultTtfFont: TtfFont get() { diff --git a/korge-core/src/common/korlibs/image/font/FontRegistry.kt b/korge-core/src/common/korlibs/image/font/FontRegistry.kt index e7ffcabaea..ff087f2aef 100644 --- a/korge-core/src/common/korlibs/image/font/FontRegistry.kt +++ b/korge-core/src/common/korlibs/image/font/FontRegistry.kt @@ -1,9 +1,7 @@ package korlibs.image.font import korlibs.datastructure.lock.* -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext -import kotlin.native.concurrent.ThreadLocal +import kotlin.coroutines.* interface FontRegistry { operator fun get(name: String?): Font @@ -14,7 +12,6 @@ interface FontRegistry { // TODO Can't use an object because that would be included in the JS output -@ThreadLocal private var SystemFontRegistryOrNull: DefaultFontRegistry? = null fun SystemFontRegistry(coroutineContext: CoroutineContext): DefaultFontRegistry { diff --git a/korge-core/src/common/korlibs/image/font/NativeSystemFontProvider.kt b/korge-core/src/common/korlibs/image/font/NativeSystemFontProvider.kt index ab77bcd390..86855456cd 100644 --- a/korge-core/src/common/korlibs/image/font/NativeSystemFontProvider.kt +++ b/korge-core/src/common/korlibs/image/font/NativeSystemFontProvider.kt @@ -16,7 +16,6 @@ import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set import kotlin.coroutines.* -import kotlin.native.concurrent.* internal fun createNativeSystemFontProvider(coroutineContext: CoroutineContext, platform: Platform = Platform): NativeSystemFontProvider = when { platform.runtime.isJs -> FallbackNativeSystemFontProvider(DefaultTtfFont) @@ -33,7 +32,6 @@ internal fun createNativeSystemFontProvider(coroutineContext: CoroutineContext, } } -@ThreadLocal var _nativeSystemFontProvider: NativeSystemFontProvider? = null fun nativeSystemFontProvider(coroutineContext: CoroutineContext): NativeSystemFontProvider = cacheLazyNullable(::_nativeSystemFontProvider) { createNativeSystemFontProvider(coroutineContext) } diff --git a/korge-core/src/common/korlibs/image/format/DDS.kt b/korge-core/src/common/korlibs/image/format/DDS.kt index dbac463e49..312cddef9b 100644 --- a/korge-core/src/common/korlibs/image/format/DDS.kt +++ b/korge-core/src/common/korlibs/image/format/DDS.kt @@ -1,15 +1,8 @@ package korlibs.image.format -import korlibs.datastructure.Extra -import korlibs.io.lang.invalidOp -import korlibs.io.stream.SyncStream -import korlibs.io.stream.openSync -import korlibs.io.stream.readAll -import korlibs.io.stream.readIntArrayLE -import korlibs.io.stream.readS32LE -import korlibs.io.stream.readStream -import korlibs.io.stream.readString -import kotlin.native.concurrent.ThreadLocal +import korlibs.datastructure.* +import korlibs.io.lang.* +import korlibs.io.stream.* object DDS : ImageFormat("dds") { override fun decodeHeader(s: SyncStream, props: ImageDecodingProps): ImageInfo? { @@ -67,5 +60,4 @@ object DDS : ImageFormat("dds") { } } -@ThreadLocal private var ImageInfo.fourcc by Extra.Property { " " } diff --git a/korge-core/src/common/korlibs/image/format/ImageData.kt b/korge-core/src/common/korlibs/image/format/ImageData.kt index 2d2c7241c6..873a58df30 100644 --- a/korge-core/src/common/korlibs/image/format/ImageData.kt +++ b/korge-core/src/common/korlibs/image/format/ImageData.kt @@ -52,7 +52,6 @@ open class ImageData constructor( data class ImageDataWithAtlas(val image: ImageData, val atlas: AtlasPacker.Result) -@ThreadLocal var ImageData.info: ImageInfo? by Extra.Property { null } //fun ImageData.packInAtlas(): ImageDataWithAtlas { diff --git a/korge-core/src/common/korlibs/image/format/ImageOrientation.kt b/korge-core/src/common/korlibs/image/format/ImageOrientation.kt index 4c96f22023..1678383305 100644 --- a/korge-core/src/common/korlibs/image/format/ImageOrientation.kt +++ b/korge-core/src/common/korlibs/image/format/ImageOrientation.kt @@ -1,11 +1,10 @@ package korlibs.image.format import korlibs.datastructure.* -import korlibs.image.atlas.MutableAtlasUnit +import korlibs.image.atlas.* import korlibs.image.bitmap.* -import korlibs.io.file.VfsFile +import korlibs.io.file.* import korlibs.math.geom.slice.* -import kotlin.native.concurrent.ThreadLocal typealias ImageOrientation = SliceOrientation @@ -19,6 +18,5 @@ suspend fun VfsFile.readBitmapSliceWithOrientation(props: ImageDecodingProps = I } } -@ThreadLocal var ImageInfo.orientation: ImageOrientation? by Extra.Property { null } val ImageInfo?.orientationSure: ImageOrientation get() = this?.orientation ?: ImageOrientation.ROTATE_0 diff --git a/korge-core/src/common/korlibs/image/format/KRA.kt b/korge-core/src/common/korlibs/image/format/KRA.kt index 87e33756f5..07de9ca2b7 100644 --- a/korge-core/src/common/korlibs/image/format/KRA.kt +++ b/korge-core/src/common/korlibs/image/format/KRA.kt @@ -257,7 +257,5 @@ object KRA : ImageFormat("kra") { } -@ThreadLocal var ImageDecodingProps.kritaLoadLayers by extraProperty { true } -@ThreadLocal var ImageDecodingProps.kritaPartialImageLayers by extraProperty { false } diff --git a/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt b/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt index d07f304c14..f4c32305bf 100644 --- a/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt +++ b/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.* import kotlin.math.* import kotlin.native.concurrent.* -@ThreadLocal expect val nativeImageFormatProvider: NativeImageFormatProvider data class NativeImageResult( diff --git a/korge-core/src/common/korlibs/image/format/RegisteredImageFormats.kt b/korge-core/src/common/korlibs/image/format/RegisteredImageFormats.kt index 81129a2333..c043232fb6 100644 --- a/korge-core/src/common/korlibs/image/format/RegisteredImageFormats.kt +++ b/korge-core/src/common/korlibs/image/format/RegisteredImageFormats.kt @@ -2,5 +2,4 @@ package korlibs.image.format import kotlin.native.concurrent.ThreadLocal -@ThreadLocal val RegisteredImageFormats: ImageFormatsMutable = ImageFormatsMutable() diff --git a/korge-core/src/common/korlibs/image/style/CSS.kt b/korge-core/src/common/korlibs/image/style/CSS.kt index 2614952aff..1aff370f4c 100644 --- a/korge-core/src/common/korlibs/image/style/CSS.kt +++ b/korge-core/src/common/korlibs/image/style/CSS.kt @@ -411,12 +411,12 @@ class CSSReader(val tokens: ListReader) { } } -@ThreadLocal val CSS.Expression.color: RGBA by extraPropertyThis { CSS.parseColor(exprStr) } -@ThreadLocal val CSS.Expression.ratio: Ratio by extraPropertyThis { CSS.parseRatio(exprStr) } -@ThreadLocal val CSS.Expression.matrix: Matrix by extraPropertyThis { CSS.parseTransform(exprStr) } -@ThreadLocal val CSS.Expression.transform: MatrixTransform by extraPropertyThis { matrix.immutable.decompose() } -@ThreadLocal val CSS.Expression.easing: Easing by extraPropertyThis { CSS.parseEasing(exprStr) } -@ThreadLocal val CSS.Declarations.animation: CSS.Animation? by extraPropertyThis { +val CSS.Expression.color: RGBA by extraPropertyThis { CSS.parseColor(exprStr) } +val CSS.Expression.ratio: Ratio by extraPropertyThis { CSS.parseRatio(exprStr) } +val CSS.Expression.matrix: Matrix by extraPropertyThis { CSS.parseTransform(exprStr) } +val CSS.Expression.transform: MatrixTransform by extraPropertyThis { matrix.immutable.decompose() } +val CSS.Expression.easing: Easing by extraPropertyThis { CSS.parseEasing(exprStr) } +val CSS.Declarations.animation: CSS.Animation? by extraPropertyThis { this["animation"]?.let { CSS.parseAnimation(it.exprStr) } } diff --git a/korge-core/src/common/korlibs/io/async/AsyncExt.kt b/korge-core/src/common/korlibs/io/async/AsyncExt.kt index ec6cfa57cf..e017a62307 100644 --- a/korge-core/src/common/korlibs/io/async/AsyncExt.kt +++ b/korge-core/src/common/korlibs/io/async/AsyncExt.kt @@ -6,7 +6,6 @@ import korlibs.platform.* import korlibs.time.* import kotlinx.coroutines.* import kotlin.coroutines.* -import kotlin.native.concurrent.* private val logger = Logger("AsyncExt") @@ -78,7 +77,6 @@ fun suspendTest(cond: () -> Boolean, timeout: TimeSpan? = DEFAULT_SUSPEND_TEST_T fun suspendTestNoBrowser(callback: suspend CoroutineScope.() -> Unit) = suspendTest({ !Platform.isJsBrowser }, callback = callback) fun suspendTestNoJs(callback: suspend CoroutineScope.() -> Unit) = suspendTest({ !Platform.isJs && !Platform.isWasm }, callback = callback) -@ThreadLocal val DEBUG_ASYNC_LAUNCH_ERRORS by lazy { Environment["DEBUG_ASYNC_LAUNCH_ERRORS"] == "true" } private fun CoroutineScope._launch(start: CoroutineStart, callback: suspend () -> Unit): Job = launch(coroutineContext, start = start) { diff --git a/korge-core/src/common/korlibs/io/dynamic/DynApi.kt b/korge-core/src/common/korlibs/io/dynamic/DynApi.kt index 3e0b6ae72c..d7681d7a49 100644 --- a/korge-core/src/common/korlibs/io/dynamic/DynApi.kt +++ b/korge-core/src/common/korlibs/io/dynamic/DynApi.kt @@ -1,7 +1,5 @@ package korlibs.io.dynamic -import kotlin.native.concurrent.ThreadLocal - interface DynApi { val global: Any? get() = null @@ -20,7 +18,6 @@ interface DynApi { val defaultDynApi: DynApi get() = DynamicInternal // @TODO: We should be able to plug-in a kotlinx-serialization version for this -@ThreadLocal var dynApi: DynApi = DynamicInternal internal expect object DynamicInternal : DynApi diff --git a/korge-core/src/common/korlibs/io/internal/TempBytes.kt b/korge-core/src/common/korlibs/io/internal/TempBytes.kt index abfbe1353b..80df679e3d 100644 --- a/korge-core/src/common/korlibs/io/internal/TempBytes.kt +++ b/korge-core/src/common/korlibs/io/internal/TempBytes.kt @@ -1,16 +1,13 @@ package korlibs.io.internal -import korlibs.datastructure.Pool -import korlibs.io.lang.threadLocal -import kotlin.native.concurrent.ThreadLocal +import korlibs.datastructure.* +import korlibs.io.lang.* @PublishedApi internal const val BYTES_TEMP_SIZE = 0x10000 @PublishedApi -@ThreadLocal internal val bytesTempPool by threadLocal { Pool(preallocate = 1) { ByteArray(BYTES_TEMP_SIZE) } } @PublishedApi -@ThreadLocal internal val smallBytesPool by threadLocal { Pool(preallocate = 16) { ByteArray(16) } } diff --git a/korge-core/src/common/korlibs/io/lang/Charset.kt b/korge-core/src/common/korlibs/io/lang/Charset.kt index ff76cb35e3..02b7b7c14a 100644 --- a/korge-core/src/common/korlibs/io/lang/Charset.kt +++ b/korge-core/src/common/korlibs/io/lang/Charset.kt @@ -1,12 +1,11 @@ package korlibs.io.lang -import korlibs.datastructure.IntIntMap -import korlibs.datastructure.iterators.fastForEach -import korlibs.datastructure.lock.NonRecursiveLock +import korlibs.datastructure.* +import korlibs.datastructure.iterators.* +import korlibs.datastructure.lock.* import korlibs.memory.* -import kotlin.math.min -import kotlin.native.concurrent.SharedImmutable -import kotlin.native.concurrent.ThreadLocal +import kotlin.math.* +import kotlin.native.concurrent.* fun interface CharsetProvider { operator fun invoke(normalizedName: String, name: String): Charset? @@ -14,9 +13,7 @@ fun interface CharsetProvider { expect val platformCharsetProvider: CharsetProvider -@ThreadLocal private val CHARSET_PROVIDERS = arrayListOf() -@ThreadLocal private val CHARSET_PROVIDERS_LOCK = NonRecursiveLock() diff --git a/korge-core/src/common/korlibs/io/lang/Environment.kt b/korge-core/src/common/korlibs/io/lang/Environment.kt index 7e2c5f9cda..7f0e5f9910 100644 --- a/korge-core/src/common/korlibs/io/lang/Environment.kt +++ b/korge-core/src/common/korlibs/io/lang/Environment.kt @@ -3,7 +3,6 @@ package korlibs.io.lang import korlibs.datastructure.* import korlibs.platform.* import kotlin.collections.set -import kotlin.native.concurrent.* internal expect object EnvironmentInternal { // Uses querystring on JS/Browser, and proper env vars in the rest @@ -12,7 +11,6 @@ internal expect object EnvironmentInternal { fun getAll(): Map } -@ThreadLocal private var customEnvironments: CaseInsensitiveStringMap? = null interface Environment { diff --git a/korge-core/src/common/korlibs/io/util/RangeExt.kt b/korge-core/src/common/korlibs/io/util/RangeExt.kt index 73553a4e1f..95dc885f52 100644 --- a/korge-core/src/common/korlibs/io/util/RangeExt.kt +++ b/korge-core/src/common/korlibs/io/util/RangeExt.kt @@ -1,6 +1,5 @@ package korlibs.io.util import kotlin.native.concurrent.ThreadLocal -@ThreadLocal val LONG_ZERO_TO_MAX_RANGE = 0L..Long.MAX_VALUE fun IntRange.toLongRange() = this.start.toLong()..this.endInclusive.toLong() diff --git a/korge-core/src/common/korlibs/template/KorteDefaults.kt b/korge-core/src/common/korlibs/template/KorteDefaults.kt index cbcc0742d2..c787ce3519 100644 --- a/korge-core/src/common/korlibs/template/KorteDefaults.kt +++ b/korge-core/src/common/korlibs/template/KorteDefaults.kt @@ -380,7 +380,6 @@ object KorteDefaultTags { ) } -@ThreadLocal var KorteTemplateConfig.debugPrintln by korteExtraProperty({ extra }) { { v: Any? -> println(v) } } object DefaultBlocks { diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index d9450559d2..dd51a7bd40 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -22,7 +22,6 @@ import kotlin.native.ThreadLocal actual val nativeSoundProvider: NativeSoundProvider get() = CORE_AUDIO_NATIVE_SOUND_PROVIDER expect fun appleInitAudio() -@ThreadLocal val CORE_AUDIO_NATIVE_SOUND_PROVIDER: CoreAudioNativeSoundProvider by lazy { CoreAudioNativeSoundProvider() } class CoreAudioNativeSoundProvider : NativeSoundProviderNew() { diff --git a/korge-core/src/darwin/korlibs/io/file/std/LocalVfsNative.kt b/korge-core/src/darwin/korlibs/io/file/std/LocalVfsNative.kt index 7bde2348ba..7a9f4f600e 100644 --- a/korge-core/src/darwin/korlibs/io/file/std/LocalVfsNative.kt +++ b/korge-core/src/darwin/korlibs/io/file/std/LocalVfsNative.kt @@ -1,43 +1,33 @@ package korlibs.io.file.std -import korlibs.time.* import korlibs.io.async.* import korlibs.io.file.* import korlibs.io.lang.* import korlibs.io.posix.* import korlibs.io.process.* import korlibs.io.stream.* +import korlibs.time.* import kotlinx.cinterop.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import platform.posix.* import kotlin.math.* -import kotlin.native.concurrent.* -@ThreadLocal val tmpdir: String by lazy { Environment["TMPDIR"] ?: Environment["TEMP"] ?: Environment["TMP"] ?: "/tmp" } - -@ThreadLocal var customCwd: String? = null -@ThreadLocal -val nativeCwd by lazy { StandardPaths.cwd } +val nativeCwd get() = StandardPaths.cwd val cwd: String get() = customCwd ?: nativeCwd -//@ThreadLocal //val cwdVfs: VfsFile by lazy { DynamicRootVfs(rootLocalVfsNative) { cwd } } val cwdVfs: VfsFile get() = rootLocalVfsNative[cwd] -@ThreadLocal actual val standardVfs: StandardVfs = object : StandardVfs() { - override val resourcesVfs: VfsFile by lazy { applicationDataVfs.jail() } + override val resourcesVfs: VfsFile get() = applicationDataVfs.jail() override val rootLocalVfs: VfsFile get() = applicationDataVfs } -@ThreadLocal actual val cacheVfs: VfsFile by lazy { MemoryVfs() } - -@ThreadLocal actual val tempVfs: VfsFile by lazy { jailedLocalVfs(tmpdir) } actual val applicationVfs: VfsFile get() = rootLocalVfsNative[customCwd ?: StandardPaths.resourcesFolder] @@ -45,10 +35,7 @@ actual val applicationDataVfs: VfsFile get() = applicationVfs actual val externalStorageVfs: VfsFile get() = applicationVfs actual val userHomeVfs: VfsFile get() = jailedLocalVfs(StandardPaths.userHome) -@ThreadLocal val rootLocalVfsNative by lazy { LocalVfsNative(async = true) } - -@ThreadLocal val rootLocalVfsNativeSync by lazy { LocalVfsNative(async = false) } actual fun localVfs(path: String, async: Boolean): VfsFile = diff --git a/korge-core/src/darwin/korlibs/io/file/std/StandardBasePathsIos.kt b/korge-core/src/darwin/korlibs/io/file/std/StandardBasePathsIos.kt index f100987c78..097e53b505 100644 --- a/korge-core/src/darwin/korlibs/io/file/std/StandardBasePathsIos.kt +++ b/korge-core/src/darwin/korlibs/io/file/std/StandardBasePathsIos.kt @@ -1,6 +1,4 @@ package korlibs.io.file.std -import platform.Foundation.* - actual object StandardPaths : StandardBasePathsDarwin(), StandardPathsBase { } diff --git a/korge-core/src/darwin/korlibs/io/lang/NativeThreadLocalNative.kt b/korge-core/src/darwin/korlibs/io/lang/NativeThreadLocalNative.kt index f5a9778dc8..be2e6ebb9a 100644 --- a/korge-core/src/darwin/korlibs/io/lang/NativeThreadLocalNative.kt +++ b/korge-core/src/darwin/korlibs/io/lang/NativeThreadLocalNative.kt @@ -1,8 +1,12 @@ package korlibs.io.lang +import korlibs.datastructure.lock.* +import korlibs.datastructure.thread.* + actual abstract class NativeThreadLocal { + private val lock = Lock() + private val perThread = LinkedHashMap() actual abstract fun initialValue(): T - private var value = initialValue() - actual fun get(): T = value - actual fun set(value: T) { this.value = value } + actual fun get(): T = lock { perThread.getOrPut(NativeThread.currentThreadId) { initialValue() } } + actual fun set(value: T) { lock { perThread[NativeThread.currentThreadId] = value } } } diff --git a/korge-core/test/common/korlibs/image/bitmap/DemoTest.kt b/korge-core/test/common/korlibs/image/bitmap/DemoTest.kt index 7973408eb7..02599acba3 100644 --- a/korge-core/test/common/korlibs/image/bitmap/DemoTest.kt +++ b/korge-core/test/common/korlibs/image/bitmap/DemoTest.kt @@ -43,7 +43,6 @@ object DebugBitmapFont { val DEBUG_FONT_BYTES: ByteArray = "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".fromBase64() } -@ThreadLocal // It is mutable because of the Extra, so can't use SharedImmutable val debugBmpFont: BitmapFont by lazy { val tex = PNG.decode(DebugBitmapFont.DEBUG_FONT_BYTES).toBMP32().premultiplied().slice() val fntAdvance = 7.0 diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_random.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_random.kt index 397e1818a0..5911ed8094 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_random.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_random.kt @@ -2,10 +2,9 @@ package korlibs.datastructure.random -import kotlin.native.concurrent.ThreadLocal -import kotlin.random.Random +import korlibs.datastructure.lock.* +import kotlin.random.* -@ThreadLocal private val LocalFastRandom = FastRandom(Random.Default.nextLong()) // Copy of XorWowRandom from the Kotlin Standard library but optimizing some methods @@ -19,7 +18,8 @@ open class FastRandom private constructor( ) : Random() { companion object : FastRandom() { val Default get() = LocalFastRandom - override fun nextBits(bitCount: Int): Int = LocalFastRandom.nextBits(bitCount) + private val lock = Lock() + override fun nextBits(bitCount: Int): Int = lock { LocalFastRandom.nextBits(bitCount) } } private constructor(seed1: Int, seed2: Int) : diff --git a/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt b/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt index cb95db802d..0dd9ebdf82 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt @@ -1,9 +1,18 @@ package korlibs.datastructure +import korlibs.datastructure.lock.* import kotlin.reflect.* -typealias ExtraType = FastStringMap? -fun ExtraTypeCreate() = FastStringMap() +class ExtraObject { + private val lock = Lock() + private val data = FastStringMap() + fun get(key: String): Any? = lock { data[key] } + fun set(key: String, value: Any?) = lock { data[key] = value } + fun contains(key: String): Boolean = lock { key in data } +} + +typealias ExtraType = ExtraObject? +fun ExtraTypeCreate() = ExtraObject() interface Extra { var extra: ExtraType diff --git a/korge-foundation/src/common/korlibs/logger/Console.kt b/korge-foundation/src/common/korlibs/logger/Console.kt index 3e1c0d98d0..da2b010d47 100644 --- a/korge-foundation/src/common/korlibs/logger/Console.kt +++ b/korge-foundation/src/common/korlibs/logger/Console.kt @@ -2,7 +2,6 @@ package korlibs.logger import kotlin.native.concurrent.ThreadLocal -@ThreadLocal @PublishedApi internal var baseConsoleHook: (( kind: BaseConsole.Kind, msg: Array, diff --git a/korge-foundation/src/common/korlibs/math/geom/_MathGeom.bezier.kt b/korge-foundation/src/common/korlibs/math/geom/_MathGeom.bezier.kt index cfef84a8cd..6c8ac14571 100644 --- a/korge-foundation/src/common/korlibs/math/geom/_MathGeom.bezier.kt +++ b/korge-foundation/src/common/korlibs/math/geom/_MathGeom.bezier.kt @@ -1837,7 +1837,6 @@ fun Curves.toNonCurveSimplePointList(out: PointArrayList = PointArrayList()): Po return out } -@ThreadLocal val Curves.isConvex: Boolean by extraPropertyThis { this.assumeConvex || Convex.isConvex(this) } fun Curves.toDashes(pattern: DoubleArray?, offset: Double = 0.0): List { diff --git a/korge-foundation/src/common/korlibs/math/geom/_MathGeom.vector.VectorPath.kt b/korge-foundation/src/common/korlibs/math/geom/_MathGeom.vector.VectorPath.kt index ecb8958c12..292d588a38 100644 --- a/korge-foundation/src/common/korlibs/math/geom/_MathGeom.vector.VectorPath.kt +++ b/korge-foundation/src/common/korlibs/math/geom/_MathGeom.vector.VectorPath.kt @@ -12,7 +12,6 @@ import korlibs.math.geom.ds.* import korlibs.math.geom.shape.* import korlibs.math.geom.trapezoid.* import korlibs.number.* -import kotlin.native.concurrent.* interface IVectorPath : VectorBuilder { fun toSvgString(): String @@ -536,10 +535,10 @@ fun VectorPath.applyTransform(m: Matrix): VectorPath = when { else -> this } -@ThreadLocal private var VectorPath._bvhCurvesCacheVersion by extraProperty { -1 } -@ThreadLocal private var VectorPath._bvhCurvesCache by extraProperty?> { null } -@ThreadLocal private var VectorPath._curvesCacheVersion by extraProperty { -1 } -@ThreadLocal private var VectorPath._curvesCache by extraProperty?> { null } +private var VectorPath._bvhCurvesCacheVersion by extraProperty { -1 } +private var VectorPath._bvhCurvesCache by extraProperty?> { null } +private var VectorPath._curvesCacheVersion by extraProperty { -1 } +private var VectorPath._curvesCache by extraProperty?> { null } fun VectorPath.getBVHBeziers(): BVH2D { if (_bvhCurvesCacheVersion != version) { diff --git a/korge-foundation/src/common/korlibs/time/KlockLocale.kt b/korge-foundation/src/common/korlibs/time/KlockLocale.kt index 88c68e11bb..c20846a574 100644 --- a/korge-foundation/src/common/korlibs/time/KlockLocale.kt +++ b/korge-foundation/src/common/korlibs/time/KlockLocale.kt @@ -3,7 +3,6 @@ package korlibs.time import korlibs.time.internal.substr import kotlin.native.concurrent.ThreadLocal -@ThreadLocal private var KlockLocale_default: KlockLocale? = null abstract class KlockLocale { diff --git a/korge-foundation/src/common/korlibs/time/locale/ExtendedTimezoneNames.kt b/korge-foundation/src/common/korlibs/time/locale/ExtendedTimezoneNames.kt index 02d1be6ffb..7f048b2af8 100644 --- a/korge-foundation/src/common/korlibs/time/locale/ExtendedTimezoneNames.kt +++ b/korge-foundation/src/common/korlibs/time/locale/ExtendedTimezoneNames.kt @@ -1,16 +1,12 @@ package korlibs.time.locale -import korlibs.time.TimezoneNames -import korlibs.time.hours -import korlibs.time.minutes -import kotlin.native.concurrent.ThreadLocal +import korlibs.time.* private fun utc(hours: Int, minutes: Int = 0) = hours.hours + minutes.minutes // @TODO: Should we include the most popular timezones increasing the artifact size? Maybe include a plugin mechanism and a registration in klock-locale? // @TODO: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations -@ThreadLocal private var ExtendedTimezoneNamesOrNull: TimezoneNames? = null val ExtendedTimezoneNames: TimezoneNames get() { diff --git a/korge-sandbox/src/commonMain/kotlin/samples/MainRotatedAtlas.kt b/korge-sandbox/src/commonMain/kotlin/samples/MainRotatedAtlas.kt index 5371aaf22a..623960f220 100644 --- a/korge-sandbox/src/commonMain/kotlin/samples/MainRotatedAtlas.kt +++ b/korge-sandbox/src/commonMain/kotlin/samples/MainRotatedAtlas.kt @@ -1,18 +1,11 @@ package samples -import korlibs.korge.scene.Scene -import korlibs.korge.view.SContainer -import korlibs.korge.view.image -import korlibs.korge.view.solidRect -import korlibs.korge.view.xy -import korlibs.image.atlas.Atlas -import korlibs.image.atlas.readAtlas -import korlibs.image.bitmap.* -import korlibs.image.color.RGBA -import korlibs.image.format.ImageOrientation -import korlibs.io.file.std.resourcesVfs -import kotlin.math.max -import kotlin.native.concurrent.ThreadLocal +import korlibs.image.atlas.* +import korlibs.image.color.* +import korlibs.io.file.std.* +import korlibs.korge.scene.* +import korlibs.korge.view.* +import kotlin.math.* class MainRotatedAtlas : Scene() { override suspend fun SContainer.sceneMain() { @@ -68,7 +61,6 @@ class MainRotatedAtlas : Scene() { } class BG { - @ThreadLocal companion object { private val colAr = arrayOf(RGBA(240, 240, 240), RGBA(220, 220, 220)) private var cur = true diff --git a/korge/src/common/korlibs/kgl/KmlGlExt.kt b/korge/src/common/korlibs/kgl/KmlGlExt.kt index 73784ac1af..3f1580b586 100644 --- a/korge/src/common/korlibs/kgl/KmlGlExt.kt +++ b/korge/src/common/korlibs/kgl/KmlGlExt.kt @@ -4,16 +4,12 @@ import korlibs.encoding.* import korlibs.logger.* import korlibs.math.geom.* import korlibs.memory.* -import kotlin.native.concurrent.* class KmlGlException(message: String) : RuntimeException(message) private val logger = Logger("KmlGlException") -@ThreadLocal private val tempNBufferByte = Buffer(1, direct = true) -@ThreadLocal private val tempNBuffer1 = Buffer(4, direct = true) -@ThreadLocal private val tempNBuffer4 = Buffer(4 * 4, direct = true) private inline fun tempByte1Buffer(value: Int = 0, block: (Buffer) -> Unit): Int = tempNBufferByte.let { diff --git a/korge/src/common/korlibs/korge/bitmapfont/DebugBitmapFont.kt b/korge/src/common/korlibs/korge/bitmapfont/DebugBitmapFont.kt index e13965aa5d..32df646c1e 100644 --- a/korge/src/common/korlibs/korge/bitmapfont/DebugBitmapFont.kt +++ b/korge/src/common/korlibs/korge/bitmapfont/DebugBitmapFont.kt @@ -6,11 +6,9 @@ import korlibs.image.bitmap.* import korlibs.image.font.* import korlibs.image.format.* import korlibs.io.stream.* -import kotlin.native.concurrent.* private val DEBUG_FONT_BYTES: ByteArray get() = "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".fromBase64() -@ThreadLocal // It is mutable because of the Extra, so can't use SharedImmutable private var debugBmpFontOrNull: BitmapFont? = null private fun debugBmpFont(tex: BmpSlice): BitmapFont { diff --git a/korge/src/common/korlibs/korge/input/KeysEvents.kt b/korge/src/common/korlibs/korge/input/KeysEvents.kt index 3929949444..b9874ae6d8 100644 --- a/korge/src/common/korlibs/korge/input/KeysEvents.kt +++ b/korge/src/common/korlibs/korge/input/KeysEvents.kt @@ -143,7 +143,6 @@ class KeysEvents(val view: View) : Closeable { } } -@ThreadLocal val View.keys: KeysEvents by Extra.PropertyThis { KeysEvents(this) } inline fun View.newKeys(callback: KeysEvents.() -> Unit): KeysEvents = KeysEvents(this).also { diff --git a/korge/src/common/korlibs/korge/input/MouseEvents.kt b/korge/src/common/korlibs/korge/input/MouseEvents.kt index ceef5afdae..d599f2b778 100644 --- a/korge/src/common/korlibs/korge/input/MouseEvents.kt +++ b/korge/src/common/korlibs/korge/input/MouseEvents.kt @@ -636,7 +636,6 @@ fun MouseEvents.multiClick(count: Int, callback: (MouseEvents) -> Unit): Closeab } } -@ThreadLocal // @TODO: Is this required? var View.cursor: ICursor? by extraProperty { null } // get() = mouse.cursor diff --git a/korge/src/common/korlibs/korge/render/LineRenderBatcher.kt b/korge/src/common/korlibs/korge/render/LineRenderBatcher.kt index 10f24a9ed5..ebd1c5eebd 100644 --- a/korge/src/common/korlibs/korge/render/LineRenderBatcher.kt +++ b/korge/src/common/korlibs/korge/render/LineRenderBatcher.kt @@ -17,7 +17,6 @@ import kotlin.native.concurrent.* /** Creates/gets a [LineRenderBatcher] associated to [this] [RenderContext] */ @Deprecated("USe useDebugLineRenderContext instead") -@ThreadLocal val RenderContext.debugLineRenderContext: LineRenderBatcher by Extra.PropertyThis { LineRenderBatcher(this) } @Suppress("DEPRECATION") diff --git a/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt b/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt index f7705ce53e..7059dad64f 100644 --- a/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt +++ b/korge/src/common/korlibs/korge/service/haptic/HapticFeedback.kt @@ -7,7 +7,6 @@ import korlibs.render.* import korlibs.time.* import kotlin.native.concurrent.* -@ThreadLocal val Views.hapticFeedback by extraPropertyThis { HapticFeedback(this) } // https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/haptics/ diff --git a/korge/src/common/korlibs/korge/service/storage/NativeStorage.kt b/korge/src/common/korlibs/korge/service/storage/NativeStorage.kt index b130b910b3..04585277c2 100644 --- a/korge/src/common/korlibs/korge/service/storage/NativeStorage.kt +++ b/korge/src/common/korlibs/korge/service/storage/NativeStorage.kt @@ -5,7 +5,6 @@ import korlibs.datastructure.lock.* import korlibs.io.serialization.json.* import korlibs.korge.view.* import kotlin.collections.* -import kotlin.native.concurrent.* /** Cross-platform way of synchronously storing small data */ //expect fun NativeStorage(views: Views): IStorageWithKeys @@ -18,7 +17,6 @@ expect class NativeStorage(views: Views) : IStorageWithKeys { override fun removeAll() } -@ThreadLocal val Views.storage: NativeStorage by Extra.PropertyThis { NativeStorage(this) } val ViewsContainer.storage: NativeStorage get() = this.views.storage diff --git a/korge/src/common/korlibs/korge/service/vibration/NativeVibration.kt b/korge/src/common/korlibs/korge/service/vibration/NativeVibration.kt index fa0c881853..a7ab78b7ca 100644 --- a/korge/src/common/korlibs/korge/service/vibration/NativeVibration.kt +++ b/korge/src/common/korlibs/korge/service/vibration/NativeVibration.kt @@ -5,7 +5,6 @@ import korlibs.time.TimeSpan import korlibs.korge.view.Views import kotlin.native.concurrent.ThreadLocal -@ThreadLocal val Views.vibration by extraPropertyThis { NativeVibration(this) } /** diff --git a/korge/src/common/korlibs/korge/tween/tweenbase.kt b/korge/src/common/korlibs/korge/tween/tweenbase.kt index 617847172b..07514d9be4 100644 --- a/korge/src/common/korlibs/korge/tween/tweenbase.kt +++ b/korge/src/common/korlibs/korge/tween/tweenbase.kt @@ -54,7 +54,6 @@ data class V2( "V2(key=${key.name}, range=[$initial-$end], startTime=$startTime, duration=$duration)" } -@ThreadLocal private object V2CallbackSupport { var dummy: Unit = Unit } diff --git a/korge/src/common/korlibs/korge/ui/UIComboBox.kt b/korge/src/common/korlibs/korge/ui/UIComboBox.kt index b365df3420..7d1ec1b402 100644 --- a/korge/src/common/korlibs/korge/ui/UIComboBox.kt +++ b/korge/src/common/korlibs/korge/ui/UIComboBox.kt @@ -28,7 +28,6 @@ inline fun Container.uiComboBox( block: @ViewDslMarker UIComboBox.() -> Unit = {} ) = UIComboBox(size, selectedIndex, items).addTo(this).apply(block) -@ThreadLocal var Views.openedComboBox by Extra.Property?>() { null } open class UIComboBox( diff --git a/korge/src/common/korlibs/korge/ui/UIFocusManager.kt b/korge/src/common/korlibs/korge/ui/UIFocusManager.kt index f6bd2e8722..b0b4651268 100644 --- a/korge/src/common/korlibs/korge/ui/UIFocusManager.kt +++ b/korge/src/common/korlibs/korge/ui/UIFocusManager.kt @@ -33,7 +33,6 @@ var UIFocusable.focused: Boolean UIFocusManager.Scope.focusView.stage?.uiFocusManager?.uiFocusedView = if (value) this else null } -@ThreadLocal private var View._focusable: UIFocusable? by extraProperty { null } var View.focusable: UIFocusable? diff --git a/korge/src/common/korlibs/korge/view/filter/Filter.kt b/korge/src/common/korlibs/korge/view/filter/Filter.kt index 4025086662..e4f2ad36ce 100644 --- a/korge/src/common/korlibs/korge/view/filter/Filter.kt +++ b/korge/src/common/korlibs/korge/view/filter/Filter.kt @@ -10,7 +10,6 @@ import korlibs.korge.view.* import korlibs.math.* import korlibs.math.geom.* import kotlin.math.* -import kotlin.native.concurrent.* /** * Interface for [View] filters. @@ -188,5 +187,4 @@ fun Filter.renderToTextureWithBorderUnsafe( fun Filter.expandedBorderRectangle(inp: Rectangle): Rectangle = inp.expanded(getBorder(inp.width.toIntCeil(), inp.height.toIntCeil())) -@ThreadLocal val RenderContext.renderToTextureResultPool by Extra.Property { Pool({ it.dispose() }) { RenderToTextureResult() } } diff --git a/korge/src/common/korlibs/korge/view/filter/ViewFilter.kt b/korge/src/common/korlibs/korge/view/filter/ViewFilter.kt index f90e634c19..85214dc67f 100644 --- a/korge/src/common/korlibs/korge/view/filter/ViewFilter.kt +++ b/korge/src/common/korlibs/korge/view/filter/ViewFilter.kt @@ -4,7 +4,6 @@ import korlibs.datastructure.* import korlibs.korge.render.* import korlibs.korge.view.* import korlibs.math.* -import kotlin.native.concurrent.* /** * An optional [Filter] attached to this view. @@ -40,7 +39,6 @@ class ViewRenderPhaseFilter(var filter: Filter? = null) : ViewRenderPhase { } /** Usually a value between [0.0, 1.0] */ -@ThreadLocal var View.filterScale: Double by extraPropertyThis(transform = { Filter.discretizeFilterScale(it) }) { 1.0 } //internal const val VIEW_FILTER_TRANSPARENT_EDGE = true diff --git a/korge/src/common/korlibs/korge/view/filter/ViewFilterExt.kt b/korge/src/common/korlibs/korge/view/filter/ViewFilterExt.kt index 1da3d8f84f..f3388f30f5 100644 --- a/korge/src/common/korlibs/korge/view/filter/ViewFilterExt.kt +++ b/korge/src/common/korlibs/korge/view/filter/ViewFilterExt.kt @@ -4,7 +4,6 @@ import korlibs.datastructure.* import korlibs.korge.view.* import kotlin.native.concurrent.* -@ThreadLocal var Views.registerFilterSerialization: Boolean by Extra.Property { false } fun ViewsContainer.registerFilterSerialization() { if (views.registerFilterSerialization) return diff --git a/korge/src/common/korlibs/korge/view/mask/ViewMask.kt b/korge/src/common/korlibs/korge/view/mask/ViewMask.kt index 89daf5599e..4cb9529153 100644 --- a/korge/src/common/korlibs/korge/view/mask/ViewMask.kt +++ b/korge/src/common/korlibs/korge/view/mask/ViewMask.kt @@ -6,12 +6,8 @@ import korlibs.graphics.annotation.* import korlibs.image.color.* import korlibs.korge.render.* import korlibs.korge.view.* -import kotlin.native.concurrent.* -@ThreadLocal private var View.__mask: View? by extraProperty { null } - -@ThreadLocal private var View.__maskFiltering: Boolean by extraProperty { true } fun T.mask(mask: View?, filtering: Boolean = true): T { diff --git a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt index f248e94afd..2d2b89b92e 100644 --- a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt +++ b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt @@ -30,15 +30,33 @@ expect val iosTvosTools: IosTvosToolsImpl open class IosTvosToolsImpl { open fun applicationDidFinishLaunching(app: UIApplication, window: UIWindow) { //window?.windowScene = windowScene + initializeIosResourcesPath() } open fun viewDidLoad(view: GLKView?) { + initializeIosResourcesPath() } open fun hapticFeedbackGenerate(kind: HapticFeedbackKind) { } } + +private fun initializeIosResourcesPath() { + if (korlibs.io.file.std.customCwd != null) return + val path = nativeCwdOrNull() + if (path != null) { + val rpath = "$path/include/app/resources" + if (NSFileManager().contentsOfDirectoryAtPath(rpath, null) != null) { + println("glkView: Switching CWD to $rpath...") + NSFileManager.defaultManager.changeCurrentDirectoryPath(rpath) + korlibs.io.file.std.customCwd = rpath + } else { + println("glkView: NOT switching CWD ($path doesn't exists)...") + } + } +} + // @TODO: Do not remove! Called from a generated .kt file : platforms/native-ios/bootstrap.kt @Suppress("unused", "UNUSED_PARAMETER") abstract class KorgwBaseNewAppDelegate { @@ -235,27 +253,22 @@ class MyGLKViewController( override fun viewWillDisappear(animated: Boolean) { eventLoopThread?.threadSuggestRunning = false + eventLoopThread = null } override fun glkView(view: GLKView, drawInRect: CValue) { if (!initialized) { initialized = true - val path = nativeCwdOrNull() - if (path != null) { - val rpath = "$path/include/app/resources" - if (NSFileManager().contentsOfDirectoryAtPath(rpath, null) != null) { - println("glkView: Switching CWD to $rpath...") - NSFileManager.defaultManager.changeCurrentDirectoryPath(rpath) - korlibs.io.file.std.customCwd = rpath - } else { - println("glkView: NOT switching CWD ($path doesn't exists)...") - } - } + initializeIosResourcesPath() //self.lastTouchId = 0; + logger.info { "gameWindow.onUpdateEvent" } + gameWindow.onUpdateEvent { + darwinGamePad.updateGamepads(gameWindow) + } logger.info { "dispatchInitEvent" } - gameWindow.dispatchInitEvent() gameWindow.queueSuspend { + gameWindow.dispatchInitEvent() logger.info { "Executing entry..." } this.entry() } @@ -264,8 +277,8 @@ class MyGLKViewController( // Context changed! val currentContext = EAGLContext.currentContext() if (myContext != currentContext) { - logger.info {"myContext = $myContext" } - logger.info {"currentContext = $currentContext" } + logger.info { "myContext = $myContext" } + logger.info { "currentContext = $currentContext" } myContext = currentContext gameWindow.ag.contextLost() } @@ -279,10 +292,10 @@ class MyGLKViewController( gameWindow.dispatchReshapeEvent(0, 0, width, height) } - darwinGamePad.updateGamepads(gameWindow) - if (gameWindow.continuousRenderMode.shouldRender()) { - gameWindow.dispatchRenderEvent() - } + //if (gameWindow.continuousRenderMode.shouldRender()) { + // gameWindow.dispatchRenderEvent() + //} + gameWindow.dispatchRenderEvent() } override fun didReceiveMemoryWarning() { diff --git a/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt b/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt index 3ac0283c6c..21ef7e4455 100644 --- a/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt +++ b/korge/src/ios/korlibs/render/DefaultGameWindowIosTools.kt @@ -8,10 +8,12 @@ actual val iosTvosTools: IosTvosToolsImpl = object : IosTvosToolsImpl() { val uiImpactFeedbackGenerator by lazy { UIImpactFeedbackGenerator() } override fun applicationDidFinishLaunching(app: UIApplication, window: UIWindow) { + super.applicationDidFinishLaunching(app, window) window.backgroundColor = UIColor.systemBackgroundColor } override fun viewDidLoad(view: GLKView?) { + super.viewDidLoad(view) view?.multipleTouchEnabled = true } From a92bc12d2e814752097ef5623e380a0a36edf699 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 17:49:36 +0200 Subject: [PATCH 56/66] More work --- .../common/korlibs/image/format/ImageInfo.kt | 5 ++--- .../korlibs/datastructure/_Delegates.kt | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/korge-core/src/common/korlibs/image/format/ImageInfo.kt b/korge-core/src/common/korlibs/image/format/ImageInfo.kt index c7d8abfe7d..c255dc901c 100644 --- a/korge-core/src/common/korlibs/image/format/ImageInfo.kt +++ b/korge-core/src/common/korlibs/image/format/ImageInfo.kt @@ -1,7 +1,6 @@ package korlibs.image.format -import korlibs.datastructure.Extra -import korlibs.datastructure.toMap +import korlibs.datastructure.* import korlibs.math.geom.* open class ImageInfo : Sizeable, Extra by Extra.Mixin() { @@ -11,7 +10,7 @@ open class ImageInfo : Sizeable, Extra by Extra.Mixin() { override val size: Size get() = Size(width, height) - override fun toString(): String = "ImageInfo(width=$width, height=$height, bpp=$bitsPerPixel, extra=${extra?.toMap()})" + override fun toString(): String = "ImageInfo(width=$width, height=$height, bpp=$bitsPerPixel, extra=${extra?.toMap()})" } fun ImageInfo(block: ImageInfo.() -> Unit): ImageInfo = ImageInfo().apply(block) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt b/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt index 0dd9ebdf82..e3a84d2d4a 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Delegates.kt @@ -3,12 +3,23 @@ package korlibs.datastructure import korlibs.datastructure.lock.* import kotlin.reflect.* -class ExtraObject { +class ExtraObject : MutableMap { private val lock = Lock() private val data = FastStringMap() - fun get(key: String): Any? = lock { data[key] } - fun set(key: String, value: Any?) = lock { data[key] = value } - fun contains(key: String): Boolean = lock { key in data } + override val size: Int get() = lock { data.size } + override val entries: MutableSet> get() = lock { data.keys.associateWith { data[it] }.toMutableMap().entries.toMutableSet() } + override val keys: MutableSet get() = lock { data.keys.toMutableSet() } + override val values: MutableCollection = lock { data.values.toMutableList() } + override operator fun get(key: String): Any? = lock { data[key] } + operator fun set(key: String, value: Any?) = lock { data[key] = value } + operator fun contains(key: String): Boolean = lock { key in data } + override fun isEmpty(): Boolean = size == 0 + override fun clear() = lock { data.clear() } + override fun remove(key: String): Any? = lock { data.remove(key) } + override fun putAll(from: Map) = lock { for ((key, value) in from) data[key] = value } + override fun put(key: String, value: Any?): Any? = lock { data[key] = value } + override fun containsValue(value: Any?): Boolean = lock { value in values } + override fun containsKey(key: String): Boolean = lock { key in data } } typealias ExtraType = ExtraObject? From 65a3b1218dbedd918ac57318c4376ff4f4a8cef0 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 19:39:03 +0200 Subject: [PATCH 57/66] Some fixes --- .../audio/sound/CoreAudioSoundProvider.kt | 44 +++++-------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt index dd51a7bd40..62c1400083 100644 --- a/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt +++ b/korge-core/src/darwin/korlibs/audio/sound/CoreAudioSoundProvider.kt @@ -2,22 +2,21 @@ package korlibs.audio.sound //import mystdio.* import cnames.structs.OpaqueAudioQueue +import korlibs.memory.* import kotlinx.cinterop.* import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.ShortVar import kotlinx.cinterop.convert -import kotlinx.cinterop.get -import kotlinx.cinterop.set import platform.AudioToolbox.* import platform.AudioToolbox.AudioQueueBufferRef import platform.AudioToolbox.AudioQueueRef import platform.CoreAudioTypes.* import platform.darwin.OSStatus +import platform.posix.* import kotlin.Int import kotlin.String import kotlin.Unit import kotlin.coroutines.* -import kotlin.native.ThreadLocal actual val nativeSoundProvider: NativeSoundProvider get() = CORE_AUDIO_NATIVE_SOUND_PROVIDER expect fun appleInitAudio() @@ -40,12 +39,17 @@ class CoreAudioNewPlatformAudioOutput( nchannels: Int, gen: (AudioSamplesInterleaved) -> Unit, ) : NewPlatformAudioOutput(coroutineContext, nchannels, freq, gen) { + //private var samples: AudioSamplesInterleaved? = null + val generator = CoreAudioGenerator(freq, nchannels, coroutineContext = coroutineContext) { data, dataSize -> - val nchannels = this.nchannels - val samples = AudioSamplesInterleaved(nchannels, dataSize) + val totalSamples = dataSize / nchannels + //if (samples == null || samples!!.totalSamples != totalSamples || samples!!.channels != channels) { + //} + val samples = AudioSamplesInterleaved(nchannels, totalSamples) genSafe(samples) - val samplesData = samples.data - for (n in 0 until dataSize) data[n] = samplesData[n] + samples.data.usePinned { + memcpy(data, it.startAddressOf, (dataSize * Short.SIZE_BYTES).convert()) + } } override fun internalStart() { generator.start() @@ -55,32 +59,6 @@ class CoreAudioNewPlatformAudioOutput( } } -class CoreAudioPlatformAudioOutput( - coroutineContext: CoroutineContext, - freq: Int -) : DequeBasedPlatformAudioOutput(coroutineContext, freq) { - val generator = CoreAudioGenerator(freq, nchannels, coroutineContext = coroutineContext) { data, dataSize -> - val temp = ShortArray(dataSize / nchannels) - for (m in 0 until nchannels) { - readShorts(m, temp) - for (n in 0 until dataSize / nchannels) { - data[n * nchannels + m] = temp[n] - } - } - //for (n in 0 until dataSize / nchannels) { - // for (m in 0 until nchannels) { - // data[n * nchannels + m] = readShort(m) - // } - //} - } - override fun start() { - generator.start() - } - override fun stop() { - generator.dispose() - } -} - /////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////// From 58f7b233e9d97b5900d8743888057165774d8142 Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 19:39:29 +0200 Subject: [PATCH 58/66] Some fixes --- .../korlibs/datastructure/_Datastructure_thread.kt | 6 ++++++ .../darwin/korlibs/datastructure/lock/LockNative.kt | 4 +++- .../src/js/korlibs/datastructure/lock/LockJs.kt | 12 +++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt index 39cc88c49b..13b481c361 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_thread.kt @@ -61,3 +61,9 @@ fun NativeThread.Companion.sleepExact(time: TimeSpan) { fun NativeThread.Companion.sleepUntil(date: DateTime, exact: Boolean = true) { sleep(date - DateTime.now(), exact) } + +fun NativeThread.Companion.sleepWhile(cond: () -> Boolean) { + while (cond()) { + NativeThread.sleep(1.milliseconds) + } +} diff --git a/korge-foundation/src/darwin/korlibs/datastructure/lock/LockNative.kt b/korge-foundation/src/darwin/korlibs/datastructure/lock/LockNative.kt index 345718bf1d..fdee529afe 100644 --- a/korge-foundation/src/darwin/korlibs/datastructure/lock/LockNative.kt +++ b/korge-foundation/src/darwin/korlibs/datastructure/lock/LockNative.kt @@ -42,6 +42,8 @@ actual class Lock actual constructor() : BaseLock { } actual override fun notify(unit: Unit) { + check(locked.value) { "Must wait inside a synchronization block" } + if (current != pthread_self()) error("Must lock the notify thread") notified.value = true } actual override fun wait(time: TimeSpan): Boolean { @@ -90,7 +92,7 @@ actual class NonRecursiveLock actual constructor() : BaseLock { notified.value = false unlock() try { - NativeThread.spinWhile { !notified.value && start.elapsedNow() < time } + NativeThread.sleepWhile { !notified.value && start.elapsedNow() < time } } finally { lock() } diff --git a/korge-foundation/src/js/korlibs/datastructure/lock/LockJs.kt b/korge-foundation/src/js/korlibs/datastructure/lock/LockJs.kt index 48c7b0814c..e020cd11c0 100644 --- a/korge-foundation/src/js/korlibs/datastructure/lock/LockJs.kt +++ b/korge-foundation/src/js/korlibs/datastructure/lock/LockJs.kt @@ -3,10 +3,20 @@ package korlibs.datastructure.lock import korlibs.time.* actual class Lock actual constructor() : BaseLock { - actual inline operator fun invoke(callback: () -> T): T = callback() + var locked = false + actual inline operator fun invoke(callback: () -> T): T { + locked = true + try { + return callback() + } finally { + locked = false + } + } actual override fun notify(unit: Unit) { + if (!locked) error("Must lock before notifying") } actual override fun wait(time: TimeSpan): Boolean { + if (!locked) error("Must lock before waiting") return false } } From 2fbdb42cd4b32927d7e0507263df82fd62ebff5e Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 19:40:11 +0200 Subject: [PATCH 59/66] Some fixes --- .../src/js/korlibs/datastructure/event/JsEventLoop.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt index bca582b391..4505b1dcb9 100644 --- a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt +++ b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt @@ -5,6 +5,8 @@ import korlibs.platform.* import korlibs.time.* object JsEventLoop : BaseEventLoop() { + override var paused: Boolean = false + override fun close() = Unit override fun setImmediate(task: () -> Unit) { jsGlobalThis.setTimeout({ task() }, 0) From 882b74d643d78b2b30049e0ec21cd9557d68586c Mon Sep 17 00:00:00 2001 From: soywiz Date: Tue, 24 Oct 2023 19:52:57 +0200 Subject: [PATCH 60/66] More work --- .../korlibs/image/format/cg/CgFloatExt.kt | 5 ++ .../common/korlibs/io/concurrent/LockTest.kt | 46 ++++++++-- .../datastructure/_Datastructure_event.kt | 1 + .../korlibs/korge/android/KorgeAndroidView.kt | 3 +- .../android/korlibs/render/KorgwActivity.kt | 10 +-- .../korlibs/render/KorgwSurfaceView.kt | 6 +- korge/src/common/korlibs/event/Events.kt | 11 +-- .../src/common/korlibs/graphics/AGObjects.kt | 10 ++- .../korlibs/korge/tests/ViewsForTesting.kt | 2 +- korge/src/common/korlibs/render/GameWindow.kt | 8 +- .../common/korlibs/render/GameWindowExt.kt | 83 ++++++++++++------- .../korlibs/render/DefaultGameWindowIos.kt | 28 +++---- .../js/korlibs/render/DefaultGameWindowJs.kt | 31 ++++--- .../jvm/korlibs/render/awt/AwtFrameTools.kt | 8 +- .../korlibs/render/awt/OldAwtGameWindow.kt | 1 - .../korlibs/korge/view/ViewsInjectorTest.kt | 2 +- 16 files changed, 157 insertions(+), 98 deletions(-) diff --git a/korge-core/src/darwin/korlibs/image/format/cg/CgFloatExt.kt b/korge-core/src/darwin/korlibs/image/format/cg/CgFloatExt.kt index fcf79c4858..8cd86e1ce7 100644 --- a/korge-core/src/darwin/korlibs/image/format/cg/CgFloatExt.kt +++ b/korge-core/src/darwin/korlibs/image/format/cg/CgFloatExt.kt @@ -1,5 +1,6 @@ package korlibs.image.format.cg +import korlibs.math.geom.* import kotlinx.cinterop.* import platform.CoreGraphics.* @@ -16,6 +17,10 @@ inline val Float.cg: CGFloat get() = this.toCgFloat() @OptIn(UnsafeNumber::class) inline val Double.cg: CGFloat get() = this.toCgFloat() +fun CGRect.toRectangle(): Rectangle = Rectangle(this.origin.x, this.origin.y, this.size.width, this.size.height) +fun CGPoint.toPoint(): Point = Point(x, y) +fun CGSize.toSize(): Size = Size(width, height) + fun CGRectMakeExt(x: Int, y: Int, width: Int, height: Int): CValue = CGRectMake(x.cg, y.cg, width.cg, height.cg) fun CGRectMakeExt(x: Float, y: Float, width: Float, height: Float): CValue = CGRectMake(x.cg, y.cg, width.cg, height.cg) fun CGRectMakeExt(x: Double, y: Double, width: Double, height: Double): CValue = CGRectMake(x.cg, y.cg, width.cg, height.cg) diff --git a/korge-core/test/common/korlibs/io/concurrent/LockTest.kt b/korge-core/test/common/korlibs/io/concurrent/LockTest.kt index 1288c82472..adeeab7fca 100644 --- a/korge-core/test/common/korlibs/io/concurrent/LockTest.kt +++ b/korge-core/test/common/korlibs/io/concurrent/LockTest.kt @@ -1,9 +1,10 @@ package korlibs.io.concurrent -import korlibs.datastructure.lock.Lock -import korlibs.datastructure.lock.NonRecursiveLock -import kotlin.test.Test -import kotlin.test.assertEquals +import korlibs.datastructure.lock.* +import korlibs.datastructure.thread.* +import korlibs.platform.* +import korlibs.time.* +import kotlin.test.* class LockTest { @Test @@ -11,7 +12,9 @@ class LockTest { val lock = Lock() var a = 0 lock { - a++ + lock { + a++ + } } assertEquals(1, a) } @@ -25,4 +28,37 @@ class LockTest { } assertEquals(1, a) } + + @Test + fun testWaitNotify() { + if (Platform.isJs) return + + val lock = Lock() + var log = arrayListOf() + nativeThread(start = true) { + NativeThread.sleep(10.milliseconds) + log += "b" + lock { lock.notify() } + } + lock { + lock { + log += "a" + lock.wait(1.seconds) + log += "c" + } + } + assertEquals("abc", log.joinToString("")) + } + + @Test + fun testNotifyError() { + val lock = Lock() + assertFails { lock.notify() } + } + + @Test + fun testWaitError() { + val lock = Lock() + assertFails { lock.wait(1.seconds) } + } } diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index fd5f319b8c..9f95c76f2b 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -36,6 +36,7 @@ class SyncPauseable : Pauseable { } abstract class BaseEventLoop : EventLoop, Pauseable { + val runLock = Lock() } class SyncEventLoop( diff --git a/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt b/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt index a9268fda4e..9eb2ceac8b 100644 --- a/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt +++ b/korge/src/android/korlibs/korge/android/KorgeAndroidView.kt @@ -24,14 +24,13 @@ open class KorgeAndroidView @JvmOverloads constructor( private var gameWindow: AndroidGameWindowNoActivity? = null private val renderEvent = RenderEvent() - private val initEvent = InitEvent() var moduleLoaded = false ; private set fun unloadModule() { if (!moduleLoaded) return - gameWindow?.dispatchDestroyEvent() + gameWindow?.dispatchDestroyEventQueued() //gameWindow?.coroutineContext = null gameWindow?.close() gameWindow?.exit() diff --git a/korge/src/android/korlibs/render/KorgwActivity.kt b/korge/src/android/korlibs/render/KorgwActivity.kt index fbb8f47d92..e16a0ea6f5 100644 --- a/korge/src/android/korlibs/render/KorgwActivity.kt +++ b/korge/src/android/korlibs/render/KorgwActivity.kt @@ -115,21 +115,21 @@ abstract class KorgwActivity( println("---------------- KorgwActivity.onResume --------------") super.onResume() mGLView?.onResume() - gameWindow?.dispatchResumeEvent() + gameWindow?.dispatchResumeEventQueued() } override fun onPause() { println("---------------- KorgwActivity.onPause --------------") super.onPause() mGLView?.onPause() - gameWindow?.dispatchPauseEvent() + gameWindow?.dispatchPauseEventQueued() KmemGC.collect() } override fun onStop() { println("---------------- KorgwActivity.onStop --------------") super.onStop() - gameWindow?.dispatchStopEvent() + gameWindow?.dispatchStopEventQueued() } override fun onDestroy() { @@ -141,7 +141,7 @@ abstract class KorgwActivity( mGLView = null setContentView(android.view.View(this)) gameWindow.queue { - gameWindow.dispatchDestroyEvent() + gameWindow.dispatchDestroyEventQueued() } //gameWindow?.close() // Do not close, since it will be automatically closed by the destroy event } @@ -195,7 +195,7 @@ abstract class KorgwActivity( override fun onBackPressed() { gameWindow.queue { - if (!gameWindow.dispatchKeyEventEx(korlibs.event.KeyEvent.Type.DOWN, 0, '\u0008', Key.BACK, KeyEvent.KEYCODE_BACK)) { + if (!gameWindow.dispatchKeyEventExQueued(korlibs.event.KeyEvent.Type.DOWN, 0, '\u0008', Key.BACK, KeyEvent.KEYCODE_BACK)) { runOnUiThread { super.onBackPressed() } diff --git a/korge/src/android/korlibs/render/KorgwSurfaceView.kt b/korge/src/android/korlibs/render/KorgwSurfaceView.kt index de3ac544b9..230e932dc3 100644 --- a/korge/src/android/korlibs/render/KorgwSurfaceView.kt +++ b/korge/src/android/korlibs/render/KorgwSurfaceView.kt @@ -82,9 +82,7 @@ open class KorgwSurfaceView constructor( } override fun onDrawFrame(unused: GL10) { - val ag = gameWindow.ag - - ag.mainFrameBuffer.setSize(width, height) + gameWindow.resized(width, height) //GLES20.glClearColor(1f, 0f, .5f, 1f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) //if (gameWindow.continuousRenderMode.shouldRender()) { @@ -129,7 +127,7 @@ open class KorgwSurfaceView constructor( //println(InputDevice.SOURCE) //val stopPropagating = gameWindow.queueBlocking { val stopPropagating = gameWindow.queue { - gameWindow.dispatchKeyEventEx( + gameWindow.dispatchKeyEventExQueued( type, 0, char, key, keyCode, shift = event.isShiftPressed, ctrl = event.isCtrlPressed, diff --git a/korge/src/common/korlibs/event/Events.kt b/korge/src/common/korlibs/event/Events.kt index 8e0374371d..da4ef168a9 100644 --- a/korge/src/common/korlibs/event/Events.kt +++ b/korge/src/common/korlibs/event/Events.kt @@ -484,19 +484,12 @@ data class FullScreenEvent(var fullscreen: Boolean = false) : TypedEvent(RenderEvent) { - companion object : RenderEvent(), EventType +class RenderEvent : TypedEvent(RenderEvent) { + companion object : EventType override fun toString(): String = "RenderEvent" } -open class InitEvent : TypedEvent(InitEvent) { - companion object : InitEvent(), EventType - - fun copyFrom(other: InitEvent) { - } -} - class ResumeEvent() : TypedEvent(ResumeEvent) { companion object : EventType diff --git a/korge/src/common/korlibs/graphics/AGObjects.kt b/korge/src/common/korlibs/graphics/AGObjects.kt index f4d6f51caf..f38ed9d458 100644 --- a/korge/src/common/korlibs/graphics/AGObjects.kt +++ b/korge/src/common/korlibs/graphics/AGObjects.kt @@ -233,12 +233,12 @@ open class AGFrameBuffer(val base: AGFrameBufferBase, val id: Int = -1) : Closea private var _scissor = RectangleInt() var scissor: RectangleInt? = null - open fun setSize(width: Int, height: Int) { - setSize(0, 0, width, height) + open fun setSize(width: Int, height: Int): Boolean { + return setSize(0, 0, width, height) } - open fun setSize(x: Int, y: Int, width: Int, height: Int, fullWidth: Int = width, fullHeight: Int = height) { - if (this.x == x && this.y == y && this.width == width && this.height == height && this.fullWidth == fullWidth && this.fullHeight == fullHeight) return + open fun setSize(x: Int, y: Int, width: Int, height: Int, fullWidth: Int = width, fullHeight: Int = height): Boolean { + if (this.x == x && this.y == y && this.width == width && this.height == height && this.fullWidth == fullWidth && this.fullHeight == fullHeight) return true tex.upload(NullBitmap(width, height)) base.estimatedMemoryUsage = ByteUnits.fromBytes(fullWidth * fullHeight * (4 + 4)) @@ -250,6 +250,8 @@ open class AGFrameBuffer(val base: AGFrameBufferBase, val id: Int = -1) : Closea this.fullWidth = fullWidth this.fullHeight = fullHeight markAsDirty() + + return false } fun scissor(scissor: RectangleInt?) { diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index df77a2b1f8..9bf094a549 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -96,7 +96,7 @@ open class ViewsForTesting( gameWindow.height = height views.scaleAnchor = scaleAnchor views.scaleMode = scaleMode - gameWindow.dispatchReshapeEvent(0, 0, width, height) + gameWindow.dispatchReshapeEventQueued(0, 0, width, height) } suspend fun deferred(block: suspend (CompletableDeferred) -> Unit): T { diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index e900657e74..3590c3b20b 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -284,7 +284,7 @@ open class GameWindow : open fun close(exitCode: Int) { if (closing) return closing = true - queue { dispatchDestroyEvent() } + queue { dispatchDestroyEventQueued() } running = false this.exitCode = exitCode logger.info { "GameWindow.close" } @@ -315,6 +315,12 @@ open class GameWindow : open suspend fun clipboardWrite(data: ClipboardData): Unit = Unit open suspend fun clipboardRead(): ClipboardData? = null + fun resized(width: Int, height: Int) { + if (ag.mainFrameBuffer.setSize(width, height)) { + dispatchReshapeEventQueued(0, 0, width, height) + } + } + //open fun lockMousePointer() = println("WARNING: lockMousePointer not implemented") //open fun unlockMousePointer() = Unit } diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index 9ef87c0a53..cad422a1b9 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -13,20 +13,22 @@ import korlibs.memory.* import korlibs.render.GameWindowQuality.* class GameWindowEventInstances { + var requestLock: () -> Unit = { } val pauseEvent = PauseEvent() val resumeEvent = ResumeEvent() val stopEvent = StopEvent() val destroyEvent = DestroyEvent() + val disposeEvent = DisposeEvent() val updateEvent = UpdateEvent() val renderEvent = RenderEvent() - val initEvent = InitEvent() - val disposeEvent = DisposeEvent() - val fullScreenEvent = FullScreenEvent() - val reshapeEvent = ReshapeEvent() - val keyEvent = KeyEvent() + val updateEvents = ConcurrentPool { UpdateEvent() } + val fullScreenEvents = ConcurrentPool { FullScreenEvent() } + val reshapeEvents = ConcurrentPool { ReshapeEvent() } + val keyEvents = ConcurrentPool { KeyEvent() } val mouseEvent = MouseEvent() - val gestureEvent = GestureEvent() - val dropFileEvent = DropFileEvent() + val mouseEvents = ConcurrentPool(reset = { it.requestLock = requestLock }) { MouseEvent().also { it.requestLock = requestLock } } + val gestureEvents = ConcurrentPool { GestureEvent() } + val dropFileEvents = ConcurrentPool { DropFileEvent() } } class GameWindowInputState { @@ -52,7 +54,7 @@ class GameWindowInputState { } fun GameWindow.dispatchKeyEvent(type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { - return dispatchKeyEventEx(type, id, character, key, keyCode, str = str) + return dispatchKeyEventExQueued(type, id, character, key, keyCode, str = str) } fun GameWindow.dispatchKeyEventDownUp(id: Int, character: Char, key: Key, keyCode: Int, str: String? = null): Boolean { @@ -78,7 +80,7 @@ fun GameWindow.dispatchGamepadUpdateAdd(info: GamepadInfo) { fun GameWindow.dispatchGamepadUpdateEnd(out: IntArrayList = IntArrayList()): IntArrayList = gamepadEmitter.dispatchGamepadUpdateEnd(out) -fun GameWindow.dispatchKeyEventEx( +fun GameWindow.dispatchKeyEventExQueued( type: KeyEvent.Type, id: Int, character: Char, key: Key, keyCode: Int, shift: Boolean = gameWindowInputState.shift, ctrl: Boolean = gameWindowInputState.ctrl, alt: Boolean = gameWindowInputState.alt, meta: Boolean = gameWindowInputState.meta, str: String? = null @@ -86,7 +88,7 @@ fun GameWindow.dispatchKeyEventEx( if (type != KeyEvent.Type.TYPE) { gameWindowInputState.keysPresing[key.ordinal] = (type == KeyEvent.Type.DOWN) } - dispatch(events.keyEvent.reset { + dispatchQueued(events.keyEvents) { this.id = id this.character = character this.key = key @@ -103,17 +105,18 @@ fun GameWindow.dispatchKeyEventEx( } else { this.str = str } - }) - return events.keyEvent.defaultPrevented + } + //return events.keyEvents.defaultPrevented + return false } -fun GameWindow.dispatchSimpleMouseEvent( +fun GameWindow.dispatchSimpleMouseEventQueued( type: MouseEvent.Type, id: Int, x: Int, y: Int, button: MouseButton, simulateClickOnUp: Boolean = false ) { - dispatchMouseEvent(type, id, x, y, button, simulateClickOnUp = simulateClickOnUp) + dispatchMouseEventQueued(type, id, x, y, button, simulateClickOnUp = simulateClickOnUp) } -fun GameWindow.dispatchMouseEvent( +fun GameWindow.dispatchMouseEventQueued( type: MouseEvent.Type, id: Int, x: Int, y: Int, button: MouseButton, buttons: Int = gameWindowInputState.mouseButtons, scrollDeltaX: Float = gameWindowInputState.scrollDeltaX, scrollDeltaY: Float = gameWindowInputState.scrollDeltaY, scrollDeltaZ: Float = gameWindowInputState.scrollDeltaZ, isShiftDown: Boolean = gameWindowInputState.shift, isCtrlDown: Boolean = gameWindowInputState.ctrl, isAltDown: Boolean = gameWindowInputState.alt, isMetaDown: Boolean = gameWindowInputState.meta, @@ -123,7 +126,7 @@ fun GameWindow.dispatchMouseEvent( if (type != MouseEvent.Type.DOWN && type != MouseEvent.Type.UP) { gameWindowInputState.mouseButtons = gameWindowInputState.mouseButtons.setBits(1 shl button.ordinal, type == MouseEvent.Type.DOWN) } - dispatch(events.mouseEvent.reset { + dispatchQueued(events.mouseEvents) { this.type = type this.id = id this.x = x @@ -139,20 +142,38 @@ fun GameWindow.dispatchMouseEvent( this.isAltDown = isAltDown this.isMetaDown = isMetaDown this.scaleCoords = scaleCoords - }) + } //if (simulateClickOnUp && type == MouseEvent.Type.UP) { // dispatchMouseEvent(MouseEvent.Type.CLICK, id, x, y, button, buttons, scrollDeltaX, scrollDeltaY, scrollDeltaZ, isShiftDown, isCtrlDown, isAltDown, isMetaDown, scaleCoords, simulateClickOnUp = false) //} } +fun GameWindow.dispatchQueued(event: T) { + queue { dispatch(event) } +} +fun GameWindow.dispatchQueued(pool: Pool, update: T.() -> Unit) { + pool.alloc { + update(it) + dispatch(it) + } + //val item = pool.alloc() + //update(item) + //queue { + // try { + // dispatch(item) + // } finally { + // pool.free(item) + // } + //} +} + +fun GameWindow.dispatchPauseEventQueued() = dispatchQueued(events.pauseEvent) +fun GameWindow.dispatchResumeEventQueued() = dispatchQueued(events.resumeEvent) +fun GameWindow.dispatchStopEventQueued() = dispatchQueued(events.stopEvent) +fun GameWindow.dispatchDestroyEventQueued() = dispatchQueued(events.destroyEvent) +fun GameWindow.dispatchDisposeEventQueued() = dispatchQueued(events.disposeEvent) -fun GameWindow.dispatchInitEvent() = dispatch(events.initEvent.reset()) -fun GameWindow.dispatchPauseEvent() = dispatch(events.pauseEvent.reset()) -fun GameWindow.dispatchResumeEvent() = dispatch(events.resumeEvent.reset()) -fun GameWindow.dispatchStopEvent() = dispatch(events.stopEvent.reset()) -fun GameWindow.dispatchDestroyEvent() = dispatch(events.destroyEvent.reset()) -fun GameWindow.dispatchDisposeEvent() = dispatch(events.disposeEvent.reset()) fun GameWindow.dispatchUpdateEvent() { updateRenderLock { dispatch(events.updateEvent) } } @@ -161,24 +182,24 @@ fun GameWindow.dispatchRenderEvent() { updateRenderLock { dispatch(events.renderEvent) } ag.finish() } -fun GameWindow.dispatchDropfileEvent(type: DropFileEvent.Type, files: List?) = dispatch(events.dropFileEvent.reset { +fun GameWindow.dispatchDropfileEventQueued(type: DropFileEvent.Type, files: List?) = dispatchQueued(events.dropFileEvents) { this.type = type this.files = files -}) -fun GameWindow.dispatchFullscreenEvent(fullscreen: Boolean) = dispatch(events.fullScreenEvent.reset { this.fullscreen = fullscreen }) +} +fun GameWindow.dispatchFullscreenEventQueued(fullscreen: Boolean) = dispatchQueued(events.fullScreenEvents) { this.fullscreen = fullscreen } -fun GameWindow.dispatchReshapeEvent(x: Int, y: Int, width: Int, height: Int) { - dispatchReshapeEventEx(x, y, width, height, width, height) +fun GameWindow.dispatchReshapeEventQueued(x: Int, y: Int, width: Int, height: Int) { + dispatchReshapeEventExQueued(x, y, width, height, width, height) } -fun GameWindow.dispatchReshapeEventEx(x: Int, y: Int, width: Int, height: Int, fullWidth: Int, fullHeight: Int) { +fun GameWindow.dispatchReshapeEventExQueued(x: Int, y: Int, width: Int, height: Int, fullWidth: Int, fullHeight: Int) { ag.mainFrameBuffer.setSize(x, y, width, height, fullWidth, fullHeight) - dispatch(events.reshapeEvent.reset { + dispatchQueued(events.reshapeEvents) { this.x = x this.y = y this.width = width this.height = height - }) + } } fun GameWindow.handleInitEventIfRequired() { diff --git a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt index 2d2b89b92e..eb829730f6 100644 --- a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt +++ b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt @@ -91,16 +91,16 @@ abstract class KorgwBaseNewAppDelegate { fun applicationWillResignActive(app: UIApplication) { logger.info {"applicationWillResignActive" } forceGC() - gameWindow.dispatchPauseEvent() + gameWindow.dispatchPauseEventQueued() } fun applicationDidBecomeActive(app: UIApplication) { logger.info {"applicationDidBecomeActive" } - gameWindow.dispatchResumeEvent() + gameWindow.dispatchResumeEventQueued() } fun applicationWillTerminate(app: UIApplication) { logger.info {"applicationWillTerminate" } - gameWindow.dispatchStopEvent() - gameWindow.dispatchDestroyEvent() + gameWindow.dispatchStopEventQueued() + gameWindow.dispatchDestroyEventQueued() } private fun forceGC() { @@ -183,7 +183,7 @@ class ViewController( val key = IosKeyMap.KEY_MAP[keyCode.toInt()] ?: Key.UNKNOWN //println("pressesHandler[$type]: ${keyCode}, ${modifierFlags}, $key, ${uiKey.charactersIgnoringModifiers}") - gameWindow.dispatchKeyEventEx( + gameWindow.dispatchKeyEventExQueued( type, 0, uiKey.charactersIgnoringModifiers.firstOrNull() ?: '\u0000', @@ -257,6 +257,10 @@ class MyGLKViewController( } override fun glkView(view: GLKView, drawInRect: CValue) { + val rect = drawInRect.useContents { toRectangle() } + + println("RENDER rect=$rect") + if (!initialized) { initialized = true initializeIosResourcesPath() @@ -266,9 +270,7 @@ class MyGLKViewController( gameWindow.onUpdateEvent { darwinGamePad.updateGamepads(gameWindow) } - logger.info { "dispatchInitEvent" } gameWindow.queueSuspend { - gameWindow.dispatchInitEvent() logger.info { "Executing entry..." } this.entry() } @@ -285,17 +287,15 @@ class MyGLKViewController( val width = (view.bounds.useContents { size.width } * view.contentScaleFactor).toInt() val height = (view.bounds.useContents { size.height } * view.contentScaleFactor).toInt() - if (lastWidth != width || lastHeight != height) { - println("RESHAPE: $lastWidth, $lastHeight -> $width, $height") - this.lastWidth = width - this.lastHeight = height - gameWindow.dispatchReshapeEvent(0, 0, width, height) - } + + gameWindow.resized(width, height) //if (gameWindow.continuousRenderMode.shouldRender()) { // gameWindow.dispatchRenderEvent() //} gameWindow.dispatchRenderEvent() + + println("/RENDER rect=$rect") } override fun didReceiveMemoryWarning() { @@ -347,7 +347,7 @@ class MyGLKViewController( fun dispatchTouchEventStartMove() = touchBuilder.startFrame(TouchEvent.Type.MOVE) fun dispatchTouchEventStartEnd() = touchBuilder.startFrame(TouchEvent.Type.END) fun dispatchTouchEventAddTouch(id: Int, x: Float, y: Float) = touchBuilder.touch(id, Point(x, y)) - fun dispatchTouchEventEnd() = gameWindow.dispatch(touchBuilder.endFrame().reset()) + fun dispatchTouchEventEnd() = gameWindow.dispatchQueued(touchBuilder.endFrame().reset()) private fun addTouches(touches: Set<*>, type: TouchType) { //println("addTouches[${touches.size}] type=$type"); diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index 390de5e57e..bed553a1d3 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -144,7 +144,7 @@ open class BrowserCanvasJsGameWindow( } //canvasRatio = (canvas.width.toDouble() / canvas.clientWidth.toDouble()) - dispatchReshapeEvent(0, 0, canvas.width, canvas.height) + dispatchReshapeEventQueued(0, 0, canvas.width, canvas.height) } inline fun transformEventX(x: Float): Float = x * canvasRatio @@ -209,7 +209,7 @@ open class BrowserCanvasJsGameWindow( } } } - dispatch(events.keyEvent { + dispatchQueued(events.keyEvents) { this.type = when (me.type) { "keydown" -> KeyEvent.Type.DOWN "keyup" -> KeyEvent.Type.UP @@ -224,7 +224,7 @@ open class BrowserCanvasJsGameWindow( this.alt = me.altKey this.meta = me.metaKey this.character = me.charCode.toChar() - }) + } // @TODO: preventDefault on all causes keypress to not happen? if (key == Key.TAB || key.isFunctionKey) { @@ -259,7 +259,7 @@ open class BrowserCanvasJsGameWindow( val tx = transformEventX(e.clientX.toFloat() - canvasBounds.left.toFloat()).toInt() val ty = transformEventY(e.clientY.toFloat() - canvasBounds.top.toFloat()).toInt() //console.log("mouseEvent", type.toString(), e.clientX, e.clientY, tx, ty) - events.mouseEvent { + events.mouseEvents.allocThis { this.type = if (e.buttons.toInt() != 0) pressingType else type this.scaleCoords = false this.id = 0 @@ -292,11 +292,10 @@ open class BrowserCanvasJsGameWindow( z = we.deltaZ.toFloat() * sensitivity, ) } - } - - // If we are in a touch device, touch events will be dispatched, and then we don't want to emit mouse events, that would be duplicated - if (!is_touch_device() || type == korlibs.event.MouseEvent.Type.SCROLL) { - dispatch(events.mouseEvent) + // If we are in a touch device, touch events will be dispatched, and then we don't want to emit mouse events, that would be duplicated + if (!is_touch_device() || type == korlibs.event.MouseEvent.Type.SCROLL) { + dispatch(this) + } } } @@ -433,18 +432,18 @@ open class BrowserCanvasJsGameWindow( // }) //}) window.addEventListener("resize", { onResized() }) - canvas.ondragenter = { dispatchDropfileEvent(DropFileEvent.Type.START, null) } - canvas.ondragexit = { dispatchDropfileEvent(DropFileEvent.Type.END, null) } - canvas.ondragleave = { dispatchDropfileEvent(DropFileEvent.Type.END, null) } + canvas.ondragenter = { dispatchDropfileEventQueued(DropFileEvent.Type.START, null) } + canvas.ondragexit = { dispatchDropfileEventQueued(DropFileEvent.Type.END, null) } + canvas.ondragleave = { dispatchDropfileEventQueued(DropFileEvent.Type.END, null) } canvas.ondragover = { it.preventDefault() } - canvas.ondragstart = { dispatchDropfileEvent(DropFileEvent.Type.START, null) } - canvas.ondragend = { dispatchDropfileEvent(DropFileEvent.Type.END, null) } + canvas.ondragstart = { dispatchDropfileEventQueued(DropFileEvent.Type.START, null) } + canvas.ondragend = { dispatchDropfileEventQueued(DropFileEvent.Type.END, null) } canvas.ondrop = { it.preventDefault() - dispatchDropfileEvent(DropFileEvent.Type.END, null) + dispatchDropfileEventQueued(DropFileEvent.Type.END, null) val items = it.dataTransfer!!.items val files = (0 until items.length).mapNotNull { items[it]?.getAsFile()?.toVfs() } - dispatchDropfileEvent(DropFileEvent.Type.DROP, files) + dispatchDropfileEventQueued(DropFileEvent.Type.DROP, files) } onResized() diff --git a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt index 557b51171d..70aec09ad9 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtFrameTools.kt @@ -361,7 +361,7 @@ fun Component.registerMouseEvents(gameWindow: GameWindow) { var mouseX: Int = 0 var mouseY: Int = 0 - gameWindow.events.mouseEvent.requestLock = { + gameWindow.events.requestLock = { val location = MouseInfo.getPointerInfo().location lockingX = location.x lockingY = location.y @@ -401,7 +401,7 @@ fun Component.registerMouseEvents(gameWindow: GameWindow) { mouseX = e.x mouseY = e.y //println("ev=$ev") - gameWindow.dispatchMouseEvent( + gameWindow.dispatchMouseEventQueued( type = ev, id = 0, x = sx.toInt(), @@ -441,7 +441,7 @@ fun Component.registerMouseEvents(gameWindow: GameWindow) { val scrollDelta = e.preciseWheelRotation.toFloat() * osfactor val isShiftDown = modifiers hasFlags JMouseEvent.SHIFT_DOWN_MASK - gameWindow.dispatchMouseEvent( + gameWindow.dispatchMouseEventQueued( type = ev, id = 0, x = sx.toInt(), @@ -496,7 +496,7 @@ fun Component.registerKeyEvents(gameWindow: GameWindow) { val keyCode = e.keyCode val key = awtKeyCodeToKey(e.keyCode) - gameWindow.dispatchKeyEventEx(ev, id, char, key, keyCode, e.isShiftDown, e.isControlDown, e.isAltDown, e.isMetaDown) + gameWindow.dispatchKeyEventExQueued(ev, id, char, key, keyCode, e.isShiftDown, e.isControlDown, e.isAltDown, e.isMetaDown) } } diff --git a/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt index 6a57857566..42657450d5 100644 --- a/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/OldAwtGameWindow.kt @@ -363,7 +363,6 @@ abstract class BaseAwtGameWindow( //}) queue { - dispatchInitEvent() dispatchReshapeEvent() } EventQueue.invokeLater { diff --git a/korge/test/common/korlibs/korge/view/ViewsInjectorTest.kt b/korge/test/common/korlibs/korge/view/ViewsInjectorTest.kt index 9783d9e4e1..3d50ac6855 100644 --- a/korge/test/common/korlibs/korge/view/ViewsInjectorTest.kt +++ b/korge/test/common/korlibs/korge/view/ViewsInjectorTest.kt @@ -16,7 +16,7 @@ class ViewsInjectorTest : ViewsForTesting() { val log = arrayListOf() class MyScene(val str: String) : Scene() { - override suspend fun SContainer.sceneInit() { log += "sinit:$str"; log += "init:${injector().getOrNull()}" } + override suspend fun SContainer.sceneInit() { log += "sinit:$str"; log += "init:${injector().getOrNull()}" } override suspend fun SContainer.sceneMain() { log += "main:${injector().getOrNull()}" } override suspend fun sceneAfterInit() { log += "ainit:${injector().getOrNull()}" } override suspend fun sceneBeforeLeaving() { log += "bleav:${injector().getOrNull()}" } From a98f4706dfdbc806abfb82d7e5e3f7eee3c8d0fd Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 25 Oct 2023 16:19:56 +0200 Subject: [PATCH 61/66] Fix JS tests --- .../datastructure/_Datastructure_event.kt | 22 ++++++- .../datastructure/event/JsEventLoop.kt | 62 ++++++++++++++++--- .../_Datastructure_event.jvmAndroid.kt | 6 ++ .../_Datastructure_event.native.kt | 6 ++ .../src/common/korlibs/korge/KorgeHeadless.kt | 8 ++- korge/src/common/korlibs/korge/scene/Scene.kt | 2 +- .../korlibs/korge/tests/ViewsForTesting.kt | 7 ++- korge/src/common/korlibs/render/GameWindow.kt | 4 +- .../js/korlibs/render/DefaultGameWindowJs.kt | 2 - 9 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 korge-foundation/src/jvmAndroid/korlibs/datastructure/_Datastructure_event.jvmAndroid.kt create mode 100644 korge-foundation/src/nativeMain/kotlin/korlibs/datastructure/_Datastructure_event.native.kt diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 8c6a10f05e..20ca117ee7 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -11,10 +11,14 @@ import korlibs.logger.* import korlibs.time.* import kotlin.time.* +expect fun createPlatformEventLoop(precise: Boolean = true): SyncEventLoop + interface EventLoop : Pauseable, Closeable { + companion object fun setImmediate(task: () -> Unit) fun setTimeout(time: TimeSpan, task: () -> Unit): Closeable fun setInterval(time: TimeSpan, task: () -> Unit): Closeable + fun setIntervalFrame(task: () -> Unit): Closeable = setInterval(60.hz.timeSpan, task) } fun EventLoop.setInterval(time: Frequency, task: () -> Unit): Closeable = setInterval(time.timeSpan, task) @@ -22,7 +26,7 @@ abstract class BaseEventLoop : EventLoop, Pauseable { val runLock = Lock() } -class SyncEventLoop( +open class SyncEventLoop( /** precise=true will have better precision at the cost of more CPU-usage (busy waiting) */ //var precise: Boolean = true, var precise: Boolean = false, @@ -210,4 +214,20 @@ class SyncEventLoop( NativeThread.sleep(1.milliseconds) } } + + // START + + private var thread: NativeThread? = null + open fun start(): Unit { + if (thread != null) return + thread = nativeThread { + while (thread?.threadSuggestRunning == true) { + runTasksUntilEmpty() + } + } + } + open fun stop(): Unit { + thread?.threadSuggestRunning = false + thread = null + } } diff --git a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt index 4505b1dcb9..8d0c37971e 100644 --- a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt +++ b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt @@ -4,21 +4,63 @@ import korlibs.datastructure.closeable.* import korlibs.platform.* import korlibs.time.* +actual fun createPlatformEventLoop(precise: Boolean): SyncEventLoop = + LocalJsEventLoop(precise) + +open class LocalJsEventLoop( + precise: Boolean = false, + immediateRun: Boolean = false, +) : SyncEventLoop(precise, immediateRun) { + private var closeable: Closeable? = null + + override fun start() { + if (closeable != null) return + + closeable = JsEventLoop.setIntervalFrame { + runTasksUntilEmpty() + } + } + + override fun stop() { + closeable?.close() + closeable = null + } + + override fun close() { + stop() + } +} + object JsEventLoop : BaseEventLoop() { override var paused: Boolean = false override fun close() = Unit - override fun setImmediate(task: () -> Unit) { - jsGlobalThis.setTimeout({ task() }, 0) - } + override fun setImmediate(task: () -> Unit) { + jsGlobalThis.setTimeout({ task() }, 0) + } - override fun setTimeout(time: TimeSpan, task: () -> Unit): Closeable { - val id = jsGlobalThis.setTimeout({ task() }, time.millisecondsInt) - return Closeable { jsGlobalThis.clearTimeout(id) } - } + override fun setTimeout(time: TimeSpan, task: () -> Unit): Closeable { + val id = jsGlobalThis.setTimeout({ task() }, time.millisecondsInt) + return Closeable { jsGlobalThis.clearTimeout(id) } + } + + override fun setInterval(time: TimeSpan, task: () -> Unit): Closeable { + val id = jsGlobalThis.setInterval({ task() }, time.millisecondsInt) + return Closeable { jsGlobalThis.clearInterval(id) } + } - override fun setInterval(time: TimeSpan, task: () -> Unit): Closeable { - val id = jsGlobalThis.setInterval({ task() }, time.millisecondsInt) - return Closeable { jsGlobalThis.clearInterval(id) } + override fun setIntervalFrame(task: () -> Unit): Closeable { + val globalThisDyn = jsGlobalThis.asDynamic() + if (!globalThisDyn.requestAnimationFrame) return super.setIntervalFrame(task) + var running = true + var gen: (() -> Unit) ? = null + gen = { + if (running) { + task() + globalThisDyn.requestAnimationFrame(gen) + } } + gen() + return Closeable { running = false } + } } diff --git a/korge-foundation/src/jvmAndroid/korlibs/datastructure/_Datastructure_event.jvmAndroid.kt b/korge-foundation/src/jvmAndroid/korlibs/datastructure/_Datastructure_event.jvmAndroid.kt new file mode 100644 index 0000000000..01db337aa8 --- /dev/null +++ b/korge-foundation/src/jvmAndroid/korlibs/datastructure/_Datastructure_event.jvmAndroid.kt @@ -0,0 +1,6 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.datastructure.event + +actual fun createPlatformEventLoop(precise: Boolean): SyncEventLoop = + SyncEventLoop(precise) diff --git a/korge-foundation/src/nativeMain/kotlin/korlibs/datastructure/_Datastructure_event.native.kt b/korge-foundation/src/nativeMain/kotlin/korlibs/datastructure/_Datastructure_event.native.kt new file mode 100644 index 0000000000..2c99e025d2 --- /dev/null +++ b/korge-foundation/src/nativeMain/kotlin/korlibs/datastructure/_Datastructure_event.native.kt @@ -0,0 +1,6 @@ +@file:Suppress("PackageDirectoryMismatch") + +package korlibs.datastructure.event + +actual fun createPlatformEventLoop(precise: Boolean): SyncEventLoop = + SyncEventLoop(precise = precise) diff --git a/korge/src/common/korlibs/korge/KorgeHeadless.kt b/korge/src/common/korlibs/korge/KorgeHeadless.kt index a6b311fc63..fb617bdb69 100644 --- a/korge/src/common/korlibs/korge/KorgeHeadless.kt +++ b/korge/src/common/korlibs/korge/KorgeHeadless.kt @@ -20,8 +20,6 @@ object KorgeHeadless { exitProcessOnClose: Boolean = false, override val devicePixelRatio: Double = 1.0, ) : GameWindow() { - val syncEventLoop by lazy { eventLoop as SyncEventLoop } - override val width: Int = size.width.toInt() override val height: Int = size.height.toInt() @@ -30,7 +28,11 @@ object KorgeHeadless { } init { - nativeThread(name = "HeadlessGameWindow-syncEventLoop") { syncEventLoop.runTasksForever() } + if (NativeThread.isSupported) { + nativeThread(name = "HeadlessGameWindow-syncEventLoop") { (eventLoop as SyncEventLoop).runTasksForever() } + } else { + eventLoop.setInterval(1.milliseconds) { } + } eventLoop.setInterval(60.hz.timeSpan) { (ag as? AGOpengl?)?.context?.set() this@HeadlessGameWindow.dispatchRenderEvent() diff --git a/korge/src/common/korlibs/korge/scene/Scene.kt b/korge/src/common/korlibs/korge/scene/Scene.kt index 21ed6db80e..a8984e9d8c 100644 --- a/korge/src/common/korlibs/korge/scene/Scene.kt +++ b/korge/src/common/korlibs/korge/scene/Scene.kt @@ -54,7 +54,7 @@ abstract class Scene : InjectorAsyncDependency, ViewsContainer, CoroutineScope, val root get() = _sceneViewContainer protected val cancellables = CancellableGroup() - override val coroutineContext by lazy { views.coroutineContext + gameWindow.coroutineContext + Job(views.coroutineContext[Job.Key]) } + override val coroutineContext by lazy { views.coroutineContext + gameWindow.coroutineContext + InjectorContext(injector) + Job(views.coroutineContext[Job.Key]) } val sceneView: SContainer by lazy { createSceneView(sceneContainer.size).apply { _sceneViewContainer += this diff --git a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt index 9bf094a549..1993aac27f 100644 --- a/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt +++ b/korge/src/common/korlibs/korge/tests/ViewsForTesting.kt @@ -39,11 +39,12 @@ open class ViewsForTesting( override fun now(): DateTime = time } inner class TestGameWindow(initialSize: Size) : GameWindowLog() { - val syncEventLoop by lazy { eventLoop as SyncEventLoop } init { - syncEventLoop.immediateRun = true - syncEventLoop.nowProvider = { time.unixMillisDouble.milliseconds } + eventLoop.immediateRun = true + eventLoop.nowProvider = { time.unixMillisDouble.milliseconds } } + + val syncEventLoop by lazy { eventLoop as SyncEventLoop } override val autoUpdateInterval = false override var androidContextAny: Any? = null override val devicePixelRatio: Double get() = this@ViewsForTesting.devicePixelRatio diff --git a/korge/src/common/korlibs/render/GameWindow.kt b/korge/src/common/korlibs/render/GameWindow.kt index c6b1feb45e..d975726126 100644 --- a/korge/src/common/korlibs/render/GameWindow.kt +++ b/korge/src/common/korlibs/render/GameWindow.kt @@ -124,9 +124,7 @@ open class GameWindow : //override val ag: AG = LogAG() override val ag: AG = AGDummy() - open fun createEventLoop(): BaseEventLoop = SyncEventLoop(precise = false) - - val eventLoop: BaseEventLoop by lazy { createEventLoop() } + val eventLoop: SyncEventLoop = createPlatformEventLoop(precise = false) //val renderEventLoop = SyncEventLoop(precise = false, immediateRun = true) open val coroutineDispatcher by lazy { EventLoopCoroutineDispatcher(eventLoop).also { diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index bed553a1d3..5dfbaed2db 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -1,6 +1,5 @@ package korlibs.render -import korlibs.datastructure.event.* import korlibs.event.* import korlibs.event.Touch import korlibs.graphics.* @@ -21,7 +20,6 @@ import org.w3c.dom.events.MouseEvent private external val navigator: dynamic open class JsGameWindow : GameWindow() { - override fun createEventLoop(): BaseEventLoop = JsEventLoop } open class BrowserCanvasJsGameWindow( From 9355ddf69d7c1dbef1384aa2559c3f5b713aec06 Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 25 Oct 2023 16:31:47 +0200 Subject: [PATCH 62/66] Some fixes --- .../common/korlibs/datastructure/_Datastructure_event.kt | 4 +--- .../src/js/korlibs/datastructure/event/JsEventLoop.kt | 2 +- .../jvm/korlibs/datastructure/event/JvmSyncEventLoopTest.kt | 4 +--- korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt | 6 ++---- korge/src/js/korlibs/render/DefaultGameWindowJs.kt | 5 +++++ korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt | 6 ++---- korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt | 4 +--- 7 files changed, 13 insertions(+), 18 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 20ca117ee7..140641269a 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -221,9 +221,7 @@ open class SyncEventLoop( open fun start(): Unit { if (thread != null) return thread = nativeThread { - while (thread?.threadSuggestRunning == true) { - runTasksUntilEmpty() - } + runTasksForever { thread?.threadSuggestRunning == true } } } open fun stop(): Unit { diff --git a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt index 8d0c37971e..2684bd402e 100644 --- a/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt +++ b/korge-foundation/src/js/korlibs/datastructure/event/JsEventLoop.kt @@ -17,7 +17,7 @@ open class LocalJsEventLoop( if (closeable != null) return closeable = JsEventLoop.setIntervalFrame { - runTasksUntilEmpty() + runAvailableNextTask() } } diff --git a/korge-foundation/test/jvm/korlibs/datastructure/event/JvmSyncEventLoopTest.kt b/korge-foundation/test/jvm/korlibs/datastructure/event/JvmSyncEventLoopTest.kt index 2c445e097d..03f0a146d9 100644 --- a/korge-foundation/test/jvm/korlibs/datastructure/event/JvmSyncEventLoopTest.kt +++ b/korge-foundation/test/jvm/korlibs/datastructure/event/JvmSyncEventLoopTest.kt @@ -56,9 +56,7 @@ class JvmSyncEventLoopTest { //println("ITEM") updateGame() } - nativeThread { - el.runTasksForever() - } + el.start() nativeThread { var running = true while (running) { diff --git a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt index eb829730f6..df932d4e8b 100644 --- a/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt +++ b/korge/src/darwin/korlibs/render/DefaultGameWindowIos.kt @@ -234,7 +234,6 @@ class MyGLKViewController( var lastWidth = 0 var lastHeight = 0 val darwinGamePad = DarwinGameControllerNative() - var eventLoopThread: NativeThread? = null override fun viewDidLoad() { val view = this.view as? GLKView? @@ -248,12 +247,11 @@ class MyGLKViewController( } override fun viewWillAppear(animated: Boolean) { - eventLoopThread = nativeThread { thread -> (gameWindow.eventLoop as SyncEventLoop).runTasksForever { thread.threadSuggestRunning } } + gameWindow.eventLoop.start() } override fun viewWillDisappear(animated: Boolean) { - eventLoopThread?.threadSuggestRunning = false - eventLoopThread = null + gameWindow.eventLoop.stop() } override fun glkView(view: GLKView, drawInRect: CValue) { diff --git a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt index 5dfbaed2db..74e44c2ad0 100644 --- a/korge/src/js/korlibs/render/DefaultGameWindowJs.kt +++ b/korge/src/js/korlibs/render/DefaultGameWindowJs.kt @@ -79,6 +79,11 @@ open class BrowserCanvasJsGameWindow( ) private val gamepad = GamepadInfo() + + init { + eventLoop.start() + } + @Suppress("UNUSED_PARAMETER") override fun updateGamepads() { try { diff --git a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt index 03fc16df31..95898480ff 100644 --- a/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt +++ b/korge/src/jvm/korlibs/render/awt/AwtGameWindow.kt @@ -1,8 +1,6 @@ package korlibs.render.awt import korlibs.datastructure.* -import korlibs.datastructure.event.* -import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.image.awt.* import korlibs.image.bitmap.* @@ -37,8 +35,8 @@ open class AwtCanvasGameWindow constructor( return _window } - val thread = nativeThread(name = "NewAwtGameWindow") { - (this.eventLoop as SyncEventLoop).runTasksForever() + init { + eventLoop.start() } override val width: Int get() = canvas.width diff --git a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt index 07e7f813d6..99b86a5707 100644 --- a/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt +++ b/korge/test/jvm/korlibs/render/awt/AwtGameCanvasTest.kt @@ -44,9 +44,7 @@ class AwtGameCanvasTest { } } - nativeThread { - el.runTasksForever() - } + el.start() frame.contentPane.layout = GridLayout(2, 2) frame.contentPane.add(AwtAGOpenglCanvas().also { From 23e5dc0d50e38221cf856c1be27bfccfd0f6000a Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 25 Oct 2023 16:54:05 +0200 Subject: [PATCH 63/66] Small fix --- korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt index 13b404e8b5..863dda0773 100644 --- a/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt +++ b/korge/src/android/korlibs/render/DefaultGameWindowAndroid.kt @@ -9,7 +9,6 @@ import korlibs.event.* import korlibs.graphics.* import korlibs.image.bitmap.* import korlibs.io.android.* -import korlibs.io.lang.TimedCache import korlibs.time.* import kotlinx.coroutines.* From ffc98885925169857060f6a567aa1de5a19fca25 Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 25 Oct 2023 19:17:24 +0200 Subject: [PATCH 64/66] More work --- korge/src/common/korlibs/korge/KorgeHeadless.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/korge/src/common/korlibs/korge/KorgeHeadless.kt b/korge/src/common/korlibs/korge/KorgeHeadless.kt index fb617bdb69..04942aa7af 100644 --- a/korge/src/common/korlibs/korge/KorgeHeadless.kt +++ b/korge/src/common/korlibs/korge/KorgeHeadless.kt @@ -1,11 +1,10 @@ package korlibs.korge -import korlibs.datastructure.event.* -import korlibs.datastructure.thread.* import korlibs.graphics.* import korlibs.graphics.gl.* import korlibs.graphics.log.* import korlibs.image.format.* +import korlibs.io.lang.* import korlibs.korge.view.* import korlibs.math.geom.* import korlibs.render.* @@ -28,14 +27,11 @@ object KorgeHeadless { } init { - if (NativeThread.isSupported) { - nativeThread(name = "HeadlessGameWindow-syncEventLoop") { (eventLoop as SyncEventLoop).runTasksForever() } - } else { - eventLoop.setInterval(1.milliseconds) { } - } + eventLoop.start() eventLoop.setInterval(60.hz.timeSpan) { - (ag as? AGOpengl?)?.context?.set() - this@HeadlessGameWindow.dispatchRenderEvent() + (ag as? AGOpengl?)?.context?.use { + this@HeadlessGameWindow.dispatchRenderEvent() + } } } From e67dc6bd6b63dfe049f43d36f1679449c080ad9c Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 25 Oct 2023 19:38:09 +0200 Subject: [PATCH 65/66] Some fixes --- .../datastructure/_Datastructure_event.kt | 2 +- korge/src/common/korlibs/kgl/KmlGlContext.kt | 9 ++++ .../src/common/korlibs/korge/KorgeHeadless.kt | 4 +- korge/src/common/korlibs/korge/view/Views.kt | 17 +++++-- .../common/korlibs/render/GameWindowExt.kt | 2 +- .../korge/testing/KorgeOffscreenTest.kt | 49 ++++++++++--------- .../jvm/korlibs/render/win32/Win32Tools.kt | 5 ++ .../jvm/korlibs/korge/image/KorgeImageTest.kt | 8 +-- .../korge/testing/KorgeScreenshotTest.kt | 19 +++---- 9 files changed, 65 insertions(+), 50 deletions(-) diff --git a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt index 140641269a..b62ca2b164 100644 --- a/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt +++ b/korge-foundation/src/common/korlibs/datastructure/_Datastructure_event.kt @@ -220,7 +220,7 @@ open class SyncEventLoop( private var thread: NativeThread? = null open fun start(): Unit { if (thread != null) return - thread = nativeThread { + thread = nativeThread(name = "eventLoopSync") { runTasksForever { thread?.threadSuggestRunning == true } } } diff --git a/korge/src/common/korlibs/kgl/KmlGlContext.kt b/korge/src/common/korlibs/kgl/KmlGlContext.kt index e7a9659c51..be05f00422 100644 --- a/korge/src/common/korlibs/kgl/KmlGlContext.kt +++ b/korge/src/common/korlibs/kgl/KmlGlContext.kt @@ -24,4 +24,13 @@ abstract class KmlGlContext(val window: Any?, val gl: KmlGl, val parent: KmlGlCo } override fun close() { } + + inline fun setUnset(block: (KmlGlContext) -> T): T { + set() + try { + return block(this) + } finally { + unset() + } + } } diff --git a/korge/src/common/korlibs/korge/KorgeHeadless.kt b/korge/src/common/korlibs/korge/KorgeHeadless.kt index 04942aa7af..2a80eb8e7b 100644 --- a/korge/src/common/korlibs/korge/KorgeHeadless.kt +++ b/korge/src/common/korlibs/korge/KorgeHeadless.kt @@ -29,9 +29,7 @@ object KorgeHeadless { init { eventLoop.start() eventLoop.setInterval(60.hz.timeSpan) { - (ag as? AGOpengl?)?.context?.use { - this@HeadlessGameWindow.dispatchRenderEvent() - } + this@HeadlessGameWindow.dispatchRenderEvent() } } diff --git a/korge/src/common/korlibs/korge/view/Views.kt b/korge/src/common/korlibs/korge/view/Views.kt index f3f9b0dbf6..1b8b30ae8d 100644 --- a/korge/src/common/korlibs/korge/view/Views.kt +++ b/korge/src/common/korlibs/korge/view/Views.kt @@ -306,7 +306,11 @@ class Views constructor( fun render() { ag.startFrame() - renderNew() + try { + renderNew() + } finally { + ag.endFrame() + } } private val eventResults = EventResult() @@ -315,10 +319,13 @@ class Views constructor( //println(this) //println("Update: $elapsed") input.startFrame(elapsed) - eventResults.reset() - stage.updateSingleViewWithViewsAll(this, elapsed) - //println("Views.update:eventResults=$eventResults") - input.endFrame(elapsed) + try { + eventResults.reset() + stage.updateSingleViewWithViewsAll(this, elapsed) + //println("Views.update:eventResults=$eventResults") + } finally { + input.endFrame(elapsed) + } } fun mouseUpdated() { diff --git a/korge/src/common/korlibs/render/GameWindowExt.kt b/korge/src/common/korlibs/render/GameWindowExt.kt index cad422a1b9..020c8ef817 100644 --- a/korge/src/common/korlibs/render/GameWindowExt.kt +++ b/korge/src/common/korlibs/render/GameWindowExt.kt @@ -180,7 +180,7 @@ fun GameWindow.dispatchUpdateEvent() { fun GameWindow.dispatchRenderEvent() { updateRenderLock { dispatch(events.renderEvent) } - ag.finish() + //ag.finish() } fun GameWindow.dispatchDropfileEventQueued(type: DropFileEvent.Type, files: List?) = dispatchQueued(events.dropFileEvents) { this.type = type diff --git a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt index e1cd182aab..fc8e4c2ada 100644 --- a/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt +++ b/korge/src/jvm/korlibs/korge/testing/KorgeOffscreenTest.kt @@ -13,33 +13,34 @@ import kotlinx.coroutines.* internal fun createKmlGlContext(fboWidth: Int, fboHeight: Int): KmlGlContext { val ctx = KmlGlContextDefault() - ctx.set() + ctx.setUnset { - val gl = ctx.gl + val gl = ctx.gl - val GL_RGBA8 = 0x8058 + val GL_RGBA8 = 0x8058 - // Build the texture that will serve as the color attachment for the framebuffer. - val colorRenderbuffer = gl.genRenderbuffer() - gl.bindRenderbuffer(KmlGl.RENDERBUFFER, colorRenderbuffer) - gl.renderbufferStorage(KmlGl.RENDERBUFFER, GL_RGBA8, fboWidth, fboHeight) - gl.bindRenderbuffer(KmlGl.RENDERBUFFER, 0) + // Build the texture that will serve as the color attachment for the framebuffer. + val colorRenderbuffer = gl.genRenderbuffer() + gl.bindRenderbuffer(KmlGl.RENDERBUFFER, colorRenderbuffer) + gl.renderbufferStorage(KmlGl.RENDERBUFFER, GL_RGBA8, fboWidth, fboHeight) + gl.bindRenderbuffer(KmlGl.RENDERBUFFER, 0) - // Build the texture that will serve as the depth attachment for the framebuffer. - val depthRenderbuffer = gl.genRenderbuffer() - gl.bindRenderbuffer(KmlGl.RENDERBUFFER, depthRenderbuffer) - gl.renderbufferStorage(KmlGl.RENDERBUFFER, KmlGl.DEPTH_COMPONENT, fboWidth, fboHeight) - gl.bindRenderbuffer(KmlGl.RENDERBUFFER, 0) + // Build the texture that will serve as the depth attachment for the framebuffer. + val depthRenderbuffer = gl.genRenderbuffer() + gl.bindRenderbuffer(KmlGl.RENDERBUFFER, depthRenderbuffer) + gl.renderbufferStorage(KmlGl.RENDERBUFFER, KmlGl.DEPTH_COMPONENT, fboWidth, fboHeight) + gl.bindRenderbuffer(KmlGl.RENDERBUFFER, 0) - // Build the framebuffer. - val framebuffer = gl.genFramebuffer() - gl.bindFramebuffer(KmlGl.FRAMEBUFFER, framebuffer) - gl.framebufferRenderbuffer(KmlGl.FRAMEBUFFER, KmlGl.COLOR_ATTACHMENT0, KmlGl.RENDERBUFFER, colorRenderbuffer) - gl.framebufferRenderbuffer(KmlGl.FRAMEBUFFER, KmlGl.DEPTH_ATTACHMENT, KmlGl.RENDERBUFFER, depthRenderbuffer) + // Build the framebuffer. + val framebuffer = gl.genFramebuffer() + gl.bindFramebuffer(KmlGl.FRAMEBUFFER, framebuffer) + gl.framebufferRenderbuffer(KmlGl.FRAMEBUFFER, KmlGl.COLOR_ATTACHMENT0, KmlGl.RENDERBUFFER, colorRenderbuffer) + gl.framebufferRenderbuffer(KmlGl.FRAMEBUFFER, KmlGl.DEPTH_ATTACHMENT, KmlGl.RENDERBUFFER, depthRenderbuffer) - val status = gl.checkFramebufferStatus(KmlGl.FRAMEBUFFER) - //if (status != GL_FRAMEBUFFER_COMPLETE) - // Error + val status = gl.checkFramebufferStatus(KmlGl.FRAMEBUFFER) + //if (status != GL_FRAMEBUFFER_COMPLETE) + // Error + } return ctx } @@ -55,10 +56,12 @@ fun suspendTestWithOffscreenAG(fboSize: Size, checkGl: Boolean = false, logGl: B ag.contextsToFree += ctx ag.mainFrameBuffer.setSize(fboWidth, fboHeight) if (setContext) ctx.set() - callback(ag) } finally { - ag.contextsToFree.forEach { it?.unset(); it?.close() } + ag.contextsToFree.forEach { + if (setContext) it?.unset() + it?.close() + } ag.contextsToFree.clear() } } diff --git a/korge/src/jvm/korlibs/render/win32/Win32Tools.kt b/korge/src/jvm/korlibs/render/win32/Win32Tools.kt index be8dbd3eb9..e5d8967c84 100644 --- a/korge/src/jvm/korlibs/render/win32/Win32Tools.kt +++ b/korge/src/jvm/korlibs/render/win32/Win32Tools.kt @@ -337,6 +337,8 @@ class Win32OpenglContext(val gwconfig: GameWindowConfig, val hWnd: WinDef.HWND, } logger.debug { "requestCoreProfile=$requestCoreProfile, isCore=$isCore, extensions=${extensions.size}" } + + releaseCurrent() } val extensions by lazy { @@ -370,9 +372,12 @@ class Win32OpenglContext(val gwconfig: GameWindowConfig, val hWnd: WinDef.HWND, } private fun makeCurrent(hDC: HDC?, hRC: WinDef.HGLRC?) { + //println("Make current ${Thread.currentThread()} ; hDC=$hDC, hRC=$hRC") + //printStackTrace() if (!WGL.wglMakeCurrent(hDC, hRC)) { val error = Win32.GetLastError() logger.error { "WGL.wglMakeCurrent($hDC, $hRC).error = $error" } + //error("error setting OpenGL context") } //println("makeCurrent") } diff --git a/korge/test/jvm/korlibs/korge/image/KorgeImageTest.kt b/korge/test/jvm/korlibs/korge/image/KorgeImageTest.kt index 10ad068d22..c551fa77ad 100644 --- a/korge/test/jvm/korlibs/korge/image/KorgeImageTest.kt +++ b/korge/test/jvm/korlibs/korge/image/KorgeImageTest.kt @@ -4,7 +4,7 @@ import korlibs.image.format.* import korlibs.io.async.* import korlibs.io.file.std.* import korlibs.korge.Korge -import korlibs.korge.testing.korgeScreenshotTestV2 +import korlibs.korge.testing.* import korlibs.korge.view.image import korlibs.math.geom.slice.* import kotlin.test.* @@ -19,9 +19,9 @@ class KorgeImageTest { } @Test - fun renderGrayscaleJpegImage() = korgeScreenshotTestV2(Korge()) { + //fun renderGrayscaleJpegImage() = korgeScreenshotTestV2(Korge()) { + fun renderGrayscaleJpegImage() = korgeScreenshotTest { val img = image(resourcesVfs["img1_grayscale.jpg"].readBitmapNative()) - it.recordGolden(img, "grayscale_jpg") - it.endTest() + assertScreenshot(img, "grayscale_jpg") } } diff --git a/korge/test/jvm/korlibs/korge/testing/KorgeScreenshotTest.kt b/korge/test/jvm/korlibs/korge/testing/KorgeScreenshotTest.kt index 18bb511072..c5e1032d9d 100644 --- a/korge/test/jvm/korlibs/korge/testing/KorgeScreenshotTest.kt +++ b/korge/test/jvm/korlibs/korge/testing/KorgeScreenshotTest.kt @@ -139,13 +139,8 @@ class KorgeScreenshotTest { ) @Test - fun test12() = korgeScreenshotTestV2( - Korge( - windowSize = Size(512, 512), - virtualSize = Size(512, 512), - backgroundColor = Colors.LIGHTGRAY - ), - ) { + //fun test12() = korgeScreenshotTestV2( + fun test12() = korgeScreenshotTest(bgcolor = Colors.LIGHTGRAY,) { val maxDegrees = (+16).degrees val rect1 = solidRect(100, 100, Colors.RED) { @@ -155,7 +150,7 @@ class KorgeScreenshotTest { position(200, 200) } - it.recordGolden(this, "initial1") + assertScreenshot(this, "initial1") val rect2 = solidRect(150, 150, Colors.YELLOW) { rotation = maxDegrees @@ -164,7 +159,7 @@ class KorgeScreenshotTest { position(350, 350) } - it.recordGolden(rect2, "initial2") + assertScreenshot(rect2, "initial2") val rect3 = solidRect(150, 150, Colors.GREEN) { rotation = maxDegrees @@ -173,7 +168,7 @@ class KorgeScreenshotTest { position(100, 350) } - it.recordGolden(this, "initial3") + assertScreenshot(this, "initial3") val rectContainer = container { val a = 100 @@ -181,8 +176,6 @@ class KorgeScreenshotTest { solidRect(a / 2, a / 2, Colors.YELLOW) } - it.recordGolden(rectContainer, "initial4", DIFF_BY_PIXELS_SETTINGS) - - it.endTest() + assertScreenshot(rectContainer, "initial4", psnr = 40.0) } } From 1bf16c8249b43d2efec91eb0ca82a56a2f64de52 Mon Sep 17 00:00:00 2001 From: soywiz Date: Wed, 25 Oct 2023 20:23:32 +0200 Subject: [PATCH 66/66] More work --- .../renderGrayscaleJpegImage_grayscale_jpg.png | Bin 0 -> 132712 bytes .../KorgeScreenshotTest/test12_initial1.png | Bin 0 -> 578 bytes .../KorgeScreenshotTest/test12_initial2.png | Bin 0 -> 129 bytes .../KorgeScreenshotTest/test12_initial3.png | Bin 0 -> 1197 bytes .../KorgeScreenshotTest/test12_initial4.png | Bin 0 -> 162 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 korge/testscreenshots/korlibs/korge/image/KorgeImageTest/renderGrayscaleJpegImage_grayscale_jpg.png create mode 100644 korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial1.png create mode 100644 korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial2.png create mode 100644 korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial3.png create mode 100644 korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial4.png diff --git a/korge/testscreenshots/korlibs/korge/image/KorgeImageTest/renderGrayscaleJpegImage_grayscale_jpg.png b/korge/testscreenshots/korlibs/korge/image/KorgeImageTest/renderGrayscaleJpegImage_grayscale_jpg.png new file mode 100644 index 0000000000000000000000000000000000000000..ccbe404480cd463247609458fff80fb99d615158 GIT binary patch literal 132712 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 z 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 M)78&qol`;+0EM-4<^TWy literal 0 HcmV?d00001 diff --git a/korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial2.png b/korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial2.png new file mode 100644 index 0000000000000000000000000000000000000000..e9a0840e9ec46ae8a36564e76f81c42e2ec46978 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm8Ax*9Z@d7cm;-!5T>t-P=sNbr706{Q3Gxg6 zpZmo8HISp@>EaktaqI0tMqVKA(1P#&x%})RKqe;mkQ`CPz!>2i5)P8{boFyt=akR{ E0Q-X;;s5{u literal 0 HcmV?d00001 diff --git a/korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial3.png b/korge/testscreenshots/korlibs/korge/testing/KorgeScreenshotTest/test12_initial3.png new file mode 100644 index 0000000000000000000000000000000000000000..d4ab35f5ff59dc8601ea05c1980279de2db08c14 GIT binary patch literal 1197 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w@L)Zt|+Cx8@BfKQ0)<;$1<1KIx>{{Lr~ zbUZ@|$Yv=C@(cbC1i2b+l|ZhKr;B4q#jUq@qstx{2)JHsne~6p<+JW#aw?2DH&yTM zO;>oSGyMy*zCKVdwSZ;Z|Bc)a88aLhlnVYy2{dr9EMR2pIPfzYC?L?lHfKLXz=1)k z;2%hUMS&U;`E>kc6um2Z5@p-*!gLek= zS3ib{f>+d;I7cN`tHN z3U-VCU!_*Cb0%!$c*V&Sv7)i6?{mCL(=7)EZj*r990}hzvaFxm7cy_^k~z2ElciLN(L`fGn2y3XcRK^79tL5BIwlFmkMbqa4V)9K z8Eb&i@`2+EM+xhRJwWky3<*p}{21aHlo@Q8Y?)#ijn*@`HCQvuVAv>pK>Pp)Fe3XM z!W+(LHwZr9V^DK=!}5XEfa!qp1?3B(3ZJGICyIA|CzjB zB6c$PbuT!7inYLvd2iUeKm7R%?gBYEEV|)(4UeT>>?&fE5q5k%)%yQO-jev83~cTO zhflE@tZR$}DrR>zxD4bhXgnFF*RWT4M!{328yg!v!{7b+-j(oZDtAIZ+q1Zx4BOl` z901xjjqMuHwt0OCbEk4Yn8|i6YUk&Ai>9wYt?g0~MNgS_*f*sDE!`~@QT6oP|Mwiz zfY#OJD;L}@OSQBfEb;#Z6k#d} z@(cd||HRh&$ALUgPZ!6Kid%1Q81gb0@UU#m|Jkh9=91NR!u`N+)u(4w2z8;ScTb82=3Y_>Z*Hy|G&AcTq2WTjRr>mdKI;Vst0O$uXWdHyG literal 0 HcmV?d00001