385 lines
15 KiB
Nim
385 lines
15 KiB
Nim
# 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,
|
|
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()
|
|
window.glfmSwapBuffers()
|
|
|
|
proc myouAndroidGetActivity*(): pointer =
|
|
when defined(android):
|
|
window.glfmAndroidGetActivity()
|
|
|
|
proc platform_switch_screen*(screen: Screen): bool {.inline.} =
|
|
discard
|
|
|
|
proc myouAndroidGetJniEnv*(): pointer =
|
|
when defined(android):
|
|
let activity = cast[ptr array[2, pointer]](window.glfmAndroidGetActivity())
|
|
# Ussing offsets from ANativeActivity
|
|
let vm = cast[ptr ptr array[7, pointer]](activity[1])
|
|
# 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 myouAndroidGetInternalDataPath*(): string =
|
|
when defined(android):
|
|
let activity = cast[ptr array[6, cstring]](window.glfmAndroidGetActivity())
|
|
return $activity[4]
|
|
|
|
proc myouAndroidGetExternalDataPath*(): string =
|
|
when defined(android):
|
|
let activity = cast[ptr array[6, cstring]](window.glfmAndroidGetActivity())
|
|
return $activity[5]
|
|
|
|
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.}
|
|
|
|
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()
|