HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
loop_win32_impl.hpp
1// Copyright Take Vos 2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
37#include "../win32_headers.hpp"
38
39#include "loop_intf.hpp"
40#include "../telemetry/telemetry.hpp"
41#include "../utility/utility.hpp"
42#include "../macros.hpp"
43#include <vector>
44#include <utility>
45#include <stop_token>
46#include <thread>
47#include <chrono>
48
49hi_export_module(hikogui.dispatch.loop : impl);
50
51namespace hi::inline v1 {
52
54public:
56 {
57 // Create an level trigger event, to use as an on/off switch.
58 if (auto handle = CreateEventW(NULL, TRUE, TRUE, NULL)) {
59 _use_vsync_handle = handle;
60 } else {
61 hi_log_fatal("Could not create an use-vsync handle. {}", get_last_error_message());
62 }
63
64 // Create a pulse trigger event.
65 if (auto handle = CreateEventW(NULL, FALSE, FALSE, NULL)) {
66 _handles.push_back(handle);
67 _sockets.push_back(-1);
68 _socket_functions.emplace_back();
69 } else {
70 hi_log_fatal("Could not create an vsync-event handle. {}", get_last_error_message());
71 }
72
73 // Create a pulse trigger event.
74 if (auto handle = CreateEventW(NULL, FALSE, FALSE, NULL)) {
75 _handles.push_back(handle);
76 _sockets.push_back(-1);
77 _socket_functions.emplace_back();
78 } else {
79 hi_log_fatal("Could not create an async-event handle. {}", get_last_error_message());
80 }
81 }
82
84 {
85 // Close all socket event handles.
86 while (_handles.size() > _socket_handle_idx) {
87 if (not WSACloseEvent(_handles.back())) {
88 hi_log_error("Could not clock socket event handle for socket {}. {}", _sockets.back(), get_last_error_message());
89 }
90
91 _handles.pop_back();
92 _sockets.pop_back();
93 }
94
95 if (_vsync_thread.joinable()) {
96 _vsync_thread.request_stop();
97 _vsync_thread.join();
98 }
99
100 if (not CloseHandle(_handles[_function_handle_idx])) {
101 hi_log_error("Could not close async-event handle. {}", get_last_error_message());
102 }
103 if (not CloseHandle(_handles[_vsync_handle_idx])) {
104 hi_log_error("Could not close vsync-event handle. {}", get_last_error_message());
105 }
106 if (not CloseHandle(_use_vsync_handle)) {
107 hi_log_error("Could not close use-vsync handle. {}", get_last_error_message());
108 }
109 }
110
111 void set_maximum_frame_rate(double frame_rate) noexcept override
112 {
113 hi_axiom(on_thread());
114 }
115
116 void set_vsync_monitor_id(uintptr_t id) noexcept override
117 {
118 _selected_monitor_id.store(id, std::memory_order::relaxed);
119 }
120
121 void subscribe_render(std::weak_ptr<loop::render_callback_type> f) noexcept override
122 {
123 hi_axiom(on_thread());
124 _render_functions.push_back(std::move(f));
125
126 // Startup the vsync thread once there is a window.
127 if (not _vsync_thread.joinable()) {
128 _vsync_thread = std::jthread{[this](std::stop_token token) {
129 return vsync_thread_proc(std::move(token));
130 }};
131 }
132 }
133
134 void add_socket(int fd, socket_event event_mask, std::function<void(int, socket_events const&)> f) override
135 {
136 hi_axiom(on_thread());
137 hi_not_implemented();
138 }
139
140 void remove_socket(int fd) override
141 {
142 hi_axiom(on_thread());
143 hi_not_implemented();
144 }
145
146 int resume(std::stop_token stop_token) noexcept override
147 {
148 // Microsoft recommends an event-loop that also renders to the screen to run at above normal priority.
149 hilet thread_handle = GetCurrentThread();
150
151 int original_thread_priority = GetThreadPriority(thread_handle);
152 if (original_thread_priority == THREAD_PRIORITY_ERROR_RETURN) {
153 original_thread_priority = THREAD_PRIORITY_NORMAL;
154 hi_log_error("GetThreadPriority() for loop failed {}", get_last_error_message());
155 }
156
157 if (is_main and original_thread_priority < THREAD_PRIORITY_ABOVE_NORMAL) {
158 if (not SetThreadPriority(thread_handle, THREAD_PRIORITY_ABOVE_NORMAL)) {
159 hi_log_error("SetThreadPriority() for loop failed {}", get_last_error_message());
160 }
161 }
162
163 _exit_code = {};
164 while (not _exit_code) {
165 resume_once(true);
166
167 if (stop_token.stop_possible()) {
168 if (stop_token.stop_requested()) {
169 // Stop immediately when stop is requested.
170 _exit_code = 0;
171 }
172 } else {
173 if (_render_functions.empty() and _function_fifo.empty() and _function_timer.empty() and
174 _handles.size() <= _socket_handle_idx) {
175 // If there is not stop token, then exit when there are no more resources to wait on.
176 _exit_code = 0;
177 }
178 }
179 }
180
181 // Set the thread priority back to what is was before resume().
182 if (is_main and original_thread_priority < THREAD_PRIORITY_ABOVE_NORMAL) {
183 if (not SetThreadPriority(thread_handle, original_thread_priority)) {
184 hi_log_error("SetThreadPriority() for loop failed {}", get_last_error_message());
185 }
186 }
187
188 return *_exit_code;
189 }
190
191 void resume_once(bool block) noexcept override
192 {
193 using namespace std::chrono_literals;
194
195 hi_axiom(on_thread());
196
197 auto current_time = std::chrono::utc_clock::now();
198 auto timeout = std::chrono::duration_cast<std::chrono::milliseconds>(_function_timer.current_deadline() - current_time);
199
200 timeout = std::clamp(timeout, 0ms, 100ms);
201 hilet timeout_ms = narrow_cast<DWORD>(timeout / 1ms);
202
203 // Only handle win32 messages when blocking.
204 // Since non-blocking is called from the win32 message-pump, we do not want to re-enter the loop.
205 hilet message_mask = is_main and block ? QS_ALLINPUT : 0;
206
207 hilet wait_r =
208 MsgWaitForMultipleObjects(narrow_cast<DWORD>(_handles.size()), _handles.data(), FALSE, timeout_ms, message_mask);
209
210 if (wait_r == WAIT_FAILED) {
211 hi_log_fatal("Failed on MsgWaitForMultipleObjects(), {}", get_last_error_message());
212
213 } else if (wait_r == WAIT_TIMEOUT) {
214 // handle_functions() and handle_timers() is called after every wake-up of MsgWaitForMultipleObjects
215 ;
216
217 } else if (wait_r == WAIT_OBJECT_0 + _vsync_handle_idx) {
218 // XXX Make sure this is not starving the win32 events.
219 // should we just empty the win32 events after every unblock?
220 handle_vsync();
221
222 } else if (wait_r == WAIT_OBJECT_0 + _function_handle_idx) {
223 // handle_functions() and handle_timers() is called after every wake-up of MsgWaitForMultipleObjects
224 ;
225
226 } else if (wait_r >= WAIT_OBJECT_0 + _socket_handle_idx and wait_r < WAIT_OBJECT_0 + _handles.size()) {
227 hilet index = wait_r - WAIT_OBJECT_0;
228
229 WSANETWORKEVENTS events;
230 if (WSAEnumNetworkEvents(_sockets[index], _handles[index], &events) != 0) {
231 switch (WSAGetLastError()) {
232 case WSANOTINITIALISED:
233 hi_log_fatal("WSAStartup was not called.");
234 case WSAENETDOWN:
235 hi_log_fatal("The network subsystem has failed.");
236 case WSAEINVAL:
237 hi_log_fatal("One of the specified parameters was invalid.");
238 case WSAEINPROGRESS:
239 hi_log_warning(
240 "A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a "
241 "callback "
242 "function.");
243 break;
244 case WSAEFAULT:
245 hi_log_fatal("The lpNetworkEvents parameter is not a valid part of the user address space.");
246 case WSAENOTSOCK:
247 // If somehow the socket was destroyed, lets just remove it.
248 hi_log_error("Error during WSAEnumNetworkEvents on socket {}: {}", _sockets[index], get_last_error_message());
249 _handles.erase(_handles.begin() + index);
250 _sockets.erase(_sockets.begin() + index);
251 _socket_functions.erase(_socket_functions.begin() + index);
252 break;
253 default:
254 hi_no_default();
255 }
256
257 } else {
258 // Because of how WSAEnumNetworkEvents() work we must only handle this specific socket.
259 _socket_functions[index](_sockets[index], socket_events_from_win32(events));
260 }
261
262 } else if (wait_r == WAIT_OBJECT_0 + _handles.size()) {
263 handle_gui_events();
264
265 } else if (wait_r >= WAIT_ABANDONED_0 and wait_r < WAIT_ABANDONED_0 + _handles.size()) {
266 hilet index = wait_r - WAIT_ABANDONED_0;
267 if (index == _vsync_handle_idx) {
268 hi_log_fatal("The vsync-handle has been abandoned.");
269
270 } else if (index == _function_handle_idx) {
271 hi_log_fatal("The async-handle has been abandoned.");
272
273 } else {
274 // Socket handle has been abandoned. Remove it from the handles.
275 hi_log_error("The socket-handle for socket {} has been abandoned.", _sockets[index]);
276 _handles.erase(_handles.begin() + index);
277 _sockets.erase(_sockets.begin() + index);
278 _socket_functions.erase(_socket_functions.begin() + index);
279 }
280
281 } else {
282 hi_no_default();
283 }
284
285 // Make sure timers are handled first, possibly they are time critical.
286 handle_timers();
287
288 // When functions are added wait-free, the function-event is never triggered.
289 // So handle messages after any kind of wake up.
290 handle_functions();
291 }
292
293private:
294 struct socket_type {
295 int fd;
296 socket_event mode;
297 std::function<void(int, socket_events const&)> callback;
298 };
299
300 constexpr static size_t _vsync_handle_idx = 0;
301 constexpr static size_t _function_handle_idx = 1;
302 constexpr static size_t _socket_handle_idx = 2;
303
311 HANDLE _use_vsync_handle;
312
316
319 bool _vsync_time_from_sleep = true;
320
325 std::atomic<uint16_t> _pull_down = 0x100;
326
331 uint64_t _sub_frame_count = 0;
332
337 uint64_t _frame_count = 0;
338
347 std::vector<HANDLE> _handles;
348
355 std::vector<int> _sockets;
356
359 std::vector<std::function<void(int, socket_events const&)>> _socket_functions;
360
363 std::jthread _vsync_thread;
364
367 HANDLE _vsync_thread_handle;
368
371 int _vsync_thread_priority = THREAD_PRIORITY_NORMAL;
372
375 std::atomic<std::uintptr_t> _selected_monitor_id = 0;
376
380 std::uintptr_t _vsync_monitor_id = 0;
381
384 IDXGIOutput *_vsync_monitor_output = nullptr;
385
386 void notify_has_send() noexcept override
387 {
388 if (not SetEvent(_handles[_function_handle_idx])) {
389 hi_log_error("Could not trigger async-event. {}", get_last_error_message());
390 }
391 }
392
397 void handle_vsync() noexcept
398 {
399 // XXX Reduce the number of redraws for each window based on the refresh rate of the monitor they are located on.
400 // XXX handle maximum frame rate and update vsync thread
401 // XXX Update active windows more often than inactive windows.
402
403 if (not _vsync_thread.joinable()) {
404 // Fallback for the vsync_time advancing when the vsync thread is not running.
405 _vsync_time.store(std::chrono::utc_clock::now());
406 }
407
408 hilet display_time = _vsync_time.load(std::memory_order::relaxed) + std::chrono::milliseconds(30);
409
410 for (auto& render_function : _render_functions) {
411 if (auto render_function_ = render_function.lock()) {
412 (*render_function_)(display_time);
413 }
414 }
415
416 std::erase_if(_render_functions, [](auto& render_function) {
417 return render_function.expired();
418 });
419
420 if (_render_functions.empty()) {
421 // Stop the vsync thread when there are no more windows.
422 if (_vsync_thread.joinable()) {
423 _vsync_thread.request_stop();
424 }
425 }
426 }
427
432 void handle_functions() noexcept
433 {
434 _function_fifo.run_all();
435 }
436
437 void handle_timers() noexcept
438 {
439 _function_timer.run_all(std::chrono::utc_clock::now());
440 }
441
446 void handle_gui_events() noexcept
447 {
448 MSG msg = {};
449 hilet t1 = trace<"loop:gui-events">();
450 while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE | PM_NOYIELD)) {
451 hilet t2 = trace<"loop:gui-event">();
452
453 if (msg.message == WM_QUIT) {
454 _exit_code = narrow_cast<int>(msg.wParam);
455 continue;
456 }
457
458 TranslateMessage(&msg);
459 DispatchMessageW(&msg);
460 }
461 }
462
467 void vsync_thread_update_dxgi_output() noexcept
468 {
469 if (not compare_store(_vsync_monitor_id, _selected_monitor_id.load(std::memory_order::relaxed))) {
470 return;
471 }
472
473 if (_vsync_monitor_output) {
474 _vsync_monitor_output->Release();
475 _vsync_monitor_output = nullptr;
476 }
477
478 IDXGIFactory *factory = nullptr;
479 if (FAILED(CreateDXGIFactory(__uuidof(IDXGIFactory), (void **)&factory))) {
480 hi_log_error_once("vsync:error:CreateDXGIFactory", "Could not IDXGIFactory. {}", get_last_error_message());
481 return;
482 }
483 hi_assert_not_null(factory);
484 auto d1 = defer([&] {
485 factory->Release();
486 });
487
488 IDXGIAdapter *adapter = nullptr;
489 if (FAILED(factory->EnumAdapters(0, &adapter))) {
490 hi_log_error_once("vsync:error:EnumAdapters", "Could not get IDXGIAdapter. {}", get_last_error_message());
491 return;
492 }
493 hi_assert_not_null(adapter);
494 auto d2 = defer([&] {
495 adapter->Release();
496 });
497
498 if (FAILED(adapter->EnumOutputs(0, &_vsync_monitor_output))) {
499 hi_log_error_once("vsync:error:EnumOutputs", "Could not get IDXGIOutput. {}", get_last_error_message());
500 return;
501 }
502
503 DXGI_OUTPUT_DESC description;
504 if (FAILED(_vsync_monitor_output->GetDesc(&description))) {
505 hi_log_error_once("vsync:error:GetDesc", "Could not get IDXGIOutput description. {}", get_last_error_message());
506 _vsync_monitor_output->Release();
507 _vsync_monitor_output = nullptr;
508 return;
509 }
510
511 if (description.Monitor != std::bit_cast<HMONITOR>(_vsync_monitor_id)) {
512 hi_log_error_once("vsync:error:not-primary-monitor", "DXGI primary monitor does not match desktop primary monitor");
513 _vsync_monitor_output->Release();
514 _vsync_monitor_output = nullptr;
515 return;
516 }
517
518 d2.cancel();
519 d1.cancel();
520 }
521
531 std::chrono::nanoseconds vsync_thread_update_time(bool on_sleep)
532 {
534 hilet new_time = time_stamp_utc::make(ts);
535
536 hilet was_sleeping = std::exchange(_vsync_time_from_sleep, on_sleep);
537 hilet old_time = _vsync_time.exchange(new_time, std::memory_order::acquire);
538
539 // If old_time was caused by sleeping it can not be used to calculate how long vsync was blocking.
540 return was_sleeping ? std::chrono::nanoseconds::max() : new_time - old_time;
541 }
542
543 void vsync_thread_wait_for_vblank() noexcept
544 {
545 using namespace std::chrono_literals;
546
547 vsync_thread_update_dxgi_output();
548
549 if (_vsync_monitor_output and FAILED(_vsync_monitor_output->WaitForVBlank())) {
550 hi_log_error_once("vsync:error:WaitForVBlank", "WaitForVBlank() failed. {}", get_last_error_message());
551 }
552
553 if (vsync_thread_update_time(false) < 1ms) {
554 hi_log_info_once("vsync:monitor-off", "WaitForVBlank() did not block; is the monitor turned off?");
555 Sleep(16);
556
557 // Fixup the time after the fallback sleep.
558 vsync_thread_update_time(true);
559 } else {
560 ++global_counter<"vsync:vertical-blank">;
561 }
562 }
563
571 [[nodiscard]] bool vsync_thread_pull_down() noexcept
572 {
573 _sub_frame_count += _pull_down.load(std::memory_order::relaxed);
574 return compare_store(_frame_count, _sub_frame_count >> 8);
575 }
576
582 void vsync_thread_update_priority(int new_priority) noexcept
583 {
584 if (std::exchange(_vsync_thread_priority, new_priority) != new_priority) {
585 if (not SetThreadPriority(_vsync_thread_handle, new_priority)) {
586 hi_log_error_once("vsync:error:SetThreadPriority", "Could not set the vsync thread priority to {}", new_priority);
587 }
588 }
589 }
590
591 void vsync_thread_proc(std::stop_token stop_token) noexcept
592 {
593 _vsync_thread_handle = GetCurrentThread();
594 set_thread_name("vsync");
595
596 while (not stop_token.stop_requested()) {
597 switch (WaitForSingleObject(_use_vsync_handle, 30)) {
598 case WAIT_TIMEOUT:
599 // When use_vsync is off wake the main loop every 30ms.
600 vsync_thread_update_time(true);
601
602 vsync_thread_update_priority(THREAD_PRIORITY_NORMAL);
603
604 ++global_counter<"vsync:low-priority">;
605 ++global_counter<"vsync:frame">;
606 SetEvent(_handles[_vsync_handle_idx]);
607 break;
608
609 case WAIT_OBJECT_0:
610 // When use_vsync is on wake the main loop based on the vertical-sync and pull_down.
611 vsync_thread_update_priority(THREAD_PRIORITY_TIME_CRITICAL);
612
613 vsync_thread_wait_for_vblank();
614
615 if (vsync_thread_pull_down()) {
616 ++global_counter<"vsync:frame">;
617 SetEvent(_handles[_vsync_handle_idx]);
618 }
619
620 break;
621
622 case WAIT_ABANDONED:
623 hi_log_error_once("vsync:error:WAIT_ABANDONED", "use_vsync_handle has been abandoned.");
624 ResetEvent(_use_vsync_handle);
625 break;
626
627 case WAIT_FAILED:
628 hi_log_error_once("vsync:error:WAIT_FAILED", "WaitForSingleObject failed. {}", get_last_error_message());
629 ResetEvent(_use_vsync_handle);
630 break;
631 }
632 }
633 }
634};
635
636inline loop::loop() : _pimpl(std::make_unique<loop_impl_win32>()) {}
637
638} // namespace hi::inline v1
Rules for working with win32 headers.
STL namespace.
DOXYGEN BUG.
Definition algorithm.hpp:16
constexpr Out narrow_cast(In const &rhs) noexcept
Cast numeric values without loss of precision.
Definition cast.hpp:377
Definition loop_intf.hpp:28
Definition loop_win32_impl.hpp:53
Definition socket_event_intf.hpp:74
Definition trace.hpp:42
Since Window's 10 QueryPerformanceCounter() counts at only 10MHz which is too low to measure performa...
Definition time_stamp_count.hpp:31
Definition time_stamp_count.hpp:34
T exchange(T... args)
T load(T... args)
T move(T... args)
T store(T... args)