1 /*
2 Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14
15 #include "video.hpp"
16
17 #include "display.hpp"
18 #include "floating_label.hpp"
19 #include "font/sdl_ttf.hpp"
20 #include "picture.hpp"
21 #include "log.hpp"
22 #include "preferences/general.hpp"
23 #include "sdl/point.hpp"
24 #include "sdl/userevent.hpp"
25 #include "sdl/utils.hpp"
26 #include "sdl/window.hpp"
27
28 #include <cassert>
29 #include <vector>
30
31 static lg::log_domain log_display("display");
32 #define LOG_DP LOG_STREAM(info, log_display)
33 #define ERR_DP LOG_STREAM(err, log_display)
34
35 #define MAGIC_DPI_SCALE_NUMBER 96
36
37 CVideo* CVideo::singleton_ = nullptr;
38
39 namespace
40 {
41 surface frameBuffer = nullptr;
42 bool fake_interactive = false;
43 }
44
45 namespace video2
46 {
47 std::list<events::sdl_handler*> draw_layers;
48
draw_layering(const bool auto_join)49 draw_layering::draw_layering(const bool auto_join)
50 : sdl_handler(auto_join)
51 {
52 draw_layers.push_back(this);
53 }
54
~draw_layering()55 draw_layering::~draw_layering()
56 {
57 draw_layers.remove(this);
58
59 video2::trigger_full_redraw();
60 }
61
trigger_full_redraw()62 void trigger_full_redraw()
63 {
64 SDL_Event event;
65 event.type = SDL_WINDOWEVENT;
66 event.window.event = SDL_WINDOWEVENT_RESIZED;
67 event.window.data1 = (*frameBuffer).h;
68 event.window.data2 = (*frameBuffer).w;
69
70 for(const auto& layer : draw_layers) {
71 layer->handle_window_event(event);
72 }
73
74 SDL_Event drawEvent;
75 sdl::UserEvent data(DRAW_ALL_EVENT);
76
77 drawEvent.type = DRAW_ALL_EVENT;
78 drawEvent.user = data;
79 SDL_FlushEvent(DRAW_ALL_EVENT);
80 SDL_PushEvent(&drawEvent);
81 }
82
83 } // video2
84
CVideo(FAKE_TYPES type)85 CVideo::CVideo(FAKE_TYPES type)
86 : window()
87 , fake_screen_(false)
88 , help_string_(0)
89 , updated_locked_(0)
90 , flip_locked_(0)
91 , refresh_rate_(0)
92 {
93 assert(!singleton_);
94 singleton_ = this;
95
96 initSDL();
97
98 switch(type) {
99 case NO_FAKE:
100 break;
101 case FAKE:
102 make_fake();
103 break;
104 case FAKE_TEST:
105 make_test_fake();
106 break;
107 }
108 }
109
initSDL()110 void CVideo::initSDL()
111 {
112 const int res = SDL_InitSubSystem(SDL_INIT_VIDEO);
113
114 if(res < 0) {
115 ERR_DP << "Could not initialize SDL_video: " << SDL_GetError() << std::endl;
116 throw CVideo::error();
117 }
118 }
119
~CVideo()120 CVideo::~CVideo()
121 {
122 if(sdl_get_version() >= version_info(2, 0, 6)) {
123 // Because SDL will free the framebuffer,
124 // ensure that we won't attempt to free it.
125 frameBuffer.clear_without_free();
126 }
127
128 LOG_DP << "calling SDL_Quit()\n";
129 SDL_Quit();
130 assert(singleton_);
131 singleton_ = nullptr;
132 LOG_DP << "called SDL_Quit()\n";
133 }
134
video_settings_report()135 std::string CVideo::video_settings_report()
136 {
137 if (singleton_ == nullptr) return "No video initialized.\n";
138 if (singleton_->non_interactive())
139 return "Initialized but non-interactive.\n";
140 sdl::window* win = singleton_->get_window();
141 if (!win) return "Interactive but no SDL window.\n";
142 std::ostringstream o;
143 o << "Current pixel resolution: "
144 << singleton_->get_width() << "x" << singleton_->get_height()
145 << '\n'
146 << "Refresh rate: " << singleton_->current_refresh_rate()
147 << '\n';
148 float hdpi, vdpi;
149 int returncode = SDL_GetDisplayDPI(win->get_display_index(),
150 nullptr, &hdpi, &vdpi);
151 if (returncode != 0) {
152 o << "SDL not supplying dots per inch.\n";
153 } else {
154 o << "SDL reports: " << hdpi << "x" << vdpi
155 << " dots per inch.\n";
156 }
157 return o.str();
158 }
159
non_interactive() const160 bool CVideo::non_interactive() const
161 {
162 return fake_interactive ? false : (window == nullptr);
163 }
164
handle_window_event(const SDL_Event & event)165 void CVideo::video_event_handler::handle_window_event(const SDL_Event& event)
166 {
167 if(event.type == SDL_WINDOWEVENT) {
168 switch(event.window.event) {
169 case SDL_WINDOWEVENT_RESIZED:
170 case SDL_WINDOWEVENT_RESTORED:
171 case SDL_WINDOWEVENT_SHOWN:
172 case SDL_WINDOWEVENT_EXPOSED:
173 // if(display::get_singleton())
174 // display::get_singleton()->redraw_everything();
175 SDL_Event drawEvent;
176 sdl::UserEvent data(DRAW_ALL_EVENT);
177
178 drawEvent.type = DRAW_ALL_EVENT;
179 drawEvent.user = data;
180
181 SDL_FlushEvent(DRAW_ALL_EVENT);
182 SDL_PushEvent(&drawEvent);
183 break;
184 }
185 }
186 }
187
blit_surface(int x,int y,surface surf,SDL_Rect * srcrect,SDL_Rect * clip_rect)188 void CVideo::blit_surface(int x, int y, surface surf, SDL_Rect* srcrect, SDL_Rect* clip_rect)
189 {
190 surface& target(getSurface());
191 SDL_Rect dst{x, y, 0, 0};
192
193 const clip_rect_setter clip_setter(target, clip_rect, clip_rect != nullptr);
194 sdl_blit(surf, srcrect, target, &dst);
195 }
196
make_fake()197 void CVideo::make_fake()
198 {
199 fake_screen_ = true;
200 refresh_rate_ = 1;
201
202 #if SDL_VERSION_ATLEAST(2, 0, 6)
203 frameBuffer = SDL_CreateRGBSurfaceWithFormat(0, 16, 16, 24, SDL_PIXELFORMAT_BGR888);
204 #else
205 frameBuffer = SDL_CreateRGBSurface(0, 16, 16, 24, 0xFF0000, 0xFF00, 0xFF, 0);
206 #endif
207 }
208
make_test_fake(const unsigned width,const unsigned height)209 void CVideo::make_test_fake(const unsigned width, const unsigned height)
210 {
211 #if SDL_VERSION_ATLEAST(2, 0, 6)
212 frameBuffer = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_BGR888);
213 #else
214 frameBuffer = SDL_CreateRGBSurface(0, width, height, 32, 0xFF0000, 0xFF00, 0xFF, 0);
215 #endif
216
217 fake_interactive = true;
218 refresh_rate_ = 1;
219 }
220
update_framebuffer()221 void CVideo::update_framebuffer()
222 {
223 if(!window) {
224 return;
225 }
226
227 surface fb = SDL_GetWindowSurface(*window);
228
229 if(frameBuffer && sdl_get_version() >= version_info(2, 0, 6)) {
230 // Because SDL has already freed the old framebuffer,
231 // ensure that we won't attempt to free it.
232 frameBuffer.clear_without_free();
233 }
234
235 frameBuffer = fb;
236 }
237
init_window()238 void CVideo::init_window()
239 {
240 // Position
241 const int x = preferences::fullscreen() ? SDL_WINDOWPOS_UNDEFINED : SDL_WINDOWPOS_CENTERED;
242 const int y = preferences::fullscreen() ? SDL_WINDOWPOS_UNDEFINED : SDL_WINDOWPOS_CENTERED;
243
244 // Dimensions
245 const point res = preferences::resolution();
246 const int w = res.x;
247 const int h = res.y;
248
249 uint32_t window_flags = 0;
250
251 // Add any more default flags here
252 window_flags |= SDL_WINDOW_RESIZABLE;
253 window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
254
255 if(preferences::fullscreen()) {
256 window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
257 } else if(preferences::maximized()) {
258 window_flags |= SDL_WINDOW_MAXIMIZED;
259 }
260
261 // Initialize window
262 window.reset(new sdl::window("", x, y, w, h, window_flags, SDL_RENDERER_SOFTWARE));
263
264 std::cerr << "Setting mode to " << w << "x" << h << std::endl;
265
266 window->set_minimum_size(preferences::min_window_width, preferences::min_window_height);
267
268 SDL_DisplayMode currentDisplayMode;
269 SDL_GetCurrentDisplayMode(window->get_display_index(), ¤tDisplayMode);
270 refresh_rate_ = currentDisplayMode.refresh_rate != 0 ? currentDisplayMode.refresh_rate : 60;
271
272 event_handler_.join_global();
273
274 update_framebuffer();
275 }
276
set_window_mode(const MODE_EVENT mode,const point & size)277 void CVideo::set_window_mode(const MODE_EVENT mode, const point& size)
278 {
279 assert(window);
280 if(fake_screen_) {
281 return;
282 }
283
284 switch(mode) {
285 case TO_FULLSCREEN:
286 window->full_screen();
287 break;
288
289 case TO_WINDOWED:
290 window->to_window();
291 window->restore();
292 break;
293
294 case TO_MAXIMIZED_WINDOW:
295 window->to_window();
296 window->maximize();
297 break;
298
299 case TO_RES:
300 window->restore();
301 window->set_size(size.x, size.y);
302 window->center();
303 break;
304 }
305
306 update_framebuffer();
307 }
308
screen_area(bool as_pixels) const309 SDL_Rect CVideo::screen_area(bool as_pixels) const
310 {
311 if(!window) {
312 return {0, 0, frameBuffer->w, frameBuffer->h};
313 }
314
315 // First, get the renderer size in pixels.
316 SDL_Point size = window->get_output_size();
317
318 // Then convert the dimensions into screen coordinates, if applicable.
319 if(!as_pixels) {
320 float scale_x, scale_y;
321 std::tie(scale_x, scale_y) = get_dpi_scale_factor();
322
323 size.x /= scale_x;
324 size.y /= scale_y;
325 }
326
327 return {0, 0, size.x, size.y};
328 }
329
get_width(bool as_pixels) const330 int CVideo::get_width(bool as_pixels) const
331 {
332 return screen_area(as_pixels).w;
333 }
334
get_height(bool as_pixels) const335 int CVideo::get_height(bool as_pixels) const
336 {
337 return screen_area(as_pixels).h;
338 }
339
delay(unsigned int milliseconds)340 void CVideo::delay(unsigned int milliseconds)
341 {
342 if(!game_config::no_delay) {
343 SDL_Delay(milliseconds);
344 }
345 }
346
flip()347 void CVideo::flip()
348 {
349 if(fake_screen_ || flip_locked_ > 0) {
350 return;
351 }
352
353 if(window) {
354 window->render();
355 }
356 }
357
lock_updates(bool value)358 void CVideo::lock_updates(bool value)
359 {
360 if(value == true) {
361 ++updated_locked_;
362 } else {
363 --updated_locked_;
364 }
365 }
366
update_locked() const367 bool CVideo::update_locked() const
368 {
369 return updated_locked_ > 0;
370 }
371
set_window_title(const std::string & title)372 void CVideo::set_window_title(const std::string& title)
373 {
374 assert(window);
375 window->set_title(title);
376 }
377
set_window_icon(surface & icon)378 void CVideo::set_window_icon(surface& icon)
379 {
380 assert(window);
381 window->set_icon(icon);
382 }
383
clear_screen()384 void CVideo::clear_screen()
385 {
386 if(!window) {
387 return;
388 }
389
390 window->fill(0, 0, 0, 255);
391 }
392
get_window()393 sdl::window* CVideo::get_window()
394 {
395 return window.get();
396 }
397
window_has_flags(uint32_t flags) const398 bool CVideo::window_has_flags(uint32_t flags) const
399 {
400 if(!window) {
401 return false;
402 }
403
404 return (window->get_flags() & flags) != 0;
405 }
406
get_dpi_scale_factor() const407 std::pair<float, float> CVideo::get_dpi_scale_factor() const
408 {
409 std::pair<float, float> result{1.0f, 1.0f};
410
411 if(!window) {
412 return result;
413 }
414
415 float hdpi, vdpi;
416 SDL_GetDisplayDPI(window->get_display_index(), nullptr, &hdpi, &vdpi);
417
418 result.first = hdpi / MAGIC_DPI_SCALE_NUMBER;
419 result.second = vdpi / MAGIC_DPI_SCALE_NUMBER;
420
421 return result;
422 }
423
get_available_resolutions(const bool include_current)424 std::vector<point> CVideo::get_available_resolutions(const bool include_current)
425 {
426 std::vector<point> result;
427
428 if(!window) {
429 return result;
430 }
431
432 const int display_index = window->get_display_index();
433
434 const int modes = SDL_GetNumDisplayModes(display_index);
435 if(modes <= 0) {
436 std::cerr << "No modes supported\n";
437 return result;
438 }
439
440 const point min_res(preferences::min_window_width, preferences::min_window_height);
441
442 #if 0
443 // DPI scale factor.
444 float scale_h, scale_v;
445 std::tie(scale_h, scale_v) = get_dpi_scale_factor();
446 #endif
447
448 // The maximum size to which this window can be set. For some reason this won't
449 // pop up as a display mode of its own.
450 SDL_Rect bounds;
451 SDL_GetDisplayBounds(display_index, &bounds);
452
453 SDL_DisplayMode mode;
454
455 for(int i = 0; i < modes; ++i) {
456 if(SDL_GetDisplayMode(display_index, i, &mode) == 0) {
457 // Exclude any results outside the range of the current DPI.
458 if(mode.w > bounds.w && mode.h > bounds.h) {
459 continue;
460 }
461
462 if(mode.w >= min_res.x && mode.h >= min_res.y) {
463 result.emplace_back(mode.w, mode.h);
464 }
465 }
466 }
467
468 if(std::find(result.begin(), result.end(), min_res) == result.end()) {
469 result.push_back(min_res);
470 }
471
472 if(include_current) {
473 result.push_back(current_resolution());
474 }
475
476 std::sort(result.begin(), result.end());
477 result.erase(std::unique(result.begin(), result.end()), result.end());
478
479 return result;
480 }
481
getSurface()482 surface& CVideo::getSurface()
483 {
484 return frameBuffer;
485 }
486
current_resolution()487 point CVideo::current_resolution()
488 {
489 return point(window->get_size()); // Convert from plain SDL_Point
490 }
491
is_fullscreen() const492 bool CVideo::is_fullscreen() const
493 {
494 return (window->get_flags() & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0;
495 }
496
set_help_string(const std::string & str)497 int CVideo::set_help_string(const std::string& str)
498 {
499 font::remove_floating_label(help_string_);
500
501 const color_t color{0, 0, 0, 0xbb};
502
503 int size = font::SIZE_LARGE;
504
505 while(size > 0) {
506 if(font::line_width(str, size) > get_width()) {
507 size--;
508 } else {
509 break;
510 }
511 }
512
513 const int border = 5;
514
515 font::floating_label flabel(str);
516 flabel.set_font_size(size);
517 flabel.set_position(get_width() / 2, get_height());
518 flabel.set_bg_color(color);
519 flabel.set_border_size(border);
520
521 help_string_ = font::add_floating_label(flabel);
522
523 const SDL_Rect& rect = font::get_floating_label_rect(help_string_);
524 font::move_floating_label(help_string_, 0.0, -double(rect.h));
525
526 return help_string_;
527 }
528
clear_help_string(int handle)529 void CVideo::clear_help_string(int handle)
530 {
531 if(handle == help_string_) {
532 font::remove_floating_label(handle);
533 help_string_ = 0;
534 }
535 }
536
clear_all_help_strings()537 void CVideo::clear_all_help_strings()
538 {
539 clear_help_string(help_string_);
540 }
541
set_fullscreen(bool ison)542 void CVideo::set_fullscreen(bool ison)
543 {
544 if(window && is_fullscreen() != ison) {
545 const point& res = preferences::resolution();
546
547 MODE_EVENT mode;
548
549 if(ison) {
550 mode = TO_FULLSCREEN;
551 } else {
552 mode = preferences::maximized() ? TO_MAXIMIZED_WINDOW : TO_WINDOWED;
553 }
554
555 set_window_mode(mode, res);
556
557 if(display* d = display::get_singleton()) {
558 d->redraw_everything();
559 }
560 }
561
562 // Change the config value.
563 preferences::_set_fullscreen(ison);
564 }
565
toggle_fullscreen()566 void CVideo::toggle_fullscreen()
567 {
568 set_fullscreen(!preferences::fullscreen());
569 }
570
set_resolution(const unsigned width,const unsigned height)571 bool CVideo::set_resolution(const unsigned width, const unsigned height)
572 {
573 return set_resolution(point(width, height));
574 }
575
set_resolution(const point & resolution)576 bool CVideo::set_resolution(const point& resolution)
577 {
578 if(resolution == current_resolution()) {
579 return false;
580 }
581
582 set_window_mode(TO_RES, resolution);
583
584 if(display* d = display::get_singleton()) {
585 d->redraw_everything();
586 }
587
588 // Change the saved values in preferences.
589 preferences::_set_resolution(resolution);
590 preferences::_set_maximized(false);
591
592 // Push a window-resized event to the queue. This is necessary so various areas
593 // of the game (like GUI2) update properly with the new size.
594 events::raise_resize_event();
595
596 return true;
597 }
598
lock_flips(bool lock)599 void CVideo::lock_flips(bool lock)
600 {
601 if(lock) {
602 ++flip_locked_;
603 } else {
604 --flip_locked_;
605 }
606 }
607