Implement deferred rendering for blurrables.

UI elements with blur effects (such as translucent windows) now defer
rendering for one frame, so they can snapshot the area underneath
for blurring.

In the future this should be done as multiple passes in one frame,
but for now this is fine.
This commit is contained in:
Tommy 2023-10-25 16:47:06 +13:00
parent 08144affdf
commit 766cfe8201
19 changed files with 180 additions and 43 deletions

View File

@ -482,6 +482,7 @@ void text_shape::draw(wfl::map_formula_callable& variables)
canvas::canvas()
: shapes_()
, blur_depth_(0)
, deferred_(false)
, w_(0)
, h_(0)
, variables_()
@ -492,6 +493,7 @@ canvas::canvas()
canvas::canvas(canvas&& c) noexcept
: shapes_(std::move(c.shapes_))
, blur_depth_(c.blur_depth_)
, deferred_(c.deferred_)
, w_(c.w_)
, h_(c.h_)
, variables_(c.variables_)
@ -499,6 +501,58 @@ canvas::canvas(canvas&& c) noexcept
{
}
// It would be better if the blur effect was managed at a higher level.
// But for now this works and should be both general and robust.
bool canvas::update_blur(const rect& screen_region, bool force)
{
if(!blur_depth_) {
// No blurring needed.
return true;
}
if(blur_texture_ && !force) {
// We already made the blur. It's expensive, so don't do it again.
return true;
}
// To blur what is underneath us, it must already be rendered somewhere.
// This is okay for sub-elements of an opaque window (panels on the main
// title screen for example) as the window will already have rendered
// its background to the render buffer before we get here.
// If however we are blurring elements behind the window, such as if
// the window itself is translucent (objectives popup), or it is
// transparent with a translucent element (character dialogue),
// then we need to render what will be behind it before capturing that
// and rendering a blur.
// We could use the previous render frame, but there could well have been
// another element there last frame such as a popup window which we
// don't want to be part of the blur.
// The only stable solution is to render in multiple passes.
// For now, we defer rendering of translucent elements to the next frame,
// so one frame is rendered without the element, then the element captures
// the result from that frame and renders itself on the next frame.
// Ultimately even with hardware acceleration of the blur effect
// a similar solution will need to be retained. The difference with a
// better future implementation would be that the multiple rendering
// passes can be managed at a higher level, and that they can be done
// within a single frame.
if(!deferred_) {
DBG_GUI_D << "Deferring blur at " << screen_region;
deferred_ = true;
return false;
}
// Read and blur pixels from the previous render frame.
DBG_GUI_D << "Blurring " << screen_region << " depth " << blur_depth_;
rect read_region = screen_region;
auto setter = draw::set_render_target({});
surface s = video::read_pixels_low_res(&read_region);
s = blur_surface(s, blur_depth_);
blur_texture_ = texture(s);
deferred_ = false;
return true;
}
void canvas::draw()
{
// This early-return has to come before the `validate(rect.w <= w_)` check, as during the boost_unit_tests execution
@ -508,18 +562,15 @@ void canvas::draw()
return;
}
// Note: this doesn't update if whatever is underneath changes.
if(blur_depth_ && !blur_texture_) {
// Cache a blurred image of whatever is underneath.
SDL_Rect rect = draw::get_viewport();
surface s = video::read_pixels_low_res(&rect);
s = blur_surface(s, blur_depth_);
blur_texture_ = texture(s);
if(deferred_) {
// We will draw next frame.
return;
}
// Draw blurred background.
// TODO: hwaccel - this should be able to be removed at some point with shaders
if(blur_depth_ && blur_texture_) {
DBG_GUI_D << "blitting blur size " << blur_texture_.draw_size();
draw::blit(blur_texture_);
}

View File

@ -25,6 +25,7 @@
#include "formula/callable.hpp"
#include "formula/function.hpp"
#include "sdl/texture.hpp"
#include "sdl/rect.hpp"
namespace wfl { class variant; }
struct point;
@ -87,6 +88,19 @@ public:
canvas& operator=(const canvas&) = delete;
canvas(canvas&& c) noexcept;
/**
* Update the background blur texture, if relevant and necessary.
*
* This should be called sometime before draw().
* Updating it later is less important as it's quite expensive.
*
* @param screen_region The area of the screen underneath the canvas.
* @param force Regenerate the blur even if we already did it.
*
* @returns True if draw should continue, false otherwise.
*/
bool update_blur(const rect& screen_region, const bool force = false);
/**
* Draw the canvas' shapes onto the screen.
*
@ -156,6 +170,9 @@ private:
/** Blurred background texture. */
texture blur_texture_;
/** Whether we have deferred rendering so we can capture for blur. */
bool deferred_;
/** The full width of the canvas. */
unsigned w_;

View File

@ -1008,9 +1008,18 @@ void grid::impl_draw_children()
continue;
}
widget->draw_background();
// We may need to defer drawing to next frame for blur processing.
if(!widget->draw_background()) {
get_window()->defer_region(widget->get_rectangle());
continue;
}
widget->draw_children();
widget->draw_foreground();
if(!widget->draw_foreground()) {
get_window()->defer_region(widget->get_rectangle());
continue;
}
}
}

View File

@ -93,11 +93,12 @@ void minimap::set_map_data(const std::string& map_data)
}
}
void minimap::impl_draw_background()
bool minimap::impl_draw_background()
{
if(map_) {
image::render_minimap(get_width(), get_height(), *map_, nullptr, nullptr, nullptr, true);
}
return true;
}
// }---------- DEFINITION ---------{

View File

@ -83,7 +83,7 @@ private:
std::unique_ptr<gamemap> map_;
/** See @ref widget::impl_draw_background. */
virtual void impl_draw_background() override;
virtual bool impl_draw_background() override;
public:
/** Static type getter that does not rely on the widget being constructed. */

View File

@ -149,9 +149,10 @@ void multi_page::finalize(std::unique_ptr<generator_base> generator, const std::
swap_grid(nullptr, &get_grid(), std::move(generator), "_content_grid");
}
void multi_page::impl_draw_background()
bool multi_page::impl_draw_background()
{
/* DO NOTHING */
return true;
}
void multi_page::set_self_active(const bool /*active*/)

View File

@ -217,7 +217,7 @@ private:
builder_grid_map page_builders_;
/** See @ref widget::impl_draw_background. */
virtual void impl_draw_background() override;
virtual bool impl_draw_background() override;
public:
/** Static type getter that does not rely on the widget being constructed. */

View File

@ -65,16 +65,24 @@ unsigned panel::get_state() const
return 0;
}
void panel::impl_draw_background()
bool panel::impl_draw_background()
{
DBG_GUI_D << LOG_HEADER << " size " << get_rectangle() << ".";
if(!get_canvas(0).update_blur(get_rectangle())) {
return false;
}
get_canvas(0).draw();
return true;
}
void panel::impl_draw_foreground()
bool panel::impl_draw_foreground()
{
DBG_GUI_D << LOG_HEADER << " size " << get_rectangle() << ".";
if(!get_canvas(1).update_blur(get_rectangle())) {
return false;
}
get_canvas(1).draw();
return true;
}
point panel::border_space() const

View File

@ -74,10 +74,10 @@ public:
private:
/** See @ref widget::impl_draw_background. */
virtual void impl_draw_background() override;
virtual bool impl_draw_background() override;
/** See @ref widget::impl_draw_foreground. */
virtual void impl_draw_foreground() override;
virtual bool impl_draw_foreground() override;
public:
/** Static type getter that does not rely on the widget being constructed. */

View File

@ -97,9 +97,10 @@ bool spacer::disable_click_dismiss() const
return false;
}
void spacer::impl_draw_background()
bool spacer::impl_draw_background()
{
/* DO NOTHING */
return true;
}
// }---------- DEFINITION ---------{

View File

@ -87,7 +87,7 @@ private:
bool fills_available_space();
/** See @ref widget::impl_draw_background. */
virtual void impl_draw_background() override;
virtual bool impl_draw_background() override;
public:
/** Static type getter that does not rely on the widget being constructed. */

View File

@ -431,17 +431,22 @@ int styled_widget::get_text_maximum_height() const
return get_height() - config_->text_extra_height;
}
void styled_widget::impl_draw_background()
bool styled_widget::impl_draw_background()
{
DBG_GUI_D << LOG_HEADER << " label '" << debug_truncate(label_.str()) << "' size "
<< get_rectangle() << ".";
if(!get_canvas(get_state()).update_blur(get_rectangle())) {
return false;
}
get_canvas(get_state()).draw();
return true;
}
void styled_widget::impl_draw_foreground()
bool styled_widget::impl_draw_foreground()
{
/* DO NOTHING */
return true;
}
point styled_widget::get_best_text_size(point minimum_size, point maximum_size) const

View File

@ -459,10 +459,10 @@ public:
protected:
/** See @ref widget::impl_draw_background. */
virtual void impl_draw_background() override;
virtual bool impl_draw_background() override;
/** See @ref widget::impl_draw_foreground. */
virtual void impl_draw_foreground() override;
virtual bool impl_draw_foreground() override;
/** Exposes font::pango_text::get_token, for the text label of this styled_widget */
std::string get_label_token(const point & position, const char * delimiters = " \n\r\t") const;

View File

@ -193,18 +193,18 @@ void toggle_panel::set_state(const state_t state)
assert(conf);
}
void toggle_panel::impl_draw_background()
bool toggle_panel::impl_draw_background()
{
// We don't have a fore and background and need to draw depending on
// our state, like a styled_widget. So we use the styled_widget's drawing method.
styled_widget::impl_draw_background();
return styled_widget::impl_draw_background();
}
void toggle_panel::impl_draw_foreground()
bool toggle_panel::impl_draw_foreground()
{
// We don't have a fore and background and need to draw depending on
// our state, like a styled_widget. So we use the styled_widget's drawing method.
styled_widget::impl_draw_foreground();
return styled_widget::impl_draw_foreground();
}
void toggle_panel::signal_handler_mouse_enter(const event::ui_event event,

View File

@ -175,10 +175,10 @@ private:
std::function<void(widget&)> callback_mouse_left_double_click_;
/** See @ref widget::impl_draw_background. */
virtual void impl_draw_background() override;
virtual bool impl_draw_background() override;
/** See @ref widget::impl_draw_foreground. */
virtual void impl_draw_foreground() override;
virtual bool impl_draw_foreground() override;
public:
/** Static type getter that does not rely on the widget being constructed. */

View File

@ -370,12 +370,12 @@ SDL_Rect widget::calculate_clipping_rectangle() const
}
}
void widget::draw_background()
bool widget::draw_background()
{
assert(visible_ == visibility::visible);
if(get_drawing_action() == redraw_action::none) {
return;
return true;
}
// Set viewport and clip so we can draw in local coordinates.
@ -389,7 +389,7 @@ void widget::draw_background()
auto clip_setter = draw::reduce_clip(clip);
draw_debug_border();
impl_draw_background();
return impl_draw_background();
}
void widget::draw_children()
@ -413,12 +413,12 @@ void widget::draw_children()
impl_draw_children();
}
void widget::draw_foreground()
bool widget::draw_foreground()
{
assert(visible_ == visibility::visible);
if(get_drawing_action() == redraw_action::none) {
return;
return true;
}
// Set viewport and clip so we can draw in local coordinates.
@ -431,7 +431,7 @@ void widget::draw_foreground()
clip.shift(-get_origin());
auto clip_setter = draw::reduce_clip(clip);
impl_draw_foreground();
return impl_draw_foreground();
}
SDL_Rect widget::get_dirty_rectangle() const

View File

@ -538,8 +538,10 @@ public:
*
* Derived should override @ref impl_draw_background instead of changing
* this function.
*
* @returns False if drawing should be deferred.
*/
void draw_background();
bool draw_background();
/**
* Draws the children of a widget.
@ -559,13 +561,16 @@ public:
*
* Derived should override @ref impl_draw_foreground instead of changing
* this function.
*
* @returns False if drawing should be deferred.
*/
void draw_foreground();
bool draw_foreground();
private:
/** See @ref draw_background. */
virtual void impl_draw_background()
virtual bool impl_draw_background()
{
return true;
}
/** See @ref draw_children. */
@ -574,8 +579,9 @@ private:
}
/** See @ref draw_foreground. */
virtual void impl_draw_foreground()
virtual bool impl_draw_foreground()
{
return true;
}
public:

View File

@ -619,13 +619,22 @@ void window::draw()
}
// Draw background.
this->draw_background();
if(!this->draw_background()) {
// We may need to blur the background behind the window,
// but at this point it hasn't been rendered yet.
// We thus defer rendering to next frame so we can snapshot what
// is underneath the window without drawing over it.
defer_region(get_rectangle());
return;
}
// Draw children.
this->draw_children();
// Draw foreground.
this->draw_foreground();
if(!this->draw_foreground()) {
defer_region(get_rectangle());
}
return;
}
@ -670,12 +679,13 @@ void window::update_render_textures()
render_buffer_.set_draw_size(draw);
}
// Clear the entire texture, just in case
for(int i = 0; i < 2; ++i) {
// Clear the entire texture.
{
auto setter = draw::set_render_target(render_buffer_);
draw::fill(0,0,0,0);
}
// Rerender everything.
queue_rerender();
}
@ -693,9 +703,24 @@ void window::queue_rerender(const rect& screen_region)
awaiting_rerender_.expand_to_cover(local_region);
}
void window::defer_region(const rect& screen_region)
{
LOG_DP << "deferring region " << screen_region;
deferred_regions_.push_back(screen_region);
}
void window::render()
{
// Ensure our render texture is correctly sized.
update_render_textures();
// Mark regions that were previously deferred for rerender and repaint.
for(auto& region : deferred_regions_) {
queue_redraw(region);
}
deferred_regions_.clear();
// Render the portion of the window awaiting rerender (if any).
if (awaiting_rerender_.empty()) {
return;
}

View File

@ -173,6 +173,9 @@ private:
/** The part of the window (if any) currently marked for rerender. */
rect awaiting_rerender_;
/** Parts of the window (if any) with rendering deferred to next frame */
std::vector<rect> deferred_regions_;
/** Ensure render textures are valid and correct. */
void update_render_textures();
@ -197,6 +200,16 @@ public:
void queue_rerender(const rect& region);
void queue_rerender();
/**
* Defer rendering of a particular region to next frame.
*
* This is used for blur, which must render the region underneath once
* before rendering the blur.
*
* @param region The region to defer in screen coordinates.
*/
void defer_region(const rect& region);
/** The status of the window. */
enum class status {
NEW, /**< The window is new and not yet shown. */