// shellMain.c -- DVX Shell entry point and main loop // // Initializes the GUI, task system, DXE export table, and loads // the desktop app. Runs the cooperative main loop, yielding to // app tasks and reaping terminated apps each frame. // // The main loop design (dvxUpdate + tsYield + reap + notify): // Each iteration does four things: // 1. dvxUpdate: processes input events, dispatches callbacks, composites // dirty rects, flushes to the LFB. This is the shell's primary job. // 2. tsYield: gives CPU time to app tasks. Without this, main-loop apps // would never run because the shell task would monopolize the CPU. // 3. shellReapApps: cleans up any apps that terminated during this frame // (either their task returned or their last window was closed). // 4. desktopUpdate: notifies the desktop app (Program Manager) if any // apps were reaped, so it can refresh its task list. // // Crash recovery uses setjmp/longjmp: // The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL // via platformInstallCrashHandler(). If a crash occurs in an app task, the // handler logs platform-specific diagnostics (register dump on DJGPP), // then longjmps back to the setjmp point in main(). This works because // longjmp restores the main task's stack frame regardless of which task // was running. tsRecoverToMain() then fixes the scheduler's bookkeeping, // the shell logs app-specific info, and the crashed app is killed. // This gives the shell Windows 3.1-style fault tolerance -- one bad app // doesn't take down the whole system. #include "shellApp.h" #include "shellInf.h" #include "dvxDlg.h" #include "dvxDraw.h" #include "dvxVideo.h" #include "dvxComp.h" #include "dvxPrefs.h" #include "dvxPlat.h" #include "stb_ds_wrap.h" #include #include #include #include #include "dvxMem.h" // ============================================================ // Module state // ============================================================ static AppContextT sCtx; static PrefsHandleT *sPrefs = NULL; // setjmp buffer for crash recovery. The crash handler longjmps here to // return control to the shell's main loop after an app crashes. static jmp_buf sCrashJmp; // Volatile because it's written from a signal handler context. Tells // the recovery code which signal fired (for logging/diagnostics). static volatile int sCrashSignal = 0; // Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel) typedef void (*DesktopUpdateFnT)(void); static DesktopUpdateFnT *sDesktopUpdateFns = NULL; // ============================================================ // Prototypes // ============================================================ static void idleYield(void *ctx); static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData); // ============================================================ // shellDesktopUpdate -- notify desktop app of state change // ============================================================ void shellDesktopUpdate(void) { for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { sDesktopUpdateFns[i](); } } // ============================================================ // ctrlEscHandler -- system-wide Ctrl+Esc callback // ============================================================ static void ctrlEscHandler(void *ctx) { if (shellCtrlEscFn) { shellCtrlEscFn((AppContextT *)ctx); } } // ============================================================ // f1HelpHandler -- system-wide F1 launches context-sensitive help // ============================================================ static void f1HelpHandler(void *ctx) { AppContextT *ac = (AppContextT *)ctx; // Find the focused window's owning app char args[1024] = {0}; int32_t focusedAppId = 0; if (ac->stack.focusedIdx >= 0) { WindowT *win = ac->stack.windows[ac->stack.focusedIdx]; focusedAppId = win->appId; } if (focusedAppId > 0) { ShellAppT *app = shellGetApp(focusedAppId); if (app && app->dxeCtx && app->dxeCtx->helpFile[0]) { if (app->dxeCtx->helpTopic[0]) { snprintf(args, sizeof(args), "%s %s", app->dxeCtx->helpFile, app->dxeCtx->helpTopic); } else { snprintf(args, sizeof(args), "%s", app->dxeCtx->helpFile); } } } // Fall back to system help if no app-specific help char viewerPath[DVX_MAX_PATH]; snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); if (!args[0]) { snprintf(args, sizeof(args), "APPS%cKPUNCH%cPROGMAN%cDVXHELP.HLP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); } shellLoadAppWithArgs(ac, viewerPath, args); } // ============================================================ // titleChangeHandler -- refresh listeners when a window title changes // ============================================================ static void titleChangeHandler(void *ctx) { (void)ctx; shellDesktopUpdate(); } // ============================================================ // idleYield -- called when no dirty rects need compositing // ============================================================ // Registered as sCtx.idleCallback. dvxUpdate calls this when it has // processed all pending events and there are no dirty rects to composite. // Instead of busy-spinning, we yield to app tasks -- this is where most // of the CPU time for main-loop apps comes from when the UI is idle. // The tsActiveCount > 1 check avoids the overhead of a tsYield call // (which would do a scheduler scan) when the shell is the only task. static void idleYield(void *ctx) { (void)ctx; if (tsActiveCount() > 1) { tsYield(); } } static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) { (void)userData; dvxLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp); } // ============================================================ // ============================================================ // shellRegisterDesktopUpdate // ============================================================ void shellRegisterDesktopUpdate(void (*updateFn)(void)) { arrput(sDesktopUpdateFns, updateFn); } // ============================================================ // shellUnregisterDesktopUpdate // ============================================================ void shellUnregisterDesktopUpdate(void (*updateFn)(void)) { for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { if (sDesktopUpdateFns[i] == updateFn) { arrdel(sDesktopUpdateFns, i); return; } } } // ============================================================ // shellMain -- entry point called by the DVX loader // ============================================================ int shellMain(int argc, char *argv[]) { (void)argc; (void)argv; dvxLog("DVX Shell starting..."); // Load preferences (missing file or keys silently use defaults) sPrefs = prefsLoad("CONFIG/DVX.INI"); // Initialize task system (no GUI needed) if (tsInit() != TS_OK) { dvxLog("Failed to initialize task system"); dvxShutdown(&sCtx); return 1; } // Shell task (task 0) gets high priority so the UI remains responsive // even when app tasks are CPU-hungry. With HIGH priority (11 credits // per epoch) vs app tasks at NORMAL (6 credits), the shell gets // roughly twice as many scheduling turns as any single app. tsSetPriority(0, TS_PRIORITY_HIGH); // Gather system information (CPU, memory, drives, etc.) shellInfoInit(&sCtx); // Initialize app slot table shellAppInit(); // Point the memory tracker at currentAppId so allocations are // attributed to whichever app is currently executing. dvxMemAppIdPtr = &sCtx.currentAppId; // Set up idle callback for cooperative yielding. When dvxUpdate has // no work to do (no input events, no dirty rects), it calls this // instead of busy-looping. This is the main mechanism for giving // app tasks CPU time during quiet periods. // Install crash handler before loading apps so faults during app // initialization are caught and recovered from gracefully. platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog); // Initialize GUI — switch from VGA splash to VESA mode as late as possible // so the VGA loading splash stays visible through all the init above. { int32_t videoW = prefsGetInt(sPrefs, "video", "width", 640); int32_t videoH = prefsGetInt(sPrefs, "video", "height", 480); int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp", 16); dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp); if (result != 0) { dvxLog("Failed to initialize DVX GUI (error %ld)", (long)result); tsShutdown(); return 1; } // Set callbacks AFTER dvxInit (which memsets the context to zero) sCtx.idleCallback = idleYield; sCtx.idleCtx = &sCtx; sCtx.onCtrlEsc = ctrlEscHandler; sCtx.ctrlEscCtx = &sCtx; sCtx.onF1 = f1HelpHandler; sCtx.f1Ctx = &sCtx; sCtx.onTitleChange = titleChangeHandler; sCtx.titleChangeCtx = &sCtx; dvxLog("Available video modes:"); platformVideoEnumModes(logVideoMode, NULL); dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); // Apply mouse preferences const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", "normal"); int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1; int32_t dblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", 500); const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium"); int32_t accelVal = 0; if (strcmp(accelStr, "off") == 0) { accelVal = 10000; } else if (strcmp(accelStr, "low") == 0) { accelVal = 100; } else if (strcmp(accelStr, "medium") == 0) { accelVal = 64; } else if (strcmp(accelStr, "high") == 0) { accelVal = 32; } dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal); dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); // Apply saved color scheme bool colorsLoaded = false; for (int32_t i = 0; i < ColorCountE; i++) { const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL); if (val) { int r, g, b; if (sscanf(val, "%d,%d,%d", &r, &g, &b) == 3) { sCtx.colorRgb[i][0] = (uint8_t)r; sCtx.colorRgb[i][1] = (uint8_t)g; sCtx.colorRgb[i][2] = (uint8_t)b; colorsLoaded = true; } } } if (colorsLoaded) { dvxApplyColorScheme(&sCtx); dvxLog("Preferences: loaded custom color scheme"); } // Apply saved wallpaper const char *wpMode = prefsGetString(sPrefs, "desktop", "mode", "stretch"); if (strcmp(wpMode, "tile") == 0) { sCtx.wallpaperMode = WallpaperTileE; } else if (strcmp(wpMode, "center") == 0) { sCtx.wallpaperMode = WallpaperCenterE; } else { sCtx.wallpaperMode = WallpaperStretchE; } const char *wpPath = prefsGetString(sPrefs, "desktop", "wallpaper", NULL); if (wpPath) { if (dvxSetWallpaper(&sCtx, wpPath)) { dvxLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode); } else { dvxLog("Preferences: failed to load wallpaper %s", wpPath); } } } // Load the desktop app (configurable via [shell] desktop= in dvx.ini) const char *desktopApp = prefsGetString(sPrefs, "shell", "desktop", SHELL_DESKTOP_APP); int32_t desktopId = shellLoadApp(&sCtx, desktopApp); if (desktopId < 0) { dvxLog("Failed to load desktop app '%s'", desktopApp); tsShutdown(); dvxShutdown(&sCtx); return 1; } dvxLog("DVX Shell ready."); // Set recovery point for crash handler. setjmp returns 0 on initial // call (falls through to the main loop). On a crash, longjmp makes // setjmp return non-zero, entering this recovery block. The recovery // code runs on the main task's stack (restored by longjmp) so it's // safe to call any shell function. if (setjmp(sCrashJmp) != 0) { // Returned here from crash handler via longjmp. // The task switcher's currentIdx still points to the crashed task. // Fix it before doing anything else so the scheduler is consistent. tsRecoverToMain(); // Platform handler already logged signal name and register dump. // Log app-specific info here. dvxLog("Current app ID: %ld", (long)sCtx.currentAppId); if (sCtx.currentAppId > 0) { ShellAppT *crashedApp = shellGetApp(sCtx.currentAppId); if (crashedApp) { dvxLog("App name: %s", crashedApp->name); dvxLog("App path: %s", crashedApp->path); dvxLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no"); dvxLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId); } } else { dvxLog("Crashed in shell (task 0)"); } dvxLog("Recovering from crash, killing app %ld", (long)sCtx.currentAppId); // Clear busy cursor so the fault dialog is interactive dvxSetBusy(&sCtx, false); if (sCtx.currentAppId > 0) { ShellAppT *app = shellGetApp(sCtx.currentAppId); if (app) { char msg[256]; snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name); shellForceKillApp(&sCtx, app); sCtx.currentAppId = 0; dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR); } } sCtx.currentAppId = 0; sCrashSignal = 0; shellDesktopUpdate(); } // Main loop -- runs until dvxQuit() sets sCtx.running = false. // Two yield points per iteration: one explicit (below) and one via // the idle callback inside dvxUpdate. The explicit yield here ensures // app tasks get CPU time even during busy frames (lots of repaints). // Without it, a flurry of mouse-move events could starve app tasks // because dvxUpdate would keep finding work to do and never call idle. while (sCtx.running) { dvxUpdate(&sCtx); // Give app tasks CPU time even during active frames if (tsActiveCount() > 1) { tsYield(); } // Reap terminated apps and notify desktop if anything changed. // This is the safe point for cleanup -- we're at the top of the // main loop, not inside any callback or compositor operation. if (shellReapApps(&sCtx)) { shellDesktopUpdate(); } } dvxLog("DVX Shell shutting down..."); // Clean shutdown: terminate all apps first (destroys windows, kills // tasks, closes DXE handles), then tear down the task system and GUI // in reverse initialization order. shellTerminateAllApps(&sCtx); tsShutdown(); dvxShutdown(&sCtx); prefsClose(sPrefs); sPrefs = NULL; dvxLog("DVX Shell exited."); return 0; }