The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
29static 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
36using std::chrono::steady_clock;
37using namespace std::chrono_literals;
38
39// This is not publically exposed, because nobody else should be using it.
40// Implementation is in video.cpp.
41namespace video { void render_screen(); }
42
43namespace {
44std::vector<top_level_drawable*> top_level_drawables_;
45std::vector<rect> invalidated_regions_;
46bool drawing_ = false;
47bool tlds_need_tidying_ = false;
48steady_clock::time_point last_sparkle_;
49bool extra_pass_requested_ = false;
50} // namespace
51
52namespace draw_manager {
53
54static void update();
55static void layout();
56static void render();
57static bool expose();
58static void wait_for_vsync();
59static void tidy_drawables();
60
61void 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
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_) {
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.
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 {
174 }
175
176 last_sparkle_ = steady_clock::now();
177}
178
179std::chrono::milliseconds get_frame_length()
180{
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
191static 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
201static 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
209static 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
217static 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
225static 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;
240next:
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
337static 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
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
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.
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
#define i