@@ -24,8 +24,6 @@
#include "../window_func.h"
#include "sdl_v.h"
#include <SDL.h>
#include <mutex>
#include <condition_variable>

#include "../safeguards.h"

@@ -35,14 +33,6 @@ static SDL_Surface *_sdl_surface;
static SDL_Surface *_sdl_realscreen;
static bool _all_modes;

/** Whether the drawing is/may be done in a separate thread. */
static bool _draw_threaded;
/** Mutex to keep the access to the shared memory controlled. */
static std::recursive_mutex *_draw_mutex = nullptr;
/** Signal to draw the next frame. */
static std::condition_variable_any *_draw_signal = nullptr;
/** Should we keep continue drawing? */
static volatile bool _draw_continue;
static Palette _local_palette;

#define MAX_DIRTY_RECTS 100
@@ -175,27 +165,6 @@ void VideoDriver_SDL::Paint()
}
}

void VideoDriver_SDL::PaintThread()
{
/* First tell the main thread we're started */
std::unique_lock<std::recursive_mutex> lock(*_draw_mutex);
_draw_signal->notify_one();

/* Now wait for the first thing to draw! */
_draw_signal->wait(*_draw_mutex);

while (_draw_continue) {
/* Then just draw and wait till we stop */
this->Paint();
_draw_signal->wait(lock);
}
}

/* static */ void VideoDriver_SDL::PaintThreadThunk(VideoDriver_SDL *drv)
{
drv->PaintThread();
}

static const Dimension _default_resolutions[] = {
{ 640, 480},
{ 800, 600},
@@ -602,10 +571,10 @@ bool VideoDriver_SDL::PollEvent()
return true;
}

const char *VideoDriver_SDL::Start(const StringList &parm)
const char *VideoDriver_SDL::Start(const StringList &param)
{
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.
@@ -631,7 +600,7 @@ const char *VideoDriver_SDL::Start(const StringList &parm)
MarkWholeScreenDirty();
SetupKeyboard();

_draw_threaded = !GetDriverParamBool(parm, "no_threads") && !GetDriverParamBool(parm, "no_thread");
this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread");

return nullptr;
}
@@ -686,80 +655,23 @@ void VideoDriver_SDL::InputLoop()

void VideoDriver_SDL::MainLoop()
{
std::thread draw_thread;
if (_draw_threaded) {
/* Initialise the mutex first, because that's the thing we *need*
* directly in the newly created thread. */
_draw_mutex = new std::recursive_mutex();
if (_draw_mutex == nullptr) {
_draw_threaded = false;
} else {
this->draw_lock = std::unique_lock<std::recursive_mutex>(*_draw_mutex);
_draw_signal = new std::condition_variable_any();
_draw_continue = true;

_draw_threaded = StartNewThread(&draw_thread, "ottd:draw-sdl", &VideoDriver_SDL::PaintThreadThunk, this);

/* Free the mutex if we won't be able to use it. */
if (!_draw_threaded) {
this->draw_lock.unlock();
this->draw_lock.release();
delete _draw_mutex;
delete _draw_signal;
_draw_mutex = nullptr;
_draw_signal = nullptr;
} else {
/* Wait till the draw mutex has started itself. */
_draw_signal->wait(*_draw_mutex);
}
}
}

DEBUG(driver, 1, "SDL: using %sthreads", _draw_threaded ? "" : "no ");
this->StartGameThread();

for (;;) {
if (_exit_game) break;

if (this->Tick()) {
if (_draw_mutex != nullptr && !HasModalProgress()) {
_draw_signal->notify_one();
} else {
this->Paint();
}
}
this->Tick();
this->SleepTillNextTick();
}

if (_draw_mutex != nullptr) {
_draw_continue = false;
/* Sending signal if there is no thread blocked
* is very valid and results in noop */
_draw_signal->notify_one();
if (this->draw_lock.owns_lock()) this->draw_lock.unlock();
this->draw_lock.release();
draw_thread.join();

delete _draw_mutex;
delete _draw_signal;

_draw_mutex = nullptr;
_draw_signal = nullptr;
}
}

bool VideoDriver_SDL::ChangeResolution(int w, int h)
{
std::unique_lock<std::recursive_mutex> lock;
if (_draw_mutex != nullptr) lock = std::unique_lock<std::recursive_mutex>(*_draw_mutex);

return CreateMainSurface(w, h);
}

bool VideoDriver_SDL::ToggleFullscreen(bool fullscreen)
{
std::unique_lock<std::recursive_mutex> lock;
if (_draw_mutex != nullptr) lock = std::unique_lock<std::recursive_mutex>(*_draw_mutex);

_fullscreen = fullscreen;
GetVideoModes(); // get the list of available video modes
bool ret = !_resolutions.empty() && CreateMainSurface(_cur_resolution.width, _cur_resolution.height);
@@ -777,25 +689,4 @@ bool VideoDriver_SDL::AfterBlitterChange()
return CreateMainSurface(_screen.width, _screen.height);
}

void VideoDriver_SDL::AcquireBlitterLock()
{
if (_draw_mutex != nullptr) _draw_mutex->lock();
}

void VideoDriver_SDL::ReleaseBlitterLock()
{
if (_draw_mutex != nullptr) _draw_mutex->unlock();
}

bool VideoDriver_SDL::LockVideoBuffer()
{
if (_draw_threaded) this->draw_lock.lock();
return true;
}

void VideoDriver_SDL::UnlockVideoBuffer()
{
if (_draw_threaded) this->draw_lock.unlock();
}

#endif /* WITH_SDL */
@@ -29,30 +29,19 @@ class VideoDriver_SDL : public VideoDriver {

bool AfterBlitterChange() override;

void AcquireBlitterLock() override;

void ReleaseBlitterLock() override;

bool ClaimMousePointer() override;

const char *GetName() const override { return "sdl"; }

protected:
void InputLoop() override;
bool LockVideoBuffer() override;
void UnlockVideoBuffer() override;
void Paint() override;
void PaintThread() override;
void CheckPaletteAnim() override;
bool PollEvent() override;

private:
std::unique_lock<std::recursive_mutex> draw_lock;

bool CreateMainSurface(uint w, uint h);
void SetupKeyboard();

static void PaintThreadThunk(VideoDriver_SDL *drv);
};

/** Factory for the SDL video driver. */
@@ -9,81 +9,130 @@

#include "../stdafx.h"
#include "../debug.h"
#include "../core/random_func.hpp"
#include "../gfx_func.h"
#include "../progress.h"
#include "../thread.h"
#include "../window_func.h"
#include "video_driver.hpp"

bool VideoDriver::Tick()
void VideoDriver::GameLoop()
{
auto cur_ticks = std::chrono::steady_clock::now();
this->next_game_tick += this->GetGameInterval();

/* If more than a millisecond has passed, increase the _realtime_tick. */
if (cur_ticks - this->last_realtime_tick > std::chrono::milliseconds(1)) {
auto delta = std::chrono::duration_cast<std::chrono::milliseconds>(cur_ticks - this->last_realtime_tick);
_realtime_tick += delta.count();
this->last_realtime_tick += delta;
/* 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;

/* Keep the interactive randomizer a bit more random by requesting
* new values when-ever we can. */
InteractiveRandom();
{
std::lock_guard<std::mutex> lock(this->game_state_mutex);

::GameLoop();
}
}

if (cur_ticks >= this->next_game_tick || (_fast_forward && !_pause_mode)) {
if (_fast_forward && !_pause_mode) {
this->next_game_tick = cur_ticks + this->GetGameInterval();
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 {
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;
/* Ensure we yield to the main thread if the draw thread 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 mutex. */
std::lock_guard<std::mutex> lock(this->game_thread_wait_mutex);
}
}
}

/* The game loop is the part that can run asynchronously.
* The rest except sleeping can't. */
this->UnlockVideoBuffer();
::GameLoop();
this->LockVideoBuffer();
/* static */ void VideoDriver::GameThreadThunk(VideoDriver *drv)
{
drv->GameThread();
}

void VideoDriver::StartGameThread()
{
if (this->is_game_threaded) {
this->is_game_threaded = StartNewThread(nullptr, "ottd:game", &VideoDriver::GameThreadThunk, this);
}

DEBUG(driver, 1, "using %sthread for game-loop", this->is_game_threaded ? "" : "no ");
}

void VideoDriver::Tick()
{
auto now = std::chrono::steady_clock::now();

/* If more than a millisecond has passed, increase the _realtime_tick. */
if (now - this->last_realtime_tick >= std::chrono::milliseconds(1)) {
auto delta = std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_realtime_tick);
_realtime_tick += delta.count();
this->last_realtime_tick += delta;
}

if (!this->is_game_threaded && 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 || HasModalProgress())) {
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;

while (this->PollEvent()) {}
this->InputLoop();
::InputLoop();
UpdateWindows();

{
/* Tell the game-thread to stop so we can have a go. */
std::lock_guard<std::mutex> lock_wait(this->game_thread_wait_mutex);
std::lock_guard<std::mutex> 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 (_switch_mode == SM_NONE || HasModalProgress()) {
TICC();
::UpdateWindows();
TOCC("update", 100);
}
}

TICC();
extern void ViewportDoBlitterAll();
ViewportDoBlitterAll();
TOCC("blitter", 100);

this->CheckPaletteAnim();
this->Paint();

return true;
this->UnlockVideoBuffer();
}

return false;
}

void VideoDriver::SleepTillNextTick()
{
/* If we are not in fast-forward, create some time between calls to ease up CPU usage. */
if (!_fast_forward || _pause_mode) {
/* 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 now = std::chrono::steady_clock::now();
auto next_tick = this->next_draw_tick;
auto now = std::chrono::steady_clock::now();

if (next_tick > now) {
this->UnlockVideoBuffer();
std::this_thread::sleep_for(next_tick - now);
this->LockVideoBuffer();
}
if (!this->is_game_threaded) {
next_tick = min(next_tick, this->next_game_tick);
}

if (next_tick > now) {
std::this_thread::sleep_for(next_tick - now);
}
}
@@ -15,7 +15,10 @@
#include "../core/math_func.hpp"
#include "../settings_type.h"
#include "../zoom_type.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <vector>

extern std::string _ini_videodriver;
@@ -29,6 +32,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.
@@ -239,11 +244,6 @@ class VideoDriver : public Driver {
*/
virtual void Paint() {}

/**
* Thread function for threaded drawing.
*/
virtual void PaintThread() {}

/**
* Process any pending palette animation.
*/
@@ -256,10 +256,16 @@ class VideoDriver : public Driver {
virtual bool PollEvent() { return false; };

/**
* Run the game for a single tick, processing boththe game-tick and draw-tick.
* @returns True if the driver should redraw the screen.
* Start the loop for game-tick.
*/
void StartGameThread();

/**
* 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.
*/
bool Tick();
void Tick();

/**
* Sleep till the next tick is about to happen.
@@ -268,6 +274,8 @@ class VideoDriver : public Driver {

std::chrono::steady_clock::duration GetGameInterval()
{
extern byte _fast_forward;
if (_fast_forward && !_pause_mode) return std::chrono::milliseconds(0);
return std::chrono::milliseconds(MILLISECONDS_PER_TICK);
}

@@ -279,6 +287,16 @@ class VideoDriver : public Driver {
std::chrono::steady_clock::time_point last_realtime_tick;
std::chrono::steady_clock::time_point next_game_tick;
std::chrono::steady_clock::time_point next_draw_tick;

bool is_game_threaded;
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 */
@@ -226,11 +226,6 @@ bool VideoDriver_Win32Base::MakeWindow(bool full_screen)
return true;
}

/* static */ void VideoDriver_Win32Base::PaintThreadThunk(VideoDriver_Win32Base *drv)
{
drv->PaintThread();
}

/** Forward key presses to the window system. */
static LRESULT HandleCharMsg(uint keycode, WChar charcode)
{
@@ -872,70 +867,14 @@ bool VideoDriver_Win32Base::PollEvent()

void VideoDriver_Win32Base::MainLoop()
{
std::thread draw_thread;

if (this->draw_threaded) {
/* Initialise the mutex first, because that's the thing we *need*
* directly in the newly created thread. */
try {
this->draw_signal = new std::condition_variable_any();
this->draw_mutex = new std::recursive_mutex();
} catch (...) {
this->draw_threaded = false;
}

if (this->draw_threaded) {
this->draw_lock = std::unique_lock<std::recursive_mutex>(*this->draw_mutex);

this->draw_continue = true;
this->draw_threaded = StartNewThread(&draw_thread, "ottd:draw-win32", &VideoDriver_Win32Base::PaintThreadThunk, this);

/* Free the mutex if we won't be able to use it. */
if (!this->draw_threaded) {
this->draw_lock.unlock();
this->draw_lock.release();
delete this->draw_mutex;
delete this->draw_signal;
this->draw_mutex = nullptr;
this->draw_signal = nullptr;
} else {
DEBUG(driver, 1, "Threaded drawing enabled");
/* Wait till the draw thread has started itself. */
this->draw_signal->wait(*this->draw_mutex);
}
}
}
this->StartGameThread();

for (;;) {
if (_exit_game) break;

/* Flush GDI buffer to ensure we don't conflict with the drawing thread. */
GdiFlush();

if (this->Tick()) {
if (this->draw_mutex != nullptr && !HasModalProgress()) {
this->draw_signal->notify_one();
} else {
this->Paint();
}
}
this->Tick();
this->SleepTillNextTick();
}

if (this->draw_threaded) {
this->draw_continue = false;
/* Sending signal if there is no thread blocked
* is very valid and results in noop */
this->draw_signal->notify_all();
if (this->draw_lock.owns_lock()) this->draw_lock.unlock();
this->draw_lock.release();
draw_thread.join();

delete this->draw_mutex;
delete this->draw_signal;

this->draw_mutex = nullptr;
}
}

void VideoDriver_Win32Base::ClientSizeChanged(int w, int h, bool force)
@@ -955,9 +894,6 @@ void VideoDriver_Win32Base::ClientSizeChanged(int w, int h, bool force)

bool VideoDriver_Win32Base::ChangeResolution(int w, int h)
{
std::unique_lock<std::recursive_mutex> lock;
if (this->draw_mutex != nullptr) lock = std::unique_lock<std::recursive_mutex>(*this->draw_mutex);

if (_window_maximize) ShowWindow(this->main_wnd, SW_SHOWNORMAL);

this->width = this->width_org = w;
@@ -968,27 +904,11 @@ bool VideoDriver_Win32Base::ChangeResolution(int w, int h)

bool VideoDriver_Win32Base::ToggleFullscreen(bool full_screen)
{
std::unique_lock<std::recursive_mutex> lock;
if (this->draw_mutex != nullptr) lock = std::unique_lock<std::recursive_mutex>(*this->draw_mutex);

return this->MakeWindow(full_screen);
}

void VideoDriver_Win32Base::AcquireBlitterLock()
{
if (this->draw_mutex != nullptr) this->draw_mutex->lock();
}

void VideoDriver_Win32Base::ReleaseBlitterLock()
{
if (this->draw_mutex != nullptr) this->draw_mutex->unlock();
}

void VideoDriver_Win32Base::EditBoxLostFocus()
{
std::unique_lock<std::recursive_mutex> lock;
if (this->draw_mutex != nullptr) lock = std::unique_lock<std::recursive_mutex>(*this->draw_mutex);

CancelIMEComposition(this->main_wnd);
SetCompositionPos(this->main_wnd);
SetCandidatePos(this->main_wnd);
@@ -1044,8 +964,6 @@ bool VideoDriver_Win32Base::LockVideoBuffer()
if (this->buffer_locked) return false;
this->buffer_locked = true;

if (this->draw_threaded) this->draw_lock.lock();

_screen.dst_ptr = this->GetVideoPointer();
assert(_screen.dst_ptr != nullptr);

@@ -1061,7 +979,6 @@ void VideoDriver_Win32Base::UnlockVideoBuffer()
_screen.dst_ptr = nullptr;
}

if (this->draw_threaded) this->draw_lock.unlock();
this->buffer_locked = false;
}

@@ -1080,8 +997,6 @@ const char *VideoDriver_Win32GDI::Start(const StringList &param)

MarkWholeScreenDirty();

this->draw_threaded = !GetDriverParam(param, "no_threads") && !GetDriverParam(param, "no_thread") && std::thread::hardware_concurrency() > 1;

return nullptr;
}

@@ -1202,13 +1117,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;
}

@@ -1231,26 +1140,6 @@ void VideoDriver_Win32GDI::Paint()
this->dirty_rect = {};
}

void VideoDriver_Win32GDI::PaintThread()
{
/* First tell the main thread we're started */
std::unique_lock<std::recursive_mutex> lock(*this->draw_mutex);
this->draw_signal->notify_one();

/* Now wait for the first thing to draw! */
this->draw_signal->wait(*this->draw_mutex);

while (this->draw_continue) {
this->Paint();

/* Flush GDI buffer to ensure drawing here doesn't conflict with any GDI usage in the main WndProc. */
GdiFlush();

this->draw_signal->wait(*this->draw_mutex);
}
}


#ifdef _DEBUG
/* Keep this function here..
* It allows you to redraw the screen from within the MSVC debugger */
@@ -1391,9 +1280,10 @@ const char *VideoDriver_Win32OpenGL::Start(const StringList &param)

this->ClientSizeChanged(this->width, this->height, true);

this->draw_threaded = false;
MarkWholeScreenDirty();

this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread");

return nullptr;
}

@@ -17,7 +17,7 @@
/** Base class for Windows video drivers. */
class VideoDriver_Win32Base : public VideoDriver {
public:
VideoDriver_Win32Base() : main_wnd(nullptr), fullscreen(false), draw_mutex(nullptr), draw_signal(nullptr) {}
VideoDriver_Win32Base() : main_wnd(nullptr), fullscreen(false) {}

void Stop() override;

@@ -29,10 +29,6 @@ class VideoDriver_Win32Base : public VideoDriver {

bool ToggleFullscreen(bool fullscreen) override;

void AcquireBlitterLock() override;

void ReleaseBlitterLock() override;

bool ClaimMousePointer() override;

void EditBoxLostFocus() override;
@@ -47,12 +43,7 @@ class VideoDriver_Win32Base : public VideoDriver {
int width_org = 0; ///< Original monitor resolution width, before we changed it.
int height_org = 0; ///< Original monitor resolution height, before we changed it.

bool draw_threaded; ///< Whether the drawing is/may be done in a separate thread.
bool buffer_locked; ///< Video buffer was locked by the main thread.
volatile bool draw_continue; ///< Should we keep continue drawing?

std::recursive_mutex *draw_mutex; ///< Mutex to keep the access to the shared memory controlled.
std::condition_variable_any *draw_signal; ///< Signal to draw the next frame.
bool buffer_locked; ///< Video buffer was locked by the main thread.

Dimension GetScreenSize() const override;
float GetDPIScale() override;
@@ -78,10 +69,6 @@ class VideoDriver_Win32Base : public VideoDriver {
virtual void PaletteChanged(HWND hWnd) = 0;

private:
std::unique_lock<std::recursive_mutex> draw_lock;

static void PaintThreadThunk(VideoDriver_Win32Base *drv);

friend LRESULT CALLBACK WndProcGdi(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
};
/** The GDI video driver for windows. */
@@ -104,7 +91,6 @@ class VideoDriver_Win32GDI : public VideoDriver_Win32Base {

void Paint() override;
void *GetVideoPointer() override { return this->buffer_bits; }
void PaintThread() override;

bool AllocateBackingStore(int w, int h, bool force = false) override;
void PaletteChanged(HWND hWnd) override;
@@ -159,7 +145,6 @@ class VideoDriver_Win32OpenGL : public VideoDriver_Win32Base {
uint8 GetFullscreenBpp() override { return 32; } // OpenGL is always 32 bpp.

void Paint() override;
void PaintThread() override {}

bool AllocateBackingStore(int w, int h, bool force = false) override;
void *GetVideoPointer() override;

Large diffs are not rendered by default.

@@ -3199,12 +3199,12 @@ void UpdateWindows()
}
}

DrawDirtyBlocks();

FOR_ALL_WINDOWS_FROM_BACK(w) {
/* Update viewport only if window is not shaded. */
if (w->viewport != nullptr && !w->IsShaded()) UpdateViewportPosition(w);
}
DrawDirtyBlocks();

NetworkDrawChatMessage();
/* Redraw mouse cursor in case it was hidden */
DrawMouseCursor();