Skip to content

Commit 01c9607

Browse files
committed
Improve rendering: ink-bounds, draw batching, text cache
This introduces rendering optimizations that reduce backend overhead for partial-redraw and text-heavy UI scenes: 1. iui_emit_box: universal draw interception point replacing all direct ctx->renderer.draw_box() calls. Route through ink-bounds tracking and optional batch system. 2. Ink-bounds tracking: per-frame union bounding box of all draw calls (boxes, text, lines, circles, arcs). Backend can query iui_ink_bounds_get() to blit only the dirty region instead of full framebuffer. Uses FLT_MAX/-FLT_MAX initialization to eliminate first-extend branch on Arm Cortex-M pipeline. Negative width/height inputs are normalized before accumulation. 3. Draw call batching — 256-command buffer with clip-rect grouping on flush. All primitive types (rect, text, line, circle, arc) are routed through the batch when enabled, preserving draw order. Vector font text correctly bypasses the text batch (renders through iui_emit_box which is already batch-aware). 4. Text width cache — 64-entry hash table with linear probing and LFU eviction. Eliminates 99% of backend text_width callbacks. Cache key includes font_height to prevent cross-size poisoning when typography helpers temporarily mutate font metrics. Amortized O(k) decay per frame avoids O(N) scan. IUI_TEXT_CACHE_SIZE enforced as power-of-two via _Static_assert. Benchmark (noop backend, 480x800, 5000 frames, median-of-3): - Settings scene: 58 draws/frame, 1.9 us baseline - Dashboard scene: 98 draws/frame, 2.4 us baseline - Form scene: 66 draws/frame, 0.8 us baseline - Text cache: 99% text_width elimination across all scenes - Ink-bounds coverage: 100-119% of screen area (tight) The ink-bounds overhead (47-72%) measured with noop backend is expected: zero-cost draw calls make tracking arithmetic dominate. With a real GPU/framebuffer backend, the dirty-rect savings from partial redraws offset this cost significantly.
1 parent aa955a7 commit 01c9607

20 files changed

Lines changed: 1306 additions & 421 deletions

include/iui.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3245,14 +3245,16 @@ int iui_a11y_describe(const iui_a11y_hint *hint, char *buf, size_t buf_size);
32453245

32463246
/* Performance Optimization API
32473247
*
3248-
* Three core performance systems (always available, disabled by default):
3248+
* Four core performance systems (always available, disabled by default):
32493249
* 1. Draw Call Batching - buffers draw commands to reduce state changes
32503250
* 2. Dirty Rectangle Tracking - skips redrawing unchanged regions
3251-
* 3. Text Width Caching - caches text measurement results
3251+
* 3. Ink-Bounds Tracking - tracks union bounding box of all draw calls
3252+
* 4. Text Width Caching - caches text measurement results
32523253
*
32533254
* Usage:
32543255
* iui_batch_enable(ctx, true); // Enable draw batching
32553256
* iui_dirty_enable(ctx, true); // Enable dirty rect tracking
3257+
* iui_ink_bounds_enable(ctx, true); // Enable ink-bounds tracking
32563258
* iui_text_cache_enable(ctx, true); // Enable text cache
32573259
*/
32583260

@@ -3291,6 +3293,21 @@ void iui_dirty_invalidate_all(iui_context *ctx);
32913293
*/
32923294
int iui_dirty_count(const iui_context *ctx);
32933295

3296+
/* Ink-Bounds Tracking
3297+
* Tracks the union bounding box of all draw calls within a frame.
3298+
* When enabled, backends can query the drawn region to blit only the
3299+
* ink-bounds area instead of the full framebuffer.
3300+
*/
3301+
void iui_ink_bounds_enable(iui_context *ctx, bool enable);
3302+
3303+
/* Query the ink-bounds rectangle for the current frame.
3304+
* Returns false if no draw calls were made this frame.
3305+
*/
3306+
bool iui_ink_bounds_get(const iui_context *ctx, iui_rect_t *out);
3307+
3308+
/* Check if any draw calls were made this frame */
3309+
bool iui_ink_bounds_valid(const iui_context *ctx);
3310+
32943311
/* Text Width Caching
32953312
* Enable/disable text measurement caching. When enabled, text width
32963313
* calculations are cached to avoid redundant measurements.

src/appbar.c

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ bool iui_top_app_bar(iui_context *ctx,
7070

7171
/* Draw background (no corner radius for app bar) */
7272
iui_rect_t bar_rect = {bar_x, bar_y, bar_width, bar_height};
73-
ctx->renderer.draw_box(bar_rect, 0.f, bg_color, ctx->renderer.user);
73+
iui_emit_box(ctx, bar_rect, 0.f, bg_color);
7474

7575
/* Draw elevation shadow if scrolled (Level 2) */
7676
if (collapse_progress > 0.f) {
@@ -108,13 +108,13 @@ bool iui_top_app_bar(iui_context *ctx,
108108
if (nav_pressed) {
109109
uint32_t state_color =
110110
iui_state_layer(icon_color, IUI_STATE_PRESS_ALPHA);
111-
ctx->renderer.draw_box(nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
112-
state_color, ctx->renderer.user);
111+
iui_emit_box(ctx, nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
112+
state_color);
113113
} else if (nav_hovered) {
114114
uint32_t state_color =
115115
iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA);
116-
ctx->renderer.draw_box(nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
117-
state_color, ctx->renderer.user);
116+
iui_emit_box(ctx, nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
117+
state_color);
118118
}
119119

120120
/* Draw menu icon */
@@ -237,13 +237,13 @@ bool iui_top_app_bar_action(iui_context *ctx, const char *icon)
237237
if (pressed) {
238238
uint32_t state_color =
239239
iui_state_layer(icon_color, IUI_STATE_PRESS_ALPHA);
240-
ctx->renderer.draw_box(action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
241-
state_color, ctx->renderer.user);
240+
iui_emit_box(ctx, action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
241+
state_color);
242242
} else if (hovered) {
243243
uint32_t state_color =
244244
iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA);
245-
ctx->renderer.draw_box(action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
246-
state_color, ctx->renderer.user);
245+
iui_emit_box(ctx, action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f,
246+
state_color);
247247
}
248248

249249
/* Draw action icon */

src/basic.c

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ void iui_segmented(iui_context *ctx,
4141
*selected = 0;
4242

4343
/* MD3: Draw unified pill background (visible container for all segments) */
44-
ctx->renderer.draw_box(
45-
(iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height},
46-
pill_radius, ctx->colors.surface_container_highest, ctx->renderer.user);
44+
iui_emit_box(
45+
ctx, (iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height},
46+
pill_radius, ctx->colors.surface_container_highest);
4747

4848
/* Track component for MD3 validation */
4949
IUI_MD3_TRACK_SEGMENTED(
@@ -63,9 +63,8 @@ void iui_segmented(iui_context *ctx,
6363
if (*selected == 0 || *selected == num_entries - 1)
6464
corner = pill_radius;
6565

66-
ctx->renderer.draw_box(
67-
(iui_rect_t) {sel_x, seg_y, seg_width, seg_height}, corner,
68-
ctx->colors.secondary_container, ctx->renderer.user);
66+
iui_emit_box(ctx, (iui_rect_t) {sel_x, seg_y, seg_width, seg_height},
67+
corner, ctx->colors.secondary_container);
6968
}
7069

7170
/* Draw each segment */
@@ -92,9 +91,9 @@ void iui_segmented(iui_context *ctx,
9291
uint32_t hover_color = iui_state_layer(
9392
ctx->colors.on_surface, iui_state_get_alpha(seg_state));
9493
float corner = (i == 0 || i == num_entries - 1) ? pill_radius : 0.f;
95-
ctx->renderer.draw_box(
96-
(iui_rect_t) {seg_x, seg_y, seg_width, seg_height}, corner,
97-
hover_color, ctx->renderer.user);
94+
iui_emit_box(ctx,
95+
(iui_rect_t) {seg_x, seg_y, seg_width, seg_height},
96+
corner, hover_color);
9897
}
9998

10099
/* Handle selection change */
@@ -288,16 +287,15 @@ float iui_slider_ex(iui_context *ctx,
288287
iui_get_component_state(ctx, touch_rect, disabled);
289288

290289
/* Draw inactive track (full width, behind active track) */
291-
ctx->renderer.draw_box(track_rect, track_rect.height * .5f, inactive_color,
292-
ctx->renderer.user);
290+
iui_emit_box(ctx, track_rect, track_rect.height * .5f, inactive_color);
293291

294292
/* Draw active track (left side up to thumb) */
295293
float active_width = thumb_x - track_rect.x;
296294
if (active_width > 0) {
297-
ctx->renderer.draw_box((iui_rect_t) {track_rect.x, track_rect.y,
298-
active_width, track_rect.height},
299-
track_rect.height * .5f, active_color,
300-
ctx->renderer.user);
295+
iui_emit_box(ctx,
296+
(iui_rect_t) {track_rect.x, track_rect.y, active_width,
297+
track_rect.height},
298+
track_rect.height * .5f, active_color);
301299
}
302300

303301
/* Handle thumb interaction */
@@ -379,9 +377,9 @@ float iui_slider_ex(iui_context *ctx,
379377
uint8_t alpha =
380378
is_dragging ? IUI_STATE_DRAG_ALPHA : IUI_STATE_HOVER_ALPHA;
381379
uint32_t state_color = iui_state_layer(handle_color, alpha);
382-
ctx->renderer.draw_box(
383-
(iui_rect_t) {state_x, state_y, state_size, state_size},
384-
state_size * 0.5f, state_color, ctx->renderer.user);
380+
iui_emit_box(ctx,
381+
(iui_rect_t) {state_x, state_y, state_size, state_size},
382+
state_size * 0.5f, state_color);
385383
}
386384

387385
/* Draw value indicator bubble during drag */
@@ -415,10 +413,10 @@ float iui_slider_ex(iui_context *ctx,
415413
}
416414

417415
/* Draw indicator background (pill shape with primary color) */
418-
ctx->renderer.draw_box((iui_rect_t) {indicator_x, indicator_y,
419-
indicator_width, indicator_height},
420-
indicator_height * 0.5f, active_color,
421-
ctx->renderer.user);
416+
iui_emit_box(ctx,
417+
(iui_rect_t) {indicator_x, indicator_y, indicator_width,
418+
indicator_height},
419+
indicator_height * 0.5f, active_color);
422420

423421
/* Draw value text centered in indicator */
424422
iui_rect_t indicator_text_rect = {
@@ -439,8 +437,7 @@ float iui_slider_ex(iui_context *ctx,
439437
}
440438

441439
/* Draw thumb (circle) */
442-
ctx->renderer.draw_box(thumb_rect, half_size, handle_color,
443-
ctx->renderer.user);
440+
iui_emit_box(ctx, thumb_rect, half_size, handle_color);
444441

445442
iui_newline(ctx);
446443

@@ -636,16 +633,15 @@ bool iui_range_slider(iui_context *ctx,
636633
thumb_high_x = norm_high * track_rect.width + track_rect.x;
637634

638635
/* Draw inactive track (full width) */
639-
ctx->renderer.draw_box(track_rect, track_height * 0.5f, inactive_color,
640-
ctx->renderer.user);
636+
iui_emit_box(ctx, track_rect, track_height * 0.5f, inactive_color);
641637

642638
/* Draw active track (between thumbs) */
643639
float active_x = thumb_low_x;
644640
float active_w = thumb_high_x - thumb_low_x;
645641
if (active_w > 0.f) {
646-
ctx->renderer.draw_box(
647-
(iui_rect_t) {active_x, track_rect.y, active_w, track_height},
648-
track_height * 0.5f, active_color, ctx->renderer.user);
642+
iui_emit_box(
643+
ctx, (iui_rect_t) {active_x, track_rect.y, active_w, track_height},
644+
track_height * 0.5f, active_color);
649645
}
650646

651647
/* State layers on hover/drag */
@@ -660,9 +656,10 @@ bool iui_range_slider(iui_context *ctx,
660656
uint8_t alpha =
661657
dragging ? IUI_STATE_DRAG_ALPHA : IUI_STATE_HOVER_ALPHA;
662658
uint32_t sc = iui_state_layer(handle_color, alpha);
663-
ctx->renderer.draw_box(
659+
iui_emit_box(
660+
ctx,
664661
(iui_rect_t) {tx - ss * 0.5f, center_y - ss * 0.5f, ss, ss},
665-
ss * 0.5f, sc, ctx->renderer.user);
662+
ss * 0.5f, sc);
666663
}
667664
}
668665

@@ -672,14 +669,16 @@ bool iui_range_slider(iui_context *ctx,
672669
float draw_size_low = draw_half_low * 2.f;
673670
float draw_size_high = draw_half_high * 2.f;
674671

675-
ctx->renderer.draw_box(
672+
iui_emit_box(
673+
ctx,
676674
(iui_rect_t) {thumb_low_x - draw_half_low, center_y - draw_half_low,
677675
draw_size_low, draw_size_low},
678-
draw_half_low, handle_color, ctx->renderer.user);
679-
ctx->renderer.draw_box(
676+
draw_half_low, handle_color);
677+
iui_emit_box(
678+
ctx,
680679
(iui_rect_t) {thumb_high_x - draw_half_high, center_y - draw_half_high,
681680
draw_size_high, draw_size_high},
682-
draw_half_high, handle_color, ctx->renderer.user);
681+
draw_half_high, handle_color);
683682

684683
/* MD3 validation */
685684
IUI_MD3_TRACK_SLIDER(touch_low, touch_low.height * 0.5f);
@@ -883,17 +882,14 @@ bool iui_button_styled(iui_context *ctx,
883882
iui_draw_focus_ring(ctx, button_rect, corner);
884883

885884
if (bg_color != 0) {
886-
ctx->renderer.draw_box(button_rect, corner, bg_color,
887-
ctx->renderer.user);
885+
iui_emit_box(ctx, button_rect, corner, bg_color);
888886
} else if (is_focused && focus_layer != 0) {
889887
/* MD3: Show focus state layer for text/outlined buttons (no bg) */
890-
ctx->renderer.draw_box(button_rect, corner, focus_layer,
891-
ctx->renderer.user);
888+
iui_emit_box(ctx, button_rect, corner, focus_layer);
892889
} else if (state == IUI_STATE_HOVERED && hover_layer != 0) {
893890
/* MD3: Text buttons show state layer on hover (no bg, but visible
894891
* hover) */
895-
ctx->renderer.draw_box(button_rect, corner, hover_layer,
896-
ctx->renderer.user);
892+
iui_emit_box(ctx, button_rect, corner, hover_layer);
897893
}
898894

899895
/* Draw border if specified (for outlined buttons) */

0 commit comments

Comments
 (0)