# The contents of this file are subject to the Common Public Attribution License # Version 1.0 (the “License”); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla # Public License Version 1.1 but Sections 14 and 15 have been added to cover use # of software over a computer network and provide for limited attribution for # the Original Developer. In addition, Exhibit A has been modified to be # consistent with Exhibit B. # # Software distributed under the License is distributed on an “AS IS” basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is Myou Engine. # # the Original Developer is the Initial Developer. # # The Initial Developer of the Original Code is the Myou Engine developers. # All portions of the code written by the Myou Engine developers are Copyright # (c) 2024. All Rights Reserved. # # Alternatively, the contents of this file may be used under the terms of the # GNU Affero General Public License version 3 (the [AGPL-3] License), in which # case the provisions of [AGPL-3] License are applicable instead of those above. # # If you wish to allow use of your version of this file only under the terms of # the [AGPL-3] License and not to allow others to use your version of this file # under the CPAL, indicate your decision by deleting the provisions above and # replace them with the notice and other provisions required by the [AGPL-3] # License. If you do not delete the provisions above, a recipient may use your # version of this file under either the CPAL or the [AGPL-3] License. import ../types import vmath except Quat, quat import glfm type Window* = ptr GLFMDisplay import std/bitops import std/math import std/sequtils import std/strformat import std/strutils import std/unicode import ./gl import ../screen import ../util import ../graphics/render import ../input proc screen*(window: Window): Screen {.inline.} = cast[Screen](window.glfmGetUserData) template to_rotation(o: GLFMInterfaceOrientation): int8 = case o: of GLFMInterfaceOrientationPortrait: 0 of GLFMInterfaceOrientationLandscapeRight: 1 of GLFMInterfaceOrientationPortraitUpsideDown: 2 of GLFMInterfaceOrientationLandscapeLeft: 3 else: 0 proc update_inset(window: Window) {.inline.} = let screen = window.screen var top, right, bottom, left = 0.0 window.glfmGetDisplayChromeInsets(top.addr, right.addr, bottom.addr, left.addr) screen.frame_inset = FrameInset(top:top, right:right, bottom:bottom, left:left) proc `screen=`*(window: Window, screen: Screen) {.inline.} = window.glfmSetUserData cast[pointer](screen) if screen == nil: return window.glfmSetSurfaceResizedFunc proc(window: Window, width, height: cint) {.cdecl.} = let scale = window.glfmGetDisplayScale() let orientation = window.glfmGetInterfaceOrientation() echo &"resizing {width} {height}" window.screen.resize(width.int32, height.int32) window.update_inset() window.screen.display_scale = scale window.glfmSetOrientationChangedFunc proc(window: Window, orientation: GLFMInterfaceOrientation) {.cdecl.} = echo &"orientation {orientation.to_rotation}" window.screen.resize(window.screen.width, window.screen.height, orientation.to_rotation) # TODO: Check if this is being called in sensorLandscape mode. # window.glfmSetDisplayChromeInsetsChangedFunc proc (display: Window, top, right, bottom, left: float) {.cdecl.} = # echo "insets:" # dump (top, right, bottom, left) # TODO: on emscripten there's no way to distinguish between # multi touch and right/middle mouse clicks!! # TODO: maybe add a couple of listeners with EM_ASM to know screen.is_touch = false when defined(android): window.glfmSetMultitouchEnabled true screen.is_touch = true var width, height: cint glfmGetDisplaySize(window, width.addr, height.addr) let orientation = window.glfmGetInterfaceOrientation() let scale = window.glfmGetDisplayScale() echo &"sizing {width} {height}" screen.resize(width.int32, height.int32, orientation.to_rotation) window.update_inset() window.screen.display_scale = scale {.emit:"void* global_glfm_window;".} var window {.importc:"global_glfm_window".}: Window proc NimMain() {.importc.} proc printf(s: cstring) {.importc,cdecl,header:"stdio.h".} {.push stackTrace:off.} proc glfmMain(w: Window) {.cdecl,exportc,noreturn.} = printf("mierda\n".cstring) window = w try: NimMain() except Exception as e: # TODO: use logging for line in e.getStackTrace.split '\n': echo line echo getCurrentExceptionMsg() {.pop.} proc make_window*(width, height: int32, title: string): Window = if window == nil: return window.glfmSetKeyFunc proc(display: ptr GLFMDisplay; keyCode: GLFMKey; action: GLFMKeyAction; mods: cint): bool {.cdecl.} = let screen = display.screen let key = cast[KeyCode](keyCode) let shiftKey = (mods and 1).bool let ctrlKey = (mods and 2).bool let altKey = (mods and 4).bool let metaKey = (mods and 8).bool for cb in screen.key_callbacks: cb(KeyEvent( pressed: action != GLFMKeyActionReleased, repeat: action == GLFMKeyActionRepeated, shiftKey: shiftKey, ctrlKey: ctrlKey, altKey: altKey, metaKey: metaKey, key: key, )) if screen.break_current_callbacks: screen.break_current_callbacks = false break return true window.glfmSetCharFunc proc(display: ptr GLFMDisplay; utf8: cstring; modifiers: cint) {.cdecl.} = let screen = display.screen let codepoint = cast[uint32](($utf8).runeAt(0)) for cb in screen.char_callbacks: cb(codepoint) if screen.break_current_callbacks: screen.break_current_callbacks = false break window.glfmSetTouchFunc proc (display: ptr GLFMDisplay; touch: cint; phase: GLFMTouchPhase; x: cdouble; y: cdouble): bool {.cdecl.} = let screen = window.screen if screen.is_touch: # mouse emulation let ending = phase in [GLFMTouchPhaseEnded, GLFMTouchPhaseCancelled] screen.emulateMouseWithTouch(touch, ending, x, y) else: if phase in [GLFMTouchPhaseMoved, GLFMTouchPhaseHover]: # TODO: check that the mousemove and mouseup events in web are # in window (so they still work after exiting the canvas) let screen = window.screen let buttons = screen.last_buttons let e = MouseMoveEvent( left: (buttons and 1).bool, middle: (buttons and 2).bool, right: (buttons and 4).bool, position: vec2(x,y), movement: vec2(x-screen.last_x, y-screen.last_y), ) for cb in screen.mouse_move_callbacks: cb(e) if screen.break_current_callbacks: screen.break_current_callbacks = false break screen.last_x = x screen.last_y = y else: let pressed = phase == GLFMTouchPhaseBegan # in glfm buttons 1 and 2 are swapped compared to glfw # (but they match DOM) let btn = case touch of 1: 2 of 2: 1 else: touch let e = MouseButtonEvent( pressed: pressed, button: cast[MouseButton](btn), position: vec2(x,y) ) for cb in window.screen.mouse_button_callbacks: cb(e) if screen.break_current_callbacks: screen.break_current_callbacks = false break if pressed: screen.last_buttons = screen.last_buttons or (1'i8 shl btn) else: screen.last_buttons = screen.last_buttons and not (1'i8 shl btn) screen.last_x = x screen.last_y = y return true window.glfmSetAppFocusFunc proc(window: Window, focused: bool) {.cdecl.} = # TODO: this event is not the same as resume/pause # but until we modify GLFM, this will do let screen = window.screen if focused: for f in screen.platform_event_callbacks: f(screen, PlatformResume) else: for f in screen.platform_event_callbacks: f(screen, PlatformPause) return window proc set_vsync*(window: Window, vsync: bool) = discard var max_messages = 0 proc init_graphics*(engine: MyouEngine, width, height: int32, title: string, opengl_version = 330, opengl_es = false, ) = assert window != nil let major = opengl_version div 100 let minor = opengl_version mod 100 div 10 let rev = opengl_version mod 10 assert major == 3 when defined(ios): # Using ANGLE on iOS let glver = GLFMRenderingAPIMetal else: let glver = case minor: of 0: GLFMRenderingAPIOpenGLES3 of 1: GLFMRenderingAPIOpenGLES31 of 2: GLFMRenderingAPIOpenGLES32 else: assert false GLFMRenderingAPIOpenGLES3 echo "configuring graphics" window.glfmSetDisplayConfig( glver, GLFMColorFormatRGB888, GLFMDepthFormat16, # GLFMDepthFormat24, GLFMStencilFormatNone, GLFMMultisampleNone) window.glfmSetSurfaceCreatedFunc proc(window: Window, w,h: cint) {.cdecl.} = # force resize window.screen = window.screen if not gladLoadGLES2(nil): echo "Could not initialize OpenGL" quit -1 window.screen.engine.renderer.initialize() window.glfmSetSurfaceDestroyedFunc proc(window: Window) {.cdecl.} = window.screen.engine.renderer.uninitialize() when not defined(release) and not defined(emscripten): proc f(source: GLenum, etype: GLenum, id: GLuint, severity: GLenum, length: GLsizei, message: cstring, userParam: pointer) {.stdcall.} = if id == 131185: # buffer usage hints return # if message == "GL_INVALID_OPERATION error generated. Target buffer must be bound.": # return if max_messages == 0: return # dump (source, etype, id, severity, length) echo "OpenGL error: ", message max_messages -= 1 if max_messages == 0: echo "No more OpenGL messages will be shown" glEnable(GL_DEBUG_OUTPUT) glDebugMessageCallback cast[GLdebugProc](f), nil glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS) max_messages = 100 # glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS) # glHint(GL_GENERATE_MIPMAP_HINT, GL_FASTEST) # glHint(GL_GENERATE_MIPMAP_HINT, GL_NICEST) var engine: MyouEngine var main_loop: proc(self: MyouEngine) proc glfm_breakpoint() {.exportc.} = # Add "b glfm_breakpoint" to your debugger startup commands discard proc start_platform_main_loop*(engine1: MyouEngine, main_loop1: proc(self: MyouEngine)) = engine = engine1 main_loop = main_loop1 window.glfmSetRenderFunc proc(window: Window) {.cdecl.} = try: engine.main_loop() except Exception as e: # TODO: use logging for line in e.getStackTrace.split '\n': echo line echo getCurrentExceptionMsg() glfm_breakpoint() # if not engine.vr_active: window.glfmSwapBuffers() type ANativeActivity = object callbacks: pointer vm: pointer env: pointer clazz: pointer internalDataPath: cstring externalDataPath: cstring sdkVersion: int32 instance: pointer assetManager: pointer obbPath: cstring # Deal with ASan false positives {.push noSanitize: ["address","hwaddress"].} #" proc myouAndroidGetActivity*(): ptr ANativeActivity = when defined(android): cast[ptr ANativeActivity](window.glfmAndroidGetActivity()) proc platform_switch_screen*(screen: Screen): bool {.inline.} = discard proc myouAndroidGetJniVM*(): pointer = when defined(android): return myouAndroidGetActivity().vm proc myouAndroidGetJniEnv*(): pointer = when defined(android): let vm = cast[ptr ptr array[7, pointer]](myouAndroidGetJniVM()) # Using offsets from jni.h let getEnv = cast[proc(vm: pointer, penv: var pointer, version: int32): int32 {.cdecl, gcsafe.}](vm[][6]) let attachCurrentThread = cast[proc(vm: pointer, penv: ptr pointer, args: pointer): int32 {.cdecl, gcsafe.}](vm[][4]) let status = getEnv(vm, result, 0x00010006'i32) if status == -2: # JNI_EDETACHED # Attach thread to VM if attachCurrentThread(vm, result.addr, nil) != 0: echo "could not attach thread to VM" result = nil elif status != 0: # JNI_OK result = nil # TODO: do we ever need to detach a thread from the VM? proc myouAndroidGetActivityContext*(): pointer = when defined(android): return myouAndroidGetActivity().clazz proc myouAndroidGetInternalDataPath*(): string = when defined(android): return $myouAndroidGetActivity().internalDataPath proc myouAndroidGetExternalDataPath*(): string = when defined(android): let ac = myouAndroidGetActivity() return $myouAndroidGetActivity().externalDataPath when defined(android): proc AAssetManager_open(asset_manager: pointer, filename: cstring, mode: int32): pointer {.importc,cdecl.} proc AAsset_close(asset: pointer) {.importc,cdecl.} proc AAsset_getBuffer(asset: pointer): pointer {.importc,cdecl.} proc AAsset_getLength64(asset: pointer): int64 {.importc,cdecl.} proc ANativeActivity_finish(activity: pointer) {.importc,cdecl.} proc myouAndroidGetEGLDisplay*(): pointer = glfmGetEGLDisplay(window) proc myouAndroidGetEGLConfig*(): pointer = glfmGetEGLConfig(window) proc myouAndroidGetEGLContext*(): pointer = glfmGetEGLContext(window) type myouAAssetObj = object asset: pointer p*: pointer len*: int type myouAAsset* = ref myouAAssetObj proc `=destroy`(x: var myouAAssetObj) = if x.asset != nil: AAsset_close(x.asset) x.asset = nil proc myouAndroidAPKFilePointerLength*(path: string): myouAAsset = let activity = cast[ptr array[9, pointer]](window.glfmAndroidGetActivity()) # Ussing offsets from ANativeActivity, assuming all pointers are aligned let asset_manager = activity[8] let asset = AAssetManager_open(asset_manager, path.cstring, 3) # 3 = buffer mode if asset == nil: echo "not found in APK: ", path return myouAAsset() let p = AAsset_getBuffer(asset) let len = AAsset_getLength64(asset).int myouAAsset(asset: asset, p: p, len: len) proc myouSetKeyboardVisible*(show: bool) = window.glfmSetKeyboardVisible(show) proc myouCloseMobileApp*() = ANativeActivity_finish(window.glfmAndroidGetActivity()) quit() {.pop.}