DVX_GUI/tasks/README.md
2026-03-20 20:00:05 -05:00

357 lines
15 KiB
Markdown

# taskswitch -- Cooperative Task Switching Library
Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS
protected mode). Part of the DVX GUI project.
Tasks voluntarily yield the CPU by calling `tsYield()`. A credit-based
weighted round-robin scheduler gives higher-priority tasks proportionally
more CPU time while guaranteeing that low-priority tasks are never
starved. A priority-10 task gets 11 turns per scheduling round; a
priority-0 task gets 1 -- but it always runs eventually.
The task array is backed by stb_ds and grows dynamically. Terminated
task slots are recycled, so there is no fixed upper limit on the number
of tasks created over the lifetime of the application.
## Why Cooperative?
DOS is single-threaded. DPMI provides no timer-based preemption. The
DVX GUI event model is inherently single-threaded: one compositor, one
input queue, one window stack. Preemptive switching would require
locking around every GUI call for no benefit. Cooperative switching
lets each task yield at safe points, avoiding synchronization entirely.
## Files
| File | Description |
|-----------------------|----------------------------------------------------|
| `taskswitch.h` | Public API -- types, constants, function prototypes |
| `taskswitch.c` | Implementation (scheduler, context switch, slots) |
| `demo.c` | Standalone test harness exercising all features |
| `thirdparty/stb_ds.h` | stb dynamic array/hashmap library (third-party) |
| `Makefile` | DJGPP cross-compilation build rules |
## Building
Cross-compile from Linux:
```
make # builds ../lib/libtasks.a
make demo # also builds ../bin/tsdemo.exe
make clean # removes objects, library, and demo binary
```
Output:
| Path | Description |
|---------------------|----------------------|
| `../lib/libtasks.a` | Static library |
| `../obj/tasks/` | Object files |
| `../bin/tsdemo.exe` | Demo executable |
## Quick Start
```c
#include <stdio.h>
#include "taskswitch.h"
void myTask(void *arg) {
const char *name = (const char *)arg;
for (int i = 0; i < 3; i++) {
printf("[%s] working...\n", name);
tsYield();
}
}
int main(void) {
tsInit();
tsCreate("alpha", myTask, "alpha", 0, TS_PRIORITY_NORMAL);
tsCreate("beta", myTask, "beta", 0, TS_PRIORITY_HIGH);
while (tsActiveCount() > 1) {
tsYield();
}
tsShutdown();
return 0;
}
```
## Lifecycle
1. `tsInit()` -- Initialize the task system. The calling context
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. No
separate stack is allocated for task 0 -- it uses the process stack.
2. `tsCreate(...)` -- Create tasks. Each gets a name, entry function,
argument pointer, stack size (0 for the default), and a priority.
Returns the task ID (>= 0) or a negative error code. Terminated
task slots are reused automatically.
3. `tsYield()` -- Call from any task (including main) to hand the CPU
to the next eligible task. This is the sole mechanism for task
switching.
4. `tsShutdown()` -- Free all task stacks and the task array.
Tasks terminate by returning from their entry function or by calling
`tsExit()`. The main task (id 0) must never call `tsExit()`. When a
task terminates, its stack is freed immediately and its slot becomes
available for reuse by the next `tsCreate()` call.
## API Reference
### Initialization and Teardown
| Function | Signature | Description |
|--------------|-------------------------|--------------------------------------------------------------------|
| `tsInit` | `int32_t tsInit(void)` | Initialize the library. Returns `TS_OK` or a negative error code. |
| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. |
### Task Creation and Termination
| Function | Signature | Description |
|------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE`. Reuses terminated slots. |
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. Never returns. |
| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Cannot kill main (id 0) or self (use `tsExit` instead). |
### Scheduling
| Function | Signature | Description |
|-----------|----------------------|-----------------------------------------------------------------|
| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. |
### Pausing and Resuming
| Function | Signature | Description |
|------------|---------------------------------|------------------------------------------------------------------------------------------------------------|
| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. Main task (id 0) cannot be paused. Self-pause triggers an implicit yield. |
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Credits are refilled so it is not penalized for having been paused. |
### Priority
| Function | Signature | Description |
|-----------------|---------------------------------------------------|-----------------------------------------------------------------------------------|
| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)` | Change a task's priority. Credits are reset so the change takes effect immediately. |
| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. |
### Crash Recovery
| Function | Signature | Description |
|-------------------|------------------------------|------------------------------------------------------------------------------------------------------------------|
| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to task 0 after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. The crashed task's slot is NOT freed -- call `tsKill` afterward. |
### Query
| Function | Signature | Description |
|-----------------|--------------------------------------|--------------------------------------------------------|
| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. |
| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. |
| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. |
| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. |
## Constants
### Error Codes
| Name | Value | Meaning |
|----------------|-------|--------------------------------------------------|
| `TS_OK` | 0 | Success |
| `TS_ERR_INIT` | -1 | Library not initialized |
| `TS_ERR_PARAM` | -2 | Invalid parameter |
| `TS_ERR_FULL` | -3 | Task table full (unused, kept for compatibility) |
| `TS_ERR_NOMEM` | -4 | Memory allocation failed |
| `TS_ERR_STATE` | -5 | Invalid state transition |
### Priority Presets
| Name | Value | Credits per Round |
|----------------------|-------|-------------------|
| `TS_PRIORITY_LOW` | 0 | 1 |
| `TS_PRIORITY_NORMAL` | 5 | 6 |
| `TS_PRIORITY_HIGH` | 10 | 11 |
Any non-negative `int32_t` may be used as a priority. The presets are
provided for convenience. In the DVX Shell, the main task runs at
`TS_PRIORITY_HIGH` to keep the UI responsive; app tasks default to
`TS_PRIORITY_NORMAL`.
### Defaults
| Name | Value | Description |
|-------------------------|-------|------------------------|
| `TS_DEFAULT_STACK_SIZE` | 32768 | Default stack per task |
| `TS_NAME_MAX` | 32 | Max task name length |
## Types
### TaskStateE
```c
typedef enum {
TaskStateReady = 0, // Eligible for scheduling
TaskStateRunning = 1, // Currently executing
TaskStatePaused = 2, // Suspended until tsResume()
TaskStateTerminated = 3 // Finished; slot will be recycled
} TaskStateE;
```
Only Ready tasks participate in scheduling. Running is cosmetic (marks
the currently executing task). Paused tasks are skipped until explicitly
resumed. Terminated slots are recycled by `tsCreate`.
### TaskEntryT
```c
typedef void (*TaskEntryT)(void *arg);
```
The signature every task entry function must follow. The `arg` parameter
is the pointer passed to `tsCreate`.
## Scheduler Details
The scheduler is a credit-based weighted round-robin, a variant of the
Linux 2.4 goodness() scheduler.
1. Every ready task holds a credit counter initialized to `priority + 1`.
2. When `tsYield()` is called, the scheduler scans tasks starting one
past the current task (wrapping around) looking for a ready task with
credits > 0. When found, that task's credits are decremented and it
becomes the running task.
3. When no ready task has credits remaining, every ready task is
refilled to `priority + 1` (one "epoch") and the scan repeats.
This means a priority-10 task receives 11 turns for every 1 turn a
priority-0 task receives, but the low-priority task still runs -- it
is never starved.
Credits are also refilled when:
- A task is created (`tsCreate`) -- starts with `priority + 1`.
- A task is resumed (`tsResume`) -- refilled so it runs promptly.
- A task's priority changes (`tsSetPriority`) -- reset to `new + 1`.
## Task Slot Management
The task array is a stb_ds dynamic array that grows automatically.
Each slot has an `allocated` flag:
- `tsCreate()` scans for the first unallocated slot (starting at index
1, since slot 0 is always the main task). If no free slot exists, the
array is extended with `arrput()`.
- `tsExit()` and `tsKill()` free the terminated task's stack immediately
and mark the slot as unallocated, making it available for the next
`tsCreate()` call.
- Task IDs are stable array indices. Slots are never removed or
reordered, so a task ID remains valid for queries until the slot is
recycled.
This supports long-running applications (like the DVX Shell) that
create and destroy many tasks over their lifetime without unbounded
memory growth.
## Context Switch Internals
Context switching uses inline assembly with both i386 and x86_64 code
paths. The `contextSwitch` function is marked `noinline` to preserve
callee-saved register assumptions.
Why inline asm instead of setjmp/longjmp: setjmp/longjmp only save
callee-saved registers and do not give control over the stack pointer
in a portable way. New tasks need a fresh stack with the instruction
pointer set to a trampoline -- setjmp cannot bootstrap that. The asm
approach also avoids ABI differences in jmp_buf layout across DJGPP
versions.
### i386 (DJGPP target)
Six callee-saved values are saved and restored per switch:
| Register | Offset | Purpose |
|----------|--------|------------------------------------------|
| EBX | 0 | Callee-saved general purpose |
| ESI | 4 | Callee-saved general purpose |
| EDI | 8 | Callee-saved general purpose |
| EBP | 12 | Frame pointer |
| ESP | 16 | Stack pointer |
| EIP | 20 | Resume address (captured as local label) |
### x86_64 (for native Linux testing)
Eight callee-saved values are saved and restored per switch:
| Register | Offset | Purpose |
|----------|--------|------------------------------------------|
| RBX | 0 | Callee-saved general purpose |
| R12 | 8 | Callee-saved general purpose |
| R13 | 16 | Callee-saved general purpose |
| R14 | 24 | Callee-saved general purpose |
| R15 | 32 | Callee-saved general purpose |
| RBP | 40 | Frame pointer |
| RSP | 48 | Stack pointer |
| RIP | 56 | Resume address (RIP-relative lea) |
Segment registers are not saved because DJGPP runs in a flat
protected-mode environment where CS, DS, ES, and SS share the same
base.
New tasks have their initial stack pointer set to a 16-byte-aligned
region at the top of a malloc'd stack, with the instruction pointer
set to an internal trampoline that calls the user's entry function
and then `tsExit()`.
## Limitations
- **Cooperative only** -- tasks must call `tsYield()` (or
`tsPause`/`tsExit`) to allow other tasks to run. A task that never
yields blocks everything.
- **Not interrupt-safe** -- no locking or volatile module state. Do not
call library functions from interrupt handlers.
- **Single-threaded** -- designed for one CPU under DOS protected mode.
- **Stack overflow is not detected** -- size the stack appropriately for
each task's needs.
## Demo
`demo.c` exercises five phases:
1. **Priority scheduling** -- creates tasks at low, normal, and high
priority. All tasks run, but the high-priority task gets significantly
more turns.
2. **Pause** -- pauses one task mid-run and shows it stops being
scheduled.
3. **Resume** -- resumes the paused task and shows it picks up where it
left off.
4. **Priority boost** -- raises the low-priority task above all others
and shows it immediately gets more turns.
5. **Slot reuse** -- creates three waves of short-lived tasks that
terminate and shows subsequent waves reuse the same task IDs.
Build and run:
```
make demo
tsdemo
```
## Third-Party Dependencies
- **stb_ds.h** (Sean Barrett) -- dynamic array and hashmap library.
Located in `thirdparty/stb_ds.h`. Used for the task control block
array. Public domain / MIT licensed.