//----------------------------------------------------------------------------- // capture tests static uint64_t tests_captureframes() { static uint64_t capture_target; do_once capture_target = optioni("--capture", 0); return capture_target; } //----------------------------------------------------------------------------- // fps locking static volatile float framerate = 0; static volatile unsigned fps_active, timer_counter, loop_counter; static int fps__timing_thread(void *arg) { int64_t ns_excess = 0; while( fps_active ) { if( framerate <= 0 ) { loop_counter = timer_counter = 0; sleep_ms(250); } else { timer_counter++; int64_t tt = (int64_t)(1e9/(float)framerate) - ns_excess; uint64_t took = -time_ns(); #if is(win32) timeBeginPeriod(1); #endif sleep_ns( tt > 0 ? (float)tt : 0.f ); took += time_ns(); ns_excess = took - tt; if( ns_excess < 0 ) ns_excess = 0; //puts( strf("%lld", ns_excess) ); } } fps_active = 1; (void)arg; return thread_exit(0), 0; } static void fps_locker( int on ) { if( on ) { // private threaded timer fps_active = 1, timer_counter = loop_counter = 0; thread_init( fps__timing_thread, 0, "fps__timing_thread()", 0 ); } else { fps_active = 0; } } // function that locks render to desired `framerate` framerate (in FPS). // - assumes fps_locker() was enabled beforehand. // - returns true if must render, else 0. static int fps_wait() { if( framerate <= 0 ) return 1; if( !fps_active ) return 1; // if we throttled too much, cpu idle wait while( fps_active && (loop_counter > timer_counter) ) { //thread_yield(); sleep_ns(100); } // max auto frameskip is 10: ie, even if speed is low paint at least one frame every 10 enum { maxframeskip = 10 }; if( timer_counter > loop_counter + maxframeskip ) { loop_counter = timer_counter; } loop_counter++; // only draw if we are fast enough, otherwise skip the frame return loop_counter >= timer_counter; } static void window_vsync(float hz) { if( tests_captureframes() ) return; if( hz <= 0 ) return; do_once fps_locker(1); framerate = hz; fps_wait(); } //----------------------------------------------------------------------------- #if 0 // deprecated static void (*hooks[64])() = {0}; static void *userdatas[64] = {0}; bool window_hook(void (*func)(), void* user) { window_unhook( func ); for( int i = 0; i < 64; ++i ) { if( !hooks[i] ) { hooks[i] = func; userdatas[i] = user; return true; } } return false; } void window_unhook(void (*func)()) { for( int i = 0; i < 64; ++i ) { if(hooks[i] == func) { hooks[i] = 0; userdatas[i] = 0; } } } #endif static GLFWwindow *window; static int w, h, xpos, ypos, paused; static int fullscreen, xprev, yprev, wprev, hprev; static uint64_t frame_count; static double t, dt, fps, hz = 0.00; static char title[128] = {0}; static char screenshot_file[DIR_MAX]; static int locked_aspect_ratio = 0; static vec4 winbgcolor = {0,0,0,1}; vec4 window_getcolor_() { return winbgcolor; } // internal // ----------------------------------------------------------------------------- // glfw struct app { GLFWwindow *window; int width, height, keep_running; unsigned flags; struct nk_context *ctx; struct nk_glfw *nk_glfw; } appHandle = {0}, *g; static void glfw_error_callback(int error, const char *description) { if( is(osx) && error == 65544 ) return; // whitelisted PANIC("%s (error %x)", description, error); } void glfw_quit(void) { do_once { glfwTerminate(); } } void glfw_init() { do_once { g = &appHandle; glfwSetErrorCallback(glfw_error_callback); int ok = !!glfwInit(); assert(ok); // if(!ok) PANIC("cannot initialize glfw"); atexit(glfw_quit); //glfwTerminate); } } void window_drop_callback(GLFWwindow* window, int count, const char** paths) { // @fixme: win: convert from utf8 to window16 before processing char pathdir[DIR_MAX]; snprintf(pathdir, DIR_MAX, "%s/import/%llu_%s/", ART, (unsigned long long)date(), ifdef(linux, getlogin(), getenv("USERNAME"))); mkdir( pathdir, 0777 ); int errors = 0; for( int i = 0; i < count; ++i ) { const char *src = paths[i]; const char *dst = va("%s%s", pathdir, file_name(src)); errors += file_copy(src, dst) ? 0 : 1; } if( errors ) PANIC("%d errors found during file dropping", errors); else window_reload(); (void)window; } void window_hints(unsigned flags) { #ifdef __APPLE__ //glfwInitHint( GLFW_COCOA_CHDIR_RESOURCES, GLFW_FALSE ); glfwWindowHint( GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE ); // @todo: remove silicon mac M1 hack //glfwWindowHint( GLFW_COCOA_GRAPHICS_SWITCHING, GLFW_FALSE ); //glfwWindowHint( GLFW_COCOA_MENUBAR, GLFW_FALSE ); #endif #ifdef __APPLE__ /* We need to explicitly ask for a 3.2 context on OS X */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // osx glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); // osx, 2:#version150,3:330 #else // Compute shaders need 4.5 otherwise. But... // According to the GLFW docs, the context version hint acts as a minimum version. // i.e, the context you actually get may be a higher or highest version (which is usually the case) glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); #endif #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //osx #endif glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //osx+ems glfwWindowHint(GLFW_STENCIL_BITS, 8); //osx #if DEBUG glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE); #endif //glfwWindowHint( GLFW_RED_BITS, 8 ); //glfwWindowHint( GLFW_GREEN_BITS, 8 ); //glfwWindowHint( GLFW_BLUE_BITS, 8 ); //glfwWindowHint( GLFW_ALPHA_BITS, 8 ); //glfwWindowHint( GLFW_DEPTH_BITS, 24 ); //glfwWindowHint(GLFW_AUX_BUFFERS, Nth); //glfwWindowHint(GLFW_STEREO, GL_TRUE); glfwWindowHint(GLFW_DOUBLEBUFFER, GL_TRUE); // Prevent fullscreen window minimize on focus loss glfwWindowHint( GLFW_AUTO_ICONIFY, GL_FALSE ); // Fix SRGB on intels glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE); glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE); glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); // glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); // makes it non-resizable if(flags & WINDOW_MSAA2) glfwWindowHint(GLFW_SAMPLES, 2); // x2 AA if(flags & WINDOW_MSAA4) glfwWindowHint(GLFW_SAMPLES, 4); // x4 AA if(flags & WINDOW_MSAA8) glfwWindowHint(GLFW_SAMPLES, 8); // x8 AA g->flags = flags; } struct nk_glfw *window_handle_nkglfw() { return g->nk_glfw; } static renderstate_t window_rs; void glNewFrame() { do_once { window_rs = renderstate(); window_rs.blend_enabled = 1; window_rs.depth_test_enabled = 1; } window_rs.clear_color[0] = winbgcolor.r; window_rs.clear_color[1] = winbgcolor.g; window_rs.clear_color[2] = winbgcolor.b; window_rs.clear_color[3] = window_has_transparent() ? 0 : winbgcolor.a; // @transparent debug // if( input_down(KEY_F1) ) window_transparent(window_has_transparent()^1); // if( input_down(KEY_F2) ) window_maximize(window_has_maximize()^1); // @transparent debug #if 0 // is(ems) int canvasWidth, canvasHeight; emscripten_get_canvas_element_size("#canvas", &canvasWidth, &canvasHeight); w = canvasWidth; h = canvasHeight; //printf("%dx%d\n", w, h); #else //glfwGetWindowSize(window, &w, &h); glfwGetFramebufferSize(window, &w, &h); //printf("%dx%d\n", w, h); #endif g->width = w; g->height = h; renderstate_apply(&window_rs); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT ); } static bool cook_done = false; bool window_create_from_handle(void *handle, float scale, unsigned flags) { // abort run if any test suite failed in unit-test mode ifdef(debug, if( flag("--test") ) exit( test_errors ? -test_errors : 0 )); glfw_init(); v4k_init(); if(!t) t = glfwGetTime(); #if is(ems) scale = 100.f; #endif if( flag("--fullscreen") ) scale = 100; scale = (scale < 1 ? scale * 100 : scale); bool FLAGS_FULLSCREEN = scale > 100; bool FLAGS_FULLSCREEN_DESKTOP = scale == 100; bool FLAGS_WINDOWED = scale < 100; bool FLAGS_TRUE_BORDERLESS = flags & WINDOW_TRUE_BORDERLESS; bool FLAGS_TRANSPARENT = flag("--transparent") || (flags & WINDOW_TRANSPARENT); if( FLAGS_TRANSPARENT ) FLAGS_FULLSCREEN = 0, FLAGS_FULLSCREEN_DESKTOP = 0, FLAGS_WINDOWED = 1; scale = (scale > 100 ? 100 : scale) / 100.f; int winWidth = window_canvas().w * scale; int winHeight = window_canvas().h * scale; if (FLAGS_TRUE_BORDERLESS) { FLAGS_FULLSCREEN = FLAGS_FULLSCREEN_DESKTOP = 0; FLAGS_WINDOWED = 1; flags |= WINDOW_BORDERLESS; } /* if (tests_captureframes()) { winWidth = 1280; winHeight = 720; } */ window_hints(flags); GLFWmonitor* monitor = NULL; #ifndef __EMSCRIPTEN__ if( FLAGS_FULLSCREEN || FLAGS_FULLSCREEN_DESKTOP ) { monitor = glfwGetPrimaryMonitor(); } if( FLAGS_FULLSCREEN_DESKTOP ) { const GLFWvidmode* mode = glfwGetVideoMode(monitor); glfwWindowHint(GLFW_RED_BITS, mode->redBits); glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits); glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits); glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate); winWidth = mode->width; winHeight = mode->height; } if( FLAGS_WINDOWED ) { #if !is(ems) if( FLAGS_TRANSPARENT ) { // @transparent //glfwWindowHint(GLFW_MOUSE_PASSTHROUGH, GLFW_TRUE); // see through. requires undecorated //glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); // always on top glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); } if( flags & WINDOW_BORDERLESS ) { // glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE); glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); } #endif // windowed float ratio = (float)winWidth / (winHeight + !winHeight); if( flags & WINDOW_SQUARE ) winWidth = winHeight = winWidth > winHeight ? winHeight : winWidth; //if( flags & WINDOW_LANDSCAPE ) if( winWidth < winHeight ) winHeight = winWidth * ratio; if( flags & WINDOW_PORTRAIT ) if( winWidth > winHeight ) winWidth = winHeight * (1.f / ratio); } #endif window = handle ? handle : glfwCreateWindow(winWidth, winHeight, "", monitor, NULL); if( !window ) return PANIC("GLFW Window creation failed"), false; glfwGetFramebufferSize(window, &w, &h); //glfwGetWindowSize(window, &w, &h); if( flags & WINDOW_FIXED ) { // disable resizing glfwSetWindowSizeLimits(window, w, h, w, h); } if( flags & (WINDOW_SQUARE | WINDOW_PORTRAIT | WINDOW_LANDSCAPE | WINDOW_ASPECT) ) { // keep aspect ratio window_aspect_lock(w, h); } #ifndef __EMSCRIPTEN__ if( FLAGS_WINDOWED ) { // center window monitor = monitor ? monitor : glfwGetPrimaryMonitor(); const GLFWvidmode* mode = glfwGetVideoMode(monitor); int area_width = mode->width, area_height = mode->height; glfwGetMonitorWorkarea(monitor, &xpos, &ypos, &area_width, &area_height); glfwSetWindowPos(window, xpos = xpos + (area_width - winWidth) / 2, ypos = ypos + (area_height - winHeight) / 2); //printf("%dx%d @(%d,%d) [res:%dx%d]\n", winWidth, winHeight, xpos,ypos, area_width, area_height ); wprev = w, hprev = h; xprev = xpos, yprev = ypos; } #endif glfwMakeContextCurrent(window); #if is(ems) if( FLAGS_FULLSCREEN ) window_fullscreen(1); #else int gl_version = gladLoadGL(glfwGetProcAddress); #endif glDebugEnable(); // setup nuklear ui ui_ctx = nk_glfw3_init(&nk_glfw, window, NK_GLFW3_INSTALL_CALLBACKS); //glEnable(GL_TEXTURE_2D); // 0:disable vsync, 1:enable vsync, <0:adaptive (allow vsync when framerate is higher than syncrate and disable vsync when framerate drops below syncrate) flags |= optioni("--vsync", 0) || flag("--vsync") ? WINDOW_VSYNC : WINDOW_VSYNC_DISABLED; flags |= optioni("--vsync-adaptive", 0) || flag("--vsync-adaptive") ? WINDOW_VSYNC_ADAPTIVE : 0; int has_adaptive_vsync = glfwExtensionSupported("WGL_EXT_swap_control_tear") || glfwExtensionSupported("GLX_EXT_swap_control_tear") || glfwExtensionSupported("EXT_swap_control_tear"); int wants_adaptive_vsync = (flags & WINDOW_VSYNC_ADAPTIVE); int interval = has_adaptive_vsync && wants_adaptive_vsync ? -1 : (flags & WINDOW_VSYNC ? 1 : 0); glfwSwapInterval(interval); const GLFWvidmode *mode = glfwGetVideoMode(monitor ? monitor : glfwGetPrimaryMonitor()); PRINTF("Build version: %s\n", BUILD_VERSION); PRINTF("Monitor: %s (%dHz, vsync=%d)\n", glfwGetMonitorName(monitor ? monitor : glfwGetPrimaryMonitor()), mode->refreshRate, interval); PRINTF("GPU device: %s\n", glGetString(GL_RENDERER)); PRINTF("GPU driver: %s\n", glGetString(GL_VERSION)); #if !is(ems) PRINTF("GPU OpenGL: %d.%d\n", GLAD_VERSION_MAJOR(gl_version), GLAD_VERSION_MINOR(gl_version)); if( FLAGS_TRANSPARENT ) { // @transparent glfwSetWindowAttrib(window, GLFW_DECORATED, GLFW_FALSE); // @todo: is decorated an attrib or a hint? if( scale >= 1 ) glfwMaximizeWindow(window); } if ( FLAGS_TRUE_BORDERLESS ) { if( scale >= 1 ) glfwMaximizeWindow(window); glfwSetWindowSize(window, w, h); } #endif g->ctx = ui_ctx; g->nk_glfw = &nk_glfw; g->window = window; g->width = window_width(); g->height = window_height(); PRINTF("Window: %dx%d\n", g->width, g->height); if (glfwRawMouseMotionSupported()) { glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); } // window_cursor(flags & WINDOW_NO_MOUSE ? false : true); glfwSetDropCallback(window, window_drop_callback); // camera inits for v4k_pre_init() -> ddraw_flush() -> get_active_camera() // static camera_t cam = {0}; id44(cam.view); id44(cam.proj); extern camera_t *last_camera; last_camera = &cam; v4k_pre_init(); // display a progress bar meanwhile cook is working in the background // Sleep(500); if( !COOK_ON_DEMAND ) if( have_tools() && cook_jobs() ) while( cook_progress() < 100 ) { for( int frames = 0; frames < 2/*10*/ && window_swap(); frames += cook_progress() >= 100 ) { window_title(va("%s %.2d%%", cook_cancelling ? "Aborting" : "Cooking assets", cook_progress())); if( input(KEY_ESC) ) cook_cancel(); glNewFrame(); static float previous[JOBS_MAX] = {0}; #define ddraw_progress_bar(JOB_ID, JOB_MAX, PERCENT) do { \ /* NDC coordinates (2d): bottom-left(-1,-1), center(0,0), top-right(+1,+1) */ \ float progress = (PERCENT+1) / 100.f; if(progress > 1) progress = 1; \ float speed = progress < 1 ? 0.05f : 0.75f; \ float smooth = previous[JOB_ID] = progress * speed + previous[JOB_ID] * (1-speed); \ \ float pixel = 2.f / window_height(), dist = smooth*2-1, y = pixel*3*JOB_ID; \ if(JOB_ID==0)ddraw_line(vec3(-1,y-pixel*2,0), vec3(1, y-pixel*2,0)); /* full line */ \ ddraw_line(vec3(-1,y-pixel ,0), vec3(dist,y-pixel ,0)); /* progress line */ \ ddraw_line(vec3(-1,y+0 ,0), vec3(dist,y+0 ,0)); /* progress line */ \ ddraw_line(vec3(-1,y+pixel ,0), vec3(dist,y+pixel ,0)); /* progress line */ \ if(JOB_ID==JOB_MAX-1)ddraw_line(vec3(-1,y+pixel*2,0), vec3(1, y+pixel*2,0)); /* full line */ \ } while(0) if( FLAGS_TRANSPARENT ) {} else // @transparent for(int i = 0; i < cook_jobs(); ++i) ddraw_progress_bar(i, cook_jobs(), jobs[i].progress); // ddraw_progress_bar(0, 1, cook_progress()); ddraw_flush(); do_once window_visible(1); // render progress bar at 30Hz + give the cook threads more time to actually cook the assets. // no big deal since progress bar is usually quiet when cooking assets most of the time. // also, make the delay even larger when window is minimized or hidden. // shaved cook times: 88s -> 57s (tcc), 50s -> 43s (vc) sleep_ms( window_has_visible() && window_has_focus() ? 8 : 16 ); } // set black screen glNewFrame(); window_swap(); #if !ENABLE_RETAIL window_title(""); #endif } if(cook_cancelling) cook_stop(), exit(-1); cook_done = true; v4k_post_init(mode->refreshRate); return true; } bool window_create(float scale, unsigned flags) { return window_create_from_handle(NULL, scale, flags); } static double boot_time = 0; char* window_stats() { static double num_frames = 0, begin = FLT_MAX, prev_frame = 0; double now = time_ss(); if( boot_time < 0 ) boot_time = now; if( begin > now ) { begin = now; num_frames = 0; } if( (now - begin) >= 0.25f ) { fps = num_frames * (1.f / (now - begin)); } if( (now - begin) > 1 ) { begin = now + ((now - begin) - 1); num_frames = 0; } const char *cmdline = app_cmdline(); // @todo: print %used/%avail kib mem, %used/%avail objs as well static char buf[256]; snprintf(buf, 256, "%s%s%s%s | boot %.2fs | %5.2ffps (%.2fms)%s%s", title, BUILD_VERSION[0] ? " (":"", BUILD_VERSION[0] ? BUILD_VERSION:"", BUILD_VERSION[0] ? ")":"", !boot_time ? now : boot_time, fps, (now - prev_frame) * 1000.f, cmdline[0] ? " | ":"", cmdline[0] ? cmdline:""); prev_frame = now; ++num_frames; return buf + strspn(buf, " "); } int window_frame_begin() { glfwPollEvents(); // we cannot simply terminate threads on some OSes. also, aborted cook jobs could leave temporary files on disc. // so let's try to be polite: we will be disabling any window closing briefly until all cook is either done or canceled. static bool has_cook; do_once has_cook = !COOK_ON_DEMAND && have_tools() && cook_jobs(); if( has_cook ) { has_cook = cook_progress() < 100; if( glfwWindowShouldClose(g->window) ) cook_cancel(); glfwSetWindowShouldClose(g->window, GLFW_FALSE); } if( glfwWindowShouldClose(g->window) ) { return 0; } glNewFrame(); ui_create(); #if !ENABLE_RETAIL bool has_menu = ui_has_menubar(); bool may_render_debug_panel = 1; if( have_tools() ) { static int cook_has_progressbar; do_once cook_has_progressbar = !COOK_ON_DEMAND; if( cook_has_progressbar) { // render profiler, unless we are in the cook progress screen static unsigned frames = 0; if(frames <= 0) frames += cook_progress() >= 100; may_render_debug_panel = (frames > 0); } } if (!win_debug_visible) may_render_debug_panel = 0; // generate Debug panel contents if( may_render_debug_panel ) { if( has_menu ? ui_window("Debug " ICON_MD_SETTINGS, 0) : ui_panel("Debug " ICON_MD_SETTINGS, 0) ) { ui_engine(); (has_menu ? ui_window_end : ui_panel_end)(); } API int engine_tick(); engine_tick(); } #endif // ENABLE_RETAIL #if 0 // deprecated // run user-defined hooks for(int i = 0; i < 64; ++i) { if( hooks[i] ) hooks[i]( userdatas[i] ); } #endif double now = paused ? t : glfwGetTime(); dt = now - t; t = now; #if !ENABLE_RETAIL char *st = window_stats(); static double timer = 0; timer += window_delta(); if( timer >= 0.25 ) { glfwSetWindowTitle(window, st); timer = 0; } #else glfwSetWindowTitle(window, title); #endif void input_update(); input_update(); return 1; } void window_frame_end() { // flush batching systems that need to be rendered before frame swapping. order matters. { font_goto(0,0); touch_flush(); sprite_flush(); // flush all debugdraw calls before swap dd_ontop = 0; ddraw_flush(); glClear(GL_DEPTH_BUFFER_BIT); dd_ontop = 1; ddraw_flush(); ui_render(); } #if !is(ems) // save screenshot if queued if( screenshot_file[0] ) { int n = 3; void *rgb = screenshot(n); stbi_flip_vertically_on_write(true); if(!stbi_write_png(screenshot_file, w, h, n, rgb, n * w) ) { PANIC("!could not write screenshot file `%s`\n", screenshot_file); } screenshot_file[0] = 0; } if( record_active() ) { void record_frame(); record_frame(); } #endif } void window_frame_swap() { // glFinish(); #if !is(ems) window_vsync(hz); #endif glfwSwapBuffers(window); // emscripten_webgl_commit_frame(); static int delay = 0; do_once delay = optioni("--delay", 0); if( delay && !COOK_ON_DEMAND && cook_progress() >= 100 ) sleep_ms( delay ); } static void window_shutdown() { do_once { #if ENABLE_SELFIES snprintf(screenshot_file, DIR_MAX, "%s.png", app_name()); int n = 3; void *rgb = screenshot(n); stbi_flip_vertically_on_write(true); if(!stbi_write_png(screenshot_file, w, h, n, rgb, n * w) ) { PANIC("!could not write screenshot file `%s`\n", screenshot_file); } screenshot_file[0] = 0; #endif window_loop_exit(); // finish emscripten loop automatically } } int window_swap() { // end frame if( frame_count > 0 ) { window_frame_end(); window_frame_swap(); } ++frame_count; // begin frame int ready = window_frame_begin(); if( !ready ) { window_shutdown(); return 0; } static uint64_t capture_frame = 0; if( cook_done && ++capture_frame == tests_captureframes() ) { mkdir( "tests/out", 0777 ); const char *screenshot_file = va("tests/out/%s.png", app_name()); int n = 3; void *rgb = screenshot(n); stbi_flip_vertically_on_write(true); if(!stbi_write_png(screenshot_file, w, h, n, rgb, n * w) ) { PANIC("!could not write screenshot file `%s`\n", screenshot_file); } return 0; } return 1; } static void (*window_render_callback)(void* loopArg); static vec2 last_canvas_size = {0}; static void window_resize() { #if is(ems) EM_ASM(canvas.canResize = 0); if (g->flags&WINDOW_FIXED) return; EM_ASM(canvas.canResize = 1); vec2 size = window_canvas(); if (size.x != last_canvas_size.x || size.y != last_canvas_size.y) { w = size.x; h = size.y; g->width = w; g->height = h; last_canvas_size = vec2(w,h); emscripten_set_canvas_size(w, h); } #endif /* __EMSCRIPTEN__ */ } static void window_loop_wrapper( void *loopArg ) { if( window_frame_begin() ) { window_resize(); window_render_callback(loopArg); window_frame_end(); window_frame_swap(); } else { do_once window_shutdown(); } } void window_loop(void (*user_function)(void* loopArg), void* loopArg ) { #if is(ems) window_render_callback = user_function; emscripten_set_main_loop_arg(window_loop_wrapper, loopArg, 0, 1); #else g->keep_running = true; while (g->keep_running) user_function(loopArg); #endif /* __EMSCRIPTEN__ */ } void window_loop_exit() { #if is(ems) emscripten_cancel_main_loop(); #else g->keep_running = false; #endif /* __EMSCRIPTEN__ */ } vec2 window_canvas() { #if is(ems) int width = EM_ASM_INT_V(return canvas.width); int height = EM_ASM_INT_V(return canvas.height); return vec2(width, height); #else glfw_init(); const GLFWvidmode* mode = glfwGetVideoMode( glfwGetPrimaryMonitor() ); assert( mode ); return vec2(mode->width, mode->height); #endif /* __EMSCRIPTEN__ */ } int window_width() { return w; } int window_height() { return h; } double window_time() { return t; } double window_delta() { return dt; } void window_debug(int visible) { win_debug_visible = visible; } int window_has_debug() { return win_debug_visible; } double window_fps() { return fps; } void window_fps_lock(float fps) { hz = fps; } void window_fps_unlock() { hz = 0; } double window_fps_target() { return hz; } void window_fps_vsync(int vsync) { glfwSwapInterval(vsync); } uint64_t window_frame() { return frame_count; } void window_title(const char *title_) { snprintf(title, 128, "%s", title_); if( !title[0] ) glfwSetWindowTitle(window, title); } void window_color(unsigned color) { unsigned r = (color >> 0) & 255; unsigned g = (color >> 8) & 255; unsigned b = (color >> 16) & 255; unsigned a = (color >> 24) & 255; winbgcolor = vec4(r / 255.0, g / 255.0, b / 255.0, a / 255.0); } static int has_icon; int window_has_icon() { return has_icon; } void window_icon(const char *file_icon) { int len = 0; void *data = vfs_load(file_icon, &len); if( !data ) data = file_read(file_icon), len = file_size(file_icon); if( data && len ) { image_t img = image_from_mem(data, len, IMAGE_RGBA); if( img.w && img.h && img.pixels ) { GLFWimage images[1]; images[0].width = img.w; images[0].height = img.h; images[0].pixels = img.pixels; glfwSetWindowIcon(window, 1, images); has_icon = 1; return; } } #if 0 // is(win32) HANDLE hIcon = LoadImageA(0, file_icon, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE); if( hIcon ) { HWND hWnd = glfwGetWin32Window(window); SendMessage(hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); SendMessage(hWnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon); SendMessage(GetWindow(hWnd, GW_OWNER), WM_SETICON, ICON_SMALL, (LPARAM)hIcon); SendMessage(GetWindow(hWnd, GW_OWNER), WM_SETICON, ICON_BIG, (LPARAM)hIcon); has_icon = 1; return; } #endif } void* window_handle() { return window; } void window_reload() { // @todo: save_on_exit(); fflush(0); // chdir(app_path()); execv(__argv[0], __argv); exit(0); } int window_record(const char *outfile_mp4) { record_start(outfile_mp4); // @todo: if( flags & RECORD_MOUSE ) if( record_active() ) window_cursor_shape(CURSOR_SW_AUTO); else window_cursor_shape(CURSOR_HW_ARROW); return record_active(); } vec2 window_dpi() { vec2 dpi = vec2(1,1); #if !is(ems) && !is(osx) // @todo: remove silicon mac M1 hack glfwGetMonitorContentScale(glfwGetPrimaryMonitor(), &dpi.x, &dpi.y); #endif return dpi; } // ----------------------------------------------------------------------------- // fullscreen static GLFWmonitor *window_find_monitor(int wx, int wy) { GLFWmonitor *monitor = glfwGetPrimaryMonitor(); // find best monitor given current window coordinates. @todo: select by ocuppied window area inside each monitor instead. int num_monitors = 0; GLFWmonitor** monitors = glfwGetMonitors(&num_monitors); #if is(ems) return *monitors; #else for( int i = 0; i < num_monitors; ++i) { int mx = 0, my = 0, mw = 0, mh = 0; glfwGetMonitorWorkarea(monitors[i], &mx, &my, &mw, &mh); monitor = wx >= mx && wx <= (mx+mw) && wy >= my && wy <= (my+mh) ? monitors[i] : monitor; } return monitor; #endif } #if 0 // to deprecate void window_fullscreen(int enabled) { fullscreen = !!enabled; #ifndef __EMSCRIPTEN__ if( fullscreen ) { int wx = 0, wy = 0; glfwGetWindowPos(window, &wx, &wy); GLFWmonitor *monitor = window_find_monitor(wx, wy); wprev = w, hprev = h, xprev = wx, yprev = wy; // save window context for further restoring int width, height; glfwGetMonitorWorkarea(monitor, NULL, NULL, &width, &height); glfwSetWindowMonitor(window, monitor, 0, 0, width, height, GLFW_DONT_CARE); } else { glfwSetWindowMonitor(window, NULL, xpos, ypos, wprev, hprev, GLFW_DONT_CARE); glfwSetWindowPos(window, xprev, yprev); } #endif } int window_has_fullscreen() { return fullscreen; } #else int window_has_fullscreen() { #if is(ems) EmscriptenFullscreenChangeEvent fsce; emscripten_get_fullscreen_status(&fsce); return !!fsce.isFullscreen; #else return !!glfwGetWindowMonitor(g->window); #endif /* __EMSCRIPTEN__ */ } void window_fullscreen(int enabled) { if( window_has_fullscreen() == !!enabled ) return; #if is(ems) #if 0 // deprecated: crash if( enabled ) { emscripten_exit_soft_fullscreen(); /* Workaround https://github.com/kripken/emscripten/issues/5124#issuecomment-292849872 */ EM_ASM(JSEvents.inEventHandler = true); EM_ASM(JSEvents.currentEventHandler = {allowsDeferredCalls:true}); EmscriptenFullscreenStrategy strategy = {0}; strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; // _ASPECT strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; // _NONE _HIDEF strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; // _NEAREST emscripten_request_fullscreen_strategy(NULL, EM_FALSE/*EM_TRUE*/, &strategy); //emscripten_enter_soft_fullscreen(NULL, &strategy); } else { emscripten_exit_fullscreen(); } #else if( enabled ) EM_ASM(Module.requestFullscreen(1, 1)); else EM_ASM(Module.exitFullscreen()); #endif #else #if 0 if( enabled ) { /*glfwGetWindowPos(g->window, &g->window_xpos, &g->window_ypos);*/ glfwGetWindowSize(g->window, &g->width, &g->height); glfwSetWindowMonitor(g->window, glfwGetPrimaryMonitor(), 0, 0, g->width, g->height, GLFW_DONT_CARE); } else { glfwSetWindowMonitor(g->window, NULL, 0, 0, g->width, g->height, GLFW_DONT_CARE); } #else if( enabled ) { int wx = 0, wy = 0; glfwGetWindowPos(window, &wx, &wy); GLFWmonitor *monitor = window_find_monitor(wx, wy); wprev = w, hprev = h, xprev = wx, yprev = wy; // save window context for further restoring int width, height; glfwGetMonitorWorkarea(monitor, NULL, NULL, &width, &height); glfwSetWindowMonitor(window, monitor, 0, 0, width, height, GLFW_DONT_CARE); } else { glfwSetWindowMonitor(window, NULL, xpos, ypos, wprev, hprev, GLFW_DONT_CARE); glfwSetWindowPos(window, xprev, yprev); } #endif #endif } #endif void window_pause(int enabled) { paused = enabled; } int window_has_pause() { return paused; } void window_focus() { glfwFocusWindow(window); } int window_has_focus() { return !!glfwGetWindowAttrib(window, GLFW_FOCUSED); } static int cursorshape = 1; void window_cursor_shape(unsigned mode) { cursorshape = (mode &= 7); static GLFWcursor* cursors[7] = { 0 }; static unsigned enums[7] = { 0, GLFW_ARROW_CURSOR, GLFW_IBEAM_CURSOR, GLFW_HRESIZE_CURSOR, GLFW_VRESIZE_CURSOR, GLFW_HAND_CURSOR, GLFW_CROSSHAIR_CURSOR, }; do_once { static unsigned pixels[16 * 16] = { 0x01000000 }; // ABGR(le) glfw3 note: A(0x00) means 0xFF for some reason static GLFWimage image = { 16, 16, (void*)pixels }; static GLFWcursor* empty; for( int x = 0; x < 16 * 16; ++x ) pixels[x] = pixels[0]; empty = glfwCreateCursor(&image, 0, 0); for(int i = 0; i < countof(enums); ++i) cursors[i] = i ? glfwCreateStandardCursor( enums[i] ) : empty; } if( mode == CURSOR_SW_AUTO ) { // UI (nuklear) driven cursor nk_style_show_cursor(ui_handle()); glfwSetCursor(window, cursors[0] ); } else { nk_style_hide_cursor(ui_handle()); glfwSetCursor(window, mode < countof(enums) ? cursors[mode] : NULL); } } void window_cursor(int visible) { (cursorshape == CURSOR_SW_AUTO && visible ? nk_style_show_cursor : nk_style_hide_cursor)(ui_handle()); glfwSetInputMode(window, GLFW_CURSOR, visible ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED); } int window_has_cursor() { return glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_NORMAL; } void window_visible(int visible) { if(!window) return; //if(window) (visible ? glfwRestoreWindow : glfwIconifyWindow)(window); (visible ? glfwShowWindow : glfwHideWindow)(window); // call glfwpollevents in linux to flush visiblity changes that would happen in next frame otherwise #if is(linux) || is(osx) glfwPollEvents(); #endif } int window_has_visible() { return glfwGetWindowAttrib(window, GLFW_VISIBLE); } void window_screenshot(const char* outfile_png) { snprintf(screenshot_file, DIR_MAX, "%s", outfile_png ? outfile_png : ""); } double window_aspect() { return (double)w / (h + !h); } void window_aspect_lock(unsigned numer, unsigned denom) { if(!window) return; if( numer * denom ) { glfwSetWindowAspectRatio(window, numer, denom); } else { glfwSetWindowAspectRatio(window, GLFW_DONT_CARE, GLFW_DONT_CARE); } } void window_aspect_unlock() { if(!window) return; window_aspect_lock(0, 0); } void window_transparent(int enabled) { #if !is(ems) if( !window_has_fullscreen() ) { if( enabled ) { glfwSetWindowAttrib(window, GLFW_DECORATED, GLFW_FALSE); //glfwSetWindowAttrib(window, GLFW_MOUSE_PASSTHROUGH , GLFW_TRUE); //glfwMaximizeWindow(window); } else { //glfwRestoreWindow(window); //glfwSetWindowAttrib(window, GLFW_MOUSE_PASSTHROUGH , GLFW_FALSE); glfwSetWindowAttrib(window, GLFW_DECORATED, GLFW_TRUE); } } #endif } int window_has_transparent() { return ifdef(ems, 0, glfwGetWindowAttrib(window, GLFW_DECORATED) != GLFW_TRUE); } void window_maximize(int enabled) { ifdef(ems, return); if( !window_has_fullscreen() ) { if( enabled ) { glfwMaximizeWindow(window); } else { glfwRestoreWindow(window); } } } int window_has_maximize() { return ifdef(ems, 0, glfwGetWindowAttrib(window, GLFW_MAXIMIZED) == GLFW_TRUE); } const char *window_clipboard() { return glfwGetClipboardString(window); } void window_setclipboard(const char *text) { glfwSetClipboardString(window, text); } static double window_scale() { // ok? @testme float xscale = 1, yscale = 1; #if !is(ems) && !is(osx) // @todo: remove silicon mac M1 hack GLFWmonitor *monitor = glfwGetPrimaryMonitor(); glfwGetMonitorContentScale(monitor, &xscale, &yscale); #endif return maxi(xscale, yscale); }