From 9b136995b7e98a01ef4be0b649438c1d7358c13b Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 26 Mar 2026 19:06:13 -0500 Subject: [PATCH] Dynamic memory tracking improved. Callback app reaping fixed. --- core/dvxApp.c | 45 ++++--- core/dvxMem.h | 36 ++---- core/platform/dvxPlatform.h | 22 ++-- core/platform/dvxPlatformDos.c | 219 ++++++++++++++++++++++----------- core/widgetEvent.c | 23 +++- loader/loaderMain.c | 6 + shell/shellApp.c | 20 +++ 7 files changed, 241 insertions(+), 130 deletions(-) diff --git a/core/dvxApp.c b/core/dvxApp.c index 0cfaace..1853cc6 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -90,6 +90,23 @@ // RGB pixel stride (bytes per pixel in 24-bit RGB) #define RGB_CHANNELS 3 +// ============================================================ +// Window callback dispatch with app ID tracking +// ============================================================ +// +// Sets currentAppId to the window's owning app before calling a +// window callback, then restores the previous value. This ensures +// allocations made during menu handlers, close handlers, paint, +// etc. are attributed to the correct app. + +#define WIN_CALLBACK(ctx, win, call) do { \ + int32_t _prevAppId = (ctx)->currentAppId; \ + (ctx)->currentAppId = (win)->appId; \ + call; \ + (ctx)->currentAppId = _prevAppId; \ +} while (0) + + // ============================================================ // Prototypes // ============================================================ @@ -304,7 +321,7 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t AccelEntryT *e = &table->entries[i]; if (e->normKey == matchKey && e->normMods == requiredMods) { - win->onMenu(win, e->cmdId); + WIN_CALLBACK(ctx, win, win->onMenu(win, e->cmdId)); return true; } } @@ -940,7 +957,7 @@ static void dispatchEvents(AppContextT *ctx) { closeAllPopups(ctx); if (win && win->onMenu) { - win->onMenu(win, menuId); + WIN_CALLBACK(ctx, win, win->onMenu(win, menuId)); } } } @@ -1753,12 +1770,12 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request wmReallocContentBuf(win, &ctx->display); if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); + WIN_CALLBACK(ctx, win, win->onResize(win, win->contentW, win->contentH)); } if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); + WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); win->contentDirty = true; } } @@ -2535,7 +2552,7 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) { // need to call dvxInvalidateWindow -- onPaint fires automatically. if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); + WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); } win->contentDirty = true; @@ -3122,7 +3139,7 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) { case SysMenuCloseE: if (win->onClose) { - win->onClose(win); + WIN_CALLBACK(ctx, win, win->onClose(win)); } else { dvxDestroyWindow(ctx, win); } @@ -3271,7 +3288,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t closeSysMenu(ctx); if (win->onClose) { - win->onClose(win); + WIN_CALLBACK(ctx, win, win->onClose(win)); } else { dvxDestroyWindow(ctx, win); } @@ -3775,7 +3792,7 @@ static void pollKeyboard(AppContextT *ctx) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; if (win->onClose) { - win->onClose(win); + WIN_CALLBACK(ctx, win, win->onClose(win)); } } @@ -4194,7 +4211,7 @@ static void pollKeyboard(AppContextT *ctx) { closeAllPopups(ctx); if (win && win->onMenu) { - win->onMenu(win, menuId); + WIN_CALLBACK(ctx, win, win->onMenu(win, menuId)); } } } else { @@ -4225,7 +4242,7 @@ static void pollKeyboard(AppContextT *ctx) { closeAllPopups(ctx); if (win && win->onMenu) { - win->onMenu(win, menuId); + WIN_CALLBACK(ctx, win, win->onMenu(win, menuId)); } } @@ -4342,7 +4359,7 @@ static void pollKeyboard(AppContextT *ctx) { if (termFocused) { // Terminal has focus -- send Tab to it if (win->onKey) { - win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags); + WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags)); } arrfree(fstack); @@ -4416,7 +4433,7 @@ static void pollKeyboard(AppContextT *ctx) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; if (win->onKey) { - win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags); + WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags)); } } @@ -4520,12 +4537,12 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t wmReallocContentBuf(win, &ctx->display); if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); + WIN_CALLBACK(ctx, win, win->onResize(win, win->contentW, win->contentH)); } if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); + WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); win->contentDirty = true; } diff --git a/core/dvxMem.h b/core/dvxMem.h index e3018f4..281f26d 100644 --- a/core/dvxMem.h +++ b/core/dvxMem.h @@ -1,27 +1,12 @@ -// dvxMem.h -- Per-app memory tracking for DVX +// dvxMem.h -- Per-app memory tracking API for DVX // -// REQUIRED: Every .c file compiled into a DXE (library, widget, shell, -// or app) MUST include this header AFTER all system includes. This -// ensures every malloc/free/calloc/realloc call goes through the -// tracking wrappers, maintaining pointer consistency. +// Declares the tracked allocation functions. DXE code does NOT need +// to include this header for tracking to work -- the DXE export table +// maps malloc/free/calloc/realloc/strdup to these wrappers transparently. // -// The implementation (dvxPlatformDos.c) does NOT include this header, -// so its dvxMalloc/dvxFree/dvxCalloc/dvxRealloc call the real libc -// functions with no recursion. -// -// How it works: -// 1. dvxMalloc prepends a 16-byte header (magic, appId, size) to -// each allocation and returns a pointer past the header. -// 2. dvxFree checks the magic value at ptr-16. If it matches, the -// header is valid and the allocation is tracked. If not, the -// pointer came from code outside DVX (libc internals, loader) -// and is passed through to the real free() unchanged. -// 3. The appId in the header comes from *dvxMemAppIdPtr, which the -// shell points at ctx->currentAppId. -// -// The magic check makes cross-boundary frees safe: if DXE code frees -// a pointer allocated by libc (e.g. from strdup, stb_ds internals), -// dvxFree detects the missing header and falls through to real free. +// This header is provided for code that needs to call the tracking +// functions by name (e.g. dvxMemGetAppUsage in the Task Manager) or +// for the dvxMemAppIdPtr declaration. #ifndef DVX_MEM_H #define DVX_MEM_H @@ -35,14 +20,9 @@ void *dvxMalloc(size_t size); void *dvxCalloc(size_t nmemb, size_t size); void *dvxRealloc(void *ptr, size_t size); void dvxFree(void *ptr); +char *dvxStrdup(const char *s); void dvxMemSnapshotLoad(int32_t appId); uint32_t dvxMemGetAppUsage(int32_t appId); void dvxMemResetApp(int32_t appId); -// The dvxMalloc/dvxFree functions are passthrough wrappers. -// Header-based per-allocation tracking was attempted but is unsafe -// in the DXE3 environment (loader-compiled code like stb_ds uses -// libc malloc while DXE code would use the wrapped version). -// Per-app memory is tracked via DPMI snapshots instead. - #endif // DVX_MEM_H diff --git a/core/platform/dvxPlatform.h b/core/platform/dvxPlatform.h index 2a38299..1a50706 100644 --- a/core/platform/dvxPlatform.h +++ b/core/platform/dvxPlatform.h @@ -24,6 +24,8 @@ #include "dvxTypes.h" +#include + #include // ============================================================ @@ -227,22 +229,18 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb); // Calls to dvxFree on non-tracked pointers (magic mismatch) fall through // to the real free() safely. -// Per-app memory tracking (see dvxMem.h for the full API). -// These are declared here for the platform implementation; DXE -// code should include dvxMem.h instead. +// Per-app memory tracking (header-based). +// The DXE export table maps malloc/free/calloc/realloc/strdup to +// these wrappers. DXE code is tracked transparently. extern int32_t *dvxMemAppIdPtr; -// Take a DPMI free-memory snapshot when an app is loaded. -// Call BEFORE the app's DXE is opened so the snapshot captures -// the free memory before the app allocates anything. +void *dvxMalloc(size_t size); +void *dvxCalloc(size_t nmemb, size_t size); +void *dvxRealloc(void *ptr, size_t size); +void dvxFree(void *ptr); +char *dvxStrdup(const char *s); void dvxMemSnapshotLoad(int32_t appId); - -// Return estimated memory usage for an app (bytes). -// Computed as the difference between the snapshot at load time -// and the current DPMI free memory. Coarse but safe. uint32_t dvxMemGetAppUsage(int32_t appId); - -// Clear the snapshot for an app (call when reaping/killing). void dvxMemResetApp(int32_t appId); // Create a directory and all parent directories (like mkdir -p). diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index 0631bb5..86e56ff 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -1113,63 +1113,35 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) { // ============================================================ -// Per-app memory tracking (DPMI snapshot) +// Per-app memory tracking (header-based) // ============================================================ // -// Tracks per-app memory by taking DPMI free-memory snapshots at -// app load time. The difference between the snapshot and current -// free memory is a coarse estimate of the app's heap footprint. +// Tracks every allocation made by DXE code via a 16-byte header +// prepended to each allocation. The DXE export table maps malloc/ +// free/calloc/realloc/strdup to these wrappers so all DXE code +// is transparently tracked without #define macros. // -// Header-based malloc wrapping (prepending a tracking header to -// each allocation) was attempted but is unsafe in the DJGPP/DXE3 -// environment: loader-compiled code (stb_ds internals, libc -// functions like strdup/localtime) allocates with libc's malloc, -// but DXE code frees with the wrapped free. Reading 16 bytes -// before an arbitrary libc pointer to check for a tracking magic -// value is undefined behavior that causes faults on 86Box/DPMI. +// stb_ds is also tracked: the loader overrides STBDS_REALLOC/FREE +// to call dvxRealloc/dvxFree before including stb_ds.h. // -// dvxMalloc/dvxFree/etc. are still exported for dvxMem.h compat -// but pass straight through to libc. +// Cross-boundary safety: when dvxFree receives a pointer that was +// allocated by libc (not our wrapper), the magic check at ptr-16 +// fails and we fall through to libc free. The DJGPP heap is a +// contiguous sbrk region so reading 16 bytes before any heap +// pointer is always valid memory (never unmapped). + +#define DVX_ALLOC_MAGIC 0xDEADBEEFUL + +typedef struct { + uint32_t magic; + int32_t appId; + uint32_t size; + uint32_t pad; +} DvxAllocHeaderT; int32_t *dvxMemAppIdPtr = NULL; -static uint32_t *sAppMemAtLoad = NULL; -static int32_t sAppMemCap = 0; - - -// Passthrough allocators (dvxMem.h #defines redirect here) -void *dvxMalloc(size_t size) { - return malloc(size); -} - - -void *dvxCalloc(size_t nmemb, size_t size) { - return calloc(nmemb, size); -} - - -void dvxFree(void *ptr) { - free(ptr); -} - - -void *dvxRealloc(void *ptr, size_t size) { - return realloc(ptr, size); -} - - -static uint32_t dpmiGetFreeKb(void) { - __dpmi_free_mem_info memInfo; - - if (__dpmi_get_free_memory_information(&memInfo) != 0) { - return 0; - } - - if (memInfo.total_number_of_free_pages == 0xFFFFFFFFUL) { - return 0; - } - - return memInfo.total_number_of_free_pages * 4; -} +static uint32_t *sAppMemUsed = NULL; +static int32_t sAppMemCap = 0; static void dvxMemGrow(int32_t appId) { @@ -1178,26 +1150,133 @@ static void dvxMemGrow(int32_t appId) { } int32_t newCap = appId + 16; - uint32_t *newArr = (uint32_t *)realloc(sAppMemAtLoad, newCap * sizeof(uint32_t)); + uint32_t *newArr = (uint32_t *)realloc(sAppMemUsed, newCap * sizeof(uint32_t)); if (!newArr) { return; } memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t)); - sAppMemAtLoad = newArr; - sAppMemCap = newCap; + sAppMemUsed = newArr; + sAppMemCap = newCap; } -void dvxMemSnapshotLoad(int32_t appId) { +void *dvxMalloc(size_t size) { + int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)malloc(sizeof(DvxAllocHeaderT) + size); + + if (!hdr) { + return NULL; + } + + hdr->magic = DVX_ALLOC_MAGIC; + hdr->appId = appId; + hdr->size = (uint32_t)size; + hdr->pad = 0; + if (appId >= 0) { dvxMemGrow(appId); if (appId < sAppMemCap) { - sAppMemAtLoad[appId] = dpmiGetFreeKb(); + sAppMemUsed[appId] += (uint32_t)size; } } + + return hdr + 1; +} + + +void *dvxCalloc(size_t nmemb, size_t size) { + size_t total = nmemb * size; + void *ptr = dvxMalloc(total); + + if (ptr) { + memset(ptr, 0, total); + } + + return ptr; +} + + +void dvxFree(void *ptr) { + if (!ptr) { + return; + } + + DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1; + + if (hdr->magic != DVX_ALLOC_MAGIC) { + // Not a tracked allocation -- pass through to real free + free(ptr); + return; + } + + int32_t appId = hdr->appId; + + if (appId >= 0 && appId < sAppMemCap) { + sAppMemUsed[appId] -= hdr->size; + } + + hdr->magic = 0; + free(hdr); +} + + +void *dvxRealloc(void *ptr, size_t size) { + if (!ptr) { + return dvxMalloc(size); + } + + if (size == 0) { + dvxFree(ptr); + return NULL; + } + + DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1; + + if (hdr->magic != DVX_ALLOC_MAGIC) { + // Not tracked -- pass through to real realloc + return realloc(ptr, size); + } + + int32_t appId = hdr->appId; + uint32_t oldSize = hdr->size; + + DvxAllocHeaderT *newHdr = (DvxAllocHeaderT *)realloc(hdr, sizeof(DvxAllocHeaderT) + size); + + if (!newHdr) { + return NULL; + } + + if (appId >= 0 && appId < sAppMemCap) { + sAppMemUsed[appId] -= oldSize; + sAppMemUsed[appId] += (uint32_t)size; + } + + newHdr->size = (uint32_t)size; + return newHdr + 1; +} + + +char *dvxStrdup(const char *s) { + if (!s) { + return NULL; + } + + size_t len = strlen(s) + 1; + char *dup = (char *)dvxMalloc(len); + + if (dup) { + memcpy(dup, s, len); + } + + return dup; +} + + +void dvxMemSnapshotLoad(int32_t appId) { + (void)appId; } @@ -1206,25 +1285,13 @@ uint32_t dvxMemGetAppUsage(int32_t appId) { return 0; } - uint32_t atLoad = sAppMemAtLoad[appId]; - - if (atLoad == 0) { - return 0; - } - - uint32_t nowFree = dpmiGetFreeKb(); - - if (nowFree >= atLoad) { - return 0; - } - - return (atLoad - nowFree) * 1024; + return sAppMemUsed[appId]; } void dvxMemResetApp(int32_t appId) { if (appId >= 0 && appId < sAppMemCap) { - sAppMemAtLoad[appId] = 0; + sAppMemUsed[appId] = 0; } } @@ -2283,6 +2350,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(dvxCalloc) DXE_EXPORT(dvxRealloc) DXE_EXPORT(dvxFree) + DXE_EXPORT(dvxStrdup) DXE_EXPORT(dvxMemSnapshotLoad) DXE_EXPORT(dvxMemGetAppUsage) DXE_EXPORT(dvxMemResetApp) @@ -2322,10 +2390,11 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(dvxLog) // --- memory --- - DXE_EXPORT(calloc) - DXE_EXPORT(free) - DXE_EXPORT(malloc) - DXE_EXPORT(realloc) + // --- memory (tracked wrappers replace libc for DXE code) --- + { "_calloc", (void *)dvxCalloc }, + { "_free", (void *)dvxFree }, + { "_malloc", (void *)dvxMalloc }, + { "_realloc", (void *)dvxRealloc }, // --- string / memory ops --- DXE_EXPORT(memchr) @@ -2339,7 +2408,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(strcmp) DXE_EXPORT(strcpy) DXE_EXPORT(strcspn) - DXE_EXPORT(strdup) + { "_strdup", (void *)dvxStrdup }, DXE_EXPORT(strerror) DXE_EXPORT(stricmp) DXE_EXPORT(strlen) diff --git a/core/widgetEvent.c b/core/widgetEvent.c index 71bba4d..f4a78d2 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -172,8 +172,14 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { return; } - // Dispatch to per-widget onKey handler via vtable + // Attribute allocations during event handling to the owning app + AppContextT *ctx = (AppContextT *)root->userData; + int32_t prevAppId = ctx->currentAppId; + ctx->currentAppId = win->appId; + wclsOnKey(focus, key, mod); + + ctx->currentAppId = prevAppId; } @@ -195,6 +201,8 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { // are also in content-buffer space (set during layout), so no // coordinate transform is needed for hit testing. +static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons); + void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; sClosedPopup = NULL; @@ -202,6 +210,19 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { if (!root) { return; } + + // Attribute allocations during event handling to the owning app + AppContextT *ctx = (AppContextT *)root->userData; + int32_t prevAppId = ctx->currentAppId; + ctx->currentAppId = win->appId; + + widgetOnMouseInner(win, root, x, y, buttons); + + ctx->currentAppId = prevAppId; +} + + +static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) { // Close popups from other windows if (sOpenPopup && sOpenPopup->window != win) { wclsClosePopup(sOpenPopup); diff --git a/loader/loaderMain.c b/loader/loaderMain.c index 5e3e473..cb5c528 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -21,6 +21,12 @@ #include #include +// Route stb_ds allocations through the tracking wrappers so that +// arrput/arrfree in DXE code is tracked per-app. +extern void *dvxRealloc(void *ptr, size_t size); +extern void dvxFree(void *ptr); +#define STBDS_REALLOC(c, p, s) dvxRealloc((p), (s)) +#define STBDS_FREE(c, p) dvxFree(p) #define STB_DS_IMPLEMENTATION #include "stb_ds.h" diff --git a/shell/shellApp.c b/shell/shellApp.c index b98ffcd..17868e8 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -557,6 +557,26 @@ bool shellReapApps(AppContextT *ctx) { if (sApps[i].state == AppStateTerminatingE) { shellReapApp(ctx, &sApps[i]); reaped = true; + continue; + } + + // Callback-only apps terminate when their last window closes. + // They have no main loop to set AppStateTerminatingE, so we + // detect termination by checking for zero remaining windows. + if (sApps[i].state == AppStateRunningE && !sApps[i].hasMainLoop) { + bool hasWindow = false; + + for (int32_t w = 0; w < ctx->stack.count; w++) { + if (ctx->stack.windows[w]->appId == sApps[i].appId) { + hasWindow = true; + break; + } + } + + if (!hasWindow) { + shellReapApp(ctx, &sApps[i]); + reaped = true; + } } }