diff --git a/src/video/allegro_v.cpp b/src/video/allegro_v.cpp --- a/src/video/allegro_v.cpp +++ b/src/video/allegro_v.cpp @@ -412,7 +412,7 @@ bool VideoDriver_Allegro::PollEvent() */ int _allegro_instance_count = 0; -const char *VideoDriver_Allegro::Start(const StringList &parm) +const char *VideoDriver_Allegro::Start(const StringList ¶m) { if (_allegro_instance_count == 0 && install_allegro(SYSTEM_AUTODETECT, &errno, nullptr)) { DEBUG(driver, 0, "allegro: install_allegro failed '%s'", allegro_error); @@ -440,6 +440,8 @@ const char *VideoDriver_Allegro::Start(c MarkWholeScreenDirty(); set_close_button_callback(HandleExitGameRequest); + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); + return nullptr; } @@ -475,12 +477,16 @@ void VideoDriver_Allegro::InputLoop() void VideoDriver_Allegro::MainLoop() { + this->StartGameThread(); + for (;;) { - if (_exit_game) return; + if (_exit_game) break; this->Tick(); this->SleepTillNextTick(); } + + this->StopGameThread(); } bool VideoDriver_Allegro::ChangeResolution(int w, int h) diff --git a/src/video/cocoa/cocoa_ogl.h b/src/video/cocoa/cocoa_ogl.h --- a/src/video/cocoa/cocoa_ogl.h +++ b/src/video/cocoa/cocoa_ogl.h @@ -33,6 +33,8 @@ public: void ClearSystemSprites() override; + void PopulateSystemSprites() override; + bool HasAnimBuffer() override { return true; } uint8 *GetAnimBuffer() override { return this->anim_buffer; } diff --git a/src/video/cocoa/cocoa_ogl.mm b/src/video/cocoa/cocoa_ogl.mm --- a/src/video/cocoa/cocoa_ogl.mm +++ b/src/video/cocoa/cocoa_ogl.mm @@ -214,6 +214,8 @@ const char *VideoDriver_CocoaOpenGL::Sta this->UpdateVideoModes(); MarkWholeScreenDirty(); + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); + return nullptr; } @@ -227,6 +229,11 @@ void VideoDriver_CocoaOpenGL::Stop() CGLReleaseContext(this->gl_context); } +void VideoDriver_CocoaOpenGL::PopulateSystemSprites() +{ + OpenGLBackend::Get()->PopulateCursorCache(); +} + void VideoDriver_CocoaOpenGL::ClearSystemSprites() { CGLSetCurrentContext(this->gl_context); diff --git a/src/video/cocoa/cocoa_v.mm b/src/video/cocoa/cocoa_v.mm --- a/src/video/cocoa/cocoa_v.mm +++ b/src/video/cocoa/cocoa_v.mm @@ -435,6 +435,8 @@ void VideoDriver_Cocoa::InputLoop() /** Main game loop. */ void VideoDriver_Cocoa::MainLoopReal() { + this->StartGameThread(); + for (;;) { @autoreleasepool { if (_exit_game) { @@ -447,6 +449,8 @@ void VideoDriver_Cocoa::MainLoopReal() this->SleepTillNextTick(); } } + + this->StopGameThread(); } @@ -558,6 +562,8 @@ const char *VideoDriver_CocoaQuartz::Sta this->GameSizeChanged(); this->UpdateVideoModes(); + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); + return nullptr; } diff --git a/src/video/dedicated_v.cpp b/src/video/dedicated_v.cpp --- a/src/video/dedicated_v.cpp +++ b/src/video/dedicated_v.cpp @@ -267,6 +267,8 @@ void VideoDriver_Dedicated::MainLoop() } } + this->is_game_threaded = false; + /* Done loading, start game! */ if (!_networking) { diff --git a/src/video/null_v.cpp b/src/video/null_v.cpp --- a/src/video/null_v.cpp +++ b/src/video/null_v.cpp @@ -48,9 +48,9 @@ void VideoDriver_Null::MainLoop() uint i; for (i = 0; i < this->ticks; i++) { - GameLoop(); - InputLoop(); - UpdateWindows(); + ::GameLoop(); + ::InputLoop(); + ::UpdateWindows(); } } diff --git a/src/video/opengl.cpp b/src/video/opengl.cpp --- a/src/video/opengl.cpp +++ b/src/video/opengl.cpp @@ -1036,7 +1036,23 @@ void OpenGLBackend::DrawMouseCursor() _cur_dpi = &_screen; for (uint i = 0; i < _cursor.sprite_count; ++i) { SpriteID sprite = _cursor.sprite_seq[i].sprite; - const Sprite *spr = GetSprite(sprite, ST_NORMAL); + + /* Sprites are cached by PopulateCursorCache(). */ + if (this->cursor_cache.Contains(sprite)) { + const Sprite *spr = GetSprite(sprite, ST_NORMAL); + + this->RenderOglSprite((OpenGLSprite *)this->cursor_cache.Get(sprite)->data, _cursor.sprite_seq[i].pal, + _cursor.pos.x + _cursor.sprite_pos[i].x + UnScaleByZoom(spr->x_offs, ZOOM_LVL_GUI), + _cursor.pos.y + _cursor.sprite_pos[i].y + UnScaleByZoom(spr->y_offs, ZOOM_LVL_GUI), + ZOOM_LVL_GUI); + } + } +} + +void OpenGLBackend::PopulateCursorCache() +{ + for (uint i = 0; i < _cursor.sprite_count; ++i) { + SpriteID sprite = _cursor.sprite_seq[i].sprite; if (!this->cursor_cache.Contains(sprite)) { Sprite *old = this->cursor_cache.Insert(sprite, (Sprite *)GetRawSprite(sprite, ST_NORMAL, &SimpleSpriteAlloc, this)); @@ -1046,11 +1062,6 @@ void OpenGLBackend::DrawMouseCursor() free(old); } } - - this->RenderOglSprite((OpenGLSprite *)this->cursor_cache.Get(sprite)->data, _cursor.sprite_seq[i].pal, - _cursor.pos.x + _cursor.sprite_pos[i].x + UnScaleByZoom(spr->x_offs, ZOOM_LVL_GUI), - _cursor.pos.y + _cursor.sprite_pos[i].y + UnScaleByZoom(spr->y_offs, ZOOM_LVL_GUI), - ZOOM_LVL_GUI); } } diff --git a/src/video/opengl.h b/src/video/opengl.h --- a/src/video/opengl.h +++ b/src/video/opengl.h @@ -88,6 +88,7 @@ public: void Paint(); void DrawMouseCursor(); + void PopulateCursorCache(); void ClearCursorCache(); void *GetVideoBuffer(); diff --git a/src/video/sdl2_default_v.cpp b/src/video/sdl2_default_v.cpp --- a/src/video/sdl2_default_v.cpp +++ b/src/video/sdl2_default_v.cpp @@ -107,13 +107,7 @@ void VideoDriver_SDL_Default::Paint() break; case Blitter::PALETTE_ANIMATION_BLITTER: { - bool need_buf = _screen.dst_ptr == nullptr; - if (need_buf) _screen.dst_ptr = this->GetVideoPointer(); blitter->PaletteAnimate(this->local_palette); - if (need_buf) { - this->ReleaseVideoPointer(); - _screen.dst_ptr = nullptr; - } break; } diff --git a/src/video/sdl2_opengl_v.cpp b/src/video/sdl2_opengl_v.cpp --- a/src/video/sdl2_opengl_v.cpp +++ b/src/video/sdl2_opengl_v.cpp @@ -110,6 +110,11 @@ const char *VideoDriver_SDL_OpenGL::Allo return OpenGLBackend::Create(&GetOGLProcAddressCallback); } +void VideoDriver_SDL_OpenGL::PopulateSystemSprites() +{ + OpenGLBackend::Get()->PopulateCursorCache(); +} + void VideoDriver_SDL_OpenGL::ClearSystemSprites() { OpenGLBackend::Get()->ClearCursorCache(); diff --git a/src/video/sdl2_opengl_v.h b/src/video/sdl2_opengl_v.h --- a/src/video/sdl2_opengl_v.h +++ b/src/video/sdl2_opengl_v.h @@ -24,6 +24,8 @@ public: void ClearSystemSprites() override; + void PopulateSystemSprites() override; + bool HasAnimBuffer() override { return true; } uint8 *GetAnimBuffer() override { return this->anim_buffer; } diff --git a/src/video/sdl2_v.cpp b/src/video/sdl2_v.cpp --- a/src/video/sdl2_v.cpp +++ b/src/video/sdl2_v.cpp @@ -541,14 +541,14 @@ const char *VideoDriver_SDL_Base::Initia return nullptr; } -const char *VideoDriver_SDL_Base::Start(const StringList &parm) +const char *VideoDriver_SDL_Base::Start(const StringList ¶m) { if (BlitterFactory::GetCurrentBlitter()->GetScreenDepth() == 0) return "Only real blitters supported"; const char *error = this->Initialize(); if (error != nullptr) return error; - this->startup_display = FindStartupDisplay(GetDriverParamInt(parm, "display", -1)); + this->startup_display = FindStartupDisplay(GetDriverParamInt(param, "display", -1)); if (!CreateMainSurface(_cur_resolution.width, _cur_resolution.height, false)) { return SDL_GetError(); @@ -562,6 +562,12 @@ const char *VideoDriver_SDL_Base::Start( SDL_StopTextInput(); this->edit_box_focused = false; +#ifdef __EMSCRIPTEN__ + this->is_game_threaded = false; +#else + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); +#endif + return nullptr; } @@ -637,9 +643,13 @@ void VideoDriver_SDL_Base::MainLoop() /* Run the main loop event-driven, based on RequestAnimationFrame. */ emscripten_set_main_loop_arg(&this->EmscriptenLoop, this, 0, 1); #else + this->StartGameThread(); + while (!_exit_game) { LoopOnce(); } + + this->StopGameThread(); #endif } diff --git a/src/video/sdl_v.cpp b/src/video/sdl_v.cpp --- a/src/video/sdl_v.cpp +++ b/src/video/sdl_v.cpp @@ -570,10 +570,10 @@ bool VideoDriver_SDL::PollEvent() return true; } -const char *VideoDriver_SDL::Start(const StringList &parm) +const char *VideoDriver_SDL::Start(const StringList ¶m) { char buf[30]; - _use_hwpalette = GetDriverParamInt(parm, "hw_palette", 2); + _use_hwpalette = GetDriverParamInt(param, "hw_palette", 2); /* Just on the offchance the audio subsystem started before the video system, * check whether any part of SDL has been initialised before getting here. @@ -599,6 +599,8 @@ const char *VideoDriver_SDL::Start(const MarkWholeScreenDirty(); SetupKeyboard(); + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); + return nullptr; } @@ -647,12 +649,16 @@ void VideoDriver_SDL::InputLoop() void VideoDriver_SDL::MainLoop() { + this->StartGameThread(); + for (;;) { if (_exit_game) break; this->Tick(); this->SleepTillNextTick(); } + + this->StopGameThread(); } bool VideoDriver_SDL::ChangeResolution(int w, int h) diff --git a/src/video/video_driver.cpp b/src/video/video_driver.cpp --- a/src/video/video_driver.cpp +++ b/src/video/video_driver.cpp @@ -8,9 +8,9 @@ /** @file video_driver.cpp Common code between video driver implementations. */ #include "../stdafx.h" -#include "../debug.h" #include "../core/random_func.hpp" #include "../network/network.h" +#include "../debug.h" #include "../gfx_func.h" #include "../progress.h" #include "../thread.h" @@ -19,40 +19,84 @@ bool _video_hw_accel; ///< Whether to consider hardware accelerated video drivers. +void VideoDriver::GameLoop() +{ + this->next_game_tick += this->GetGameInterval(); + + /* Avoid next_game_tick getting behind more and more if it cannot keep up. */ + auto now = std::chrono::steady_clock::now(); + if (this->next_game_tick < now - ALLOWED_DRIFT * this->GetGameInterval()) this->next_game_tick = now; + + { + std::lock_guard lock(this->game_state_mutex); + + ::GameLoop(); + } +} + +void VideoDriver::GameThread() +{ + while (!_exit_game) { + this->GameLoop(); + + auto now = std::chrono::steady_clock::now(); + if (this->next_game_tick > now) { + std::this_thread::sleep_for(this->next_game_tick - now); + } else { + /* Ensure we yield this thread if drawings wants to take a lock on + * the game state. This is mainly because most OSes have an + * optimization that if you unlock/lock a mutex in the same thread + * quickly, it will never context switch even if there is another + * thread waiting to take the lock on the same mutex. */ + std::lock_guard lock(this->game_thread_wait_mutex); + } + } +} + +/* static */ void VideoDriver::GameThreadThunk(VideoDriver *drv) +{ + drv->GameThread(); +} + +void VideoDriver::StartGameThread() +{ + if (this->is_game_threaded) { + this->is_game_threaded = StartNewThread(&this->game_thread, "ottd:game", &VideoDriver::GameThreadThunk, this); + } + + DEBUG(driver, 1, "using %sthread for game-loop", this->is_game_threaded ? "" : "no "); +} + +void VideoDriver::StopGameThread() +{ + if (!this->is_game_threaded) return; + + this->game_thread.join(); +} + void VideoDriver::Tick() { - auto cur_ticks = std::chrono::steady_clock::now(); - - if (cur_ticks >= this->next_game_tick) { - this->next_game_tick += this->GetGameInterval(); - /* Avoid next_game_tick getting behind more and more if it cannot keep up. */ - if (this->next_game_tick < cur_ticks - ALLOWED_DRIFT * this->GetGameInterval()) this->next_game_tick = cur_ticks; - - /* The game loop is the part that can run asynchronously. - * The rest except sleeping can't. */ - this->UnlockVideoBuffer(); - ::GameLoop(); - this->LockVideoBuffer(); + if (!this->is_game_threaded && std::chrono::steady_clock::now() >= this->next_game_tick) { + this->GameLoop(); /* For things like dedicated server, don't run a separate draw-tick. */ if (!this->HasGUI()) { ::InputLoop(); - UpdateWindows(); + ::UpdateWindows(); this->next_draw_tick = this->next_game_tick; } } - /* Prevent drawing when switching mode, as windows can be removed when they should still appear. */ - if (this->HasGUI() && cur_ticks >= this->next_draw_tick && (_switch_mode == SM_NONE || _game_mode == GM_BOOTSTRAP || HasModalProgress())) { + auto now = std::chrono::steady_clock::now(); + if (this->HasGUI() && now >= this->next_draw_tick) { this->next_draw_tick += this->GetDrawInterval(); /* Avoid next_draw_tick getting behind more and more if it cannot keep up. */ - if (this->next_draw_tick < cur_ticks - ALLOWED_DRIFT * this->GetDrawInterval()) this->next_draw_tick = cur_ticks; + if (this->next_draw_tick < now - ALLOWED_DRIFT * this->GetDrawInterval()) this->next_draw_tick = now; /* Keep the interactive randomizer a bit more random by requesting * new values when-ever we can. */ InteractiveRandom(); - while (this->PollEvent()) {} this->InputLoop(); /* Check if the fast-forward button is still pressed. */ @@ -64,23 +108,41 @@ void VideoDriver::Tick() this->fast_forward_via_key = false; } - ::InputLoop(); - UpdateWindows(); + { + /* Tell the game-thread to stop so we can have a go. */ + std::lock_guard lock_wait(this->game_thread_wait_mutex); + std::lock_guard lock_state(this->game_state_mutex); + + this->LockVideoBuffer(); + + while (this->PollEvent()) {} + ::InputLoop(); + + /* Prevent drawing when switching mode, as windows can be removed when they should still appear. */ + if (_game_mode == GM_BOOTSTRAP || _switch_mode == SM_NONE || HasModalProgress()) { + ::UpdateWindows(); + } + + this->PopulateSystemSprites(); + } this->CheckPaletteAnim(); this->Paint(); + + this->UnlockVideoBuffer(); } } void VideoDriver::SleepTillNextTick() { - /* See how much time there is till we have to process the next event, and try to hit that as close as possible. */ - auto next_tick = std::min(this->next_draw_tick, this->next_game_tick); + auto next_tick = this->next_draw_tick; auto now = std::chrono::steady_clock::now(); + if (!this->is_game_threaded) { + next_tick = min(next_tick, this->next_game_tick); + } + if (next_tick > now) { - this->UnlockVideoBuffer(); std::this_thread::sleep_for(next_tick - now); - this->LockVideoBuffer(); } } diff --git a/src/video/video_driver.hpp b/src/video/video_driver.hpp --- a/src/video/video_driver.hpp +++ b/src/video/video_driver.hpp @@ -16,7 +16,11 @@ #include "../gfx_func.h" #include "../settings_type.h" #include "../zoom_type.h" +#include #include +#include +#include +#include #include extern std::string _ini_videodriver; @@ -31,6 +35,8 @@ class VideoDriver : public Driver { const uint DEFAULT_WINDOW_HEIGHT = 480u; ///< Default window height. public: + VideoDriver() : is_game_threaded(true) {} + /** * Mark a particular area dirty. * @param left The left most line of the dirty area. @@ -84,6 +90,11 @@ public: } /** + * Populate all sprites in cache. + */ + virtual void PopulateSystemSprites() {} + + /** * Clear all cached sprites. */ virtual void ClearSystemSprites() {} @@ -240,6 +251,16 @@ protected: virtual bool PollEvent() { return false; }; /** + * Start the loop for game-tick. + */ + void StartGameThread(); + + /** + * Stop the loop for the game-tick. This can still tick at most one time before truly shutting down. + */ + void StopGameThread(); + + /** * Give the video-driver a tick. * It will process any potential game-tick and/or draw-tick, and/or any * other video-driver related event. @@ -271,6 +292,17 @@ protected: bool fast_forward_key_pressed; ///< The fast-forward key is being pressed. bool fast_forward_via_key; ///< The fast-forward was enabled by key press. + + bool is_game_threaded; + std::thread game_thread; + std::mutex game_state_mutex; + std::mutex game_thread_wait_mutex; + + static void GameThreadThunk(VideoDriver *drv); + +private: + void GameLoop(); + void GameThread(); }; #endif /* VIDEO_VIDEO_DRIVER_HPP */ diff --git a/src/video/win32_v.cpp b/src/video/win32_v.cpp --- a/src/video/win32_v.cpp +++ b/src/video/win32_v.cpp @@ -864,12 +864,16 @@ bool VideoDriver_Win32Base::PollEvent() void VideoDriver_Win32Base::MainLoop() { + this->StartGameThread(); + for (;;) { if (_exit_game) break; this->Tick(); this->SleepTillNextTick(); } + + this->StopGameThread(); } void VideoDriver_Win32Base::ClientSizeChanged(int w, int h, bool force) @@ -995,6 +999,8 @@ const char *VideoDriver_Win32GDI::Start( MarkWholeScreenDirty(); + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); + return nullptr; } @@ -1115,13 +1121,7 @@ void VideoDriver_Win32GDI::Paint() break; case Blitter::PALETTE_ANIMATION_BLITTER: { - bool need_buf = _screen.dst_ptr == nullptr; - if (need_buf) _screen.dst_ptr = this->GetVideoPointer(); blitter->PaletteAnimate(_local_palette); - if (need_buf) { - this->ReleaseVideoPointer(); - _screen.dst_ptr = nullptr; - } break; } @@ -1291,6 +1291,8 @@ const char *VideoDriver_Win32OpenGL::Sta MarkWholeScreenDirty(); + this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); + return nullptr; } @@ -1371,6 +1373,11 @@ bool VideoDriver_Win32OpenGL::AfterBlitt return true; } +void VideoDriver_Win32OpenGL::PopulateSystemSprites() +{ + OpenGLBackend::Get()->PopulateCursorCache(); +} + void VideoDriver_Win32OpenGL::ClearSystemSprites() { OpenGLBackend::Get()->ClearCursorCache(); diff --git a/src/video/win32_v.h b/src/video/win32_v.h --- a/src/video/win32_v.h +++ b/src/video/win32_v.h @@ -129,6 +129,8 @@ public: bool UseSystemCursor() override { return true; } + void PopulateSystemSprites() override; + void ClearSystemSprites() override; bool HasAnimBuffer() override { return true; }