Skip to content

Commit 85173f1

Browse files
committed
Systematically refine memory consumption
This introduces mechanisms to eliminate per-frame heap allocations from the rendering hot path and provide memory visibility for constrained devices: Scratch arena: a 32 KB bump allocator on twin_screen_t, reset per screen update. Polygon edge arrays (poly.c), composite mask pixmaps (path.c), and convex hull arrays (hull.c) allocate from the arena first, falling back to malloc only when exhausted. Save/restore semantics support nested temporary allocations. Reusable object caches: a 4-element path cache (acquire/release) avoids repeated create/destroy for stroke rendering temporaries. A grow-only A8 mask cache on twin_screen_t eliminates per-composite malloc for masks exceeding the scratch arena. Inline path storage: struct _twin_path embeds 64-point and 4-sublen arrays, avoiding heap allocation entirely for typical small paths (rectangles, circles, rounded rectangles). Growth from inline to heap uses malloc+memcpy; destroy only frees heap-allocated arrays. Memory tracking (CONFIG_MEMORY_STATS, default y): twin_malloc/twin_free wrapper macros route all core allocations through a tracking layer that maintains current/peak byte counters and alloc/free counts. The tracker uses a linear-scan table with inline storage -- no heap allocation for bookkeeping itself. Public API: twin_memory_{get_info,reset_peak}. twin_destroy() logs stats after teardown. CI workflow captures memory statistics from headless test runs. Budget-aware APIs: twin_pixmap_create_budget() computes the largest pixmap fitting a byte budget while preserving aspect ratio. twin_tvg_to_pixmap_budget() auto-scales TVG rendering to fit a memory budget. CONFIG_TVG_MEMORY_BUDGET Kconfig option caps per-image memory. The ARGB32-only restriction on TVG rendering is removed -- RGB16 now works, halving memory for opaque content. Additional optimizations: the image viewer app evicts previous pixmaps instead of caching all six TVG images simultaneously. Background loading skips full-screen scaling when the source fits within screen dimensions. twin_destroy() centralizes screen teardown that backends previously omitted. Closes #149
1 parent ec31787 commit 85173f1

31 files changed

Lines changed: 1001 additions & 194 deletions

.github/workflows/headless-test.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ jobs:
3535
3636
- name: Run basic headless test
3737
run: |
38-
# Start demo in background
39-
timeout 30s ./demo-headless &
38+
# Start demo in background, capture stderr for memory stats
39+
timeout 30s ./demo-headless 2>demo_stderr.log &
4040
DEMO_PID=$!
4141
echo "Started demo-headless with PID $DEMO_PID"
4242
@@ -62,6 +62,13 @@ jobs:
6262
wait $DEMO_PID || true
6363
echo "Basic test completed"
6464
65+
- name: Report memory statistics
66+
if: always()
67+
run: |
68+
echo "=== Mado Memory Statistics ==="
69+
grep -i "memory" demo_stderr.log 2>/dev/null || echo "(no memory stats in output)"
70+
cat demo_stderr.log 2>/dev/null || true
71+
6572
- name: Verify screenshot output
6673
run: |
6774
test -f test_output.png || (echo "Screenshot not created" && exit 1)
@@ -106,7 +113,7 @@ jobs:
106113
--errors-for-leak-kinds=definite \
107114
--error-exitcode=1 \
108115
--log-file=valgrind.log \
109-
./demo-headless &
116+
./demo-headless 2>valgrind_demo_stderr.log &
110117
VALGRIND_PID=$!
111118
112119
sleep 5
@@ -124,6 +131,10 @@ jobs:
124131
grep -A 10 "LEAK SUMMARY" valgrind.log || true
125132
grep "ERROR SUMMARY" valgrind.log || true
126133
134+
# Display memory statistics from twin_destroy
135+
echo "=== Mado Memory Statistics (Valgrind run) ==="
136+
grep -i "memory" valgrind_demo_stderr.log 2>/dev/null || true
137+
127138
# Check for definite leaks
128139
if grep -q "definitely lost: [1-9]" valgrind.log; then
129140
echo "ERROR: Memory leaks detected"

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ src/composite-decls.h: scripts/gen-composite-decls.py
8787

8888
libtwin.a_files-$(CONFIG_LOGGING) += src/log.c
8989
libtwin.a_files-$(CONFIG_CURSOR) += src/cursor.c
90+
libtwin.a_files-y += src/memstats.c
9091

9192
# Rendering backends
9293
# Screen compositing operations (always needed for screen buffer management)

apps/image.c

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,18 @@ static inline twin_pixmap_t *_apps_image_pixmap(twin_custom_widget_t *image)
2020
#define APP_WIDTH 400
2121
#define APP_HEIGHT 400
2222

23+
static twin_pixmap_t *load_tvg(const char *path)
24+
{
25+
#if defined(CONFIG_TVG_MEMORY_BUDGET) && CONFIG_TVG_MEMORY_BUDGET > 0
26+
return twin_tvg_to_pixmap_budget(path, TWIN_ARGB32,
27+
(size_t) CONFIG_TVG_MEMORY_BUDGET * 1024);
28+
#else
29+
return twin_tvg_to_pixmap_scale(path, TWIN_ARGB32, APP_WIDTH, APP_HEIGHT);
30+
#endif
31+
}
32+
2333
typedef struct {
24-
twin_pixmap_t **pixes;
34+
twin_pixmap_t *current_pix; /* only the visible image is kept */
2535
int image_idx;
2636
} apps_image_data_t;
2737

@@ -44,9 +54,11 @@ static void _apps_image_paint(twin_custom_widget_t *custom)
4454
{
4555
apps_image_data_t *img =
4656
(apps_image_data_t *) twin_custom_widget_data(custom);
57+
if (!img->current_pix)
58+
return;
4759
twin_operand_t srcop = {
4860
.source_kind = TWIN_PIXMAP,
49-
.u.pixmap = img->pixes[img->image_idx],
61+
.u.pixmap = img->current_pix,
5062
};
5163

5264
twin_composite(_apps_image_pixmap(custom), 0, 0, &srcop, 0, 0, NULL, 0, 0,
@@ -67,6 +79,15 @@ static twin_dispatch_result_t _apps_image_dispatch(twin_widget_t *widget,
6779
case TwinEventPaint:
6880
_apps_image_paint(custom);
6981
break;
82+
case TwinEventDestroy: {
83+
apps_image_data_t *img =
84+
(apps_image_data_t *) twin_custom_widget_data(custom);
85+
if (img->current_pix) {
86+
twin_pixmap_destroy(img->current_pix);
87+
img->current_pix = NULL;
88+
}
89+
break;
90+
}
7091
default:
7192
break;
7293
}
@@ -87,13 +108,15 @@ static twin_dispatch_result_t _apps_image_button_clicked(twin_widget_t *widget,
87108
(apps_image_data_t *) twin_custom_widget_data(custom);
88109
const int n = sizeof(tvg_files) / sizeof(tvg_files[0]);
89110
img->image_idx = img->image_idx == n - 1 ? 0 : img->image_idx + 1;
90-
if (!img->pixes[img->image_idx]) {
91-
twin_pixmap_t *pix = twin_tvg_to_pixmap_scale(
92-
tvg_files[img->image_idx], TWIN_ARGB32, APP_WIDTH, APP_HEIGHT);
93-
if (!pix)
94-
return TwinDispatchContinue;
95-
img->pixes[img->image_idx] = pix;
111+
112+
/* Evict the old pixmap before loading the new one */
113+
if (img->current_pix) {
114+
twin_pixmap_destroy(img->current_pix);
115+
img->current_pix = NULL;
96116
}
117+
img->current_pix = load_tvg(tvg_files[img->image_idx]);
118+
if (!img->current_pix)
119+
return TwinDispatchContinue;
97120
twin_custom_widget_queue_paint(custom);
98121
return TwinDispatchDone;
99122
}
@@ -109,10 +132,7 @@ static twin_custom_widget_t *_apps_image_init(twin_box_t *parent)
109132
apps_image_data_t *img =
110133
(apps_image_data_t *) twin_custom_widget_data(custom);
111134
img->image_idx = 0;
112-
img->pixes = calloc(sizeof(tvg_files) / sizeof(tvg_files[0]),
113-
sizeof(twin_pixmap_t *));
114-
img->pixes[0] = twin_tvg_to_pixmap_scale(tvg_files[0], TWIN_ARGB32,
115-
APP_WIDTH, APP_HEIGHT);
135+
img->current_pix = load_tvg(tvg_files[0]);
116136

117137
twin_button_t *button =
118138
twin_button_create(parent, "Next Image", 0xFF482722, D(10),

apps/main.c

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,43 @@
2929
* Load the background pixmap from storage.
3030
* Return a default pattern if the load of @path or image loader fails.
3131
*/
32+
/*
33+
* Load the background pixmap from storage.
34+
* The screen compositor tiles backgrounds smaller than the screen,
35+
* so scaling is only needed when the source is larger. When the
36+
* source fits, use it directly to avoid a full-screen allocation.
37+
*/
3238
static twin_pixmap_t *load_background(twin_screen_t *screen, const char *path)
3339
{
3440
twin_pixmap_t *raw_background = twin_pixmap_from_file(path, TWIN_ARGB32);
35-
if (!raw_background) /* Fallback to a default pattern */
41+
if (!raw_background)
3642
return twin_make_pattern();
3743

38-
if (screen->height == raw_background->height &&
39-
screen->width == raw_background->width)
44+
/* Source fits on screen -- use directly (tiles if smaller) */
45+
if (raw_background->width <= screen->width &&
46+
raw_background->height <= screen->height)
4047
return raw_background;
4148

42-
/* Scale as needed. */
43-
twin_pixmap_t *scaled_background =
49+
/* Source is larger -- scale down to screen size */
50+
twin_pixmap_t *scaled =
4451
twin_pixmap_create(TWIN_ARGB32, screen->width, screen->height);
45-
if (!scaled_background) {
52+
if (!scaled) {
4653
twin_pixmap_destroy(raw_background);
4754
return twin_make_pattern();
4855
}
49-
twin_fixed_t sx, sy;
50-
sx = twin_fixed_div(twin_int_to_fixed(raw_background->width),
51-
twin_int_to_fixed(screen->width));
52-
sy = twin_fixed_div(twin_int_to_fixed(raw_background->height),
53-
twin_int_to_fixed(screen->height));
54-
56+
twin_fixed_t sx = twin_fixed_div(twin_int_to_fixed(raw_background->width),
57+
twin_int_to_fixed(screen->width));
58+
twin_fixed_t sy = twin_fixed_div(twin_int_to_fixed(raw_background->height),
59+
twin_int_to_fixed(screen->height));
5560
twin_matrix_scale(&raw_background->transform, sx, sy);
5661
twin_operand_t srcop = {
5762
.source_kind = TWIN_PIXMAP,
5863
.u.pixmap = raw_background,
5964
};
60-
twin_composite(scaled_background, 0, 0, &srcop, 0, 0, NULL, 0, 0,
61-
TWIN_SOURCE, screen->width, screen->height);
62-
65+
twin_composite(scaled, 0, 0, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE,
66+
screen->width, screen->height);
6367
twin_pixmap_destroy(raw_background);
64-
65-
return scaled_background;
68+
return scaled;
6669
}
6770

6871
static twin_context_t *tx = NULL;

configs/Kconfig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ config LOGGING_CALLBACK
171171
Allow custom callback functions for log handling.
172172
Enables application-specific log processing.
173173

174+
config MEMORY_STATS
175+
bool "Enable memory usage tracking"
176+
default y
177+
help
178+
Track heap allocation statistics including current usage,
179+
peak usage, and allocation counts. Adds small overhead per
180+
malloc/free call. Useful for profiling memory consumption
181+
on constrained devices.
182+
174183
comment "Logging is disabled"
175184
depends on !LOGGING
176185

@@ -251,6 +260,16 @@ config LOADER_TVG
251260
Enable TinyVG vector graphics format support.
252261
Compact binary format for scalable graphics.
253262

263+
config TVG_MEMORY_BUDGET
264+
int "TVG rendering memory budget (KB)"
265+
default 0
266+
depends on LOADER_TVG
267+
help
268+
Maximum memory in kilobytes for a single TVG pixmap.
269+
The renderer auto-scales the output to fit within this
270+
budget while preserving aspect ratio.
271+
Set to 0 for unlimited (use native document dimensions).
272+
254273
endmenu
255274

256275
menu "Demo Applications"

configs/defconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ CONFIG_DEMO_CLOCK=y
88
CONFIG_DEMO_CALCULATOR=y
99
CONFIG_DEMO_SPLINE=y
1010
CONFIG_DEMO_ANIMATION=y
11+
CONFIG_MEMORY_STATS=y

include/twin.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,21 @@ struct _twin_screen {
348348
twin_argb32_t *span_cache; /**< Cached span buffer */
349349
twin_coord_t span_cache_width; /**< Cached span buffer width */
350350

351+
/* Scratch arena for per-frame temporary allocations */
352+
void *scratch_buf; /**< Scratch buffer (bump allocator) */
353+
size_t scratch_size; /**< Scratch buffer capacity in bytes */
354+
355+
/* Reusable path object cache to avoid repeated alloc/free */
356+
#define TWIN_PATH_CACHE_SIZE 4
357+
struct _twin_path *path_cache[TWIN_PATH_CACHE_SIZE];
358+
int path_cache_count;
359+
360+
/* Cached A8 mask pixmap for twin_composite_path -- avoids
361+
* per-composite malloc for masks larger than the scratch arena. */
362+
twin_pixmap_t *mask_cache;
363+
twin_coord_t mask_cache_width;
364+
twin_coord_t mask_cache_height;
365+
351366
/* Event processing: event filter callback */
352367
bool (*event_filter)(twin_screen_t *screen, twin_event_t *event);
353368
};
@@ -1582,6 +1597,28 @@ twin_pixmap_t *twin_tvg_to_pixmap_scale(const char *filepath,
15821597
twin_coord_t w,
15831598
twin_coord_t h);
15841599

1600+
twin_pixmap_t *twin_tvg_to_pixmap_budget(const char *filepath,
1601+
twin_format_t fmt,
1602+
size_t memory_budget);
1603+
1604+
twin_pixmap_t *twin_pixmap_create_budget(twin_format_t format,
1605+
twin_coord_t max_width,
1606+
twin_coord_t max_height,
1607+
size_t memory_budget);
1608+
1609+
/**
1610+
* Memory usage statistics (requires CONFIG_MEMORY_STATS)
1611+
*/
1612+
typedef struct _twin_memory_info {
1613+
size_t current_bytes; /**< Currently allocated heap bytes */
1614+
size_t peak_bytes; /**< High-water mark */
1615+
size_t total_allocs; /**< Lifetime allocation count */
1616+
size_t total_frees; /**< Lifetime free count */
1617+
} twin_memory_info_t;
1618+
1619+
void twin_memory_get_info(twin_memory_info_t *info);
1620+
void twin_memory_reset_peak(void);
1621+
15851622
twin_context_t *twin_create(int width, int height);
15861623

15871624
void twin_destroy(twin_context_t *ctx);

src/animation.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
#include <stdlib.h>
77

8-
#include "twin.h"
8+
#include "twin_private.h"
99

1010
twin_time_t twin_animation_get_current_delay(const twin_animation_t *anim)
1111
{
@@ -33,18 +33,18 @@ void twin_animation_destroy(twin_animation_t *anim)
3333
if (!anim)
3434
return;
3535

36-
free(anim->iter);
36+
twin_free(anim->iter);
3737
for (twin_count_t i = 0; i < anim->n_frames; i++) {
3838
twin_pixmap_destroy(anim->frames[i]);
3939
}
40-
free(anim->frames);
41-
free(anim->frame_delays);
42-
free(anim);
40+
twin_free(anim->frames);
41+
twin_free(anim->frame_delays);
42+
twin_free(anim);
4343
}
4444

4545
twin_animation_iter_t *twin_animation_iter_init(twin_animation_t *anim)
4646
{
47-
twin_animation_iter_t *iter = malloc(sizeof(twin_animation_iter_t));
47+
twin_animation_iter_t *iter = twin_malloc(sizeof(twin_animation_iter_t));
4848
if (!iter || !anim)
4949
return NULL;
5050
iter->current_index = 0;

src/api.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,25 @@ void twin_destroy(twin_context_t *ctx)
5757

5858
assert(g_twin_backend.exit && "Backend not registered");
5959

60+
/* Screen teardown may emit damage callbacks through backend-owned closure.
61+
*/
62+
if (ctx->screen) {
63+
twin_screen_destroy(ctx->screen);
64+
ctx->screen = NULL;
65+
}
66+
67+
/* Backend frees its private data and ctx */
6068
g_twin_backend.exit(ctx);
69+
70+
/* Report memory statistics after all teardown */
71+
twin_memory_info_t mem;
72+
twin_memory_get_info(&mem);
73+
if (mem.peak_bytes)
74+
log_info(
75+
"Memory: current %zu bytes, peak %zu bytes, "
76+
"allocs %zu, frees %zu",
77+
mem.current_bytes, mem.peak_bytes, mem.total_allocs,
78+
mem.total_frees);
6179
}
6280

6381
/**

src/box.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget,
228228

229229
twin_box_t *twin_box_create(twin_box_t *parent, twin_box_dir_t dir)
230230
{
231-
twin_box_t *box = malloc(sizeof(twin_box_t));
231+
twin_box_t *box = twin_malloc(sizeof(twin_box_t));
232232
if (!box)
233233
return NULL;
234234

0 commit comments

Comments
 (0)