The Battle for Wesnoth  1.19.28+dev
draw_manager.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2022 - 2025
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 (std::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 (std::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 (std::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  // Compositing here blits each TLD's own render buffer on to the shared
230  // primary buffer using screen-absolute rectangles. SDL3 interprets those
231  // rectangles relative to the current viewport, and nothing else resets the
232  // viewport before this point, so it can still reflect whatever a
233  // widget's own draw call last left it as (eg a stale, narrower size
234  // from before a pixel-scale change). Force it to cover the whole
235  // target for the duration of exposure.
236  auto viewport_setter = draw::set_viewport(video::output_area());
237 
238  // For now just send all regions to all TLDs in the correct order.
239  bool drawn = false;
240 next:
241  while (!invalidated_regions_.empty()) {
242  rect r = invalidated_regions_.back();
243  invalidated_regions_.pop_back();
244  // check if this will be superceded by or should be merged with another
245  for (auto& other : invalidated_regions_) {
246  // r will never contain other, due to construction
247  if (other.contains(r)) {
248  DBG_DM << "skipping redundant draw " << r;
249  //STREAMING_LOG << "-";
250  goto next;
251  }
252  rect m = other.minimal_cover(r);
253  if (m.area() <= r.area() + other.area()) {
254  DBG_DM << "merging inefficient draws " << r;
255  //STREAMING_LOG << "=";
256  other = m;
257  goto next;
258  }
259  }
260  DBG_DM << "drawing " << r;
261  //STREAMING_LOG << "+";
262  auto clipper = draw::override_clip(r);
263  for (auto tld : top_level_drawables_) {
264  if (!tld) { continue; }
265  rect i = r.intersect(tld->screen_location());
266  if (i.empty()) {
267  //DBG_DM << " skip " << static_cast<void*>(tld);
268  //STREAMING_LOG << "x";
269  continue;
270  }
271  DBG_DM << " to " << static_cast<void*>(tld);
272  //STREAMING_LOG << "*";
273  try {
274  drawn |= tld->expose(i);
275  } catch(...) {
276  WRN_DM << "exception " << utils::get_unknown_exception_type()
277  << " thrown during expose " << static_cast<void*>(tld);
278  drawing_ = false;
279  throw;
280  }
281  }
282  }
283  drawing_ = false;
284  return drawn;
285 }
286 
287 // Note: This function ensures that multiple copies are not added.
288 // We can assume top_level_drawables_ will contain at most one of each TLD.
290 {
291  DBG_DM << "registering TLD " << static_cast<void*>(tld);
292  auto& vec = top_level_drawables_;
293  if (std::find(vec.begin(), vec.end(), tld) != vec.end()) {
294  raise_drawable(tld);
295  } else {
296  top_level_drawables_.push_back(tld);
297  }
298 }
299 
301 {
302  DBG_DM << "deregistering TLD " << static_cast<void*>(tld);
303  auto& vec = top_level_drawables_;
304  auto it = std::find(vec.begin(), vec.end(), tld);
305  // Sanity check
306  if (it == vec.end()) {
307  WRN_DM << "attempted to deregister nonexistent TLD "
308  << static_cast<void*>(tld);
309  return;
310  }
311  // Replace it with a null pointer. We will tidy it later.
312  // This prevents removals from interfering with TLD iteration.
313  *it = nullptr;
314  tlds_need_tidying_ = true;
315 }
316 
318 {
319  DBG_DM << "raising TLD " << static_cast<void*>(tld);
320  auto& vec = top_level_drawables_;
321  auto it = std::find(vec.begin(), vec.end(), tld);
322  // Sanity check
323  if (it == vec.end()) {
324  ERR_DM << "attempted to raise nonexistent TLD "
325  << static_cast<void*>(tld);
326  return;
327  }
328  // Invalidate existing occurances. They will be removed later.
329  for ( ; it != vec.end(); it = std::find(it, vec.end(), tld)) {
330  *it = nullptr;
331  }
332  // Then just readd it on the end.
333  vec.push_back(tld);
334  tlds_need_tidying_ = true;
335 }
336 
337 static void tidy_drawables()
338 {
339  // Remove all invalidated TLDs from the list.
340  DBG_DM << "tidying " << top_level_drawables_.size() << " drawables";
341  utils::erase(top_level_drawables_, nullptr);
342  DBG_DM << top_level_drawables_.size() << " after tidying";
343 }
344 
345 } // 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:1031
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 ::rect &clip)
Override the clipping area.
Definition: draw.cpp:590
viewport_setter set_viewport(const ::rect &viewport)
Set the viewport.
Definition: draw.cpp:689
std::size_t erase(Container &container, const Value &value)
Convenience wrapper for using std::remove on a container.
Definition: general.hpp:119
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:142
bool headless()
The game is running headless.
Definition: video.cpp:146
rect output_area()
{0, 0, output_size().x, output_size().y}
Definition: video.cpp:460
void render_screen()
Definition: video.cpp:575
bool testing()
The game is running unit tests.
Definition: video.cpp:151
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:495
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:49
rect minimal_cover(const rect &r) const
Calculates the minimal rectangle that completely contains both this rectangle and the given rectangle...
Definition: rect.cpp:79
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:54
constexpr int area() const
The area of this rectangle, in square pixels.
Definition: rect.hpp:103
rect intersect(const rect &r) const
Calculates the intersection of this rectangle and another; that is, the maximal rectangle that is con...
Definition: rect.cpp:92
rect & expand_to_cover(const rect &r)
Minimally expand this rect to fully contain another.
Definition: rect.cpp:86
static map_location::direction s