// 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 shell installs signal handlers for SIGSEGV, SIGFPE, SIGILL. If a // crash occurs in an app task, the handler 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, 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 "shellInfo.h" #include "dvxDialog.h" #include "dvxPrefs.h" #include "platform/dvxPlatform.h" #include #include #include #include #include // DJGPP-specific: provides __djgpp_exception_state_ptr for accessing // CPU register state at the point of the exception #include // ============================================================ // Module state // ============================================================ static AppContextT sCtx; // 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; static FILE *sLogFile = NULL; static void (*sDesktopUpdateFn)(void) = NULL; // ============================================================ // Prototypes // ============================================================ static void crashHandler(int sig); static void idleYield(void *ctx); static void installCrashHandler(void); static void logCrash(int sig); static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData); // ============================================================ // crashHandler — catch page faults and other fatal signals // ============================================================ // Signal handler for fatal exceptions. DJGPP uses System V signal // semantics where the handler is reset to SIG_DFL after each delivery, // so we must re-install it before doing anything else. // // The longjmp is the key to crash recovery: it unwinds whatever stack // we're on (potentially a crashed app's task stack) and restores the // main task's stack frame to the setjmp point in main(). This is safe // because cooperative switching means the main task's stack is always // intact — it was cleanly suspended at a yield point. The crashed // task's stack is abandoned (and later freed by tsKill). static void crashHandler(int sig) { logCrash(sig); // Re-install handler (DJGPP resets to SIG_DFL after delivery) signal(sig, crashHandler); sCrashSignal = sig; longjmp(sCrashJmp, 1); } // ============================================================ // shellDesktopUpdate — notify desktop app of state change // ============================================================ void shellDesktopUpdate(void) { if (sDesktopUpdateFn) { sDesktopUpdateFn(); } } // ============================================================ // 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(); } } // ============================================================ // installCrashHandler // ============================================================ static void installCrashHandler(void) { signal(SIGSEGV, crashHandler); signal(SIGFPE, crashHandler); signal(SIGILL, crashHandler); } // ============================================================ // logCrash — write exception details to the log // ============================================================ // Dump as much diagnostic info as possible before longjmp destroys the // crash context. This runs inside the signal handler, so only // async-signal-safe functions should be used — but since we're in // DJGPP (single-threaded DOS), reentrancy isn't a practical concern // and vfprintf/fflush are safe to call here. static void logCrash(int sig) { const char *sigName = "UNKNOWN"; if (sig == SIGSEGV) { sigName = "SIGSEGV (page fault)"; } else if (sig == SIGFPE) { sigName = "SIGFPE (floating point exception)"; } else if (sig == SIGILL) { sigName = "SIGILL (illegal instruction)"; } shellLog("=== CRASH ==="); shellLog("Signal: %d (%s)", sig, sigName); shellLog("Current app ID: %ld", (long)sCurrentAppId); if (sCurrentAppId > 0) { ShellAppT *app = shellGetApp(sCurrentAppId); if (app) { shellLog("App name: %s", app->name); shellLog("App path: %s", app->path); shellLog("Has main loop: %s", app->hasMainLoop ? "yes" : "no"); shellLog("Task ID: %lu", (unsigned long)app->mainTaskId); } } else { shellLog("Crashed in shell (task 0)"); } // __djgpp_exception_state_ptr is a DJGPP extension that captures the // full CPU register state at the point of the exception. This gives // us the faulting EIP, stack pointer, and all GPRs — invaluable for // post-mortem debugging of app crashes from the log file. jmp_buf *estate = __djgpp_exception_state_ptr; if (estate) { struct __jmp_buf *regs = &(*estate)[0]; shellLog("EIP: 0x%08lx CS: 0x%04x", regs->__eip, regs->__cs); shellLog("EAX: 0x%08lx EBX: 0x%08lx ECX: 0x%08lx EDX: 0x%08lx", regs->__eax, regs->__ebx, regs->__ecx, regs->__edx); shellLog("ESI: 0x%08lx EDI: 0x%08lx EBP: 0x%08lx ESP: 0x%08lx", regs->__esi, regs->__edi, regs->__ebp, regs->__esp); shellLog("DS: 0x%04x ES: 0x%04x FS: 0x%04x GS: 0x%04x SS: 0x%04x", regs->__ds, regs->__es, regs->__fs, regs->__gs, regs->__ss); shellLog("EFLAGS: 0x%08lx", regs->__eflags); } } static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) { (void)userData; shellLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp); } // ============================================================ // shellLog — append a line to DVX.LOG // ============================================================ void shellLog(const char *fmt, ...) { if (!sLogFile) { return; } va_list ap; va_start(ap, fmt); vfprintf(sLogFile, fmt, ap); va_end(ap); fprintf(sLogFile, "\n"); fflush(sLogFile); } // ============================================================ // shellRegisterDesktopUpdate // ============================================================ void shellRegisterDesktopUpdate(void (*updateFn)(void)) { sDesktopUpdateFn = updateFn; } // ============================================================ // main // ============================================================ int main(int argc, char *argv[]) { (void)argc; // Change to the directory containing the executable so that relative // paths (CONFIG/, APPS/, etc.) resolve correctly regardless of where // the user launched from. char exeDir[260]; strncpy(exeDir, argv[0], sizeof(exeDir) - 1); exeDir[sizeof(exeDir) - 1] = '\0'; char *sep = platformPathDirEnd(exeDir); if (sep) { *sep = '\0'; platformChdir(exeDir); } sLogFile = fopen("dvx.log", "w"); shellLog("DVX Shell starting..."); // Load preferences (missing file or keys silently use defaults) prefsLoad("CONFIG/DVX.INI"); int32_t videoW = prefsGetInt("video", "width", 640); int32_t videoH = prefsGetInt("video", "height", 480); int32_t videoBpp = prefsGetInt("video", "bpp", 32); shellLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); // Initialize GUI int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp); if (result == 0) { // Apply mouse preferences const char *wheelStr = prefsGetString("mouse", "wheel", "normal"); int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1; int32_t dblClick = prefsGetInt("mouse", "doubleclick", 500); // Map acceleration name to double-speed threshold (mickeys/sec). // "off" sets a very high threshold so acceleration never triggers. const char *accelStr = prefsGetString("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); shellLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); } if (result != 0) { shellLog("Failed to initialize DVX GUI (error %ld)", (long)result); if (sLogFile) { fclose(sLogFile); } return 1; } shellLog("Available video modes:"); platformVideoEnumModes(logVideoMode, NULL); shellLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); // Initialize task system if (tsInit() != TS_OK) { shellLog("Failed to initialize task system"); dvxShutdown(&sCtx); if (sLogFile) { fclose(sLogFile); } 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); // Register DXE export table shellExportInit(); // Initialize app slot table shellAppInit(); // 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. sCtx.idleCallback = idleYield; sCtx.idleCtx = &sCtx; // Load the desktop app int32_t desktopId = shellLoadApp(&sCtx, SHELL_DESKTOP_APP); if (desktopId < 0) { shellLog("Failed to load desktop app '%s'", SHELL_DESKTOP_APP); tsShutdown(); dvxShutdown(&sCtx); if (sLogFile) { fclose(sLogFile); } return 1; } // Install crash handler after everything is initialized — if // initialization itself crashes, we want the default DJGPP behavior // (abort with register dump) rather than our recovery path, because // the system isn't in a recoverable state yet. installCrashHandler(); shellLog("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(); shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId); if (sCurrentAppId > 0) { ShellAppT *app = shellGetApp(sCurrentAppId); if (app) { char msg[256]; snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name); shellForceKillApp(&sCtx, app); sCurrentAppId = 0; dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR); } } sCurrentAppId = 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(); } } shellLog("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); prefsFree(); shellLog("DVX Shell exited."); if (sLogFile) { fclose(sLogFile); sLogFile = NULL; } return 0; }