Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Efficient Text Rendering with SkTextBlob

Recommended resources

Welcome back. In our previous lesson, you learned how to render text onto the canvas using SkFont and SkCanvas::drawString, which seamlessly integrates with the camera system you built. While drawString is perfectly suitable for text that changes every frame, using it for static or rarely changing text is a significant performance anti-pattern.

This lesson directly addresses your goal of building a highly performative system by introducing a powerful optimization technique. We will explore why drawString can be inefficient and how to mitigate this by pre-calculating text layouts using SkTextBlob. This is especially relevant for your project, where you'll be displaying questions extracted from PDFs—text that, once placed, will not change.

The Hidden Cost of drawString

From a high level, drawString seems simple. However, every time it is called, Skia performs a series of complex operations under the hood. For a system programmer focused on performance, it's crucial to understand what this work entails:

  1. Character-to-Glyph Mapping: It iterates through the input string and, for each character, finds the corresponding glyph in the SkTypeface data. A glyph is the actual vector shape that gets drawn.
  2. Glyph Positioning (Shaping): It calculates the position of each glyph. This isn't just a simple offset. It involves using the font's metrics, such as the advance width of each glyph (how much horizontal space it takes) and applying kerning (adjusting the space between specific pairs of letters, like 'A' and 'V').
  3. Tessellation and Rasterization Prep: The vector paths of the positioned glyphs are processed and prepared for the GPU to render.

When you call drawString inside your main application loop for text that doesn't change, you are forcing the CPU to repeat this expensive shaping and layout process every single frame. This is wasted work. The principle of "do work once, use many times" is fundamental to performance engineering, and this is where SkTextBlob comes in.


SkTextBlob: The "Compile Once, Run Many" Approach

An SkTextBlob is an immutable object that captures the result of the text shaping process. Think of it as a pre-compiled, optimized representation of your text. You create the blob once, specifying the string, font, and other properties. Skia performs the expensive character-to-glyph mapping and positioning calculations at that time. The resulting SkTextBlob contains all the information needed to draw the text, packaged in a highly efficient format.

Once you have the blob, you can render it repeatedly using SkCanvas::drawTextBlob. This call is significantly faster than drawString because all the layout work is already done. The GPU can essentially replay a cached set of drawing commands.

The official Skia documentation provides the definitive description of this object.

SkTextBlob Class Reference - Skia

This is the official API documentation for SkTextBlob. It explains its purpose and structure.

Read the brief class description right at the top. It defines SkTextBlob as an "immutable container" for "multiple text runs." Continue to the detailed description. Note the key sentence: "Each text run consists of glyphs, SkPaint, and position." This confirms that the blob stores the pre-calculated glyph and position data, which is the essence of this optimization.


Creating and Drawing SkTextBlob

To use this optimization, you replace your drawString call with a two-step process: creation and drawing.

1. Creation: You create the SkTextBlob object outside of your render loop, typically during an initialization phase or whenever the text content actually changes. Skia provides several static factory functions on the SkTextBlob class for this purpose. The most common is MakeFromString.

2. Drawing: Inside your render loop, you call SkCanvas::drawTextBlob, passing the blob object you created, the desired coordinates, and a SkPaint object to control the drawing style (like color).

Let's examine the factory functions that allow you to create these blobs.

SkTextBlob Class Reference - Skia

This part of the API documentation details the static methods used to construct SkTextBlob objects.

First, look at the MakeFromText function signature and description. You can find it by searching for the heading MakeFromText(). This function takes a raw character buffer and its length. Next, look at the more convenient MakeFromString() overload. This function works directly with a null-terminated C-style string, which is often easier to use. Notice the documentation mentions that these simple methods do not perform "kerning or other complex shaping." For most applications, this is acceptable, but for advanced typography, you would use a more powerful tool like SkTextBlobBuilder, which gives you finer control. For your project's purpose of displaying extracted text, MakeFromString is perfectly suitable and highly effective. Here is a C++ code example demonstrating the entire workflow.

Notice the documentation mentions that these simple methods do not perform "kerning or other complex shaping." For most applications, this is acceptable, but for advanced typography, you would use a more powerful tool like SkTextBlobBuilder, which gives you finer control. For your project's purpose of displaying extracted text, MakeFromString is perfectly suitable and highly effective.

Here is a C++ code example demonstrating the entire workflow.

#include <SkCanvas.h>
#include <SkFont.h>
#include <SkPaint.h>
#include <SkTextBlob.h>
#include <SkTypeface.h>

// Assume 'canvas', 'font', and 'paint' are already initialized.
// SkFont font(SkTypeface::MakeDefault(), 48.0f);
// SkPaint paint;
// paint.setColor(SK_ColorBLACK);

// --- The Wrong Way (inefficient) ---
// This would be inside your render loop, running every frame.
// canvas->drawString("Expensive Repeated Text", 50.0f, 100.0f, font, paint);


// --- The Right Way (performant) ---

// 1. CREATION: Do this ONCE, outside the render loop.
// For your project, you would do this after extracting a question from a PDF.
sk_sp<SkTextBlob> textBlob = SkTextBlob::MakeFromString(
    "Optimized Repeated Text", 
    font
);

// ... later, inside your render loop ...

// 2. DRAWING: Do this every frame. It's very fast.
if (textBlob) {
    canvas->drawTextBlob(textBlob, 50.0f, 200.0f, paint);
}

In this example, the expensive work of figuring out the glyphs and positions for "Optimized Repeated Text" is done only once. The call to drawTextBlob inside the loop is lightweight, simply telling Skia to draw the pre-computed object at a given position. The SkPaint still controls the final color, allowing you to change the text color dynamically without having to recreate the blob.

This pattern is a cornerstone of high-performance graphics programming. By identifying immutable data and pre-processing it, you free up valuable CPU cycles in your time-critical render loop, leading to a smoother and more responsive application.


Conclusion

You have now learned a critical optimization technique for text rendering that directly applies to your goal of building a high-performance whiteboard. By moving from the "interpretive" model of drawString to the "compiled" model of SkTextBlob, you can significantly reduce the per-frame workload for rendering static text.

Key Takeaways:

  • Calling SkCanvas::drawString repeatedly for static text is inefficient because it recalculates glyph layout and positioning every frame.
  • SkTextBlob is an immutable object that caches the results of these expensive text-shaping operations.
  • You create a blob once using a factory function like SkTextBlob::MakeFromString when your text content is defined.
  • You render the blob many times within your main loop using the fast SkCanvas::drawTextBlob method.
  • This approach embodies the performance principle of pre-computation: do expensive work once and reuse the results.

In the next module, we will pivot from drawing static content to handling dynamic user input. The first lesson will cover how to capture high-frequency mouse and stylus events from SDL2, including pressure data. This is the first step toward implementing the core interactive drawing feature of your application.