Render only a viewport area in pango_text and text_shape

The line-by-line workaround for rendering huge areas of text is dropped, as we
now only expect to be rendering screen-sized or smaller parts of the text.

Rendering huge texts (greater than 64kpixels high) now requires the caller of
pango_text::render() to change to calling pango_text::render(SDL_Rect) instead.
This is used by text_shape, fixing the crash when opening the credits screen.

This should reduce memory usage when rendering text. It doesn't seem to reduce
the amount of work done by Pango, but the drawing will be clipped in the Cairo
library.
This commit is contained in:
Steve Cotton 2021-04-29 13:21:26 +02:00 committed by Steve Cotton
parent b740153d92
commit f282eb7948
4 changed files with 56 additions and 74 deletions

View file

@ -19,7 +19,7 @@
* Modify implementation of overwrite_specials attribute for replace yes/no parameter by none/one_side/both_sides and select abilities used like weapons and specials who must be overwrited(owned by fighter where special applied or both) * Modify implementation of overwrite_specials attribute for replace yes/no parameter by none/one_side/both_sides and select abilities used like weapons and specials who must be overwrited(owned by fighter where special applied or both)
* Add a 'ability_id_active' attribute to [filter] * Add a 'ability_id_active' attribute to [filter]
### Miscellaneous and Bug Fixes ### Miscellaneous and Bug Fixes
* More optimizations in the UI drawing code, these shouldn't have visible effects (PR #5681). * More optimization in the UI drawing code, fixes the crash displaying the full credits (issue #5043).
* Made GUI.pyw compatible with Python 3.9 (issue #5719). * Made GUI.pyw compatible with Python 3.9 (issue #5719).
* Removed workarounds for bugs affecting older SDL 2.0 versions, including an extra copy of the game screen made during gamemap scrolling (PR #5736). * Removed workarounds for bugs affecting older SDL 2.0 versions, including an extra copy of the game screen made during gamemap scrolling (PR #5736).
* FPS values calculated when the :fps or :benchmark are now written to a file which can then be used to track FPS values over time. * FPS values calculated when the :fps or :benchmark are now written to a file which can then be used to track FPS values over time.

View file

@ -63,6 +63,7 @@ pango_text::pango_text()
, calculation_dirty_(true) , calculation_dirty_(true)
, length_(0) , length_(0)
, surface_dirty_(true) , surface_dirty_(true)
, rendered_viewport_()
, surface_buffer_() , surface_buffer_()
{ {
// With 72 dpi the sizes are the same as with SDL_TTF so hardcoded. // With 72 dpi the sizes are the same as with SDL_TTF so hardcoded.
@ -89,12 +90,19 @@ pango_text::pango_text()
cairo_font_options_destroy(fo); cairo_font_options_destroy(fo);
} }
surface& pango_text::render() surface& pango_text::render(const SDL_Rect& viewport)
{ {
this->rerender(); rerender(viewport);
return surface_; return surface_;
} }
surface& pango_text::render()
{
recalculate();
auto viewport = SDL_Rect{0, 0, rect_.x + rect_.width, rect_.y + rect_.height};
rerender(viewport);
return surface_;
}
int pango_text::get_width() const int pango_text::get_width() const
{ {
@ -639,25 +647,23 @@ static void from_cairo_format(uint32_t & c)
c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b); c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
} }
void pango_text::render(PangoLayout& layout, const PangoRectangle& rect, const std::size_t surface_buffer_offset, const unsigned stride) void pango_text::render(PangoLayout& layout, const SDL_Rect& viewport, const unsigned stride)
{ {
int width = rect.x + rect.width;
int height = rect.y + rect.height;
if(maximum_width_ > 0) { width = std::min(width, maximum_width_); }
if(maximum_height_ > 0) { height = std::min(height, maximum_height_); }
cairo_format_t format = CAIRO_FORMAT_ARGB32; cairo_format_t format = CAIRO_FORMAT_ARGB32;
uint8_t* buffer = &surface_buffer_[surface_buffer_offset]; uint8_t* buffer = &surface_buffer_[0];
std::unique_ptr<cairo_surface_t, std::function<void(cairo_surface_t*)>> cairo_surface( std::unique_ptr<cairo_surface_t, std::function<void(cairo_surface_t*)>> cairo_surface(
cairo_image_surface_create_for_data(buffer, format, width, height, stride), cairo_surface_destroy); cairo_image_surface_create_for_data(buffer, format, viewport.w, viewport.h, stride), cairo_surface_destroy);
std::unique_ptr<cairo_t, std::function<void(cairo_t*)>> cr(cairo_create(cairo_surface.get()), cairo_destroy); std::unique_ptr<cairo_t, std::function<void(cairo_t*)>> cr(cairo_create(cairo_surface.get()), cairo_destroy);
if(cairo_status(cr.get()) == CAIRO_STATUS_INVALID_SIZE) { if(cairo_status(cr.get()) == CAIRO_STATUS_INVALID_SIZE) {
throw std::length_error("Text is too long to render"); throw std::length_error("Text is too long to render");
} }
// The top-left of the text, which can be outside the area to be rendered
cairo_move_to(cr.get(), -viewport.x, -viewport.y);
// //
// TODO: the outline may be slightly cut off around certain text if it renders too // TODO: the outline may be slightly cut off around certain text if it renders too
// close to the surface's edge. That causes the outline to extend just slightly // close to the surface's edge. That causes the outline to extend just slightly
@ -692,88 +698,54 @@ void pango_text::render(PangoLayout& layout, const PangoRectangle& rect, const s
pango_cairo_show_layout(cr.get(), &layout); pango_cairo_show_layout(cr.get(), &layout);
} }
void pango_text::rerender() void pango_text::rerender(const SDL_Rect& viewport)
{ {
if(surface_dirty_) { if(surface_dirty_ || !SDL_RectEquals(&rendered_viewport_, &viewport)) {
assert(layout_.get()); assert(layout_.get());
this->recalculate(); this->recalculate();
surface_dirty_ = false; surface_dirty_ = false;
rendered_viewport_ = viewport;
int width = rect_.x + rect_.width;
int height = rect_.y + rect_.height;
if(maximum_width_ > 0) { width = std::min(width, maximum_width_); }
if(maximum_height_ > 0) { height = std::min(height, maximum_height_); }
cairo_format_t format = CAIRO_FORMAT_ARGB32; cairo_format_t format = CAIRO_FORMAT_ARGB32;
const int stride = cairo_format_stride_for_width(format, width); const int stride = cairo_format_stride_for_width(format, viewport.w);
// The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from // The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from
// Cairo. Width isn't tested here because it's implied by stride. // Cairo. Width isn't tested here because it's implied by stride.
if(stride <= 0 || height <= 0) { if(stride <= 0 || viewport.h <= 0) {
surface_ = surface(0, 0); surface_ = surface(0, 0);
surface_buffer_.clear(); surface_buffer_.clear();
return; return;
} }
// TODO: a sane value should be chosen for this arbitrary limit. The limit currently merely prevents arithmetic // Check to prevent arithmetic overflow when calculating (stride * height).
// overflow when calculating (stride * height), and still allows this function to allocate a 2 gigabyte surface. // The size of the viewport should already provide a far lower limit on the
// // maximum size, but this is left in as a sanity check.
// Making the limit match the amount that can be handled by a single call to render() would allow this function if(viewport.h > std::numeric_limits<int>::max() / stride) {
// to be simplified, removing the next try...catch block and its line-by-line workaround. The credits are likely
// to be the only text which exceeds render()'s limit of approx 2**15 pixels in height, so reimplementing
// end_credits.cpp should be enough to support this refactor.
if(height > std::numeric_limits<int>::max() / stride) {
throw std::length_error("Text is too long to render"); throw std::length_error("Text is too long to render");
} }
// Resize buffer appropriately and set all pixel values to 0. // Resize buffer appropriately and set all pixel values to 0.
surface_ = nullptr; // Don't leave a dangling pointer to the old buffer surface_ = nullptr; // Don't leave a dangling pointer to the old buffer
surface_buffer_.assign(height * stride, 0); surface_buffer_.assign(viewport.h * stride, 0);
try { // Try rendering the whole text in one go. If this throws a length_error
// Try rendering the whole text in one go // then leave it to the caller to handle; one reason it may throw is that
render(*layout_, rect_, 0u, stride); // cairo surfaces are limited to approximately 2**15 pixels in height.
} catch (std::length_error&) { render(*layout_, viewport, stride);
// Try rendering line-by-line, this is a workaround for cairo
// surfaces being limited to approx 2**15 pixels in height. If this
// also throws a length_error then leave it to the caller to
// handle.
std::size_t cumulative_height = 0u;
auto start_of_line = text_.cbegin();
while (start_of_line != text_.cend()) {
auto end_of_line = std::find(start_of_line, text_.cend(), '\n');
auto part_layout = std::unique_ptr<PangoLayout, std::function<void(void*)>> { pango_layout_new(context_.get()), g_object_unref};
auto line = std::string_view(&*start_of_line, std::distance(start_of_line, end_of_line));
set_markup(line, *part_layout);
copy_layout_properties(*layout_, *part_layout);
auto part_rect = calculate_size(*part_layout);
render(*part_layout, part_rect, cumulative_height * stride, stride);
cumulative_height += part_rect.height;
start_of_line = end_of_line;
if (start_of_line != text_.cend()) {
// skip over the \n
++start_of_line;
}
}
}
// The cairo surface is in CAIRO_FORMAT_ARGB32 which uses // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
// pre-multiplied alpha. SDL doesn't use that so the pixels need to be // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
// decoded again. // decoded again.
for(int y = 0; y < height; ++y) { for(int y = 0; y < viewport.h; ++y) {
uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]); uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]);
for(int x = 0; x < width; ++x) { for(int x = 0; x < viewport.w; ++x) {
from_cairo_format(pixels[x]); from_cairo_format(pixels[x]);
} }
} }
surface_ = SDL_CreateRGBSurfaceWithFormatFrom( surface_ = SDL_CreateRGBSurfaceWithFormatFrom(
&surface_buffer_[0], width, height, 32, stride, SDL_PIXELFORMAT_ARGB8888); &surface_buffer_[0], viewport.w, viewport.h, 32, stride, SDL_PIXELFORMAT_ARGB8888);
} }
} }

View file

@ -83,8 +83,20 @@ public:
/** /**
* Returns the rendered text. * Returns the rendered text.
* *
* Before rendering it tests whether a redraw is needed and if so it first * @param viewport Only this area needs to be drawn - the returned
* redraws the surface before returning it. * surface's origin will correspond to viewport.x and viewport.y, the
* width and height will be at least viewport.w and viewport.h (although
* they may be larger).
*/
surface& render(const SDL_Rect& viewport);
/**
* Equivalent to render(viewport), where the viewport's top-left is at
* (0,0) and the area is large enough to contain the full text.
*
* The top-left of the viewport will be at (0,0), regardless of the values
* of x and y. If the x or y co-ordinates are non-zero, then x columns and
* y rows of blank space are included in the amount of memory allocated.
*/ */
surface& render(); surface& render();
@ -362,15 +374,17 @@ private:
/** The dirty state of the surface. */ /** The dirty state of the surface. */
mutable bool surface_dirty_; mutable bool surface_dirty_;
/** The area that's cached in surface_, which is the area that was rendered when surface_dirty_ was last set to false. */
SDL_Rect rendered_viewport_;
/** /**
* Renders the text. * Renders the text.
* *
* It will do a recalculation first so no need to call both. * It will do a recalculation first so no need to call both.
*/ */
void rerender(); void rerender(const SDL_Rect& viewport);
void render(PangoLayout& layout, const PangoRectangle& rect, void render(PangoLayout& layout, const SDL_Rect& viewport, const unsigned stride);
const std::size_t surface_buffer_offset, const unsigned stride);
/** /**
* Buffer to store the image on. * Buffer to store the image on.

View file

@ -738,20 +738,16 @@ void text_shape::draw(surface& canvas,
return; return;
} }
// TODO: This creates a surface that's the full text_width x text_height, and then discards most of it by surface& surf = text_renderer.render(rects.clip_in_shape);
// calling blit_surface(... , &rects.clip_in_shape, ..., ...). Should be improved with a change to pango_text,
// so that we can call text_renderer.render(rects.clip_in_shape) and get a smaller surface instead.
surface& surf = text_renderer.render();
if(surf->w == 0) { if(surf->w == 0) {
DBG_GUI_D << "Text: Rendering '" << text DBG_GUI_D << "Text: Rendering '" << text
<< "' resulted in an empty canvas, leave.\n"; << "' resulted in an empty canvas, leave.\n";
return; return;
} }
// Blit the clipped region - this needs non-const copies of the rects // Blit the clipped region - this needs a non-const copy of the rect
auto clip_in_shape = rects.clip_in_shape;
auto dst_in_viewport = rects.dst_in_viewport; auto dst_in_viewport = rects.dst_in_viewport;
blit_surface(surf, &clip_in_shape, canvas, &dst_in_viewport); blit_surface(surf, nullptr, canvas, &dst_in_viewport);
} }
/***** ***** ***** ***** ***** CANVAS ***** ***** ***** ***** *****/ /***** ***** ***** ***** ***** CANVAS ***** ***** ***** ***** *****/