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