Haiku BView Invalidation: Reconstructing Dirty Regions Without Flicker
Use BView::Invalidate, Draw, update rectangles, and clipping regions as a retained redraw contract instead of painting transient pixels directly.
Haiku’s Interface Kit does not treat a view as a blank bitmap that the application must repaint continuously. A BView participates in an update system coordinated with its BWindow and the Application Server. When some region becomes invalid, the view is asked to reconstruct that region through Draw(). This retained redraw contract is why a correct view survives being uncovered, resized, shown again, or scrolled without relying on pixels that happened to remain in a backing store.
The practical consequence is important: store the state that defines the image, invalidate the affected area when that state changes, and make Draw() capable of producing the same result from that state at any time. Code that draws a temporary highlight directly in response to a mouse message may look right until another update erases it. Code that updates a model, invalidates a region, and redraws from the model remains coherent after normal window-system events.
Invalidation requests an update; it does not paint immediately
BView::Invalidate() marks the view’s bounds, or a specified rectangle, as needing an update. The rectangle is expressed in the view’s coordinate system. The request is meaningful only when the view is attached to a window: an unattached view has no Application Server counterpart to update. Invalidation should therefore be understood as scheduling or requesting future drawing, not as a synchronous call to Draw().
That separation lets Haiku coalesce work. Multiple changes can affect nearby pixels before the update is serviced; repainting each intermediate state immediately would waste work and could expose inconsistent frames. Application logic should update its authoritative model first, then call Invalidate() for the region whose appearance changed. If a change affects layout, child geometry, or the entire view, the invalidation boundary should include every pixel whose prior output is no longer valid.
Draw() must be a reconstruction function
Implement Draw(BRect updateRect) as a rendering of current state, not as a replay of the event that triggered it. The supplied rectangle identifies the requested update area; it is not necessarily the entire visible view. A robust implementation can redraw all of its content and let clipping constrain the result, or use the update rectangle to skip expensive objects that cannot intersect it.
For example, a simple model-driven view can keep a vector of points and invalidate after appending one:
void PlotView::AppendPoint(BPoint point)
{
fPoints.push_back(point);
Invalidate(BRect(point.x - 2, point.y - 2,
point.x + 2, point.y + 2));
}
void PlotView::Draw(BRect updateRect)
{
for (const BPoint& point : fPoints) {
if (updateRect.Contains(point))
FillEllipse(point, BPoint(2, 2));
}
}
This is a schematic example, not a complete view class: it omits coordinate transforms, ownership, bounds clipping, and synchronization of fPoints if other threads modify the model. The key is that every call can reconstruct the output from fPoints; the previous on-screen pixels are not the source of truth.
Respect the update region and clipping state
The update rectangle is an optimization hint; the actual clipping region can be smaller. The Application Server may preserve and shift portions of a view during scrolling, and only the newly exposed or invalidated area then needs drawing. A view hierarchy also means that invalidation can affect descendants that intersect the changed area. A child view’s drawable region must therefore be reasoned about in its own local coordinate system and through the parent hierarchy, rather than by assuming that a top-level screen rectangle applies everywhere.
Use the current clipping region when expensive drawing needs a precise visible update area, and intersect it with application-level regions only when there is a measured benefit. Do not cache one clipping region across updates: it describes the current drawing operation and may change after resize, scroll, invalidation, or occlusion changes. Test partial invalidations as well as full-window redraws; an implementation that always passes when the whole view is redrawn can still leave stale pixels at the edge of a narrowly invalidated rectangle.
Opt in to drawing and attach before expecting updates
A view whose subclass implements Draw() must be created with the B_WILL_DRAW flag. Without it, the view may be skipped during update handling; a non-drawing view can instead act as a container for child views. Make the flag an explicit part of the view’s construction contract, especially in helper factories where the constructor flags are assembled conditionally.
Likewise, do not assume drawing functions or invalidation work before the view is attached to a window. Window() returns null when a view has no window. Build and configure the hierarchy first, attach it, and only then rely on server-mediated drawing and updates. Avoid deleting an attached view directly; remove it from the hierarchy under the correct window synchronization, or let the owning window’s documented teardown remove its children.
Separate model synchronization from window locking
Haiku routes a window’s messages through its looper thread. Calls that manipulate a window’s view hierarchy or server-backed view state have locking requirements. If a worker thread changes the model that Draw() reads, a separate synchronization strategy is still required; a window lock is not automatically a general-purpose lock for every object the application shares. Conversely, holding a model mutex while waiting for a window lock can create a lock-order cycle if the window thread needs that same mutex during drawing.
A safer pattern is to keep background work independent, publish a completed state change to the window thread, and invalidate from the window’s serialized message path. If the model must be shared, define and document one lock order, copy a minimal immutable snapshot for rendering, or use message passing to transfer ownership of the update. Keep Draw() bounded: expensive file access or blocking waits in the window thread delay input and repaint processing.
Avoid confusing drawing, flushing, and synchronization
Drawing calls are buffered for delivery to the Application Server. The update system flushes as part of ordinary drawing, but code drawing outside an update may need an explicit flush if it must promptly submit instructions. Flush() submits queued work; Sync() additionally waits for the server to execute work already sent. Those operations are distinct from invalidation: invalidation asks the view system to schedule a redraw, while flushing a connection submits drawing commands that have already been issued.
Do not call Sync() on an unattached view; the API documentation warns that this can crash. Also avoid synchronizing after every primitive: it defeats batching and can turn a responsive sequence of drawing operations into repeated cross-process round trips. Use synchronization only when later code genuinely depends on the server having executed earlier drawing, for example when completing a server-rendered bitmap operation.
Test the redraw contract, not only the first paint
Exercise a view by changing its model and invalidating a small rectangle; cover and uncover it; hide and show the window; resize the window in both directions; scroll the view; add, remove, and resize child views; and trigger a full update after many incremental ones. Check whether every pixel can be reconstructed after the system discards the prior visible contents. Instrument Draw() calls with update bounds and count the area repainted so excessive invalidation can be distinguished from missing redraws.
For a persistent visual defect, compare the authoritative model with the region invalidated after the change. A stale artifact usually means the invalidated area is too small, a draw path relies on prior pixels, or a child/view coordinate conversion is wrong. A flicker or sluggish update often means state changes are painted outside the normal update mechanism, an expensive draw blocks the looper, or the application is forcing unnecessary full-window synchronization.
The Haiku Book’s BView and Interface Kit drawing documentation should be treated as the API contract. The broader lesson is useful well beyond Haiku: invalidation systems work reliably when rendering is reproducible from state and dirty-region boundaries are explicit.
Related:
- How Haiku’s Interface Kit and app_server Render Native Windows
- Haiku BRegion: Building Exact Clipping and Damage Geometry
Sources: