Add a draw manager, to manage drawing to the screen
There is a new global interface draw_manager which handles drawing, and a new abstract base class top_level_drawable (TLD) to indicate that an object is something that has a place on the screen and should be drawn. Basic usage is fairly straightforward. Top-level objects (such as windows) inherit from top_level_drawable. They must implement the functions layout(), screen_location(), and expose(). layout() should ensure that screen location and animations are current. screen_location() should return the current draw location on screen. expose() should do the actual drawing. The draw manager keeps track of what regions of the screen, if any, need to be redrawn. Regions must be invalidated by calling invalidate_region(rect). This can be done at any time other than during expose(). It must not be called during expose(). The draw manager has one main callable function, sparkle(). This may be renamed at some point. It manages calling the TLD functions on all TLDs in the correct order. It also manages loop delay and vsync. The standard game loop now becomes: 1. events::pump() 2. draw_manager::sparkle()
This commit is contained in:
parent
b988608bfb
commit
3dbf212f8c
7 changed files with 487 additions and 0 deletions
|
@ -73,6 +73,7 @@ desktop/paths.cpp
|
|||
desktop/version.cpp
|
||||
display_chat_manager.cpp
|
||||
draw.cpp
|
||||
draw_manager.cpp
|
||||
editor/action/action.cpp
|
||||
editor/action/action_item.cpp
|
||||
editor/action/action_label.cpp
|
||||
|
@ -160,6 +161,7 @@ gui/core/placer/horizontal_list.cpp
|
|||
gui/core/placer/vertical_list.cpp
|
||||
gui/core/static_registry.cpp
|
||||
gui/core/timer.cpp
|
||||
gui/core/top_level_drawable.cpp
|
||||
gui/core/widget_definition.cpp
|
||||
gui/core/window_builder.cpp
|
||||
gui/core/window_builder/helper.cpp
|
||||
|
|
233
src/draw_manager.cpp
Normal file
233
src/draw_manager.cpp
Normal file
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
Copyright (C) 2022
|
||||
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY.
|
||||
|
||||
See the COPYING file for more details.
|
||||
*/
|
||||
|
||||
#include "draw_manager.hpp"
|
||||
|
||||
#include "draw.hpp"
|
||||
#include "exceptions.hpp"
|
||||
#include "log.hpp"
|
||||
#include "gui/core/top_level_drawable.hpp"
|
||||
#include "sdl/rect.hpp"
|
||||
#include "video.hpp"
|
||||
|
||||
#include <SDL2/SDL_rect.h>
|
||||
#include <SDL2/SDL_timer.h>
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
static lg::log_domain log_draw_man("draw/manager");
|
||||
#define ERR_DM LOG_STREAM(err, log_draw_man)
|
||||
#define WRN_DM LOG_STREAM(warn, log_draw_man)
|
||||
#define LOG_DM LOG_STREAM(info, log_draw_man)
|
||||
#define DBG_DM LOG_STREAM(debug, log_draw_man)
|
||||
|
||||
using std::endl;
|
||||
using gui2::top_level_drawable;
|
||||
|
||||
namespace {
|
||||
std::vector<top_level_drawable*> top_level_drawables_;
|
||||
std::vector<rect> invalidated_regions_;
|
||||
bool drawing_ = false;
|
||||
uint32_t last_sparkle_ = 0;
|
||||
} // namespace
|
||||
|
||||
namespace draw_manager {
|
||||
|
||||
static void layout();
|
||||
static void render();
|
||||
static bool draw();
|
||||
static void wait_for_vsync();
|
||||
|
||||
void invalidate_region(const rect& region)
|
||||
{
|
||||
if (drawing_) {
|
||||
ERR_DM << "Attempted to invalidate region " << region
|
||||
<< " during draw" << endl;
|
||||
throw game::error("invalidate during draw");
|
||||
}
|
||||
|
||||
// On-add region optimization
|
||||
rect progressive_cover = region;
|
||||
int64_t cumulative_area = 0;
|
||||
for (auto& r : invalidated_regions_) {
|
||||
if (r.contains(region)) {
|
||||
// An existing invalidated region already contains it,
|
||||
// no need to do anything in this case.
|
||||
//DBG_DM << "no need to invalidate " << region << endl;
|
||||
//PLAIN_LOG << '.';
|
||||
return;
|
||||
}
|
||||
if (region.contains(r)) {
|
||||
// This region contains a previously invalidated region,
|
||||
// might as well supercede it with this.
|
||||
DBG_DM << "superceding previous invalidation " << r
|
||||
<< " with " << region << endl;
|
||||
//PLAIN_LOG << '\'';
|
||||
r = region;
|
||||
return;
|
||||
}
|
||||
// maybe merge with another rect
|
||||
rect m = r.minimal_cover(region);
|
||||
if (m.area() <= r.area() + region.area()) {
|
||||
// This won't always be the best,
|
||||
// but it also won't ever be the worst.
|
||||
DBG_DM << "merging " << region << " with " << r
|
||||
<< " to invalidate " << m << endl;
|
||||
//PLAIN_LOG << ':';
|
||||
r = m;
|
||||
return;
|
||||
}
|
||||
// maybe merge *all* the rects
|
||||
progressive_cover.expand_to_cover(r);
|
||||
cumulative_area += r.area();
|
||||
if (progressive_cover.area() <= cumulative_area) {
|
||||
DBG_DM << "conglomerating invalidations to "
|
||||
<< progressive_cover << endl;
|
||||
//PLAIN_LOG << '%';
|
||||
// replace the first one, so we can easily prune later
|
||||
invalidated_regions_[0] = progressive_cover;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No optimization was found, so add a new invalidation
|
||||
DBG_DM << "invalidating region " << region << endl;
|
||||
//PLAIN_LOG << '.';
|
||||
invalidated_regions_.push_back(region);
|
||||
}
|
||||
|
||||
void sparkle()
|
||||
{
|
||||
if (drawing_) {
|
||||
ERR_DM << "Draw recursion detected" << endl;
|
||||
throw game::error("recursive draw");
|
||||
}
|
||||
|
||||
// Ensure layout and animations are up-to-date.
|
||||
draw_manager::layout();
|
||||
|
||||
// Ensure any off-screen render buffers are up-to-date.
|
||||
draw_manager::render();
|
||||
|
||||
// Draw to the screen.
|
||||
if (draw_manager::draw()) {
|
||||
// We only need to flip the screen if something was drawn.
|
||||
CVideo::get_singleton().render_screen();
|
||||
} else {
|
||||
wait_for_vsync();
|
||||
}
|
||||
|
||||
last_sparkle_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
static void wait_for_vsync()
|
||||
{
|
||||
int rr = CVideo::get_singleton().current_refresh_rate();
|
||||
if (rr <= 0) {
|
||||
// make something up
|
||||
rr = 60;
|
||||
}
|
||||
// allow 1ms for general processing
|
||||
int vsync_delay = (1000 / rr) - 1;
|
||||
int time_to_wait = last_sparkle_ + vsync_delay - SDL_GetTicks();
|
||||
if (time_to_wait > 0) {
|
||||
// delay a maximum of 1 second in case something crazy happens
|
||||
SDL_Delay(std::min(time_to_wait, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
static void layout()
|
||||
{
|
||||
for (auto tld : top_level_drawables_) {
|
||||
tld->layout();
|
||||
}
|
||||
}
|
||||
|
||||
static void render()
|
||||
{
|
||||
for (auto tld : top_level_drawables_) {
|
||||
tld->render();
|
||||
}
|
||||
}
|
||||
|
||||
static bool draw()
|
||||
{
|
||||
// TODO: draw_manager - some things were skipping draw when video is faked. Should this skip all in this case?
|
||||
|
||||
drawing_ = true;
|
||||
|
||||
// For now just send all regions to all TLDs in the correct order.
|
||||
bool drawn = false;
|
||||
next:
|
||||
while (!invalidated_regions_.empty()) {
|
||||
rect r = invalidated_regions_.back();
|
||||
invalidated_regions_.pop_back();
|
||||
// check if this will be superceded by or should be merged with another
|
||||
for (auto& other : invalidated_regions_) {
|
||||
// r will never contain other, due to construction
|
||||
if (other.contains(r)) {
|
||||
DBG_DM << "skipping redundant draw " << r << endl;
|
||||
//PLAIN_LOG << "-";
|
||||
goto next;
|
||||
}
|
||||
rect m = other.minimal_cover(r);
|
||||
if (m.area() <= r.area() + other.area()) {
|
||||
DBG_DM << "merging inefficient draws " << r << endl;
|
||||
//PLAIN_LOG << "=";
|
||||
other = m;
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
DBG_DM << "drawing " << r << endl;
|
||||
//PLAIN_LOG << "+";
|
||||
auto clipper = draw::set_clip(r);
|
||||
for (auto tld : top_level_drawables_) {
|
||||
rect i = r.intersect(tld->screen_location());
|
||||
if (i.empty()) {
|
||||
//DBG_DM << " skip " << static_cast<void*>(tld) << endl;
|
||||
//PLAIN_LOG << "x";
|
||||
continue;
|
||||
}
|
||||
DBG_DM << " to " << static_cast<void*>(tld) << endl;
|
||||
//PLAIN_LOG << "*";
|
||||
drawn |= tld->expose(i);
|
||||
}
|
||||
}
|
||||
drawing_ = false;
|
||||
return drawn;
|
||||
}
|
||||
|
||||
void register_drawable(top_level_drawable* tld)
|
||||
{
|
||||
DBG_DM << "registering TLD " << static_cast<void*>(tld) << endl;
|
||||
top_level_drawables_.push_back(tld);
|
||||
}
|
||||
|
||||
void deregister_drawable(top_level_drawable* tld)
|
||||
{
|
||||
DBG_DM << "deregistering TLD " << static_cast<void*>(tld) << endl;
|
||||
auto& vec = top_level_drawables_;
|
||||
vec.erase(std::remove(vec.begin(), vec.end(), tld), vec.end());
|
||||
}
|
||||
|
||||
void raise_drawable(top_level_drawable* tld)
|
||||
{
|
||||
DBG_DM << "raising TLD " << static_cast<void*>(tld) << endl;
|
||||
auto& vec = top_level_drawables_;
|
||||
vec.erase(std::remove(vec.begin(), vec.end(), tld), vec.end());
|
||||
vec.push_back(tld);
|
||||
}
|
||||
|
||||
} // namespace draw_manager
|
118
src/draw_manager.hpp
Normal file
118
src/draw_manager.hpp
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
Copyright (C) 2007 - 2022
|
||||
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY.
|
||||
|
||||
See the COPYING file for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "sdl/rect.hpp"
|
||||
|
||||
namespace gui2 { class top_level_drawable; }
|
||||
|
||||
/**
|
||||
* A global draw management interface.
|
||||
*
|
||||
* This interface governs drawing things to the screen in the correct order.
|
||||
* Drawable objects must inherit from gui2::top_level_drawable, and may be
|
||||
* referred to as "TLD"s.
|
||||
*
|
||||
* There is an absolute requirement that events happen in a certain order.
|
||||
* This is mostly managed by this interface, which calls the TLD methods in
|
||||
* order at most once every vsync.
|
||||
*
|
||||
* The order of events is:
|
||||
* 1. General event processing. This is governed by events::pump()
|
||||
* and is currently independent of this interface.
|
||||
* 2. Layout and animation. TLDs should ensure their layout information
|
||||
* is up-to-date inside gui2::top_level_drawable::layout(). After
|
||||
* this call they should know where they are on the screen, and be
|
||||
* ready to report it via gui2::top_level_drawable::screen_location().
|
||||
* Animation can also be performed during this stage.
|
||||
* 3. Offscreen rendering. TLDs should perform any offscreen rendering
|
||||
* during gui2::top_level_drawable::render(). After this call, all
|
||||
* internal drawing buffers should be in a state ready for display.
|
||||
* 4. Drawing to the screen. Drawing to the screen should only ever happen
|
||||
* inside gui2::top_level_drawable::expose(). Drawing to the screen at
|
||||
* any other point is an error.
|
||||
*
|
||||
* The draw manager will call layout(), render() and expose() in the correct
|
||||
* order to ensure all TLD objects are laid out correctly and drawn in the
|
||||
* correct order.
|
||||
*
|
||||
* Drawing order of TLDs is initially set by creation time, but a TLD may
|
||||
* be raised to the top of the drawing stack by calling
|
||||
* draw_manager::raise_drawable() manually. register_drawable() and
|
||||
* deregister_drawable() are called automatically by gui2::top_level_drawable
|
||||
* in its constructor and destructor, and do not need to be manually managed.
|
||||
*
|
||||
* The drawing process happens inside draw_manager::sparkle(). In general,
|
||||
* a game loop should perform two basic steps.
|
||||
* 1. call events::pump() to process events. Anything other than drawing
|
||||
* to the screen may happen during this step.
|
||||
* 2. call draw_manager::sparkle() to draw the screen, if necessary.
|
||||
*
|
||||
* The main sparkle() function will also rate-limit, so callers do not need
|
||||
* to add their own delay to their loops. If vsync is disabled, drawing will
|
||||
* happen as frequently as possible. If vsync is enabled, this function will
|
||||
* wait for the next screen refresh before drawing. In both cases, if nothing
|
||||
* needs to be drawn the function will block for an appropriate length of
|
||||
* time before returning.
|
||||
*
|
||||
* To ensure they are presented for drawing, any drawable object must call
|
||||
* draw_manager::invalidate_region() to indicate that an area of the screen
|
||||
* needs to be redrawn. This may be called during any phase other than the
|
||||
* draw phase. Invalidating regions during the draw phase is an error and
|
||||
* will throw an exception.
|
||||
*/
|
||||
namespace draw_manager
|
||||
{
|
||||
|
||||
/**
|
||||
* Mark a region of the screen as requiring redraw.
|
||||
*
|
||||
* This should be called any time an item changes in such a way as to
|
||||
* require redrawing.
|
||||
*
|
||||
* This may only be called outside the Draw phase.
|
||||
*
|
||||
* Regions are combined to result in a minimal number of draw calls,
|
||||
* so this may be called for every invalidation without much concern.
|
||||
*/
|
||||
void invalidate_region(const rect& region);
|
||||
|
||||
/**
|
||||
* Ensure that everything which needs to be drawn is drawn.
|
||||
*
|
||||
* This includes making sure window sizes and locations are up to date,
|
||||
* updating animation frames, and drawing whatever regions of the screen
|
||||
* need drawing or redrawing.
|
||||
*
|
||||
* If vsync is enabled, this function will block until the next vblank.
|
||||
* If nothing is drawn, it will still block for an appropriate amount of
|
||||
* time to simulate vsync, even if vsync is disabled.
|
||||
*/
|
||||
void sparkle();
|
||||
|
||||
/** Register a top-level drawable.
|
||||
*
|
||||
* Registered drawables will be drawn in the order of registration,
|
||||
* so the most recently-registered drawable will be "on top".
|
||||
*/
|
||||
void register_drawable(gui2::top_level_drawable* tld);
|
||||
|
||||
/** Remove a top-level drawable from the drawing stack. */
|
||||
void deregister_drawable(gui2::top_level_drawable* tld);
|
||||
|
||||
/** Raise a TLD to the top of the drawing stack. */
|
||||
void raise_drawable(gui2::top_level_drawable* tld);
|
||||
|
||||
} // namespace draw_manager
|
32
src/gui/core/top_level_drawable.cpp
Normal file
32
src/gui/core/top_level_drawable.cpp
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Copyright (C) 2022
|
||||
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY.
|
||||
|
||||
See the COPYING file for more details.
|
||||
*/
|
||||
|
||||
#include "gui/core/top_level_drawable.hpp"
|
||||
|
||||
#include "draw_manager.hpp"
|
||||
|
||||
namespace gui2
|
||||
{
|
||||
|
||||
top_level_drawable::top_level_drawable()
|
||||
{
|
||||
draw_manager::register_drawable(this);
|
||||
}
|
||||
|
||||
top_level_drawable::~top_level_drawable()
|
||||
{
|
||||
draw_manager::deregister_drawable(this);
|
||||
}
|
||||
|
||||
} // namespace gui2
|
90
src/gui/core/top_level_drawable.hpp
Normal file
90
src/gui/core/top_level_drawable.hpp
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
Copyright (C) 2007 - 2022
|
||||
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY.
|
||||
|
||||
See the COPYING file for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "sdl/rect.hpp"
|
||||
|
||||
namespace gui2
|
||||
{
|
||||
|
||||
/**
|
||||
* A top-level drawable item (TLD), such as a window.
|
||||
*
|
||||
* For now, TLDs keep track of where they are on the screen on their own.
|
||||
* They must draw themselves when requested via expose().
|
||||
*
|
||||
* The TLD interface will be called in the following order:
|
||||
* - layout()
|
||||
* - render()
|
||||
* - screen_location()
|
||||
* - zero or more expose() calls
|
||||
*/
|
||||
class top_level_drawable
|
||||
{
|
||||
protected:
|
||||
top_level_drawable();
|
||||
virtual ~top_level_drawable();
|
||||
public:
|
||||
/**
|
||||
* Size and position the drawable and its children.
|
||||
*
|
||||
* If this results in a change, it should invalidate both the previous
|
||||
* and new drawable regions via draw_manager::invalidate_region().
|
||||
* If the drawable was not previously laid out, it should invalidate
|
||||
* the newly determined region.
|
||||
*
|
||||
* TLDs must not perform any actual drawing during layout.
|
||||
*
|
||||
* Implementation of this interface is mandatory.
|
||||
*/
|
||||
virtual void layout() = 0;
|
||||
|
||||
/**
|
||||
* Perform any internal rendering necessary to prepare the drawable.
|
||||
*
|
||||
* For example if the drawable has an offscreen buffer it manages,
|
||||
* it should ensure this buffer is up to date.
|
||||
*
|
||||
* TLDs should also invalidate any regions visibly changed by this call.
|
||||
*
|
||||
* This interface is optional.
|
||||
*/
|
||||
virtual void render() {}
|
||||
|
||||
/**
|
||||
* Draw the portion of the drawable intersecting @p region to the screen.
|
||||
*
|
||||
* TLDs must not invalidate regions during expose. Only drawing must
|
||||
* occur, with no modification of layout.
|
||||
*
|
||||
* Implementation of this interface is mandatory.
|
||||
*
|
||||
* @param region The region to expose, in absolute draw-space
|
||||
* coordinates.
|
||||
* @returns True if anything was drawn, false otherwise.
|
||||
*/
|
||||
virtual bool expose(const SDL_Rect& region) = 0;
|
||||
|
||||
/**
|
||||
* The location of the TLD on the screen, in drawing coordinates.
|
||||
*
|
||||
* This will be used to determine the region (if any) to expose.
|
||||
*
|
||||
* Implementation of this interface is mandatory.
|
||||
*/
|
||||
virtual rect screen_location() = 0;
|
||||
};
|
||||
|
||||
} // namespace gui2
|
|
@ -83,6 +83,12 @@ rect rect::minimal_cover(const SDL_Rect& other) const
|
|||
return result;
|
||||
}
|
||||
|
||||
rect& rect::expand_to_cover(const SDL_Rect& other)
|
||||
{
|
||||
SDL_UnionRect(this, &other, this);
|
||||
return *this;
|
||||
}
|
||||
|
||||
rect rect::intersect(const SDL_Rect& other) const
|
||||
{
|
||||
rect result;
|
||||
|
|
|
@ -70,6 +70,9 @@ public:
|
|||
rect operator/(int s) const { return {x/s, y/s, w/s, h/s}; }
|
||||
rect& operator/=(int s) { x/=s; y/=s; w/=s; h/=s; return *this; }
|
||||
|
||||
/** The area of this rectangle, in square pixels. */
|
||||
int area() const { return w * h; }
|
||||
|
||||
/** False if both w and h are > 0, true otherwise. */
|
||||
bool empty() const;
|
||||
|
||||
|
@ -89,6 +92,9 @@ public:
|
|||
*/
|
||||
rect minimal_cover(const SDL_Rect& r) const;
|
||||
|
||||
/** Minimally expand this rect to fully contain another. */
|
||||
rect& expand_to_cover(const SDL_Rect& r);
|
||||
|
||||
/**
|
||||
* Calculates the intersection of this rectangle and another;
|
||||
* that is, the maximal rectangle that is contained by both.
|
||||
|
|
Loading…
Add table
Reference in a new issue