build system revision
[vg.git] / vg_engine.c
diff --git a/vg_engine.c b/vg_engine.c
new file mode 100644 (file)
index 0000000..0d8c015
--- /dev/null
@@ -0,0 +1,1192 @@
+#include "vg_engine.h"
+#include "vg_async.h"
+
+struct vg_engine vg = { 
+   .time_rate = 1.0, 
+   .time_fixed_delta = VG_TIMESTEP_FIXED 
+};
+
+#include <string.h>
+
+enum engine_status _vg_engine_status(void)
+{
+   SDL_AtomicLock( &vg.sl_status );
+   enum engine_status status = vg.engine_status;
+   SDL_AtomicUnlock( &vg.sl_status );
+
+   return status;
+}
+
+enum vg_thread_purpose vg_thread_purpose(void)
+{
+   SDL_AtomicLock( &vg.sl_status );
+
+   if( vg.thread_id_main == SDL_GetThreadID(NULL) ){
+      SDL_AtomicUnlock( &vg.sl_status );
+      return k_thread_purpose_main;
+   }
+   else{
+      SDL_AtomicUnlock( &vg.sl_status );
+      return k_thread_purpose_loader;
+   }
+}
+
+static void vg_assert_thread( enum vg_thread_purpose required )
+{
+   enum vg_thread_purpose purpose = vg_thread_purpose();
+
+   if( purpose != required ){
+      vg_fatal_error( "thread_purpose must be %u not %u\n", required, purpose );
+   }
+}
+
+static void _vg_opengl_sync_init(void)
+{
+   vg.sem_loader = SDL_CreateSemaphore(1);
+}
+
+#include "vg_console.h"
+#include "vg_profiler.h"
+#ifndef VG_NO_AUDIO
+  #include "vg_audio.h"
+#endif
+#include "vg_shader.h"
+#include "vg_tex.h"
+#include "vg_input.h"
+#include "vg_imgui.h"
+#include "vg_lines.h"
+#include "vg_rigidbody_view.h"
+#include "vg_loader.h"
+#include "vg_opt.h"
+
+/* Diagnostic */
+static struct vg_profile vg_prof_update = {.name="update()"},
+                         vg_prof_render = {.name="render()"},
+                         vg_prof_swap   = {.name="swap"};
+
+void vg_checkgl( const char *src_info )
+{
+   int fail = 0;
+
+   GLenum err;
+   while( (err = glGetError()) != GL_NO_ERROR ){
+      vg_error( "(%s) OpenGL Error: #%d\n", src_info, err );
+      fail = 1;
+   }
+
+   if( fail )
+      vg_fatal_error( "OpenGL Error" );
+}
+
+static void async_vg_bake_shaders( void *payload, u32 size )
+{
+   vg_shaders_compile();
+}
+
+void vg_bake_shaders(void)
+{
+   vg_console_reg_cmd( "reload_shaders", vg_shaders_live_recompile, NULL );
+   vg_async_call( async_vg_bake_shaders, NULL, 0 );
+}
+
+void async_internal_complete( void *payload, u32 size )
+{
+   vg_success( "Internal async setup complete\n" );
+   SDL_AtomicLock( &vg.sl_status );
+
+   if( vg.engine_status == k_engine_status_crashed ){
+      SDL_AtomicUnlock( &vg.sl_status );
+      return;
+   }
+   else{
+      vg.engine_status = k_engine_status_running;
+   }
+
+   SDL_AtomicUnlock( &vg.sl_status );
+}
+
+static void _vg_load_full( void *data )
+{
+   vg_preload();
+   vg_tex2d_replace_with_error_async( &vg.tex_missing );
+   vg_ui.tex_bg = vg.tex_missing;
+
+   /* internal */
+   vg_loader_step( vg_input_init, vg_input_free );
+   vg_loader_step( vg_lines_init, NULL );
+   vg_loader_step( vg_rb_view_init, NULL );
+#ifndef VG_NO_AUDIO
+   vg_loader_step( vg_audio_init, vg_audio_free );
+#endif
+   vg_loader_step( vg_profiler_init, NULL );
+
+   /* client */
+   vg_load();
+
+   vg_async_call( async_internal_complete, NULL, 0 );
+
+   vg_success( "Client loaded in %fs\n", vg.time_real );
+}
+
+static void _vg_process_events(void)
+{
+   v2_zero( vg.mouse_wheel );
+   v2_zero( vg.mouse_delta );
+
+   /* Update input */
+   vg_process_inputs();
+
+   /* SDL event loop */
+   SDL_Event event;
+   while( SDL_PollEvent( &event ) ){
+      if( event.type == SDL_KEYDOWN ){
+         if( vg_console.enabled && 
+               (vg_ui.focused_control_type != k_ui_control_modal) ){
+            if( event.key.keysym.sym == SDLK_ESCAPE ||
+                event.key.keysym.scancode == SDL_SCANCODE_GRAVE ){
+               vg_console.enabled = 0;
+               ui_defocus_all();
+            }
+            else if( (event.key.keysym.mod & KMOD_CTRL) && 
+                      event.key.keysym.sym == SDLK_n ){
+               console_suggest_next();
+            }
+            else if( (event.key.keysym.mod & KMOD_CTRL ) &&
+                      event.key.keysym.sym == SDLK_p ){
+               console_suggest_prev();
+            }
+            else{
+               ui_proc_key( event.key.keysym );
+            }
+         }
+         else{
+            if( event.key.keysym.scancode == SDL_SCANCODE_GRAVE ){
+               vg_console.enabled = 1;
+            }
+            else {
+               ui_proc_key( event.key.keysym );
+            }
+         }
+      }
+      else if( event.type == SDL_MOUSEWHEEL ){
+         vg.mouse_wheel[0] += event.wheel.preciseX;
+         vg.mouse_wheel[1] += event.wheel.preciseY;
+      }
+      else if( event.type == SDL_CONTROLLERDEVICEADDED ||
+               event.type == SDL_CONTROLLERDEVICEREMOVED )
+      {
+         vg_input_device_event( &event );
+      }
+      else if( event.type == SDL_CONTROLLERAXISMOTION ||
+               event.type == SDL_CONTROLLERBUTTONDOWN ||
+               event.type == SDL_CONTROLLERBUTTONUP )
+      {
+         vg_input_controller_event( &event );
+      }
+      else if( event.type == SDL_MOUSEMOTION ){
+         vg.mouse_delta[0] += event.motion.xrel;
+         vg.mouse_delta[1] += event.motion.yrel;
+      }
+      else if( event.type == SDL_WINDOWEVENT ){
+         if( event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ){
+            int w, h;
+            SDL_GL_GetDrawableSize( vg.window, &w, &h );
+
+            if( !w || !h ){
+               vg_warn( "Got a invalid framebuffer size: "
+                        "%dx%d... ignoring\n", w, h );
+            }
+            else{
+               vg.window_x = w;
+               vg.window_y = h;
+
+               vg_framebuffer_resize(w,h);
+            }
+         }
+         else if( event.window.event == SDL_WINDOWEVENT_CLOSE ){
+            vg.window_should_close = 1;
+         }
+      }
+      else if( event.type == SDL_TEXTINPUT ){
+         ui_proc_utf8( event.text.text );
+      }
+   }
+
+   SDL_GetMouseState( &vg.mouse_pos[0], &vg.mouse_pos[1] );
+}
+
+static void _vg_gameloop_update(void)
+{
+   vg_profile_begin( &vg_prof_update );
+
+   vg.engine_stage = k_engine_stage_update;
+   vg_pre_update();
+
+   /* Fixed update loop */
+   vg.engine_stage = k_engine_stage_update_fixed;
+
+   vg.fixed_iterations = 0;
+   vg_lines.enabled = vg_lines.render;
+   vg.time_fixed_accumulator += vg.time_delta;
+
+   while( vg.time_fixed_accumulator >= vg.time_fixed_delta ){
+      vg_fixed_update();
+      vg_lines.enabled = 0;
+      vg.time_fixed_accumulator -= vg.time_fixed_delta;
+
+      vg.fixed_iterations ++;
+      if( vg.fixed_iterations == 8 ){
+         break;
+      }
+   }
+   vg_lines.enabled = vg_lines.render;
+   vg.time_fixed_extrapolate = vg.time_fixed_accumulator / vg.time_fixed_delta;
+
+   vg.engine_stage = k_engine_stage_update;
+   vg_post_update();
+   vg_profile_end( &vg_prof_update );
+}
+
+static void vg_settings_gui(void);
+static void _vg_gameloop_render(void)
+{
+   vg_profile_begin( &vg_prof_render );
+
+   /* render */
+   vg.engine_stage = k_engine_stage_rendering;
+   vg_render();
+
+   vg_profile_end( &vg_prof_render );
+
+   /* ui */
+   vg.engine_stage = k_engine_stage_ui;
+   {
+      ui_prerender();
+      if( vg_console.enabled ){ 
+         vg_ui.ignore_input_frames = 10;
+         vg_gui();
+         vg_ui.ignore_input_frames = 0;
+         vg_ui.wants_mouse = 1;
+         vg_console_draw();
+      }
+      else vg_gui();
+
+      if( vg.settings_open )
+         vg_settings_gui();
+
+      /* vg tools */
+#ifndef VG_NO_AUDIO
+      audio_debug_ui( vg.pv );
+#endif
+
+               /* profiling */
+      if( vg_profiler ){
+         int frame_target = vg.display_refresh_rate;
+         if( vg.fps_limit > 0 ) frame_target = vg.fps_limit;
+         vg_profile_drawn( 
+               (struct vg_profile *[]){
+                  &vg_prof_update,&vg_prof_render,&vg_prof_swap}, 3,
+               (1.0f/(float)frame_target)*1000.0f, 
+               (ui_rect){ 4, 4, 250, 0 }, 0, 0
+         );
+         char perf[256];
+         
+         snprintf( perf, 255, 
+               "x: %d y: %d\n"
+               "refresh: %d (%.1fms)\n"
+               "samples: %d\n"
+               "iterations: %d (acc: %.3fms%%)\n"
+               "time: real(%.2f) delta(%.2f) rate(%.2f)\n"
+               "      extrap(%.2f) frame(%.2f) spin( "PRINTF_U64" )\n",
+               vg.window_x, vg.window_y, 
+               frame_target, (1.0f/(float)frame_target)*1000.0f,
+               vg.samples, 
+               vg.fixed_iterations, 
+               (vg.time_fixed_accumulator/VG_TIMESTEP_FIXED)*100.0f,
+               vg.time_real, vg.time_delta, vg.time_rate,
+               vg.time_fixed_extrapolate, vg.time_frame_delta,
+               vg.time_spinning );
+
+         ui_text( (ui_rect){258,4,900,900},perf,1,0,k_ui_align_left);
+      }
+      ui_postrender();
+   }
+}
+
+static void vg_changevsync(void){
+   if( vg.vsync && (vg.vsync_feature != k_vsync_feature_error) ){
+      /* turn on vsync if not enabled */
+
+      enum vsync_feature requested = k_vsync_feature_enabled;
+      if( vg.vsync < 0 ) requested = k_vsync_feature_enabled_adaptive;
+
+      if( vg.vsync_feature != requested ){
+         vg_info( "Setting swap interval\n" );
+
+         int swap_interval = 1;
+         if( requested == k_vsync_feature_enabled_adaptive ) 
+            swap_interval = -1;
+
+         if( SDL_GL_SetSwapInterval( swap_interval ) == -1 ){
+            if( requested == k_vsync_feature_enabled ){
+               vg_error( "Vsync is not supported by your system\n" );
+               vg_warn( "You may be overriding it in your"
+                        " graphics control panel.\n" );
+            }
+            else{
+               vg_error( "Adaptive Vsync is not supported by your system\n" );
+            }
+
+            vg.vsync_feature = k_vsync_feature_error;
+            vg.vsync = 0;
+            /* TODO: Make popup to notify user that this happened */
+         }
+         else{
+            vg_success( "Vsync enabled (%d)\n", requested );
+            vg.vsync_feature = requested;
+         }
+      }
+   }
+   else {
+      if( vg.vsync_feature != k_vsync_feature_disabled ){
+         SDL_GL_SetSwapInterval( 0 );
+         vg.vsync_feature = k_vsync_feature_disabled;
+      }
+   }
+}
+
+static int vg_framefilter( double dt ){
+   if( vg.fps_limit < 24 ) vg.fps_limit = 24;
+   if( vg.fps_limit > 300 ) vg.fps_limit = 300;
+
+   double min_frametime = 1.0/(double)vg.fps_limit;
+   if( vg.time_frame_delta < min_frametime ){
+      /* TODO: we can use high res nanosleep on Linux here */
+      double sleep_ms = (min_frametime-vg.time_frame_delta) * 1000.0;
+      u32 ms = (u32)floor( sleep_ms );
+
+      if( ms ){
+         if( !vg_loader_availible() )
+            SDL_Delay(1);
+         else
+            SDL_Delay(ms);
+      }
+      else{
+         vg.time_spinning ++;
+      }
+
+      return 1;
+   }
+
+   return 0;
+}
+
+static int _vg_crashscreen(void)
+{
+#if 0
+   if( vg_getkey( SDLK_ESCAPE ) )
+      return 1;
+#endif
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   glEnable(GL_BLEND);
+   glDisable(GL_DEPTH_TEST);
+   glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA);
+   glBlendEquation(GL_FUNC_ADD);
+
+   glClearColor( 0.15f + sinf(vg.time_real)*0.1f, 0.0f, 0.0f,1.0f );
+   glClear( GL_COLOR_BUFFER_BIT );
+   glViewport( 0,0, vg.window_x, vg.window_y );
+
+#if 0
+   _vg_render_log();
+#endif
+
+   return 0;
+}
+
+static void _vg_gameloop(void){
+   //vg.time_fixed_accumulator = 0.75f * (1.0f/60.0f);
+
+   vg.time_hp = SDL_GetPerformanceCounter();
+   vg.time_hp_last = vg.time_hp;
+
+   int post_start = 0;
+   while(1){
+      vg.time_hp = SDL_GetPerformanceCounter();
+      u64 udt = vg.time_hp - vg.time_hp_last;
+      vg.time_hp_last = vg.time_hp;
+
+      double dt = (double)udt / (double)SDL_GetPerformanceFrequency();
+
+      vg.time_frame_delta += dt;
+      vg_run_async_checked();
+
+      if( vg_framefilter( dt ) )
+         continue;
+
+      vg_changevsync();
+
+      enum engine_status status = _vg_engine_status();
+      if( status == k_engine_status_running )
+         vg_profile_begin( &vg_prof_swap );
+
+      SDL_GL_SwapWindow( vg.window );
+
+      if( status == k_engine_status_running )
+         vg_profile_end( &vg_prof_swap );
+
+      vg.time_real += vg.time_frame_delta;
+      vg.time_delta = vg.time_frame_delta * vg.time_rate;
+      vg.time += vg.time_delta;
+
+      _vg_process_events();
+
+      if( vg.window_should_close )
+         break;
+         
+      if( status == k_engine_status_crashed ){
+         if( _vg_crashscreen() )
+            break;
+      }
+      else{
+         if( status == k_engine_status_running ){
+            _vg_gameloop_update();
+            _vg_gameloop_render();
+         }
+         else{
+            vg_loader_render();
+         }
+      }
+
+      if( vg.loader_ring > 0.01f ){
+         vg_loader_render_ring( vg.loader_ring );
+         vg.loader_ring -= vg.time_frame_delta * 0.5f;
+      }
+
+      vg.time_frame_delta = 0.0;
+      vg.time_spinning = 0;
+   }
+}
+
+static void _vg_process_launch_opts_internal( int argc, char *argv[] )
+{
+   char *arg;
+   while( vg_argp( argc, argv ) ){
+      if( (arg = vg_opt_arg( 'w' )) ){
+         vg.window_x = atoi( arg );
+      }
+
+      if( (arg = vg_opt_arg( 'h' )) ){
+         vg.window_y = atoi( arg );
+      }
+
+      if( (arg = vg_long_opt_arg( "samples" )) ){
+         vg.samples = VG_MAX( 0, VG_MIN( 8, atoi( arg ) ) );
+      }
+
+      if(  vg_long_opt( "use-libc-malloc" ) ){
+         vg_mem.use_libc_malloc = 1;
+      }
+
+      if( vg_long_opt( "high-performance" ) ){
+         vg.quality_profile = k_quality_profile_low;
+      }
+
+      vg_launch_opt();
+   }
+}
+
+static void _vg_init_window( const char *window_name )
+{
+   vg_info( "SDL_INIT\n" );
+
+   if( SDL_Init( SDL_INIT_VIDEO ) != 0  ){
+      vg_error( "SDL_Init failed: %s\n", SDL_GetError() );
+      exit(0);
+   }
+
+#ifndef VG_NO_AUDIO
+   SDL_InitSubSystem( SDL_INIT_AUDIO );
+#endif
+   SDL_InitSubSystem( SDL_INIT_GAMECONTROLLER );
+
+   char *exe_basepath = SDL_GetBasePath();
+   u32 len = vg_align8( strlen(exe_basepath)+1 );
+   char *dest  = vg_linear_alloc( vg_mem.rtmemory, len );
+   strcpy( dest, exe_basepath );
+   SDL_free( exe_basepath );
+   vg.base_path = dest;
+
+   vg_info( "Basepath: %s\n", vg.base_path );
+
+   SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );
+   SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 );
+   SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 3 );
+   SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, 
+                           SDL_GL_CONTEXT_PROFILE_CORE );
+
+   SDL_GL_SetAttribute( SDL_GL_CONTEXT_RELEASE_BEHAVIOR,
+                           SDL_GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH );
+   
+   SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 8 );
+   SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 8 );
+   SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 8 );
+   SDL_GL_SetAttribute( SDL_GL_ALPHA_SIZE, 8 );
+   SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 0 );
+
+   /* 
+    * Get monitor information 
+    */
+   vg_info( "Getting display count\n" );
+   int display_count = 0, 
+       display_index = 0, 
+       mode_index = 0;
+
+   SDL_DisplayMode video_mode;
+   if( SDL_GetDesktopDisplayMode( display_index, &video_mode ) ){
+      vg_error( "SDL_GetDesktopDisplayMode failed: %s\n", SDL_GetError() );
+      SDL_Quit();
+      exit(0);
+   }
+
+   vg.display_refresh_rate = video_mode.refresh_rate;
+   vg.window_x = video_mode.w;
+   vg.window_y = video_mode.h;
+
+   if( vg.screen_mode == 2 ){
+      vg.window_x = 1280;
+      vg.window_y = 720;
+   }
+
+#ifndef _WIN32
+       SDL_SetHint( "SDL_VIDEO_X11_XINERAMA", "1" );
+       SDL_SetHint( "SDL_VIDEO_X11_XRANDR", "0" );
+       SDL_SetHint( "SDL_VIDEO_X11_XVIDMODE", "0" );
+#endif
+
+   u32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_INPUT_GRABBED |
+               SDL_WINDOW_RESIZABLE;
+
+   if( vg.screen_mode == 1 )
+      flags |= SDL_WINDOW_FULLSCREEN;
+   else if( vg.screen_mode == 0 )
+      flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
+
+   vg_info( "CreateWindow( %d %d %u )\n", vg.window_x, vg.window_y, flags );
+
+   if((vg.window = SDL_CreateWindow( window_name, 0, 0, 
+                                     vg.window_x, vg.window_y, flags ))){
+      if( vg.screen_mode == 2 )
+         SDL_SetWindowPosition( vg.window, video_mode.w-vg.window_x, 0 );
+   }
+   else{
+      vg_error( "SDL_CreateWindow failed: %s", SDL_GetError() );
+      exit(0);
+   }
+
+   SDL_RaiseWindow( vg.window );
+   SDL_SetWindowMinimumSize( vg.window, 1280, 720 );
+   SDL_SetWindowMaximumSize( vg.window, 4096, 4096 );
+
+   vg_info( "CreateContext\n" );
+
+   /* ????? */
+   if( SDL_IsTextInputActive() ) SDL_StopTextInput();
+
+   /* 
+    * OpenGL loading 
+    */
+   if( (vg.gl_context = SDL_GL_CreateContext(vg.window) )){
+      SDL_GL_GetDrawableSize( vg.window, &vg.window_x, &vg.window_y );
+      vg_success( "Window created (%dx%d)\n", vg.window_x, vg.window_y );
+   }
+   else{
+      vg_error( "SDL_GL_CreateContext failed: %s\n", SDL_GetError() );
+      SDL_Quit();
+      exit(0);
+   }
+
+   if( !gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress) ) {
+      vg_error( "Glad Failed to initialize\n" );
+      SDL_GL_DeleteContext( vg.gl_context );
+      SDL_Quit();
+      exit(0);
+   }
+
+   const unsigned char* glver = glGetString( GL_VERSION );
+   vg_success( "Load setup complete, OpenGL version: %s\n", glver );
+
+   SDL_GL_SetSwapInterval(0); /* disable vsync while loading */
+
+   SDL_DisplayMode dispmode;
+   if( !SDL_GetWindowDisplayMode( vg.window, &dispmode ) ){
+      if( dispmode.refresh_rate ){
+         vg.display_refresh_rate = dispmode.refresh_rate;
+      }
+   }
+
+   if( vg.display_refresh_rate < 25 || vg.display_refresh_rate > 300 ){
+      vg.display_refresh_rate = 60;
+   }
+
+   vg_info( "Display refresh rate: %d\n", dispmode.refresh_rate );
+   if( !vg.fps_limit) vg.fps_limit = vg.display_refresh_rate;
+}
+
+static void _vg_terminate(void)
+{
+   /* Shutdown */
+   vg_console_write_persistent();
+
+   SDL_AtomicLock( &vg.sl_status );
+   vg.engine_status = k_engine_status_none;
+   SDL_AtomicUnlock( &vg.sl_status );
+
+   vg_loader_free();
+
+   vg_success( "If you see this it means everything went.. \"well\".....\n" );
+
+   SDL_GL_DeleteContext( vg.gl_context );
+   SDL_Quit();
+   exit(0);
+}
+
+static int cmd_vg_settings_toggle( int argc, const char *argv[] );
+void vg_enter( int argc, char *argv[], const char *window_name )
+{
+   vg_rand_seed( &vg.rand, 461 );
+   _vg_process_launch_opts_internal( argc, argv );
+
+   /* Systems init */
+   vg_alloc_quota();
+   vg_console_init();
+   
+   vg_console_reg_var( "vg_fps_limit", &vg.fps_limit, 
+                        k_var_dtype_i32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "vg_vsync", &vg.vsync, 
+                        k_var_dtype_i32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "vg_quality", &vg.quality_profile, 
+                        k_var_dtype_i32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "vg_screen_mode", &vg.screen_mode,
+                        k_var_dtype_i32, VG_VAR_PERSISTENT );
+   vg_audio_register();
+   vg_console_load_autos();
+
+   vg_console_reg_cmd( "vg_settings", cmd_vg_settings_toggle, NULL );
+   _vg_init_window( window_name );
+
+   vg_async_init();
+   SDL_SetRelativeMouseMode(1);
+
+   vg.thread_id_main = SDL_GetThreadID(NULL);
+   
+   /* Opengl-required systems */
+   vg_ui_init();
+   vg_loader_init();
+
+   vg.engine_status = k_engine_status_load_internal;
+
+   _vg_opengl_sync_init();
+    vg_loader_start( _vg_load_full, NULL );
+   _vg_gameloop();
+   _vg_terminate();
+}
+
+void vg_fatal_error( const char *fmt, ... )
+{
+   va_list args;
+   va_start( args, fmt );
+   _vg_logx_va( stderr, NULL, "fatal", KRED, fmt, args );
+   va_end( args );
+
+   vg_print_backtrace();
+
+   SDL_AtomicLock( &vg.sl_status );
+   vg.engine_status = k_engine_status_crashed;
+   SDL_AtomicUnlock( &vg.sl_status );
+
+   if( vg_thread_purpose() == k_thread_purpose_loader )
+   {
+      longjmp( vg.env_loader_exit, 1 );
+   }
+   else
+   {
+      vg_error( "There is no jump to the error runner thing yet! bai bai\n" );
+      _vg_terminate();
+   }
+}
+
+/* 
+ * settings menu
+ * ---------------------------------------------------------------------------
+ */
+
+#ifdef VG_GAME_SETTINGS
+extern void vg_game_settings_gui( ui_rect panel ) ;
+extern void vg_game_settings_init(void);
+#endif 
+
+struct ui_enum_opt vg_settings_vsync_enum[] = { 
+   { 0, "None" },
+   { 1, "On" },
+   {-1, "Adaptive" },
+};
+
+struct ui_enum_opt vg_settings_quality_enum[] = { 
+   { 0, "High Quality" },
+   { 1, "Faster" },
+   { 2, "Absolute Minimum" },
+};
+
+struct ui_enum_opt vg_settings_screen_mode_enum[] = {
+   { 0, "Fullscreen (desktop)" },
+   { 1, "Fullscreen (native)" },
+   { 2, "Floating Window" }
+};
+
+struct ui_enum_opt vg_settings_dsp_enum[] = {
+   { 1, "Enabled" },
+   { 0, "Disabled" },
+};
+
+struct {
+   struct vg_setting_ranged_i32 fps_limit;
+   struct vg_setting_enum vsync, quality, screenmode, audio_devices, dsp;
+   i32 temp_audio_choice;
+}
+static vg_settings = {
+   .fps_limit =   { .label = "Fps Limit",
+                     .min=24, .max=300, .actual_value = &vg.fps_limit },
+   .vsync =       { .label = "Vsync",
+                     .actual_value = &vg.vsync,
+                     .options = vg_settings_vsync_enum, .option_count = 3 },
+   .quality =     { .label = "Graphic Quality",
+                     .actual_value = &vg.quality_profile,
+                     .options = vg_settings_quality_enum, .option_count = 3 },
+   .screenmode =  { .label = "Type",
+                     .actual_value = &vg.screen_mode,
+                     .options = vg_settings_screen_mode_enum, .option_count=3 },
+   .audio_devices = { .label = "Audio Device",
+                      .actual_value = &vg_settings.temp_audio_choice,
+                      .options = NULL, .option_count = 0 },
+   .dsp = { .label = "Audio effects (reverb etc.)",
+             .actual_value = &vg_audio.dsp_enabled,
+             .options = vg_settings_dsp_enum, .option_count=2 },
+};
+
+static void vg_settings_ui_draw_diff( ui_rect orig ){
+   ui_rect l,r;
+   ui_split( orig, k_ui_axis_v, -32, 0, l, r );
+   ui_text( r, "*", 1, k_ui_align_middle_center, ui_colour(k_ui_blue) );
+}
+
+/* i32 settings 
+ * ------------------------------------------------------------------------- */
+
+static void vg_settings_ui_int( char *buf, u32 len ){
+   for( u32 i=0, j=0; i<len; i ++ ){
+      if( ((buf[i] >= '0') && (buf[i] <= '9')) || (buf[i] == '\0') )
+         buf[j ++] = buf[i];
+   }
+}
+
+struct ui_textbox_callbacks static vg_settings_ui_int_callbacks = {
+   .change = vg_settings_ui_int
+};
+
+static bool vg_settings_ranged_i32_valid( struct vg_setting_ranged_i32 *prop ){
+   if( prop->new_value < prop->min ) return 0;
+   if( prop->new_value > prop->max ) return 0;
+   return 1;
+}
+
+static bool vg_settings_ranged_i32_diff( struct vg_setting_ranged_i32 *prop ){
+   if( prop->new_value != *prop->actual_value ) return 1;
+   else return 0;
+}
+
+static bool vg_settings_ui_ranged_i32( struct vg_setting_ranged_i32 *prop, 
+                                       ui_rect rect ){
+   ui_rect orig;
+   rect_copy( rect, orig );
+
+   ui_textbox( rect, prop->label, prop->buf, sizeof(prop->buf), 
+               1, 0, &vg_settings_ui_int_callbacks );
+   prop->new_value = atoi( prop->buf );
+
+   if( vg_settings_ranged_i32_diff( prop ) )
+      vg_settings_ui_draw_diff( orig );
+
+   bool valid = vg_settings_ranged_i32_valid( prop );
+   if( !valid ){
+      ui_rect _null, line;
+      ui_split( orig, k_ui_axis_h, -1, 0, _null, line );
+      line[1] += 3;
+
+      ui_fill( line, ui_colour( k_ui_red ) );
+   }
+
+   return valid;
+}
+
+void ui_settings_ranged_i32_init( struct vg_setting_ranged_i32 *prop )
+{
+   vg_str tmp;
+   vg_strnull( &tmp, prop->buf, sizeof(prop->buf) );
+   vg_strcati32( &tmp, *prop->actual_value );
+   prop->new_value = *prop->actual_value;
+}
+
+/* enum settings
+ * ------------------------------------------------------------------------- */
+
+bool vg_settings_enum_diff( struct vg_setting_enum *prop )
+{
+   if( prop->new_value != *prop->actual_value ) return 1;
+   else return 0;
+}
+
+bool vg_settings_enum( struct vg_setting_enum *prop, ui_rect rect )
+{
+   ui_rect orig;
+   rect_copy( rect, orig );
+
+   ui_enum( rect, prop->label,
+            prop->options, prop->option_count, &prop->new_value );
+
+   if( vg_settings_enum_diff( prop ) )
+      vg_settings_ui_draw_diff( orig );
+   
+   return 1;
+}
+
+void ui_settings_enum_init( struct vg_setting_enum *prop )
+{
+   prop->new_value = *prop->actual_value;
+}
+
+/* .. */
+
+void vg_settings_ui_header( ui_rect inout_panel, const char *name )
+{
+   ui_rect rect;
+   ui_standard_widget( inout_panel, rect, 2 );
+   ui_text( rect, name, 1, k_ui_align_middle_center, ui_colour(k_ui_fg+3) );
+}
+
+
+bool vg_settings_apply_button( ui_rect inout_panel, bool validated )
+{
+   ui_rect last_row;
+   ui_px height = (vg_ui.font->glyph_height + 18) * k_ui_scale;
+   ui_split( inout_panel, k_ui_axis_h, -height, k_ui_padding, 
+             inout_panel, last_row );
+
+   const char *string = "Apply";
+   if( validated ){
+      if( ui_button( last_row, string ) == 1 )
+         return 1;
+   }
+   else{
+      ui_rect rect;
+      ui_standard_widget( last_row, rect, 1 );
+      ui_fill( rect, ui_colour( k_ui_bg+1 ) );
+      ui_outline( rect, -1, ui_colour( k_ui_red ), 0 );
+
+      ui_rect t = { 0,0, ui_text_line_width( string ), 14 };
+      ui_rect_center( rect, t );
+      ui_text( t, string, 1, k_ui_align_left, ui_colour(k_ui_fg+3) );
+   }
+
+   return 0;
+}
+
+static void vg_settings_video_apply(void){
+   if( vg_settings_enum_diff( &vg_settings.screenmode ) ){
+      vg.screen_mode = vg_settings.screenmode.new_value;
+
+      if( (vg.screen_mode == 0) || (vg.screen_mode == 1) ){
+         SDL_DisplayMode video_mode;
+         if( SDL_GetDesktopDisplayMode( 0, &video_mode ) ){
+            vg_error("SDL_GetDesktopDisplayMode failed: %s\n", SDL_GetError());
+         }
+         else {
+            //vg.display_refresh_rate = video_mode.refresh_rate;
+            vg.window_x = video_mode.w;
+            vg.window_y = video_mode.h;
+         }
+         SDL_SetWindowSize( vg.window, vg.window_x, vg.window_y );
+      }
+
+      if( vg.screen_mode == 0 )
+         SDL_SetWindowFullscreen( vg.window, SDL_WINDOW_FULLSCREEN_DESKTOP );
+      if( vg.screen_mode == 1 )
+         SDL_SetWindowFullscreen( vg.window, SDL_WINDOW_FULLSCREEN );
+      if( vg.screen_mode == 2 ){
+         SDL_SetWindowFullscreen( vg.window, 0 );
+         SDL_SetWindowSize( vg.window, 1280, 720 );
+         SDL_SetWindowPosition( vg.window, 16, 16 );
+         SDL_SetWindowMinimumSize( vg.window, 1280, 720 );
+         SDL_SetWindowMaximumSize( vg.window, 4096, 4096 );
+      }
+   }
+
+   vg.fps_limit = vg_settings.fps_limit.new_value;
+   vg.quality_profile = vg_settings.quality.new_value;
+   vg.vsync = vg_settings.vsync.new_value;
+}
+
+static void vg_settings_video_gui( ui_rect panel ){
+   bool validated = 1;
+   ui_rect rq;
+   ui_standard_widget( panel, rq, 1 );
+   vg_settings_enum( &vg_settings.quality, rq );
+
+   /* FIXME */
+#if 0
+   if( vg.vsync_feature == k_vsync_feature_error ){
+      ui_info( panel, "There was an error activating vsync feature." );
+   }
+#endif
+
+   /* frame timing */
+   vg_settings_ui_header( panel, "Frame Timing" );
+   ui_rect duo, d0,d1;
+   ui_standard_widget( panel, duo, 1 );
+   ui_split_ratio( duo, k_ui_axis_v, 0.5f, 16, d0, d1 );
+
+   vg_settings_enum( &vg_settings.vsync, d0 );
+   validated &= vg_settings_ui_ranged_i32( &vg_settings.fps_limit, d1 );
+
+   /* profiler */
+   ui_standard_widget( panel, duo, 10 );
+   int frame_target = vg.display_refresh_rate;
+   if( !vg.vsync ) frame_target = vg.fps_limit;
+   vg_profile_drawn( 
+         (struct vg_profile *[]){
+            &vg_prof_update,&vg_prof_render,&vg_prof_swap}, 3,
+         (1.0f/(f32)frame_target)*1500.0f, 
+         duo, 1, 0
+   );
+
+   ui_fill( (ui_rect){ duo[0], duo[1]+(duo[3]*2)/3, duo[2], 1 },
+             ui_colour(k_ui_fg) );
+
+   /* window spec */
+   vg_settings_ui_header( panel, "Window Specification" );
+
+   ui_standard_widget( panel, duo, 1 );
+   vg_settings_enum( &vg_settings.screenmode, duo );
+
+   if( vg_settings_apply_button( panel, validated ) )
+      vg_settings_video_apply();
+}
+
+static void vg_settings_audio_apply(void){
+   if( vg_settings_enum_diff( &vg_settings.audio_devices ) ){
+      if( vg_audio.sdl_output_device ){
+         vg_info( "Closing audio device %d\n", vg_audio.sdl_output_device );
+         SDL_CloseAudioDevice( vg_audio.sdl_output_device );
+      }
+      
+      vg_strfree( &vg_audio.device_choice );
+
+      if( vg_settings.audio_devices.new_value == -1 ){ }
+      else if( vg_settings.audio_devices.new_value == -2 ){
+         vg_fatal_error( "Programming error\n" );
+      }
+      else {
+         struct ui_enum_opt *selected = NULL, *oi;
+
+         for( int i=0; i<vg_settings.audio_devices.option_count; i ++ ){
+            oi = &vg_settings.audio_devices.options[i];
+
+            if( oi->value == vg_settings.audio_devices.new_value ){
+               selected = oi;
+               break;
+            }
+         }
+
+         vg_strnull( &vg_audio.device_choice, NULL, -1 );
+         vg_strcat( &vg_audio.device_choice, oi->alias );
+      }
+
+      vg_audio_device_init();
+      *vg_settings.audio_devices.actual_value = 
+         vg_settings.audio_devices.new_value;
+   }
+
+   audio_lock();
+   if( vg_settings_enum_diff( &vg_settings.dsp ) ){
+      *vg_settings.dsp.actual_value =
+         vg_settings.dsp.new_value;
+   }
+
+   audio_unlock();
+}
+
+static void vg_settings_audio_gui( ui_rect panel ){
+   ui_rect rq;
+   ui_standard_widget( panel, rq, 1 );
+   vg_settings_enum( &vg_settings.audio_devices, rq );
+   
+   ui_standard_widget( panel, rq, 1 );
+   vg_settings_enum( &vg_settings.dsp, rq );
+
+   if( vg_settings_apply_button( panel, 1 ) )
+      vg_settings_audio_apply();
+}
+
+void vg_settings_open(void)
+{
+   vg.settings_open = 1;
+
+   ui_settings_ranged_i32_init( &vg_settings.fps_limit );
+   ui_settings_enum_init( &vg_settings.vsync );
+   ui_settings_enum_init( &vg_settings.quality );
+   ui_settings_enum_init( &vg_settings.screenmode );
+   
+   /* Create audio options */
+   int count = SDL_GetNumAudioDevices( 0 );
+
+   struct ui_enum_opt *options = malloc( sizeof(struct ui_enum_opt)*(count+1) );
+   vg_settings.audio_devices.options = options;
+   vg_settings.audio_devices.option_count = count+1;
+
+   struct ui_enum_opt *o0 = &options[0];
+   o0->alias = "OS Default";
+   o0->value = -1;
+
+   for( int i=0; i<count; i ++ ){
+      struct ui_enum_opt *oi = &options[i+1];
+
+      const char *device_name = SDL_GetAudioDeviceName( i, 0 );
+      int len = strlen(device_name);
+
+      oi->alias = malloc( len+1 );
+      memcpy( (void *)oi->alias, device_name, len+1 );
+      oi->value = i;
+   }
+
+   if( vg_audio.device_choice.buffer ){
+      vg_settings.temp_audio_choice = -2;
+
+      for( int i=0; i<count; i ++ ){
+         struct ui_enum_opt *oi = &options[i+1];
+         if( !strcmp( oi->alias, vg_audio.device_choice.buffer ) ){
+            vg_settings.temp_audio_choice = oi->value;
+            break;
+         }
+      }
+   }
+   else {
+      vg_settings.temp_audio_choice = -1;
+   }
+
+   ui_settings_enum_init( &vg_settings.audio_devices );
+   ui_settings_enum_init( &vg_settings.dsp );
+
+#ifdef VG_GAME_SETTINGS
+   vg_game_settings_init();
+#endif
+}
+
+void vg_settings_close(void)
+{
+   vg.settings_open = 0;
+
+   struct ui_enum_opt *options = vg_settings.audio_devices.options;
+   for( int i=1; i < vg_settings.audio_devices.option_count; i ++ )
+      free( (void *)options[i].alias );
+   free( vg_settings.audio_devices.options );
+}
+
+static void vg_settings_gui(void)
+{
+   ui_rect null;
+   ui_rect screen = { 0, 0, vg.window_x, vg.window_y };
+   ui_rect window = { 0, 0, 1000, 700 };
+   ui_rect_center( screen, window );
+   vg_ui.wants_mouse = 1;
+
+   ui_fill( window, ui_colour( k_ui_bg+1 ) );
+   ui_outline( window, 1, ui_colour( k_ui_bg+7 ), 0 );
+
+   ui_rect title, panel;
+   ui_split( window, k_ui_axis_h, 28, 0, title, panel );
+   ui_fill( title, ui_colour( k_ui_bg+7 ) );
+   ui_text( title, "Settings", 1, k_ui_align_middle_center, 
+            ui_colourcont(k_ui_bg+7) );
+
+   ui_rect quit_button;
+   ui_split( title, k_ui_axis_v, title[2]-title[3], 2, title, quit_button );
+
+   if( ui_button_text( quit_button, "X", 1 ) == 1 ){
+      vg_settings_close();
+      return;
+   }
+
+   ui_rect_pad( panel, (ui_px[2]){ 8, 8 } );
+   
+   const char *opts[] = { "video", "audio",
+#ifdef VG_GAME_SETTINGS
+   "game"
+#endif
+   };
+
+   static i32 page = 0;
+   ui_tabs( panel, panel, opts, vg_list_size(opts), &page );
+
+   if( page == 0 ){
+      vg_settings_video_gui( panel );
+   }
+   else if( page == 1 )
+      vg_settings_audio_gui( panel );
+
+#ifdef VG_GAME_SETTINGS
+   else if( page == 2 )
+      vg_game_settings_gui( panel );
+#endif
+}
+
+static int cmd_vg_settings_toggle( int argc, const char *argv[] ){
+   vg_settings_open();
+   return 0;
+}
+
+/*
+ * Graphic cards will check these to force it to use the GPU.
+ *   TODO: explicit declexport. since -flto strips these symbols in release.
+ */
+u32 NvOptimusEnablement = 0x00000001;
+int AmdPowerXpressRequestHighPerformance = 1;
+
+#include "vg_async.c"
+#include "vg_audio.c"
+#include "vg_audio_dsp.c"
+#include "vg_audio_synth_bird.c"
+#include "vg_binstr.c"
+#include "vg_bvh.c"
+#include "vg_camera.c"
+#include "vg_lines.c"
+#include "vg_console.c"
+#include "vg_imgui.c"
+#include "vg_input.c"
+#include "vg_io.c"
+#include "vg_loader.c"
+#include "vg_log.c"
+#include "vg_tex.c"
+#include "vg_mem.c"
+#include "vg_mem_pool.c"
+#include "vg_mem_queue.c"
+#include "vg_msg.c"
+#include "vg_opt.c"
+#include "vg_perlin.c"
+#include "vg_string.c"
+#include "vg_profiler.c"
+#include "vg_rigidbody_collision.c"
+#include "vg_rigidbody_constraints.c"
+#include "vg_rigidbody.c"
+#include "vg_rigidbody_view.c"
+#include "vg_shader.c"