The Battle for Wesnoth  1.17.6+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 "video.hpp"
24 
25 #include <SDL2/SDL_rect.h>
26 #include <SDL2/SDL_timer.h>
27 
28 #include <algorithm>
29 #include <vector>
30 #include <map>
31 
32 static lg::log_domain log_draw_man("draw/manager");
33 #define ERR_DM LOG_STREAM(err, log_draw_man)
34 #define WRN_DM LOG_STREAM(warn, log_draw_man)
35 #define LOG_DM LOG_STREAM(info, log_draw_man)
36 #define DBG_DM LOG_STREAM(debug, log_draw_man)
37 
39 
40 // This is not publically exposed, because nobody else should be using it.
41 // Implementation is in video.cpp.
42 namespace video { void render_screen(); }
43 
44 namespace {
45 std::vector<top_level_drawable*> top_level_drawables_;
46 std::vector<rect> invalidated_regions_;
47 bool drawing_ = false;
48 bool tlds_need_tidying_ = false;
49 uint32_t last_sparkle_ = 0;
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 << "superceding 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 
125 void sparkle()
126 {
127  if (drawing_) {
128  ERR_DM << "Draw recursion detected";
129  throw game::error("recursive draw");
130  }
131 
132  // Remove any invalidated TLDs from previous iterations or events.
133  if (tlds_need_tidying_) {
134  tidy_drawables();
135  tlds_need_tidying_ = false;
136  }
137 
138  // Animate, process, and update state.
140 
141  // Ensure layout is up-to-date.
143 
144  // If we are running headless or executing unit tests, do not render.
145  // There are not currently any tests for actual rendering output.
146  if(video::headless() || video::testing()) {
147  invalidated_regions_.clear();
148  return;
149  }
150 
151  // Ensure any off-screen render buffers are up-to-date.
153 
154  // Draw to the screen.
155  if (draw_manager::expose()) {
156  // We only need to flip the screen if something was drawn.
158  } else {
159  wait_for_vsync();
160  }
161 
162  last_sparkle_ = SDL_GetTicks();
163 }
164 
166 {
167  int rr = video::current_refresh_rate();
168  if (rr <= 0) {
169  // make something up
170  rr = 60;
171  }
172  // allow 1ms for general processing
173  int vsync_delay = (1000 / rr) - 1;
174  // if there's a preferred limit, limit to that
175  return std::clamp(vsync_delay, preferences::draw_delay(), 1000);
176 }
177 
178 static void wait_for_vsync()
179 {
180  int time_to_wait = last_sparkle_ + get_frame_length() - SDL_GetTicks();
181  if (time_to_wait > 0) {
182  // delay a maximum of 1 second in case something crazy happens
183  SDL_Delay(std::min(time_to_wait, 1000));
184  }
185 }
186 
187 static void update()
188 {
189  for (size_t i = 0; i < top_level_drawables_.size(); ++i) {
190  top_level_drawable* tld = top_level_drawables_[i];
191  if (tld) { tld->update(); }
192  }
193 }
194 
195 static void layout()
196 {
197  for (size_t i = 0; i < top_level_drawables_.size(); ++i) {
198  top_level_drawable* tld = top_level_drawables_[i];
199  if (tld) { tld->layout(); }
200  }
201 }
202 
203 static void render()
204 {
205  for (size_t i = 0; i < top_level_drawables_.size(); ++i) {
206  top_level_drawable* tld = top_level_drawables_[i];
207  if (tld) { tld->render(); }
208  }
209 }
210 
211 static bool expose()
212 {
213  drawing_ = true;
214 
215  // For now just send all regions to all TLDs in the correct order.
216  bool drawn = false;
217 next:
218  while (!invalidated_regions_.empty()) {
219  rect r = invalidated_regions_.back();
220  invalidated_regions_.pop_back();
221  // check if this will be superceded by or should be merged with another
222  for (auto& other : invalidated_regions_) {
223  // r will never contain other, due to construction
224  if (other.contains(r)) {
225  DBG_DM << "skipping redundant draw " << r;
226  //STREAMING_LOG << "-";
227  goto next;
228  }
229  rect m = other.minimal_cover(r);
230  if (m.area() <= r.area() + other.area()) {
231  DBG_DM << "merging inefficient draws " << r;
232  //STREAMING_LOG << "=";
233  other = m;
234  goto next;
235  }
236  }
237  DBG_DM << "drawing " << r;
238  //STREAMING_LOG << "+";
239  auto clipper = draw::override_clip(r);
240  for (auto tld : top_level_drawables_) {
241  if (!tld) { continue; }
242  rect i = r.intersect(tld->screen_location());
243  if (i.empty()) {
244  //DBG_DM << " skip " << static_cast<void*>(tld);
245  //STREAMING_LOG << "x";
246  continue;
247  }
248  DBG_DM << " to " << static_cast<void*>(tld);
249  //STREAMING_LOG << "*";
250  try {
251  drawn |= tld->expose(i);
252  } catch(...) {
253  WRN_DM << "exception thrown during expose "
254  << static_cast<void*>(tld);
255  drawing_ = false;
256  throw;
257  }
258  }
259  }
260  drawing_ = false;
261  return drawn;
262 }
263 
264 // Note: This function ensures that multiple copies are not added.
265 // We can assume top_level_drawables_ will contain at most one of each TLD.
267 {
268  DBG_DM << "registering TLD " << static_cast<void*>(tld);
269  auto& vec = top_level_drawables_;
270  if (std::find(vec.begin(), vec.end(), tld) != vec.end()) {
271  raise_drawable(tld);
272  } else {
273  top_level_drawables_.push_back(tld);
274  }
275 }
276 
278 {
279  DBG_DM << "deregistering TLD " << static_cast<void*>(tld);
280  auto& vec = top_level_drawables_;
281  auto it = std::find(vec.begin(), vec.end(), tld);
282  // Sanity check
283  if (it == vec.end()) {
284  WRN_DM << "attempted to deregister nonexistant TLD "
285  << static_cast<void*>(tld);
286  return;
287  }
288  // Replace it with a null pointer. We will tidy it later.
289  // This prevents removals from interfering with TLD iteration.
290  *it = nullptr;
291  tlds_need_tidying_ = true;
292 }
293 
295 {
296  DBG_DM << "raising TLD " << static_cast<void*>(tld);
297  auto& vec = top_level_drawables_;
298  auto it = std::find(vec.begin(), vec.end(), tld);
299  // Sanity check
300  if (it == vec.end()) {
301  ERR_DM << "attempted to raise nonexistant TLD "
302  << static_cast<void*>(tld);
303  return;
304  }
305  // Invalidate existing occurances. They will be removed later.
306  for ( ; it != vec.end(); it = std::find(it, vec.end(), tld)) {
307  *it = nullptr;
308  }
309  // Then just readd it on the end.
310  vec.push_back(tld);
311  tlds_need_tidying_ = true;
312 }
313 
314 static void tidy_drawables()
315 {
316  // Remove all invalidated TLDs from the list.
317  DBG_DM << "tidying " << top_level_drawables_.size() << " drawables";
318  auto& vec = top_level_drawables_;
319  vec.erase(std::remove(vec.begin(), vec.end(), nullptr), vec.end());
320  DBG_DM << top_level_drawables_.size() << " after tidying";
321 }
322 
323 } // 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:898
int current_refresh_rate()
The refresh rate of the screen.
Definition: video.cpp:476
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.
#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).
int area() const
The area of this rectangle, in square pixels.
Definition: rect.hpp:78
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.