From 01c96077c45171fcc39c06f7b3c4719aa8cc83c8 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 8 Mar 2026 16:34:29 +0800 Subject: [PATCH] Improve rendering: ink-bounds, draw batching, text cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- include/iui.h | 21 +- src/appbar.c | 18 +- src/basic.c | 78 +++--- src/chips.c | 73 +++--- src/container.c | 113 ++++----- src/core.c | 26 +- src/dialog.c | 18 +- src/draw.c | 401 +++++++++++++++++++++++++++---- src/fab.c | 6 +- src/icons.c | 5 +- src/input.c | 108 ++++----- src/internal.h | 98 +++++++- src/layout.c | 19 +- src/list.c | 18 +- src/menu.c | 11 +- src/navigation.c | 62 ++--- src/pickers.c | 62 ++--- src/searchbar.c | 21 +- src/tabs.c | 17 +- tests/bench-render.c | 552 +++++++++++++++++++++++++++++++++++++++++++ 20 files changed, 1306 insertions(+), 421 deletions(-) create mode 100644 tests/bench-render.c diff --git a/include/iui.h b/include/iui.h index 7c36a06..b8d6fc6 100644 --- a/include/iui.h +++ b/include/iui.h @@ -3245,14 +3245,16 @@ int iui_a11y_describe(const iui_a11y_hint *hint, char *buf, size_t buf_size); /* Performance Optimization API * - * Three core performance systems (always available, disabled by default): + * Four core performance systems (always available, disabled by default): * 1. Draw Call Batching - buffers draw commands to reduce state changes * 2. Dirty Rectangle Tracking - skips redrawing unchanged regions - * 3. Text Width Caching - caches text measurement results + * 3. Ink-Bounds Tracking - tracks union bounding box of all draw calls + * 4. Text Width Caching - caches text measurement results * * Usage: * iui_batch_enable(ctx, true); // Enable draw batching * iui_dirty_enable(ctx, true); // Enable dirty rect tracking + * iui_ink_bounds_enable(ctx, true); // Enable ink-bounds tracking * iui_text_cache_enable(ctx, true); // Enable text cache */ @@ -3291,6 +3293,21 @@ void iui_dirty_invalidate_all(iui_context *ctx); */ int iui_dirty_count(const iui_context *ctx); +/* Ink-Bounds Tracking + * Tracks the union bounding box of all draw calls within a frame. + * When enabled, backends can query the drawn region to blit only the + * ink-bounds area instead of the full framebuffer. + */ +void iui_ink_bounds_enable(iui_context *ctx, bool enable); + +/* Query the ink-bounds rectangle for the current frame. + * Returns false if no draw calls were made this frame. + */ +bool iui_ink_bounds_get(const iui_context *ctx, iui_rect_t *out); + +/* Check if any draw calls were made this frame */ +bool iui_ink_bounds_valid(const iui_context *ctx); + /* Text Width Caching * Enable/disable text measurement caching. When enabled, text width * calculations are cached to avoid redundant measurements. diff --git a/src/appbar.c b/src/appbar.c index 7e78a8d..ee1341b 100644 --- a/src/appbar.c +++ b/src/appbar.c @@ -70,7 +70,7 @@ bool iui_top_app_bar(iui_context *ctx, /* Draw background (no corner radius for app bar) */ iui_rect_t bar_rect = {bar_x, bar_y, bar_width, bar_height}; - ctx->renderer.draw_box(bar_rect, 0.f, bg_color, ctx->renderer.user); + iui_emit_box(ctx, bar_rect, 0.f, bg_color); /* Draw elevation shadow if scrolled (Level 2) */ if (collapse_progress > 0.f) { @@ -108,13 +108,13 @@ bool iui_top_app_bar(iui_context *ctx, if (nav_pressed) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_PRESS_ALPHA); - ctx->renderer.draw_box(nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } else if (nav_hovered) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } /* Draw menu icon */ @@ -237,13 +237,13 @@ bool iui_top_app_bar_action(iui_context *ctx, const char *icon) if (pressed) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_PRESS_ALPHA); - ctx->renderer.draw_box(action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } else if (hovered) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } /* Draw action icon */ diff --git a/src/basic.c b/src/basic.c index 722274f..9d2c858 100644 --- a/src/basic.c +++ b/src/basic.c @@ -41,9 +41,9 @@ void iui_segmented(iui_context *ctx, *selected = 0; /* MD3: Draw unified pill background (visible container for all segments) */ - ctx->renderer.draw_box( - (iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height}, - pill_radius, ctx->colors.surface_container_highest, ctx->renderer.user); + iui_emit_box( + ctx, (iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height}, + pill_radius, ctx->colors.surface_container_highest); /* Track component for MD3 validation */ IUI_MD3_TRACK_SEGMENTED( @@ -63,9 +63,8 @@ void iui_segmented(iui_context *ctx, if (*selected == 0 || *selected == num_entries - 1) corner = pill_radius; - ctx->renderer.draw_box( - (iui_rect_t) {sel_x, seg_y, seg_width, seg_height}, corner, - ctx->colors.secondary_container, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {sel_x, seg_y, seg_width, seg_height}, + corner, ctx->colors.secondary_container); } /* Draw each segment */ @@ -92,9 +91,9 @@ void iui_segmented(iui_context *ctx, uint32_t hover_color = iui_state_layer( ctx->colors.on_surface, iui_state_get_alpha(seg_state)); float corner = (i == 0 || i == num_entries - 1) ? pill_radius : 0.f; - ctx->renderer.draw_box( - (iui_rect_t) {seg_x, seg_y, seg_width, seg_height}, corner, - hover_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {seg_x, seg_y, seg_width, seg_height}, + corner, hover_color); } /* Handle selection change */ @@ -288,16 +287,15 @@ float iui_slider_ex(iui_context *ctx, iui_get_component_state(ctx, touch_rect, disabled); /* Draw inactive track (full width, behind active track) */ - ctx->renderer.draw_box(track_rect, track_rect.height * .5f, inactive_color, - ctx->renderer.user); + iui_emit_box(ctx, track_rect, track_rect.height * .5f, inactive_color); /* Draw active track (left side up to thumb) */ float active_width = thumb_x - track_rect.x; if (active_width > 0) { - ctx->renderer.draw_box((iui_rect_t) {track_rect.x, track_rect.y, - active_width, track_rect.height}, - track_rect.height * .5f, active_color, - ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {track_rect.x, track_rect.y, active_width, + track_rect.height}, + track_rect.height * .5f, active_color); } /* Handle thumb interaction */ @@ -379,9 +377,9 @@ float iui_slider_ex(iui_context *ctx, uint8_t alpha = is_dragging ? IUI_STATE_DRAG_ALPHA : IUI_STATE_HOVER_ALPHA; uint32_t state_color = iui_state_layer(handle_color, alpha); - ctx->renderer.draw_box( - (iui_rect_t) {state_x, state_y, state_size, state_size}, - state_size * 0.5f, state_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {state_x, state_y, state_size, state_size}, + state_size * 0.5f, state_color); } /* Draw value indicator bubble during drag */ @@ -415,10 +413,10 @@ float iui_slider_ex(iui_context *ctx, } /* Draw indicator background (pill shape with primary color) */ - ctx->renderer.draw_box((iui_rect_t) {indicator_x, indicator_y, - indicator_width, indicator_height}, - indicator_height * 0.5f, active_color, - ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {indicator_x, indicator_y, indicator_width, + indicator_height}, + indicator_height * 0.5f, active_color); /* Draw value text centered in indicator */ iui_rect_t indicator_text_rect = { @@ -439,8 +437,7 @@ float iui_slider_ex(iui_context *ctx, } /* Draw thumb (circle) */ - ctx->renderer.draw_box(thumb_rect, half_size, handle_color, - ctx->renderer.user); + iui_emit_box(ctx, thumb_rect, half_size, handle_color); iui_newline(ctx); @@ -636,16 +633,15 @@ bool iui_range_slider(iui_context *ctx, thumb_high_x = norm_high * track_rect.width + track_rect.x; /* Draw inactive track (full width) */ - ctx->renderer.draw_box(track_rect, track_height * 0.5f, inactive_color, - ctx->renderer.user); + iui_emit_box(ctx, track_rect, track_height * 0.5f, inactive_color); /* Draw active track (between thumbs) */ float active_x = thumb_low_x; float active_w = thumb_high_x - thumb_low_x; if (active_w > 0.f) { - ctx->renderer.draw_box( - (iui_rect_t) {active_x, track_rect.y, active_w, track_height}, - track_height * 0.5f, active_color, ctx->renderer.user); + iui_emit_box( + ctx, (iui_rect_t) {active_x, track_rect.y, active_w, track_height}, + track_height * 0.5f, active_color); } /* State layers on hover/drag */ @@ -660,9 +656,10 @@ bool iui_range_slider(iui_context *ctx, uint8_t alpha = dragging ? IUI_STATE_DRAG_ALPHA : IUI_STATE_HOVER_ALPHA; uint32_t sc = iui_state_layer(handle_color, alpha); - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {tx - ss * 0.5f, center_y - ss * 0.5f, ss, ss}, - ss * 0.5f, sc, ctx->renderer.user); + ss * 0.5f, sc); } } @@ -672,14 +669,16 @@ bool iui_range_slider(iui_context *ctx, float draw_size_low = draw_half_low * 2.f; float draw_size_high = draw_half_high * 2.f; - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {thumb_low_x - draw_half_low, center_y - draw_half_low, draw_size_low, draw_size_low}, - draw_half_low, handle_color, ctx->renderer.user); - ctx->renderer.draw_box( + draw_half_low, handle_color); + iui_emit_box( + ctx, (iui_rect_t) {thumb_high_x - draw_half_high, center_y - draw_half_high, draw_size_high, draw_size_high}, - draw_half_high, handle_color, ctx->renderer.user); + draw_half_high, handle_color); /* MD3 validation */ IUI_MD3_TRACK_SLIDER(touch_low, touch_low.height * 0.5f); @@ -883,17 +882,14 @@ bool iui_button_styled(iui_context *ctx, iui_draw_focus_ring(ctx, button_rect, corner); if (bg_color != 0) { - ctx->renderer.draw_box(button_rect, corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner, bg_color); } else if (is_focused && focus_layer != 0) { /* MD3: Show focus state layer for text/outlined buttons (no bg) */ - ctx->renderer.draw_box(button_rect, corner, focus_layer, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner, focus_layer); } else if (state == IUI_STATE_HOVERED && hover_layer != 0) { /* MD3: Text buttons show state layer on hover (no bg, but visible * hover) */ - ctx->renderer.draw_box(button_rect, corner, hover_layer, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner, hover_layer); } /* Draw border if specified (for outlined buttons) */ diff --git a/src/chips.c b/src/chips.c index 8cf43b8..ec23406 100644 --- a/src/chips.c +++ b/src/chips.c @@ -181,8 +181,7 @@ static bool iui_chip_internal(iui_context *ctx, /* Draw chip container */ if (draw_container) { - ctx->renderer.draw_box(chip_rect, corner_radius, container_color, - ctx->renderer.user); + iui_emit_box(ctx, chip_rect, corner_radius, container_color); } /* Draw outline */ @@ -190,62 +189,63 @@ static bool iui_chip_internal(iui_context *ctx, float outline_width = 1.f; if (ctx->renderer.draw_arc) { - /* Best quality: use arcs for smooth rounded corners */ + /* Best quality: use arcs for smooth rounded corners. + * Track ink-bounds for the full chip outline once. + * Expand by outline_width to include arc stroke thickness + * so dirty-region redraw does not miss corner pixels. + */ + iui_ink_bounds_extend(ctx, chip_rect.x - outline_width, + chip_rect.y - outline_width, + chip_rect.width + 2 * outline_width, + chip_rect.height + 2 * outline_width); /* Top edge */ iui_rect_t top_edge = {chip_rect.x + corner_radius, chip_rect.y, chip_rect.width - 2 * corner_radius, outline_width}; - ctx->renderer.draw_box(top_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, top_edge, 0.f, outline_color); /* Bottom edge */ iui_rect_t bottom_edge = { chip_rect.x + corner_radius, chip_rect.y + chip_rect.height - outline_width, chip_rect.width - 2 * corner_radius, outline_width}; - ctx->renderer.draw_box(bottom_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, bottom_edge, 0.f, outline_color); /* Left edge */ iui_rect_t left_edge = {chip_rect.x, chip_rect.y + corner_radius, outline_width, chip_rect.height - 2 * corner_radius}; - ctx->renderer.draw_box(left_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, left_edge, 0.f, outline_color); /* Right edge */ iui_rect_t right_edge = { chip_rect.x + chip_rect.width - outline_width, chip_rect.y + corner_radius, outline_width, chip_rect.height - 2 * corner_radius}; - ctx->renderer.draw_box(right_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, right_edge, 0.f, outline_color); /* Top-left corner arc */ - ctx->renderer.draw_arc(chip_rect.x + corner_radius, - chip_rect.y + corner_radius, corner_radius, - IUI_PI, 1.5f * IUI_PI, outline_width, - outline_color, ctx->renderer.user); + iui_draw_arc_soft(ctx, chip_rect.x + corner_radius, + chip_rect.y + corner_radius, corner_radius, + IUI_PI, 1.5f * IUI_PI, outline_width, + outline_color); /* Top-right corner arc */ - ctx->renderer.draw_arc( - chip_rect.x + chip_rect.width - corner_radius, + iui_draw_arc_soft( + ctx, chip_rect.x + chip_rect.width - corner_radius, chip_rect.y + corner_radius, corner_radius, 1.5f * IUI_PI, - 2.f * IUI_PI, outline_width, outline_color, ctx->renderer.user); + 2.f * IUI_PI, outline_width, outline_color); /* Bottom-right corner arc */ - ctx->renderer.draw_arc( - chip_rect.x + chip_rect.width - corner_radius, + iui_draw_arc_soft( + ctx, chip_rect.x + chip_rect.width - corner_radius, chip_rect.y + chip_rect.height - corner_radius, corner_radius, - 0.f, 0.5f * IUI_PI, outline_width, outline_color, - ctx->renderer.user); + 0.f, 0.5f * IUI_PI, outline_width, outline_color); /* Bottom-left corner arc */ - ctx->renderer.draw_arc( - chip_rect.x + corner_radius, - chip_rect.y + chip_rect.height - corner_radius, corner_radius, - 0.5f * IUI_PI, IUI_PI, outline_width, outline_color, - ctx->renderer.user); + iui_draw_arc_soft(ctx, chip_rect.x + corner_radius, + chip_rect.y + chip_rect.height - corner_radius, + corner_radius, 0.5f * IUI_PI, IUI_PI, + outline_width, outline_color); } else if (draw_container) { /* Fallback with container: overlay approach creates outline * by drawing outer rect with outline color and inner rect with * container color to simulate border */ - ctx->renderer.draw_box(chip_rect, corner_radius, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, chip_rect, corner_radius, outline_color); /* Inner rounded rect (container color) to create outline effect */ iui_rect_t inner_rect = {chip_rect.x + outline_width, chip_rect.y + outline_width, @@ -254,8 +254,7 @@ static bool iui_chip_internal(iui_context *ctx, float inner_corner = corner_radius > outline_width ? corner_radius - outline_width : 0.f; - ctx->renderer.draw_box(inner_rect, inner_corner, container_color, - ctx->renderer.user); + iui_emit_box(ctx, inner_rect, inner_corner, container_color); } else { /* Fallback without container: draw edges only (corners have small * gaps) @@ -264,25 +263,21 @@ static bool iui_chip_internal(iui_context *ctx, */ iui_rect_t top_edge_full = {chip_rect.x, chip_rect.y, chip_rect.width, outline_width}; - ctx->renderer.draw_box(top_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, top_edge_full, 0.f, outline_color); /* Bottom edge */ iui_rect_t bottom_edge_full = { chip_rect.x, chip_rect.y + chip_rect.height - outline_width, chip_rect.width, outline_width}; - ctx->renderer.draw_box(bottom_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, bottom_edge_full, 0.f, outline_color); /* Left edge */ iui_rect_t left_edge_full = {chip_rect.x, chip_rect.y, outline_width, chip_rect.height}; - ctx->renderer.draw_box(left_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, left_edge_full, 0.f, outline_color); /* Right edge */ iui_rect_t right_edge_full = { chip_rect.x + chip_rect.width - outline_width, chip_rect.y, outline_width, chip_rect.height}; - ctx->renderer.draw_box(right_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, right_edge_full, 0.f, outline_color); } } diff --git a/src/container.c b/src/container.c index 2b7a1a7..f5270b2 100644 --- a/src/container.c +++ b/src/container.c @@ -90,9 +90,9 @@ void iui_progress_linear(iui_context *ctx, }; /* Draw background track */ - ctx->renderer.draw_box(bar_rect, - bar_rect.height * 0.5f, /* Pill-shaped corners */ - ctx->colors.surface_container, ctx->renderer.user); + iui_emit_box(ctx, bar_rect, + bar_rect.height * 0.5f, /* Pill-shaped corners */ + ctx->colors.surface_container); /* Draw progress indicator */ float progress_ratio = @@ -113,19 +113,19 @@ void iui_progress_linear(iui_context *ctx, float draw_x = fmaxf(anim_x, bar_rect.x); float draw_width = fminf(anim_x + bar_width, bar_rect.x + bar_rect.width) - draw_x; - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {draw_x, bar_rect.y, draw_width, bar_rect.height}, - bar_rect.height * 0.5f, ctx->colors.primary, - ctx->renderer.user); + bar_rect.height * 0.5f, ctx->colors.primary); } } else { /* Draw determinate progress */ if (filled_width > 0) { - ctx->renderer.draw_box( - (iui_rect_t) {bar_rect.x, bar_rect.y, filled_width, - bar_rect.height}, - bar_rect.height * 0.5f, /* Pill-shaped corners */ - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {bar_rect.x, bar_rect.y, filled_width, + bar_rect.height}, + bar_rect.height * 0.5f, /* Pill-shaped corners */ + ctx->colors.primary); } } @@ -250,8 +250,7 @@ bool iui_snackbar(iui_context *ctx, /* Draw snackbar background using inverse colors for contrast */ iui_rect_t bar_rect = {snackbar_x, snackbar_y, snackbar_width, snackbar_height}; - ctx->renderer.draw_box(bar_rect, corner_radius, ctx->colors.inverse_surface, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, corner_radius, ctx->colors.inverse_surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_SNACKBAR(bar_rect, corner_radius); @@ -284,8 +283,7 @@ bool iui_snackbar(iui_context *ctx, pressed ? IUI_STATE_PRESS_ALPHA : IUI_STATE_HOVER_ALPHA; uint32_t layer_color = iui_state_layer(ctx->colors.inverse_primary, alpha); - ctx->renderer.draw_box(action_rect, corner_radius, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, action_rect, corner_radius, layer_color); } /* Draw action text */ @@ -469,9 +467,8 @@ bool iui_scroll_end(iui_context *ctx, iui_scroll_state *state) thumb_rect.y = thumb_y; } - ctx->renderer.draw_box(track_rect, scrollbar_width * 0.5f, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, track_rect, scrollbar_width * 0.5f, + ctx->colors.surface_container_high); uint32_t thumb_color = ctx->colors.outline; if (is_dragging) { @@ -479,8 +476,7 @@ bool iui_scroll_end(iui_context *ctx, iui_scroll_state *state) } else if (in_rect(&thumb_rect, ctx->mouse_pos)) { thumb_color = ctx->colors.on_surface_variant; } - ctx->renderer.draw_box(thumb_rect, scrollbar_width * 0.5f, thumb_color, - ctx->renderer.user); + iui_emit_box(ctx, thumb_rect, scrollbar_width * 0.5f, thumb_color); } /* Pop after scrollbar draw so the thumb stays bounded by the viewport */ @@ -576,10 +572,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx, iui_rect_t scrim_rect = {0, 0, screen_width, screen_height}; uint8_t scrim_alpha = (uint8_t) (IUI_SCRIM_ALPHA * state->anim_progress); - ctx->renderer.draw_box( - scrim_rect, 0.f, - (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF), - ctx->renderer.user); + iui_emit_box(ctx, scrim_rect, 0.f, + (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF)); /* Push modal layer */ iui_push_layer(ctx, 100); @@ -593,9 +587,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx, /* Draw sheet background with rounded top corners */ iui_rect_t sheet_rect = {0, sheet_y, screen_width, current_height + 100.f}; - ctx->renderer.draw_box(sheet_rect, IUI_BOTTOM_SHEET_CORNER_RADIUS, - ctx->colors.surface_container_low, - ctx->renderer.user); + iui_emit_box(ctx, sheet_rect, IUI_BOTTOM_SHEET_CORNER_RADIUS, + ctx->colors.surface_container_low); /* Track component for MD3 validation (use logical height, not padded rect) */ @@ -609,9 +602,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx, iui_rect_t handle_rect = {handle_x, handle_y, IUI_BOTTOM_SHEET_DRAG_HANDLE_WIDTH, IUI_BOTTOM_SHEET_DRAG_HANDLE_HEIGHT}; - ctx->renderer.draw_box(handle_rect, - IUI_BOTTOM_SHEET_DRAG_HANDLE_HEIGHT * 0.5f, - ctx->colors.on_surface_variant, ctx->renderer.user); + iui_emit_box(ctx, handle_rect, IUI_BOTTOM_SHEET_DRAG_HANDLE_HEIGHT * 0.5f, + ctx->colors.on_surface_variant); /* Handle drag interaction */ iui_rect_t drag_area = {0, sheet_y, screen_width, 48.f}; @@ -719,8 +711,8 @@ void iui_tooltip(iui_context *ctx, const char *text) /* Draw tooltip background using inverse colors for contrast */ iui_rect_t tooltip_rect = {x, y, width, height}; - ctx->renderer.draw_box(tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, - ctx->colors.inverse_surface, ctx->renderer.user); + iui_emit_box(ctx, tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, + ctx->colors.inverse_surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_TOOLTIP(tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS); @@ -793,8 +785,8 @@ bool iui_tooltip_rich(iui_context *ctx, /* Draw background */ iui_rect_t tooltip_rect = {x, y, width, height}; - ctx->renderer.draw_box(tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, - ctx->colors.inverse_surface, ctx->renderer.user); + iui_emit_box(ctx, tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, + ctx->colors.inverse_surface); /* Draw content */ float text_y = y + IUI_TOOLTIP_PADDING; @@ -822,8 +814,7 @@ bool iui_tooltip_rich(iui_context *ctx, if (hovered) { uint32_t hover_color = iui_state_layer(ctx->colors.inverse_primary, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(action_rect, 4.f, hover_color, - ctx->renderer.user); + iui_emit_box(ctx, action_rect, 4.f, hover_color); } iui_internal_draw_text(ctx, x + IUI_TOOLTIP_PADDING + 4.f, text_y, @@ -848,10 +839,10 @@ void iui_badge_dot(iui_context *ctx, float anchor_x, float anchor_y) float x = anchor_x - IUI_BADGE_OFFSET_X; float y = anchor_y + IUI_BADGE_OFFSET_Y; - ctx->renderer.draw_box( - (iui_rect_t) {x - radius, y - radius, IUI_BADGE_DOT_SIZE, - IUI_BADGE_DOT_SIZE}, - radius, ctx->colors.error, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {x - radius, y - radius, IUI_BADGE_DOT_SIZE, + IUI_BADGE_DOT_SIZE}, + radius, ctx->colors.error); } void iui_badge_number(iui_context *ctx, @@ -886,8 +877,8 @@ void iui_badge_number(iui_context *ctx, float y = anchor_y + IUI_BADGE_OFFSET_Y - height * 0.5f; /* Draw badge background */ - ctx->renderer.draw_box((iui_rect_t) {x, y, width, height}, radius, - ctx->colors.error, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {x, y, width, height}, radius, + ctx->colors.error); /* Draw badge text centered */ float text_x = x + (width - text_width) * 0.5f; @@ -920,7 +911,7 @@ static bool draw_banner_action(iui_context *ctx, if (hovered) { uint32_t hover = iui_state_layer(ctx->colors.primary, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(btn_rect, 4.f, hover, ctx->renderer.user); + iui_emit_box(ctx, btn_rect, 4.f, hover); if (ctx->mouse_released & IUI_MOUSE_LEFT) clicked = true; } @@ -947,17 +938,16 @@ int iui_banner(iui_context *ctx, const iui_banner_options *options) height}; /* Draw banner background */ - ctx->renderer.draw_box(banner_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, banner_rect, 0.f, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_BANNER(banner_rect, 0.f); /* Draw divider at bottom */ - ctx->renderer.draw_box( - (iui_rect_t) {banner_rect.x, banner_rect.y + height - 1.f, - banner_rect.width, 1.f}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {banner_rect.x, banner_rect.y + height - 1.f, + banner_rect.width, 1.f}, + 0.f, ctx->colors.outline_variant); float content_x = banner_rect.x + IUI_BANNER_PADDING; @@ -1093,8 +1083,7 @@ void iui_table_header(iui_context *ctx, /* Draw header cell background */ iui_rect_t cell_rect = {state->current_x, state->row_y, width, height}; - ctx->renderer.draw_box(cell_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, cell_rect, 0.f, ctx->colors.surface_container); /* Draw header text with label_large style (bold) */ float text_x = state->current_x + IUI_TABLE_CELL_PADDING; @@ -1109,11 +1098,11 @@ void iui_table_header(iui_context *ctx, if (state->current_col >= state->cols) { state->row_y += height; /* Draw divider below header */ - ctx->renderer.draw_box( - (iui_rect_t) {state->start_x, - state->row_y - IUI_TABLE_DIVIDER_HEIGHT, - ctx->layout.width, IUI_TABLE_DIVIDER_HEIGHT}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {state->start_x, + state->row_y - IUI_TABLE_DIVIDER_HEIGHT, + ctx->layout.width, IUI_TABLE_DIVIDER_HEIGHT}, + 0.f, ctx->colors.outline_variant); state->in_header = false; } } @@ -1157,9 +1146,7 @@ void iui_table_cell(iui_context *ctx, /* Alternate row background for zebra striping */ if (state->row_index % 2 == 1) { iui_rect_t cell_rect = {state->current_x, state->row_y, width, height}; - ctx->renderer.draw_box(cell_rect, 0.f, - ctx->colors.surface_container_low, - ctx->renderer.user); + iui_emit_box(ctx, cell_rect, 0.f, ctx->colors.surface_container_low); } /* Draw cell text */ @@ -1180,10 +1167,11 @@ void iui_table_row_end(iui_context *ctx, iui_table_state *state) state->row_y += IUI_TABLE_ROW_HEIGHT; /* Draw row divider */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {state->start_x, state->row_y - IUI_TABLE_DIVIDER_HEIGHT, ctx->layout.width, IUI_TABLE_DIVIDER_HEIGHT}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + 0.f, ctx->colors.outline_variant); } void iui_table_end(iui_context *ctx, const iui_table_state *state) @@ -1240,8 +1228,7 @@ bool iui_carousel_item(iui_context *ctx, uint32_t bg_color = ctx->colors.surface_container; /* Draw background */ - ctx->renderer.draw_box(item_rect, IUI_CAROUSEL_CORNER_RADIUS, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, IUI_CAROUSEL_CORNER_RADIUS, bg_color); /* State layer */ iui_draw_state_layer(ctx, item_rect, IUI_CAROUSEL_CORNER_RADIUS, diff --git a/src/core.c b/src/core.c index 5e140eb..bfd6f69 100644 --- a/src/core.c +++ b/src/core.c @@ -542,13 +542,15 @@ static void iui_emit_glyph(iui_context *ctx, float mx = (x + nx) * 0.5f, my = (y + ny) * 0.5f; float w = fmaxf(len, 1.f), h = fmaxf(pen_w, 1.f); if (fabsf(dx) > fabsf(dy)) - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {fminf(x, nx), my - h * 0.5f, w, h}, 0, - color, ctx->renderer.user); + color); else - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {mx - h * 0.5f, fminf(y, ny), h, len}, - 0, color, ctx->renderer.user); + 0, color); } } x = nx, y = ny; @@ -592,15 +594,17 @@ static void iui_emit_glyph(iui_context *ctx, if (len > 0.5f) { float w = fmaxf(len, 1.f), h = fmaxf(pen_w, 1.f); if (fabsf(dx) > fabsf(dy)) - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {fminf(x, nx), (y + ny) * 0.5f - h * 0.5f, w, h}, - 0, color, ctx->renderer.user); + 0, color); else - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {(x + nx) * 0.5f - h * 0.5f, fminf(y, ny), h, len}, - 0, color, ctx->renderer.user); + 0, color); } x = nx; y = ny; @@ -804,6 +808,7 @@ iui_context *iui_init(const iui_config_t *config) /* Initialize performance systems (disabled by default) */ iui_batch_init(ctx); iui_dirty_init(ctx); + iui_ink_bounds_init(ctx); iui_text_cache_init(ctx); return ctx; } @@ -998,7 +1003,7 @@ void iui_draw_focus_ring(iui_context *ctx, uint32_t ring_color = ctx->colors.primary; /* Draw outer ring (full box) */ - ctx->renderer.draw_box(ring, outer_corner, ring_color, ctx->renderer.user); + iui_emit_box(ctx, ring, outer_corner, ring_color); /* Draw inner cutout with surface color to create ring effect */ iui_rect_t inner = { @@ -1010,8 +1015,7 @@ void iui_draw_focus_ring(iui_context *ctx, float inner_corner = corner_radius + offset; /* Use surface color to "cut out" the inner area */ - ctx->renderer.draw_box(inner, inner_corner, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, inner, inner_corner, ctx->colors.surface); } #else /* Stub: Focus ring disabled - draw nothing */ diff --git a/src/dialog.c b/src/dialog.c index ded43e2..10b6ae1 100644 --- a/src/dialog.c +++ b/src/dialog.c @@ -170,17 +170,16 @@ int iui_dialog(iui_context *ctx, /* Draw scrim (semi-transparent overlay) */ uint32_t scrim_color = ctx->colors.scrim; - ctx->renderer.draw_box((iui_rect_t) {0, 0, screen_width, screen_height}, 0, - scrim_color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {0, 0, screen_width, screen_height}, 0, + scrim_color); /* Draw dialog shadow (elevation_3 for dialogs per MD3) */ float corner = IUI_DIALOG_CORNER_RADIUS; iui_draw_shadow(ctx, dialog_bounds, corner, IUI_ELEVATION_3); /* Draw dialog background */ - ctx->renderer.draw_box(dialog_bounds, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, dialog_bounds, corner, + ctx->colors.surface_container_high); /* Track component for MD3 validation */ IUI_MD3_TRACK_DIALOG(dialog_bounds, corner); @@ -239,8 +238,7 @@ int iui_dialog(iui_context *ctx, /* Primary button (rightmost) - filled style */ uint32_t bg_color = ctx->colors.primary; text_color = ctx->colors.on_primary; - ctx->renderer.draw_box(btn_rect, btn_corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, btn_rect, btn_corner, bg_color); } /* Draw state layer for hover/press */ @@ -341,8 +339,7 @@ bool iui_fullscreen_dialog_begin(iui_context *ctx, iui_register_blocking_region(ctx, screen_bounds); /* Draw full-screen surface background */ - ctx->renderer.draw_box(screen_bounds, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, screen_bounds, 0.f, ctx->colors.surface); /* Header bar dimensions */ float header_h = IUI_FULLSCREEN_DIALOG_HEADER_HEIGHT; @@ -436,8 +433,7 @@ bool iui_fullscreen_dialog_action(iui_context *ctx, if (dialog->action_count == 0) { /* Primary action - filled button */ - ctx->renderer.draw_box(btn_rect, corner, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, btn_rect, corner, ctx->colors.primary); text_color = ctx->colors.on_primary; } else { /* Secondary action - text button */ diff --git a/src/draw.c b/src/draw.c index eaa74f0..9d20af6 100644 --- a/src/draw.c +++ b/src/draw.c @@ -1,5 +1,200 @@ #include "internal.h" +#include + +/* Ink-bounds tracking */ + +void iui_ink_bounds_init(iui_context *ctx) +{ + if (!ctx) + return; + ctx->ink_bounds.min_x = FLT_MAX; + ctx->ink_bounds.min_y = FLT_MAX; + ctx->ink_bounds.max_x = -FLT_MAX; + ctx->ink_bounds.max_y = -FLT_MAX; + ctx->ink_bounds.valid = false; + ctx->ink_bounds.enabled = false; +} + +void iui_ink_bounds_reset(iui_context *ctx) +{ + if (!ctx) + return; + ctx->ink_bounds.min_x = FLT_MAX; + ctx->ink_bounds.min_y = FLT_MAX; + ctx->ink_bounds.max_x = -FLT_MAX; + ctx->ink_bounds.max_y = -FLT_MAX; + ctx->ink_bounds.valid = false; +} + +void iui_ink_bounds_enable(iui_context *ctx, bool enable) +{ + if (!ctx) + return; + ctx->ink_bounds.enabled = enable; + if (enable) + iui_ink_bounds_reset(ctx); +} + +bool iui_ink_bounds_get(const iui_context *ctx, iui_rect_t *out) +{ + if (!ctx || !ctx->ink_bounds.valid || !out) + return false; + out->x = ctx->ink_bounds.min_x; + out->y = ctx->ink_bounds.min_y; + out->width = ctx->ink_bounds.max_x - ctx->ink_bounds.min_x; + out->height = ctx->ink_bounds.max_y - ctx->ink_bounds.min_y; + return true; +} + +bool iui_ink_bounds_valid(const iui_context *ctx) +{ + return ctx && ctx->ink_bounds.valid; +} + +/* Draw call batch add functions */ + +bool iui_batch_add_rect(iui_context *ctx, + float x, + float y, + float w, + float h, + float radius, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) { + /* Buffer full: flush and retry */ + iui_batch_flush(ctx); + } + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_RECT; + cmd->data.rect.x = x; + cmd->data.rect.y = y; + cmd->data.rect.w = w; + cmd->data.rect.h = h; + cmd->data.rect.radius = radius; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_text(iui_context *ctx, + float x, + float y, + const char *text, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled || !text) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_TEXT; + cmd->data.text.x = x; + cmd->data.text.y = y; + /* Truncate text to inline buffer size */ + size_t len = strlen(text); + if (len >= sizeof(cmd->data.text.text)) + len = sizeof(cmd->data.text.text) - 1; + memcpy(cmd->data.text.text, text, len); + cmd->data.text.text[len] = '\0'; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_line(iui_context *ctx, + float x0, + float y0, + float x1, + float y1, + float width, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_LINE; + cmd->data.line.x0 = x0; + cmd->data.line.y0 = y0; + cmd->data.line.x1 = x1; + cmd->data.line.y1 = y1; + cmd->data.line.width = width; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_circle(iui_context *ctx, + float cx, + float cy, + float radius, + uint32_t fill_color, + uint32_t stroke_color, + float stroke_width) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_CIRCLE; + cmd->data.circle.cx = cx; + cmd->data.circle.cy = cy; + cmd->data.circle.radius = radius; + cmd->data.circle.fill_color = fill_color; + cmd->data.circle.stroke_width = stroke_width; + cmd->color = fill_color; + cmd->color2 = stroke_color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_arc(iui_context *ctx, + float cx, + float cy, + float radius, + float start_angle, + float end_angle, + float width, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_ARC; + cmd->data.arc.cx = cx; + cmd->data.arc.cy = cy; + cmd->data.arc.radius = radius; + cmd->data.arc.start_angle = start_angle; + cmd->data.arc.end_angle = end_angle; + cmd->data.arc.width = width; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + /* Text width caching */ void iui_text_cache_init(iui_context *ctx) @@ -26,14 +221,27 @@ void iui_text_cache_clear(iui_context *ctx) memset(ctx->text_cache.entries, 0, sizeof(ctx->text_cache.entries)); } +/* Mix font_height bits into text hash to prevent cross-size cache poisoning. + * Typography paths temporarily mutate ctx->font_height, so same text at + * different sizes must hash to different slots. + */ +static uint32_t text_cache_hash(const char *text, float font_height) +{ + uint32_t h = iui_hash_str(text); + uint32_t fh; + memcpy(&fh, &font_height, sizeof(fh)); + h ^= fh * 2654435761u; /* Knuth multiplicative hash mixing */ + if (h == 0) + h = 1; + return h; +} + static bool text_cache_get(iui_context *ctx, const char *text, float *width) { if (!ctx->text_cache.enabled || !text || !width) return false; - uint32_t hash = iui_hash_str(text); - if (hash == 0) - hash = 1; + uint32_t hash = text_cache_hash(text, ctx->font_height); /* Bitwise AND for power-of-2 cache size (faster than modulo) */ int start = hash & (IUI_TEXT_CACHE_SIZE - 1); @@ -45,8 +253,9 @@ static bool text_cache_get(iui_context *ctx, const char *text, float *width) ctx->text_cache.misses++; return false; } - /* Compare both hash and pointer to handle collisions */ - if (entry->hash == hash && entry->text == text) { + /* Compare hash, pointer, and font size to handle collisions */ + if (entry->hash == hash && entry->text == text && + entry->font_height == ctx->font_height) { *width = entry->width; if (entry->hits < 255) entry->hits++; @@ -63,9 +272,7 @@ static void text_cache_put(iui_context *ctx, const char *text, float width) if (!ctx->text_cache.enabled || !text) return; - uint32_t hash = iui_hash_str(text); - if (hash == 0) - hash = 1; + uint32_t hash = text_cache_hash(text, ctx->font_height); /* Bitwise AND for power-of-2 cache size (faster than modulo) */ int start = hash & (IUI_TEXT_CACHE_SIZE - 1), evict_idx = -1; @@ -75,10 +282,12 @@ static void text_cache_put(iui_context *ctx, const char *text, float width) int idx = (start + i) & (IUI_TEXT_CACHE_SIZE - 1); iui_text_cache_entry *entry = &ctx->text_cache.entries[idx]; - /* Empty slot or exact match (same hash and pointer) */ - if (entry->hash == 0 || (entry->hash == hash && entry->text == text)) { + /* Empty slot or exact match (same hash, pointer, and font size) */ + if (entry->hash == 0 || (entry->hash == hash && entry->text == text && + entry->font_height == ctx->font_height)) { entry->hash = hash; entry->text = text; + entry->font_height = ctx->font_height; entry->width = width; entry->hits = 1; return; @@ -94,6 +303,7 @@ static void text_cache_put(iui_context *ctx, const char *text, float width) iui_text_cache_entry *entry = &ctx->text_cache.entries[evict_idx]; entry->hash = hash; entry->text = text; + entry->font_height = ctx->font_height; entry->width = width; entry->hits = 1; } @@ -128,6 +338,9 @@ void iui_text_cache_frame_end(iui_context *ctx) /* Text Width and Drawing */ float iui_get_text_width(iui_context *ctx, const char *text) { + if (!ctx || !text) + return 0.f; + /* Check cache first */ float cached_width; if (text_cache_get(ctx, text, &cached_width)) @@ -187,6 +400,28 @@ void iui_internal_draw_text(iui_context *ctx, const char *text, uint32_t color) { + /* Track text bounding box in ink-bounds: (x, y) to (x + width, y + h) */ + if (ctx->ink_bounds.enabled && text && *text) { + float tw = iui_get_text_width(ctx, text); + iui_ink_bounds_extend(ctx, x, y, tw, ctx->font_height); + } + + /* Route through batch to preserve draw order with boxes. + * Only batch when the backend has draw_text; vector font rendering + * goes through iui_emit_box which is already batch-aware. + * Fall back to direct draw for text exceeding inline buffer (64 bytes) + * to avoid silent truncation producing different rendering. + */ + if (ctx->batch.enabled && ctx->renderer.draw_text) { + if (text && strlen(text) >= 64) { + iui_batch_flush(ctx); + ctx->renderer.draw_text(x, y, text, color, ctx->renderer.user); + return; + } + iui_batch_add_text(ctx, x, y, text, color); + return; + } + if (ctx->renderer.draw_text) ctx->renderer.draw_text(x, y, text, color, ctx->renderer.user); else @@ -330,8 +565,8 @@ static void iui_divider_internal(iui_context *ctx, /* Guard against negative width in narrow containers */ if (w > 0.f) { float x = ctx->layout.x + left_inset; - ctx->renderer.draw_box((iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, - ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, + ctx->colors.outline_variant); } /* MD3: 8dp vertical margin below (advance by 1dp line + 8dp margin) */ @@ -363,20 +598,22 @@ void iui_draw_rect_outline(iui_context *ctx, uint32_t color) { /* Top */ - ctx->renderer.draw_box((iui_rect_t) {rect.x, rect.y, rect.width, width}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {rect.x, rect.y, rect.width, width}, 0.f, + color); /* Bottom */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {rect.x, rect.y + rect.height - width, rect.width, width}, - 0.f, color, ctx->renderer.user); + 0.f, color); /* Left */ - ctx->renderer.draw_box((iui_rect_t) {rect.x, rect.y, width, rect.height}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {rect.x, rect.y, width, rect.height}, 0.f, + color); /* Right */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {rect.x + rect.width - width, rect.y, width, rect.height}, - 0.f, color, ctx->renderer.user); + 0.f, color); } /* Internal: Draw line with fallback to box */ @@ -389,6 +626,15 @@ void iui_draw_line_soft(iui_context *ctx, uint32_t color) { if (ctx->renderer.draw_line) { + /* Conservative AABB: bounding box of endpoints + half-width */ + float hw = width * 0.5f; + iui_ink_bounds_extend(ctx, fminf(x0, x1) - hw, fminf(y0, y1) - hw, + fabsf(x1 - x0) + width, fabsf(y1 - y0) + width); + /* Route through batch to preserve draw order with boxes */ + if (ctx->batch.enabled) { + iui_batch_add_line(ctx, x0, y0, x1, y1, width, color); + return; + } ctx->renderer.draw_line(x0, y0, x1, y1, width, color, ctx->renderer.user); } else { @@ -399,15 +645,15 @@ void iui_draw_line_soft(iui_context *ctx, /* For simple horizontal/vertical lines, draw_box is perfect */ if (fabsf(dy) < 0.1f) { - ctx->renderer.draw_box( - (iui_rect_t) {fminf(x0, x1), y0 - width * 0.5f, fabsf(dx), - width}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {fminf(x0, x1), y0 - width * 0.5f, + fabsf(dx), width}, + 0.f, color); } else if (fabsf(dx) < 0.1f) { - ctx->renderer.draw_box( - (iui_rect_t) {x0 - width * 0.5f, fminf(y0, y1), width, - fabsf(dy)}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {x0 - width * 0.5f, fminf(y0, y1), width, + fabsf(dy)}, + 0.f, color); } else { /* For diagonal lines without rotation support, approximate with * bounding box or thin rects. @@ -423,16 +669,16 @@ void iui_draw_line_soft(iui_context *ctx, */ if (fabsf(dx) > fabsf(dy)) { float my = (y0 + y1) * 0.5f; - ctx->renderer.draw_box( - (iui_rect_t) {fminf(x0, x1), my - width * 0.5f, fabsf(dx), - width}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {fminf(x0, x1), my - width * 0.5f, + fabsf(dx), width}, + 0.f, color); } else { float mx = (x0 + x1) * 0.5f; - ctx->renderer.draw_box( - (iui_rect_t) {mx - width * 0.5f, fminf(y0, y1), width, - fabsf(dy)}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {mx - width * 0.5f, fminf(y0, y1), + width, fabsf(dy)}, + 0.f, color); } } } @@ -448,14 +694,24 @@ void iui_draw_circle_soft(iui_context *ctx, float stroke_width) { if (ctx->renderer.draw_circle) { + float outer = radius + stroke_width; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + /* Route through batch to preserve draw order with boxes */ + if (ctx->batch.enabled) { + iui_batch_add_circle(ctx, cx, cy, radius, fill_color, stroke_color, + stroke_width); + return; + } ctx->renderer.draw_circle(cx, cy, radius, fill_color, stroke_color, stroke_width, ctx->renderer.user); } else { /* Fallback: draw box (square) approximating circle */ if (fill_color) { - ctx->renderer.draw_box((iui_rect_t) {cx - radius, cy - radius, - radius * 2.f, radius * 2.f}, - radius, fill_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {cx - radius, cy - radius, radius * 2.f, + radius * 2.f}, + radius, fill_color); } /* If stroked (border), simulate with two boxes or just one filled box * with stroke color if no fill. 'draw_box' does not support borders @@ -466,9 +722,10 @@ void iui_draw_circle_soft(iui_context *ctx, */ if (stroke_color && !fill_color) { /* Crude approximation: just draw a filled square for the outline */ - ctx->renderer.draw_box((iui_rect_t) {cx - radius, cy - radius, - radius * 2.f, radius * 2.f}, - radius, stroke_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {cx - radius, cy - radius, radius * 2.f, + radius * 2.f}, + radius, stroke_color); } } } @@ -484,6 +741,16 @@ void iui_draw_arc_soft(iui_context *ctx, uint32_t color) { if (ctx->renderer.draw_arc) { + /* Conservative: full circle bounding box + arc width */ + float outer = radius + width * 0.5f; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + /* Route through batch to preserve draw order with boxes */ + if (ctx->batch.enabled) { + iui_batch_add_arc(ctx, cx, cy, radius, start_angle, end_angle, + width, color); + return; + } ctx->renderer.draw_arc(cx, cy, radius, start_angle, end_angle, width, color, ctx->renderer.user); } else { @@ -494,9 +761,10 @@ void iui_draw_arc_soft(iui_context *ctx, * Simplified: just draw a small box at the center to indicate something * is there */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {cx - radius, cy - radius, radius * 2.f, radius * 2.f}, - radius, color, ctx->renderer.user); + radius, color); } } @@ -522,6 +790,13 @@ bool iui_draw_line(iui_context *ctx, { if (!ctx || !ctx->renderer.draw_line) return false; + float hw = width * 0.5f; + iui_ink_bounds_extend(ctx, fminf(x0, x1) - hw, fminf(y0, y1) - hw, + fabsf(x1 - x0) + width, fabsf(y1 - y0) + width); + if (ctx->batch.enabled) { + iui_batch_add_line(ctx, x0, y0, x1, y1, width, color); + return true; + } ctx->renderer.draw_line(x0, y0, x1, y1, width, color, ctx->renderer.user); return true; } @@ -536,6 +811,14 @@ bool iui_draw_circle(iui_context *ctx, { if (!ctx || !ctx->renderer.draw_circle) return false; + float outer = radius + stroke_width; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + if (ctx->batch.enabled) { + iui_batch_add_circle(ctx, cx, cy, radius, fill_color, stroke_color, + stroke_width); + return true; + } ctx->renderer.draw_circle(cx, cy, radius, fill_color, stroke_color, stroke_width, ctx->renderer.user); return true; @@ -552,6 +835,14 @@ bool iui_draw_arc(iui_context *ctx, { if (!ctx || !ctx->renderer.draw_arc) return false; + float outer = radius + width * 0.5f; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + if (ctx->batch.enabled) { + iui_batch_add_arc(ctx, cx, cy, radius, start_angle, end_angle, width, + color); + return true; + } ctx->renderer.draw_arc(cx, cy, radius, start_angle, end_angle, width, color, ctx->renderer.user); return true; @@ -635,8 +926,7 @@ void iui_draw_state_layer(iui_context *ctx, uint32_t layer_color = iui_state_layer(content_color, alpha); /* Draw the state layer overlay */ - ctx->renderer.draw_box(bounds, corner_radius, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, bounds, corner_radius, layer_color); } /* MD3 Elevation Shadow System @@ -728,8 +1018,7 @@ void iui_draw_shadow(iui_context *ctx, .height = bounds.height + spread * 2.f, }; - ctx->renderer.draw_box(rect, corner_radius + spread * 0.5f, color, - ctx->renderer.user); + iui_emit_box(ctx, rect, corner_radius + spread * 0.5f, color); } /* Render key shadow (directional, offset down) @@ -756,8 +1045,7 @@ void iui_draw_shadow(iui_context *ctx, .height = bounds.height + spread, }; - ctx->renderer.draw_box(rect, corner_radius + spread * 0.3f, color, - ctx->renderer.user); + iui_emit_box(ctx, rect, corner_radius + spread * 0.3f, color); } } @@ -794,7 +1082,7 @@ void iui_draw_elevated_box(iui_context *ctx, #endif /* Draw the box on top */ - ctx->renderer.draw_box(bounds, corner_radius, color, ctx->renderer.user); + iui_emit_box(ctx, bounds, corner_radius, color); } /* Clip stack functions */ @@ -807,7 +1095,8 @@ bool iui_push_clip(iui_context *ctx, iui_rect_t rect) if (ctx->clip.depth > 0) { iui_rect_t current = ctx->clip.stack[ctx->clip.depth - 1]; /* Compute right/bottom BEFORE clamping origin so the original - * rect.x + rect.width is used, not the shifted value. */ + * rect.x + rect.width is used, not the shifted value. + */ float right = fminf(rect.x + rect.width, current.x + current.width); float bottom = fminf(rect.y + rect.height, current.y + current.height); rect.x = fmaxf(rect.x, current.x); @@ -941,6 +1230,14 @@ static void batch_flush_internal(iui_context *ctx) } } ctx->batch.count = 0; + + /* Restore the logical clip state so subsequent direct draws (e.g. long-text + * fallback) use the correct clip rectangle, not whatever the last batched + * command happened to set. + */ + ctx->renderer.set_clip_rect(ctx->current_clip.minx, ctx->current_clip.miny, + ctx->current_clip.maxx, ctx->current_clip.maxy, + ctx->renderer.user); } void iui_batch_init(iui_context *ctx) diff --git a/src/fab.c b/src/fab.c index 87d062a..2af8f1c 100644 --- a/src/fab.c +++ b/src/fab.c @@ -55,8 +55,7 @@ static bool iui_fab_internal(iui_context *ctx, iui_draw_shadow(ctx, fab_rect, corner_radius, IUI_ELEVATION_3); /* Draw container background */ - ctx->renderer.draw_box(fab_rect, corner_radius, container_color, - ctx->renderer.user); + iui_emit_box(ctx, fab_rect, corner_radius, container_color); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, fab_rect, corner_radius, content_color, state); @@ -247,8 +246,7 @@ static bool iui_icon_button_internal(iui_context *ctx, /* Draw container background (if applicable) */ if (draw_container) { - ctx->renderer.draw_box(button_rect, corner_radius, container_color, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner_radius, container_color); } /* Draw outline (for outlined variant) */ diff --git a/src/icons.c b/src/icons.c index a0b9fbd..671712b 100644 --- a/src/icons.c +++ b/src/icons.c @@ -99,9 +99,8 @@ static void iui_draw_icon_error(iui_context *ctx, float dot_r = size * 0.08f; /* Vertical bar */ - ctx->renderer.draw_box( - (iui_rect_t) {cx - 1.5f, cy - bar_h, 3.f, bar_h * 1.4f}, 1.f, color, - ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {cx - 1.5f, cy - bar_h, 3.f, bar_h * 1.4f}, + 1.f, color); /* Dot below */ iui_draw_circle_soft(ctx, cx, cy + bar_h * 0.6f, dot_r, color, 0, 0); diff --git a/src/input.c b/src/input.c index e9a61de..1e7b4f5 100644 --- a/src/input.c +++ b/src/input.c @@ -45,19 +45,18 @@ static void textfield_draw_background(iui_context *ctx, const textfield_colors_t *c) { if (style == IUI_TEXTFIELD_OUTLINED) { - ctx->renderer.draw_box(rect, ctx->corner, c->border, - ctx->renderer.user); - ctx->renderer.draw_box((iui_rect_t) {rect.x + 1, rect.y + 1, - rect.width - 2, rect.height - 2}, - ctx->corner - 1, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, rect, ctx->corner, c->border); + iui_emit_box(ctx, + (iui_rect_t) {rect.x + 1, rect.y + 1, rect.width - 2, + rect.height - 2}, + ctx->corner - 1, ctx->colors.surface_container_high); } else { - ctx->renderer.draw_box(rect, ctx->corner, c->bg, ctx->renderer.user); - ctx->renderer.draw_box( + iui_emit_box(ctx, rect, ctx->corner, c->bg); + iui_emit_box( + ctx, (iui_rect_t) {rect.x, rect.y + rect.height - c->indicator_height, rect.width, c->indicator_height}, - 0.f, c->border, ctx->renderer.user); + 0.f, c->border); } } @@ -83,8 +82,7 @@ static void textfield_draw_icons(iui_context *ctx, /* Trailing icon hover effect */ if (!opts->disabled && in_rect(&trailing_rect, ctx->mouse_pos)) { uint32_t hover = iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(trailing_rect, icon_size * 0.5f, hover, - ctx->renderer.user); + iui_emit_box(ctx, trailing_rect, icon_size * 0.5f, hover); } iui_draw_textfield_icon(ctx, opts->trailing_icon, cx, cy, icon_size * 0.8f, icon_color); @@ -257,10 +255,10 @@ iui_textfield_result iui_textfield(iui_context *ctx, pos = len; float cursor_x = text_x + textfield_get_width_to_pos( ctx, buffer, pos, opts.password_mode); - ctx->renderer.draw_box( - (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, - ctx->font_height}, - 0.f, ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, + ctx->font_height}, + 0.f, ctx->colors.primary); } iui_newline(ctx); @@ -810,10 +808,11 @@ bool iui_edit_with_selection(iui_context *ctx, has_focus ? IUI_SELECTION_ALPHA : (IUI_SELECTION_ALPHA / 2); uint32_t selection_color = iui_state_layer(ctx->colors.primary, sel_alpha); - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {visible_start, text_y, visible_end - visible_start, ctx->font_height}, - 0.f, selection_color, ctx->renderer.user); + 0.f, selection_color); } } @@ -827,10 +826,11 @@ bool iui_edit_with_selection(iui_context *ctx, ctx, buffer, state->cursor, false); if (cursor_x >= text_clip.x && cursor_x <= text_clip.x + text_clip.width) { - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, ctx->font_height}, - 0.f, ctx->colors.primary, ctx->renderer.user); + 0.f, ctx->colors.primary); } } @@ -1080,10 +1080,11 @@ iui_textfield_result iui_textfield_with_selection( has_focus ? IUI_SELECTION_ALPHA : (IUI_SELECTION_ALPHA / 2); uint32_t selection_color = iui_state_layer(ctx->colors.primary, sel_alpha); - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {visible_start, text_y, visible_end - visible_start, ctx->font_height}, - 0.f, selection_color, ctx->renderer.user); + 0.f, selection_color); } } @@ -1110,10 +1111,11 @@ iui_textfield_result iui_textfield_with_selection( opts.password_mode); if (cursor_x >= text_x_start && cursor_x <= text_x_start + text_area_width) { - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, ctx->font_height}, - 0.f, ctx->colors.primary, ctx->renderer.user); + 0.f, ctx->colors.primary); } } @@ -1154,15 +1156,14 @@ static void checkbox_draw(iui_context *ctx, iui_state_layer(ctx->colors.on_primary, IUI_STATE_FOCUS_ALPHA); bg_color = iui_blend_color(bg_color, focus_layer); } - ctx->renderer.draw_box(widget_rect, corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, widget_rect, corner, bg_color); float mark_margin = widget_rect.width * 0.25f; - ctx->renderer.draw_box( - (iui_rect_t) {widget_rect.x + mark_margin, - widget_rect.y + mark_margin, - widget_rect.width - mark_margin * 2, - widget_rect.height - mark_margin * 2}, - corner * 0.5f, ctx->colors.on_primary, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {widget_rect.x + mark_margin, + widget_rect.y + mark_margin, + widget_rect.width - mark_margin * 2, + widget_rect.height - mark_margin * 2}, + corner * 0.5f, ctx->colors.on_primary); } else { uint32_t bg_color = ctx->colors.surface_container; if (is_focused) { @@ -1170,8 +1171,7 @@ static void checkbox_draw(iui_context *ctx, iui_state_layer(ctx->colors.primary, IUI_STATE_FOCUS_ALPHA); bg_color = iui_blend_color(bg_color, focus_layer); } - ctx->renderer.draw_box(widget_rect, corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, widget_rect, corner, bg_color); } } @@ -1191,15 +1191,16 @@ static void radio_draw(iui_context *ctx, IUI_STATE_FOCUS_ALPHA); bg_color = iui_blend_color(bg_color, focus_layer); } - ctx->renderer.draw_box(widget_rect, corner, bg_color, ctx->renderer.user); + iui_emit_box(ctx, widget_rect, corner, bg_color); if (is_active) { float dot_size = widget_rect.width * 0.5f; float dot_margin = (widget_rect.width - dot_size) * 0.5f; - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {widget_rect.x + dot_margin, widget_rect.y + dot_margin, dot_size, dot_size}, - dot_size * 0.5f, ctx->colors.on_primary, ctx->renderer.user); + dot_size * 0.5f, ctx->colors.on_primary); } } @@ -1369,17 +1370,17 @@ bool iui_switch(iui_context *ctx, } /* Draw track (MD3: filled track when on, surface_variant when off) */ - ctx->renderer.draw_box((iui_rect_t) {track_rect.x, track_rect.y, - track_rect.width, switch_track_height}, - corner, track_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {track_rect.x, track_rect.y, track_rect.width, + switch_track_height}, + corner, track_color); /* Draw thumb (filled circle) */ float thumb_y = track_rect.y + thumb_margin; uint32_t thumb_color = *value ? ctx->colors.on_primary : ctx->colors.outline; - ctx->renderer.draw_box( - (iui_rect_t) {thumb_x, thumb_y, thumb_size, thumb_size}, - thumb_size * 0.5f, thumb_color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {thumb_x, thumb_y, thumb_size, thumb_size}, + thumb_size * 0.5f, thumb_color); /* Optionally draw icons inside thumb (scale to fit within thumb) */ if ((*value && on_icon) || (!(*value) && off_icon)) { @@ -1522,7 +1523,7 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) options->disabled ? iui_state_layer(ctx->colors.surface_container_high, IUI_STATE_DISABLE_ALPHA) : ctx->colors.surface_container_highest; - ctx->renderer.draw_box(field_rect, corner, bg_color, ctx->renderer.user); + iui_emit_box(ctx, field_rect, corner, bg_color); /* Draw underline indicator for filled style */ if (!is_open) { @@ -1532,21 +1533,19 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) : ctx->colors.on_surface_variant; iui_rect_t underline = {field_rect.x, field_rect.y + field_h - 1.f, field_rect.width, 1.f}; - ctx->renderer.draw_box(underline, 0.f, line_color, ctx->renderer.user); + iui_emit_box(ctx, underline, 0.f, line_color); } else { /* Draw active indicator (2px primary underline) */ iui_rect_t underline = {field_rect.x, field_rect.y + field_h - 2.f, field_rect.width, 2.f}; - ctx->renderer.draw_box(underline, 0.f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, underline, 0.f, ctx->colors.primary); } /* Draw state layer */ if (!options->disabled && iui_state_is_interactive(state)) { uint32_t layer_color = iui_state_layer(ctx->colors.on_surface, iui_state_get_alpha(state)); - ctx->renderer.draw_box(field_rect, corner, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, field_rect, corner, layer_color); } /* Draw floating label */ @@ -1626,8 +1625,7 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) /* Draw menu shadow and background */ iui_draw_shadow(ctx, menu_rect, corner, IUI_ELEVATION_3); - ctx->renderer.draw_box(menu_rect, corner, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, menu_rect, corner, ctx->colors.surface_container); /* Draw menu items */ for (int i = 0; i < options->option_count; i++) { @@ -1647,14 +1645,12 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) /* Draw selection/hover background */ if (i == selected) { /* Selected item has subtle background */ - ctx->renderer.draw_box(item_rect, 0.f, - ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 0.f, + ctx->colors.secondary_container); } else if (iui_state_is_interactive(item_state)) { uint32_t layer_color = iui_state_layer( ctx->colors.on_surface, iui_state_get_alpha(item_state)); - ctx->renderer.draw_box(item_rect, 0.f, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 0.f, layer_color); } /* Draw item text */ diff --git a/src/internal.h b/src/internal.h index 6c56fb6..b6d98da 100644 --- a/src/internal.h +++ b/src/internal.h @@ -63,6 +63,10 @@ #define IUI_TEXT_CACHE_DECAY_COUNT 4 /* entries to age per frame */ #endif +/* Text cache uses (size - 1) bitmask for indexing; must be power of two */ +_Static_assert((IUI_TEXT_CACHE_SIZE & (IUI_TEXT_CACHE_SIZE - 1)) == 0, + "IUI_TEXT_CACHE_SIZE must be a power of two"); + /* Per-frame field tracking constants */ #ifndef IUI_MAX_TRACKED_TEXTFIELDS #define IUI_MAX_TRACKED_TEXTFIELDS 32 /* max text fields per frame */ @@ -353,12 +357,24 @@ typedef struct { bool enabled; } iui_dirty_state; +/* Per-frame ink-bounds tracking: union bounding box of all draw calls. + * Backend can use this to blit only the drawn region instead of the full + * framebuffer. Reset each frame in iui_begin_frame(). + */ +typedef struct { + float min_x, min_y, max_x, max_y; + bool valid; /* false = no draw calls this frame */ + bool enabled; +} iui_ink_bounds_t; + /* Text width cache entry */ typedef struct { - uint32_t hash; /* 0 = empty slot */ - const char *text; /* pointer for collision detection */ - float width; /* cached width */ - uint8_t hits; /* usage counter for eviction */ + uint32_t hash; /* 0 = empty slot */ + const char *text; /* pointer for collision detection */ + float font_height; /* font size at cache time (prevents cross-size + poisoning) */ + float width; /* cached width */ + uint8_t hits; /* usage counter for eviction */ } iui_text_cache_entry; /* Text width cache state */ @@ -501,6 +517,7 @@ struct iui_context { /* PERFORMANCE SYSTEMS - Optimization Caches */ iui_dirty_state dirty; + iui_ink_bounds_t ink_bounds; iui_text_cache_state text_cache; iui_draw_batch batch; iui_field_tracking field_tracking; @@ -1148,6 +1165,79 @@ bool iui_batch_add_arc(iui_context *ctx, float width, uint32_t color); +/* Ink-bounds tracking - internal functions (draw.c) + * Tracks the union bounding box of all draw calls within a frame. + * Note: iui_ink_bounds_enable/get/valid are public in iui.h + */ +void iui_ink_bounds_init(iui_context *ctx); +void iui_ink_bounds_reset(iui_context *ctx); + +/* Internal draw emission: updates ink-bounds and routes through batch when + * enabled. All internal code should call iui_emit_box() instead of + * ctx->renderer.draw_box() directly. + * + * Branchless accumulation: reset initializes bounds to FLT_MAX/-FLT_MAX + * so the first extend unconditionally wins all comparisons. Eliminates the + * per-call 'valid' branch on Arm Cortex-M pipeline. + */ +static inline void iui_ink_bounds_extend(iui_context *ctx, + float x, + float y, + float w, + float h) +{ + if (!ctx->ink_bounds.enabled) + return; + + /* Reject NaN/Inf early: NaN silently fails all comparisons (leaving bounds + * untouched) and Inf corrupts the bounding box geometry. isfinite() is C99 + * , typically a compiler builtin on ARM. + */ + if (!isfinite(x) || !isfinite(y) || !isfinite(w) || !isfinite(h)) + return; + + float x1 = x + w, y1 = y + h; + + /* Normalize: handle negative width/height */ + if (w < 0) { + float t = x; + x = x1; + x1 = t; + } + if (h < 0) { + float t = y; + y = y1; + y1 = t; + } + + if (x < ctx->ink_bounds.min_x) + ctx->ink_bounds.min_x = x; + if (y < ctx->ink_bounds.min_y) + ctx->ink_bounds.min_y = y; + if (x1 > ctx->ink_bounds.max_x) + ctx->ink_bounds.max_x = x1; + if (y1 > ctx->ink_bounds.max_y) + ctx->ink_bounds.max_y = y1; + ctx->ink_bounds.valid = true; +} + +/* Universal draw emission: updates ink-bounds then routes to batch or + * direct renderer. All internal code should call iui_emit_box() instead + * of ctx->renderer.draw_box() directly. + */ +static inline void iui_emit_box(iui_context *ctx, + iui_rect_t rect, + float radius, + uint32_t color) +{ + iui_ink_bounds_extend(ctx, rect.x, rect.y, rect.width, rect.height); + if (ctx->batch.enabled) + iui_batch_add_rect(ctx, rect.x, rect.y, rect.width, rect.height, radius, + color); + else + ctx->renderer.draw_box(rect, radius, color, ctx->renderer.user); +} + /* Dirty rectangle tracking - internal functions (layout.c) * Note: iui_dirty_enable/mark/invalidate_all/check/count are public in iui.h */ diff --git a/src/layout.c b/src/layout.c index 643d534..1e7ca75 100644 --- a/src/layout.c +++ b/src/layout.c @@ -511,6 +511,9 @@ void iui_begin_frame(iui_context *ctx, float delta_time) /* Reset batch command buffer for new frame */ ctx->batch.count = 0; + + /* Reset ink-bounds for new frame */ + iui_ink_bounds_reset(ctx); } bool iui_begin_window(iui_context *ctx, @@ -619,13 +622,12 @@ bool iui_begin_window(iui_context *ctx, /* MD3 Card/Dialog: unified surface_container_high background with outline */ - ctx->renderer.draw_box( - (iui_rect_t) {w->pos.x, w->pos.y, w->width, w->height}, ctx->corner, - ctx->colors.outline_variant, ctx->renderer.user); - ctx->renderer.draw_box((iui_rect_t) {w->pos.x + 1.f, w->pos.y + 1.f, - w->width - 2.f, w->height - 2.f}, - ctx->corner, ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {w->pos.x, w->pos.y, w->width, w->height}, + ctx->corner, ctx->colors.outline_variant); + iui_emit_box(ctx, + (iui_rect_t) {w->pos.x + 1.f, w->pos.y + 1.f, w->width - 2.f, + w->height - 2.f}, + ctx->corner, ctx->colors.surface_container_high); ctx->layout = (iui_rect_t) { .x = w->pos.x + ctx->padding, @@ -643,8 +645,7 @@ bool iui_begin_window(iui_context *ctx, /* draw resize handle */ if (w->options & IUI_WINDOW_RESIZABLE) - ctx->renderer.draw_box(handle_rect, 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, handle_rect, 0.f, ctx->colors.outline_variant); /* Push window content clip so nested clips (scroll, banners) intersect * with window bounds instead of escaping when depth == 0. */ diff --git a/src/list.c b/src/list.c index cc59c88..1875f4d 100644 --- a/src/list.c +++ b/src/list.c @@ -113,10 +113,11 @@ static void draw_leading_element(iui_context *ctx, case IUI_LIST_LEADING_IMAGE: /* Draw square image placeholder */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {x, cy - IUI_LIST_ONE_LINE_HEIGHT * 0.5f, IUI_LIST_ONE_LINE_HEIGHT, IUI_LIST_ONE_LINE_HEIGHT}, - 4.f, ctx->colors.surface_container_high, ctx->renderer.user); + 4.f, ctx->colors.surface_container_high); break; default: @@ -197,8 +198,7 @@ static void draw_trailing_element(iui_context *ctx, uint32_t track_color = *item->checkbox_value ? ctx->colors.primary : ctx->colors.surface_container_highest; - ctx->renderer.draw_box(switch_rect, switch_h * 0.5f, track_color, - ctx->renderer.user); + iui_emit_box(ctx, switch_rect, switch_h * 0.5f, track_color); /* Draw thumb */ float thumb_x = *item->checkbox_value @@ -360,9 +360,8 @@ bool iui_list_item_ex(iui_context *ctx, float divider_x = text_x; float divider_y = item_y + item_height - 1.f; float divider_w = item_width - (text_x - item_x) - IUI_LIST_PADDING_H; - ctx->renderer.draw_box( - (iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, 0.f, - ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, + 0.f, ctx->colors.outline_variant); } /* Advance layout */ @@ -425,9 +424,8 @@ void iui_list_divider(iui_context *ctx) float divider_y = ctx->layout.y; float divider_w = ctx->layout.width - IUI_LIST_DIVIDER_INSET; - ctx->renderer.draw_box((iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, - 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, 0.f, + ctx->colors.outline_variant); /* Add small vertical spacing */ ctx->layout.y += 1.f; diff --git a/src/menu.c b/src/menu.c index 97d81b4..18be9db 100644 --- a/src/menu.c +++ b/src/menu.c @@ -82,8 +82,7 @@ bool iui_menu_begin(iui_context *ctx, iui_draw_shadow(ctx, bg_rect, corner, IUI_ELEVATION_3); /* Draw menu background */ - ctx->renderer.draw_box(bg_rect, corner, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, bg_rect, corner, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_MENU(bg_rect, corner); @@ -118,10 +117,10 @@ bool iui_menu_add_item(iui_context *ctx, if (item->is_divider) { float div_y = menu->y + menu->height, div_h = IUI_MENU_DIVIDER_HEIGHT; float line_y = div_y + div_h * 0.5f; - ctx->renderer.draw_box( - (iui_rect_t) {menu->x + IUI_MENU_PADDING_H, line_y - 0.5f, - menu->width - IUI_MENU_PADDING_H * 2.f, 1.f}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {menu->x + IUI_MENU_PADDING_H, line_y - 0.5f, + menu->width - IUI_MENU_PADDING_H * 2.f, 1.f}, + 0.f, ctx->colors.outline_variant); menu->height += div_h; return false; /* Dividers are not clickable */ diff --git a/src/navigation.c b/src/navigation.c index c7b2dac..d08429b 100644 --- a/src/navigation.c +++ b/src/navigation.c @@ -27,8 +27,7 @@ void iui_nav_rail_begin(iui_context *ctx, /* Draw rail background */ iui_rect_t rail_rect = {x, y, width, height}; - ctx->renderer.draw_box(rail_rect, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, rail_rect, 0.f, ctx->colors.surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_NAV_RAIL(rail_rect, 0.f); @@ -55,8 +54,7 @@ bool iui_nav_rail_fab(iui_context *ctx, /* Draw FAB background */ uint32_t fab_bg = ctx->colors.primary_container; - ctx->renderer.draw_box(fab_rect, IUI_FAB_CORNER_RADIUS, fab_bg, - ctx->renderer.user); + iui_emit_box(ctx, fab_rect, IUI_FAB_CORNER_RADIUS, fab_bg); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, fab_rect, IUI_FAB_CORNER_RADIUS, @@ -118,18 +116,16 @@ bool iui_nav_rail_item(iui_context *ctx, float indicator_w = IUI_NAV_RAIL_WIDTH + text_width; iui_rect_t indicator_rect = {indicator_x, indicator_y, indicator_w, IUI_NAV_RAIL_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, - ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, + ctx->colors.secondary_container); IUI_MD3_TRACK_NAV_RAIL_INDICATOR(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS); } else { iui_rect_t indicator_rect = {indicator_x, indicator_y, IUI_NAV_RAIL_INDICATOR_WIDTH, IUI_NAV_RAIL_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, - ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, + ctx->colors.secondary_container); IUI_MD3_TRACK_NAV_RAIL_INDICATOR(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS); } @@ -144,8 +140,7 @@ bool iui_nav_rail_item(iui_context *ctx, iui_rect_t hover_rect = {indicator_x, indicator_y, IUI_NAV_RAIL_INDICATOR_WIDTH, IUI_NAV_RAIL_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(hover_rect, IUI_NAV_RAIL_CORNER_RADIUS, - layer_color, ctx->renderer.user); + iui_emit_box(ctx, hover_rect, IUI_NAV_RAIL_CORNER_RADIUS, layer_color); } /* Draw icon — centered inside the indicator */ @@ -222,8 +217,7 @@ void iui_nav_bar_begin(iui_context *ctx, /* Draw bar background */ iui_rect_t bar_rect = {x, y, width, IUI_NAV_BAR_HEIGHT}; - ctx->renderer.draw_box(bar_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, 0.f, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_NAV_BAR(bar_rect, 0.f); @@ -266,9 +260,8 @@ bool iui_nav_bar_item(iui_context *ctx, iui_rect_t indicator_rect = {indicator_x, indicator_y, IUI_NAV_BAR_INDICATOR_WIDTH, IUI_NAV_BAR_INDICATOR_HEIGHT}; - ctx->renderer.draw_box( - indicator_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, - ctx->colors.secondary_container, ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, + ctx->colors.secondary_container); } /* Draw state layer on hover/press */ @@ -280,8 +273,8 @@ bool iui_nav_bar_item(iui_context *ctx, iui_rect_t hover_rect = {indicator_x, indicator_y, IUI_NAV_BAR_INDICATOR_WIDTH, IUI_NAV_BAR_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(hover_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, - layer_color, ctx->renderer.user); + iui_emit_box(ctx, hover_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, + layer_color); } /* Draw icon */ @@ -372,10 +365,8 @@ bool iui_nav_drawer_begin(iui_context *ctx, iui_rect_t screen_rect = {0, 0, 10000.f, height}; uint8_t scrim_alpha = (uint8_t) (IUI_SCRIM_ALPHA * state->anim_progress); - ctx->renderer.draw_box( - screen_rect, 0.f, - (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF), - ctx->renderer.user); + iui_emit_box(ctx, screen_rect, 0.f, + (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF)); /* Push modal layer for input blocking */ iui_push_layer(ctx, 100); @@ -389,8 +380,7 @@ bool iui_nav_drawer_begin(iui_context *ctx, /* Draw drawer background */ iui_rect_t drawer_rect = {animated_x, y, drawer_width, height}; - ctx->renderer.draw_box(drawer_rect, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, drawer_rect, 0.f, ctx->colors.surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_NAV_DRAWER(drawer_rect, 0.f); @@ -429,13 +419,11 @@ bool iui_nav_drawer_item(iui_context *ctx, /* Draw selection or hover background */ if (selected) { - ctx->renderer.draw_box(item_rect, 28.f, ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 28.f, ctx->colors.secondary_container); } else if (iui_state_is_interactive(comp_state)) { uint32_t layer_color = iui_state_layer(ctx->colors.on_surface, iui_state_get_alpha(comp_state)); - ctx->renderer.draw_box(item_rect, 28.f, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 28.f, layer_color); } /* Draw icon */ @@ -480,8 +468,8 @@ void iui_nav_drawer_divider(iui_context *ctx) ctx->layout.y += 8.f; float x = ctx->layout.x + IUI_NAV_DRAWER_PADDING_H; float w = IUI_NAV_DRAWER_WIDTH - 2 * IUI_NAV_DRAWER_PADDING_H; - ctx->renderer.draw_box((iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, - ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, + ctx->colors.outline_variant); ctx->layout.y += 1.f + 8.f; } @@ -518,8 +506,7 @@ void iui_bottom_app_bar_begin(iui_context *ctx, /* Draw bar background */ iui_rect_t bar_rect = {x, y, width, IUI_BOTTOM_APP_BAR_HEIGHT}; - ctx->renderer.draw_box(bar_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, 0.f, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_BOTTOM_APP_BAR(bar_rect, 0.f); @@ -600,8 +587,7 @@ bool iui_bottom_app_bar_fab(iui_context *ctx, iui_state_t comp_state = iui_get_component_state(ctx, fab_rect, false); /* Draw FAB background */ - ctx->renderer.draw_box(fab_rect, corner_radius, - ctx->colors.primary_container, ctx->renderer.user); + iui_emit_box(ctx, fab_rect, corner_radius, ctx->colors.primary_container); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, fab_rect, corner_radius, @@ -702,8 +688,7 @@ bool iui_side_sheet_begin(iui_context *ctx, (uint8_t) (IUI_SCRIM_ALPHA * state->anim_progress); uint32_t scrim_color = (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF); - ctx->renderer.draw_box(screen_rect, 0.f, scrim_color, - ctx->renderer.user); + iui_emit_box(ctx, screen_rect, 0.f, scrim_color); /* Close on scrim click (protect against opening click release using * frames_since_open, similar to iui_modal_should_close) */ @@ -725,8 +710,7 @@ bool iui_side_sheet_begin(iui_context *ctx, /* Draw left border for standard sheets (outline variant) */ if (!state->modal) { iui_rect_t border_rect = {animated_x - 1.f, 0, 1.f, sheet_height}; - ctx->renderer.draw_box(border_rect, 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, border_rect, 0.f, ctx->colors.outline_variant); } iui_draw_elevated_box(ctx, sheet_rect, 0.f, elevation, diff --git a/src/pickers.c b/src/pickers.c index 8756d3b..9d6a715 100644 --- a/src/pickers.c +++ b/src/pickers.c @@ -145,17 +145,14 @@ bool iui_date_picker(iui_context *ctx, /* Draw scrim */ iui_rect_t scrim_rect = {0, 0, screen_width, screen_height}; - ctx->renderer.draw_box(scrim_rect, 0, ctx->colors.scrim, - ctx->renderer.user); + iui_emit_box(ctx, scrim_rect, 0, ctx->colors.scrim); /* Draw dialog shadow */ float corner = IUI_DIALOG_CORNER_RADIUS; iui_draw_shadow(ctx, dialog_rect, corner, IUI_ELEVATION_3); /* Draw dialog background */ - ctx->renderer.draw_box(dialog_rect, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, dialog_rect, corner, ctx->colors.surface_container_high); /* Navigation: Month Year with arrows (MD3 uses nav_h for this row) */ float nav_y = dialog_y + padding; @@ -272,8 +269,7 @@ bool iui_date_picker(iui_context *ctx, /* Draw selection background OR state layer (mutually exclusive) */ if (is_selected) { /* Selected day: filled primary rounded rect */ - ctx->renderer.draw_box(vis_rect, day_corner, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, vis_rect, day_corner, ctx->colors.primary); } else if (cell_state == IUI_STATE_HOVERED || cell_state == IUI_STATE_PRESSED) { /* State layer covers full touch target for proper feedback */ @@ -282,10 +278,8 @@ bool iui_date_picker(iui_context *ctx, : IUI_STATE_HOVER_ALPHA; /* circular touch feedback */ float touch_corner = cell_w * 0.5f; - ctx->renderer.draw_box( - touch_rect, touch_corner, - iui_state_layer(ctx->colors.on_surface, alpha), - ctx->renderer.user); + iui_emit_box(ctx, touch_rect, touch_corner, + iui_state_layer(ctx->colors.on_surface, alpha)); } /* Draw day number centered in visual rect */ @@ -344,8 +338,7 @@ bool iui_date_picker(iui_context *ctx, /* OK button (filled style) */ iui_rect_t ok_rect = {ok_x, btn_y, ok_w, button_h}; iui_state_t ok_state = iui_get_component_state(ctx, ok_rect, false); - ctx->renderer.draw_box(ok_rect, button_h * 0.5f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, ok_rect, button_h * 0.5f, ctx->colors.primary); iui_draw_state_layer(ctx, ok_rect, button_h * 0.5f, ctx->colors.on_primary, ok_state); float ok_text_x = @@ -489,17 +482,14 @@ bool iui_time_picker(iui_context *ctx, /* Draw scrim */ iui_rect_t scrim_rect = {0, 0, screen_width, screen_height}; - ctx->renderer.draw_box(scrim_rect, 0, ctx->colors.scrim, - ctx->renderer.user); + iui_emit_box(ctx, scrim_rect, 0, ctx->colors.scrim); /* Draw dialog shadow */ float corner = IUI_SHAPE_EXTRA_LARGE; iui_draw_shadow(ctx, dialog_rect, corner, IUI_ELEVATION_3); /* Draw dialog background */ - ctx->renderer.draw_box(dialog_rect, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, dialog_rect, corner, ctx->colors.surface_container_high); /* Header: Time display (HH:MM) */ float header_y = dialog_y + padding; @@ -534,7 +524,7 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t hour_text = hour_active ? ctx->colors.on_primary_container : ctx->colors.on_surface; - ctx->renderer.draw_box(hour_rect, 8.f, hour_bg, ctx->renderer.user); + iui_emit_box(ctx, hour_rect, 8.f, hour_bg); iui_draw_state_layer(ctx, hour_rect, 8.f, hour_text, hour_state); float hour_text_w = iui_get_text_width(ctx, hour_str); float hour_text_x = hour_rect.x + (hour_rect.width - hour_text_w) * 0.5f; @@ -557,15 +547,13 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t min_text = minute_active ? ctx->colors.on_primary_container : ctx->colors.on_surface; - ctx->renderer.draw_box(minute_rect, 8.f, min_bg, ctx->renderer.user); + iui_emit_box(ctx, minute_rect, 8.f, min_bg); if (minute_state == IUI_STATE_HOVERED || minute_state == IUI_STATE_PRESSED) { uint8_t alpha = (minute_state == IUI_STATE_PRESSED) ? IUI_STATE_PRESS_ALPHA : IUI_STATE_HOVER_ALPHA; - ctx->renderer.draw_box(minute_rect, 8.f, - iui_state_layer(min_text, alpha), - ctx->renderer.user); + iui_emit_box(ctx, minute_rect, 8.f, iui_state_layer(min_text, alpha)); } float min_text_w = iui_get_text_width(ctx, minute_str); float min_text_x = minute_rect.x + (minute_rect.width - min_text_w) * 0.5f; @@ -600,7 +588,7 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t am_text_color = am_selected ? ctx->colors.on_tertiary_container : ctx->colors.on_surface_variant; - ctx->renderer.draw_box(am_rect, 8.f, am_bg, ctx->renderer.user); + iui_emit_box(ctx, am_rect, 8.f, am_bg); iui_draw_state_layer(ctx, am_rect, 8.f, am_text_color, am_state); float am_w = iui_get_text_width(ctx, "AM"); iui_internal_draw_text( @@ -618,7 +606,7 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t pm_text_color = pm_selected ? ctx->colors.on_tertiary_container : ctx->colors.on_surface_variant; - ctx->renderer.draw_box(pm_rect, 8.f, pm_bg, ctx->renderer.user); + iui_emit_box(ctx, pm_rect, 8.f, pm_bg); iui_draw_state_layer(ctx, pm_rect, 8.f, pm_text_color, pm_state); float pm_w = iui_get_text_width(ctx, "PM"); iui_internal_draw_text( @@ -644,16 +632,14 @@ bool iui_time_picker(iui_context *ctx, /* Draw dial background */ iui_rect_t dial_bg_rect = {dial_x, dial_y, dial_size, dial_size}; - ctx->renderer.draw_box(dial_bg_rect, dial_r, - ctx->colors.surface_container_highest, - ctx->renderer.user); + iui_emit_box(ctx, dial_bg_rect, dial_r, + ctx->colors.surface_container_highest); /* Draw center dot */ iui_rect_t center_dot_rect = {dial_cx - center_dot * 0.5f, dial_cy - center_dot * 0.5f, center_dot, center_dot}; - ctx->renderer.draw_box(center_dot_rect, center_dot * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, center_dot_rect, center_dot * 0.5f, ctx->colors.primary); /* Calculate number positions and draw */ int num_count = 12; /* 12 numbers for both hour and minute */ @@ -710,17 +696,15 @@ bool iui_time_picker(iui_context *ctx, /* Draw selection circle */ if (is_selected) { - ctx->renderer.draw_box(num_rect, selector_size * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, num_rect, selector_size * 0.5f, + ctx->colors.primary); } else if (num_state == IUI_STATE_HOVERED || num_state == IUI_STATE_PRESSED) { uint8_t alpha = (num_state == IUI_STATE_PRESSED) ? IUI_STATE_PRESS_ALPHA : IUI_STATE_HOVER_ALPHA; - ctx->renderer.draw_box( - num_rect, selector_size * 0.5f, - iui_state_layer(ctx->colors.on_surface, alpha), - ctx->renderer.user); + iui_emit_box(ctx, num_rect, selector_size * 0.5f, + iui_state_layer(ctx->colors.on_surface, alpha)); } /* Draw number text */ @@ -743,6 +727,9 @@ bool iui_time_picker(iui_context *ctx, iui_polar_to_cart(dial_cx, dial_cy, num_radius - selector_size * 0.3f, sel_angle, &sel_x, &sel_y); + iui_ink_bounds_extend( + ctx, fminf(dial_cx, sel_x) - 1.f, fminf(dial_cy, sel_y) - 1.f, + fabsf(sel_x - dial_cx) + 2.f, fabsf(sel_y - dial_cy) + 2.f); ctx->renderer.draw_line(dial_cx, dial_cy, sel_x, sel_y, 2.f, ctx->colors.primary, ctx->renderer.user); } @@ -788,8 +775,7 @@ bool iui_time_picker(iui_context *ctx, /* OK button */ iui_rect_t ok_rect = {ok_x, btn_y, ok_w, button_h}; iui_state_t ok_state = iui_get_component_state(ctx, ok_rect, false); - ctx->renderer.draw_box(ok_rect, button_h * 0.5f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, ok_rect, button_h * 0.5f, ctx->colors.primary); iui_draw_state_layer(ctx, ok_rect, button_h * 0.5f, ctx->colors.on_primary, ok_state); float ok_text_x = diff --git a/src/searchbar.c b/src/searchbar.c index 46c9a48..5271cc3 100644 --- a/src/searchbar.c +++ b/src/searchbar.c @@ -120,9 +120,8 @@ iui_search_bar_result iui_search_bar_ex(iui_context *ctx, /* Draw container background (surface_container_high with full round * corners) */ - ctx->renderer.draw_box(bar_rect, corner_radius, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, corner_radius, + ctx->colors.surface_container_high); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, bar_rect, corner_radius, ctx->colors.on_surface, @@ -174,8 +173,7 @@ iui_search_bar_result iui_search_bar_ex(iui_context *ctx, iui_rect_t cursor_rect = { cursor_x, cursor_y, IUI_TEXTFIELD_CURSOR_WIDTH, cursor_height}; - ctx->renderer.draw_box(cursor_rect, 0.f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, cursor_rect, 0.f, ctx->colors.primary); } } @@ -197,8 +195,7 @@ iui_search_bar_result iui_search_bar_ex(iui_context *ctx, iui_rect_t trail_layer_rect = {trailing_icon_x - icon_size * 0.5f, icon_cy - icon_size * 0.5f, icon_size, icon_size}; - ctx->renderer.draw_box(trail_layer_rect, icon_size * 0.5f, - layer_color, ctx->renderer.user); + iui_emit_box(ctx, trail_layer_rect, icon_size * 0.5f, layer_color); } iui_draw_fab_icon(ctx, trailing_icon_x, icon_cy, icon_size, trail_icon, @@ -300,8 +297,7 @@ bool iui_search_view_begin(iui_context *ctx, iui_register_blocking_region(ctx, screen_bounds); /* Draw full-screen surface background */ - ctx->renderer.draw_box(screen_bounds, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, screen_bounds, 0.f, ctx->colors.surface); /* Header dimensions */ float header_h = IUI_SEARCH_VIEW_HEADER_HEIGHT; @@ -346,9 +342,7 @@ bool iui_search_view_begin(iui_context *ctx, /* Draw search field background */ float corner = IUI_SEARCH_BAR_CORNER_RADIUS; - ctx->renderer.draw_box(field_rect, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, field_rect, corner, ctx->colors.surface_container_high); /* Auto-focus the search field and register for tracking */ ctx->focused_edit = search->query; @@ -390,8 +384,7 @@ bool iui_search_view_begin(iui_context *ctx, float cursor_x = text_x + iui_get_text_width(ctx, temp); iui_rect_t cursor_rect = {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, ctx->font_height}; - ctx->renderer.draw_box(cursor_rect, 0.f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, cursor_rect, 0.f, ctx->colors.primary); } /* Draw clear button if text present */ diff --git a/src/tabs.c b/src/tabs.c index 575f039..f8754a6 100644 --- a/src/tabs.c +++ b/src/tabs.c @@ -43,8 +43,7 @@ static int iui_tabs_internal(iui_context *ctx, /* Draw container background (surface color) */ iui_rect_t container_rect = {tabs_x, tabs_y, container_width, tab_height}; - ctx->renderer.draw_box(container_rect, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, container_rect, 0.f, ctx->colors.surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_TAB(container_rect, 0.f); @@ -92,8 +91,7 @@ static int iui_tabs_internal(iui_context *ctx, if (!is_selected && iui_state_is_interactive(state)) { uint32_t layer_color = iui_state_layer(ctx->colors.on_surface, iui_state_get_alpha(state)); - ctx->renderer.draw_box(tab_rect, 0.f, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, tab_rect, 0.f, layer_color); } /* Determine text/icon colors based on selection state */ @@ -155,8 +153,8 @@ static int iui_tabs_internal(iui_context *ctx, /* Primary tabs: full-width indicator with rounded corners */ iui_rect_t indicator_rect = {indicator_x, indicator_y, indicator_width, indicator_height}; - ctx->renderer.draw_box(indicator_rect, indicator_height * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, indicator_height * 0.5f, + ctx->colors.primary); } else { /* Secondary tabs: shorter indicator centered under tab */ float short_indicator_width = indicator_width * 0.6f; @@ -165,15 +163,14 @@ static int iui_tabs_internal(iui_context *ctx, iui_rect_t short_indicator_rect = {short_indicator_x, indicator_y, short_indicator_width, indicator_height}; - ctx->renderer.draw_box(short_indicator_rect, indicator_height * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, short_indicator_rect, indicator_height * 0.5f, + ctx->colors.primary); } /* Draw bottom divider line (outline_variant color) */ iui_rect_t divider_rect = {tabs_x, tabs_y + tab_height - 1.f, container_width, 1.f}; - ctx->renderer.draw_box(divider_rect, 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, divider_rect, 0.f, ctx->colors.outline_variant); /* Advance layout cursor */ ctx->layout.y += tab_height + ctx->padding; diff --git a/tests/bench-render.c b/tests/bench-render.c new file mode 100644 index 0000000..ce8caf7 --- /dev/null +++ b/tests/bench-render.c @@ -0,0 +1,552 @@ +/* + * Rendering Benchmark + * + * Profiles the rendering pipeline under realistic application workloads. + * Uses a simulated backend with per-call costs that model real hardware: + * + * draw_box : pixel fill proportional to area + * draw_text : glyph rasterization (area per character) + * text_width : font shaping with glyph-advance + kerning table lookups + * set_clip : GPU scissor state change + * + * Scenes mirror real application screens (480x800 mobile form factor): + * Settings : scrollable list with toggles, radios, section headers + * Dashboard: metric cards, chip filters, progress bars, action buttons + * Form : sliders, checkboxes, buttons with varied label strings + */ + +#include "common.h" + +#include + +/* high-resolution timer */ +static uint64_t now_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec; +} + +/* benchmark parameters */ +#define SCREEN_W 480 +#define SCREEN_H 800 +#define FRAMES 5000 +#define WARMUP 300 +#define RUNS 3 + +/* simulated backend */ +static volatile uint64_t g_pixels; +static volatile int g_draws; +static volatile int g_clips; +static volatile int g_tmeas; + +static void sim_draw_box(iui_rect_t r, float radius, uint32_t color, void *user) +{ + (void) radius; + (void) color; + (void) user; + uint64_t area = (uint64_t) (r.width > 0 ? r.width : 0) * + (uint64_t) (r.height > 0 ? r.height : 0); + g_pixels += area; + g_draws++; +} + +static void sim_set_clip(uint16_t x0, + uint16_t y0, + uint16_t x1, + uint16_t y1, + void *user) +{ + (void) x0; + (void) y0; + (void) x1; + (void) y1; + (void) user; + g_clips++; +} + +static void sim_draw_text(float x, + float y, + const char *text, + uint32_t color, + void *user) +{ + (void) x; + (void) y; + (void) color; + (void) user; + g_pixels += (uint64_t) strlen(text) * 80; + g_draws++; +} + +/* Font shaping simulation. + * + * Models per-glyph work that a real font backend performs: + * glyph-advance table lookup + kerning pair probe. The volatile tables prevent + * the compiler from constant-folding the result, making each call cost ~2 + * cache-line touches per character. + */ +static volatile float g_glyph_advances[256]; +static volatile uint8_t g_kern_table[256]; + +static void init_font_tables(void) +{ + for (int i = 0; i < 256; i++) { + g_glyph_advances[i] = 6.f + (float) (i % 5); + g_kern_table[i] = (uint8_t) (i * 31 + 17); + } +} + +static float sim_text_width(const char *text, void *user) +{ + (void) user; + g_tmeas++; + float w = 0.f; + uint8_t prev = 0; + for (const unsigned char *p = (const unsigned char *) text; *p; p++) { + w += g_glyph_advances[*p]; + uint8_t pair = (uint8_t) (prev ^ *p); + w += (float) g_kern_table[pair] * 0.01f; + prev = *p; + } + return w; +} + +/* scenes */ + +typedef void (*scene_fn)(iui_context *); + +/* Scene A: Settings - heavy on text labels and toggles */ +static void scene_settings(iui_context *ctx) +{ + static bool toggles[16]; + static int radio_val = 1; + + iui_begin_frame(ctx, 0.016f); + iui_begin_window(ctx, "settings", 0, 0, SCREEN_W, SCREEN_H, 0); + + iui_text(ctx, IUI_ALIGN_LEFT, "General Settings"); + iui_divider(ctx); + for (int i = 0; i < 8; i++) { + char label[48]; + snprintf(label, sizeof(label), "Enable feature %d (recommended)", + i + 1); + iui_checkbox(ctx, label, &toggles[i]); + } + + iui_text(ctx, IUI_ALIGN_LEFT, "Display"); + iui_divider(ctx); + for (int i = 8; i < 16; i++) { + char label[48]; + snprintf(label, sizeof(label), "Display option %d", i - 7); + iui_checkbox(ctx, label, &toggles[i]); + } + + iui_text(ctx, IUI_ALIGN_LEFT, "Theme"); + iui_divider(ctx); + static const char *themes[] = {"Light", "Dark", "System default", + "High contrast"}; + for (int i = 0; i < 4; i++) + iui_radio(ctx, themes[i], &radio_val, i); + + iui_text(ctx, IUI_ALIGN_LEFT, "About this application"); + iui_divider(ctx); + iui_text(ctx, IUI_ALIGN_LEFT, "Version 1.4.2 (build 2891)"); + iui_text(ctx, IUI_ALIGN_LEFT, + "Copyright 2024-2026 Example Corporation. All rights reserved."); + + iui_button(ctx, "Check for updates", IUI_ALIGN_CENTER); + iui_button(ctx, "Reset all settings", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* Scene B: Dashboard - cards, chips, progress, mixed widgets */ +static void scene_dashboard(iui_context *ctx) +{ + static bool filters[6] = {true, false, true, false, true, false}; + + iui_begin_frame(ctx, 0.016f); + iui_begin_window(ctx, "dash", 0, 0, SCREEN_W, SCREEN_H, 0); + + /* Filter chip bar */ + iui_chip_filter(ctx, "All items", &filters[0]); + iui_chip_filter(ctx, "Active", &filters[1]); + iui_chip_filter(ctx, "Pending review", &filters[2]); + iui_chip_filter(ctx, "Archived", &filters[3]); + iui_chip_filter(ctx, "Starred", &filters[4]); + iui_chip_filter(ctx, "Flagged", &filters[5]); + + /* Metric cards */ + for (int i = 0; i < 6; i++) { + float cx = (float) (i % 2) * (SCREEN_W / 2); + float cy = 80.f + (float) (i / 2) * 150.f; + iui_card_begin(ctx, cx, cy, SCREEN_W / 2 - 8, 140, IUI_CARD_ELEVATED); + + char title[32]; + snprintf(title, sizeof(title), "Metric %c", 'A' + i); + iui_text(ctx, IUI_ALIGN_LEFT, title); + + float pct = 0.15f * (float) (i + 1); + iui_progress_linear(ctx, pct, 1.f, false); + + char detail[64]; + snprintf(detail, sizeof(detail), "%.0f%% complete - %d of %d items", + pct * 100, (int) (pct * 120), 120); + iui_text(ctx, IUI_ALIGN_LEFT, detail); + + iui_card_end(ctx); + } + + /* Action row */ + iui_button(ctx, "Export Report", IUI_ALIGN_CENTER); + iui_button(ctx, "Refresh Data", IUI_ALIGN_CENTER); + iui_button(ctx, "Share Dashboard", IUI_ALIGN_CENTER); + iui_button(ctx, "Print Summary", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* Scene C: Form - sliders, checkboxes, buttons with varied labels */ +static void scene_form(iui_context *ctx) +{ + static float sliders[8] = {0.3f, 0.5f, 0.7f, 0.1f, 0.9f, 0.4f, 0.6f, 0.2f}; + static bool checks[8]; + + iui_begin_frame(ctx, 0.016f); + iui_begin_window(ctx, "form", 0, 0, SCREEN_W, SCREEN_H, 0); + + iui_text(ctx, IUI_ALIGN_LEFT, "Image Processing Configuration"); + iui_divider(ctx); + + static const char *slider_labels[] = { + "Brightness", "Contrast", "Saturation", "Sharpness", + "Exposure bias", "Gamma curve", "Highlights", "Shadows", + }; + for (int i = 0; i < 8; i++) + iui_slider(ctx, slider_labels[i], 0.f, 1.f, 0.01f, &sliders[i], NULL); + + iui_text(ctx, IUI_ALIGN_LEFT, "Processing Options"); + iui_divider(ctx); + + static const char *check_labels[] = { + "Auto white balance", "Noise reduction (luminance)", + "HDR tone mapping", "Lens distortion correction", + "Chromatic aberration fix", "Vignette removal", + "Perspective correction", "Auto-crop to content", + }; + for (int i = 0; i < 8; i++) + iui_checkbox(ctx, check_labels[i], &checks[i]); + + iui_text(ctx, IUI_ALIGN_LEFT, "Output"); + iui_divider(ctx); + iui_text(ctx, IUI_ALIGN_LEFT, + "Processed images will be saved to the output directory."); + + iui_button(ctx, "Apply Changes", IUI_ALIGN_CENTER); + iui_button(ctx, "Reset to Defaults", IUI_ALIGN_CENTER); + iui_button(ctx, "Preview Result", IUI_ALIGN_CENTER); + iui_button(ctx, "Cancel", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* Scene D: Dialog - small overlay window (partial-screen update). + * This is the primary use-case for ink-bounds: only the dialog region (roughly + * 60% x 30% of screen) needs to be blitted to the display, saving ~80% of the + * framebuffer transfer cost. + */ +static void scene_dialog(iui_context *ctx) +{ + iui_begin_frame(ctx, 0.016f); + + /* Dialog overlay: centered, 280x240 on 480x800 screen */ + float dw = 280, dh = 240; + float dx = (SCREEN_W - dw) * 0.5f; + float dy = (SCREEN_H - dh) * 0.5f; + iui_begin_window(ctx, "dialog", dx, dy, dw, dh, 0); + + iui_text(ctx, IUI_ALIGN_LEFT, "Confirm Action"); + iui_divider(ctx); + iui_text(ctx, IUI_ALIGN_LEFT, "Are you sure you want to proceed?"); + iui_text(ctx, IUI_ALIGN_LEFT, "This action cannot be undone."); + + iui_button(ctx, "Confirm", IUI_ALIGN_CENTER); + iui_button(ctx, "Cancel", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* bench harness */ +typedef struct { + double us_per_frame; + int draws_per_frame; + int clips_per_frame; + int tmeas_per_frame; +} bench_result_t; + +/* Run benchmark, return median of RUNS iterations */ +static bench_result_t bench_run(iui_context *ctx, scene_fn fn) +{ + double results[RUNS]; + + for (int run = 0; run < RUNS; run++) { + for (int i = 0; i < WARMUP; i++) + fn(ctx); + + g_pixels = 0; + g_draws = 0; + g_clips = 0; + g_tmeas = 0; + + uint64_t start = now_ns(); + for (int i = 0; i < FRAMES; i++) + fn(ctx); + uint64_t elapsed = now_ns() - start; + results[run] = (double) elapsed / (double) FRAMES / 1e3; + } + + /* Simple sort for median */ + for (int i = 0; i < RUNS - 1; i++) + for (int j = i + 1; j < RUNS; j++) + if (results[j] < results[i]) { + double tmp = results[i]; + results[i] = results[j]; + results[j] = tmp; + } + + bench_result_t r; + r.us_per_frame = results[RUNS / 2]; /* median */ + r.draws_per_frame = g_draws / FRAMES; + r.clips_per_frame = g_clips / FRAMES; + r.tmeas_per_frame = g_tmeas / FRAMES; + return r; +} + +static void print_header(const char *scene) +{ + printf("\n %s\n ", scene); + for (int i = 0; scene[i]; i++) + putchar('-'); + putchar('\n'); +} + +static void print_row(const char *label, bench_result_t r) +{ + printf(" %-36s %6.1f us/frame %3d draws %3d clips %3d tmeas\n", label, + r.us_per_frame, r.draws_per_frame, r.clips_per_frame, + r.tmeas_per_frame); +} + +static void print_pct(const char *label, double base_us, double test_us) +{ + double pct = (test_us - base_us) / base_us * 100.0; + printf(" %-32s %+6.1f%%\n", label, pct); +} + +/* per-scene benchmark */ +static void bench_scene(const char *title, scene_fn fn, iui_context *ctx) +{ + print_header(title); + + iui_ink_bounds_enable(ctx, false); + iui_text_cache_enable(ctx, false); + bench_result_t base = bench_run(ctx, fn); + print_row("baseline (all opts OFF)", base); + + iui_ink_bounds_enable(ctx, true); + iui_text_cache_enable(ctx, false); + bench_result_t ink = bench_run(ctx, fn); + print_row("ink-bounds ON", ink); + + iui_ink_bounds_enable(ctx, false); + iui_text_cache_enable(ctx, true); + bench_result_t tc = bench_run(ctx, fn); + print_row("text-cache ON", tc); + + iui_ink_bounds_enable(ctx, true); + iui_text_cache_enable(ctx, true); + bench_result_t both = bench_run(ctx, fn); + print_row("all opts ON", both); + + printf("\n"); + print_pct("ink-bounds overhead:", base.us_per_frame, ink.us_per_frame); + print_pct("text-cache effect:", base.us_per_frame, tc.us_per_frame); + print_pct("combined:", base.us_per_frame, both.us_per_frame); +} + +/* ink-bounds & text cache analysis */ +static void analyze_ink_bounds(iui_context *ctx, scene_fn fn, const char *name) +{ + iui_ink_bounds_enable(ctx, true); + fn(ctx); + iui_rect_t b; + float total = (float) SCREEN_W * (float) SCREEN_H; + if (iui_ink_bounds_get(ctx, &b)) { + float dirty = b.width * b.height; + float pct = dirty / total * 100.f; + printf(" %-18s (%.0f,%.0f) %.0fx%.0f = %5.1f%%", name, b.x, b.y, + b.width, b.height, pct); + if (pct < 99.f) + printf(" -> %.1f%% blit saved", 100.f - pct); + printf("\n"); + } + iui_ink_bounds_enable(ctx, false); +} + +static void analyze_text_cache(iui_context *ctx, scene_fn fn, const char *name) +{ + /* Measure WITHOUT cache: count raw text_width calls */ + iui_text_cache_enable(ctx, false); + g_tmeas = 0; + for (int i = 0; i < 100; i++) + fn(ctx); + int raw_calls = g_tmeas; + + /* Measure WITH cache: count backend calls that survive */ + iui_text_cache_enable(ctx, true); + iui_text_cache_clear(ctx); + g_tmeas = 0; + for (int i = 0; i < 100; i++) + fn(ctx); + int cached_calls = g_tmeas; + + int hits, misses; + iui_text_cache_stats(ctx, &hits, &misses); + int total = hits + misses; + float rate = total > 0 ? (float) hits / (float) total * 100.f : 0.f; + + int saved = raw_calls - cached_calls; + float save_pct = + raw_calls > 0 ? (float) saved / (float) raw_calls * 100.f : 0.f; + + printf( + " %-18s %4d calls/frame -> %d with cache (%.1f%% eliminated, " + "%.1f%% hit rate)\n", + name, raw_calls / 100, cached_calls / 100, save_pct, rate); + iui_text_cache_enable(ctx, false); +} + +int main(void) +{ + init_font_tables(); + + static union { + uint8_t buf[65536]; + void *align; + } mem; + + iui_renderer_t renderer = { + .draw_box = sim_draw_box, + .set_clip_rect = sim_set_clip, + .text_width = sim_text_width, + .draw_text = sim_draw_text, + }; + iui_config_t cfg = iui_make_config(mem.buf, renderer, 14.f, NULL); + iui_context *ctx = iui_init(&cfg); + if (!ctx) { + fprintf(stderr, "iui_init failed\n"); + return 1; + } + + printf( + "==================================================================\n"); + printf(" libiui Rendering Benchmark\n"); + printf(" Screen: %dx%d Frames: %d Runs: %d (median)\n", SCREEN_W, + SCREEN_H, FRAMES, RUNS); + printf( + "==================================================================\n"); + + bench_scene("Settings (16 toggles, 4 radios, labels)", scene_settings, ctx); + bench_scene("Dashboard (6 cards, 6 chips, 4 buttons)", scene_dashboard, + ctx); + bench_scene("Form (8 sliders, 8 checks, 4 buttons)", scene_form, ctx); + bench_scene("Dialog (confirm overlay, partial screen)", scene_dialog, ctx); + + /* Ink-bounds coverage */ + print_header("Ink-Bounds Coverage"); + analyze_ink_bounds(ctx, scene_settings, "Settings"); + analyze_ink_bounds(ctx, scene_dashboard, "Dashboard"); + analyze_ink_bounds(ctx, scene_form, "Form"); + analyze_ink_bounds(ctx, scene_dialog, "Dialog"); + + /* Text cache effectiveness */ + print_header("Text Cache Effectiveness (100 frames)"); + analyze_text_cache(ctx, scene_settings, "Settings"); + analyze_text_cache(ctx, scene_dashboard, "Dashboard"); + analyze_text_cache(ctx, scene_form, "Form"); + analyze_text_cache(ctx, scene_dialog, "Dialog"); + + /* Backend cost model: estimates real-world savings from ink-bounds + * partial blit + text cache. Based on measured pixel counts and + * text_width call counts from the scenes above. + */ + print_header("Estimated Backend Savings"); + { + const double blit_us_per_pixel = 1.0 / 2.5; /* 0.4 us per pixel */ + const double tw_us_per_call = 5.0; /* 5 us per text_width */ + double full_blit_us = + (double) SCREEN_W * (double) SCREEN_H * blit_us_per_pixel; + + scene_fn scenes[] = {scene_settings, scene_dashboard, scene_form, + scene_dialog}; + const char *names[] = {"Settings", "Dashboard", "Form", "Dialog"}; + + for (int s = 0; s < 4; s++) { + /* Measure ink-bounds region */ + iui_ink_bounds_enable(ctx, true); + iui_text_cache_enable(ctx, false); + g_tmeas = 0; + scenes[s](ctx); + int raw_tw = g_tmeas; + + iui_rect_t bounds; + double partial_blit_us = full_blit_us; + float coverage = 100.f; + if (iui_ink_bounds_get(ctx, &bounds)) { + double dirty = (double) bounds.width * (double) bounds.height; + partial_blit_us = dirty * blit_us_per_pixel; + coverage = + (float) (dirty / ((double) SCREEN_W * (double) SCREEN_H) * + 100.0); + } + + /* Measure cached text_width calls */ + iui_text_cache_enable(ctx, true); + iui_text_cache_clear(ctx); + g_tmeas = 0; + scenes[s](ctx); /* prime cache */ + g_tmeas = 0; + scenes[s](ctx); /* steady state */ + int cached_tw = g_tmeas; + iui_ink_bounds_enable(ctx, false); + iui_text_cache_enable(ctx, false); + + double blit_save_us = full_blit_us - partial_blit_us; + double tw_save_us = (double) (raw_tw - cached_tw) * tw_us_per_call; + double total_save_us = blit_save_us + tw_save_us; + double baseline_us = + full_blit_us + (double) raw_tw * tw_us_per_call; + double save_pct = + baseline_us > 0 ? total_save_us / baseline_us * 100.0 : 0.0; + + printf(" %-18s blit: %.0f -> %.0f us (%.0f%% coverage)\n", + names[s], full_blit_us, partial_blit_us, (double) coverage); + printf(" %-18s text_width: %d -> %d calls (%.0f us saved)\n", "", + raw_tw, cached_tw, tw_save_us); + printf(" %-18s total: %.0f us saved/frame (%.1f%% of backend)\n", + "", total_save_us, save_pct); + if (s < 3) + printf("\n"); + } + } + + printf( + "\n==================================================================" + "\n"); + return 0; +}