The Battle for Wesnoth  1.17.10+dev
draw_manager.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2022
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 "draw_manager.hpp"
16 
17 #include "draw.hpp"
18 #include "exceptions.hpp"
19 #include "log.hpp"
21 #include "preferences/general.hpp"
22 #include "sdl/rect.hpp"
23 #include "utils/general.hpp"
24 #include "video.hpp"
25 
26 #include <SDL2/SDL_rect.h>
27 #include <SDL2/SDL_timer.h>
28 
29 #include <algorithm>
30 #include <vector>
31 #include <map>
32 
33 static lg::log_domain log_draw_man("draw/manager");
34 #define ERR_DM LOG_STREAM(err, log_draw_man)
35 #define WRN_DM LOG_STREAM(warn, log_draw_man)
36 #define LOG_DM LOG_STREAM(info, log_draw_man)
37 #define DBG_DM LOG_STREAM(debug, log_draw_man)
38 
40 
41 // This is not publically exposed, because nobody else should be using it.
42 // Implementation is in video.cpp.
43 namespace video { void render_screen(); }
44 
45 namespace {
46 std::vector<top_level_drawable*> top_level_drawables_;
47 std::vector<rect> invalidated_regions_;
48 bool drawing_ = false;
49 bool tlds_need_tidying_ = false;
50 uint32_t last_sparkle_ = 0;
51 } // namespace
52 
53 namespace draw_manager {
54 
55 static void update();
56 static void layout();
57 static void render();
58 static bool expose();
59 static void wait_for_vsync();
60 static void tidy_drawables();
61 
62 void invalidate_region(const rect& region)
63 {
64  if (drawing_) {
65  ERR_DM << "Attempted to invalidate region " << region
66  << " during draw";
67  throw game::error("invalidate during draw");
68  }
69 
70  // On-add region optimization
71  rect progressive_cover = region;
72  int64_t cumulative_area = 0;
73  for (auto& r : invalidated_regions_) {
74  if (r.contains(region)) {
75  // An existing invalidated region already contains it,
76  // no need to do anything in this case.
77  //DBG_DM << "no need to invalidate " << region;
78  //STREAMING_LOG << '.';
79  return;
80  }
81  if (region.contains(r)) {
82  // This region contains a previously invalidated region,
83  // might as well supercede it with this.
84  DBG_DM << "superceding previous invalidation " << r
85  << " with " << region;
86  //STREAMING_LOG << '\'';
87  r = region;
88  return;
89  }
90  // maybe merge with another rect
91  rect m = r.minimal_cover(region);
92  if (m.area() <= r.area() + region.area()) {
93  // This won't always be the best,
94  // but it also won't ever be the worst.
95  DBG_DM << "merging " << region << " with " << r
96  << " to invalidate " << m;
97  //STREAMING_LOG << ':';
98  r = m;
99  return;
100  }
101  // maybe merge *all* the rects
102  progressive_cover.expand_to_cover(r);
103  cumulative_area += r.area();
104  if (progressive_cover.area() <= cumulative_area) {
105  DBG_DM << "conglomerating invalidations to "
106  << progressive_cover;
107  //STREAMING_LOG << '%';
108  // replace the first one, so we can easily prune later
109  invalidated_regions_[0] = progressive_cover;
110  return;
111  }
112  }
113 
114  // No optimization was found, so add a new invalidation
115  DBG_DM << "invalidating region " << region;
116  //STREAMING_LOG << '.';
117  invalidated_regions_.push_back(region);
118 }
119 
121 {
122  // Note: this does not support render targets other than the screen.
124 }
125 
126 void sparkle()
127 {
128  if (drawing_) {
129  ERR_DM << "Draw recursion detected";
130  throw game::error("recursive draw");
131  }
132 
133  // Remove any invalidated TLDs from previous iterations or events.
134  if (tlds_need_tidying_) {
135  tidy_drawables();
136  tlds_need_tidying_ = false;
137  }
138 
139  // Animate, process, and update state.
141 
142  // Ensure layout is up-to-date.
144 
145  // If we are running headless or executing unit tests, do not render.
146  // There are not currently any tests for actual rendering output.
147  if(video::headless() || video::testing()) {
148  invalidated_regions_.clear();
149  return;
150  }
151 
152  // Ensure any off-screen render buffers are up-to-date.
154 
155  // Draw to the screen.
156  if (draw_manager::expose()) {
157  // We only need to flip the screen if something was drawn.
159  } else {
160  wait_for_vsync();
161  }
162 
163  last_sparkle_ = SDL_GetTicks();
164 }
165 
167 {
168  int rr = video::current_refresh_rate();
169  if (rr <= 0) {
170  // make something up
171  rr = 60;
172  }
173  // allow 1ms for general processing
174  int vsync_delay = (1000 / rr) - 1;
175  // if there's a preferred limit, limit to that
176  return std::clamp(vsync_delay, preferences::draw_delay(), 1000);
177 }
178 
179 static void wait_for_vsync()
180 {
181  int time_to_wait = last_sparkle_ + get_frame_length() - SDL_GetTicks();
182  if (time_to_wait > 0) {
183  // delay a maximum of 1 second in case something crazy happens
184  SDL_Delay(std::min(time_to_wait, 1000));
185  }
186 }
187 
188 static void update()
189 {
190  for (size_t i = 0; i < top_level_drawables_.size(); ++i) {
191  top_level_drawable* tld = top_level_drawables_[i];
192  if (tld) { tld->update(); }
193  }
194 }
195 
196 static void layout()
197 {
198  for (size_t i = 0; i < top_level_drawables_.size(); ++i) {
199  top_level_drawable* tld = top_level_drawables_[i];
200  if (tld) { tld->layout(); }
201  }
202 }
203 
204 static void render()
205 {
206  for (size_t i = 0; i < top_level_drawables_.size(); ++i) {
207  top_level_drawable* tld = top_level_drawables_[i];
208  if (tld) { tld->render(); }
209  }
210 }
211 
212 static bool expose()
213 {
214  drawing_ = true;
215 
216  // For now just send all regions to all TLDs in the correct order.
217  bool drawn = false;
218 next:
219  while (!invalidated_regions_.empty()) {
220  rect r = invalidated_regions_.back();
221  invalidated_regions_.pop_back();
222  // check if this will be superceded by or should be merged with another
223  for (auto& other : invalidated_regions_) {
224  // r will never contain other, due to construction
225  if (other.contains(r)) {
226  DBG_DM << "skipping redundant draw " << r;
227  //STREAMING_LOG << "-";
228  goto next;
229  }
230  rect m = other.minimal_cover(r);
231  if (m.area() <= r.area() + other.area()) {
232  DBG_DM << "merging inefficient draws " << r;
233  //STREAMING_LOG << "=";
234  other = m;
235  goto next;
236  }
237  }
238  DBG_DM << "drawing " << r;
239  //STREAMING_LOG << "+";
240  auto clipper = draw::override_clip(r);
241  for (auto tld : top_level_drawables_) {
242  if (!tld) { continue; }
243  rect i = r.intersect(tld->screen_location());
244  if (i.empty()) {
245  //DBG_DM << " skip " << static_cast<void*>(tld);
246  //STREAMING_LOG << "x";
247  continue;
248  }
249  DBG_DM << " to " << static_cast<void*>(tld);
250  //STREAMING_LOG << "*";
251  try {
252  drawn |= tld->expose(i);
253  } catch(...) {
254  WRN_DM << "exception " << utils::get_unknown_exception_type()
255  << " thrown during expose " << static_cast<void*>(tld);
256  drawing_ = false;
257  throw;
258  }
259  }
260  }
261  drawing_ = false;
262  return drawn;
263 }
264 
265 // Note: This function ensures that multiple copies are not added.
266 // We can assume top_level_drawables_ will contain at most one of each TLD.
268 {
269  DBG_DM << "registering TLD " << static_cast<void*>(tld);
270  auto& vec = top_level_drawables_;
271  if (std::find(vec.begin(), vec.end(), tld) != vec.end()) {
272  raise_drawable(tld);
273  } else {
274  top_level_drawables_.push_back(tld);
275  }
276 }
277 
279 {
280  DBG_DM << "deregistering TLD " << static_cast<void*>(tld);
281  auto& vec = top_level_drawables_;
282  auto it = std::find(vec.begin(), vec.end(), tld);
283  // Sanity check
284  if (it == vec.end()) {
285  WRN_DM << "attempted to deregister nonexistant TLD "
286  << static_cast<void*>(tld);
287  return;
288  }
289  // Replace it with a null pointer. We will tidy it later.
290  // This prevents removals from interfering with TLD iteration.
291  *it = nullptr;
292  tlds_need_tidying_ = true;
293 }
294 
296 {
297  DBG_DM << "raising TLD " << static_cast<void*>(tld);
298  auto& vec = top_level_drawables_;
299  auto it = std::find(vec.begin(), vec.end(), tld);
300  // Sanity check
301  if (it == vec.end()) {
302  ERR_DM << "attempted to raise nonexistant TLD "
303  << static_cast<void*>(tld);
304  return;
305  }
306  // Invalidate existing occurances. They will be removed later.
307  for ( ; it != vec.end(); it = std::find(it, vec.end(), tld)) {
308  *it = nullptr;
309  }
310  // Then just readd it on the end.
311  vec.push_back(tld);
312  tlds_need_tidying_ = true;
313 }
314 
315 static void tidy_drawables()
316 {
317  // Remove all invalidated TLDs from the list.
318  DBG_DM << "tidying " << top_level_drawables_.size() << " drawables";
319  auto& vec = top_level_drawables_;
320  vec.erase(std::remove(vec.begin(), vec.end(), nullptr), vec.end());
321  DBG_DM << top_level_drawables_.size() << " after tidying";
322 }
323 
324 } // namespace draw_manager
Drawing functions, for drawing things on the screen.
void remove()
Removes a tip.
Definition: tooltip.cpp:111
#define ERR_DM
void render_screen()
Definition: video.cpp:528
int draw_delay()
Definition: general.cpp:908
int current_refresh_rate()
The refresh rate of the screen.
Definition: video.cpp:476
std::string get_unknown_exception_type()
Utility function for finding the type of thing caught with catch(...).
Definition: general.cpp:23
bool testing()
The game is running unit tests.
Definition: video.cpp:148
void deregister_drawable(top_level_drawable *tld)
Remove a top-level drawable from the drawing stack.
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:54
static void layout()
static lg::log_domain log_draw_man("draw/manager")
void invalidate_all()
Mark the entire screen as requiring redraw.
clip_setter override_clip(const SDL_Rect &clip)
Override the clipping area.
Definition: draw.cpp:443
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:417
rect & expand_to_cover(const SDL_Rect &r)
Minimally expand this rect to fully contain another.
Definition: rect.cpp:86
#define WRN_DM
rect minimal_cover(const SDL_Rect &r) const
Calculates the minimal rectangle that completely contains both this rectangle and the given rectangle...
Definition: rect.cpp:79
bool headless()
The game is running headless.
Definition: video.cpp:143
void sparkle()
Ensure that everything which needs to be drawn is drawn.
int get_frame_length()
Returns the length of one display frame, in milliseconds.
A top-level drawable item (TLD), such as a window.
constexpr int area() const
The area of this rectangle, in square pixels.
Definition: rect.hpp:101
#define DBG_DM
static void wait_for_vsync()
bool empty() const
False if both w and h are > 0, true otherwise.
Definition: rect.cpp:49
static bool expose()
std::size_t i
Definition: function.cpp:967
virtual void update()
Update state and any parameters that may effect layout, or any of the later stages.
A global draw management interface.
static void update()
static void tidy_drawables()
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:46
void register_drawable(top_level_drawable *tld)
Register a top-level drawable.
static void render()
Contains the SDL_Rect helper code.
Base class for all the errors encountered by the engine.
Definition: exceptions.hpp:28
Standard logging facilities (interface).
virtual void layout()
Finalize the size and position of the drawable and its children, and invalidate any regions requiring...
virtual void render()
Perform any internal rendering necessary to prepare the drawable.
void invalidate_region(const rect &region)
Mark a region of the screen as requiring redraw.
void raise_drawable(top_level_drawable *tld)
Raise a TLD to the top of the drawing stack.