eliminating lag from a chess clock
-
I'm cleaning up a chess program of mine, which includes a clock feature. For those unfamiliar with chess clocks, both players start the game with a certain amount of time, and each of their clocks counts down when and only when it's currently their turn. The program has GUI, and the displays for the timers have to be prompted to redraw every time a second flips. I had previously implemented this by having a timer thread that stores the time when a turn starts, then constantly re-checks the time in a loop, using this to update the active player's time on every iteration. This worked like a charm, but I understand that spinning like that is bad practice for wasting cycles, so I tried redoing it using wait_until(), roughly like this:
HWND g_mainWindowHandle;
std::condition_variable g_timerConditional;
std::mutex g_timerLock;
std::atomicg_turnIsOver;
std::atomicg_blackPlayerNanosecondsLeft;
std::atomicg_whitePlayerNanosecondsLeft;void turnTimer(std::atomic*activePlayerNanosecondsLeft,
Rect*displayRect)
{
std::chrono::nanoseconds currentNanosecondsLeft{ activePlayerNanosecondsLeft->load() };
std::chrono::nanoseconds nanosecondsToNextSecond{ currentNanosecondsLeft % 1000000000 };
std::chrono::steady_clock::time_point nextSecondBoundary{
std::chrono::steady_clock::now() + nanosecondsToNextSecond };
std::unique_locklock(g_timerLock);
std::cv_status status{
g_timerConditional.std::condition_variable::wait_until(lock, nextSecondBoundary) };
while (!g_turnIsOver.load())
{
if (status == std::cv_status::timeout)
{
currentNanosecondsLeft -= nanosecondsToNextSecond;
*activePlayerNanosecondsLeft = currentNanosecondsLeft;
nanosecondsToNextSecond = std::chrono::nanoseconds(1000000000);
nextSecondBoundary += nanosecondsToNextSecond;
InvalidateRect(g_mainWindowHandle, displayRect, FALSE);
}
status = g_timerConditional.std::condition_variable::wait_until(lock, nextSecondBoundary);
}
currentNanosecondsLeft -=
std::chrono::steady_clock::now() - (nextSecondBoundary - nanosecondsToNextSecond);
*activePlayerNanosecondsLeft = currentNanosecondsLeft;
g_turnIsOver = false;
InvalidateRect(g_mainWindowHandle, displayRect, FALSE);
}Each time the main thread moves a piece, it sets g_
-
I'm cleaning up a chess program of mine, which includes a clock feature. For those unfamiliar with chess clocks, both players start the game with a certain amount of time, and each of their clocks counts down when and only when it's currently their turn. The program has GUI, and the displays for the timers have to be prompted to redraw every time a second flips. I had previously implemented this by having a timer thread that stores the time when a turn starts, then constantly re-checks the time in a loop, using this to update the active player's time on every iteration. This worked like a charm, but I understand that spinning like that is bad practice for wasting cycles, so I tried redoing it using wait_until(), roughly like this:
HWND g_mainWindowHandle;
std::condition_variable g_timerConditional;
std::mutex g_timerLock;
std::atomicg_turnIsOver;
std::atomicg_blackPlayerNanosecondsLeft;
std::atomicg_whitePlayerNanosecondsLeft;void turnTimer(std::atomic*activePlayerNanosecondsLeft,
Rect*displayRect)
{
std::chrono::nanoseconds currentNanosecondsLeft{ activePlayerNanosecondsLeft->load() };
std::chrono::nanoseconds nanosecondsToNextSecond{ currentNanosecondsLeft % 1000000000 };
std::chrono::steady_clock::time_point nextSecondBoundary{
std::chrono::steady_clock::now() + nanosecondsToNextSecond };
std::unique_locklock(g_timerLock);
std::cv_status status{
g_timerConditional.std::condition_variable::wait_until(lock, nextSecondBoundary) };
while (!g_turnIsOver.load())
{
if (status == std::cv_status::timeout)
{
currentNanosecondsLeft -= nanosecondsToNextSecond;
*activePlayerNanosecondsLeft = currentNanosecondsLeft;
nanosecondsToNextSecond = std::chrono::nanoseconds(1000000000);
nextSecondBoundary += nanosecondsToNextSecond;
InvalidateRect(g_mainWindowHandle, displayRect, FALSE);
}
status = g_timerConditional.std::condition_variable::wait_until(lock, nextSecondBoundary);
}
currentNanosecondsLeft -=
std::chrono::steady_clock::now() - (nextSecondBoundary - nanosecondsToNextSecond);
*activePlayerNanosecondsLeft = currentNanosecondsLeft;
g_turnIsOver = false;
InvalidateRect(g_mainWindowHandle, displayRect, FALSE);
}Each time the main thread moves a piece, it sets g_
I would use a different approach: 1. Have an event queue (practically - a single element) that contains pairs - 2. When a player makes a move, insert a pair into the queue 3. Have a timer thread that updates the clock display every milliseconds 4. Before updating the clock, it: 4.1 Reads the current time using the same clock used by the player threads 4.2 Checks the event queue to see if a move was made, and when it was made 4.3 If a move was made, it updates the players' times, stops one clock, and starts the other There will always be an unavoidable lag in the visual display of the clock because of the O/S's scheduling requirements, but the internal counts will be accurate.
Freedom is the freedom to say that two plus two make four. If that is granted, all else follows. -- 6079 Smith W.
-
I would use a different approach: 1. Have an event queue (practically - a single element) that contains pairs - 2. When a player makes a move, insert a pair into the queue 3. Have a timer thread that updates the clock display every milliseconds 4. Before updating the clock, it: 4.1 Reads the current time using the same clock used by the player threads 4.2 Checks the event queue to see if a move was made, and when it was made 4.3 If a move was made, it updates the players' times, stops one clock, and starts the other There will always be an unavoidable lag in the visual display of the clock because of the O/S's scheduling requirements, but the internal counts will be accurate.
Freedom is the freedom to say that two plus two make four. If that is granted, all else follows. -- 6079 Smith W.
Having a timer activate every millisecond involves using something like sleep_for, right? Is that less susceptible to delays than wait_until is? Either way, it does certainly having the advantage that any timer-related delays won't block the main thread.
-
Having a timer activate every millisecond involves using something like sleep_for, right? Is that less susceptible to delays than wait_until is? Either way, it does certainly having the advantage that any timer-related delays won't block the main thread.
Hi, I doubt your condition_variable::wait_until code is causing the redraw delay based on what you've shown. Invalidating the rect simply creates an update region and then marks an internal 'dirty bit' associated with the window. The operating system is somewhat lazy updating the window... it could be redrawn a few milliseconds later... or several seconds later depending on how many messages are in the message queue. If you want an immediate redraw add a call to the [UpdateWindow function](https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-updatewindow) immediately following your invalidation. Best Wishes, -David Delaune
-
Hi, I doubt your condition_variable::wait_until code is causing the redraw delay based on what you've shown. Invalidating the rect simply creates an update region and then marks an internal 'dirty bit' associated with the window. The operating system is somewhat lazy updating the window... it could be redrawn a few milliseconds later... or several seconds later depending on how many messages are in the message queue. If you want an immediate redraw add a call to the [UpdateWindow function](https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-updatewindow) immediately following your invalidation. Best Wishes, -David Delaune
While I must admit that I don't know how to gracefully debug the timer, I'm pretty sure the join call is producing at least some of the noticeable delays. I tried just adding an UpdateWindow call after the InvalidateRect call in turnTimer as a simple test, but that deadlocked the program, which led me to read about how it's flatly incorrect to do a lot of kinds of window manipulations from another thread. I'm not sure if InvalidateRect is one of them and I just kept getting lucky with the original implementation, which also called it from the timer thread, but I suppose the fact that I'm not sure is enough reason to switch to something like Daniel described even if I weren't getting the lags. I'll try it and see what happens.
-
While I must admit that I don't know how to gracefully debug the timer, I'm pretty sure the join call is producing at least some of the noticeable delays. I tried just adding an UpdateWindow call after the InvalidateRect call in turnTimer as a simple test, but that deadlocked the program, which led me to read about how it's flatly incorrect to do a lot of kinds of window manipulations from another thread. I'm not sure if InvalidateRect is one of them and I just kept getting lucky with the original implementation, which also called it from the timer thread, but I suppose the fact that I'm not sure is enough reason to switch to something like Daniel described even if I weren't getting the lags. I'll try it and see what happens.
Hi, Yep, Daniel gave you some great advice. You don't really need a second thread. My assessment is the same... InvalidateRect does not redraw the window, it simply marks it as dirty. Your main source of visual delay is most likely waiting for a WM_PAINT message. If you use the event driven model Daniel suggested maybe you could add a redraw event handler that invalidates and redraws the window. Best Wishes, -David Delaune
-
I would use a different approach: 1. Have an event queue (practically - a single element) that contains pairs - 2. When a player makes a move, insert a pair into the queue 3. Have a timer thread that updates the clock display every milliseconds 4. Before updating the clock, it: 4.1 Reads the current time using the same clock used by the player threads 4.2 Checks the event queue to see if a move was made, and when it was made 4.3 If a move was made, it updates the players' times, stops one clock, and starts the other There will always be an unavoidable lag in the visual display of the clock because of the O/S's scheduling requirements, but the internal counts will be accurate.
Freedom is the freedom to say that two plus two make four. If that is granted, all else follows. -- 6079 Smith W.
I've implemented this approach, and it's working for me. It proved to be easier to factor for multi-platform support than what I had before would have been, too.
-
I've implemented this approach, and it's working for me. It proved to be easier to factor for multi-platform support than what I had before would have been, too.
I'm happy to have helped.
Freedom is the freedom to say that two plus two make four. If that is granted, all else follows. -- 6079 Smith W.