Index: trunk/src/formats/md3_loader.cc
===================================================================
--- trunk/src/formats/md3_loader.cc	(revision 351)
+++ trunk/src/formats/md3_loader.cc	(revision 352)
@@ -204,6 +204,9 @@
 	source.read( md3->frames, sizeof( md3_frame_t ), static_cast<size_t>( md3->header.num_frames ) );
 
-	source.seek( md3->header.ofs_tags, origin::SET );
-	source.read( md3->tags, sizeof( md3_tag_t ), static_cast<size_t>( md3->header.num_tags * md3->header.num_frames ) );
+	if ( md3->header.num_tags > 0 )
+	{
+		source.seek( md3->header.ofs_tags, origin::SET );
+		source.read( md3->tags, sizeof( md3_tag_t ), static_cast<size_t>( md3->header.num_tags * md3->header.num_frames ) );
+	}
 
 	source.seek( md3->header.ofs_surfaces, origin::SET );
Index: trunk/src/rocket/rocket_interface.cc
===================================================================
--- trunk/src/rocket/rocket_interface.cc	(revision 352)
+++ trunk/src/rocket/rocket_interface.cc	(revision 352)
@@ -0,0 +1,499 @@
+#include "nv/rocket/rocket_interface.hh"
+
+#include <nv/interface/context.hh>
+#include <nv/gl/gl_device.hh>
+#include <nv/core/time.hh>
+#include <Rocket/Core.h>
+#include <Rocket/Debugger/Debugger.h>
+
+
+class rocket_c_file_interface : public Rocket::Core::FileInterface
+{
+public:
+	rocket_c_file_interface( const Rocket::Core::String& root ) : root( root ) {}
+	virtual ~rocket_c_file_interface() {}
+	virtual Rocket::Core::FileHandle Open( const Rocket::Core::String& path );
+	virtual void Close( Rocket::Core::FileHandle file );
+	virtual size_t Read( void* buffer, size_t size, Rocket::Core::FileHandle file );
+	virtual bool Seek( Rocket::Core::FileHandle file, long offset, int origin );
+	virtual size_t Tell( Rocket::Core::FileHandle file );
+private:
+	Rocket::Core::String root;
+};
+
+class rocket_sdl2_system_interface : public Rocket::Core::SystemInterface
+{
+public:
+	float GetElapsedTime();
+	bool LogMessage( Rocket::Core::Log::Type type, const Rocket::Core::String& message );
+};
+
+class rocket_sdl2_render_interface : public Rocket::Core::RenderInterface
+{
+public:
+	rocket_sdl2_render_interface( nv::context* context );
+
+	/// Called by Rocket when it wants to render geometry that it does not wish to optimise.
+	virtual void RenderGeometry( Rocket::Core::Vertex* vertices, int num_vertices, int* indices, int num_indices, Rocket::Core::TextureHandle texture, const Rocket::Core::Vector2f& translation );
+	/// Called by Rocket when it wants to enable or disable scissoring to clip content.
+	virtual void EnableScissorRegion( bool enable );
+	/// Called by Rocket when it wants to change the scissor region.
+	virtual void SetScissorRegion( int x, int y, int width, int height );
+
+	/// Called by Rocket when a texture is required by the library.
+	virtual bool LoadTexture( Rocket::Core::TextureHandle& texture_handle, Rocket::Core::Vector2i& texture_dimensions, const Rocket::Core::String& source );
+	/// Called by Rocket when a texture is required to be built from an internally-generated sequence of pixels.
+	virtual bool GenerateTexture( Rocket::Core::TextureHandle& texture_handle, const Rocket::Core::byte* source, const Rocket::Core::Vector2i& source_dimensions );
+	/// Called by Rocket when a loaded texture is no longer required.
+	virtual void ReleaseTexture( Rocket::Core::TextureHandle texture_handle );
+private:
+	nv::device*      m_device;
+	nv::context*     m_context;
+	nv::program      m_program;
+	nv::scene_state  m_state;
+	nv::render_state m_rstate;
+};
+
+
+static const char *rocket_vertex_shader = R"(
+#version 120
+attribute vec2 nv_position;
+attribute vec2 nv_texcoord;
+attribute vec4 nv_color;
+varying vec4 v_color;
+varying vec2 v_texcoord;
+uniform mat4 nv_m_projection;
+uniform mat4 nv_m_model;
+uniform vec2 texsize;
+void main(void)
+{
+	gl_Position = nv_m_projection * nv_m_model * vec4(nv_position.x, nv_position.y, 0.0, 1.0);
+	v_texcoord  = nv_texcoord; // / texsize;
+	v_color     = nv_color / 256;
+}
+)";
+
+static const char *rocket_fragment_shader = R"(
+#version 120
+varying vec4 v_color;
+varying vec2 v_texcoord;
+uniform sampler2D nv_t_diffuse;
+void main(void)
+{
+	vec4 tex_color = texture2D( nv_t_diffuse, v_texcoord );
+
+	gl_FragColor   = tex_color * v_color;
+}
+)";
+
+Rocket::Core::FileHandle rocket_c_file_interface::Open( const Rocket::Core::String& path )
+{
+	// Attempt to open the file relative to the application's root.
+	FILE* fp = fopen( ( root + path ).CString(), "rb" );
+	if ( fp != NULL )
+		return ( Rocket::Core::FileHandle ) fp;
+
+	// Attempt to open the file relative to the current working directory.
+	fp = fopen( path.CString(), "rb" );
+	return ( Rocket::Core::FileHandle ) fp;
+}
+
+void rocket_c_file_interface::Close( Rocket::Core::FileHandle file )
+{
+	fclose( (FILE*)file );
+}
+
+size_t rocket_c_file_interface::Read( void* buffer, size_t size, Rocket::Core::FileHandle file )
+{
+	return fread( buffer, 1, size, (FILE*)file );
+}
+
+bool rocket_c_file_interface::Seek( Rocket::Core::FileHandle file, long offset, int origin )
+{
+	return fseek( (FILE*)file, offset, origin ) == 0;
+}
+
+size_t rocket_c_file_interface::Tell( Rocket::Core::FileHandle file )
+{
+	return ftell( (FILE*)file );
+}
+
+float rocket_sdl2_system_interface::GetElapsedTime()
+{
+	return nv::get_ticks() / 1000;
+}
+
+bool rocket_sdl2_system_interface::LogMessage( Rocket::Core::Log::Type type, const Rocket::Core::String& message )
+{
+	switch ( type )
+	{
+	case Rocket::Core::Log::LT_ALWAYS: NV_LOG( nv::LOG_NOTICE, message.CString() ); break;
+	case Rocket::Core::Log::LT_ERROR: NV_LOG( nv::LOG_ERROR, message.CString() ); break;
+	case Rocket::Core::Log::LT_ASSERT: NV_LOG( nv::LOG_CRITICAL, message.CString() ); break;
+	case Rocket::Core::Log::LT_WARNING: NV_LOG( nv::LOG_WARNING, message.CString() ); break;
+	case Rocket::Core::Log::LT_INFO: NV_LOG( nv::LOG_INFO, message.CString() ); break;
+	case Rocket::Core::Log::LT_DEBUG: NV_LOG( nv::LOG_DEBUG, message.CString() ); break;
+	case Rocket::Core::Log::LT_MAX: break;
+	};
+
+	return true;
+}
+
+struct rocket_vertex
+{
+	nv::vec2   position;
+	nv::u8vec4 color;
+	nv::vec2   texcoord;
+};
+
+
+union texture_handle_convert
+{
+	Rocket::Core::TextureHandle th;
+	struct
+	{
+		nv::uint16 index;
+		nv::uint16 counter;
+	};
+};
+
+rocket_sdl2_render_interface::rocket_sdl2_render_interface( nv::context* context )
+{
+	m_context = context;
+	m_device = m_context->get_device();
+	m_rstate.depth_test.enabled = false;
+	m_rstate.culling.enabled = false;
+	m_rstate.blending.enabled = true;
+	m_rstate.blending.src_rgb_factor = nv::blending::SRC_ALPHA;
+	m_rstate.blending.dst_rgb_factor = nv::blending::ONE_MINUS_SRC_ALPHA;
+	m_rstate.blending.src_alpha_factor = nv::blending::SRC_ALPHA;
+	m_rstate.blending.dst_alpha_factor = nv::blending::ONE_MINUS_SRC_ALPHA;
+
+	m_program = m_device->create_program( rocket_vertex_shader, rocket_fragment_shader );
+
+	m_state.get_camera().set_ortho( 0.0f, float( 1024 ), float( 728 ), 0.0f );
+}
+
+void rocket_sdl2_render_interface::EnableScissorRegion( bool enable )
+{
+	m_rstate.scissor_test.enabled = enable;
+}
+
+void rocket_sdl2_render_interface::SetScissorRegion( int x, int y, int width, int height )
+{
+	m_rstate.scissor_test.pos.x = x;
+	m_rstate.scissor_test.pos.y = y;
+	m_rstate.scissor_test.dim.x = width;
+	m_rstate.scissor_test.pos.y = height;
+}
+
+bool rocket_sdl2_render_interface::LoadTexture( Rocket::Core::TextureHandle& texture_handle, Rocket::Core::Vector2i& texture_dimensions, const Rocket::Core::String& source )
+{
+	Rocket::Core::FileInterface* file_interface = Rocket::Core::GetFileInterface();
+	Rocket::Core::FileHandle file_handle = file_interface->Open( source );
+	if ( !file_handle )
+		return false;
+
+	file_interface->Seek( file_handle, 0, SEEK_END );
+	size_t buffer_size = file_interface->Tell( file_handle );
+	file_interface->Seek( file_handle, 0, SEEK_SET );
+
+	nv::uint8* buffer = new nv::uint8[buffer_size];
+	file_interface->Read( buffer, buffer_size, file_handle );
+	file_interface->Close( file_handle );
+
+	Rocket::Core::String ext = source.Substring( source.Length() - 3, 3 ).ToLower();
+
+	nv::sampler sampler( nv::sampler::LINEAR, nv::sampler::REPEAT );
+	nv::image_data* data = m_device->create_image_data( buffer, buffer_size );
+	nv::image_format iformat( nv::RGBA, nv::UBYTE );
+	if ( ext == "tga" ) iformat.format = nv::BGRA;
+
+	nv::texture tex = m_device->create_texture( data->get_size(), iformat, sampler, (void*)data->get_data() );
+	texture_dimensions.x = data->get_size().x;
+	texture_dimensions.y = data->get_size().y;
+	delete data;
+
+	typedef nv::handle_operator< nv::texture > op;
+	texture_handle_convert thc;
+	thc.index = op::get_index( tex );
+	thc.counter = op::get_counter( tex );
+	texture_handle = thc.th;
+	NV_LOG( nv::LOG_DEBUG, "load " << source.CString() << " as " << thc.index << "," << thc.counter );
+	return true;
+}
+
+bool rocket_sdl2_render_interface::GenerateTexture( Rocket::Core::TextureHandle& texture_handle, const Rocket::Core::byte* source, const Rocket::Core::Vector2i& source_dimensions )
+{
+	nv::sampler sampler( nv::sampler::LINEAR, nv::sampler::REPEAT );
+	nv::texture tex = m_device->create_texture( nv::ivec2( source_dimensions.x, source_dimensions.y ), nv::image_format( nv::RGBA, nv::UBYTE ), sampler, (void*)source );
+	typedef nv::handle_operator< nv::texture > op;
+	texture_handle_convert thc;
+	thc.index = op::get_index( tex );
+	thc.counter = op::get_counter( tex );
+	texture_handle = thc.th;
+	return true;
+}
+
+void rocket_sdl2_render_interface::ReleaseTexture( Rocket::Core::TextureHandle texture_handle )
+{
+	texture_handle_convert thc;
+	thc.th = texture_handle;
+	typedef nv::handle_operator< nv::texture > op;
+	m_device->release( op::create( thc.index, thc.counter ) );
+}
+
+
+void rocket_sdl2_render_interface::RenderGeometry( Rocket::Core::Vertex* vertices, int num_vertices, int* indices, int num_indices, Rocket::Core::TextureHandle texture, const Rocket::Core::Vector2f& translation )
+{
+	const nv::texture_info* info = nullptr;
+	if ( texture != 0 )
+	{
+		texture_handle_convert thc;
+		thc.th = texture;
+		typedef nv::handle_operator< nv::texture > op;
+		nv::texture tex( op::create( thc.index, thc.counter ) );
+		m_context->bind( tex, nv::TEX_DIFFUSE );
+		info = m_device->get_texture_info( tex );
+	}
+	else
+	{
+		m_context->bind( nv::texture(), nv::TEX_DIFFUSE );
+		return;
+	}
+
+	rocket_vertex* rv = (rocket_vertex*)vertices;
+
+	nv::vertex_array m_va = m_context->create_vertex_array( rv, num_vertices, (unsigned*)indices, num_indices, nv::STATIC_DRAW );
+	//if ( info )	m_device->set_uniform( m_program, "texsize", nv::vec2( info->size ) );
+	m_state.set_model( glm::translate( glm::mat4(), nv::vec3( translation.x, translation.y, 0.0f ) ) );
+	m_context->draw( nv::TRIANGLES, m_rstate, m_state, m_program, m_va, num_indices );
+	m_context->release( m_va );
+
+}
+
+/*
+int RocketSDL2SystemInterface::TranslateMouseButton( Uint8 button )
+{
+	switch ( button )
+	{
+	case SDL_BUTTON_LEFT:
+		return 0;
+	case SDL_BUTTON_RIGHT:
+		return 1;
+	case SDL_BUTTON_MIDDLE:
+		return 2;
+	default:
+		return 3;
+	}
+}
+
+int RocketSDL2SystemInterface::GetKeyModifiers()
+{
+	SDL_Keymod sdlMods = SDL_GetModState();
+
+	int retval = 0;
+
+	if ( sdlMods & KMOD_CTRL )
+		retval |= Rocket::Core::Input::KM_CTRL;
+
+	if ( sdlMods & KMOD_SHIFT )
+		retval |= Rocket::Core::Input::KM_SHIFT;
+
+	if ( sdlMods & KMOD_ALT )
+		retval |= Rocket::Core::Input::KM_ALT;
+
+	return retval;
+}
+
+*/
+
+int rocket_get_key_modifiers()
+{
+	return 0;
+}
+
+int rocket_get_mouse_buttons( const nv::mouse_button_event& button )
+{
+	return 0;
+}
+
+Rocket::Core::Input::KeyIdentifier rocket_translate_key( const nv::key_event& key )
+{
+	using namespace Rocket::Core::Input;
+
+	switch ( key.code )
+	{
+	case nv::KEY_NONE  : return KI_UNKNOWN;
+	case nv::KEY_SPACE : return KI_SPACE;
+	case nv::KEY_0: return KI_0;
+	case nv::KEY_1: return KI_1;
+	case nv::KEY_2: return KI_2;
+	case nv::KEY_3: return KI_3;
+	case nv::KEY_4: return KI_4;
+	case nv::KEY_5: return KI_5;
+	case nv::KEY_6: return KI_6;
+	case nv::KEY_7: return KI_7;
+	case nv::KEY_8: return KI_8;
+	case nv::KEY_9: return KI_9;
+	case nv::KEY_A: return KI_A;
+	case nv::KEY_B: return KI_B;
+	case nv::KEY_C: return KI_C;
+	case nv::KEY_D: return KI_D;
+	case nv::KEY_E: return KI_E;
+	case nv::KEY_F: return KI_F;
+	case nv::KEY_G: return KI_G;
+	case nv::KEY_H: return KI_H;
+	case nv::KEY_I: return KI_I;
+	case nv::KEY_J: return KI_J;
+	case nv::KEY_K: return KI_K;
+	case nv::KEY_L: return KI_L;
+	case nv::KEY_M: return KI_M;
+	case nv::KEY_N: return KI_N;
+	case nv::KEY_O: return KI_O;
+	case nv::KEY_P: return KI_P;
+	case nv::KEY_Q: return KI_Q;
+	case nv::KEY_R: return KI_R;
+	case nv::KEY_S: return KI_S;
+	case nv::KEY_T: return KI_T;
+	case nv::KEY_U: return KI_U;
+	case nv::KEY_V: return KI_V;
+	case nv::KEY_W: return KI_W;
+	case nv::KEY_X: return KI_X;
+	case nv::KEY_Y: return KI_Y;
+	case nv::KEY_Z: return KI_Z;
+	case nv::KEY_SCOLON:return KI_OEM_1;
+//	case nv::KEY_PLUS:return KI_OEM_PLUS;
+	case nv::KEY_COMMA:return KI_OEM_COMMA;
+	case nv::KEY_MINUS:return KI_OEM_MINUS;
+	case nv::KEY_PERIOD:return KI_OEM_PERIOD;
+	case nv::KEY_SLASH:return KI_OEM_2;
+	case nv::KEY_BQUOTE:return KI_OEM_3;
+	case nv::KEY_LBRACKET:return KI_OEM_4;
+	case nv::KEY_BSLASH:return KI_OEM_5;
+	case nv::KEY_RBRACKET:return KI_OEM_6;
+//	case nv::KEY_QUOTEDBL:return KI_OEM_7;
+// 	case nv::KEY_KP_0:return KI_NUMPAD0;
+// 	case nv::KEY_KP_1:return KI_NUMPAD1;
+// 	case nv::KEY_KP_2:return KI_NUMPAD2;
+// 	case nv::KEY_KP_3:return KI_NUMPAD3;
+// 	case nv::KEY_KP_4:return KI_NUMPAD4;
+// 	case nv::KEY_KP_5:return KI_NUMPAD5;
+// 	case nv::KEY_KP_6:return KI_NUMPAD6;
+// 	case nv::KEY_KP_7:return KI_NUMPAD7;
+// 	case nv::KEY_KP_8:return KI_NUMPAD8;
+// 	case nv::KEY_KP_9:return KI_NUMPAD9;
+// 	case nv::KEY_KP_ENTER:return KI_NUMPADENTER;
+// 	case nv::KEY_KP_MULTIPLY:return KI_MULTIPLY;
+// 	case nv::KEY_KP_PLUS:return KI_ADD;
+//	case nv::KEY_KP_MINUS: return KI_SUBTRACT;
+//	case nv::KEY_KP_PERIOD: return KI_DECIMAL;
+//	case nv::KEY_KP_DIVIDE: return KI_DIVIDE;
+//	case nv::KEY_KP_EQUALS: return KI_OEM_NEC_EQUAL;
+	case nv::KEY_BACK    : return KI_BACK;
+	case nv::KEY_TAB     : return KI_TAB;
+//	case nv::KEY_CLEAR   : return KI_CLEAR;
+	case nv::KEY_ENTER   : return KI_RETURN;
+//	case nv::KEY_PAUSE   : return KI_PAUSE;
+//	case nv::KEY_CAPSLOCK: return KI_CAPITAL;
+	case nv::KEY_PGUP  : return KI_PRIOR;
+	case nv::KEY_PGDOWN: return KI_NEXT;
+	case nv::KEY_END   : return KI_END;
+	case nv::KEY_HOME  : return KI_HOME;
+	case nv::KEY_LEFT  : return KI_LEFT;
+	case nv::KEY_UP    : return KI_UP;
+	case nv::KEY_RIGHT : return KI_RIGHT;
+	case nv::KEY_DOWN  : return KI_DOWN;
+	case nv::KEY_INSERT: return KI_INSERT;
+	case nv::KEY_DELETE: return KI_DELETE;
+	case nv::KEY_F1    : return KI_F1;
+	case nv::KEY_F2    : return KI_F2;
+	case nv::KEY_F3    : return KI_F3;
+	case nv::KEY_F4    : return KI_F4;
+	case nv::KEY_F5    : return KI_F5;
+	case nv::KEY_F6    : return KI_F6;
+	case nv::KEY_F7    : return KI_F7;
+	case nv::KEY_F8    : return KI_F8;
+	case nv::KEY_F9    : return KI_F9;
+	case nv::KEY_F10   : return KI_F10;
+	case nv::KEY_F11   : return KI_F11;
+	case nv::KEY_F12   : return KI_F12;
+// 	case nv::KEY_F13 : return KI_F13;
+// 	case nv::KEY_F14 : return KI_F14;
+// 	case nv::KEY_F15 : return KI_F15;
+// 	case nv::KEY_NUMLOCKCLEAR : return KI_NUMLOCK;
+// 	case nv::KEY_SCROLLLOCK : return KI_SCROLL;
+// 	case nv::KEY_LSHIFT  : return KI_LSHIFT;
+// 	case nv::KEY_RSHIFT  : return KI_RSHIFT;
+// 	case nv::KEY_LCTRL   : return KI_LCONTROL;
+// 	case nv::KEY_RCTRL   : return KI_RCONTROL;
+// 	case nv::KEY_LALT    : return KI_LMENU;
+// 	case nv::KEY_RALT    : return KI_RMENU;
+// 	case nv::KEY_LGUI    : return KI_LMETA;
+// 	case nv::KEY_RGUI    : return KI_RMETA;
+// 	case nv::KEY_LSUPER  : return KI_LWIN;
+//  case nv::KEY_RSUPER  : return KI_RWIN;
+	default: return KI_UNKNOWN;
+	}
+
+	return KI_UNKNOWN;
+}
+
+static rocket_sdl2_render_interface* s_render_interface = nullptr;
+static rocket_sdl2_system_interface* s_system_interface = nullptr;
+static rocket_c_file_interface*      s_file_interface   = nullptr;
+
+bool nv::rocket_event( void* context, nv::io_event& event )
+{
+	Rocket::Core::Context *Context = ( Rocket::Core::Context * )context;
+	switch ( event.type )
+	{
+	case nv::EV_MOUSE_MOVE :
+		Context->ProcessMouseMove( event.mmove.x, event.mmove.y, rocket_get_key_modifiers() );
+		break;
+	case nv::EV_MOUSE_BUTTON :
+		if ( event.mbutton.pressed )
+			Context->ProcessMouseButtonDown( rocket_get_mouse_buttons( event.mbutton ), rocket_get_key_modifiers() );
+		else
+			Context->ProcessMouseButtonUp( rocket_get_mouse_buttons( event.mbutton ), rocket_get_key_modifiers() );
+		break;
+	case nv::EV_MOUSE_WHEEL:
+		return Context->ProcessMouseWheel( event.mwheel.y, rocket_get_key_modifiers() );
+	case nv::EV_KEY:
+		if (event.key.pressed)
+		{
+			if ( event.key.code == KEY_BQUOTE )
+			{
+				Rocket::Debugger::SetVisible( !Rocket::Debugger::IsVisible() );
+				return true;
+			}
+
+			return Context->ProcessKeyDown( rocket_translate_key( event.key ) , rocket_get_key_modifiers() );
+		}
+		break;
+	default:
+		break;
+	}
+	return false;
+}
+
+bool nv::rocket_initialize( nv::window* w, const char* assets_root )
+{
+	s_render_interface = new rocket_sdl2_render_interface( w->get_context() );
+	s_system_interface = new rocket_sdl2_system_interface();
+	s_file_interface   = new rocket_c_file_interface( assets_root );
+
+	Rocket::Core::SetFileInterface( s_file_interface );
+	Rocket::Core::SetRenderInterface( s_render_interface );
+	Rocket::Core::SetSystemInterface( s_system_interface );
+
+	return Rocket::Core::Initialise();
+}
+
+void nv::rocket_shutdown()
+{
+	Rocket::Core::Shutdown();
+	delete s_render_interface;
+	delete s_system_interface;
+	delete s_file_interface;
+}
Index: trunk/src/wx/wx_canvas.cc
===================================================================
--- trunk/src/wx/wx_canvas.cc	(revision 352)
+++ trunk/src/wx/wx_canvas.cc	(revision 352)
@@ -0,0 +1,90 @@
+// Copyright (C) 2014 ChaosForge Ltd 
+// http://chaosforge.org/
+//
+// This file is part of NV Libraries.
+// For conditions of distribution and use, see copyright notice in nv.hh
+
+#include <nv/wx/wx_canvas.hh>
+
+wxBEGIN_EVENT_TABLE( nv::wx_gl_canvas, wxWindow )
+EVT_PAINT( nv::wx_gl_canvas::on_paint )
+wxEND_EVENT_TABLE()
+
+nv::wx_gl_canvas::wx_gl_canvas( wxWindow *parent )
+	: wxWindow( parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
+	wxFULL_REPAINT_ON_RESIZE )
+{
+	nv::load_gl_no_context();
+	wxClientDC dc( this );
+	m_wm = new nv::sdl::window_manager;
+	m_device = new nv::gl_device();
+	m_window = new nv::gl_window( m_device, m_wm, new nv::sdl::input(), GetHWND(), dc.GetHDC() );
+	m_context = m_window->get_context();
+}
+
+nv::wx_gl_canvas::~wx_gl_canvas()
+{
+	delete m_window;
+	delete m_device;
+}
+
+void nv::wx_gl_canvas::on_paint( wxPaintEvent& )
+{
+	const wxSize client_size = GetClientSize();
+	m_context->set_viewport( nv::ivec4( nv::ivec2(), client_size.x, client_size.y ) );
+	on_update();
+}
+
+void nv::wx_gl_canvas::stop()
+{
+	Disconnect( wxEVT_IDLE, wxIdleEventHandler( wx_gl_canvas::on_idle ) );
+	//m_update_timer->Stop();
+}
+
+void nv::wx_gl_canvas::start()
+{
+	Connect( wxID_ANY, wxEVT_IDLE, wxIdleEventHandler( wx_gl_canvas::on_idle ) );
+	//m_update_timer->Start(20);
+}
+
+void nv::wx_gl_canvas::on_idle( wxIdleEvent& evt )
+{
+	if ( m_render )
+	{
+		on_update();
+		evt.RequestMore(); // render continuously, not only once on idle
+	}
+}
+
+nv::wx_log_text_ctrl_sink::wx_log_text_ctrl_sink( wxTextCtrl* a_text_ctrl ) : m_text_ctrl( a_text_ctrl )
+{
+
+}
+
+void nv::wx_log_text_ctrl_sink::log( nv::log_level level, const std::string& message )
+{
+	wxString str;
+	str << timestamp() << " [" << padded_level_name( level ) << "] " << message << "\n";
+	m_text_ctrl->AppendText( str );
+}
+
+nv::wx_app_base::wx_app_base()
+{
+	static nv::logger log( nv::LOG_TRACE );
+	log.add_sink( new nv::log_file_sink( "log.txt" ), nv::LOG_TRACE );
+	NV_LOG( nv::LOG_NOTICE, "Logging started" );
+}
+
+bool nv::wx_app_base::OnInit()
+{
+	if ( !wxApp::OnInit() )
+		return false;
+
+	return initialize();
+}
+
+int nv::wx_app_base::OnExit()
+{
+	NV_LOG( nv::LOG_NOTICE, "Logging stopped" );
+	return wxApp::OnExit();
+}
