Index: source/tools/atlas/AtlasUI/Misc/DLLInterface.h =================================================================== --- source/tools/atlas/AtlasUI/Misc/DLLInterface.h +++ source/tools/atlas/AtlasUI/Misc/DLLInterface.h @@ -29,8 +29,6 @@ ATLASDLLIMPEXP void Atlas_GLSetCurrent(void* context); ATLASDLLIMPEXP void Atlas_GLSwapBuffers(void* context); -ATLASDLLIMPEXP void Atlas_NotifyEndOfFrame(); - ATLASDLLIMPEXP void Atlas_DisplayError(const wchar_t* text, size_t flags); #endif // DLLINTERFACE_INCLUDED Index: source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.h =================================================================== --- source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.h +++ source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.h @@ -87,6 +87,7 @@ ToolManager m_ToolManager; wxTimer m_Timer; + wxTimer m_RenderTimer; SectionLayout m_SectionLayout; Index: source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp =================================================================== --- source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp +++ source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp @@ -58,6 +58,15 @@ using namespace AtlasMessage; +enum TIMER_TYPE +{ + TIMER_TOOL, + TIMER_RENDER +}; + +// This records the last activity on the WX side. +double last_wx_user_activity = 0.0; + ////////////////////////////////////////////////////////////////////////// // GL functions exported from DLL, and called by game (in a separate @@ -126,6 +135,7 @@ { // Key event has been handled by the tool, so don't try // to use it for camera motion too + last_wx_user_activity = g_Timer.GetTime(); return; } @@ -140,7 +150,10 @@ void OnKeyUp(wxKeyEvent& evt) { if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_UP)) + { + last_wx_user_activity = g_Timer.GetTime(); return; + } if (KeyScroll(evt, false)) return; @@ -153,7 +166,10 @@ void OnChar(wxKeyEvent& evt) { if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_CHAR)) + { + last_wx_user_activity = g_Timer.GetTime(); return; + } // Alt+enter toggles fullscreen if (evt.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_ALT)) @@ -168,6 +184,7 @@ if (evt.GetKeyCode() == 'c') { POST_MESSAGE(CameraReset, ()); + last_wx_user_activity = g_Timer.GetTime(); return; } @@ -223,6 +240,7 @@ if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnMouse(evt)) { + last_wx_user_activity = g_Timer.GetTime(); // Mouse event has been handled by the tool, so don't try // to use it for camera motion too return; @@ -300,13 +318,6 @@ ////////////////////////////////////////////////////////////////////////// -volatile bool g_FrameHasEnded; -// Called from game thread -ATLASDLLIMPEXP void Atlas_NotifyEndOfFrame() -{ - g_FrameHasEnded = true; -} - enum { ID_Quit = 1, @@ -597,8 +608,13 @@ // Set up a timer to make sure tool-updates happen frequently (in addition // to the idle handler (which makes them happen more frequently if there's nothing // else to do)) - m_Timer.SetOwner(this); - m_Timer.Start(20); + m_Timer.SetOwner(this, TIMER_TOOL); + m_Timer.Start(16); // in ms + + // Set up a timer to trigger engine rendering. + // This is split from the tool timer to enable different framerates. + m_RenderTimer.SetOwner(this, TIMER_RENDER); + m_RenderTimer.Start(16); // in ms } wxToolBar* ScenarioEditor::OnCreateToolBar(long style, wxWindowID id, const wxString& WXUNUSED(name)) @@ -657,20 +673,27 @@ static void UpdateTool(ToolManager& toolManager) { - // Don't keep posting events if the game can't keep up - if (g_FrameHasEnded) - { - g_FrameHasEnded = false; // (thread safety doesn't matter here) - // TODO: Smoother timing stuff? - static double last = g_Timer.GetTime(); - double time = g_Timer.GetTime(); - toolManager.GetCurrentTool()->OnTick(time-last); - last = time; - } + // TODO: Smoother timing stuff? + static double last = g_Timer.GetTime(); + double time = g_Timer.GetTime(); + toolManager.GetCurrentTool()->OnTick(time-last); + last = time; } -void ScenarioEditor::OnTimer(wxTimerEvent&) + +void ScenarioEditor::OnTimer(wxTimerEvent& evt) { - UpdateTool(m_ToolManager); + if (evt.GetId() == TIMER_TOOL) + UpdateTool(m_ToolManager); + else + { + AtlasMessage::qRenderLoop qryRenderLoop; + qryRenderLoop.Post(); + if (!qryRenderLoop.wantHighFPS && + qryRenderLoop.timeSinceActivity > 1.0 && g_Timer.GetTime() - last_wx_user_activity > 1.0) + m_RenderTimer.Start(200); // Use 5 FPS to save CPU & GPU. + else + m_RenderTimer.Start(16); // Try for 60 FPS. + } } void ScenarioEditor::OnIdle(wxIdleEvent&) { Index: source/tools/atlas/GameInterface/GameLoop.h =================================================================== --- source/tools/atlas/GameInterface/GameLoop.h +++ source/tools/atlas/GameInterface/GameLoop.h @@ -24,6 +24,8 @@ class AtlasView; +void RendererIncrementalLoad(); + struct GameLoopState { GameLoopState() : running(false) {}; Index: source/tools/atlas/GameInterface/GameLoop.cpp =================================================================== --- source/tools/atlas/GameInterface/GameLoop.cpp +++ source/tools/atlas/GameInterface/GameLoop.cpp @@ -29,7 +29,6 @@ #include "InputProcessor.h" #include "graphics/TextureManager.h" -#include "gui/GUIManager.h" #include "lib/app_hooks.h" #include "lib/external_libraries/libsdl.h" #include "lib/timer.h" @@ -58,7 +57,6 @@ void (*Atlas_SetMessagePasser)(MessagePasser*); void (*Atlas_GLSetCurrent)(void* cavas); void (*Atlas_GLSwapBuffers)(void* canvas); -void (*Atlas_NotifyEndOfFrame)(); void (*Atlas_DisplayError)(const wchar_t* text, size_t flags); namespace AtlasMessage { @@ -70,21 +68,10 @@ MessagePasser* AtlasMessage::g_MessagePasser = NULL; -static InputProcessor g_Input; - static GameLoopState state; GameLoopState* g_AtlasGameLoop = &state; - -static ErrorReactionInternal AtlasDisplayError(const wchar_t* text, size_t flags) -{ - // TODO: after Atlas has been unloaded, don't do this - Atlas_DisplayError(text, flags); - - return ERI_CONTINUE; -} - -static void RendererIncrementalLoad() +void RendererIncrementalLoad() { // TODO: shouldn't duplicate this code from main.cpp @@ -101,175 +88,6 @@ while (more && timer_Time() - startTime < maxTime); } -static void RunEngine(const CmdLineArgs& args) -{ - debug_SetThreadName("engine_thread"); - - // Set new main thread so that all the thread-safety checks pass - ThreadUtil::SetMainThread(); - - g_Profiler2.RegisterCurrentThread("atlasmain"); - - MessagePasserImpl* msgPasser = (MessagePasserImpl*)AtlasMessage::g_MessagePasser; - - // Register all the handlers for message which might be passed back - RegisterHandlers(); - - // Override ah_display_error to pass all errors to the Atlas UI - // TODO: this doesn't work well because it doesn't pause the game thread - // and the error box is ugly, so only use it if we fix those issues - // (use INIT_HAVE_DISPLAY_ERROR init flag to test this) - AppHooks hooks = {0}; - hooks.display_error = AtlasDisplayError; - app_hooks_update(&hooks); - - // Disable the game's cursor rendering - extern CStrW g_CursorName; - g_CursorName = L""; - - state.args = args; - state.running = true; - state.view = AtlasView::GetView_None(); - state.glCanvas = NULL; - - double last_activity = timer_Time(); - - while (state.running) - { - bool recent_activity = false; - - ////////////////////////////////////////////////////////////////////////// - // (TODO: Work out why these things have to be in this order (to avoid - // jumps when starting to move, etc)) - - // Calculate frame length - { - const double time = timer_Time(); - static double last_time = time; - const double realFrameLength = time-last_time; - last_time = time; - ENSURE(realFrameLength >= 0.0); - // TODO: filter out big jumps, e.g. when having done a lot of slow - // processing in the last frame - state.realFrameLength = realFrameLength; - } - - // Process the input that was received in the past - if (g_Input.ProcessInput(&state)) - recent_activity = true; - - ////////////////////////////////////////////////////////////////////////// - - { - IMessage* msg; - while ((msg = msgPasser->Retrieve()) != NULL) - { - recent_activity = true; - - std::string name (msg->GetName()); - - msgHandlers::const_iterator it = GetMsgHandlers().find(name); - if (it != GetMsgHandlers().end()) - { - it->second(msg); - } - else - { - debug_warn(L"Unrecognised message"); - // CLogger might not be initialised, but this error will be sent - // to the debug output window anyway so people can still see it - LOGERROR("Unrecognised message (%s)", name.c_str()); - } - - if (msg->GetType() == IMessage::Query) - { - // For queries, we need to notify MessagePasserImpl::Query - // that the query has now been processed. - sem_post((sem_t*) static_cast(msg)->m_Semaphore); - // (msg may have been destructed at this point, so don't use it again) - - // It's quite possible that the querier is going to do a tiny - // bit of processing on the query results and then issue another - // query, and repeat lots of times in a loop. To avoid slowing - // that down by rendering between every query, make this - // thread yield now. - SDL_Delay(0); - } - else - { - // For non-queries, we need to delete the object, since we - // took ownership of it. - AtlasMessage::ShareableDelete(msg); - } - } - } - - // Exit, if desired - if (! state.running) - break; - - ////////////////////////////////////////////////////////////////////////// - - // Do per-frame processing: - - ReloadChangedFiles(); - - RendererIncrementalLoad(); - - // Pump SDL events (e.g. hotkeys) - SDL_Event_ ev; - while (in_poll_priority_event(&ev)) - in_dispatch_event(&ev); - - if (g_GUI) - g_GUI->TickObjects(); - - state.view->Update(state.realFrameLength); - - state.view->Render(); - - if (CProfileManager::IsInitialised()) - g_Profiler.Frame(); - - - double time = timer_Time(); - if (recent_activity) - last_activity = time; - - // Be nice to the processor (by sleeping lots) if we're not doing anything - // useful, and nice to the user (by just yielding to other threads) if we are - bool yield = (time - last_activity > 0.5); - - // But make sure we aren't doing anything interesting right now, where - // the user wants to see the screen updating even though they're not - // interacting with it - if (state.view->WantsHighFramerate()) - yield = false; - - if (yield) // if there was no recent activity... - { - double sleepUntil = time + 0.5; // only redraw at 2fps - while (time < sleepUntil) - { - // To minimise latency when the user starts doing stuff, only - // sleep for a short while, then check if anything's happened, - // then go back to sleep - // (TODO: This should probably be done with something like semaphores) - Atlas_NotifyEndOfFrame(); // (TODO: rename to NotifyEndOfQuiteShortProcessingPeriodSoPleaseSendMeNewMessages or something) - SDL_Delay(50); - if (!msgPasser->IsEmpty()) - break; - time = timer_Time(); - } - } - else - { - Atlas_NotifyEndOfFrame(); - SDL_Delay(0); - } - } -} - bool BeginAtlas(const CmdLineArgs& args, const DllLoader& dll) { // Load required symbols from the DLL @@ -281,7 +99,6 @@ dll.LoadSymbol("Atlas_SetConfigDirectory", Atlas_SetConfigDirectory); dll.LoadSymbol("Atlas_GLSetCurrent", Atlas_GLSetCurrent); dll.LoadSymbol("Atlas_GLSwapBuffers", Atlas_GLSwapBuffers); - dll.LoadSymbol("Atlas_NotifyEndOfFrame", Atlas_NotifyEndOfFrame); dll.LoadSymbol("Atlas_DisplayError", Atlas_DisplayError); dll.LoadSymbol("ShareableMalloc", ShareableMallocFptr); dll.LoadSymbol("ShareableFree", ShareableFreeFptr); @@ -307,16 +124,21 @@ // Tell Atlas the location of the user config directory Atlas_SetConfigDirectory(paths.Config().string().c_str()); - // Run the engine loop in a new thread - std::thread engineThread = std::thread(RunEngine, std::ref(args)); + RegisterHandlers(); + + // Disable the game's cursor rendering + extern CStrW g_CursorName; + g_CursorName = L""; + + state.args = args; + state.running = true; + state.view = AtlasView::GetView_None(); + state.glCanvas = NULL; // Start Atlas UI on main thread // (required for wxOSX/Cocoa compatibility - see http://trac.wildfiregames.com/ticket/500) Atlas_StartWindow(L"ScenarioEditor"); - // Wait for the engine to exit - engineThread.join(); - // TODO: delete all remaining messages, to avoid memory leak warnings // Restore main thread Index: source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp =================================================================== --- source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp +++ source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp @@ -22,9 +22,11 @@ #include "../CommandProc.h" #include "../ActorViewer.h" #include "../View.h" +#include "../InputProcessor.h" #include "graphics/GameView.h" #include "graphics/ObjectManager.h" +#include "gui/GUIManager.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "maths/MathUtil.h" @@ -35,6 +37,12 @@ #include "ps/GameSetup/GameSetup.h" #include "renderer/Renderer.h" +static InputProcessor g_Input; + +// This keeps track of the last in-game user input. +// It is used to throttle FPS to save CPU & GPU. +static double last_user_activity; + namespace AtlasMessage { // see comment in GameLoop.cpp about ah_display_error before using INIT_HAVE_DISPLAY_ERROR @@ -192,6 +200,46 @@ #endif } +QUERYHANDLER(RenderLoop) +{ + { + const double time = timer_Time(); + static double last_time = time; + const double realFrameLength = time-last_time; + last_time = time; + ENSURE(realFrameLength >= 0.0); + // TODO: filter out big jumps, e.g. when having done a lot of slow + // processing in the last frame + g_AtlasGameLoop->realFrameLength = realFrameLength; + } + + if (g_Input.ProcessInput(g_AtlasGameLoop)) + last_user_activity = timer_Time(); + + msg->timeSinceActivity = timer_Time() - last_user_activity; + + ReloadChangedFiles(); + + RendererIncrementalLoad(); + + // Pump SDL events (e.g. hotkeys) + SDL_Event_ ev; + while (in_poll_priority_event(&ev)) + in_dispatch_event(&ev); + + if (g_GUI) + g_GUI->TickObjects(); + + g_AtlasGameLoop->view->Update(g_AtlasGameLoop->realFrameLength); + + g_AtlasGameLoop->view->Render(); + + if (CProfileManager::IsInitialised()) + g_Profiler.Frame(); + + msg->wantHighFPS = g_AtlasGameLoop->view->WantsHighFramerate(); +} + ////////////////////////////////////////////////////////////////////////// MESSAGEHANDLER(RenderStyle) Index: source/tools/atlas/GameInterface/MessagePasser.h =================================================================== --- source/tools/atlas/GameInterface/MessagePasser.h +++ source/tools/atlas/GameInterface/MessagePasser.h @@ -34,8 +34,6 @@ virtual void Add(IMessage*) = 0; // takes ownership of IMessage object - virtual IMessage* Retrieve() = 0; - virtual void Query(QueryMessage*, void(*timeoutCallback)()) = 0; // blocks; caller retains ownership of QueryMessage object }; Index: source/tools/atlas/GameInterface/MessagePasserImpl.h =================================================================== --- source/tools/atlas/GameInterface/MessagePasserImpl.h +++ source/tools/atlas/GameInterface/MessagePasserImpl.h @@ -20,32 +20,19 @@ #include "MessagePasser.h" -#include "lib/posix/posix_pthread.h" -#include "ps/CStr.h" -#include - -#include - class MessagePasserImpl : public AtlasMessage::MessagePasser { NONCOPYABLE(MessagePasserImpl); public: - MessagePasserImpl(); - ~MessagePasserImpl(); + MessagePasserImpl() = default; + virtual void Add(AtlasMessage::IMessage* msg); - virtual AtlasMessage::IMessage* Retrieve(); virtual void Query(AtlasMessage::QueryMessage* qry, void(*timeoutCallback)()); - bool IsEmpty(); - void SetTrace(bool t); private: - std::mutex m_Mutex; - CStr m_SemaphoreName; - sem_t* m_Semaphore; - std::queue m_Queue; - bool m_Trace; + bool m_Trace = false; }; #endif // INCLUDED_MESSAGEPASSERIMPL Index: source/tools/atlas/GameInterface/MessagePasserImpl.cpp =================================================================== --- source/tools/atlas/GameInterface/MessagePasserImpl.cpp +++ source/tools/atlas/GameInterface/MessagePasserImpl.cpp @@ -21,62 +21,13 @@ #include "MessagePasserImpl.h" #include "Messages.h" +#include "Handlers/MessageHandler.h" #include "lib/timer.h" -#include "lib/rand.h" -#include "lib/posix/posix_filesystem.h" using namespace AtlasMessage; - -MessagePasserImpl::MessagePasserImpl() -: m_Trace(false), m_Semaphore(NULL) -{ - int tries = 0; - while (tries++ < 16) // some arbitrary cut-off point to avoid infinite loops - { - static char name[64]; - sprintf_s(name, ARRAY_SIZE(name), "/wfg-atlas-msgpass-%d-%d", - (int)rand(1, 1000), (int)(time(0)%1000)); - sem_t* sem = sem_open(name, O_CREAT | O_EXCL, 0700, 0); - - // This cast should not be necessary, but apparently SEM_FAILED is not - // a value of a pointer type - if (sem == (sem_t*)SEM_FAILED || !sem) - { - int err = errno; - if (err == EEXIST) - { - // Semaphore already exists - try another one - continue; - } - // Otherwise, it's a probably-fatal error - debug_warn(L"sem_open failed"); - break; - } - // Succeeded - use this semaphore - m_Semaphore = sem; - m_SemaphoreName = name; - break; - } - - if (! m_Semaphore) - { - debug_warn(L"Failed to create semaphore for Atlas - giving up"); - // We will probably crash later - maybe we could fall back on sem_init, if this - // ever fails in practice - } -} - -MessagePasserImpl::~MessagePasserImpl() -{ - if (m_Semaphore) - { - // Clean up - sem_close(m_Semaphore); - sem_unlink(m_SemaphoreName.c_str()); - } -} +double last_user_activity = 0.0; void MessagePasserImpl::Add(IMessage* msg) { @@ -86,107 +37,42 @@ if (m_Trace) debug_printf("%8.3f add message: %s\n", timer_Time(), msg->GetName()); + msgHandlers::const_iterator it = GetMsgHandlers().find(msg->GetName()); + if (it != GetMsgHandlers().end()) { - std::lock_guard lock(m_Mutex); - m_Queue.push(msg); + it->second(msg); } -} - -IMessage* MessagePasserImpl::Retrieve() -{ - // (It should be fairly easy to use a more efficient thread-safe queue, - // since there's only one thread adding items and one thread consuming; - // but it's not worthwhile yet.) - - IMessage* msg = NULL; - + else { - std::lock_guard lock(m_Mutex); - if (! m_Queue.empty()) - { - msg = m_Queue.front(); - m_Queue.pop(); - } + debug_warn(L"Unrecognised message"); + // CLogger might not be initialised, but this error will be sent + // to the debug output window anyway so people can still see it + LOGERROR("Unrecognised message (%s)", msg->GetName()); } - - if (m_Trace && msg) - debug_printf("%8.3f retrieved message: %s\n", timer_Time(), msg->GetName()); - - return msg; + // Delete the object - we took ownership of it. + AtlasMessage::ShareableDelete(msg); } -void MessagePasserImpl::Query(QueryMessage* qry, void(* UNUSED(timeoutCallback) )()) +void MessagePasserImpl::Query(QueryMessage* msg, void(* UNUSED(timeoutCallback) )()) { - ENSURE(qry); - ENSURE(qry->GetType() == IMessage::Query); + ENSURE(msg); + ENSURE(msg->GetType() == IMessage::Query); if (m_Trace) - debug_printf("%8.3f add query: %s\n", timer_Time(), qry->GetName()); - - // Set the semaphore, so we can block until the query has been handled - qry->m_Semaphore = static_cast(m_Semaphore); + debug_printf("%8.3f add query: %s\n", timer_Time(), msg->GetName()); + msgHandlers::const_iterator it = GetMsgHandlers().find(msg->GetName()); + if (it != GetMsgHandlers().end()) { - std::lock_guard lock(m_Mutex); - m_Queue.push(qry); + it->second(msg); } - - // Wait until the query handler has handled the query and called sem_post: - - - // The following code was necessary to avoid deadlock, but it still breaks - // in some cases (e.g. when Atlas issues a query before its event loop starts - // running) and doesn't seem to be the simplest possible solution. - // So currently we're trying to not do anything like that at all, and - // just stop the game making windows (which is what seems (from experience) to - // deadlock things) by overriding ah_display_error. Hopefully it'll work like - // that, and the redundant code below/elsewhere can be removed, but it's - // left in here in case it needs to be reinserted in the future to make it - // work. - // (See http://www.wildfiregames.com/forum/index.php?s=&showtopic=10236&view=findpost&p=174617) - -// // At least on Win32, it is necessary for the UI thread to run its event -// // loop to avoid deadlocking the system (particularly when the game -// // tries to show a dialog box); so timeoutCallback is called whenever we -// // think it's necessary for that to happen. -// -// #if OS_WIN -// // On Win32, use MsgWaitForMultipleObjects, which waits on the semaphore -// // but is also interrupted by incoming Windows-messages. -// // while (0 != (err = sem_msgwait_np(psem))) -// -// while (0 != (err = sem_wait(psem))) -// #else -// // TODO: On non-Win32, I have no idea whether the same problem exists; but -// // it might do, so call the callback every few seconds just in case it helps. -// struct timespec abs_timeout; -// clock_gettime(CLOCK_REALTIME, &abs_timeout); -// abs_timeout.tv_sec += 2; -// while (0 != (err = sem_timedwait(psem, &abs_timeout))) -// #endif - - while (0 != sem_wait(m_Semaphore)) + else { - // If timed out, call callback and try again -// if (errno == ETIMEDOUT) -// timeoutCallback(); -// else - // Keep retrying while EINTR, but other errors are probably fatal - if (errno != EINTR) - { - debug_warn(L"Semaphore wait failed"); - return; // (leaks the semaphore) - } + debug_warn(L"Unrecognised message"); + // CLogger might not be initialised, but this error will be sent + // to the debug output window anyway so people can still see it + LOGERROR("Unrecognised message (%s)", msg->GetName()); } - - // Clean up - qry->m_Semaphore = NULL; -} - -bool MessagePasserImpl::IsEmpty() -{ - std::lock_guard lock(m_Mutex); - return m_Queue.empty(); } void MessagePasserImpl::SetTrace(bool t) Index: source/tools/atlas/GameInterface/Messages.h =================================================================== --- source/tools/atlas/GameInterface/Messages.h +++ source/tools/atlas/GameInterface/Messages.h @@ -148,6 +148,11 @@ ((int, height)) ); +QUERY(RenderLoop, , + ((bool, wantHighFPS)) + ((double, timeSinceActivity)) + ); + ////////////////////////////////////////////////////////////////////////// // Messages for map panel Index: source/tools/atlas/GameInterface/Shareable.h =================================================================== --- source/tools/atlas/GameInterface/Shareable.h +++ source/tools/atlas/GameInterface/Shareable.h @@ -96,6 +96,7 @@ SHAREABLE_PRIMITIVE(long); SHAREABLE_PRIMITIVE(bool); SHAREABLE_PRIMITIVE(float); +SHAREABLE_PRIMITIVE(double); SHAREABLE_PRIMITIVE(void*); #undef SHAREABLE_PRIMITIVE