No more hard coded limits. All dynamic.

This commit is contained in:
Scott Duensing 2026-03-26 18:33:32 -05:00
parent a793941357
commit 97503080a5
20 changed files with 387 additions and 310 deletions

View file

@ -478,7 +478,7 @@ static void compositeAndFlush(AppContextT *ctx) {
// Pre-filter visible, non-minimized windows once to avoid
// re-checking visibility in the inner dirty-rect loop
int32_t visibleIdx[MAX_WINDOWS];
int32_t visibleIdx[ws->count > 0 ? ws->count : 1];
int32_t visibleCount = 0;
for (int32_t j = 0; j < ws->count; j++) {
@ -1446,10 +1446,22 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w,
// integer compare per entry.
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
if (!table || table->count >= MAX_ACCEL_ENTRIES) {
if (!table) {
return;
}
if (table->count >= table->cap) {
int32_t newCap = table->cap ? table->cap * 2 : 8;
AccelEntryT *newBuf = (AccelEntryT *)realloc(table->entries, newCap * sizeof(AccelEntryT));
if (!newBuf) {
return;
}
table->entries = newBuf;
table->cap = newCap;
}
int32_t normKey = key;
if (normKey >= 'a' && normKey <= 'z') {
@ -1877,6 +1889,10 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) {
// ============================================================
void dvxFreeAccelTable(AccelTableT *table) {
if (table) {
free(table->entries);
}
free(table);
}
@ -2755,6 +2771,20 @@ void dvxShutdown(AppContextT *ctx) {
wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]);
}
free(ctx->stack.windows);
ctx->stack.windows = NULL;
ctx->stack.count = 0;
ctx->stack.cap = 0;
free(ctx->popup.parentStack);
ctx->popup.parentStack = NULL;
ctx->popup.parentCap = 0;
free(ctx->dirty.rects);
ctx->dirty.rects = NULL;
ctx->dirty.count = 0;
ctx->dirty.cap = 0;
free(ctx->wallpaperBuf);
ctx->wallpaperBuf = NULL;
arrfree(ctx->videoModes);
@ -3434,8 +3464,6 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
// Pushes the current popup state onto parentStack and opens the submenu
// as the new current level. The submenu is positioned at the right edge
// of the current popup, vertically aligned with the hovered item.
// MAX_SUBMENU_DEPTH prevents runaway nesting from overflowing the stack.
static void openSubMenu(AppContextT *ctx) {
if (!ctx->popup.active || !ctx->popup.menu) {
return;
@ -3453,8 +3481,17 @@ static void openSubMenu(AppContextT *ctx) {
return;
}
if (ctx->popup.depth >= MAX_SUBMENU_DEPTH) {
return;
// Grow parent stack if needed
if (ctx->popup.depth >= ctx->popup.parentCap) {
int32_t newCap = ctx->popup.parentCap ? ctx->popup.parentCap * 2 : 4;
PopupLevelT *newBuf = (PopupLevelT *)realloc(ctx->popup.parentStack, newCap * sizeof(PopupLevelT));
if (!newBuf) {
return;
}
ctx->popup.parentStack = newBuf;
ctx->popup.parentCap = newCap;
}
// Push current state to parent stack
@ -4256,15 +4293,14 @@ static void pollKeyboard(AppContextT *ctx) {
if (win->widgetRoot) {
// Find currently focused widget
WidgetT *current = NULL;
WidgetT *fstack[64];
int32_t ftop = 0;
fstack[ftop++] = win->widgetRoot;
WidgetT **fstack = NULL;
arrput(fstack, win->widgetRoot);
while (ftop > 0) {
WidgetT *w = fstack[--ftop];
while (arrlen(fstack) > 0) {
WidgetT *w = fstack[arrlen(fstack) - 1];
arrsetlen(fstack, arrlen(fstack) - 1);
if (w->focused && widgetIsFocusable(w->type)) {
// Don't tab out of widgets that swallow Tab
if (w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) {
current = NULL;
break;
@ -4275,21 +4311,21 @@ static void pollKeyboard(AppContextT *ctx) {
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible && ftop < 64) {
fstack[ftop++] = c;
if (c->visible) {
arrput(fstack, c);
}
}
}
// Terminal swallowed Tab -- send to widget system instead
if (current == NULL) {
// Check if a terminal is focused
ftop = 0;
fstack[ftop++] = win->widgetRoot;
arrsetlen(fstack, 0);
arrput(fstack, win->widgetRoot);
bool termFocused = false;
while (ftop > 0) {
WidgetT *w = fstack[--ftop];
while (arrlen(fstack) > 0) {
WidgetT *w = fstack[arrlen(fstack) - 1];
arrsetlen(fstack, arrlen(fstack) - 1);
if (w->focused && w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) {
termFocused = true;
@ -4297,8 +4333,8 @@ static void pollKeyboard(AppContextT *ctx) {
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible && ftop < 64) {
fstack[ftop++] = c;
if (c->visible) {
arrput(fstack, c);
}
}
}
@ -4309,6 +4345,7 @@ static void pollKeyboard(AppContextT *ctx) {
win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags);
}
arrfree(fstack);
continue;
}
}
@ -4366,6 +4403,8 @@ static void pollKeyboard(AppContextT *ctx) {
wgtInvalidate(win->widgetRoot);
}
arrfree(fstack);
}
}

View file

@ -20,7 +20,9 @@
#include "dvxComp.h"
#include "dvxPlatform.h"
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"
// Rects within this many pixels of each other get merged even if they don't
// overlap. A small gap tolerance absorbs jitter from mouse movement and
@ -42,32 +44,24 @@ static inline void rectUnion(const RectT *a, const RectT *b, RectT *result);
// dirtyListAdd
// ============================================================
//
// Appends a dirty rect to the list. Uses a fixed-size array (MAX_DIRTY_RECTS
// = 128) rather than a dynamic allocation -- this is called on every UI
// mutation (drag, repaint, focus change) so allocation overhead must be zero.
//
// When the list fills up, an eager merge pass tries to consolidate rects.
// If the list is STILL full after merging (pathological scatter), the
// nuclear option collapses everything into one bounding box. This guarantees
// the list never overflows, at the cost of potentially over-painting a large
// rect. In practice the merge pass almost always frees enough slots because
// GUI mutations tend to cluster spatially.
// Appends a dirty rect to the list. The array grows dynamically but a
// merge pass fires at 128 entries to keep the list short. If the list
// is still long after merging (pathological scatter), everything collapses
// into one bounding box. In practice the merge pass almost always frees
// enough slots because GUI mutations tend to cluster spatially.
#define DIRTY_SOFT_CAP 128
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
// Branch hint: degenerate rects are rare -- callers usually validate first
if (__builtin_expect(w <= 0 || h <= 0, 0)) {
return;
}
// Overflow path: try merging, then fall back to a single bounding rect
if (__builtin_expect(dl->count >= MAX_DIRTY_RECTS, 0)) {
// Soft cap: merge when we accumulate too many rects to keep composite fast
if (__builtin_expect(dl->count >= DIRTY_SOFT_CAP, 0)) {
dirtyListMerge(dl);
if (dl->count >= MAX_DIRTY_RECTS) {
// Still full -- collapse the entire list plus the new rect into one
// bounding box. This is a last resort; it means the next flush will
// repaint a potentially large region, but at least we won't lose
// dirty information or crash.
if (dl->count >= DIRTY_SOFT_CAP) {
RectT merged = dl->rects[0];
for (int32_t i = 1; i < dl->count; i++) {
@ -83,6 +77,19 @@ void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
}
}
// Grow array if needed
if (dl->count >= dl->cap) {
int32_t newCap = dl->cap ? dl->cap * 2 : DIRTY_SOFT_CAP;
RectT *newBuf = (RectT *)realloc(dl->rects, newCap * sizeof(RectT));
if (!newBuf) {
return;
}
dl->rects = newBuf;
dl->cap = newCap;
}
dl->rects[dl->count].x = x;
dl->rects[dl->count].y = y;
dl->rects[dl->count].w = w;

View file

@ -25,8 +25,7 @@
// Zero the dirty rect count. Called at the start of each frame.
void dirtyListInit(DirtyListT *dl);
// Enqueue a dirty rectangle. If the list is full (MAX_DIRTY_RECTS),
// this should degrade gracefully (e.g. expand an existing rect).
// Enqueue a dirty rectangle. Grows dynamically; merges at a soft cap.
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h);
// Consolidate the dirty list by merging overlapping and adjacent rects.

View file

@ -629,7 +629,6 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t
// listbox to distinguish them from files, following the DOS convention.
// FD_MAX_PATH is 260 to match DOS MAX_PATH (including null terminator)
#define FD_MAX_ENTRIES 512
#define FD_MAX_PATH 260
#define FD_NAME_LEN 64
@ -642,10 +641,10 @@ typedef struct {
const FileFilterT *filters; // caller-provided filter list
int32_t filterCount;
int32_t activeFilter; // index into filters[]
char *entryNames[FD_MAX_ENTRIES]; // heap-allocated, freed by fdFreeEntries
bool entryIsDir[FD_MAX_ENTRIES];
char **entryNames; // dynamic array of heap-allocated strings
bool *entryIsDir; // dynamic array, parallel to entryNames
int32_t entryCount;
const char *listItems[FD_MAX_ENTRIES]; // pointers into entryNames, for listbox API
const char **listItems; // dynamic array, pointers into entryNames
WidgetT *fileList;
WidgetT *pathInput;
WidgetT *nameInput;
@ -745,9 +744,14 @@ static bool fdFilterMatch(const char *name, const char *pattern) {
static void fdFreeEntries(void) {
for (int32_t i = 0; i < sFd.entryCount; i++) {
free(sFd.entryNames[i]);
sFd.entryNames[i] = NULL;
}
free(sFd.entryNames);
free(sFd.entryIsDir);
free(sFd.listItems);
sFd.entryNames = NULL;
sFd.entryIsDir = NULL;
sFd.listItems = NULL;
sFd.entryCount = 0;
}
@ -803,7 +807,7 @@ static void fdLoadDir(void) {
struct dirent *ent;
while ((ent = readdir(dir)) != NULL && sFd.entryCount < FD_MAX_ENTRIES) {
while ((ent = readdir(dir)) != NULL) {
// Skip "."
if (strcmp(ent->d_name, ".") == 0) {
continue;
@ -826,7 +830,15 @@ static void fdLoadDir(void) {
continue;
}
// Grow arrays
int32_t idx = sFd.entryCount;
sFd.entryNames = (char **)realloc(sFd.entryNames, (idx + 1) * sizeof(char *));
sFd.entryIsDir = (bool *)realloc(sFd.entryIsDir, (idx + 1) * sizeof(bool));
if (!sFd.entryNames || !sFd.entryIsDir) {
break;
}
sFd.entryIsDir[idx] = isDir;
if (isDir) {
@ -843,8 +855,17 @@ static void fdLoadDir(void) {
closedir(dir);
if (sFd.entryCount == 0) {
wgtListBoxSetItems(sFd.fileList, NULL, 0);
return;
}
// Sort: build index array, sort, reorder
int32_t sortIdx[FD_MAX_ENTRIES];
int32_t *sortIdx = (int32_t *)malloc(sFd.entryCount * sizeof(int32_t));
if (!sortIdx) {
return;
}
for (int32_t i = 0; i < sFd.entryCount; i++) {
sortIdx[i] = i;
@ -853,18 +874,26 @@ static void fdLoadDir(void) {
qsort(sortIdx, sFd.entryCount, sizeof(int32_t), fdEntryCompare);
// Rebuild arrays in sorted order
char *tmpNames[FD_MAX_ENTRIES];
bool tmpIsDir[FD_MAX_ENTRIES];
char **tmpNames = (char **)malloc(sFd.entryCount * sizeof(char *));
bool *tmpIsDir = (bool *)malloc(sFd.entryCount * sizeof(bool));
for (int32_t i = 0; i < sFd.entryCount; i++) {
tmpNames[i] = sFd.entryNames[sortIdx[i]];
tmpIsDir[i] = sFd.entryIsDir[sortIdx[i]];
if (tmpNames && tmpIsDir) {
for (int32_t i = 0; i < sFd.entryCount; i++) {
tmpNames[i] = sFd.entryNames[sortIdx[i]];
tmpIsDir[i] = sFd.entryIsDir[sortIdx[i]];
}
memcpy(sFd.entryNames, tmpNames, sizeof(char *) * sFd.entryCount);
memcpy(sFd.entryIsDir, tmpIsDir, sizeof(bool) * sFd.entryCount);
}
memcpy(sFd.entryNames, tmpNames, sizeof(char *) * sFd.entryCount);
memcpy(sFd.entryIsDir, tmpIsDir, sizeof(bool) * sFd.entryCount);
free(sortIdx);
free(tmpNames);
free(tmpIsDir);
// Build listItems pointer array for the listbox
sFd.listItems = (const char **)realloc(sFd.listItems, sFd.entryCount * sizeof(const char *));
for (int32_t i = 0; i < sFd.entryCount; i++) {
sFd.listItems[i] = sFd.entryNames[i];
}

View file

@ -52,7 +52,9 @@
#include "dvxDraw.h"
#include "dvxPlatform.h"
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"
// ============================================================
// Prototypes

View file

@ -30,3 +30,6 @@
#include "thirdparty/stb_image.h"
#pragma GCC diagnostic pop
#include <stdlib.h>
#include "dvxMem.h"

View file

@ -18,3 +18,6 @@
#include "thirdparty/stb_image_write.h"
#pragma GCC diagnostic pop
#include <stdlib.h>
#include "dvxMem.h"

View file

@ -26,6 +26,7 @@
#ifndef DVX_MEM_H
#define DVX_MEM_H
#include <stdint.h>
#include <stdlib.h>
extern int32_t *dvxMemAppIdPtr;
@ -38,11 +39,10 @@ void dvxMemSnapshotLoad(int32_t appId);
uint32_t dvxMemGetAppUsage(int32_t appId);
void dvxMemResetApp(int32_t appId);
// Redirect standard allocator calls to tracking wrappers.
// This MUST appear after <stdlib.h> so the real prototypes exist.
#define malloc(s) dvxMalloc(s)
#define calloc(n, s) dvxCalloc((n), (s))
#define realloc(p, s) dvxRealloc((p), (s))
#define free(p) dvxFree(p)
// 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

View file

@ -10,6 +10,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"
// stb_ds dynamic arrays (implementation lives in libtasks.a)
#include "thirdparty/stb_ds.h"

View file

@ -259,11 +259,10 @@ typedef struct {
// cache-friendly at this size. If the list fills up, the compositor
// can merge aggressively or fall back to a full-screen repaint.
#define MAX_DIRTY_RECTS 128
typedef struct {
RectT rects[MAX_DIRTY_RECTS];
RectT *rects; // dynamic array (realloc'd on demand)
int32_t count;
int32_t cap;
} DirtyListT;
// ============================================================
@ -305,8 +304,6 @@ typedef struct {
// The forward declaration of MenuT is needed because MenuItemT contains
// a pointer to its parent type (circular reference between item and menu).
#define MAX_MENU_ITEMS 16
#define MAX_MENUS 8
#define MAX_MENU_LABEL 32
// Forward declaration for submenu pointers
@ -333,8 +330,9 @@ typedef struct {
// needs a forward pointer to it for cascading submenus.
struct MenuT {
char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File")
MenuItemT items[MAX_MENU_ITEMS];
int32_t itemCount;
MenuItemT *items; // dynamic array (realloc'd on demand)
int32_t itemCount;
int32_t itemCap;
int32_t barX; // computed position on menu bar
int32_t barW; // computed width on menu bar
char accelKey; // lowercase accelerator character, 0 if none
@ -344,8 +342,9 @@ struct MenuT {
// menu bar is actually drawn, avoiding redundant work when multiple
// menus are added in sequence during window setup.
typedef struct {
MenuT menus[MAX_MENUS];
MenuT *menus; // dynamic array (realloc'd on demand)
int32_t menuCount;
int32_t menuCap;
int32_t activeIdx; // menu bar item with open popup (-1 = none)
bool positionsDirty; // true = barX/barW need recomputation
} MenuBarT;
@ -394,7 +393,6 @@ typedef struct {
// match in O(n) with just two integer comparisons per entry, avoiding
// case-folding and modifier normalization on every keystroke.
#define MAX_ACCEL_ENTRIES 32
// Modifier flags for accelerators (match BIOS shift state bits)
#define ACCEL_SHIFT 0x03
@ -431,8 +429,9 @@ typedef struct {
} AccelEntryT;
typedef struct {
AccelEntryT entries[MAX_ACCEL_ENTRIES];
int32_t count;
AccelEntryT *entries; // dynamic array (realloc'd on demand)
int32_t count;
int32_t cap;
} AccelTableT;
// ============================================================
@ -453,7 +452,6 @@ typedef struct {
// RESIZE_xxx flags are bitfields so that corner resizes can combine two
// edges (e.g. RESIZE_LEFT | RESIZE_TOP for the top-left corner).
#define MAX_WINDOWS 64
#define MAX_TITLE_LEN 128
#define RESIZE_NONE 0
@ -577,8 +575,9 @@ typedef struct WindowT {
// you can't drag two windows simultaneously with a single mouse.
typedef struct {
WindowT *windows[MAX_WINDOWS];
int32_t count;
WindowT **windows; // dynamic array (realloc'd on demand)
int32_t count;
int32_t cap;
int32_t focusedIdx;
int32_t dragWindow;
int32_t dragOffX;
@ -629,15 +628,14 @@ typedef struct {
// Only one popup chain can be active at a time (menus are modal).
// Cascading submenus are tracked as a stack of PopupLevelT frames,
// where each level saves the parent menu's state so it can be restored
// when the submenu closes. MAX_SUBMENU_DEPTH of 4 allows File > Recent >
// Category > Item style nesting, which is deeper than any sane DOS UI needs.
// when the submenu closes. The stack grows dynamically to support
// arbitrary nesting depth.
//
// The popup's screen coordinates are stored directly rather than computed
// relative to the window, because the popup is painted on top of
// everything during the compositor's overlay pass and needs absolute
// screen positioning.
#define MAX_SUBMENU_DEPTH 4
// Saved parent popup state when a submenu is open
typedef struct {
@ -662,7 +660,8 @@ typedef struct {
int32_t hoverItem; // highlighted item in current popup (-1 = none)
MenuT *menu; // direct pointer to current menu (avoids lookup for submenus)
int32_t depth; // 0 = top-level only, 1+ = submenu depth
PopupLevelT parentStack[MAX_SUBMENU_DEPTH];
PopupLevelT *parentStack; // dynamic array (realloc'd on demand)
int32_t parentCap;
} PopupStateT;
// ============================================================

View file

@ -45,7 +45,9 @@
#include "dvxPlatform.h"
#include "dvxPalette.h"
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"
// ============================================================

View file

@ -11,7 +11,7 @@
// = front). This was chosen over a linked list because hit-testing walks
// the stack front-to-back every mouse event, and array iteration has
// better cache behavior on 486/Pentium. Raising a window is O(N) shift
// but N is bounded by MAX_WINDOWS=64 and raise is infrequent.
// but N is bounded by the window count and raise is infrequent.
//
// - Window chrome uses a Motif/GEOS Ensemble visual style with fixed 4px
// outer bevels and 2px inner bevels. The fixed bevel widths avoid per-
@ -39,6 +39,7 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "dvxMem.h"
// ============================================================
// Constants
@ -712,6 +713,11 @@ static void freeMenuRecursive(MenuT *menu) {
menu->items[i].subMenu = NULL;
}
}
free(menu->items);
menu->items = NULL;
menu->itemCount = 0;
menu->itemCap = 0;
}
@ -861,7 +867,7 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// ============================================================
//
// Adds a top-level menu to a window's menu bar. The MenuT struct is
// stored inline in the MenuBarT's fixed-size array (MAX_MENUS=8) rather
// stored inline in the MenuBarT's dynamic array rather
// than heap-allocated, since menus are created once at window setup and
// the count is small. The positionsDirty flag is set so that the next
// paint triggers computeMenuBarPositions to lay out all labels.
@ -871,9 +877,35 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// stored on the MenuT so the event loop can match keyboard shortcuts
// without re-parsing the label string on every keypress.
static bool menuGrowItems(MenuT *menu) {
if (menu->itemCount < menu->itemCap) {
return true;
}
int32_t newCap = menu->itemCap ? menu->itemCap * 2 : 8;
MenuItemT *newBuf = (MenuItemT *)realloc(menu->items, newCap * sizeof(MenuItemT));
if (!newBuf) {
return false;
}
menu->items = newBuf;
menu->itemCap = newCap;
return true;
}
MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
if (bar->menuCount >= MAX_MENUS) {
return NULL;
if (bar->menuCount >= bar->menuCap) {
int32_t newCap = bar->menuCap ? bar->menuCap * 2 : 8;
MenuT *newBuf = (MenuT *)realloc(bar->menus, newCap * sizeof(MenuT));
if (!newBuf) {
return NULL;
}
bar->menus = newBuf;
bar->menuCap = newCap;
}
MenuT *menu = &bar->menus[bar->menuCount];
@ -895,7 +927,7 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
// Allocates and attaches a menu bar to a window. The menu bar is heap-
// allocated separately from the window because most windows don't have
// one, and keeping it out of WindowT saves ~550 bytes per window
// (MAX_MENUS * sizeof(MenuT)). wmUpdateContentRect is called to shrink
// wmUpdateContentRect is called to shrink
// the content area by CHROME_MENU_HEIGHT to make room for the bar.
MenuBarT *wmAddMenuBar(WindowT *win) {
@ -918,7 +950,7 @@ MenuBarT *wmAddMenuBar(WindowT *win) {
// ============================================================
void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) {
if (menu->itemCount >= MAX_MENU_ITEMS) {
if (!menuGrowItems(menu)) {
return;
}
@ -940,7 +972,7 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) {
// ============================================================
void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) {
if (menu->itemCount >= MAX_MENU_ITEMS) {
if (!menuGrowItems(menu)) {
return;
}
@ -962,7 +994,7 @@ void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked
// ============================================================
void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) {
if (menu->itemCount >= MAX_MENU_ITEMS) {
if (!menuGrowItems(menu)) {
return;
}
@ -984,7 +1016,7 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked
// ============================================================
void wmAddMenuSeparator(MenuT *menu) {
if (menu->itemCount >= MAX_MENU_ITEMS) {
if (!menuGrowItems(menu)) {
return;
}
@ -1007,7 +1039,7 @@ void wmAddMenuSeparator(MenuT *menu) {
// item opens the child rather than firing a command.
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) {
if (parentMenu->itemCount >= MAX_MENU_ITEMS) {
if (!menuGrowItems(parentMenu)) {
return NULL;
}
@ -1084,9 +1116,17 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// and wmRaiseWindow if desired.
WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
if (stack->count >= MAX_WINDOWS) {
fprintf(stderr, "WM: Maximum windows (%d) reached\n", MAX_WINDOWS);
return NULL;
if (stack->count >= stack->cap) {
int32_t newCap = stack->cap ? stack->cap * 2 : 16;
WindowT **newBuf = (WindowT **)realloc(stack->windows, newCap * sizeof(WindowT *));
if (!newBuf) {
fprintf(stderr, "WM: Failed to grow window stack\n");
return NULL;
}
stack->windows = newBuf;
stack->cap = newCap;
}
WindowT *win = (WindowT *)malloc(sizeof(WindowT));
@ -1188,6 +1228,7 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
freeMenuRecursive(&win->menuBar->menus[i]);
}
free(win->menuBar->menus);
free(win->menuBar);
}
@ -2626,6 +2667,7 @@ void wmFreeMenu(MenuT *menu) {
}
}
free(menu->items);
free(menu);
}

View file

@ -80,7 +80,7 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked
void wmAddMenuSeparator(MenuT *menu);
// Create a cascading submenu attached to the parent menu. Returns the
// child MenuT to populate, or NULL if MAX_MENU_ITEMS is exhausted.
// child MenuT to populate, or NULL on allocation failure.
// The child MenuT is heap-allocated and freed when the parent window
// is destroyed.
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label);

View file

@ -1113,31 +1113,63 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) {
// ============================================================
// Per-app memory tracking (header-based)
// Per-app memory tracking (DPMI snapshot)
// ============================================================
//
// Every DXE .c file includes dvxMem.h which #defines malloc/free/
// calloc/realloc to dvxMalloc/dvxFree/dvxCalloc/dvxRealloc. This
// file does NOT include dvxMem.h, so calls to malloc/free here go
// directly to libc with no recursion.
// 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.
//
// Each tracked allocation has a 16-byte header prepended. dvxFree
// checks the magic before adjusting the pointer -- if it doesn't
// match (pointer came from libc internals or loader code), it
// falls through to the real free() unchanged.
#define DVX_ALLOC_MAGIC 0xDEADBEEFUL
typedef struct {
uint32_t magic;
int32_t appId;
uint32_t size;
uint32_t pad;
} DvxAllocHeaderT;
// 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.
//
// dvxMalloc/dvxFree/etc. are still exported for dvxMem.h compat
// but pass straight through to libc.
int32_t *dvxMemAppIdPtr = NULL;
static uint32_t *sAppMemUsed = NULL;
static int32_t sAppMemCap = 0;
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 void dvxMemGrow(int32_t appId) {
@ -1146,114 +1178,26 @@ static void dvxMemGrow(int32_t appId) {
}
int32_t newCap = appId + 16;
uint32_t *newArr = (uint32_t *)realloc(sAppMemUsed, newCap * sizeof(uint32_t));
uint32_t *newArr = (uint32_t *)realloc(sAppMemAtLoad, newCap * sizeof(uint32_t));
if (!newArr) {
return;
}
memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t));
sAppMemUsed = newArr;
sAppMemCap = newCap;
}
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) { 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 (libc, stb_ds, loader) -- pass through
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
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;
sAppMemAtLoad = newArr;
sAppMemCap = newCap;
}
void dvxMemSnapshotLoad(int32_t appId) {
(void)appId;
if (appId >= 0) {
dvxMemGrow(appId);
if (appId < sAppMemCap) {
sAppMemAtLoad[appId] = dpmiGetFreeKb();
}
}
}
@ -1262,13 +1206,25 @@ uint32_t dvxMemGetAppUsage(int32_t appId) {
return 0;
}
return sAppMemUsed[appId];
uint32_t atLoad = sAppMemAtLoad[appId];
if (atLoad == 0) {
return 0;
}
uint32_t nowFree = dpmiGetFreeKb();
if (nowFree >= atLoad) {
return 0;
}
return (atLoad - nowFree) * 1024;
}
void dvxMemResetApp(int32_t appId) {
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] = 0;
sAppMemAtLoad[appId] = 0;
}
}

View file

@ -49,9 +49,9 @@ WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
WidgetT *sDragWidget = NULL; // widget being dragged (any drag type)
// Shared clipboard -- process-wide, not per-widget.
#define CLIPBOARD_MAX 4096
static char sClipboard[CLIPBOARD_MAX];
static int32_t sClipboardLen = 0;
static char *sClipboard = NULL;
static int32_t sClipboardLen = 0;
static int32_t sClipboardCap = 0;
// Multi-click state (used by widgetEvent.c for universal dbl-click detection)
static clock_t sLastClickTime = 0;
@ -69,8 +69,16 @@ void clipboardCopy(const char *text, int32_t len) {
return;
}
if (len > CLIPBOARD_MAX - 1) {
len = CLIPBOARD_MAX - 1;
if (len + 1 > sClipboardCap) {
int32_t newCap = len + 1;
char *newBuf = (char *)realloc(sClipboard, newCap);
if (!newBuf) {
return;
}
sClipboard = newBuf;
sClipboardCap = newCap;
}
memcpy(sClipboard, text, len);
@ -97,7 +105,7 @@ const char *clipboardGet(int32_t *outLen) {
// ============================================================
int32_t clipboardMaxLen(void) {
return CLIPBOARD_MAX - 1;
return 65536;
}
@ -378,73 +386,62 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
// ============================================================
//
// Shift+Tab navigation: finds the previous focusable widget.
// Unlike findNextFocusable which can short-circuit during traversal,
// finding the PREVIOUS widget requires knowing the full order.
// So this collects all focusable widgets into an array, finds the
// target's index, and returns index-1 (with wraparound).
//
// The explicit stack-based DFS (rather than recursion) is used here
// because we need to push children in reverse order to get the same
// left-to-right depth-first ordering as the recursive version.
// Fixed-size arrays (128 widgets, 64 stack depth) are adequate for
// any reasonable dialog layout and avoid dynamic allocation.
// Collects all focusable widgets via DFS, then returns the one
// before 'before' (with wraparound). Uses stb_ds dynamic arrays
// so there's no fixed limit on widget count or tree depth.
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
WidgetT *list[128];
int32_t count = 0;
WidgetT **list = NULL;
WidgetT **stack = NULL;
// Collect all focusable widgets via depth-first traversal
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
arrput(stack, root);
while (top > 0) {
WidgetT *w = stack[--top];
while (arrlen(stack) > 0) {
WidgetT *w = stack[arrlen(stack) - 1];
arrsetlen(stack, arrlen(stack) - 1);
if (!w->visible || !w->enabled) {
continue;
}
if (widgetIsFocusable(w->type) && count < 128) {
list[count++] = w;
if (widgetIsFocusable(w->type)) {
arrput(list, w);
}
// Push children in reverse order so first child is processed first
WidgetT *children[64];
int32_t childCount = 0;
// Walk to end of sibling list, then push backwards
WidgetT **children = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (childCount < 64) {
children[childCount++] = c;
arrput(children, c);
}
for (int32_t i = arrlen(children) - 1; i >= 0; i--) {
arrput(stack, children[i]);
}
arrfree(children);
}
WidgetT *result = NULL;
int32_t count = arrlen(list);
if (count > 0) {
int32_t idx = -1;
for (int32_t i = 0; i < count; i++) {
if (list[i] == before) {
idx = i;
break;
}
}
for (int32_t i = childCount - 1; i >= 0; i--) {
if (top < 64) {
stack[top++] = children[i];
}
}
result = (idx <= 0) ? list[count - 1] : list[idx - 1];
}
if (count == 0) {
return NULL;
}
// Find 'before' in the list
int32_t idx = -1;
for (int32_t i = 0; i < count; i++) {
if (list[i] == before) {
idx = i;
break;
}
}
if (idx <= 0) {
return list[count - 1]; // Wrap to last
}
return list[idx - 1];
arrfree(list);
arrfree(stack);
return result;
}

View file

@ -172,14 +172,8 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return;
}
// Attribute allocations during event handling to the owning app
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
// Dispatch to per-widget onKey handler via vtable
wclsOnKey(focus, key, mod);
ctx->currentAppId = prevAppId;
}
@ -201,8 +195,6 @@ 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;
@ -210,19 +202,6 @@ 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);

View file

@ -82,11 +82,15 @@ static int32_t allocSlot(void) {
// this function never completes -- the task is killed externally via
// shellForceKillApp + tsKill.
static void appTaskWrapper(void *arg) {
ShellAppT *app = (ShellAppT *)arg;
app->dxeCtx.shellCtx->currentAppId = app->appId;
app->entryFn(&app->dxeCtx);
app->dxeCtx.shellCtx->currentAppId = 0;
// Look up the app by ID, not by pointer. The sApps array may have
// been reallocated between tsCreate and the first time this task runs
// (e.g. another app was loaded in the meantime), which would
// invalidate a direct ShellAppT pointer.
int32_t appId = (int32_t)(intptr_t)arg;
ShellAppT *app = &sApps[appId];
app->dxeCtx->shellCtx->currentAppId = app->appId;
app->entryFn(app->dxeCtx);
app->dxeCtx->shellCtx->currentAppId = 0;
// App returned from its main loop -- mark for reaping
app->state = AppStateTerminatingE;
@ -261,6 +265,8 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
}
cleanupTempFile(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId);
app->state = AppStateFreeE;
dvxLog("Shell: force-killed app '%s'", app->name);
@ -399,29 +405,39 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
app->shutdownFn = shutdown;
app->state = AppStateLoadedE;
// Set up the context passed to appMain
app->dxeCtx.shellCtx = ctx;
app->dxeCtx.appId = id;
// Heap-allocate the DxeAppContextT so its address is stable across
// sApps reallocs. Apps save this pointer in their static globals.
app->dxeCtx = (DxeAppContextT *)calloc(1, sizeof(DxeAppContextT));
if (!app->dxeCtx) {
ctx->currentAppId = 0;
dlclose(handle);
app->state = AppStateFreeE;
return -1;
}
app->dxeCtx->shellCtx = ctx;
app->dxeCtx->appId = id;
// Derive app directory from path (everything up to the last separator).
// This lets apps load resources relative to their own location rather
// than the shell's working directory.
snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path);
snprintf(app->dxeCtx->appDir, sizeof(app->dxeCtx->appDir), "%s", path);
char *sep = platformPathDirEnd(app->dxeCtx.appDir);
char *sep = platformPathDirEnd(app->dxeCtx->appDir);
if (sep) {
*sep = '\0';
} else {
app->dxeCtx.appDir[0] = '.';
app->dxeCtx.appDir[1] = '\0';
app->dxeCtx->appDir[0] = '.';
app->dxeCtx->appDir[1] = '\0';
}
// Derive config directory: replace the APPS/ prefix with CONFIG/.
// e.g. "APPS/GAMES/TETRIS" -> "CONFIG/GAMES/TETRIS"
// If the path doesn't start with "apps/" or "APPS/", fall back to
// "CONFIG/<appname>" using the descriptor name.
const char *appDirStr = app->dxeCtx.appDir;
const char *appDirStr = app->dxeCtx->appDir;
const char *appsPrefix = NULL;
if (strncasecmp(appDirStr, "apps/", 5) == 0 || strncasecmp(appDirStr, "apps\\", 5) == 0) {
@ -431,9 +447,9 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
}
if (appsPrefix && appsPrefix[0]) {
snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", appsPrefix);
snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", appsPrefix);
} else {
snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", desc->name);
snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", desc->name);
}
// Launch. Set currentAppId before any app code runs so that
@ -444,12 +460,14 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE;
int32_t priority = desc->priority;
int32_t taskId = tsCreate(desc->name, appTaskWrapper, app, stackSize, priority);
int32_t taskId = tsCreate(desc->name, appTaskWrapper, (void *)(intptr_t)id, stackSize, priority);
if (taskId < 0) {
ctx->currentAppId = 0;
dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR);
dlclose(handle);
free(app->dxeCtx);
app->dxeCtx = NULL;
app->state = AppStateFreeE;
return -1;
}
@ -460,7 +478,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// The app creates its windows and returns. From this point on,
// the app lives entirely through event callbacks dispatched by
// the shell's dvxUpdate loop. No separate task or stack needed.
app->entryFn(&app->dxeCtx);
app->entryFn(app->dxeCtx);
}
ctx->currentAppId = 0;
@ -511,6 +529,8 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
}
cleanupTempFile(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId);
dvxLog("Shell: reaped app '%s'", app->name);
app->state = AppStateFreeE;

View file

@ -93,7 +93,7 @@ typedef struct {
uint32_t mainTaskId; // task ID if hasMainLoop, else 0
int32_t (*entryFn)(DxeAppContextT *);
void (*shutdownFn)(void); // may be NULL
DxeAppContextT dxeCtx; // context passed to appMain
DxeAppContextT *dxeCtx; // heap-allocated; address stable across sApps realloc
} ShellAppT;
// ============================================================

View file

@ -374,7 +374,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
// Ctrl+Z -- undo
if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
// Swap current and undo
char tmpBuf[clipboardMaxLen() + 1];
char tmpBuf[*pLen + 1];
int32_t tmpLen = *pLen;
int32_t tmpCursor = *pCursor;
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;

View file

@ -111,7 +111,6 @@ typedef struct {
#define TEXTAREA_SB_W 14
#define TEXTAREA_MIN_ROWS 4
#define TEXTAREA_MIN_COLS 20
#define CLIPBOARD_MAX 4096
// Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c)
#define CURSOR_BLINK_MS 250
@ -950,7 +949,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == 26) {
if (ta->undoBuf && ta->undoLen >= 0) {
// Swap current and undo
char tmpBuf[CLIPBOARD_MAX];
char tmpBuf[*pLen + 1];
int32_t tmpLen = *pLen;
int32_t tmpCursor = CUR_OFF();
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;