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(), &currentDisplayMode);
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