diff --git a/08_graphics/README.md b/08_graphics/README.md
index b03b879..16911c0 100644
--- a/08_graphics/README.md
+++ b/08_graphics/README.md
@@ -2,20 +2,22 @@
# Graphics Subsystem — Overview
+The Amiga graphics system is built on custom DMA-driven hardware (Agnus/Alice + Denise/Lisa) managed through `graphics.library`. It supports planar bitmaps, hardware sprites, a Copper display coprocessor, and a Blitter for fast 2D operations. Three chipset generations (OCS → ECS → AGA) expanded resolution, colour depth, and bandwidth.
+
## Section Index
| File | Description |
|---|---|
-| [gfx_base.md](gfx_base.md) | GfxBase structure and global graphics state |
+| [gfx_base.md](gfx_base.md) | GfxBase structure, chipset detection (OCS/ECS/AGA), PAL/NTSC, display pipeline (MakeVPort/MrgCop/LoadView), blitter queue |
| [bitmap.md](bitmap.md) | BitMap structure, planar layout, allocation |
+| [display_modes.md](display_modes.md) | Chipset comparison (OCS/ECS/AGA), ModeID system, PAL/NTSC timing, DMA slot budget |
+| [ham_ehb_modes.md](ham_ehb_modes.md) | HAM6/HAM8 encoding pipeline, EHB half-brite, fringing, palette programming, FPGA decoder logic |
| [copper.md](copper.md) | Copper coprocessor, instruction format, UCopList |
+| [copper_programming.md](copper_programming.md) | Copper deep dive: architecture, copper list construction, gradient and raster effects |
| [blitter.md](blitter.md) | Blitter DMA engine, minterms, BltBitMap |
-| [sprites.md](sprites.md) | Hardware sprites, SimpleSprite, MoveSprite |
-| [rastport.md](rastport.md) | RastPort, drawing primitives, layers |
+| [blitter_programming.md](blitter_programming.md) | Blitter deep dive: minterms, cookie-cut masking, line draw, fill mode |
+| [sprites.md](sprites.md) | Hardware sprites: DMA engine, data format, attached 15-colour sprites, multiplexing, AGA enhancements, priority control |
+| [rastport.md](rastport.md) | RastPort drawing context: draw modes, patterns, layer clipping, text pipeline, blitter minterms |
| [views.md](views.md) | View, ViewPort, MakeVPort, display construction |
-| [text_fonts.md](text_fonts.md) | TextFont, TextAttr, OpenFont, Text rendering |
-| [display_modes.md](display_modes.md) | Display database, ModeID, monitor specs |
-| [ham_ehb_modes.md](ham_ehb_modes.md) | HAM6, HAM8, and EHB special display modes |
+| [text_fonts.md](text_fonts.md) | TextFont bitmap layout, baseline rendering, algorithmic styles, AvailFonts enumeration |
| [animation.md](animation.md) | AnimOb, BOB, VSprite, GEL system |
-| [copper_programming.md](copper_programming.md) | Copper deep dive: architecture, examples, system API |
-| [blitter_programming.md](blitter_programming.md) | Blitter deep dive: minterms, cookie-cut, line draw |
diff --git a/08_graphics/display_modes.md b/08_graphics/display_modes.md
index 026e03f..2d79369 100644
--- a/08_graphics/display_modes.md
+++ b/08_graphics/display_modes.md
@@ -1,74 +1,217 @@
[← Home](../README.md) · [Graphics](README.md)
-# Display Modes — Display Database, ModeID, Monitor Specs
+# Display Modes — Chipset Generations, ModeID, and Timing
## Overview
-OS 3.0+ provides a **display database** that abstracts monitor/chipset capabilities. Applications query available modes by `ModeID` rather than hardcoding `HIRES`/`LACE` flags.
+The Amiga's display system evolved through three generations of custom chips: **OCS** (Original Chip Set, A1000/A500/A2000), **ECS** (Enhanced, A3000/A600), and **AGA** (Advanced Graphics Architecture, A1200/A4000). Each generation expanded resolution, colour depth, and display flexibility while maintaining backward compatibility.
+
+OS 3.0+ provides a **display database** that abstracts these capabilities. Applications query available modes by `ModeID` rather than hardcoding chipset-specific flags.
+
+---
+
+## Chipset Comparison
+
+| Feature | OCS (Agnus/Denise) | ECS (Fat Agnus/Super Denise) | AGA (Alice/Lisa) |
+|---|---|---|---|
+| **Max Chip RAM** | 512 KB (8372) / 1 MB (8372A) | 2 MB (8375) | 2 MB (8374) |
+| **Bitplanes** | 6 (32 colours, lowres) | 6 | 8 (256 colours) |
+| **Palette entries** | 32 (4096 total, 12-bit RGB) | 32 (4096) | 256 (16.7M, 24-bit RGB) |
+| **Max lowres** | 320×256 (PAL) | 320×256 | 320×256 |
+| **Max hires** | 640×256 | 640×256 | 640×256 |
+| **Super hires** | — | 1280×256 | 1280×256 |
+| **Scan-doubled** | — | — | 640×512 non-interlaced |
+| **HAM** | HAM6 (4096 colours) | HAM6 | HAM8 (262,144 colours) |
+| **EHB** | EHB (64 colours) | EHB | EHB (superseded by 8 planes) |
+| **Sprites** | 8 × 16px × 3 colours | 8 × 16px × 3 colours | 8 × 16/32/64px × 3/15 colours |
+| **Fetch modes** | 1× | 1× | 1×, 2×, 4× (wider data bus) |
+| **Bandwidth** | 3.58 MHz pixel clock | 3.58/7.16/14.32 MHz | Up to 28.64 MHz (4× fetch) |
+
+---
+
+## Display Timing Fundamentals
+
+All Amiga display modes are based on PAL or NTSC television timing:
+
+### PAL (Europe, Australia)
+
+```
+Line frequency: 15,625 Hz
+Frame frequency: 50 Hz (25 Hz interlaced)
+Lines per frame: 312.5 (625 interlaced)
+Active lines: ~256 (non-interlaced) / ~512 (interlaced)
+Colour clock: 3,546,895 Hz
+Pixel clock (lores): 7,093,790 Hz (1 pixel = 2 colour clocks)
+Pixel clock (hires): 14,187,580 Hz
+```
+
+### NTSC (Americas, Japan)
+
+```
+Line frequency: 15,734 Hz
+Frame frequency: 60 Hz (30 Hz interlaced)
+Lines per frame: 262.5 (525 interlaced)
+Active lines: ~200 (non-interlaced) / ~400 (interlaced)
+Colour clock: 3,579,545 Hz
+Pixel clock (lores): 7,159,090 Hz
+Pixel clock (hires): 14,318,180 Hz
+```
+
+### Display Cycle Anatomy
+
+```
+ ←── Horizontal line (~64 µs PAL) ──→
+┌───────────────────────────────────────────────────────────┐
+│ HSYNC │ Left │ Active Display Area │ Right │
+│ │Border │ (bitplane DMA + sprite DMA) │Border │
+│ ~4.7µs│ │ │ │
+└───────────────────────────────────────────────────────────┘
+
+Vertical:
+ ┌── VSYNC (2.5 lines) ──┐
+ │ Top Border │
+ │ Active Display │ ← 256 lines (PAL) / 200 (NTSC)
+ │ Bottom Border │
+ └────────────────────────┘
+```
+
+> **FPGA implication**: the MiSTer core must replicate these exact timings for correct DMA slot allocation. Many programs (especially demos) count DMA cycles and will break if timing is even slightly off.
---
## ModeID Format
-A ModeID is a ULONG:
+A ModeID is a 32-bit value encoding the monitor driver and mode:
+
```
-bits 31–16: Monitor ID
-bits 15–0: Mode within that monitor
+┌──────────────────────────────────────────────┐
+│ 31 16 │ 15 0 │
+│ Monitor ID │ Mode within monitor │
+└──────────────────────────────────────────────┘
```
-| ModeID | Name | Resolution |
+### Standard Mode IDs
+
+| ModeID | Name | Resolution | Depth | Chipset |
+|---|---|---|---|---|
+| `$00000000` | PAL:LORES | 320×256 | 5 | OCS+ |
+| `$00008000` | PAL:HIRES | 640×256 | 4 | OCS+ |
+| `$00000004` | PAL:LORES-LACE | 320×512 | 5 | OCS+ |
+| `$00008004` | PAL:HIRES-LACE | 640×512 | 4 | OCS+ |
+| `$00080000` | PAL:SUPERHIRES | 1280×256 | 2 | ECS+ |
+| `$00080004` | PAL:SUPERHIRES-LACE | 1280×512 | 2 | ECS+ |
+| `$00039000` | DBLPAL:LORES | 320×256 | 8 | AGA |
+| `$00039004` | DBLPAL:HIRES | 640×256 | 8 | AGA |
+| `$00039024` | DBLPAL:HIRES-LACE | 640×512 | 8 | AGA |
+| `$00011000` | DBLNTSC:LORES | 320×200 | 8 | AGA |
+| `$00011004` | DBLNTSC:HIRES | 640×200 | 8 | AGA |
+| `$00000800` | HAM | 320×256 HAM6 | 6 | OCS+ |
+| `$00000080` | EHB | 320×256 EHB | 6 | OCS+ |
+
+### Mode Flags (bits within ModeID)
+
+| Bit | Mask | Meaning |
|---|---|---|
-| `$00000000` | LORES | 320×256 (PAL) / 320×200 (NTSC) |
-| `$00008000` | HIRES | 640×256/200 |
-| `$00000004` | LORES-LACE | 320×512/400 |
-| `$00008004` | HIRES-LACE | 640×512/400 |
-| `$00080000` | SUPERHIRES | 1280×256 (ECS+) |
-| `$00080004` | SUPERHIRES-LACE | 1280×512 |
-| `$00039000` | DBLPAL-LORES | 320×256 (AGA scan-doubled) |
-| `$00039004` | DBLPAL-HIRES | 640×256 (AGA) |
-| `$00011000` | DBLNTSC-LORES | 320×200 (AGA) |
+| 2 | `$0004` | LACE — interlaced (double vertical resolution) |
+| 11 | `$0800` | HAM — Hold-And-Modify mode |
+| 7 | `$0080` | EHB — Extra Half-Brite mode |
+| 15 | `$8000` | HIRES — double horizontal resolution |
+| 19 | `$80000` | SUPERHIRES — quadruple horizontal resolution (ECS+) |
+
+---
+
+## AGA Fetch Modes
+
+AGA introduced wider data fetch widths, reducing DMA overhead:
+
+| Fetch Mode | Bits per Fetch | FMODE Value | Effect |
+|---|---|---|---|
+| 1× | 16 bits | 0 | OCS compatible — 1 word per slot |
+| 2× | 32 bits | 1 | 2 words per slot — more bandwidth for deeper modes |
+| 4× | 64 bits | 3 | 4 words per slot — required for 8-plane hires |
+
+```c
+/* Set AGA fetch mode (custom register): */
+custom->fmode = 3; /* 4× fetch — maximum bandwidth */
+```
+
+> [!WARNING]
+> 4× fetch mode causes 64-pixel horizontal alignment constraints. Sprites also widen to 32 or 64 pixels in 2×/4× mode. Many OCS-era programs break if FMODE ≠ 0.
---
## Querying the Display Database
```c
-/* graphics.library 39+ */
+/* graphics.library 39+ — enumerate all available modes: */
ULONG modeID = INVALID_ID;
struct DisplayInfo di;
struct DimensionInfo dims;
+struct MonitorInfo mon;
-while ((modeID = NextDisplayInfo(modeID)) != INVALID_ID) {
+while ((modeID = NextDisplayInfo(modeID)) != INVALID_ID)
+{
if (GetDisplayInfoData(NULL, (UBYTE *)&di, sizeof(di),
- DTAG_DISP, modeID)) {
- if (GetDisplayInfoData(NULL, (UBYTE *)&dims, sizeof(dims),
- DTAG_DIMS, modeID)) {
- Printf("ModeID $%08lx: %ldx%ld, %ld colours\n",
- modeID,
- dims.Nominal.MaxX - dims.Nominal.MinX + 1,
- dims.Nominal.MaxY - dims.Nominal.MinY + 1,
- 1 << di.NotAvailable ? 0 : dims.MaxDepth);
- }
+ DTAG_DISP, modeID))
+ {
+ if (di.NotAvailable) continue; /* skip unavailable modes */
+
+ GetDisplayInfoData(NULL, (UBYTE *)&dims, sizeof(dims),
+ DTAG_DIMS, modeID);
+ GetDisplayInfoData(NULL, (UBYTE *)&mon, sizeof(mon),
+ DTAG_MNTR, modeID);
+
+ Printf("$%08lx: %ldx%ld, %ld colours, %s\n",
+ modeID,
+ dims.Nominal.MaxX - dims.Nominal.MinX + 1,
+ dims.Nominal.MaxY - dims.Nominal.MinY + 1,
+ 1L << dims.MaxDepth,
+ mon.Mspc->ms_Node.xln_Name);
}
}
```
----
-
-## Best Mode Selection
+### Best Mode Selection
```c
-ULONG bestMode = BestModeIDA((struct TagItem[]){
- { BIDTAG_NominalWidth, 640 },
- { BIDTAG_NominalHeight, 480 },
- { BIDTAG_Depth, 8 },
- { TAG_DONE, 0 }
-});
+/* Find the best mode matching desired specs: */
+ULONG bestMode = BestModeID(
+ BIDTAG_NominalWidth, 640,
+ BIDTAG_NominalHeight, 480,
+ BIDTAG_Depth, 8,
+ BIDTAG_MonitorID, PAL_MONITOR_ID,
+ TAG_DONE);
+
+if (bestMode != INVALID_ID)
+ Printf("Best mode: $%08lx\n", bestMode);
```
---
+## DMA Slot Budget
+
+The display system shares DMA bandwidth with other custom chips. Each scanline has a fixed number of DMA slots:
+
+| DMA Consumer | Slots Used |
+|---|---|
+| Disk DMA | 3 words |
+| Audio (4 channels) | 4 words |
+| Sprites (8) | 16 words |
+| Bitplane (lowres, 5 planes) | 40 words |
+| Bitplane (hires, 4 planes) | 80 words |
+| Copper | 1 per instruction pair |
+| Blitter | Variable (steals from CPU) |
+| CPU | Whatever is left |
+
+> In high-resolution 4-plane mode, bitplane DMA alone consumes 80 words per line — nearly the entire available bandwidth. This is why OCS/ECS hires is limited to 4 planes (16 colours) and AGA needed wider fetch modes.
+
+---
+
## References
-- NDK39: `graphics/displayinfo.h`, `graphics/modeid.h`
+- NDK39: `graphics/displayinfo.h`, `graphics/modeid.h`, `graphics/gfxbase.h`
+- HRM: *Amiga Hardware Reference Manual* — Display chapters
- ADCD 2.1: `NextDisplayInfo`, `GetDisplayInfoData`, `BestModeIDA`
+- See also: [views.md](views.md) — ViewPort and View construction
+- See also: [copper.md](copper.md) — Copper display list programming
+- See also: [ham_ehb_modes.md](ham_ehb_modes.md) — special display modes
diff --git a/08_graphics/gfx_base.md b/08_graphics/gfx_base.md
index 6dcd414..2524597 100644
--- a/08_graphics/gfx_base.md
+++ b/08_graphics/gfx_base.md
@@ -4,7 +4,29 @@
## Overview
-`graphics.library` manages all display output, drawing primitives, and custom chip programming. `GfxBase` is the library base containing display state, chip revision info, and the system copper lists.
+`graphics.library` is the core drawing library in AmigaOS. It manages all display output, drawing primitives, font rendering, and custom chip programming. `GfxBase` — the library base — contains the system's display state, chip revision info, system copper lists, and the display mode database.
+
+```mermaid
+flowchart TD
+ subgraph "GfxBase — Global Graphics State"
+ AV["ActiView
(current display)"]
+ COP["copinit
(system copper list)"]
+ CHIP["ChipRevBits0
(OCS/ECS/AGA detection)"]
+ FONTS["TextFonts
(system font list)"]
+ MODES["ModesList
(display mode database)"]
+ VBF["VBlankFrequency
(50=PAL, 60=NTSC)"]
+ end
+
+ APP["Application"] -->|"OpenLibrary"| GFX["graphics.library"]
+ GFX --> AV
+ GFX --> COP
+ GFX --> CHIP
+
+ AV -->|"MakeVPort / MrgCop /
LoadView"| HW["Custom Chips
(Agnus/Denise)"]
+
+ style GFX fill:#e8f4fd,stroke:#2196f3,color:#333
+ style HW fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
---
@@ -15,7 +37,7 @@
struct GfxBase {
struct Library LibNode;
struct View *ActiView; /* currently active View */
- struct copinit *copinit; /* system copper list init */
+ struct copinit *copinit; /* system copper list initialisation */
LONG *cia; /* CIA base (deprecated) */
LONG *blitter; /* blitter base (deprecated) */
UWORD *LOFlist; /* long-frame copper list pointer */
@@ -24,26 +46,26 @@ struct GfxBase {
struct bltnode *blttl; /* blitter queue tail */
struct bltnode *bsblthd;
struct bltnode *bsblttl;
- struct Interrupt vbsrv; /* vertical blank server list */
- struct Interrupt timsrv;
- struct Interrupt bltsrv;
+ struct Interrupt vbsrv; /* vertical blank server chain */
+ struct Interrupt timsrv; /* timer server chain */
+ struct Interrupt bltsrv; /* blitter-done server chain */
struct List TextFonts; /* system font list */
- struct TextFont *DefaultFont; /* default system font */
+ struct TextFont *DefaultFont; /* default system font (topaz.8) */
UWORD Modes; /* current display mode bits */
BYTE VBlankFrequency; /* 50 (PAL) or 60 (NTSC) */
BYTE DisplayFlags; /* PAL/NTSC/GENLOCK flags */
- UWORD NormalDisplayColumns;
- UWORD NormalDisplayRows;
+ UWORD NormalDisplayColumns; /* default display width */
+ UWORD NormalDisplayRows; /* default display height */
UWORD MaxDisplayColumn;
UWORD MaxDisplayRow;
- UWORD ChipRevBits0; /* chip revision flags */
+ UWORD ChipRevBits0; /* chip revision flags — see below */
/* ... many more fields ... */
struct MonitorSpec *monitor_id;
- struct List MonitorList;
+ struct List MonitorList; /* installed monitors */
struct List ModesList; /* display mode database */
UBYTE MemType; /* memory type flags */
/* OS 3.x additions */
- APTR ChunkyToPlanarPtr; /* c2p conversion routine */
+ APTR ChunkyToPlanarPtr; /* c2p conversion routine pointer */
};
```
@@ -51,14 +73,16 @@ struct GfxBase {
## Chip Revision Detection
+The `ChipRevBits0` field identifies which chipset generation is present — essential for FPGA cores that need to report their emulated chipset level:
+
```c
/* graphics/gfxbase.h */
-#define GFXB_BIG_BLITS 0 /* big blitter (ECS Agnus) */
-#define GFXB_HR_AGNUS 0 /* same — hi-res Agnus */
+#define GFXB_BIG_BLITS 0 /* big blitter (ECS Agnus — 1MB chip) */
+#define GFXB_HR_AGNUS 0 /* hi-res Agnus (same bit as above) */
#define GFXB_HR_DENISE 1 /* ECS Denise (SuperHires, scan-doubling) */
#define GFXB_AA_ALICE 2 /* AGA Alice (A1200/A4000) */
-#define GFXB_AA_LISA 3 /* AGA Lisa */
-#define GFXB_AA_MLISA 4 /* AGA Lisa (modified) */
+#define GFXB_AA_LISA 3 /* AGA Lisa (A1200/A4000) */
+#define GFXB_AA_MLISA 4 /* AGA modified Lisa */
#define GFXF_BIG_BLITS (1<<0)
#define GFXF_HR_AGNUS (1<<0)
@@ -66,20 +90,140 @@ struct GfxBase {
#define GFXF_AA_ALICE (1<<2)
#define GFXF_AA_LISA (1<<3)
#define GFXF_AA_MLISA (1<<4)
+```
-/* Check for AGA: */
-if (GfxBase->ChipRevBits0 & GFXF_AA_ALICE) {
- /* AGA chipset — 8-bit planar, 256 colours in indexed mode */
+### Detecting the Chipset
+
+```c
+struct GfxBase *GfxBase = (struct GfxBase *)
+ OpenLibrary("graphics.library", 0);
+
+if (GfxBase->ChipRevBits0 & GFXF_AA_ALICE)
+{
+ /* AGA chipset (A1200/A4000) */
+ /* 8-bit planar, 256 colours, 24-bit palette */
}
+else if (GfxBase->ChipRevBits0 & GFXF_HR_DENISE)
+{
+ /* ECS chipset (A3000/A600) */
+ /* SuperHires, productivity modes, scan-doubler */
+}
+else if (GfxBase->ChipRevBits0 & GFXF_HR_AGNUS)
+{
+ /* ECS Agnus with OCS Denise (A500+/A2000 upgraded) */
+ /* 1 MB Chip RAM support, but no ECS display features */
+}
+else
+{
+ /* OCS chipset (original A500/A1000/A2000) */
+ /* 512 KB Chip RAM, 4096 colour palette */
+}
+```
+
+| Bits Set | Chipset | Systems |
+|---|---|---|
+| None | OCS | A500, A1000, A2000 |
+| `HR_AGNUS` | ECS Agnus only | A500+ (Fat Agnus upgrade) |
+| `HR_AGNUS + HR_DENISE` | Full ECS | A600, A3000 |
+| `AA_ALICE + AA_LISA` | AGA | A1200, A4000, CD32 |
+
+---
+
+## PAL vs NTSC Detection
+
+```c
+if (GfxBase->VBlankFrequency == 50)
+{
+ /* PAL: 50 Hz, 312 lines/field, 625 total (interlaced) */
+ /* Standard resolution: 320×256 */
+}
+else /* 60 */
+{
+ /* NTSC: 60 Hz, 262 lines/field, 525 total (interlaced) */
+ /* Standard resolution: 320×200 */
+}
+
+/* DisplayFlags also has the info: */
+if (GfxBase->DisplayFlags & PAL) /* PAL system */
+if (GfxBase->DisplayFlags & NTSC) /* NTSC system */
```
---
-## PAL vs NTSC
+## The Display Pipeline
+
+```mermaid
+flowchart TD
+ subgraph "Application"
+ VP["ViewPort
(one per screen)"]
+ end
+
+ subgraph "GfxBase Pipeline"
+ MV["MakeVPort(view, vp)
→ build copper instructions
for this viewport"]
+ MC["MrgCop(view)
→ merge all viewport copper
into single list"]
+ LV["LoadView(view)
→ install merged copper list
into hardware"]
+ end
+
+ subgraph "Hardware"
+ COP["Copper DMA
executes copper list"]
+ CHIPS["Custom Chips
display image"]
+ end
+
+ VP --> MV --> MC --> LV --> COP --> CHIPS
+
+ style MV fill:#fff9c4,stroke:#f9a825,color:#333
+ style MC fill:#fff9c4,stroke:#f9a825,color:#333
+ style LV fill:#fff9c4,stroke:#f9a825,color:#333
+```
+
+### Key Display Functions
+
+| Function | Purpose |
+|---|---|
+| `MakeVPort(view, vp)` | Generate copper instructions for one ViewPort |
+| `MrgCop(view)` | Merge all ViewPort copper lists into unified system list |
+| `LoadView(view)` | Install the merged copper list into the hardware (LOF/SHF) |
+| `WaitTOF()` | Wait for top of frame (vertical blank) — used to sync display updates |
+| `WaitBOVP(vp)` | Wait for the beam to pass a specific ViewPort |
+
+---
+
+## Blitter Queue
+
+GfxBase maintains a queue of pending Blitter operations (`blthd`/`blttl`). When an application calls `BltBitMap`, the operation may be queued and executed asynchronously by the Blitter interrupt:
```c
-if (GfxBase->VBlankFrequency == 50) /* PAL: 50Hz, 625 lines */
-if (GfxBase->VBlankFrequency == 60) /* NTSC: 60Hz, 525 lines */
+/* Start a blit — may be queued: */
+BltBitMap(srcBM, sx, sy, dstBM, dx, dy, w, h, 0xC0, 0xFF, NULL);
+
+/* Wait for ALL pending blits to complete: */
+WaitBlit();
+
+/* Or use OwnBlitter/DisownBlitter for exclusive access: */
+OwnBlitter(); /* blocks until blitter is free, then locks it */
+/* ... direct blitter register access ... */
+DisownBlitter(); /* release for other tasks */
+```
+
+---
+
+## System Fonts
+
+```c
+/* Get the default system font: */
+struct TextFont *sysFont = GfxBase->DefaultFont;
+Printf("System font: %s, size %d\n",
+ sysFont->tf_Message.mn_Node.ln_Name,
+ sysFont->tf_YSize);
+
+/* Enumerate all loaded fonts: */
+struct Node *node;
+for (node = GfxBase->TextFonts.lh_Head;
+ node->ln_Succ;
+ node = node->ln_Succ)
+{
+ Printf("Font: %s\n", node->ln_Name);
+}
```
---
@@ -88,3 +232,6 @@ if (GfxBase->VBlankFrequency == 60) /* NTSC: 60Hz, 525 lines */
- NDK39: `graphics/gfxbase.h`
- ADCD 2.1: graphics.library autodocs
+- See also: [views.md](views.md) — View/ViewPort construction
+- See also: [copper.md](copper.md) — Copper coprocessor
+- See also: [display_modes.md](display_modes.md) — display mode database
diff --git a/08_graphics/ham_ehb_modes.md b/08_graphics/ham_ehb_modes.md
index 46c64a2..aa8215d 100644
--- a/08_graphics/ham_ehb_modes.md
+++ b/08_graphics/ham_ehb_modes.md
@@ -4,7 +4,7 @@
## Overview
-The Amiga offers two unique display modes that squeeze many more colours from limited bitplane hardware: **EHB** (Extra Half-Brite) and **HAM** (Hold-And-Modify). These modes have no direct equivalent on other platforms and are critical for understanding Amiga graphics capability.
+The Amiga offers two unique display modes that squeeze many more colours from limited bitplane hardware: **EHB** (Extra Half-Brite) and **HAM** (Hold-And-Modify). These modes have no direct equivalent on other platforms and are critical for understanding Amiga graphics capability and for FPGA implementation.
---
@@ -14,121 +14,429 @@ The Amiga offers two unique display modes that squeeze many more colours from li
Uses **6 bitplanes** (64 possible values):
- Bitplane values 0–31: index into the 32-colour palette normally
-- Bitplane values 32–63: display the colour from register (value − 32) at **half brightness** (all RGB components halved)
+- Bitplane values 32–63: display the colour from register (value − 32) at **half brightness** (all RGB components shifted right by 1)
-```
-Bitplanes 0–4 → 5-bit colour index (0–31)
-Bitplane 5 → "half brightness" flag
+```mermaid
+flowchart LR
+ BP["6 Bitplanes"] --> SPLIT{"Bit 5?"}
+ SPLIT -->|"0"| NORMAL["Bits 4-0 = index
Palette lookup"]
+ SPLIT -->|"1"| EHB["Bits 4-0 = index
Palette lookup
then R>>1, G>>1, B>>1"]
+ NORMAL --> DAC["RGB to DAC"]
+ EHB --> DAC
+
+ style EHB fill:#fff3e0,stroke:#ff9800,color:#333
+ style NORMAL fill:#e8f4fd,stroke:#2196f3,color:#333
```
-### Effective Result
+```
+Example pixel value = 37 (binary: 100101):
+ Bit 5 = 1 → half-brite
+ Bits 4-0 = 00101 = palette index 5
+ Output colour = palette[5] >> 1 (each R,G,B component halved)
-- 32 programmer-defined colours + 32 fixed half-brightness versions = **64 colours**
-- Zero additional palette RAM needed
-- Useful for shadows and smooth gradients
+Example pixel value = 5 (binary: 000101):
+ Bit 5 = 0 → normal
+ Bits 4-0 = 00101 = palette index 5
+ Output colour = palette[5] (full brightness)
+```
-### Enabling EHB
+### Programming EHB
```c
-/* In ViewPort ColorMap: */
-/* Simply use 6 bitplanes with no HAM flag */
-struct BitMap bm;
-InitBitMap(&bm, 6, 320, 256); /* 6 planes */
-/* No EXTRA_HALFBRITE flag needed — it's automatic when depth=6 */
+/* EHB is automatic when using 6 bitplanes without HAM flag: */
+struct Screen *scr = OpenScreenTags(NULL,
+ SA_Width, 320,
+ SA_Height, 256,
+ SA_Depth, 6, /* 6 planes → EHB mode */
+ SA_DisplayID, EXTRAHALFBRITE_KEY,
+ TAG_DONE);
+
+/* Set the 32 base colours: */
+ULONG colours32[32 * 3 + 2];
+colours32[0] = 32 << 16; /* count = 32, first = 0 */
+/* ... fill RGB values ... */
+colours32[32 * 3 + 1] = 0; /* terminator */
+LoadRGB32(&scr->ViewPort, colours32);
+
+/* Pixels 0–31 use base palette directly.
+ Pixels 32–63 are automatically half-brightness versions.
+ No need to set colours 32–63 — hardware does it. */
```
---
-## HAM — Hold-And-Modify (OCS/ECS)
+## HAM6 — Hold-And-Modify (OCS/ECS/AGA)
-### How It Works
+### Pixel Encoding
-Uses **6 bitplanes**. Each pixel's 6 bits are interpreted as:
+Uses **6 bitplanes**. Each pixel's 6 bits are split into a 2-bit command and 4-bit data:
-| Bits 5–4 | Meaning | Bits 3–0 |
-|---|---|---|
-| `00` | **SET** — index colour register | 4-bit palette index (0–15) |
-| `01` | **MODIFY BLUE** — hold R,G; set B | New blue nibble |
-| `10` | **MODIFY RED** — hold G,B; set R | New red nibble |
-| `11` | **MODIFY GREEN** — hold R,B; set G | New green nibble |
+```mermaid
+flowchart TD
+ subgraph "6-Bit Pixel Value"
+ direction LR
+ CMD["Bits 5-4
(command)"] --- DATA["Bits 3-0
(data)"]
+ end
-### Effective Result
+ CMD --> C00["00 = SET"]
+ CMD --> C01["01 = MOD BLUE"]
+ CMD --> C10["10 = MOD RED"]
+ CMD --> C11["11 = MOD GREEN"]
-- Each pixel can set one of 16 base colours, OR modify one component of the previous pixel's colour
-- Theoretical maximum: **4,096 colours** on screen simultaneously
-- Practical result: colour fringing at sharp edges (each pixel depends on its left neighbour)
+ C00 --> R00["Output = Palette at data
R,G,B all from palette"]
+ C01 --> R01["Output = prev_R, prev_G, data
Only Blue changes"]
+ C10 --> R10["Output = data, prev_G, prev_B
Only Red changes"]
+ C11 --> R11["Output = prev_R, data, prev_B
Only Green changes"]
-### OCS/ECS Limitations
+ style C00 fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style C01 fill:#bbdefb,stroke:#1565c0,color:#333
+ style C10 fill:#ffcdd2,stroke:#c62828,color:#333
+ style C11 fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
-- Only 16 base colours (SET mode uses 4 bits → 16 palette entries)
-- Only 4-bit component modification → 16 levels per channel
-- Total colour space: 12-bit (4096 colours)
+### How the Hardware Decodes — Per-Pixel Pipeline
+
+```mermaid
+flowchart LR
+ PREV["Previous pixel
R=A G=7 B=3"] --> DECODE{"Command?"}
+ DECODE -->|"00 (SET)"| PAL["Palette[data]
R=F G=0 B=8"]
+ DECODE -->|"01 (MOD B)"| MODB["R=A G=7 B=data"]
+ DECODE -->|"10 (MOD R)"| MODR["R=data G=7 B=3"]
+ DECODE -->|"11 (MOD G)"| MODG["R=A G=data B=3"]
+
+ PAL --> NEXT["Current pixel
→ becomes 'previous'
for next pixel"]
+ MODB --> NEXT
+ MODR --> NEXT
+ MODG --> NEXT
+```
+
+> [!IMPORTANT]
+> **Each scanline starts fresh** — the first pixel of each line has no "previous pixel" to modify. The hardware resets to the background colour (register 0) at the start of each line. This is why HAM images often have a visible "colour ramp" at the left edge.
+
+### Practical Example — Encoding a HAM6 Scanline
+
+Suppose we want to display these colours on a scanline:
+
+```
+Target: RGB(A,7,3) → RGB(A,7,F) → RGB(F,7,F) → RGB(F,0,8)
+
+Encoding:
+ Pixel 0: 00 xxxx (SET palette[n] = A,7,3) → SET to base colour
+ Pixel 1: 01 1111 (MOD BLUE = F) → A,7,3 → A,7,F ✓
+ Pixel 2: 10 1111 (MOD RED = F) → A,7,F → F,7,F ✓
+ Pixel 3: 00 xxxx (SET palette[m] = F,0,8) → SET to nearest base colour
+
+Note: pixel 3 needs to change ALL THREE components.
+Since HAM can only modify ONE component per pixel, we must either:
+ a) Use 3 pixels to transition (changing R, G, B separately) → "fringing"
+ b) Pick a base palette colour that's close to the target → "SET"
+```
+
+### The Fringing Problem
+
+```mermaid
+flowchart LR
+ subgraph "Desired: sharp edge"
+ direction LR
+ R1["R:F,0,0"] --- R2["R:F,0,0"] --- R3["R:F,0,0"] --- G1["G:0,F,0"] --- G2["G:0,F,0"] --- G3["G:0,F,0"]
+ end
+
+ subgraph "HAM6 reality: 2-pixel fringe"
+ direction LR
+ H1["SET red
F,0,0"] --- H2["SET red
F,0,0"] --- H3["MOD_R 0
0,0,0"] --- H4["MOD_G F
0,F,0"] --- H5["SET green
0,F,0"] --- H6["SET green
0,F,0"]
+ end
+
+ style H3 fill:#ffcdd2,stroke:#c62828,color:#333
+ style H4 fill:#ffcdd2,stroke:#c62828,color:#333
+```
+
+Pixels H3 and H4 are **fringing artifacts** — wrong colours visible during the transition. The encoder must change R, G, B individually (one per pixel), so sharp multi-component transitions always produce visible intermediate colours.
+
+The encoder (usually offline) optimises palette choice and pixel encoding to minimise fringing. Common strategies:
+- Choose 16 base palette colours via **median-cut** from the image histogram
+- Use SET pixels at strong edges
+- Sequence MODIFY commands to approach target in fewest steps
+
+```mermaid
+flowchart TD
+ IMG["Source Image
(24-bit RGB)"] --> HIST["Histogram Analysis"]
+ HIST --> MEDCUT["Median-Cut
Select 16 base colours"]
+ MEDCUT --> PAL["Optimal 16-entry palette"]
+
+ IMG --> SCAN["Process scanlines
left to right"]
+ PAL --> SCAN
+
+ SCAN --> DECIDE{"Distance to target?"}
+ DECIDE -->|"Close base colour exists"| SET["SET command
(no fringing)"]
+ DECIDE -->|"Only 1 component differs"| MOD["MODIFY command
(no fringing)"]
+ DECIDE -->|"2-3 components differ"| FRINGE["2-3 MODIFY sequence
(fringing visible)"]
+
+ style FRINGE fill:#ffcdd2,stroke:#c62828,color:#333
+ style SET fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style MOD fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+### Programming HAM6 from C
+
+```c
+/* Open a HAM6 screen: */
+struct Screen *scr = OpenScreenTags(NULL,
+ SA_Width, 320,
+ SA_Height, 256,
+ SA_Depth, 6,
+ SA_DisplayID, HAM_KEY,
+ TAG_DONE);
+
+/* Set the 16 base palette colours: */
+ULONG hamPalette[16 * 3 + 2];
+hamPalette[0] = 16 << 16; /* count=16, first=0 */
+/* Palette entry 0: R=$A0, G=$70, B=$30 (12-bit values scaled to 32-bit) */
+hamPalette[1] = 0xA0000000; /* R */
+hamPalette[2] = 0x70000000; /* G */
+hamPalette[3] = 0x30000000; /* B */
+/* ... fill remaining 15 entries ... */
+hamPalette[16 * 3 + 1] = 0;
+LoadRGB32(&scr->ViewPort, hamPalette);
+
+/* Write pixels directly to bitplane data: */
+/* Each pixel = 6 bits across 6 bitplanes */
+struct BitMap *bm = scr->RastPort.BitMap;
+UBYTE *plane[6];
+for (int p = 0; p < 6; p++)
+ plane[p] = bm->Planes[p];
+
+/* Encode pixel at position x on line y: */
+void SetHAMPixel(UBYTE *plane[], int x, int y, UBYTE cmd, UBYTE data)
+{
+ int byteOffset = y * 40 + (x >> 3); /* 40 bytes/line for 320px */
+ int bitPos = 7 - (x & 7);
+ UBYTE val = (cmd << 4) | (data & 0x0F); /* 6-bit HAM value */
+
+ for (int p = 0; p < 6; p++)
+ {
+ if (val & (1 << p))
+ plane[p][byteOffset] |= (1 << bitPos);
+ else
+ plane[p][byteOffset] &= ~(1 << bitPos);
+ }
+}
+
+/* Example: SET colour 5, then modify blue to $F: */
+SetHAMPixel(plane, 0, 0, 0x00, 5); /* 00 0101 = SET palette[5] */
+SetHAMPixel(plane, 1, 0, 0x01, 0xF); /* 01 1111 = MOD BLUE = $F */
+```
---
## HAM8 — AGA Enhanced HAM
-### How It Works
+### Pixel Encoding
-Uses **8 bitplanes**:
+Uses **8 bitplanes**. Same principle, wider data:
| Bits 7–6 | Meaning | Bits 5–0 |
|---|---|---|
-| `00` | **SET** — index colour register | 6-bit palette index (0–63) |
-| `01` | **MODIFY BLUE** | 6-bit blue value |
-| `10` | **MODIFY RED** | 6-bit red value |
-| `11` | **MODIFY GREEN** | 6-bit green value |
+| `00` | **SET** — palette index | 6-bit index (0–63 of 256-entry palette) |
+| `01` | **MODIFY BLUE** | 6-bit blue value (0–63) |
+| `10` | **MODIFY RED** | 6-bit red value (0–63) |
+| `11` | **MODIFY GREEN** | 6-bit green value (0–63) |
-### Effective Result
+### Improvements over HAM6
-- 64 base colours from the 256-entry palette
-- 6-bit component modification → 64 levels per channel
-- Total colour space: **18-bit** (262,144 colours)
-- Significantly reduced fringing compared to HAM6
+| Aspect | HAM6 | HAM8 |
+|---|---|---|
+| Base palette entries | 16 | 64 |
+| Colour component precision | 4-bit (16 levels) | 6-bit (64 levels) |
+| Total colour space | 12-bit (4,096) | 18-bit (262,144) |
+| Fringing severity | Severe | Mild (more base colours to SET from) |
+| Memory per 320×256 screen | 6 × 40 × 256 = 60 KB | 8 × 40 × 256 = 80 KB |
-### HAM8 Memory Layout
+### HAM8 Palette Setup
-```
-8 bitplanes × 320 pixels × 256 lines = 81,920 bytes per plane × 8
-= 655,360 bytes (640 KB) for a single HAM8 320×256 display
+```c
+/* HAM8 uses 64 of the 256 AGA palette entries as base colours: */
+struct Screen *scr = OpenScreenTags(NULL,
+ SA_Width, 320,
+ SA_Height, 256,
+ SA_Depth, 8,
+ SA_DisplayID, HAM_KEY, /* HAM flag + 8 planes = HAM8 on AGA */
+ TAG_DONE);
+
+/* Load all 256 palette entries (HAM8 uses entries 0–63 as base): */
+ULONG palette[256 * 3 + 2];
+palette[0] = 256 << 16; /* count=256, first=0 */
+/* AGA palette is 24-bit — each component is 8 bits stored in upper byte: */
+palette[1] = 0xFF000000; /* Entry 0 red = $FF */
+palette[2] = 0x00000000; /* Entry 0 green = $00 */
+palette[3] = 0x00000000; /* Entry 0 blue = $00 */
+/* ... fill 255 more entries ... */
+palette[256 * 3 + 1] = 0;
+LoadRGB32(&scr->ViewPort, palette);
```
---
-## Enabling HAM
+## DMA Timing — How Bitplane Data Reaches the Display
+
+### Bitplane-to-Pixel Pipeline
+
+```mermaid
+flowchart LR
+ CHIP["Chip RAM
(Bitplane data)"] -->|"DMA fetch"| SR["Shift Registers
(6 or 8 parallel)"]
+ SR -->|"1 bit per plane
per pixel clock"| MUX["Bitplane MUX
6/8-bit value"]
+ MUX --> MODE{"Display Mode?"}
+ MODE -->|"Normal"| PAL["Palette Lookup
32/256 entries"]
+ MODE -->|"EHB"| EHB["Palette + Halve"]
+ MODE -->|"HAM"| HAM["HAM Decoder
(cmd + prev pixel)"]
+ PAL --> DAC["RAMDAC
→ Video Out"]
+ EHB --> DAC
+ HAM --> DAC
+
+ style HAM fill:#fff9c4,stroke:#f9a825,color:#333
+ style EHB fill:#fff3e0,stroke:#ff9800,color:#333
+ style PAL fill:#e8f4fd,stroke:#2196f3,color:#333
+```
+
+### Scanline DMA Fetch Cycle
+
+The display hardware fetches bitplane data in DMA slots during each scanline:
+
+```
+One scanline (~64 µs PAL):
+┌────────┬──────────────────────────────────────────┬────────┐
+│ HBlank │ Active Display Area │ HBlank │
+│ │← DMA fetch window (variable width) → │ │
+└────────┴──────────────────────────────────────────┴────────┘
+
+DMA slots consumed per bitplane per lowres line:
+ 1 bitplane = 8 DMA words (16 bytes)
+ 6 bitplanes = 48 DMA words (HAM6/EHB)
+ 8 bitplanes = 64 DMA words (HAM8)
+```
+
+### HAM Decode Pipeline (Hardware)
+
+The HAM decoder operates **one pixel clock behind** the bitplane data output:
+
+```
+Bitplane DMA → Bitplane shift registers → HAM decoder → Colour register → DAC → Video out
+ ↑
+ 1-pixel delay
+ (needs previous pixel's colour)
+```
+
+For FPGA implementation, the HAM decoder is a simple combinational circuit:
+
+```verilog
+// HAM6 decoder (simplified)
+always @(*) begin
+ case (pixel_data[5:4])
+ 2'b00: begin // SET
+ out_r = palette[pixel_data[3:0]][11:8];
+ out_g = palette[pixel_data[3:0]][7:4];
+ out_b = palette[pixel_data[3:0]][3:0];
+ end
+ 2'b01: begin // MODIFY BLUE
+ out_r = prev_r;
+ out_g = prev_g;
+ out_b = pixel_data[3:0];
+ end
+ 2'b10: begin // MODIFY RED
+ out_r = pixel_data[3:0];
+ out_g = prev_g;
+ out_b = prev_b;
+ end
+ 2'b11: begin // MODIFY GREEN
+ out_r = prev_r;
+ out_g = pixel_data[3:0];
+ out_b = prev_b;
+ end
+ endcase
+end
+```
+
+---
+
+## Standard Palette Modes — For Comparison
+
+### Setting Palette Colours (Non-HAM)
```c
-/* Via ViewPort: */
-vp->Modes |= HAM; /* HAMF flag in modes */
+/* OS 3.0+ — 24-bit precision (AGA): */
+ULONG colours[3 * 3 + 2]; /* 3 colours */
+colours[0] = 3 << 16; /* count=3, first entry=0 */
+/* Entry 0: black */
+colours[1] = 0x00000000; colours[2] = 0x00000000; colours[3] = 0x00000000;
+/* Entry 1: bright red */
+colours[4] = 0xFF000000; colours[5] = 0x00000000; colours[6] = 0x00000000;
+/* Entry 2: pure blue */
+colours[7] = 0x00000000; colours[8] = 0x00000000; colours[9] = 0xFF000000;
+colours[10] = 0; /* terminator */
+LoadRGB32(vp, colours);
-/* Via SA_ tags (Intuition screen): */
-struct TagItem scrTags[] = {
- { SA_Width, 320 },
- { SA_Height, 256 },
- { SA_Depth, 6 }, /* 6 for HAM6, 8 for HAM8 */
- { SA_DisplayID, HAM_KEY }, /* or SUPER_KEY|HAM for Super-HiRes HAM */
- { TAG_DONE, 0 }
-};
+/* OCS/ECS — 12-bit precision: */
+UWORD oldPalette[] = { 0x000, 0xF00, 0x00F }; /* 4 bits per channel */
+LoadRGB4(vp, oldPalette, 3);
+
+/* Direct hardware (bypass OS — for demos/games): */
+custom->color[0] = 0x000; /* $DFF180: COLOR00 (background) */
+custom->color[1] = 0xF00; /* $DFF182: COLOR01 */
+custom->color[2] = 0x00F; /* $DFF184: COLOR02 */
+/* AGA: extra bits via BPLCON3 bank select */
```
+### Colour Cycling (Palette Animation)
+
+```c
+/* Rotate palette entries for animation — common demo/game technique: */
+void CyclePalette(struct ViewPort *vp, int first, int last)
+{
+ ULONG saved[3]; /* save last entry */
+ GetRGB32(vp->ColorMap, last, 1, saved);
+
+ /* Shift all entries up by one: */
+ for (int i = last; i > first; i--)
+ {
+ ULONG rgb[3];
+ GetRGB32(vp->ColorMap, i - 1, 1, rgb);
+ SetRGB32(vp, i, rgb[0], rgb[1], rgb[2]);
+ }
+
+ /* Wrap last to first: */
+ SetRGB32(vp, first, saved[0], saved[1], saved[2]);
+
+ /* Force display update: */
+ MakeVPort(GfxBase->ActiView, vp);
+ MrgCop(GfxBase->ActiView);
+ LoadView(GfxBase->ActiView);
+}
+```
+
+> [!TIP]
+> Colour cycling is extremely cheap — only palette registers change, not pixel data. A single `SetRGB32` call costs a few microseconds vs redrawing the entire screen. This is why palette animation was so popular on the Amiga.
+
---
## Comparison Table
-| Feature | EHB | HAM6 | HAM8 |
-|---|---|---|---|
-| Bitplanes | 6 | 6 | 8 |
-| Chipset | OCS/ECS/AGA | OCS/ECS/AGA | AGA only |
-| Base palette | 32 | 16 | 64 |
-| Max on-screen colours | 64 | 4,096 | 262,144 |
-| Colour depth | 12-bit | 12-bit | 24-bit (via 18-bit HAM) |
-| Fringing | None | Significant | Mild |
-| Good for | GUI, sprites | Photos, static art | Photos, video stills |
-| Bad for | — | Animation, scrolling | Memory-hungry |
+| Feature | Normal (5-plane) | EHB | HAM6 | HAM8 |
+|---|---|---|---|---|
+| Bitplanes | 5 | 6 | 6 | 8 |
+| Chipset | OCS/ECS/AGA | OCS/ECS/AGA | OCS/ECS/AGA | AGA only |
+| Programmable colours | 32 | 32 | 16 | 64 |
+| Total on-screen | 32 | 64 | 4,096 | 262,144 |
+| Colour depth | 12-bit (OCS) / 24-bit (AGA) | 12/24-bit | 12-bit | 18-bit |
+| Fringing | None | None | Significant | Mild |
+| Good for | GUI, games | GUI with shadows | Photos, static art | Photos, video stills |
+| Bad for | Photo-realism | Limited palette control | Animation, scrolling | Memory: 80 KB/frame |
+| DMA words/line (lores) | 40 | 48 | 48 | 64 |
---
## References
-- HRM: *Display modes* chapter
+- HRM: *Display modes* chapter — HAM decode logic
- NDK39: `graphics/displayinfo.h` — `HAM_KEY`, `EXTRAHALFBRITE_KEY`
+- See also: [display_modes.md](display_modes.md) — ModeID system and chipset comparison
+- See also: [copper_programming.md](copper_programming.md) — Copper-driven palette tricks
+- See also: [bitmap.md](bitmap.md) — bitplane memory layout
diff --git a/08_graphics/rastport.md b/08_graphics/rastport.md
index 62b7d66..e990b61 100644
--- a/08_graphics/rastport.md
+++ b/08_graphics/rastport.md
@@ -4,7 +4,31 @@
## Overview
-`RastPort` is the primary drawing context in AmigaOS. All graphics primitives (pixel, line, rectangle, polygon, text) operate through a `RastPort`, which links a `BitMap`, drawing pen colours, pattern, font, and optional `Layer` for clipping.
+`RastPort` is the primary drawing context in AmigaOS — the equivalent of a "device context" (Windows) or "graphics context" (X11). All graphics primitives (pixel, line, rectangle, polygon, text) operate through a RastPort, which bundles together a target `BitMap`, drawing pen colours, patterns, font, draw mode, and an optional `Layer` for clipping.
+
+Every Intuition window and screen has its own RastPort. When you draw into a window, you're drawing through its RastPort.
+
+```mermaid
+flowchart TD
+ subgraph "RastPort — Drawing Context"
+ RP["struct RastPort"]
+ RP --> BM["BitMap
(pixel storage)"]
+ RP --> PEN["Pens
FgPen, BgPen, AOlPen"]
+ RP --> DM["DrawMode
JAM1/JAM2/COMPLEMENT"]
+ RP --> FONT["TextFont
(current font)"]
+ RP --> PAT["AreaPtrn
(fill pattern)"]
+ RP --> LAYER["Layer
(clipping region)"]
+ RP --> CP["cp_x, cp_y
(cursor position)"]
+ end
+
+ APP["Application Code"] -->|"SetAPen, Move,
Draw, RectFill,
Text, ClipBlit"| RP
+ RP -->|"Drawing operations"| BM
+ LAYER -->|"Clips to visible region"| BM
+
+ style RP fill:#e8f4fd,stroke:#2196f3,color:#333
+ style BM fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style LAYER fill:#fff9c4,stroke:#f9a825,color:#333
+```
---
@@ -49,64 +73,331 @@ struct RastPort {
## Draw Modes
-```c
-#define JAM1 0 /* draw FgPen only; background transparent */
-#define JAM2 1 /* draw FgPen and BgPen (opaque) */
-#define COMPLEMENT 2 /* XOR with existing pixels */
-#define INVERSVID 4 /* invert video (swap fg/bg for text) */
+The draw mode controls how new pixels combine with existing content:
+
+```mermaid
+flowchart LR
+ subgraph "JAM1 — Transparent"
+ J1BG["Background
unchanged"] --- J1FG["Foreground
drawn with FgPen"]
+ end
+
+ subgraph "JAM2 — Opaque"
+ J2BG["Background
drawn with BgPen"] --- J2FG["Foreground
drawn with FgPen"]
+ end
+
+ subgraph "COMPLEMENT — XOR"
+ XBG["All pixels
XOR with existing"]
+ end
```
+```c
+#define JAM1 0 /* draw FgPen only; background pixels are NOT touched */
+#define JAM2 1 /* draw FgPen AND BgPen — fully opaque */
+#define COMPLEMENT 2 /* XOR all drawn pixels with existing content */
+#define INVERSVID 4 /* swap foreground/background (for text inverse) */
+```
+
+| Mode | Text Example | Fill Example | Use Case |
+|---|---|---|---|
+| `JAM1` | Characters drawn, background shows through | Solid fill with FgPen | Transparent overlays, labels on images |
+| `JAM2` | Characters + background box drawn | Same as JAM1 for fills | Opaque text on busy backgrounds |
+| `COMPLEMENT` | XOR of text pixels with screen | XOR fill | Rubber-band selection, dragging cursors |
+| `JAM1 | INVERSVID` | Background drawn with FgPen, chars transparent | — | Highlighted/selected text |
+
---
## Drawing Primitives
-```c
-/* Set pen colour: */
-SetAPen(rp, 1); /* foreground = colour 1 */
-SetBPen(rp, 0); /* background = colour 0 */
-SetDrMd(rp, JAM1); /* transparent background */
+### Pen and Position Setup
-/* Move to position: */
+```c
+/* Set pen colours: */
+SetAPen(rp, 1); /* foreground = colour register 1 */
+SetBPen(rp, 0); /* background = colour register 0 */
+SetDrMd(rp, JAM1); /* transparent background mode */
+
+/* OS 3.0+ — use named pen for correct Workbench colours: */
+SetAPen(rp, screen->RastPort.BitMap->Depth > 1 ?
+ ObtainBestPen(screen->ViewPort.ColorMap,
+ 0xFF000000, 0x00000000, 0x00000000, /* red */
+ OBP_Precision, PRECISION_GUI,
+ TAG_DONE) : 1);
+```
+
+### Lines and Pixels
+
+```mermaid
+flowchart LR
+ M["Move(rp, 10, 20)"] -->|"sets cp_x, cp_y"| D1["Draw(rp, 100, 20)"]
+ D1 -->|"draws line,
updates cp"| D2["Draw(rp, 100, 80)"]
+ D2 -->|"draws line,
updates cp"| D3["Draw(rp, 10, 80)"]
+ D3 -->|"draws line,
updates cp"| D4["Draw(rp, 10, 20)"]
+
+ style M fill:#e8f4fd,stroke:#2196f3,color:#333
+```
+
+```c
+/* Move cursor (no drawing): */
Move(rp, 100, 50);
-/* Draw line from current position to (x,y): */
+/* Draw line from current position to (x,y), update cp: */
Draw(rp, 200, 100);
+/* cp is now (200, 100) — next Draw continues from here */
-/* Write pixel: */
+/* Draw a connected polygon: */
+Move(rp, 10, 10);
+Draw(rp, 100, 10); /* top edge */
+Draw(rp, 100, 80); /* right edge */
+Draw(rp, 10, 80); /* bottom edge */
+Draw(rp, 10, 10); /* close: left edge */
+
+/* Single pixel: */
WritePixel(rp, 160, 120);
-
-/* Read pixel: */
LONG colour = ReadPixel(rp, 160, 120);
-/* Filled rectangle: */
-RectFill(rp, 10, 10, 100, 50);
+/* Dashed lines: */
+SetDrPt(rp, 0xF0F0); /* 16-bit pattern: 1111000011110000 */
+Draw(rp, 200, 100); /* draws dashed line */
+SetDrPt(rp, 0xFFFF); /* restore solid */
+```
-/* Draw text at current position: */
-Move(rp, 20, 30);
-Text(rp, "Hello", 5);
+### Rectangles and Fills
-/* Blitter copy between RastPorts: */
-ClipBlit(srcRP, sx, sy, dstRP, dx, dy, w, h, minterm);
+```c
+/* Solid filled rectangle (uses FgPen + DrawMode): */
+SetAPen(rp, 3);
+RectFill(rp, 10, 10, 100, 50); /* x1,y1 to x2,y2 inclusive */
+
+/* Erase to background (clear a region): */
+SetAPen(rp, 0);
+SetDrMd(rp, JAM1);
+RectFill(rp, 0, 0, 319, 255);
+
+/* Scroll a region (with clear): */
+ScrollRaster(rp, dx, dy, x1, y1, x2, y2);
+/* Shifts content by (dx,dy); exposed area cleared to BgPen */
+```
+
+### Text Rendering
+
+```mermaid
+flowchart LR
+ ATTR["TextAttr
(name, size)"] -->|"OpenFont /
OpenDiskFont"| TF["TextFont"]
+ TF -->|"SetFont(rp, font)"| RP["RastPort"]
+ RP -->|"Move + Text"| OUT["Rendered text
on bitmap"]
+```
+
+```c
+/* Set font: */
+struct TextAttr ta = {"topaz.font", 8, 0, FPF_ROMFONT};
+struct TextFont *font = OpenFont(&ta);
+SetFont(rp, font);
+
+/* Render text at position: */
+Move(rp, 20, 30); /* baseline position */
+Text(rp, "Hello Amiga", 11);
+
+/* Measure text width before rendering (for alignment): */
+UWORD width = TextLength(rp, "Hello Amiga", 11);
+/* Right-align: */
+Move(rp, screenWidth - width - 10, 30);
+Text(rp, "Hello Amiga", 11);
+
+/* Bold/italic (algorithmic): */
+UWORD supported = AskSoftStyle(rp);
+SetSoftStyle(rp, FSF_BOLD | FSF_ITALIC, supported);
+Text(rp, "Bold Italic", 11);
+SetSoftStyle(rp, 0, supported); /* restore */
+```
+
+### Area Fills (Polygons)
+
+```c
+/* Area fills require setup: TmpRas + AreaInfo */
+UBYTE areaBuffer[5 * 5]; /* 5 vertices × 5 bytes each */
+struct AreaInfo areaInfo;
+InitArea(&areaInfo, areaBuffer, 5);
+rp->AreaInfo = &areaInfo;
+
+PLANEPTR tmpRasData = AllocRaster(320, 256);
+struct TmpRas tmpRas;
+InitTmpRas(&tmpRas, tmpRasData, RASSIZE(320, 256));
+rp->TmpRas = &tmpRas;
+
+/* Draw a filled triangle: */
+AreaMove(rp, 100, 10); /* first vertex */
+AreaDraw(rp, 200, 180); /* second vertex */
+AreaDraw(rp, 20, 180); /* third vertex */
+AreaEnd(rp); /* fill and close */
+
+/* Cleanup: */
+FreeRaster(tmpRasData, 320, 256);
+```
+
+### Flood Fill
+
+```c
+/* Flood fill from a seed point: */
+/* Requires TmpRas (same setup as area fills) */
+Flood(rp, 1, 50, 50);
+/* mode 1 = fill until FgPen colour boundary */
+/* mode 0 = fill all connected pixels of same colour as seed */
+```
+
+### Blitting (Block Transfer)
+
+```mermaid
+flowchart LR
+ SRC["Source RastPort
(or BitMap)"] -->|"ClipBlit /
BltBitMapRastPort"| DST["Destination RastPort"]
+
+ subgraph "Minterm Controls"
+ MT["0xC0 = copy
0x30 = invert copy
0x50 = XOR
0x00 = clear"]
+ end
+ MT --> DST
+```
+
+```c
+/* Copy region between RastPorts (respects clipping): */
+ClipBlit(srcRP, sx, sy, /* source position */
+ dstRP, dx, dy, /* destination position */
+ width, height,
+ 0xC0); /* minterm: straight copy */
+
+/* Copy from BitMap to RastPort: */
+BltBitMapRastPort(srcBM, sx, sy,
+ dstRP, dx, dy,
+ width, height,
+ 0xC0);
+
+/* Common minterms: */
+/* 0xC0 = A (copy source) */
+/* 0x30 = NOT A (invert source) */
+/* 0x50 = A XOR B (toggle) */
+/* 0x00 = clear destination */
+/* 0xFF = set all bits */
```
---
-## Layers
+## Layers — Window Clipping
-When `rp->Layer != NULL`, all drawing is clipped to the layer's bounds and damage regions. Layers are managed by `layers.library`:
+When `rp->Layer != NULL`, all drawing is automatically clipped to the layer's visible region. Intuition creates layers for every window — this is how overlapping windows work without drawing over each other.
+
+```mermaid
+flowchart TD
+ subgraph "Screen"
+ subgraph "Window A (front)"
+ LA["Layer A
(fully visible)"]
+ end
+ subgraph "Window B (behind)"
+ LB["Layer B
(partially obscured)"]
+ end
+ subgraph "Window C (behind both)"
+ LC["Layer C
(mostly obscured)"]
+ end
+ end
+
+ DRAW["Draw(windowB->RPort, ...)"] --> LB
+ LB -->|"ClipRects exclude
obscured regions"| BM["BitMap
(only visible
portions drawn)"]
+
+ style LA fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style LB fill:#fff9c4,stroke:#f9a825,color:#333
+ style LC fill:#ffcdd2,stroke:#c62828,color:#333
+```
+
+Each layer maintains a list of **ClipRects** — rectangles defining which parts of the layer are visible. When you draw to a partially obscured window, `layers.library` breaks your drawing operation into multiple clipped sub-draws, one per visible ClipRect.
```c
+/* Direct layer creation (Intuition does this for windows): */
struct Layer_Info *li = NewLayerInfo();
-struct Layer *layer = CreateUpfrontLayer(li, bm, x1, y1, x2, y2, flags, NULL);
-rp = layer->rp; /* use this RastPort for drawing */
-/* ... draw ... */
+struct Layer *layer = CreateUpfrontLayer(li, bm,
+ x1, y1, x2, y2,
+ LAYERSIMPLE, /* or LAYERSMART, LAYERSUPER */
+ NULL);
+struct RastPort *rp = layer->rp; /* use this for drawing */
+
+/* LAYERSIMPLE — no backing store; app must redraw damaged areas */
+/* LAYERSMART — automatic backing store; OS handles redraw */
+/* LAYERSUPER — super bitmap; full offscreen buffer */
+
+/* Layer clipping is automatic — Draw, RectFill, Text all respect it */
+Draw(rp, 200, 100); /* clipped to visible portion of layer */
+
+/* Cleanup: */
DeleteLayer(0, layer);
DisposeLayerInfo(li);
```
+### Layer Types
+
+| Type | Flag | Backing Store | Damage Handling | Memory |
+|---|---|---|---|---|
+| Simple | `LAYERSIMPLE` | None | App receives `IDCMP_REFRESHWINDOW` and must redraw | Minimal |
+| Smart | `LAYERSMART` | Auto-saved obscured regions | OS restores automatically | Moderate |
+| Super | `LAYERSUPER` | Full off-screen bitmap | Full bitmap always valid | High |
+
+> [!TIP]
+> **`WFLG_SIMPLE_REFRESH`** windows use the least memory but require the most application code (you must handle `IDCMP_REFRESHWINDOW`). **`WFLG_SMART_REFRESH`** is the default for most applications — Intuition saves/restores obscured regions automatically.
+
+---
+
+## Patterns
+
+### Line Patterns
+
+```c
+/* 16-bit repeating pattern for dashed lines: */
+SetDrPt(rp, 0xFF00); /* ████████░░░░░░░░ — long dash */
+SetDrPt(rp, 0xF0F0); /* ████░░░░████░░░░ — medium dash */
+SetDrPt(rp, 0xAAAA); /* █░█░█░█░█░█░█░█░ — dotted */
+SetDrPt(rp, 0xFCFC); /* ██████░░██████░░ — dash-dot */
+```
+
+### Area Fill Patterns
+
+```c
+/* Area patterns are powers-of-2 height, 16 bits wide: */
+UWORD checkerPattern[] = { 0x5555, 0xAAAA }; /* 2-line checkerboard */
+rp->AreaPtrn = checkerPattern;
+rp->AreaPtSz = 1; /* log2(2) = 1 */
+
+/* Now RectFill uses the pattern instead of solid fill: */
+RectFill(rp, 10, 10, 100, 80);
+
+/* Multiplane patterns (one set per bitplane): */
+UWORD brickPattern[] = {
+ /* plane 0: */ 0xFFFF, 0x8080, 0xFFFF, 0x0808,
+ /* plane 1: */ 0xFFFF, 0x8080, 0xFFFF, 0x0808
+};
+rp->AreaPtrn = brickPattern;
+rp->AreaPtSz = 2; /* log2(4 lines) = 2 */
+rp->Mask |= 0x02; /* enable plane 1 pattern */
+```
+
+---
+
+## Plane Mask — Selective Bitplane Drawing
+
+```c
+/* Mask controls which bitplanes are affected by drawing: */
+rp->Mask = 0xFF; /* all planes — default */
+rp->Mask = 0x01; /* only plane 0 — fast for single-plane effects */
+rp->Mask = 0x03; /* planes 0 and 1 only */
+
+/* Use case: draw a 2-colour overlay without disturbing other planes: */
+rp->Mask = 0x04; /* only plane 2 */
+SetAPen(rp, 4); /* colour index with bit 2 set */
+RectFill(rp, 0, 0, 319, 255);
+rp->Mask = 0xFF; /* restore */
+```
+
---
## References
- NDK39: `graphics/rastport.h`, `graphics/gfxmacros.h`
- ADCD 2.1: `SetAPen`, `Move`, `Draw`, `Text`, `RectFill`, `ClipBlit`
+- See also: [bitmap.md](bitmap.md) — BitMap structure and allocation
+- See also: [layers.md](../../11_libraries/layers.md) — layers.library detailed reference
+- See also: [text_fonts.md](text_fonts.md) — font loading and rendering
+- See also: [blitter.md](blitter.md) — hardware Blitter used by BltBitMap
diff --git a/08_graphics/sprites.md b/08_graphics/sprites.md
index 84cb0ad..40c77a3 100644
--- a/08_graphics/sprites.md
+++ b/08_graphics/sprites.md
@@ -1,102 +1,305 @@
[← Home](../README.md) · [Graphics](README.md)
-# Hardware Sprites — SimpleSprite, MoveSprite
+# Hardware Sprites — DMA Engine, Multiplexing, and Tricks
## Overview
-The Amiga has **8 hardware sprites**, each 16 pixels wide with 3 colours + transparent. Sprites are DMA-driven — the Copper sets their pointers each frame and the display hardware renders them with zero CPU overhead.
+The Amiga has **8 hardware sprites**, each 16 pixels wide with 3 colours + transparent. Sprites are entirely DMA-driven — the custom chips fetch sprite data from Chip RAM and composite them over the playfield with zero CPU overhead. The Copper reloads sprite pointers every frame.
+
+Sprite 0 is reserved by Intuition for the **mouse pointer**. Sprites 1–7 are available for application use.
+
+```mermaid
+flowchart LR
+ subgraph "Chip RAM"
+ SD0["Sprite 0 data
(mouse pointer)"]
+ SD1["Sprite 1 data"]
+ SD2["Sprite 2-7 data"]
+ end
+
+ subgraph "Custom Chips (Denise/Lisa)"
+ DMA["Sprite DMA
(8 channels)"] --> MUX["Priority MUX"]
+ PF["Playfield
(bitplane data)"] --> MUX
+ MUX --> DAC["Colour DAC
→ Video Out"]
+ end
+
+ SD0 --> DMA
+ SD1 --> DMA
+ SD2 --> DMA
+
+ subgraph "Copper"
+ COP["Copper List"] -->|"Set SPRxPTH/L
every frame"| DMA
+ end
+
+ style DMA fill:#e8f4fd,stroke:#2196f3,color:#333
+ style COP fill:#fff9c4,stroke:#f9a825,color:#333
+```
---
## Sprite DMA Data Format
-Each sprite scanline is two words (4 bytes):
+Each sprite is stored as a contiguous block in Chip RAM:
```
-Word 0 (DATA): bits 15–0 = pixel colour bit 0 for this line
-Word 1 (DATB): bits 15–0 = pixel colour bit 1 for this line
+┌──────────────────────────────────────────┐
+│ Header Word 0: VSTART/HSTART │ Position control
+│ Header Word 1: VSTOP/Control │
+├──────────────────────────────────────────┤
+│ Line 0: DATA word (bit 0 of each pixel) │ ← 16 pixels per line
+│ Line 0: DATB word (bit 1 of each pixel) │
+├──────────────────────────────────────────┤
+│ Line 1: DATA word │
+│ Line 1: DATB word │
+├──────────────────────────────────────────┤
+│ ...repeat for each line... │
+├──────────────────────────────────────────┤
+│ Terminator: 0x0000 │ End marker
+│ Terminator: 0x0000 │
+└──────────────────────────────────────────┘
+```
+### Pixel Colour Encoding
+
+```
Pixel colour = (DATB_bit << 1) | DATA_bit
- 00 = transparent
- 01 = colour 1 (from sprite palette)
- 10 = colour 2
- 11 = colour 3
+
+ 00 = transparent (playfield shows through)
+ 01 = sprite colour 1
+ 10 = sprite colour 2
+ 11 = sprite colour 3
```
-### Sprite Header
+### Header Bit Layout
```
-WORD 0: VSTART (vertical start position) + HSTART high bits
-WORD 1: VSTOP (vertical stop position) + control bits
+Word 0 (SPRxPOS):
+ Bits 15–8: VSTART[7:0] (vertical start line, low 8 bits)
+ Bits 7–0: HSTART[8:1] (horizontal start, in low-res pixels ÷ 2)
+
+Word 1 (SPRxCTL):
+ Bits 15–8: VSTOP[7:0] (vertical stop line, low 8 bits)
+ Bit 7: unused
+ Bit 6: unused
+ Bit 5: unused
+ Bit 4: unused
+ Bit 3: VSTART[8] (bit 8 of start — for lines > 255)
+ Bit 2: VSTOP[8] (bit 8 of stop)
+ Bit 1: HSTART[0] (low bit of horizontal position)
+ Bit 0: ATTACH (1 = attached to previous sprite)
```
-### End marker
-```
-WORD 0: 0x0000
-WORD 1: 0x0000
-```
+> [!IMPORTANT]
+> **Horizontal position is in low-res pixel units ÷ 2** — a sprite can only be positioned on even low-res pixel boundaries. In hires/superhires modes, this means sprites have coarser horizontal positioning than playfield pixels.
---
## Sprite Colour Palette
-| Sprite pair | Colour registers | Custom addresses |
-|---|---|---|
-| 0–1 | `COLOR17`–`COLOR19` | `$DFF1A2`–`$DFF1A6` |
-| 2–3 | `COLOR21`–`COLOR23` | `$DFF1AA`–`$DFF1AE` |
-| 4–5 | `COLOR25`–`COLOR27` | `$DFF1B2`–`$DFF1B6` |
-| 6–7 | `COLOR29`–`COLOR31` | `$DFF1BA`–`$DFF1BE` |
+Each pair of sprites shares 3 colour registers (colour 0 = transparent for all):
-Sprite pairs can be **attached** to form a single 15-colour sprite (using both sets of 2 bits = 4 bits per pixel).
+| Sprite Pair | Colour Registers | Custom Addresses | Notes |
+|---|---|---|---|
+| 0–1 | `COLOR17`–`COLOR19` | `$DFF1A2`–`$DFF1A6` | Pair with mouse pointer |
+| 2–3 | `COLOR21`–`COLOR23` | `$DFF1AA`–`$DFF1AE` | |
+| 4–5 | `COLOR25`–`COLOR27` | `$DFF1B2`–`$DFF1B6` | |
+| 6–7 | `COLOR29`–`COLOR31` | `$DFF1BA`–`$DFF1BE` | |
+
+```c
+/* Set sprite 0-1 colours directly: */
+custom->color[17] = 0xF00; /* red */
+custom->color[18] = 0x0F0; /* green */
+custom->color[19] = 0xFFF; /* white */
+```
---
-## OS-Level Sprite API
+## Attached Sprites — 15 Colours
+
+Two sprites from the same pair can be **attached** to form a single 15-colour (+ transparent) sprite:
+
+```mermaid
+flowchart LR
+ subgraph "Normal (2 independent sprites)"
+ S0["Sprite 0
3 colours"] --- S1["Sprite 1
3 colours"]
+ end
+
+ subgraph "Attached (1 wide-colour sprite)"
+ SA["Sprites 0+1 attached
4 bits per pixel
15 colours + transparent"]
+ end
+
+ style SA fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+When attached, the even sprite provides bits 0–1 and the odd sprite provides bits 2–3 of the colour index. The 4-bit value indexes into colour registers 16–31.
```c
-/* graphics.library */
-struct SimpleSprite ss;
-WORD sprnum;
-
-/* Obtain a free sprite: */
-sprnum = GetSprite(&ss, -1); /* -1 = any available */
-if (sprnum >= 0) {
- ss.x = 100;
- ss.y = 50;
- ss.height = 16;
-
- /* Move sprite to position: */
- MoveSprite(NULL, &ss, 100, 50);
-
- /* Set sprite image data: */
- ChangeSprite(NULL, &ss, spriteData);
-
- /* Release: */
- FreeSprite(sprnum);
-}
+/* Enable attachment: set bit 0 of odd sprite's CTL word */
+oddSpriteData[1] |= 0x0001; /* ATTACH bit */
```
---
+## Sprite Multiplexing — More Than 8 Sprites
+
+The hardware only supports 8 simultaneous sprites, but demos and games use **multiplexing** to display many more by reusing sprite channels on different scanlines:
+
+```mermaid
+flowchart TD
+ subgraph "Screen (top to bottom)"
+ Z1["Lines 0-50:
Sprite 2 = Enemy A"]
+ Z2["Lines 60-110:
Sprite 2 = Enemy B"]
+ Z3["Lines 120-170:
Sprite 2 = Enemy C"]
+ Z4["Lines 180-230:
Sprite 2 = Enemy D"]
+ end
+
+ COP["Copper List"] -->|"WAIT line 0
Set SPR2PT → Enemy A"| Z1
+ COP -->|"WAIT line 55
Set SPR2PT → Enemy B"| Z2
+ COP -->|"WAIT line 115
Set SPR2PT → Enemy C"| Z3
+ COP -->|"WAIT line 175
Set SPR2PT → Enemy D"| Z4
+
+ style COP fill:#fff9c4,stroke:#f9a825,color:#333
+```
+
+The Copper waits for a line after one sprite ends, then reprograms the sprite pointer register for the next object. This gives effectively **unlimited sprites** as long as they don't overlap vertically on the same channel.
+
+### Multiplexing Rules
+
+| Rule | Explanation |
+|---|---|
+| No vertical overlap on same channel | Two sprites using the same DMA channel cannot appear on the same scanline |
+| 1-line gap required | After a sprite ends (VSTOP), the DMA channel needs at least 1 blank line before starting the next |
+| Copper must be fast enough | Copper WAIT + MOVE takes 2 DMA cycles; pointer reload = 2 MOVEs (PTH+PTL) |
+| Max per scanline: 8 | Absolute hardware limit — 8 DMA channels, one fetch per channel per line |
+
+---
+
+## AGA Sprite Enhancements
+
+| Feature | OCS/ECS | AGA |
+|---|---|---|
+| Width | 16 pixels | 16, 32, or 64 pixels (via FMODE) |
+| Colours (single) | 3 + transparent | 3 + transparent |
+| Colours (attached) | 15 + transparent | 15 + transparent |
+| Horizontal resolution | Low-res ÷ 2 | Same (unchanged) |
+
+```c
+/* AGA: wider sprites via FMODE register */
+custom->fmode = 3; /* 4× fetch — sprites become 64 pixels wide */
+/* WARNING: this also affects bitplane fetch! */
+```
+
+---
+
+## Sprite-Playfield Priority
+
+Sprites interact with playfields via the **priority control register** (`BPLCON2`):
+
+```c
+/* BPLCON2 ($DFF104) priority bits: */
+/* PF2P2-PF2P0: playfield 2 priority vs sprites */
+/* PF1P2-PF1P0: playfield 1 priority vs sprites */
+
+/* Priority order (front to back): */
+/* SP01 > SP23 > SP45 > SP67 > PF1 > PF2 (default) */
+
+/* Make sprites appear behind playfield 1: */
+custom->bplcon2 = 0x0024; /* PF1 in front of all sprites */
+```
+
+```mermaid
+flowchart LR
+ subgraph "Default Priority (front → back)"
+ direction LR
+ S01["Sprites 0-1"] --- S23["Sprites 2-3"] --- S45["Sprites 4-5"] --- S67["Sprites 6-7"] --- PF1["Playfield 1"] --- PF2["Playfield 2"]
+ end
+
+ style S01 fill:#ffcdd2,stroke:#c62828,color:#333
+ style PF1 fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style PF2 fill:#bbdefb,stroke:#1565c0,color:#333
+```
+
+The priority can be configured so that sprites appear **between** playfield 1 and playfield 2 — creating the illusion of depth (e.g., a character walking behind foreground objects but in front of the background).
+
+---
+
## Sprite Pointer Registers
| Register | Address | Sprite |
|---|---|---|
-| `SPR0PTH/L` | `$DFF120–$DFF122` | Sprite 0 |
-| `SPR1PTH/L` | `$DFF124–$DFF126` | Sprite 1 |
-| `SPR2PTH/L` | `$DFF128–$DFF12A` | Sprite 2 |
-| `SPR3PTH/L` | `$DFF12C–$DFF12E` | Sprite 3 |
-| `SPR4PTH/L` | `$DFF130–$DFF132` | Sprite 4 |
-| `SPR5PTH/L` | `$DFF134–$DFF136` | Sprite 5 |
-| `SPR6PTH/L` | `$DFF138–$DFF13A` | Sprite 6 |
-| `SPR7PTH/L` | `$DFF13C–$DFF13E` | Sprite 7 |
+| `SPR0PTH/L` | `$DFF120–$DFF123` | Sprite 0 (mouse pointer) |
+| `SPR1PTH/L` | `$DFF124–$DFF127` | Sprite 1 |
+| `SPR2PTH/L` | `$DFF128–$DFF12B` | Sprite 2 |
+| `SPR3PTH/L` | `$DFF12C–$DFF12F` | Sprite 3 |
+| `SPR4PTH/L` | `$DFF130–$DFF133` | Sprite 4 |
+| `SPR5PTH/L` | `$DFF134–$DFF137` | Sprite 5 |
+| `SPR6PTH/L` | `$DFF138–$DFF13B` | Sprite 6 |
+| `SPR7PTH/L` | `$DFF13C–$DFF13F` | Sprite 7 |
-These must be set every frame — typically via the Copper list.
+These must be set **every frame** — typically by the Copper list during vertical blank. If not set, the sprite DMA will fetch from wherever the pointer was left, producing garbage.
+
+---
+
+## OS-Level Sprite API
+
+```c
+/* graphics.library — system-friendly sprite access */
+struct SimpleSprite ss;
+WORD sprnum;
+
+/* Obtain a free sprite (Intuition reserves sprite 0 for the pointer): */
+sprnum = GetSprite(&ss, -1); /* -1 = any available */
+if (sprnum >= 0)
+{
+ ss.x = 100;
+ ss.y = 50;
+ ss.height = 16;
+
+ /* Set sprite image data (must be in Chip RAM!): */
+ ChangeSprite(NULL, &ss, spriteData);
+
+ /* Move sprite to screen position: */
+ MoveSprite(NULL, &ss, 100, 50);
+
+ /* Release when done: */
+ FreeSprite(sprnum);
+}
+```
+
+### Custom Mouse Pointer
+
+```c
+/* Change the Intuition mouse pointer: */
+UWORD pointerData[] = {
+ 0x0000, 0x0000, /* reserved (position) */
+ /* 16 lines of sprite data: */
+ 0x8000, 0xC000, /* line 0 */
+ 0xC000, 0xE000, /* line 1 */
+ 0xE000, 0xF000, /* line 2 */
+ /* ... etc ... */
+ 0x0000, 0x0000 /* terminator */
+};
+SetPointer(window, pointerData, height, width, xOffset, yOffset);
+/* Restore default: */
+ClearPointer(window);
+```
+
+---
+
+## Common Pitfalls
+
+| Pitfall | Consequence | Fix |
+|---|---|---|
+| Sprite data not in Chip RAM | DMA can't access it — sprite invisible or garbage | Use `AllocMem(MEMF_CHIP)` for sprite data |
+| Not setting pointer every frame | Sprite DMA reads stale pointer → random data displayed | Use Copper list to reload SPRxPT |
+| Forgetting `FreeSprite` | Sprite channel stays reserved → other apps can't use it | Always free in cleanup |
+| Using sprite 0 directly | Conflicts with Intuition's mouse pointer | Use `GetSprite(-1)` to get a free one |
---
## References
- HRM: *Amiga Hardware Reference Manual* — Sprites chapter
-- NDK39: `graphics/sprite.h`
+- NDK39: `graphics/sprite.h`, `hardware/custom.h`
- ADCD 2.1: `GetSprite`, `MoveSprite`, `ChangeSprite`, `FreeSprite`
+- See also: [copper_programming.md](copper_programming.md) — Copper-driven sprite multiplexing
+- See also: [rastport.md](rastport.md) — BOBs (software sprites via Blitter)
diff --git a/08_graphics/text_fonts.md b/08_graphics/text_fonts.md
index 42a881b..6c7f58c 100644
--- a/08_graphics/text_fonts.md
+++ b/08_graphics/text_fonts.md
@@ -1,10 +1,26 @@
[← Home](../README.md) · [Graphics](README.md)
-# Text and Fonts — TextFont, TextAttr, OpenFont, Text
+# Text and Fonts — TextFont, TextAttr, Rendering
## Overview
-AmigaOS uses bitmap fonts rendered through `graphics.library`. Fonts are described by `TextAttr` (request) and `TextFont` (loaded instance). Disk fonts require `diskfont.library`.
+AmigaOS uses **bitmap fonts** rendered through `graphics.library`. Each font character is stored as a strip of pixels in a single bitmap. Fonts can be ROM-resident (topaz — always available), loaded from disk via `diskfont.library`, or generated algorithmically (bold, italic, underline).
+
+```mermaid
+flowchart LR
+ subgraph "Font Sources"
+ ROM["ROM Fonts
(topaz 8, topaz 9)"]
+ DISK["Disk Fonts
(FONTS: directory)"]
+ end
+
+ ROM -->|"OpenFont"| TF["struct TextFont"]
+ DISK -->|"OpenDiskFont"| TF
+
+ TF -->|"SetFont(rp, font)"| RP["RastPort"]
+ RP -->|"Text(rp, str, len)"| BM["BitMap
(rendered text)"]
+
+ style TF fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
@@ -12,6 +28,8 @@ AmigaOS uses bitmap fonts rendered through `graphics.library`. Fonts are describ
```c
/* graphics/text.h — NDK39 */
+
+/* Font request — describes what you want: */
struct TextAttr {
STRPTR ta_Name; /* font name, e.g. "topaz.font" */
UWORD ta_YSize; /* desired height in pixels */
@@ -19,40 +37,67 @@ struct TextAttr {
UBYTE ta_Flags; /* FPF_ROMFONT, FPF_DISKFONT, etc. */
};
+/* Loaded font instance: */
struct TextFont {
- struct Message tf_Message;
- UWORD tf_YSize; /* font height */
- UBYTE tf_Style;
+ struct Message tf_Message; /* standard message header */
+ UWORD tf_YSize; /* actual font height */
+ UBYTE tf_Style; /* styles this font has built-in */
UBYTE tf_Flags;
- UWORD tf_XSize; /* nominal width */
- UWORD tf_Baseline; /* baseline from top */
- UWORD tf_BoldSmear; /* bold smear width */
- UWORD tf_Accessors; /* open count */
- UBYTE tf_LoChar; /* first character code */
- UBYTE tf_HiChar; /* last character code */
- APTR tf_CharData; /* bitmap data */
+ UWORD tf_XSize; /* nominal character width */
+ UWORD tf_Baseline; /* pixels from top to baseline */
+ UWORD tf_BoldSmear; /* extra pixels for algorithmic bold */
+ UWORD tf_Accessors; /* current open count */
+ UBYTE tf_LoChar; /* first character code (usually 32) */
+ UBYTE tf_HiChar; /* last character code (usually 127 or 255) */
+ APTR tf_CharData; /* bitmap strip containing all glyphs */
UWORD tf_Modulo; /* bytes per row of font bitmap */
- APTR tf_CharLoc; /* character location table */
- APTR tf_CharSpace; /* proportional spacing table */
- APTR tf_CharKern; /* kerning table */
+ APTR tf_CharLoc; /* location table: offset + width per char */
+ APTR tf_CharSpace; /* proportional spacing table (NULL = fixed) */
+ APTR tf_CharKern; /* kerning adjustment table (NULL = none) */
};
```
+### Font Bitmap Layout
+
+All characters are stored in a single bitmap strip. The `tf_CharLoc` table tells the renderer where each character starts:
+
+```
+tf_CharData bitmap:
+┌──┬───┬──┬───┬──┬──┬───────────────────┐
+│A │ B │C │ D │E │F │ ... all chars ... │
+└──┴───┴──┴───┴──┴──┴───────────────────┘
+
+tf_CharLoc[ch - tf_LoChar]:
+ bits 31–16 = bit offset into tf_CharData
+ bits 15–0 = character width in pixels
+
+tf_CharSpace[ch - tf_LoChar]:
+ spacing advance (proportional fonts)
+```
+
---
## Opening Fonts
```c
-/* ROM font (topaz, always available): */
+/* ROM font (topaz — always available, no disk access): */
struct TextAttr ta = {"topaz.font", 8, 0, FPF_ROMFONT};
struct TextFont *font = OpenFont(&ta);
/* Disk font (requires diskfont.library): */
+struct Library *DiskfontBase = OpenLibrary("diskfont.library", 0);
struct TextAttr ta2 = {"helvetica.font", 24, 0, FPF_DISKFONT};
struct TextFont *font2 = OpenDiskFont(&ta2);
-/* Set in RastPort: */
+/* Request with style (may get algorithmically generated): */
+struct TextAttr ta3 = {"topaz.font", 8, FSF_BOLD, FPF_ROMFONT};
+struct TextFont *bold = OpenFont(&ta3);
+
+/* Assign to RastPort: */
SetFont(rp, font);
+
+/* Close when done: */
+CloseFont(font);
```
---
@@ -60,31 +105,111 @@ SetFont(rp, font);
## Rendering Text
```c
-Move(rp, 10, 20); /* position cursor */
-Text(rp, "Hello Amiga", 11); /* render 11 characters */
+/* Position cursor then render: */
+Move(rp, 10, 20 + rp->Font->tf_Baseline); /* baseline-relative! */
+Text(rp, "Hello Amiga", 11);
-/* Get pixel width of a string: */
-UWORD width = TextLength(rp, "Hello", 5);
+/* Measure width before rendering (for centering/alignment): */
+UWORD width = TextLength(rp, "Hello Amiga", 11);
+
+/* Centre text: */
+WORD centreX = (screenWidth - width) / 2;
+Move(rp, centreX, 100);
+Text(rp, "Hello Amiga", 11);
+
+/* Pixel-perfect extent info: */
+struct TextExtent te;
+TextExtent(rp, "Hello", 5, &te);
+/* te.te_Width = total pixel width */
+/* te.te_Height = total pixel height */
+/* te.te_Extent = bounding rectangle */
```
+> [!IMPORTANT]
+> `Text()` renders at the **current pen position**, which should be at the font's **baseline** — not the top of the character. The baseline offset is `font->tf_Baseline` pixels below the top.
+
---
-## Style Flags
+## Algorithmic Styles
```c
+/* Style flags: */
#define FSF_UNDERLINED 0x01
#define FSF_BOLD 0x02
#define FSF_ITALIC 0x04
#define FSF_EXTENDED 0x08
-/* Set algorithmic style: */
-SetSoftStyle(rp, FSF_BOLD | FSF_ITALIC,
- AskSoftStyle(rp)); /* ask which styles the font supports */
+/* Ask which styles this font supports algorithmically: */
+UWORD supported = AskSoftStyle(rp);
+
+/* Apply bold + italic: */
+SetSoftStyle(rp, FSF_BOLD | FSF_ITALIC, supported);
+Text(rp, "Bold Italic Text", 16);
+
+/* Reset to normal: */
+SetSoftStyle(rp, 0, supported);
+```
+
+| Style | Method | Notes |
+|---|---|---|
+| Bold | Smear right by `tf_BoldSmear` | Characters become slightly wider |
+| Italic | Shear top scanlines right | Fixed-angle slant |
+| Underline | Draw line at descender level | 1-pixel line below baseline |
+| Extended | Widen each character | Rarely used |
+
+---
+
+## Available Font Lists
+
+```c
+/* List all fonts available on FONTS: */
+struct AvailFontsHeader *afh;
+LONG bufSize = 4096;
+do {
+ afh = AllocMem(bufSize, MEMF_ANY);
+ LONG shortBy = AvailFonts((STRPTR)afh, bufSize,
+ AFF_DISK | AFF_MEMORY | AFF_SCALED);
+ if (shortBy > 0) {
+ FreeMem(afh, bufSize);
+ bufSize += shortBy;
+ afh = NULL;
+ }
+} while (!afh);
+
+struct AvailFonts *af = &afh->afh_AF;
+for (int i = 0; i < afh->afh_NumEntries; i++)
+{
+ Printf("Font: %s, size %ld, type %s\n",
+ af[i].af_Attr.ta_Name,
+ af[i].af_Attr.ta_YSize,
+ (af[i].af_Type & AFF_DISK) ? "disk" : "ROM");
+}
+FreeMem(afh, bufSize);
+```
+
+---
+
+## Font Preferences
+
+The system font can be changed via Preferences. Applications should respect the user's choice:
+
+```c
+/* Get the current screen font (user's preference): */
+struct TextAttr *screenFont;
+struct Preferences prefs;
+GetPrefs(&prefs, sizeof(prefs));
+/* prefs contains font info */
+
+/* Better: use the screen's font directly: */
+struct TextFont *scrFont = screen->RastPort.Font;
+SetFont(myRastPort, scrFont);
```
---
## References
-- NDK39: `graphics/text.h`
+- NDK39: `graphics/text.h`, `graphics/rastport.h`
- ADCD 2.1: `OpenFont`, `OpenDiskFont`, `SetFont`, `Text`, `TextLength`
+- See also: [diskfont.md](../11_libraries/diskfont.md) — disk font loading
+- See also: [rastport.md](rastport.md) — RastPort text rendering context
diff --git a/10_devices/README.md b/10_devices/README.md
index 5752f76..200364e 100644
--- a/10_devices/README.md
+++ b/10_devices/README.md
@@ -9,12 +9,12 @@ Amiga devices are shared libraries with an exec I/O request interface. They prov
| File | Description |
|---|---|
| [trackdisk.md](trackdisk.md) | Floppy disk DMA: MFM encoding, track format, disk geometry, track caching, direct HW access |
-| [scsi.md](scsi.md) | scsi.device / 2nd.scsi.device — hard disk I/O |
+| [scsi.md](scsi.md) | Hard disk/CD-ROM I/O: per-model interfaces, Gayle bandwidth limits, native vs vendor drivers, HD_SCSICMD, CD-ROM commands, TD64/NSD 64-bit |
| [serial.md](serial.md) | UART/RS-232: CIA registers, baud rate calculation, serial debugging (KPrintF) |
-| [parallel.md](parallel.md) | parallel.device — Centronics parallel port |
-| [timer.md](timer.md) | CIA timers, E-clock, VBlank: delays, ReadEClock, periodic patterns, signal-based waiting |
+| [parallel.md](parallel.md) | Centronics parallel port: CIA-A Port B mapping, hardware pinout, direct register access |
+| [timer.md](timer.md) | CIA timers, E-clock, VBlank: delays, ReadEClock, periodic patterns, signal multiplexing, resource exhaustion |
| [audio.md](audio.md) | 4-channel DMA audio: hardware registers, priority allocation, double-buffering, modulation |
| [keyboard.md](keyboard.md) | CIA-A serial handshake, raw key codes, key matrix, reset sequence, FPGA protocol notes |
-| [gameport.md](gameport.md) | gameport.device — joystick/mouse port reading |
+| [gameport.md](gameport.md) | Joystick/mouse port: quadrature decoding, XOR state, fire buttons, controller types |
| [input.md](input.md) | Input handler chain: priority dispatch, event classes, key remapping, Commodities Exchange |
-| [console.md](console.md) | console.device — text terminal I/O |
+| [console.md](console.md) | Text terminal I/O: ANSI escape sequences, cursor/colour control, raw key events, CON:/RAW: handlers |
diff --git a/10_devices/console.md b/10_devices/console.md
index 0d926c9..6e347f2 100644
--- a/10_devices/console.md
+++ b/10_devices/console.md
@@ -4,44 +4,241 @@
## Overview
-`console.device` provides ANSI-compatible text rendering into Intuition windows. It translates raw keycodes to ASCII and supports a rich set of escape sequences for cursor positioning, colour, and text formatting.
+`console.device` provides an ANSI-compatible text terminal within Intuition windows. It handles:
+- **Input**: translating raw keycodes from `input.device` into ASCII/ANSI characters
+- **Output**: rendering text, parsing escape sequences for cursor control, colour, and formatting
+- **Clipboard**: cut/copy/paste integration
+
+Every CLI/Shell window is backed by a console.device unit. Applications can open their own console units in any Intuition window for text I/O without implementing their own keyboard translation or cursor rendering.
+
+```mermaid
+flowchart LR
+ KB["Keyboard
(raw keycodes)"] --> INPUT["input.device"]
+ INPUT --> CON["console.device"]
+ CON -->|"Read: ASCII chars"| APP["Application"]
+ APP -->|"Write: text + ESC sequences"| CON
+ CON -->|"Renders text via
RastPort drawing"| WIN["Intuition Window"]
+
+ style CON fill:#e8f4fd,stroke:#2196f3,color:#333
+ style WIN fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
---
## Opening
```c
-struct IOStdReq *con = CreateStdIO(port);
-con->io_Data = (APTR)win; /* the Intuition Window */
+struct MsgPort *conPort = CreateMsgPort();
+struct IOStdReq *con = (struct IOStdReq *)
+ CreateIORequest(conPort, sizeof(struct IOStdReq));
+
+/* Attach to an Intuition window: */
+con->io_Data = (APTR)window;
con->io_Length = sizeof(struct Window);
-OpenDevice("console.device", 0, (struct IORequest *)con, 0);
+
+if (OpenDevice("console.device", CONU_STANDARD, (struct IORequest *)con, 0))
+{
+ /* error — can't open console */
+}
+```
+
+### Unit Types
+
+| Unit | Constant | Description |
+|---|---|---|
+| 0 | `CONU_STANDARD` | Full-feature console with cursor and scrolling |
+| 1 | `CONU_CHARMAP` | Character-mapped console (OS 3.0+) — faster for full-screen updates |
+| 3 | `CONU_SNIPMAP` | Snip-mapped: supports clipboard cut/paste (OS 3.0+) |
+| -1 | `CONU_LIBRARY` | Library mode — no window, just keymap translation |
+
+---
+
+## Writing Text and Escape Sequences
+
+```c
+/* Write text to the console window: */
+void ConPuts(struct IOStdReq *con, char *str)
+{
+ con->io_Command = CMD_WRITE;
+ con->io_Data = (APTR)str;
+ con->io_Length = -1; /* -1 = null-terminated */
+ DoIO((struct IORequest *)con);
+}
+
+/* Usage: */
+ConPuts(con, "Hello, Amiga!\n");
+ConPuts(con, "\033[1mBold text\033[0m\n"); /* bold on/off */
+ConPuts(con, "\033[33mYellow text\033[0m\n"); /* colour */
+ConPuts(con, "\033[10;20HText at row 10 col 20"); /* absolute position */
```
---
-## Common Escape Sequences
+## Reading Input
+
+```c
+/* Read characters (blocking): */
+char buffer[256];
+con->io_Command = CMD_READ;
+con->io_Data = (APTR)buffer;
+con->io_Length = sizeof(buffer);
+DoIO((struct IORequest *)con);
+/* con->io_Actual = number of bytes read */
+
+/* Non-blocking read via SendIO + WaitPort: */
+con->io_Command = CMD_READ;
+con->io_Data = (APTR)buffer;
+con->io_Length = 1; /* read 1 char at a time */
+SendIO((struct IORequest *)con);
+
+/* Wait for input alongside other events: */
+ULONG consoleSig = 1 << conPort->mp_SigBit;
+ULONG windowSig = 1 << window->UserPort->mp_SigBit;
+
+ULONG sigs = Wait(consoleSig | windowSig);
+if (sigs & consoleSig)
+{
+ WaitIO((struct IORequest *)con);
+ char ch = buffer[0];
+ /* process character... */
+}
+```
+
+---
+
+## ANSI Escape Sequences
+
+Console.device supports a rich subset of ANSI/VT100 escape sequences (CSI = `\033[` = ESC + `[`):
+
+### Cursor Movement
+
+| Sequence | Description | Example |
+|---|---|---|
+| `\033[nA` | Cursor up n lines | `\033[5A` = up 5 |
+| `\033[nB` | Cursor down n lines | |
+| `\033[nC` | Cursor right n columns | |
+| `\033[nD` | Cursor left n columns | |
+| `\033[y;xH` | Move to row y, column x (1-based) | `\033[1;1H` = home |
+| `\033[H` | Home cursor (top-left) | |
+| `\033[6n` | Report cursor position → replies `\033[y;xR` | |
+| `\033[s` | Save cursor position | |
+| `\033[u` | Restore cursor position | |
+
+### Erasing
| Sequence | Description |
|---|---|
-| `\033[H` | Home cursor |
-| `\033[nA` | Cursor up n lines |
-| `\033[nB` | Cursor down n lines |
-| `\033[nC` | Cursor right n columns |
-| `\033[nD` | Cursor left n columns |
-| `\033[y;xH` | Move to row y, column x |
-| `\033[J` | Clear to end of screen |
-| `\033[K` | Clear to end of line |
-| `\033[nm` | Set graphics rendition (colour/style) |
-| `\033[0m` | Reset attributes |
-| `\033[1m` | Bold |
-| `\033[3m` | Italic |
-| `\033[4m` | Underline |
-| `\033[30–37m` | Foreground colour |
-| `\033[40–47m` | Background colour |
+| `\033[J` | Clear from cursor to end of screen |
+| `\033[1J` | Clear from start of screen to cursor |
+| `\033[2J` | Clear entire screen |
+| `\033[K` | Clear from cursor to end of line |
+| `\033[1K` | Clear from start of line to cursor |
+| `\033[2K` | Clear entire line |
+
+### Text Attributes (SGR)
+
+| Sequence | Effect |
+|---|---|
+| `\033[0m` | Reset all attributes |
+| `\033[1m` | **Bold** (high intensity) |
+| `\033[3m` | *Italic* |
+| `\033[4m` | Underline |
+| `\033[7m` | Inverse video (swap fg/bg) |
+| `\033[22m` | Normal intensity (cancel bold) |
+| `\033[23m` | Cancel italic |
+| `\033[24m` | Cancel underline |
+
+### Colours
+
+| Sequence | Foreground | Background |
+|---|---|---|
+| `\033[30m` / `\033[40m` | Black | Black |
+| `\033[31m` / `\033[41m` | Red | Red |
+| `\033[32m` / `\033[42m` | Green | Green |
+| `\033[33m` / `\033[43m` | Yellow/Brown | Yellow/Brown |
+| `\033[34m` / `\033[44m` | Blue | Blue |
+| `\033[35m` / `\033[45m` | Magenta | Magenta |
+| `\033[36m` / `\033[46m` | Cyan | Cyan |
+| `\033[37m` / `\033[47m` | White | White |
+| `\033[39m` / `\033[49m` | Default | Default |
+
+> [!NOTE]
+> Colour indices map to the **Intuition pen palette** of the window's screen, not absolute colours. Pen 0 = background, pen 1 = foreground by default.
+
+### Amiga-Specific Extensions
+
+| Sequence | Description |
+|---|---|
+| `\033[>1h` | Enable auto-scroll |
+| `\033[>1l` | Disable auto-scroll |
+| `\033[ p` | Enable cursor |
+| `\033[0 p` | Disable cursor |
+| `\033[t` / `\033[b` | Set top/bottom scroll margins |
+| `\033[20h` | Linefeed mode (LF = CR+LF) |
+
+---
+
+## Raw Key Events
+
+In addition to ASCII, console.device reports **special keys** as multi-byte escape sequences:
+
+| Key | Sequence Received |
+|---|---|
+| Cursor Up | `\033[A` |
+| Cursor Down | `\033[B` |
+| Cursor Right | `\033[C` |
+| Cursor Left | `\033[D` |
+| Shift+Up | `\033[T` |
+| Shift+Down | `\033[S` |
+| F1–F10 | `\033[0~` – `\033[9~` |
+| Shift+F1–F10 | `\033[10~` – `\033[19~` |
+| Help | `\033[?~` |
+
+---
+
+## Proper Shutdown
+
+```c
+/* Must abort any pending read before closing: */
+if (!CheckIO((struct IORequest *)con))
+{
+ AbortIO((struct IORequest *)con);
+ WaitIO((struct IORequest *)con);
+}
+CloseDevice((struct IORequest *)con);
+DeleteIORequest((struct IORequest *)con);
+DeleteMsgPort(conPort);
+```
+
+---
+
+## CON: and RAW: Handlers
+
+The AmigaDOS file handlers `CON:` and `RAW:` are wrappers around console.device:
+
+| Handler | Description |
+|---|---|
+| `CON:` | Line-buffered console — input is buffered until Enter is pressed. Supports line editing. |
+| `RAW:` | Raw console — each keypress is delivered immediately. No line editing. |
+
+```c
+/* Open a CON: window from DOS: */
+BPTR fh = Open("CON:0/0/640/200/My Window/CLOSE", MODE_OLDFILE);
+FPuts(fh, "Type something: ");
+char buf[80];
+FGets(fh, buf, sizeof(buf));
+Close(fh);
+
+/* RAW: for unbuffered key-by-key input: */
+BPTR raw = Open("RAW:0/0/640/200/Raw Input", MODE_OLDFILE);
+/* Each Read returns immediately with 1 char */
+```
---
## References
-- NDK39: `devices/conunit.h`
+- NDK39: `devices/conunit.h`, `devices/console.h`
- ADCD 2.1: console.device autodocs
+- See also: [keyboard.md](keyboard.md) — raw keycode to console.device pipeline
+- See also: [input.md](input.md) — input handler chain
diff --git a/10_devices/gameport.md b/10_devices/gameport.md
index d127f01..90eb5b1 100644
--- a/10_devices/gameport.md
+++ b/10_devices/gameport.md
@@ -1,64 +1,130 @@
[← Home](../README.md) · [Devices](README.md)
-# gameport.device — Joystick and Mouse
+# gameport.device — Joystick and Mouse Ports
## Overview
-`gameport.device` reads joystick and mouse ports (active on port 1 for joystick, port 0 for mouse). Uses CIA and custom chip registers.
+`gameport.device` provides access to the Amiga's two **controller ports** (mouse/joystick). The hardware reads controller state through custom chip registers `JOY0DAT`/`JOY1DAT` and the CIA-A port for fire buttons. Understanding the hardware protocol is critical for FPGA cores.
+
+---
+
+## Hardware Registers
+
+| Register | Address | Controller | Function |
+|---|---|---|---|
+| `JOY0DAT` | `$DFF00A` | Port 1 (mouse) | Quadrature-encoded position data |
+| `JOY1DAT` | `$DFF00C` | Port 2 (joystick) | Direction bits or quadrature data |
+| `JOYTEST` | `$DFF036` | Both | Write to set counter test value |
+| `POTGO` | `$DFF034` | Both | Proportional/paddle port control |
+| `POTGOR` | `$DFF016` | Both | Read paddle/proportional values |
+| CIA-A PRA | `$BFE001` | Both | Fire buttons (active low) |
+
+### JOYxDAT Bit Layout
+
+```
+Bits 15–8: Y counter (or vertical quadrature)
+Bits 7–0: X counter (or horizontal quadrature)
+```
+
+### Mouse Quadrature Decoding
+
+For mouse input, the X/Y values are **quadrature counters** — they increment/decrement as the mouse moves:
+
+```c
+/* Read mouse delta: */
+UWORD joy = custom->joy0dat; /* current counter */
+WORD dx = (WORD)(joy & 0xFF) - (WORD)(prev_joy & 0xFF);
+WORD dy = (WORD)(joy >> 8) - (WORD)(prev_joy >> 8);
+/* dx/dy = movement since last read (signed, wraps at 255→0) */
+prev_joy = joy;
+```
+
+### Joystick Digital Decoding
+
+For digital joysticks, the direction bits are encoded:
+
+```c
+UWORD joy = custom->joy1dat;
+BOOL right = joy & 0x0002;
+BOOL left = (joy >> 1) ^ (joy & 0x0001); /* XOR of bit 1 and bit 0 */
+BOOL down = joy & 0x0200;
+BOOL up = (joy >> 9) ^ ((joy >> 8) & 0x0001);
+
+/* Fire button — from CIA-A: */
+BOOL fire = !(ciaa->ciapra & 0x80); /* port 2 button, active low */
+/* Port 1 fire = bit 6 of CIAA PRA */
+```
+
+> [!IMPORTANT]
+> The joystick directional decoding uses **XOR** between adjacent bits — not direct reads. This is because the hardware shares the same quadrature interface used for mice. This is a common source of bugs in FPGA implementations.
+
+### Fire Buttons
+
+| Button | Register | Bit | Port |
+|---|---|---|---|
+| Port 1 fire (left mouse) | CIA-A PRA `$BFE001` | 6 | Mouse port |
+| Port 2 fire (joystick) | CIA-A PRA `$BFE001` | 7 | Joystick port |
+| Port 1 middle/right | POTGOR `$DFF016` | 8, 10 | Mouse port |
+| Port 2 second button | POTGOR `$DFF016` | 12, 14 | Joystick port |
+
+---
+
+## Using gameport.device (OS Level)
+
+```c
+struct MsgPort *gpPort = CreateMsgPort();
+struct IOStdReq *gpReq = (struct IOStdReq *)
+ CreateIORequest(gpPort, sizeof(struct IOStdReq));
+
+/* Open port 1 (joystick port): */
+OpenDevice("gameport.device", 1, (struct IORequest *)gpReq, 0);
+
+/* Set controller type: */
+UBYTE type = GPCT_ABSJOYSTICK;
+gpReq->io_Command = GPD_SETCTYPE;
+gpReq->io_Data = (APTR)&type;
+gpReq->io_Length = 1;
+DoIO((struct IORequest *)gpReq);
+
+/* Set trigger conditions: */
+struct GamePortTrigger trigger;
+trigger.gpt_Keys = GPTF_UPKEYS | GPTF_DOWNKEYS;
+trigger.gpt_Timeout = 0; /* no timeout */
+trigger.gpt_XDelta = 1; /* report on any movement */
+trigger.gpt_YDelta = 1;
+gpReq->io_Command = GPD_SETTRIGGER;
+gpReq->io_Data = (APTR)&trigger;
+gpReq->io_Length = sizeof(trigger);
+DoIO((struct IORequest *)gpReq);
+
+/* Read events: */
+struct InputEvent ie;
+gpReq->io_Command = GPD_READEVENT;
+gpReq->io_Data = (APTR)&ie;
+gpReq->io_Length = sizeof(ie);
+SendIO((struct IORequest *)gpReq);
+
+/* Wait for joystick event: */
+Wait(1L << gpPort->mp_SigBit);
+WaitIO((struct IORequest *)gpReq);
+/* ie now contains joystick movement/button data */
+```
---
## Controller Types
-```c
-/* devices/gameport.h */
-#define GPCT_ALLOCATED -1 /* port is allocated */
-#define GPCT_NOCONTROLLER 0 /* nothing connected */
-#define GPCT_MOUSE 1 /* mouse */
-#define GPCT_RELJOYSTICK 2 /* relative joystick (proportional) */
-#define GPCT_ABSJOYSTICK 3 /* absolute joystick */
-```
-
----
-
-## Reading a Joystick
-
-```c
-struct IOStdReq *gp = CreateStdIO(port);
-OpenDevice("gameport.device", 1, (struct IORequest *)gp, 0);
-
-/* Set controller type: */
-UBYTE type = GPCT_ABSJOYSTICK;
-gp->io_Command = GPD_SETCTYPE;
-gp->io_Data = &type;
-gp->io_Length = 1;
-DoIO((struct IORequest *)gp);
-
-/* Set trigger conditions: */
-struct GamePortTrigger trigger = {
- GPTF_UPKEYS | GPTF_DOWNKEYS, /* report buttons */
- 10000, /* X delta timeout */
- 10000, /* Y delta timeout */
- 1, 1 /* X/Y delta threshold */
-};
-gp->io_Command = GPD_SETTRIGGER;
-gp->io_Data = &trigger;
-gp->io_Length = sizeof(trigger);
-DoIO((struct IORequest *)gp);
-
-/* Read events: */
-struct InputEvent ie;
-gp->io_Command = GPD_READEVENT;
-gp->io_Data = &ie;
-gp->io_Length = sizeof(ie);
-DoIO((struct IORequest *)gp);
-/* ie.ie_Code, ie.ie_position.ie_xy give button/direction */
-
-CloseDevice((struct IORequest *)gp);
-```
+| Constant | Value | Device |
+|---|---|---|
+| `GPCT_MOUSE` | 1 | Standard Amiga mouse |
+| `GPCT_RELJOYSTICK` | 2 | Relative joystick (proportional) |
+| `GPCT_ABSJOYSTICK` | 3 | Absolute/digital joystick |
---
## References
-- NDK39: `devices/gameport.h`
+- NDK39: `devices/gameport.h`, `hardware/custom.h`, `hardware/cia.h`
+- HRM: *Amiga Hardware Reference Manual* — Controller Ports chapter
+- See also: [input.md](input.md) — input handler chain that receives gameport events
+- See also: [keyboard.md](keyboard.md) — keyboard shares CIA-A interrupt infrastructure
diff --git a/10_devices/parallel.md b/10_devices/parallel.md
index ecd32be..4760bca 100644
--- a/10_devices/parallel.md
+++ b/10_devices/parallel.md
@@ -4,33 +4,146 @@
## Overview
-`parallel.device` provides I/O for the Amiga's Centronics-compatible parallel port, used primarily for printers.
+`parallel.device` provides I/O for the Amiga's Centronics-compatible 25-pin parallel port. Primarily used for printers, it also serves as a general-purpose 8-bit data port for hardware projects, dongles, MIDI interfaces, and inter-machine transfers (Laplink-style).
+
+The parallel port is directly connected to **CIA-A** Port B data register (`$BFE101`) with handshake lines on CIA-A and CIA-B.
+
+```mermaid
+flowchart LR
+ APP["Application"] -->|"CMD_WRITE
CMD_READ"| PAR["parallel.device"]
+ PAR --> CIA["CIA-A Port B
($BFE101)
8 data bits"]
+ CIA --> PORT["DB-25 Connector
(pins 2-9 = data)"]
+ PAR --> CIAB["CIA-B
Handshake lines"]
+
+ style PAR fill:#e8f4fd,stroke:#2196f3,color:#333
+ style CIA fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+---
+
+## Hardware Pinout
+
+| Pin | Signal | Direction | Description |
+|---|---|---|---|
+| 1 | /STROBE | Output | Data strobe (active low) |
+| 2–9 | D0–D7 | I/O | 8-bit bidirectional data |
+| 10 | /ACK | Input | Acknowledge from printer |
+| 11 | BUSY | Input | Printer busy |
+| 12 | POUT | Input | Paper out |
+| 13 | SELECT | Input | Printer selected |
+| 14–17 | — | — | Reserved / unused |
+| 18–25 | GND | — | Ground |
---
## Opening
```c
+struct MsgPort *parPort = CreateMsgPort();
struct IOExtPar *par = (struct IOExtPar *)
- CreateIORequest(port, sizeof(struct IOExtPar));
-OpenDevice("parallel.device", 0, (struct IORequest *)par, 0);
+ CreateIORequest(parPort, sizeof(struct IOExtPar));
+
+if (OpenDevice("parallel.device", 0, (struct IORequest *)par, 0))
+{
+ Printf("Cannot open parallel.device\n");
+ /* Port may be in use by another application (printer, etc.) */
+}
```
+> [!NOTE]
+> Only **one** application can have the parallel port open at a time. If the port is busy (e.g., printer spooler has it), `OpenDevice` will fail. This is unlike serial.device which supports shared modes.
+
---
## Commands
| Code | Constant | Description |
|---|---|---|
-| 2 | `CMD_READ` | Read bytes (if bidirectional) |
-| 3 | `CMD_WRITE` | Write bytes |
-| 5 | `CMD_CLEAR` | Clear buffers |
-| 8 | `CMD_FLUSH` | Abort pending |
-| 9 | `PDCMD_QUERY` | Get status |
-| 10 | `PDCMD_SETPARAMS` | Set parameters |
+| 2 | `CMD_READ` | Read bytes (bidirectional mode) |
+| 3 | `CMD_WRITE` | Write bytes to the port |
+| 5 | `CMD_CLEAR` | Clear internal buffers |
+| 7 | `CMD_RESET` | Reset the device |
+| 8 | `CMD_FLUSH` | Abort all pending I/O |
+| 9 | `PDCMD_QUERY` | Get port status (busy, paper out, etc.) |
+| 10 | `PDCMD_SETPARAMS` | Configure port parameters |
+
+### Writing Data
+
+```c
+/* Send data to the parallel port: */
+char printData[] = "Hello, Printer!\r\n";
+par->IOPar.io_Command = CMD_WRITE;
+par->IOPar.io_Data = printData;
+par->IOPar.io_Length = sizeof(printData) - 1;
+DoIO((struct IORequest *)par);
+
+if (par->IOPar.io_Error)
+ Printf("Write error: %ld\n", par->IOPar.io_Error);
+```
+
+### Querying Status
+
+```c
+par->IOPar.io_Command = PDCMD_QUERY;
+DoIO((struct IORequest *)par);
+
+UBYTE status = par->io_Status;
+if (status & IOPTF_PARBUSY) Printf("Printer busy\n");
+if (status & IOPTF_PAPEROUT) Printf("Paper out\n");
+if (status & IOPTF_PARSEL) Printf("Printer selected\n");
+```
+
+### Setting Parameters
+
+```c
+/* Configure for custom use (e.g., no handshaking): */
+par->IOPar.io_Command = PDCMD_SETPARAMS;
+par->io_ParFlags = PARF_SHARED; /* shared access (OS 2.0+) */
+DoIO((struct IORequest *)par);
+```
+
+---
+
+## Direct Hardware Access
+
+For hardware projects that need precise control (bypassing the device driver):
+
+```c
+/* Direct CIA-A Port B access: */
+volatile UBYTE *ciaaPrb = (UBYTE *)0xBFE101; /* data register */
+volatile UBYTE *ciaaDdrb = (UBYTE *)0xBFE301; /* data direction */
+
+/* Set all 8 bits as output: */
+*ciaaDdrb = 0xFF;
+
+/* Write a byte: */
+*ciaaPrb = 0x42;
+
+/* Set as input: */
+*ciaaDdrb = 0x00;
+UBYTE value = *ciaaPrb;
+```
+
+> [!CAUTION]
+> Direct hardware access bypasses the OS completely. Do not mix direct register access with parallel.device operations — close the device first or don't open it at all.
+
+---
+
+## Common Uses
+
+| Use Case | Method |
+|---|---|
+| Printer output | `CMD_WRITE` through printer.device (which opens parallel.device) |
+| Hardware dongle | Direct CIA-A Port B register read |
+| MIDI interface | Parallel-to-MIDI adapter with custom timing |
+| Amiga-to-PC transfer | Laplink cable with ParNet or ParBench software |
+| Sampling hardware | ADC/DAC connected to data lines |
---
## References
- NDK39: `devices/parallel.h`
+- ADCD 2.1: parallel.device autodocs
+- See also: [serial.md](serial.md) — serial port I/O
+- See also: [cia_chips.md](../01_hardware/common/cia_chips.md) — CIA register details
diff --git a/10_devices/scsi.md b/10_devices/scsi.md
index c391deb..1e677c0 100644
--- a/10_devices/scsi.md
+++ b/10_devices/scsi.md
@@ -1,66 +1,283 @@
[← Home](../README.md) · [Devices](README.md)
-# scsi.device — Hard Disk I/O
+# scsi.device — Hard Disk, CD-ROM, and Mass Storage I/O
## Overview
-SCSI and IDE hard disks are accessed via `scsi.device` (A3000/A4000 built-in) or `2nd.scsi.device` / `ide.device` (A1200/A600). The API is the same as trackdisk for basic read/write, with additional SCSI direct commands.
+SCSI and IDE hard disks on the Amiga are accessed through `scsi.device` or compatible device drivers. The API is consistent across implementations — the same IORequest structure and commands work regardless of whether the underlying hardware is Commodore's native Gayle IDE, a Zorro SCSI card, or an accelerator-integrated controller.
+
+---
+
+## Disk Interfaces by Amiga Model
+
+| Model | Interface | Controller | Device Name | Bandwidth Limit |
+|---|---|---|---|---|
+| A500 | None (stock) | — | — | Requires external SCSI card |
+| A500+ | None (stock) | — | — | Requires external SCSI card |
+| A600 | IDE (44-pin) | **Gayle** | `scsi.device` | ~1.5 MB/s (PIO, 16-bit CIA) |
+| A1000 | None (stock) | — | — | Requires SCSI sidecar |
+| A1200 | IDE (44-pin) | **Gayle** | `scsi.device` | ~1.5 MB/s (PIO) |
+| A2000 | None (stock) | — | — | Zorro II SCSI cards |
+| A3000 | SCSI (50-pin) | **WD33C93** | `scsi.device` | ~3 MB/s (DMA) |
+| A4000 | IDE (40-pin) | **A4000 IDE** | `scsi.device` | ~2 MB/s (PIO) |
+| A4000T | SCSI (50-pin) + IDE | **NCR 53C710** + IDE | `2nd.scsi.device` / `scsi.device` | ~10 MB/s (SCSI DMA) |
+| CD32 | IDE (internal CD) | **Akiko** | `scsi.device` | ~1.5 MB/s |
+| CDTV | SCSI (internal CD) | Custom | `scsi.device` | Slow |
+
+### Why Native Bandwidth Is Limited
+
+The A600/A1200 **Gayle** IDE interface is the most common and the most constrained:
+
+```mermaid
+flowchart LR
+ CPU["68020/68030
(fast bus)"] -->|"PIO transfer
WORD at a time"| GAYLE["Gayle IDE
(no DMA!)"]
+ GAYLE -->|"16-bit ATA"| DRIVE["IDE Drive"]
+
+ style GAYLE fill:#ffcdd2,stroke:#c62828,color:#333
+```
+
+| Bottleneck | Explanation |
+|---|---|
+| **No DMA** | Gayle has no DMA engine — every word must be moved by the CPU (`MOVE.W` loop) |
+| **16-bit bus** | Gayle connects via 16-bit data path even on 32-bit CPUs |
+| **PIO mode 0** | Stock Gayle supports only PIO mode 0 (~3.3 MB/s theoretical, ~1.5 MB/s actual) |
+| **CIA timing** | CIA chip access introduces wait states |
+| **CPU overhead** | 100% CPU utilisation during transfers — no multitasking during disk I/O |
+
+### Community Solutions — Fast IDE
+
+| Solution | Method | Improvement |
+|---|---|---|
+| **FastATA** (E-Matrix/Elbox) | Zorro SCSI/IDE card with DMA | Up to ~10 MB/s, frees CPU |
+| **Buddha/Catweasel** | Clock port / Zorro IDE with optimised driver | ~2–3 MB/s, multiple IDE channels |
+| **Blizzard SCSI** | Accelerator-integrated SCSI | Up to ~5 MB/s with DMA |
+| **GVP Series II** | Zorro II SCSI with custom DMA ASIC | ~3 MB/s, DMA frees CPU |
+| **A4091** | Zorro III SCSI (NCR 53C710) | **~10 MB/s** — fastest stock Amiga SCSI |
+| **CF adapters** | CompactFlash in IDE socket | Same speed, but no mechanical seek latency |
+| **SD/microSD** | Adapter on clock port | Varies, typically slow but no noise |
+
+---
+
+## Native vs. Vendor Drivers — Software Differences
+
+All drivers expose the same `CMD_READ`/`CMD_WRITE`/`HD_SCSICMD` API, but internal differences matter:
+
+| Aspect | Commodore `scsi.device` | Vendor Drivers (GVP, A4091, etc.) |
+|---|---|---|
+| **Source** | Commodore ROM or Devs: | Third-party (ships with hardware) |
+| **DMA support** | None (Gayle PIO) | Often DMA-capable |
+| **Interrupt handling** | Level 2 interrupt (CIA) | Card-specific interrupt level |
+| **TD64 / NSD** | Added via patches | Often built-in from factory |
+| **Multi-LUN** | Usually ignored | Proper LUN scanning on SCSI |
+| **Disconnect/reselect** | Not supported (Gayle) | SCSI disconnect on good controllers |
+| **Tagged queuing** | No | Some advanced SCSI controllers |
+| **CD-ROM support** | Basic (atapi.device) | Full ATAPI/SCSI CD support |
+| **Removable media** | Basic change detection | Proper unit attention handling |
+
+> [!IMPORTANT]
+> When writing software that accesses disks, **always use the device name from the mountlist** — don't hardcode `scsi.device`. Different systems use different device names for the same physical drive.
---
## Opening
```c
-struct IOStdReq *scsi = CreateStdIO(port);
-OpenDevice("scsi.device", 0, (struct IORequest *)scsi, 0);
-/* unit 0 = first SCSI/IDE device */
+struct MsgPort *diskPort = CreateMsgPort();
+struct IOStdReq *diskReq = (struct IOStdReq *)
+ CreateIORequest(diskPort, sizeof(struct IOStdReq));
+
+/* Unit 0 = first device on the bus (master/ID 0): */
+if (OpenDevice("scsi.device", 0, (struct IORequest *)diskReq, 0))
+{
+ Printf("Cannot open scsi.device unit 0\n");
+}
```
+### Units
+
+| Unit | SCSI Meaning | IDE Meaning |
+|---|---|---|
+| 0 | SCSI ID 0 | Master |
+| 1 | SCSI ID 1 | Slave |
+| 2–6 | SCSI ID 2–6 | N/A |
+| 7 | Host adapter (reserved) | N/A |
+
---
## Standard Commands
-Same as trackdisk: `CMD_READ`, `CMD_WRITE`, `CMD_UPDATE`, `TD_CHANGENUM`, etc.
+```c
+/* Read 512 bytes from byte offset on disk: */
+diskReq->io_Command = CMD_READ;
+diskReq->io_Data = buffer;
+diskReq->io_Length = 512;
+diskReq->io_Offset = 0; /* byte offset */
+DoIO((struct IORequest *)diskReq);
+
+/* Write: */
+diskReq->io_Command = CMD_WRITE;
+diskReq->io_Data = buffer;
+diskReq->io_Length = 512;
+diskReq->io_Offset = 512;
+DoIO((struct IORequest *)diskReq);
+
+/* Flush write cache: */
+diskReq->io_Command = CMD_UPDATE;
+DoIO((struct IORequest *)diskReq);
+```
+
+> [!IMPORTANT]
+> The `io_Offset` field is a `ULONG` (32-bit), limiting addressable space to **4 GB**. For larger drives, use TD64/NSD 64-bit commands.
---
## Direct SCSI Commands (HD_SCSICMD)
+For operations beyond read/write — device identification, mode pages, CD-ROM commands:
+
```c
+#include
+
struct SCSICmd scsicmd;
-UBYTE cdb[10]; /* SCSI CDB */
-UBYTE sense[20]; /* sense data buffer */
-UBYTE data[512]; /* data buffer */
+UBYTE cdb[10], sense[20], data[512];
-/* Read 1 sector at LBA 0: */
-cdb[0] = 0x28; /* READ(10) */
-cdb[1] = 0;
-cdb[2] = 0; cdb[3] = 0; cdb[4] = 0; cdb[5] = 0; /* LBA = 0 */
-cdb[6] = 0;
-cdb[7] = 0; cdb[8] = 1; /* transfer length = 1 sector */
-cdb[9] = 0;
+/* READ(10) — read 1 sector at LBA 0: */
+memset(cdb, 0, sizeof(cdb));
+cdb[0] = 0x28; /* READ(10) opcode */
+cdb[7] = 0; cdb[8] = 1; /* transfer length = 1 sector */
-scsicmd.scsi_Data = (UWORD *)data;
-scsicmd.scsi_Length = 512;
-scsicmd.scsi_Command = cdb;
-scsicmd.scsi_CmdLength = 10;
-scsicmd.scsi_Flags = SCSIF_READ;
-scsicmd.scsi_SenseData = sense;
+scsicmd.scsi_Data = (UWORD *)data;
+scsicmd.scsi_Length = 512;
+scsicmd.scsi_Command = cdb;
+scsicmd.scsi_CmdLength = 10;
+scsicmd.scsi_Flags = SCSIF_READ | SCSIF_AUTOSENSE;
+scsicmd.scsi_SenseData = sense;
scsicmd.scsi_SenseLength = sizeof(sense);
-scsi->io_Command = HD_SCSICMD;
-scsi->io_Data = &scsicmd;
-scsi->io_Length = sizeof(scsicmd);
-DoIO((struct IORequest *)scsi);
+diskReq->io_Command = HD_SCSICMD;
+diskReq->io_Data = &scsicmd;
+diskReq->io_Length = sizeof(scsicmd);
+DoIO((struct IORequest *)diskReq);
-if (scsicmd.scsi_Status != 0) {
- /* SCSI error — check sense data */
-}
+if (scsicmd.scsi_Status != 0)
+ Printf("SCSI error: status=%ld, sense key=%ld\n",
+ scsicmd.scsi_Status, sense[2] & 0x0F);
```
+### Common SCSI Commands
+
+| Opcode | Name | CDB | Description |
+|---|---|---|---|
+| `0x00` | TEST UNIT READY | 6 | Check device readiness |
+| `0x03` | REQUEST SENSE | 6 | Get error details |
+| `0x12` | INQUIRY | 6 | Device identification |
+| `0x1A` | MODE SENSE(6) | 6 | Read device parameters |
+| `0x25` | READ CAPACITY | 10 | Get total sectors and sector size |
+| `0x28` | READ(10) | 10 | Read sectors (32-bit LBA) |
+| `0x2A` | WRITE(10) | 10 | Write sectors |
+| `0x35` | SYNCHRONIZE CACHE | 10 | Flush write cache |
+| `0x43` | READ TOC | 10 | Read CD table of contents |
+| `0xBE` | READ CD | 12 | Read CD sector (any format) |
+
+---
+
+## CD-ROM Specifics
+
+CD-ROM drives require special handling — they use ATAPI (IDE) or SCSI commands but with CD-specific extensions:
+
+### Opening a CD-ROM
+
+```c
+/* CD-ROM is typically on a different device or unit: */
+/* A1200 + ATAPI CD: */
+OpenDevice("scsi.device", 2, req, 0); /* unit 2 = slave on second channel */
+
+/* External SCSI CD: */
+OpenDevice("2nd.scsi.device", 3, req, 0); /* SCSI ID 3 */
+
+/* AmiCDFS or CacheCDFS handles mounting automatically via:
+ DEVS:DOSDrivers/CD0 mountlist entry */
+```
+
+### Reading the Table of Contents
+
+```c
+UBYTE tocData[804]; /* max TOC size */
+memset(cdb, 0, 10);
+cdb[0] = 0x43; /* READ TOC */
+cdb[6] = 1; /* starting track */
+cdb[7] = (sizeof(tocData) >> 8) & 0xFF;
+cdb[8] = sizeof(tocData) & 0xFF;
+
+scsicmd.scsi_Data = (UWORD *)tocData;
+scsicmd.scsi_Length = sizeof(tocData);
+scsicmd.scsi_Command = cdb;
+scsicmd.scsi_CmdLength = 10;
+scsicmd.scsi_Flags = SCSIF_READ | SCSIF_AUTOSENSE;
+DoIO((struct IORequest *)diskReq);
+```
+
+### Audio CD Playback
+
+```c
+/* PLAY AUDIO MSF — play from start to end: */
+memset(cdb, 0, 10);
+cdb[0] = 0x47; /* PLAY AUDIO MSF */
+cdb[3] = 0; /* start minute */
+cdb[4] = 2; /* start second */
+cdb[5] = 0; /* start frame */
+cdb[6] = 60; /* end minute */
+cdb[7] = 0; /* end second */
+cdb[8] = 0; /* end frame */
+
+scsicmd.scsi_Flags = SCSIF_READ;
+DoIO((struct IORequest *)diskReq);
+```
+
+### CD Filesystem Drivers
+
+| Driver | Type | Features |
+|---|---|---|
+| **AmiCDFS** | Commodore (stock) | Basic ISO 9660, slow |
+| **CacheCDFS** | Third-party (popular) | ISO 9660 + Joliet + RockRidge, caching, fast |
+| **AsimCDFS** | Third-party | Similar to CacheCDFS, commercial |
+
+---
+
+## 64-Bit Addressing (TD64 / NSD)
+
+For drives larger than 4 GB:
+
+```c
+/* TD64 — uses io_Actual for high 32 bits: */
+diskReq->io_Command = NSCMD_TD_READ64;
+diskReq->io_Data = buffer;
+diskReq->io_Length = 512;
+diskReq->io_Offset = lowOffset; /* low 32 bits */
+diskReq->io_Actual = highOffset; /* high 32 bits */
+DoIO((struct IORequest *)diskReq);
+```
+
+> [!TIP]
+> **New Style Device (NSD)** provides a standard way to query 64-bit support: send `NSCMD_DEVICEQUERY` and check the returned command list.
+
+---
+
+## MiSTer / FPGA Notes
+
+| Aspect | Implementation |
+|---|---|
+| IDE emulation | MiSTer emulates Gayle IDE — presents virtual ATA backed by SD card `.hdf` |
+| SCSI emulation | A2091/A3000 SCSI cores map targets to `.hdf` files |
+| Sector size | Must be 512 bytes — all Amiga drivers assume this |
+| RDB | Rigid Disk Block at sectors 0–15 — must be present for HDToolBox |
+| Performance | Virtual IDE is faster than real Gayle (no PIO bottleneck) |
+
---
## References
-- NDK39: `devices/scsidisk.h`
+- NDK39: `devices/scsidisk.h`, `devices/trackdisk.h`
- ADCD 2.1: scsi.device autodocs
+- SCSI-2 standard: ANSI X3.131-1994
+- See also: [trackdisk.md](trackdisk.md) — floppy I/O (shares the same API model)
diff --git a/10_devices/timer.md b/10_devices/timer.md
index 1b8e5bc..f9fb26d 100644
--- a/10_devices/timer.md
+++ b/10_devices/timer.md
@@ -4,63 +4,132 @@
## Overview
-`timer.device` provides all timing services on AmigaOS: delays, system clock queries, and high-resolution timestamps. It interfaces with two independent hardware sources — the **CIA timers** (microsecond resolution) and the **vertical blank interrupt** (frame-rate resolution).
+`timer.device` is the system's central timing service — every delay, timeout, periodic callback, and timestamp on AmigaOS flows through it. Unlike most devices that map to one piece of hardware, timer.device **virtualises** two physical clock sources ([CIA timers](../01_hardware/common/cia_chips.md) and the vertical blank interrupt) into an unlimited number of independent timer requests.
+
+> For low-level CIA register programming and hardware timer theory, see [CIA Chips — Hardware Reference](../01_hardware/common/cia_chips.md). This article covers the OS-level `timer.device` API that sits on top of that hardware.
+
+**Key insight**: timer.device is **fully multiplexed**. Any number of tasks can have active timer requests simultaneously — the device maintains a sorted queue of pending requests and satisfies them from the same hardware clocks. There is no "one subscriber per timer" limit.
---
-## Units
-
-| Unit | Constant | Resolution | Clock Source | Use Case |
-|---|---|---|---|---|
-| 0 | `UNIT_MICROHZ` | ~1.4 µs (E-clock tick) | CIA-A Timer A | Short, precise delays |
-| 1 | `UNIT_VBLANK` | ~20 ms (PAL) / ~16.7 ms (NTSC) | VBlank interrupt | Long delays, low CPU overhead |
-| 2 | `UNIT_ECLOCK` | ~1.4 µs | CIA-B Timer A | Highest resolution timing (OS 2.0+) |
-| 3 | `UNIT_WAITUNTIL` | absolute time | System clock | Wait until specific wall-clock time |
-| 4 | `UNIT_WAITECLOCK` | E-clock absolute | CIA | Wait until specific E-clock value |
-
-### Which Unit to Use?
+## System Architecture
```mermaid
flowchart TD
- Q["How long is the delay?"] --> SHORT["< 100 ms"]
- Q --> LONG["> 100 ms"]
- SHORT --> MICRO["UNIT_MICROHZ
or UNIT_ECLOCK"]
- LONG --> VBLANK["UNIT_VBLANK
(lower CPU overhead)"]
- Q --> MEASURE["Need to measure
elapsed time?"]
- MEASURE --> ECLOCK["ReadEClock()"]
+ subgraph "Hardware"
+ CIAA["CIA-A Timer A/B
709,379 Hz (PAL)
715,909 Hz (NTSC)"]
+ CIAB["CIA-B Timer A/B"]
+ VBLANK["VBlank Interrupt
50 Hz (PAL) / 60 Hz (NTSC)"]
+ end
+
+ subgraph "timer.device (kernel)"
+ UM["UNIT_MICROHZ
queue"] --> CIAA
+ UE["UNIT_ECLOCK
queue"] --> CIAB
+ UV["UNIT_VBLANK
queue"] --> VBLANK
+ UW["UNIT_WAITUNTIL"] --> CIAA
+ UWE["UNIT_WAITECLOCK"] --> CIAB
+
+ CLOCK["System Clock
(seconds since 1978-01-01)"]
+ CIAA --> CLOCK
+ VBLANK --> CLOCK
+ end
+
+ subgraph "User Tasks (unlimited)"
+ T1["App 1: 100ms delay"]
+ T2["App 2: 50ms delay"]
+ T3["App 3: 2s delay"]
+ T4["Game: 20ms periodic"]
+ T5["Audio: 5ms refill"]
+ end
+
+ T1 --> UM
+ T2 --> UM
+ T3 --> UV
+ T4 --> UV
+ T5 --> UE
+
+ style UM fill:#e8f4fd,stroke:#2196f3,color:#333
+ style UV fill:#fff9c4,stroke:#f9a825,color:#333
+ style UE fill:#fff3e0,stroke:#ff9800,color:#333
```
+### How Multiplexing Works
+
+timer.device keeps a **sorted linked list** of pending requests per unit. When a `TR_ADDREQUEST` arrives:
+
+1. The device calculates the absolute expiry time (current time + requested delay)
+2. Inserts the request into the sorted queue
+3. Programs the hardware timer to fire at the **soonest** expiry time
+4. When the timer interrupt fires, the device scans the queue, replies all expired requests, and reprograms for the next one
+
+This means 1000 applications can each have an active timer — the device just has one hardware timer firing at the nearest deadline. The overhead is the sorted insertion (O(n) in the worst case, but n is rarely large).
+
+### Can You Run Out of Timer Resources?
+
+**Short answer**: timer.device itself never runs out — you can queue an unlimited number of requests. But the **surrounding resources** can be exhausted:
+
+| Resource | Limit | What Happens When Exhausted |
+|---|---|---|
+| **Signal bits** | **32 per task** (and ~16 are reserved by the system) | `CreateMsgPort()` returns NULL because `AllocSignal()` can't find a free bit. You can't create a new MsgPort — but you can share one port across multiple timers (see "Multiple Timers Per Task" below). |
+| **Memory for IORequests** | Available RAM | `CreateIORequest()` returns NULL. Each `timerequest` is 40 bytes — you'd need thousands to notice. |
+| **Queue depth (device internal)** | Unbounded linked list | Theoretically infinite. In practice, if you queue 10,000+ pending requests, the O(n) sorted insertion becomes noticeable — each new request must walk the list to find its insertion point. At ~50,000+ the system may feel sluggish. |
+| **Hardware timers (CIA)** | 4 total (2 per CIA) | **Not your problem.** timer.device owns the hardware timers and virtualises them. You never directly compete for CIA timer resources — the device multiplexes everything internally. |
+| **VBlank slots** | 1 per frame (50/60 Hz) | Not a resource limit — VBlank fires once per frame regardless of how many UNIT_VBLANK requests are queued. All expired requests are serviced in the same interrupt. |
+
+**The real bottleneck is signal bits.** A task only has 32, and each `CreateMsgPort` consumes one. If your application already uses signals for windows, ARexx, commodities, and other devices, you may only have 5–10 left. The solution is **sharing a single MsgPort** across multiple timer requests and using `GetMsg()` to distinguish which timer fired (compare the reply message pointer to each `timerequest`).
+
+> [!TIP]
+> `OpenDevice("timer.device", ...)` itself never fails — timer.device is always available and has no "maximum open count". You can open it thousands of times. It's `CreateMsgPort` (signal bits) and `CreateIORequest` (memory) that can fail.
+
+---
+
+## Units — When to Use What
+
+| Unit | Constant | Resolution | Clock Source | Best For |
+|---|---|---|---|---|
+| 0 | `UNIT_MICROHZ` | ~1.4 µs | CIA-A Timer A | Sub-millisecond delays, benchmarking |
+| 1 | `UNIT_VBLANK` | 20 ms (PAL) / 16.7 ms (NTSC) | VBlank IRQ | Game loops, UI timeouts, long waits |
+| 2 | `UNIT_ECLOCK` | ~1.4 µs | CIA-B Timer A | Highest resolution measurement (OS 2.0+) |
+| 3 | `UNIT_WAITUNTIL` | absolute | System clock | Wake at specific wall-clock time |
+| 4 | `UNIT_WAITECLOCK` | E-clock ticks | CIA | Wait until specific E-clock count |
+
+```mermaid
+flowchart TD
+ Q["What do you need?"] --> |"< 20ms delay"| MICRO["UNIT_MICROHZ"]
+ Q --> |"> 100ms delay"| VBLANK["UNIT_VBLANK
(lower CPU cost)"]
+ Q --> |"Measure elapsed time"| ECLOCK["ReadEClock()
via UNIT_ECLOCK"]
+ Q --> |"Wake at specific time"| WAITUNTIL["UNIT_WAITUNTIL"]
+ Q --> |"Frame-rate sync"| VBLANK2["UNIT_VBLANK
(natural frame sync)"]
+```
+
+> [!IMPORTANT]
+> **UNIT_VBLANK has 20ms granularity** — requesting a 5ms delay will actually wait 0–20ms. For anything shorter than one frame, use UNIT_MICROHZ or UNIT_ECLOCK.
+
---
## Hardware Foundation
### CIA Timer Internals
-The timing hardware lives in the two CIA (Complex Interface Adapter) chips:
-
-| CIA | Base | Timer | E-Clock Frequency |
+| CIA | Base Address | Timer | E-Clock Frequency |
|---|---|---|---|
| CIA-A | `$BFE001` | Timer A, Timer B | 709,379 Hz (PAL) / 715,909 Hz (NTSC) |
| CIA-B | `$BFD000` | Timer A, Timer B | Same |
-The **E-clock** is derived from the system clock ÷ 10 (PAL: 7,093,790 / 10 = 709,379 Hz). Each tick is ~1.4 µs.
-
-```c
-/* E-clock ticks per second: */
-#define ECLOCK_PAL 709379
-#define ECLOCK_NTSC 715909
-
-/* Example: 100 ms delay = 70,938 ticks (PAL) */
-```
+The **E-clock** is derived from the system clock ÷ 10:
+- PAL: 7,093,790 Hz / 10 = **709,379 Hz** → tick = **1.410 µs**
+- NTSC: 7,159,090 Hz / 10 = **715,909 Hz** → tick = **1.397 µs**
### VBlank Timing
-UNIT_VBLANK piggybacks on the vertical blank interrupt — one tick per video frame:
+UNIT_VBLANK is synchronised to the display's vertical blank interrupt:
-| Standard | VBlank Rate | Resolution |
-|---|---|---|
-| PAL | 50 Hz | 20.0 ms |
-| NTSC | 60 Hz | 16.7 ms |
+| Standard | VBlank Rate | Period | Use |
+|---|---|---|---|
+| PAL | 50 Hz | 20.0 ms | European systems |
+| NTSC | 60 Hz | 16.7 ms | American/Japanese systems |
+
+VBlank is the natural heartbeat of the system — graphics updates, animation, and game logic are traditionally synced to it.
---
@@ -69,170 +138,186 @@ UNIT_VBLANK piggybacks on the vertical blank interrupt — one tick per video fr
```c
/* devices/timer.h — NDK39 */
struct timeval {
- ULONG tv_secs; /* seconds */
+ ULONG tv_secs; /* seconds (since midnight 1 Jan 1978) */
ULONG tv_micro; /* microseconds (0–999999) */
};
struct timerequest {
- struct IORequest tr_node;
- struct timeval tr_time;
+ struct IORequest tr_node; /* standard I/O request header */
+ struct timeval tr_time; /* delay/time value */
};
-/* sizeof(timerequest) = sizeof(IORequest) + 8 */
-```
-### EClockVal (OS 2.0+)
-
-```c
-struct EClockVal {
- ULONG ev_hi; /* high 32 bits of 64-bit tick counter */
- ULONG ev_lo; /* low 32 bits */
+struct EClockVal { /* OS 2.0+ */
+ ULONG ev_hi; /* high 32 bits of 64-bit tick counter */
+ ULONG ev_lo; /* low 32 bits */
};
```
---
-## Opening timer.device
+## Proper Initialisation and Shutdown
+
+timer.device uses the standard Exec I/O model: a **MsgPort** for signal delivery, an **IORequest** to describe the operation, and an explicit **open/close** lifecycle. Every step matters — skipping any one causes subtle or catastrophic failures.
+
+### Opening (The Right Way)
+
+The correct sequence is: create a MsgPort (for reply signals) → create an IORequest (the "ticket" for timer operations) → open the device (binds the IORequest to the hardware). Each step depends on the previous one, so errors must unwind in reverse order:
```c
struct MsgPort *timerPort = CreateMsgPort();
+if (!timerPort) { /* handle error */ }
+
struct timerequest *tr = (struct timerequest *)
CreateIORequest(timerPort, sizeof(struct timerequest));
+if (!tr) { DeleteMsgPort(timerPort); /* handle error */ }
BYTE err = OpenDevice("timer.device", UNIT_MICROHZ,
(struct IORequest *)tr, 0);
-if (err != 0) { /* handle error */ }
+if (err != 0)
+{
+ DeleteIORequest((struct IORequest *)tr);
+ DeleteMsgPort(timerPort);
+ /* handle error — timer.device should always be available */
+}
-/* IMPORTANT: after opening, you can get TimerBase for direct calls: */
+/* After opening, get TimerBase for utility functions: */
struct Library *TimerBase = (struct Library *)tr->tr_node.io_Device;
-/* Now you can call AddTime(), SubTime(), CmpTime(), ReadEClock() */
+/* Now AddTime(), SubTime(), CmpTime(), ReadEClock() are available */
```
+**Why each step is mandatory:**
+
+| Shortcut | Consequence |
+|---|---|
+| Skip `CreateMsgPort` | No signal bit allocated → `Wait()` will never wake up. Or worse: signal bit 0 (CTRL-C) gets reused, causing random task termination. |
+| Skip error check on `CreateIORequest` | NULL IORequest passed to `OpenDevice` → immediate crash (NULL pointer dereference in Exec). |
+| Skip `OpenDevice` error check | If device can't open (e.g. wrong unit), the IORequest is uninitialised — any subsequent `DoIO`/`SendIO` writes to random memory → Guru Meditation. |
+| Use `AllocMem` instead of `CreateIORequest` | IORequest fields (`io_Message.mn_ReplyPort`, `io_Message.mn_Length`) are not initialised → device replies to garbage address → memory corruption. |
+| Not saving `TimerBase` | Can't call `AddTime`/`SubTime`/`CmpTime`/`ReadEClock` — they require the device's library base in A6. |
+
+### Shutdown (The Right Way)
+
+Shutdown must drain all pending I/O before freeing anything. The device's internal request queue holds a pointer to your IORequest — if you free it while it's still queued, the next timer interrupt dereferences freed memory.
+
+```c
+/* CRITICAL: abort any pending request before closing! */
+if (!CheckIO((struct IORequest *)tr))
+{
+ AbortIO((struct IORequest *)tr);
+ WaitIO((struct IORequest *)tr); /* MUST wait even after abort */
+}
+
+CloseDevice((struct IORequest *)tr);
+DeleteIORequest((struct IORequest *)tr);
+DeleteMsgPort(timerPort);
+```
+
+**Why `AbortIO` + `WaitIO` — not just one or the other:**
+
+- **`AbortIO` alone is not enough**: `AbortIO` marks the request for cancellation, but the device may be in the middle of processing it (e.g., inside the timer interrupt handler). The request isn't truly "done" until the device replies it back to your MsgPort. `WaitIO` collects that reply.
+- **`WaitIO` alone is not enough**: If you `WaitIO` a request that has 5 minutes left, your task blocks for 5 minutes. `AbortIO` tells the device "cancel this immediately" so `WaitIO` returns right away.
+- **Skipping both**: The IORequest stays in the device's sorted queue. When the timer fires later, the device writes to your (now freed) IORequest → memory corruption → delayed Guru Meditation, often in an unrelated task.
+
+> [!WARNING]
+> **Never call `CloseDevice` with a pending timer request.** This corrupts the device's internal queue and will eventually Guru Meditation. Always `AbortIO` + `WaitIO` first. This is the single most common timer.device bug in Amiga software.
+
---
-## Simple Delay
+## Use Case 1: Simple Blocking Delay
```c
/* Block the current task for exactly 2.5 seconds: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 2;
-tr->tr_time.tv_micro = 500000; /* 0.5 sec */
-DoIO((struct IORequest *)tr); /* blocks until done */
+tr->tr_time.tv_micro = 500000; /* 500ms */
+DoIO((struct IORequest *)tr);
+/* Task is now blocked — other tasks run during this time */
```
-### Non-Blocking Delay
+---
+
+## Use Case 2: Non-Blocking Timeout (UI Pattern)
+
+The standard Intuition event loop with a timeout — essential for UI applications that need to update periodically:
```c
-/* Start delay, continue doing work, then wait: */
-tr->tr_node.io_Command = TR_ADDREQUEST;
-tr->tr_time.tv_secs = 0;
-tr->tr_time.tv_micro = 100000; /* 100 ms */
-SendIO((struct IORequest *)tr); /* non-blocking */
-
-/* ... do other work ... */
-
-/* Check if timer expired: */
-if (CheckIO((struct IORequest *)tr))
-{
- WaitIO((struct IORequest *)tr); /* collect result */
- /* timer expired */
-}
-```
-
-### Signal-Based Waiting
-
-```c
-/* Wait for timer OR user input: */
-ULONG timerSig = 1L << timerPort->mp_SigBit;
+/* Classic Intuition event loop with timer: */
+ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG windowSig = 1L << window->UserPort->mp_SigBit;
+BOOL timerPending = FALSE;
+/* Start a 1-second timeout: */
+tr->tr_node.io_Command = TR_ADDREQUEST;
+tr->tr_time.tv_secs = 1;
+tr->tr_time.tv_micro = 0;
SendIO((struct IORequest *)tr);
+timerPending = TRUE;
-ULONG sigs = Wait(timerSig | windowSig);
-if (sigs & timerSig) {
- WaitIO((struct IORequest *)tr);
- /* handle timeout */
+BOOL running = TRUE;
+while (running)
+{
+ ULONG sigs = Wait(timerSig | windowSig | SIGBREAKF_CTRL_C);
+
+ /* Handle window events: */
+ if (sigs & windowSig)
+ {
+ struct IntuiMessage *msg;
+ while ((msg = (struct IntuiMessage *)GetMsg(window->UserPort)))
+ {
+ switch (msg->Class)
+ {
+ case IDCMP_CLOSEWINDOW:
+ running = FALSE;
+ break;
+ case IDCMP_GADGETUP:
+ HandleGadget(msg);
+ break;
+ }
+ ReplyMsg((struct Message *)msg);
+ }
+ }
+
+ /* Handle timer expiry: */
+ if (sigs & timerSig)
+ {
+ WaitIO((struct IORequest *)tr);
+ timerPending = FALSE;
+
+ /* --- Update UI clock, animation, status bar, etc. --- */
+ UpdateStatusBar();
+
+ /* Re-arm timer for next second: */
+ tr->tr_node.io_Command = TR_ADDREQUEST;
+ tr->tr_time.tv_secs = 1;
+ tr->tr_time.tv_micro = 0;
+ SendIO((struct IORequest *)tr);
+ timerPending = TRUE;
+ }
+
+ if (sigs & SIGBREAKF_CTRL_C)
+ running = FALSE;
}
-if (sigs & windowSig) {
+
+/* Clean shutdown: */
+if (timerPending)
+{
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr);
- /* handle window event */
}
```
---
-## Getting Current Time
+## Use Case 3: Game/Demo Frame Sync (Periodic Timer)
```c
-/* Get system time (wall clock since midnight Jan 1, 1978): */
-tr->tr_node.io_Command = TR_GETSYSTIME;
-DoIO((struct IORequest *)tr);
-Printf("Time: %lu.%06lu seconds since epoch\n",
- tr->tr_time.tv_secs, tr->tr_time.tv_micro);
-```
-
-### Time Arithmetic
-
-```c
-/* After opening timer.device and getting TimerBase: */
-struct timeval t1, t2, diff;
-
-/* Measure elapsed time: */
-tr->tr_node.io_Command = TR_GETSYSTIME;
-DoIO((struct IORequest *)tr);
-t1 = tr->tr_time;
-
-/* ... do work ... */
-
-DoIO((struct IORequest *)tr);
-t2 = tr->tr_time;
-
-/* Compute difference: */
-diff = t2;
-SubTime(&diff, &t1);
-Printf("Elapsed: %lu.%06lu s\n", diff.tv_secs, diff.tv_micro);
-
-/* Compare times: */
-LONG cmp = CmpTime(&t1, &t2); /* <0: t10: t1>t2 */
-```
-
----
-
-## High-Resolution Timing (ReadEClock)
-
-```c
-/* Most precise timing available — E-clock resolution: */
-struct EClockVal start, end;
-ULONG efreq = ReadEClock(&start); /* returns ticks/second */
-
-/* ... code to benchmark ... */
-
-ReadEClock(&end);
-
-/* Compute elapsed microseconds: */
-ULONG ticks = end.ev_lo - start.ev_lo; /* assumes <4 billion ticks */
-ULONG usecs = ticks * 1000000 / efreq;
-Printf("Elapsed: %lu µs (E-clock freq: %lu Hz)\n", usecs, efreq);
-```
-
-| Standard | E-clock Freq | Tick Resolution |
-|---|---|---|
-| PAL | 709,379 Hz | ~1.410 µs |
-| NTSC | 715,909 Hz | ~1.397 µs |
-
----
-
-## Periodic Timer (Game Loop / Audio Refill)
-
-```c
-/* Classic pattern: periodic callback using timer.device */
-#define FRAME_USEC 20000 /* 50 Hz (PAL frame rate) */
+/* 50 Hz game loop synchronised to PAL frame rate: */
+#define FRAME_USEC 20000 /* 1/50th second = 20ms */
void GameLoop(void)
{
ULONG timerSig = 1L << timerPort->mp_SigBit;
- /* Kick off first timer request: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = FRAME_USEC;
@@ -247,11 +332,13 @@ void GameLoop(void)
{
WaitIO((struct IORequest *)tr);
- /* --- Game logic here --- */
- UpdateGame();
+ /* === Frame logic === */
+ ReadInput();
+ UpdatePhysics();
RenderFrame();
+ SwapBuffers();
- /* Re-arm timer: */
+ /* Re-arm for next frame: */
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = FRAME_USEC;
SendIO((struct IORequest *)tr);
@@ -266,17 +353,169 @@ void GameLoop(void)
}
```
+> **Demo effects**: For smooth copper-style effects at higher rates (100+ Hz), demos typically bypass timer.device entirely and use direct CIA timer interrupts or copper waits. timer.device is better suited for system-friendly applications.
+
---
-## Common Pitfalls
+## Use Case 4: Audio Buffer Refill
+
+```c
+/* Double-buffered audio playback with timer-driven refill: */
+#define AUDIO_BUFFER_MS 10 /* refill every 10ms */
+
+void AudioRefillLoop(void)
+{
+ ULONG timerSig = 1L << timerPort->mp_SigBit;
+
+ /* Use UNIT_MICROHZ for sub-frame precision: */
+ tr->tr_node.io_Command = TR_ADDREQUEST;
+ tr->tr_time.tv_secs = 0;
+ tr->tr_time.tv_micro = AUDIO_BUFFER_MS * 1000;
+ SendIO((struct IORequest *)tr);
+
+ while (!quit)
+ {
+ Wait(timerSig);
+ WaitIO((struct IORequest *)tr);
+
+ /* Fill the next audio DMA buffer: */
+ FillAudioBuffer(currentBuffer);
+ SwapAudioBuffers();
+
+ /* Re-arm: */
+ tr->tr_time.tv_secs = 0;
+ tr->tr_time.tv_micro = AUDIO_BUFFER_MS * 1000;
+ SendIO((struct IORequest *)tr);
+ }
+
+ AbortIO((struct IORequest *)tr);
+ WaitIO((struct IORequest *)tr);
+}
+```
+
+---
+
+## Use Case 5: Benchmarking with ReadEClock
+
+```c
+/* Precise code benchmarking using E-clock: */
+struct EClockVal start, end;
+ULONG efreq = ReadEClock(&start);
+
+/* --- Code to benchmark --- */
+SortLargeArray(data, count);
+/* --- End benchmark --- */
+
+ReadEClock(&end);
+
+/* Calculate elapsed microseconds: */
+ULONG ticks = end.ev_lo - start.ev_lo;
+ULONG usecs = (ULONG)((UQUAD)ticks * 1000000ULL / efreq);
+Printf("Elapsed: %lu µs (%lu E-clock ticks at %lu Hz)\n",
+ usecs, ticks, efreq);
+```
+
+---
+
+## Use Case 6: Getting System Time
+
+```c
+/* Read wall-clock time: */
+tr->tr_node.io_Command = TR_GETSYSTIME;
+DoIO((struct IORequest *)tr);
+Printf("Seconds since 1978-01-01: %lu.%06lu\n",
+ tr->tr_time.tv_secs, tr->tr_time.tv_micro);
+
+/* Time arithmetic: */
+struct timeval t1, t2, elapsed;
+/* ... get t1, do work, get t2 ... */
+elapsed = t2;
+SubTime(&elapsed, &t1);
+Printf("Operation took: %lu.%06lu s\n",
+ elapsed.tv_secs, elapsed.tv_micro);
+
+/* Compare times: */
+LONG cmp = CmpTime(&t1, &t2);
+/* Returns: -1 if t1 > t2, 0 if equal, +1 if t1 < t2 */
+/* WARNING: return values are opposite to strcmp convention! */
+```
+
+---
+
+## Multiple Timers Per Task
+
+A single task can have **multiple simultaneous timer requests** — just use separate `timerequest` structures sharing the same MsgPort:
+
+```c
+/* Two independent timers on one port: */
+struct timerequest *tr_fast = CreateIORequest(port, sizeof(*tr_fast));
+struct timerequest *tr_slow = CreateIORequest(port, sizeof(*tr_slow));
+
+OpenDevice("timer.device", UNIT_MICROHZ, (struct IORequest *)tr_fast, 0);
+
+/* Clone the device for the second request: */
+*tr_slow = *tr_fast; /* copy device/unit/port */
+
+/* Start both timers: */
+tr_fast->tr_time.tv_micro = 50000; /* 50ms — UI animation */
+SendIO((struct IORequest *)tr_fast);
+
+tr_slow->tr_time.tv_secs = 5; /* 5s — autosave */
+SendIO((struct IORequest *)tr_slow);
+
+/* Wait for either: */
+while (running)
+{
+ ULONG sigs = Wait(1L << port->mp_SigBit);
+ struct Message *msg;
+ while ((msg = GetMsg(port)))
+ {
+ if (msg == (struct Message *)tr_fast)
+ {
+ /* Fast timer expired — animate */
+ WaitIO((struct IORequest *)tr_fast);
+ AnimateUI();
+ tr_fast->tr_time.tv_micro = 50000;
+ SendIO((struct IORequest *)tr_fast);
+ }
+ else if (msg == (struct Message *)tr_slow)
+ {
+ /* Slow timer expired — autosave */
+ WaitIO((struct IORequest *)tr_slow);
+ AutoSave();
+ tr_slow->tr_time.tv_secs = 5;
+ SendIO((struct IORequest *)tr_slow);
+ }
+ }
+}
+```
+
+---
+
+## Common Pitfalls and Anti-Patterns
| Pitfall | Problem | Solution |
|---|---|---|
-| Reusing active IORequest | Sending a `TR_ADDREQUEST` while previous is pending | Use two timerequest structs, or `WaitIO` first |
-| Forgetting `WaitIO` after `AbortIO` | Leaves IORequest in limbo — crash on next use | Always `WaitIO` after `AbortIO`, even if aborted |
-| Using `UNIT_VBLANK` for short delays | 20 ms granularity — actual delay is 0 to 20 ms | Use `UNIT_MICROHZ` for sub-20ms precision |
-| Not opening timer for `ReadEClock` | `TimerBase` is NULL — crash | Must `OpenDevice` first to get `TimerBase` |
-| Ignoring PAL/NTSC differences | Hardcoded periods wrong on other standard | Use `ReadEClock()` frequency for calculations |
+| **Reusing active IORequest** | `SendIO` while previous is pending → queue corruption | Use two `timerequest` structs, or `WaitIO` first |
+| **Missing `WaitIO` after `AbortIO`** | IORequest in limbo — crash on next use | **Always** `WaitIO` after `AbortIO`, even if aborted |
+| **Using `UNIT_VBLANK` for short delays** | 20ms granularity — actual delay is 0–20ms | Use `UNIT_MICROHZ` for sub-20ms precision |
+| **Not opening timer for `ReadEClock`** | `TimerBase` is NULL — immediate crash | Must `OpenDevice` first to get `TimerBase` |
+| **Hardcoded PAL/NTSC values** | Wrong timing on the other standard | Use `ReadEClock()` frequency for calculations |
+| **Polling in a loop instead of `Wait()`** | Burns 100% CPU for no benefit | Use signal-based `Wait()` — CPU sleeps until timer fires |
+| **Forgetting to abort on shutdown** | Device queue contains pointer to freed memory | Always `AbortIO`+`WaitIO` before `CloseDevice` |
+| **Using Delay() for everything** | `Delay()` has 20ms granularity and blocks the process | Use `SendIO` + signals for responsive apps |
+
+### The `Delay()` Trap
+
+`dos.library/Delay()` uses timer.device internally, but:
+- Fixed to UNIT_VBLANK (20ms granularity)
+- Blocks the entire process (no signal checking possible)
+- Takes ticks, not milliseconds: `Delay(50)` = 1 second (at 50 Hz)
+
+```c
+/* DON'T use Delay() in event loops — use timer.device directly */
+Delay(25); /* blocks for 0.5s — can't handle window events during this! */
+```
---
@@ -286,3 +525,5 @@ void GameLoop(void)
- ADCD 2.1: timer.device autodocs
- HRM: CIA timer chapter
- See also: [interrupts.md](../06_exec_os/interrupts.md) — VBlank interrupt chain
+- See also: [multitasking.md](../06_exec_os/multitasking.md) — task scheduling and signals
+- See also: [audio.md](audio.md) — audio buffer timing
diff --git a/11_libraries/README.md b/11_libraries/README.md
index fdb5ec4..0b7f343 100644
--- a/11_libraries/README.md
+++ b/11_libraries/README.md
@@ -2,18 +2,20 @@
# Common Libraries — Overview
+Shared libraries beyond the core exec/dos/graphics/intuition subsystems. These provide utility functions, file format parsing, hardware expansion support, and Workbench integration.
+
## Section Index
| File | Description |
|---|---|
-| [utility.md](utility.md) | utility.library — TagItem, hooks, date |
-| [expansion.md](expansion.md) | expansion.library — Zorro/AutoConfig |
-| [icon.md](icon.md) | icon.library — Workbench icons (.info) |
-| [workbench.md](workbench.md) | workbench.library — Workbench integration |
-| [iffparse.md](iffparse.md) | iffparse.library — IFF file parsing |
-| [locale.md](locale.md) | locale.library — internationalisation |
-| [keymap.md](keymap.md) | keymap.library — keyboard mapping |
-| [rexxsyslib.md](rexxsyslib.md) | rexxsyslib.library — ARexx interface |
-| [mathffp.md](mathffp.md) | mathffp/mathieeesingbas — floating point |
-| [layers.md](layers.md) | layers.library — window clipping layers |
-| [diskfont.md](diskfont.md) | diskfont.library — disk-based fonts |
+| [utility.md](utility.md) | TagItem lists with chaining (TAG_MORE), callback hooks (register convention), date/time utilities, tag iteration patterns |
+| [expansion.md](expansion.md) | Zorro II/III bus architecture, AutoConfig ROM layout, board enumeration, FPGA implementation notes |
+| [icon.md](icon.md) | Workbench icons (.info): DiskObject structure, ToolType parsing, icon types, OS 3.5+ true-colour icons |
+| [workbench.md](workbench.md) | Workbench integration: WBStartup handling, AppWindow drag-and-drop, AppIcon, AppMenuItem |
+| [iffparse.md](iffparse.md) | IFF file parsing: ILBM/8SVX/ANIM, BitMapHeader, ByteRun1 compression, clipboard integration |
+| [locale.md](locale.md) | Internationalisation: catalogue system (.cd/.ct files), locale-aware date/number formatting, character classification |
+| [keymap.md](keymap.md) | Keyboard mapping: raw-to-ASCII translation, KeyMap structure, dead keys, rawkey codes, national layouts |
+| [rexxsyslib.md](rexxsyslib.md) | ARexx scripting: hosting ARexx ports, command parsing, sending commands, return codes |
+| [mathffp.md](mathffp.md) | Motorola FFP and IEEE 754 floating point |
+| [layers.md](layers.md) | Window clipping: ClipRect engine, Simple/Smart/Super refresh, damage repair, backfill hooks, layer locking |
+| [diskfont.md](diskfont.md) | Disk-based fonts: FONTS: directory structure, AvailFonts enumeration, colour fonts (OS 3.0+) |
diff --git a/11_libraries/diskfont.md b/11_libraries/diskfont.md
index a2d73ac..6f51da0 100644
--- a/11_libraries/diskfont.md
+++ b/11_libraries/diskfont.md
@@ -1,10 +1,48 @@
[← Home](../README.md) · [Libraries](README.md)
-# diskfont.library — Disk-Based Fonts
+# diskfont.library — Disk-Based Font Loading
## Overview
-`diskfont.library` loads bitmap fonts from disk (the `FONTS:` assign). ROM fonts (topaz 8, topaz 9) are always available; all others require this library.
+`diskfont.library` loads bitmap fonts from disk (the `FONTS:` assign). Only two fonts are built into ROM — **topaz 8** and **topaz 9**. All other fonts (helvetica, times, courier, etc.) must be loaded from disk via this library.
+
+```mermaid
+flowchart LR
+ APP["Application"] -->|"OpenDiskFont(&ta)"| DFL["diskfont.library"]
+ DFL -->|"Scans FONTS: directory"| FONTS["FONTS:
helvetica.font
helvetica/24"]
+ DFL -->|"Returns loaded font"| TF["struct TextFont"]
+ TF -->|"SetFont(rp, font)"| RP["RastPort"]
+
+ ROM["ROM"] -->|"OpenFont(&ta)"| TOPAZ["topaz 8/9
(always available)"]
+
+ style DFL fill:#e8f4fd,stroke:#2196f3,color:#333
+ style FONTS fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+---
+
+## Font Directory Structure
+
+Amiga bitmap fonts are stored as a descriptor file plus per-size data files:
+
+```
+FONTS:
+ helvetica.font ← font descriptor (FontContents header)
+ helvetica/
+ 9 ← bitmap data for 9-pixel height
+ 11 ← bitmap data for 11-pixel height
+ 13 ← bitmap data for 13-pixel height
+ 18 ← bitmap data for 18-pixel height
+ 24 ← bitmap data for 24-pixel height
+ times.font
+ times/
+ 11
+ 13
+ 18
+ 24
+```
+
+The `.font` descriptor file contains a `FontContentsHeader` listing all available sizes, styles, and flags.
---
@@ -13,45 +51,106 @@
```c
struct Library *DiskfontBase = OpenLibrary("diskfont.library", 0);
+/* Request a specific font and size: */
struct TextAttr ta = {"helvetica.font", 24, 0, 0};
struct TextFont *font = OpenDiskFont(&ta);
-if (font) {
+
+if (font)
+{
SetFont(rp, font);
- /* ... render ... */
+ Move(rp, 10, 30);
+ Text(rp, "Disk Font Text", 14);
+
+ /* When done with the font: */
CloseFont(font);
}
+else
+{
+ /* Font not found — fall back to ROM font: */
+ struct TextAttr fallback = {"topaz.font", 8, 0, FPF_ROMFONT};
+ struct TextFont *topaz = OpenFont(&fallback);
+ SetFont(rp, topaz);
+}
CloseLibrary(DiskfontBase);
```
----
+### Size Matching
-## Font Directory Structure
-
-```
-FONTS:
- helvetica.font ← font descriptor file
- helvetica/
- 24 ← bitmap data for size 24
- 11 ← bitmap data for size 11
-```
+If the exact requested size is not available, `OpenDiskFont` returns NULL. To find the nearest available size, use `AvailFonts` to enumerate, then request the closest match.
---
-## Listing Available Fonts
+## Enumerating Available Fonts
```c
-struct List *fontList = NULL;
-LONG count = AvailFonts(buf, bufsize, AFF_DISK | AFF_MEMORY);
+/* AvailFonts returns all fonts in ROM and on disk: */
+LONG bufSize = 4096;
+APTR buf = AllocMem(bufSize, MEMF_ANY | MEMF_CLEAR);
+
+LONG shortfall = AvailFonts(buf, bufSize, AFF_DISK | AFF_MEMORY);
+if (shortfall > 0)
+{
+ /* Buffer too small — reallocate and retry: */
+ FreeMem(buf, bufSize);
+ bufSize += shortfall;
+ buf = AllocMem(bufSize, MEMF_ANY | MEMF_CLEAR);
+ AvailFonts(buf, bufSize, AFF_DISK | AFF_MEMORY);
+}
+
struct AvailFontsHeader *afh = (struct AvailFontsHeader *)buf;
struct AvailFonts *af = (struct AvailFonts *)&afh[1];
-for (int i = 0; i < afh->afh_NumEntries; i++) {
- Printf("%s %ld\n", af[i].af_Attr.ta_Name, af[i].af_Attr.ta_YSize);
+
+for (int i = 0; i < afh->afh_NumEntries; i++)
+{
+ Printf("%s size %-3ld %s\n",
+ af[i].af_Attr.ta_Name,
+ af[i].af_Attr.ta_YSize,
+ (af[i].af_Type & AFF_DISK) ? "disk" :
+ (af[i].af_Type & AFF_MEMORY) ? "ROM" : "scaled");
}
+
+FreeMem(buf, bufSize);
+```
+
+---
+
+## Font Types
+
+| Flag | Source | Notes |
+|---|---|---|
+| `AFF_MEMORY` | ROM or already loaded in memory | topaz, or previously opened disk fonts |
+| `AFF_DISK` | Available on `FONTS:` | Requires disk access to load |
+| `AFF_SCALED` | Algorithmically scaled from another size | Lower quality; avoid when native size exists |
+| `AFF_BITMAP` | Bitmap (pixel) font | Standard Amiga font format |
+| `AFF_TAGGED` | Tagged (OS 3.0+ extended) font | Supports colour fonts, outlined fonts |
+
+---
+
+## Colour Fonts (OS 3.0+)
+
+OS 3.0 introduced **colour bitmap fonts** — each glyph can have multiple bitplanes:
+
+```c
+/* Colour fonts use ColorTextFont — an extension of TextFont: */
+struct ColorTextFont {
+ struct TextFont ctf_TF; /* standard TextFont */
+ UWORD ctf_Flags; /* CT_COLORFONT etc. */
+ UBYTE ctf_Depth; /* number of bitplanes */
+ UBYTE ctf_FgColor; /* default foreground pen */
+ UBYTE ctf_Low; /* lowest colour used */
+ UBYTE ctf_High; /* highest colour used */
+ APTR ctf_PlanePick; /* plane selection */
+ APTR ctf_PlaneOnOff; /* plane on/off defaults */
+ struct ColorFontColors *ctf_ColorTable;
+ APTR ctf_CharData[8]; /* per-plane glyph data */
+};
```
---
## References
-- NDK39: `diskfont/diskfont.h`
+- NDK39: `diskfont/diskfont.h`, `graphics/text.h`
+- ADCD 2.1: diskfont.library autodocs
+- See also: [text_fonts.md](../08_graphics/text_fonts.md) — TextFont structure and rendering
diff --git a/11_libraries/expansion.md b/11_libraries/expansion.md
index 4c6c09c..fe4ea07 100644
--- a/11_libraries/expansion.md
+++ b/11_libraries/expansion.md
@@ -4,18 +4,93 @@
## Overview
-`expansion.library` handles automatic configuration of Zorro II/III expansion boards. At boot, the OS scans the expansion bus and assigns base addresses to each board based on its AutoConfig ROM.
+`expansion.library` handles automatic configuration of Zorro II/III expansion boards. At boot, the OS scans the expansion bus and assigns base addresses to each board based on its **AutoConfig ROM** — a 256-byte structure that identifies the board's manufacturer, product, memory requirements, and bus type.
+
+Understanding AutoConfig is essential for FPGA core development — MiSTer cores that emulate expansion hardware (RAM boards, accelerators, RTG cards) must present valid AutoConfig data to the boot ROM.
+
+---
+
+## Zorro Bus Architecture
+
+```mermaid
+flowchart LR
+ subgraph "Zorro II Address Space (8 MB)"
+ Z2["$200000–$9FFFFF"]
+ Z2A["Board A: $200000 (2 MB)"]
+ Z2B["Board B: $400000 (512 KB)"]
+ Z2C["Board C: $480000 (64 KB)"]
+ end
+
+ subgraph "Zorro III Address Space (1.75 GB)"
+ Z3["$10000000–$7FFFFFFF"]
+ Z3A["Board D: $40000000 (16 MB)"]
+ end
+
+ CPU["68020/030/040"] --> Z2
+ CPU --> Z3
+```
+
+| Feature | Zorro II | Zorro III |
+|---|---|---|
+| Bus width | 16-bit | 32-bit |
+| Address space | $200000–$9FFFFF (8 MB) | $10000000–$7FFFFFFF |
+| Max board size | 8 MB | 1 GB |
+| Burst transfer | No | Yes (37 MB/s peak) |
+| Auto-sizing | No | Yes (dynamic) |
+| DMA capable | Yes | Yes |
+| Systems | A2000, A500, A1200 | A3000, A4000 |
---
## AutoConfig Sequence
-1. Board asserts `CFGIN` to request configuration
-2. CPU reads 256-byte config area at `$E80000`
-3. Board reports: manufacturer ID, product ID, board size, flags
-4. OS assigns a base address via `WriteExpansionByte`
-5. Board relocates to assigned address
-6. Next board in chain is configured
+The AutoConfig mechanism runs during early boot, before DOS is loaded:
+
+```mermaid
+sequenceDiagram
+ participant ROM as Kickstart ROM
+ participant BUS as Zorro Bus
+ participant BOARD as Expansion Board
+
+ ROM->>BUS: Assert CFGOUT chain
+ BOARD->>BUS: Assert CFGIN (ready to configure)
+ ROM->>BOARD: Read config area at $E80000
+ Note over ROM: Parse manufacturer, product, size, type
+ ROM->>BOARD: Write assigned base address
+ Note over BOARD: Board relocates to assigned address
+ ROM->>BUS: Pass CFGIN to next board
+ Note over ROM: Repeat until no more boards respond
+```
+
+### AutoConfig ROM Layout ($E80000)
+
+The board presents its identity at the configuration address. Each byte is read from **even** addresses only ($E80000, $E80002, $E80004...):
+
+| Offset | Register | Bits | Description |
+|---|---|---|---|
+| $00 | `er_Type` | 7:6 | Board type: 11=Zorro III, 10=Zorro II |
+| $00 | `er_Type` | 5 | Memory board (1) or I/O board (0) |
+| $00 | `er_Type` | 4 | Chain bit — more boards follow |
+| $00 | `er_Type` | 3:0 | Size code (see table below) |
+| $02 | `er_Product` | 7:0 | Product number (0–255) |
+| $04 | `er_Flags` | 7 | Can be shut up (mapped out) |
+| $04 | `er_Flags` | 5 | Board's memory is free (add to system pool) |
+| $08/$0A | `er_Manufacturer` | 15:0 | Manufacturer ID (IANA-like registry) |
+| $0C–$12 | `er_SerialNumber` | 31:0 | Serial number (unique per board) |
+| $20/$22 | `er_InitDiagVec` | 15:0 | Offset to optional boot ROM (DiagArea) |
+
+### Size Code
+
+| Code | Zorro II Size | Zorro III Size |
+|---|---|---|
+| $0 | 8 MB | 16 MB |
+| $1 | 64 KB | 32 MB |
+| $2 | 128 KB | 64 MB |
+| $3 | 256 KB | 128 MB |
+| $4 | 512 KB | 256 MB |
+| $5 | 1 MB | 512 MB |
+| $6 | 2 MB | 1 GB |
+| $7 | 4 MB | — |
---
@@ -25,27 +100,27 @@
/* libraries/configvars.h — NDK39 */
struct ConfigDev {
struct Node cd_Node;
- UBYTE cd_Flags; /* CDF_* flags */
+ UBYTE cd_Flags; /* CDF_CONFIGME, CDF_BADMEMORY, etc. */
UBYTE cd_Pad;
- struct ExpansionRom cd_Rom; /* AutoConfig ROM data */
+ struct ExpansionRom cd_Rom; /* AutoConfig ROM data (copied) */
APTR cd_BoardAddr; /* assigned base address */
ULONG cd_BoardSize; /* board size in bytes */
UWORD cd_SlotAddr; /* slot address */
UWORD cd_SlotSize;
APTR cd_Driver; /* driver bound to this board */
struct ConfigDev *cd_NextCD; /* next in chain */
- /* ... */
};
struct ExpansionRom {
UBYTE er_Type; /* board type + size code */
- UBYTE er_Product; /* product number */
- UBYTE er_Flags;
+ UBYTE er_Product; /* product number (0–255) */
+ UBYTE er_Flags; /* can shut up, has memory, etc. */
UBYTE er_Reserved03;
- UWORD er_Manufacturer; /* manufacturer ID */
- ULONG er_SerialNumber;
- UWORD er_InitDiagVec; /* boot ROM offset */
- /* ... */
+ UWORD er_Manufacturer; /* manufacturer ID (16-bit) */
+ ULONG er_SerialNumber; /* board serial number */
+ UWORD er_InitDiagVec; /* offset to DiagArea boot ROM */
+ APTR er_Reserved0c;
+ APTR er_Reserved10;
};
```
@@ -57,9 +132,24 @@ struct ExpansionRom {
struct Library *ExpansionBase = OpenLibrary("expansion.library", 0);
struct ConfigDev *cd = NULL;
-while ((cd = FindConfigDev(cd, 2167, 11))) {
- /* manufacturer=2167 (Individual Computers), product=11 (ACA500plus) */
- Printf("Board at $%08lx, size %lu\n", cd->cd_BoardAddr, cd->cd_BoardSize);
+/* Find all boards from a specific manufacturer+product: */
+while ((cd = FindConfigDev(cd, 2167, 11)))
+{
+ Printf("Board at $%08lx, size %lu bytes\n",
+ cd->cd_BoardAddr, cd->cd_BoardSize);
+ Printf(" Manufacturer: %u, Product: %u\n",
+ cd->cd_Rom.er_Manufacturer, cd->cd_Rom.er_Product);
+}
+
+/* Find ANY board: use -1 for wildcard */
+cd = NULL;
+while ((cd = FindConfigDev(cd, -1, -1)))
+{
+ Printf("Mfr=%u Prod=%u at $%08lx (%lu bytes)\n",
+ cd->cd_Rom.er_Manufacturer,
+ cd->cd_Rom.er_Product,
+ cd->cd_BoardAddr,
+ cd->cd_BoardSize);
}
```
@@ -67,12 +157,54 @@ while ((cd = FindConfigDev(cd, 2167, 11))) {
## Common Manufacturer IDs
-| ID | Manufacturer |
+| ID | Manufacturer | Notable Products |
+|---|---|---|
+| 514 | Commodore | A2091 SCSI, A2065 Ethernet, A2232 serial |
+| 1030 | Supra | SupraRAM, SupraDrive |
+| 2017 | GVP | Impact A2000, Series II SCSI+RAM |
+| 2167 | Individual Computers | Buddha, ACA500+, ACA1233 |
+| 2168 | Kupke | Golem RAM |
+| 4096 | University of Lowell | — |
+| 4626 | ACT | Apollo accelerators |
+| 4754 | MacroSystem | Retina, Warp Engine |
+| 8512 | Phase5 | CyberStorm, Blizzard, CyberVision |
+| 12802 | Village Tronic | Picasso II, Picasso IV |
+
+---
+
+## DiagArea — Boot ROMs on Expansion Boards
+
+If `er_InitDiagVec` is non-zero, the board has a boot ROM that runs during expansion configuration:
+
+```c
+struct DiagArea {
+ UBYTE da_Config; /* DAC_WORDWIDE, DAC_BYTEWIDE, etc. */
+ UBYTE da_Flags; /* DAC_CONFIGTIME or DAC_BINDTIME */
+ UWORD da_Size; /* total size of DiagArea */
+ UWORD da_DiagPoint; /* offset to diagnostic routine */
+ UWORD da_BootPoint; /* offset to boot code */
+ char da_Name[]; /* optional handler name */
+};
+```
+
+Boot ROMs are used by:
+- SCSI controllers (to boot from hard disk)
+- Network cards (to boot via TFTP)
+- Accelerator boards (to patch CPU-specific features)
+
+---
+
+## FPGA Implementation Notes
+
+For MiSTer or other FPGA cores emulating Zorro expansion:
+
+| Aspect | Requirement |
|---|---|
-| 514 | Commodore |
-| 2017 | GVP |
-| 2167 | Individual Computers |
-| 8512 | Phase5 |
+| AutoConfig ROM | Must present valid er_Type, er_Product, er_Manufacturer at $E80000 |
+| Address assignment | Must accept base address write and relocate accordingly |
+| CFGIN/CFGOUT chain | Must implement daisy-chain protocol for multi-board configs |
+| Memory type flag | Set `er_Flags` bit 5 if board memory should be added to system pool |
+| Shut-up | Board must go silent after configuration (stop responding at $E80000) |
---
@@ -80,3 +212,6 @@ while ((cd = FindConfigDev(cd, 2167, 11))) {
- NDK39: `libraries/configvars.h`, `libraries/expansion.h`
- ADCD 2.1: expansion.library autodocs
+- Dave Haynie: *"The Amiga Zorro III Bus Specification"* — definitive bus reference
+- See also: [rtg_driver.md](../16_driver_development/rtg_driver.md) — RTG cards use Zorro AutoConfig
+- See also: [device_driver_basics.md](../16_driver_development/device_driver_basics.md) — driver binding to ConfigDev
diff --git a/11_libraries/icon.md b/11_libraries/icon.md
index c6c92f7..348cb4d 100644
--- a/11_libraries/icon.md
+++ b/11_libraries/icon.md
@@ -4,7 +4,28 @@
## Overview
-Every Workbench-visible file has a companion `.info` file containing its icon imagery, tooltypes, and default tool. `icon.library` provides reading/writing these structures.
+Every Workbench-visible file has a companion `.info` file containing its icon imagery, tool types (key=value metadata), default tool, stack size, and position. `icon.library` provides reading, writing, and manipulating these structures.
+
+The `.info` file format is binary, not text. icon.library handles all serialisation.
+
+```mermaid
+flowchart LR
+ subgraph "Disk"
+ FILE["myapp"]
+ INFO["myapp.info"]
+ end
+
+ subgraph "icon.library"
+ GET["GetDiskObject"] --> DO["struct DiskObject"]
+ DO --> IMG["Icon imagery
(selected + unselected)"]
+ DO --> TT["ToolTypes
(key=value strings)"]
+ DO --> DT["DefaultTool
(path to handler)"]
+ DO --> POS["Position
(x, y on Workbench)"]
+ end
+
+ INFO --> GET
+ style DO fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
@@ -14,16 +35,16 @@ Every Workbench-visible file has a companion `.info` file containing its icon im
/* workbench/workbench.h — NDK39 */
struct DiskObject {
UWORD do_Magic; /* $E310 = WB_DISKMAGIC */
- UWORD do_Version; /* WB_DISKVERSION */
- struct Gadget do_Gadget; /* the icon gadget */
- UBYTE do_Type; /* WBDISK, WBTOOL, WBPROJECT, etc. */
+ UWORD do_Version; /* WB_DISKVERSION (1) */
+ struct Gadget do_Gadget; /* the icon gadget (contains imagery) */
+ UBYTE do_Type; /* icon type: WBDISK, WBTOOL, etc. */
char *do_DefaultTool; /* default tool path */
char **do_ToolTypes; /* NULL-terminated string array */
- LONG do_CurrentX; /* icon X position */
+ LONG do_CurrentX; /* icon X position on Workbench */
LONG do_CurrentY; /* icon Y position */
BPTR do_DrawerData; /* drawer window position (BPTR) */
- char *do_ToolWindow; /* tool window spec */
- LONG do_StackSize; /* stack size for tool */
+ char *do_ToolWindow; /* tool window spec (CON: string) */
+ LONG do_StackSize; /* stack size for tool launch */
};
```
@@ -31,36 +52,137 @@ struct DiskObject {
## Icon Types
-| Constant | Value | Description |
-|---|---|---|
-| `WBDISK` | 1 | Disk/volume icon |
-| `WBDRAWER` | 2 | Drawer (directory) |
-| `WBTOOL` | 3 | Executable tool |
-| `WBPROJECT` | 4 | Project (document) |
-| `WBGARBAGE` | 5 | Trashcan |
-| `WBDEVICE` | 6 | Device |
-| `WBKICK` | 7 | Kickstart disk |
-| `WBAPPICON` | 8 | AppIcon (OS 2.0+) |
+| Constant | Value | Description | Workbench Behaviour |
+|---|---|---|---|
+| `WBDISK` | 1 | Disk/volume icon | Opens drawer showing disk contents |
+| `WBDRAWER` | 2 | Drawer (directory) | Opens drawer window |
+| `WBTOOL` | 3 | Executable tool | Launches the program |
+| `WBPROJECT` | 4 | Project (document) | Launches `do_DefaultTool` with this file as argument |
+| `WBGARBAGE` | 5 | Trashcan | Special drawer for deleted files |
+| `WBDEVICE` | 6 | Device | Shown in Workbench root |
+| `WBKICK` | 7 | Kickstart disk | ROM update disk |
+| `WBAPPICON` | 8 | AppIcon (OS 2.0+) | Application-registered dynamic icon |
---
-## ToolTypes
-
-ToolTypes are key=value strings stored in `do_ToolTypes`:
+## Reading Icons
```c
+struct Library *IconBase = OpenLibrary("icon.library", 0);
+
+/* Load a DiskObject from disk: */
+struct DiskObject *dobj = GetDiskObject("SYS:Utilities/MultiView");
+if (dobj)
+{
+ Printf("Type: %ld\n", dobj->do_Type);
+ Printf("Default tool: %s\n", dobj->do_DefaultTool ?
+ dobj->do_DefaultTool : "(none)");
+ Printf("Stack: %ld bytes\n", dobj->do_StackSize);
+
+ FreeDiskObject(dobj);
+}
+
+/* Get the default icon for a file type: */
+struct DiskObject *defIcon = GetDefDiskObject(WBPROJECT);
+/* ... use defIcon ... */
+FreeDiskObject(defIcon);
+```
+
+---
+
+## ToolTypes — Key/Value Metadata
+
+ToolTypes are the Amiga's equivalent of application-specific metadata. They're stored as a NULL-terminated array of strings in the `.info` file.
+
+```c
+/* Example .info file ToolTypes:
+ PUBSCREEN=Workbench
+ NOBLITTER=YES
+ DONOTWAIT
+ CX_PRIORITY=0
+ (DISABLED_OPTION)
+*/
+
struct DiskObject *dobj = GetDiskObject("myapp");
-if (dobj) {
- char *val = FindToolType(dobj->do_ToolTypes, "PUBSCREEN");
- if (val) Printf("Screen: %s\n", val);
- if (MatchToolValue(FindToolType(dobj->do_ToolTypes, "FLAGS"), "DEBUG"))
- Printf("Debug mode\n");
+if (dobj)
+{
+ /* Find a specific ToolType: */
+ char *screen = FindToolType(dobj->do_ToolTypes, "PUBSCREEN");
+ if (screen)
+ Printf("Public screen: %s\n", screen); /* "Workbench" */
+
+ /* Check if a ToolType exists (no value): */
+ if (FindToolType(dobj->do_ToolTypes, "DONOTWAIT"))
+ Printf("DONOTWAIT is set\n");
+
+ /* Check for a specific value within a multi-value ToolType: */
+ char *flags = FindToolType(dobj->do_ToolTypes, "FLAGS");
+ if (MatchToolValue(flags, "DEBUG"))
+ Printf("Debug mode enabled\n");
+
+ /* Enumerate all ToolTypes: */
+ char **tt = dobj->do_ToolTypes;
+ while (*tt)
+ {
+ Printf(" ToolType: %s\n", *tt);
+ tt++;
+ }
+
FreeDiskObject(dobj);
}
```
+### ToolType Conventions
+
+| Convention | Meaning | Example |
+|---|---|---|
+| `KEY=VALUE` | Named setting with value | `PUBSCREEN=Workbench` |
+| `KEY` (no value) | Boolean flag — presence = true | `DONOTWAIT` |
+| `(KEY)` | Disabled/commented out | `(CX_POPUP=NO)` |
+| `KEY=val1|val2` | Multi-value (check with `MatchToolValue`) | `FLAGS=DEBUG|VERBOSE` |
+
+---
+
+## Writing / Modifying Icons
+
+```c
+/* Write a DiskObject to disk: */
+PutDiskObject("myfile", dobj);
+
+/* Create a new icon programmatically: */
+struct DiskObject *newIcon = GetDefDiskObject(WBPROJECT);
+newIcon->do_DefaultTool = "SYS:Utilities/MultiView";
+newIcon->do_StackSize = 8192;
+
+char *toolTypes[] = {
+ "PUBSCREEN=Workbench",
+ "DONOTWAIT",
+ NULL
+};
+newIcon->do_ToolTypes = toolTypes;
+
+PutDiskObject("myfile", newIcon);
+FreeDiskObject(newIcon);
+```
+
+---
+
+## OS 3.5+ New-Style Icons
+
+AmigaOS 3.5 introduced **true-colour icons** (PNG-based) alongside the legacy planar format. The `icon.library` v46+ handles both transparently — `GetDiskObject` returns the best available format.
+
+| Feature | Legacy (OS 1.x–3.1) | New-Style (OS 3.5+) |
+|---|---|---|
+| Format | Planar bitplane imagery | PNG/true-colour embedded |
+| Colours | 4–16 (Workbench palette) | 24-bit true colour |
+| Size | Fixed (standard sizes) | Scalable |
+| Transparency | 1-bit mask | 8-bit alpha channel |
+| Storage | `do_Gadget.GadgetRender` | Extended chunks in `.info` |
+
---
## References
- NDK39: `workbench/workbench.h`, `workbench/icon.h`
+- ADCD 2.1: icon.library autodocs
+- See also: [workbench.md](workbench.md) — Workbench integration (AppIcon, AppWindow)
diff --git a/11_libraries/iffparse.md b/11_libraries/iffparse.md
index 019dafe..d7d276e 100644
--- a/11_libraries/iffparse.md
+++ b/11_libraries/iffparse.md
@@ -4,53 +4,124 @@
## Overview
-IFF (Interchange File Format) is EA/Commodore's universal container format. `iffparse.library` provides stream-oriented parsing/writing of IFF files. Common IFF types: ILBM (images), 8SVX (audio), ANIM (animation), FTXT (formatted text).
+IFF (Interchange File Format) is EA/Commodore's universal container format used throughout the Amiga ecosystem. `iffparse.library` provides stream-oriented parsing and writing of IFF files, handling the nested chunk structure, byte ordering, and padding automatically.
+
+Common IFF types:
+- **ILBM** — Interleaved Bitmap (images)
+- **8SVX** — 8-bit sampled voice (audio)
+- **ANIM** — Animation sequences
+- **FTXT** — Formatted text
+
+```mermaid
+flowchart TD
+ subgraph "IFF ILBM File"
+ FORM["FORM ILBM"] --> BMHD["BMHD
(bitmap header)"]
+ FORM --> CMAP["CMAP
(colour palette)"]
+ FORM --> CAMG["CAMG
(Amiga view mode)"]
+ FORM --> BODY["BODY
(pixel data)"]
+ end
+
+ subgraph "iffparse.library"
+ IH["AllocIFF"] --> INIT["InitIFFasDOS"]
+ INIT --> OPEN["OpenIFF(READ)"]
+ OPEN --> PARSE["ParseIFF(SCAN)"]
+ PARSE --> READ["ReadChunkBytes"]
+ READ --> CLOSE["CloseIFF"]
+ end
+
+ FORM -.-> PARSE
+ style FORM fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
## IFF Structure
+All IFF files follow a nested chunk structure. Everything is **big-endian** (network byte order):
+
```
-FORM
-
-
- ...
+FORM
+ [pad byte if odd size]
+ [pad byte]
+ ...
+
+Nested FORMs:
+ LIST
+ FORM
+ ...
+ FORM
+ ...
```
-All values are big-endian. Chunks are padded to even byte boundaries.
+| Container | Purpose |
+|---|---|
+| `FORM` | A single structured data object |
+| `LIST` | Ordered collection of FORMs |
+| `CAT ` | Unordered concatenation of FORMs |
+| `PROP` | Default property block (within LIST) |
---
-## Key Functions
+## Reading an IFF File
```c
struct Library *IFFParseBase = OpenLibrary("iffparse.library", 0);
struct IFFHandle *iff = AllocIFF();
-/* Open from file: */
+/* Open from AmigaDOS file: */
iff->iff_Stream = (ULONG)Open("image.iff", MODE_OLDFILE);
-InitIFFasDOS(iff);
-OpenIFF(iff, IFFF_READ);
+if (!iff->iff_Stream) { /* error */ }
+
+InitIFFasDOS(iff); /* use DOS Read/Write/Seek hooks */
+
+if (OpenIFF(iff, IFFF_READ)) { /* error */ }
/* Register chunks we want to stop at: */
StopChunk(iff, ID_ILBM, ID_BMHD);
-StopChunk(iff, ID_ILBM, ID_BODY);
StopChunk(iff, ID_ILBM, ID_CMAP);
+StopChunk(iff, ID_ILBM, ID_CAMG);
+StopChunk(iff, ID_ILBM, ID_BODY);
-/* Parse: */
+/* Parse — stops at each registered chunk: */
LONG error;
-while ((error = ParseIFF(iff, IFFPARSE_SCAN)) == 0) {
+while ((error = ParseIFF(iff, IFFPARSE_SCAN)) == 0)
+{
struct ContextNode *cn = CurrentChunk(iff);
- switch (cn->cn_ID) {
+
+ switch (cn->cn_ID)
+ {
case ID_BMHD:
+ {
+ struct BitMapHeader bmhd;
ReadChunkBytes(iff, &bmhd, sizeof(bmhd));
+ Printf("Image: %ldx%ld, %ld planes\n",
+ bmhd.bmh_Width, bmhd.bmh_Height, bmhd.bmh_Depth);
break;
+ }
case ID_CMAP:
- ReadChunkBytes(iff, palette, cn->cn_Size);
+ {
+ UBYTE palette[256 * 3];
+ LONG palSize = ReadChunkBytes(iff, palette, cn->cn_Size);
+ LONG numColours = palSize / 3;
+ Printf("Palette: %ld colours\n", numColours);
break;
+ }
+ case ID_CAMG:
+ {
+ ULONG viewMode;
+ ReadChunkBytes(iff, &viewMode, 4);
+ if (viewMode & HAM) Printf("HAM mode\n");
+ break;
+ }
case ID_BODY:
- ReadChunkBytes(iff, bodydata, cn->cn_Size);
+ {
+ /* Read pixel data (may be compressed) */
+ UBYTE *bodyData = AllocMem(cn->cn_Size, MEMF_ANY);
+ ReadChunkBytes(iff, bodyData, cn->cn_Size);
+ /* ... decompress if bmhd.bmh_Compression == 1 (ByteRun1) ... */
+ FreeMem(bodyData, cn->cn_Size);
break;
+ }
}
}
@@ -61,21 +132,140 @@ FreeIFF(iff);
---
+## Writing an IFF File
+
+```c
+struct IFFHandle *iff = AllocIFF();
+iff->iff_Stream = (ULONG)Open("output.iff", MODE_NEWFILE);
+InitIFFasDOS(iff);
+OpenIFF(iff, IFFF_WRITE);
+
+/* Start the FORM: */
+PushChunk(iff, ID_ILBM, ID_FORM, IFFSIZE_UNKNOWN);
+
+/* Write BMHD chunk: */
+PushChunk(iff, 0, ID_BMHD, sizeof(struct BitMapHeader));
+WriteChunkBytes(iff, &bmhd, sizeof(bmhd));
+PopChunk(iff);
+
+/* Write CMAP chunk: */
+PushChunk(iff, 0, ID_CMAP, numColours * 3);
+WriteChunkBytes(iff, palette, numColours * 3);
+PopChunk(iff);
+
+/* Write BODY chunk: */
+PushChunk(iff, 0, ID_BODY, IFFSIZE_UNKNOWN);
+WriteChunkBytes(iff, bodyData, bodySize);
+PopChunk(iff);
+
+/* Close the FORM: */
+PopChunk(iff);
+
+CloseIFF(iff);
+Close((BPTR)iff->iff_Stream);
+FreeIFF(iff);
+```
+
+---
+
+## ILBM BitMapHeader
+
+```c
+struct BitMapHeader {
+ UWORD bmh_Width; /* image width in pixels */
+ UWORD bmh_Height; /* image height in pixels */
+ WORD bmh_Left; /* x offset (usually 0) */
+ WORD bmh_Top; /* y offset (usually 0) */
+ UBYTE bmh_Depth; /* number of bitplanes */
+ UBYTE bmh_Masking; /* 0=none, 1=hasMask, 2=hasTransparentColor */
+ UBYTE bmh_Compression; /* 0=none, 1=ByteRun1 */
+ UBYTE bmh_Pad;
+ UWORD bmh_Transparent; /* transparent colour index */
+ UBYTE bmh_XAspect; /* pixel aspect ratio */
+ UBYTE bmh_YAspect;
+ WORD bmh_PageWidth; /* source page width */
+ WORD bmh_PageHeight; /* source page height */
+};
+```
+
+---
+
+## ByteRun1 Compression
+
+ILBM BODY data is typically compressed with **ByteRun1** (a simple RLE):
+
+```
+For each byte n:
+ 0..127: copy next n+1 bytes literally
+ -1..-127: repeat next byte (-n+1) times
+ -128: no-op (skip)
+```
+
+```c
+/* Decompress ByteRun1: */
+void DecompressByteRun1(UBYTE *src, UBYTE *dst, LONG dstSize)
+{
+ UBYTE *end = dst + dstSize;
+ while (dst < end)
+ {
+ BYTE n = *src++;
+ if (n >= 0)
+ {
+ LONG count = n + 1;
+ memcpy(dst, src, count);
+ src += count;
+ dst += count;
+ }
+ else if (n != -128)
+ {
+ LONG count = -n + 1;
+ memset(dst, *src++, count);
+ dst += count;
+ }
+ }
+}
+```
+
+---
+
## Common Chunk IDs
-| FORM Type | Chunk | Description |
-|---|---|---|
-| `ILBM` | `BMHD` | Bitmap header (width, height, depth) |
-| `ILBM` | `CMAP` | Colour map (R,G,B triples) |
-| `ILBM` | `BODY` | Image body data |
-| `ILBM` | `CAMG` | Amiga display mode |
-| `8SVX` | `VHDR` | Voice header |
-| `8SVX` | `BODY` | Audio sample data |
-| `ANIM` | `ANHD` | Animation header |
-| `ANIM` | `DLTA` | Delta frame data |
+| FORM Type | Chunk | Size | Description |
+|---|---|---|---|
+| `ILBM` | `BMHD` | 20 | Bitmap header (width, height, depth, compression) |
+| `ILBM` | `CMAP` | n×3 | Colour map (R,G,B triples, 8-bit each) |
+| `ILBM` | `CAMG` | 4 | Amiga display mode (ModeID for ViewPort) |
+| `ILBM` | `BODY` | varies | Pixel data (interleaved bitplanes) |
+| `ILBM` | `CRNG` | 8 | Colour cycling range (DPaint) |
+| `ILBM` | `GRAB` | 4 | Hotspot (cursor/brush grab point) |
+| `8SVX` | `VHDR` | 20 | Voice header (rate, volume, octaves) |
+| `8SVX` | `BODY` | varies | Audio sample data (signed 8-bit) |
+| `ANIM` | `ANHD` | 24 | Animation frame header |
+| `ANIM` | `DLTA` | varies | Delta-compressed frame data |
+| `FTXT` | `CHRS` | varies | Character string data |
+
+---
+
+## Using IFF with Clipboard
+
+```c
+/* Read from clipboard instead of file: */
+struct IFFHandle *iff = AllocIFF();
+struct ClipboardHandle *ch = OpenClipboard(PRIMARY_CLIP);
+iff->iff_Stream = (ULONG)ch;
+InitIFFasClip(iff); /* use clipboard hooks instead of DOS */
+OpenIFF(iff, IFFF_READ);
+/* ... parse as normal ... */
+CloseIFF(iff);
+CloseClipboard(ch);
+FreeIFF(iff);
+```
---
## References
- NDK39: `libraries/iffparse.h`, `datatypes/pictureclass.h`
+- EA IFF-85 specification: the original format definition
+- ADCD 2.1: iffparse.library autodocs
+- See also: [ham_ehb_modes.md](../08_graphics/ham_ehb_modes.md) — HAM-encoded ILBM files
diff --git a/11_libraries/keymap.md b/11_libraries/keymap.md
index e3153a9..90e2632 100644
--- a/11_libraries/keymap.md
+++ b/11_libraries/keymap.md
@@ -4,21 +4,41 @@
## Overview
-`keymap.library` translates raw keycodes from `keyboard.device` into character codes using the active keymap. Each keymap defines the mapping from physical keys to characters, including dead keys and string sequences.
+`keymap.library` translates **raw keycodes** from `keyboard.device` into character codes using the active keymap. The Amiga keyboard generates hardware scancodes (0x00–0x77); the keymap defines how each physical key maps to characters, including shifted variants, dead keys (accented characters), and string sequences (function keys).
+
+```mermaid
+flowchart LR
+ KB["Physical Keyboard"] -->|"Raw scancode
(0x00-0x77)"| KBD["keyboard.device"]
+ KBD -->|"InputEvent
(rawkey + qualifiers)"| KM["keymap.library
MapRawKey"]
+ KM -->|"ASCII / ANSI
character(s)"| APP["Application
or console.device"]
+
+ style KM fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
## Key Functions
```c
-/* Map a raw keycode + qualifiers to ASCII: */
-LONG actual = MapRawKey(&inputEvent, buffer, bufsize, NULL);
-/* Returns number of characters, -1 if buffer too small */
+#include
+#include
-/* Map ANSI code back to raw key: */
-WORD MapANSI(STRPTR string, WORD count,
- STRPTR buffer, WORD length,
- struct KeyMap *keyMap);
+/* Map a raw keycode + qualifiers to characters: */
+struct InputEvent ie;
+ie.ie_Class = IECLASS_RAWKEY;
+ie.ie_Code = rawKeyCode; /* 0x00–0x77 */
+ie.ie_Qualifier = qualifiers; /* IEQUALIFIER_LSHIFT, etc. */
+ie.ie_EventAddress = NULL;
+
+char buffer[16];
+LONG numChars = MapRawKey(&ie, buffer, sizeof(buffer), NULL);
+/* Returns number of characters, or -1 if buffer too small */
+/* numChars=0 means the key doesn't produce a character (e.g., Shift alone) */
+
+/* Map ANSI characters back to raw key events: */
+WORD result = MapANSI(string, numChars,
+ outBuffer, outLength,
+ NULL); /* NULL = use default keymap */
```
---
@@ -28,19 +48,115 @@ WORD MapANSI(STRPTR string, WORD count,
```c
/* devices/keymap.h — NDK39 */
struct KeyMap {
- UBYTE *km_LoKeyMapTypes; /* type array for keys 0x00–0x3F */
- ULONG *km_LoKeyMap; /* mapping array for keys 0x00–0x3F */
- UBYTE *km_LoCapsable; /* caps-lock bitmap */
+ UBYTE *km_LoKeyMapTypes; /* type byte per key, 0x00–0x3F */
+ ULONG *km_LoKeyMap; /* mapping data per key, 0x00–0x3F */
+ UBYTE *km_LoCapsable; /* caps-lock bitmap (1 bit per key) */
UBYTE *km_LoRepeatable; /* auto-repeat bitmap */
- UBYTE *km_HiKeyMapTypes; /* type array for keys 0x40–0x77 */
- ULONG *km_HiKeyMap;
+ UBYTE *km_HiKeyMapTypes; /* type byte per key, 0x40–0x77 */
+ ULONG *km_HiKeyMap; /* mapping data per key, 0x40–0x77 */
UBYTE *km_HiCapsable;
UBYTE *km_HiRepeatable;
};
```
+The keymap is split into two halves:
+- **Lo keys** (0x00–0x3F): main keyboard area (letters, numbers, symbols)
+- **Hi keys** (0x40–0x77): function keys, cursor keys, keypad, special keys
+
+### Key Type Flags
+
+| Flag | Meaning |
+|---|---|
+| `KCF_SHIFT` | Key has shifted variant |
+| `KCF_ALT` | Key has Alt variant |
+| `KCF_CONTROL` | Key has Control variant |
+| `KCF_DEAD` | Dead key (accent prefix — combines with next keypress) |
+| `KCF_STRING` | Key produces a multi-byte string (e.g., function keys → escape sequences) |
+| `KCF_NOP` | Key produces no character |
+
+---
+
+## Raw Keycodes
+
+Key physical positions are fixed across all Amiga keyboards:
+
+| Rawkey | Key | Rawkey | Key |
+|---|---|---|---|
+| `0x00` | \` (backtick) | `0x40` | Space |
+| `0x01` | 1 | `0x41` | Backspace |
+| `0x02` | 2 | `0x42` | Tab |
+| `0x03`–`0x0A` | 3–0 | `0x43` | Enter (keypad) |
+| `0x10`–`0x19` | Q–P row | `0x44` | Return |
+| `0x20`–`0x28` | A–L row | `0x45` | Escape |
+| `0x31`–`0x39` | Z–/ row | `0x46` | Delete |
+| `0x30` | — | `0x4C` | Cursor Up |
+| | | `0x4D` | Cursor Down |
+| | | `0x4E` | Cursor Right |
+| | | `0x4F` | Cursor Left |
+| `0x50`–`0x59` | F1–F10 | `0x60` | Left Shift |
+| | | `0x61` | Right Shift |
+| | | `0x63` | Control |
+| | | `0x64` | Left Alt |
+| | | `0x65` | Right Alt |
+| | | `0x66` | Left Amiga |
+| | | `0x67` | Right Amiga |
+
+> [!NOTE]
+> **Key-up events** have bit 7 set: rawkey `0x80 | keycode`. So rawkey `0xC5` = Escape key released.
+
+---
+
+## Dead Keys (Accented Characters)
+
+Dead keys work in two presses:
+1. Press the dead key (e.g., Alt+H = acute accent ´)
+2. Press the base letter (e.g., e)
+3. Result: é
+
+```c
+/* Dead key handling is automatic in MapRawKey if you pass
+ the previous dead key's InputEvent via ie_EventAddress: */
+struct InputEvent deadEvent;
+/* ... first keypress (dead key) stored here ... */
+
+ie.ie_EventAddress = (APTR)&deadEvent; /* chain to dead key */
+numChars = MapRawKey(&ie, buffer, sizeof(buffer), NULL);
+/* buffer now contains the composed character */
+```
+
+---
+
+## Changing Keymaps
+
+```c
+/* Set a different keymap: */
+struct KeyMap *germanMap;
+/* Load from DEVS:Keymaps/d (German layout) */
+
+/* System-wide change via Preferences: */
+/* Use the Input prefs editor, or: */
+SetKeyMapDefault(newKeyMap);
+```
+
+Available keymaps in `DEVS:Keymaps/`:
+
+| File | Layout |
+|---|---|
+| `usa` | US English (QWERTY) — default |
+| `gb` | British English |
+| `d` | German (QWERTZ) |
+| `f` | French (AZERTY) |
+| `i` | Italian |
+| `e` | Spanish |
+| `dk` | Danish |
+| `s` | Swedish |
+| `n` | Norwegian |
+
---
## References
- NDK39: `devices/keymap.h`, `libraries/keymap.h`
+- ADCD 2.1: keymap.library autodocs
+- See also: [console.md](../10_devices/console.md) — console.device uses keymap for input
+- See also: [input_events.md](../09_intuition/input_events.md) — InputEvent structure
diff --git a/11_libraries/layers.md b/11_libraries/layers.md
index d1e23e3..bbc671d 100644
--- a/11_libraries/layers.md
+++ b/11_libraries/layers.md
@@ -1,50 +1,224 @@
[← Home](../README.md) · [Libraries](README.md)
-# layers.library — Window Clipping Layers
+# layers.library — Window Clipping and Damage Repair
## Overview
-`layers.library` provides the clipping and damage-repair infrastructure that Intuition windows are built on. Each window's `RastPort` is backed by a `Layer` that manages overlapping regions.
+`layers.library` provides the clipping and damage-repair infrastructure that Intuition windows are built on. Every window's `RastPort` is backed by a `Layer` that manages overlapping regions, damage tracking, and optional backing-store for obscured content.
+
+When you draw to a window, all drawing operations are automatically clipped to the window's visible area by the layer system. You never draw "outside" your window or over another window — layers enforces this transparently.
+
+```mermaid
+flowchart TD
+ subgraph "Screen BitMap"
+ subgraph "Layer A (front)"
+ LA["Fully visible
ClipRect covers
entire layer"]
+ end
+ subgraph "Layer B (middle)"
+ LB["Partially obscured
by Layer A"]
+ LB1["ClipRect 1
(visible)"]
+ LB2["ClipRect 2
(obscured)"]
+ end
+ subgraph "Layer C (back)"
+ LC["Mostly obscured"]
+ end
+ end
+
+ DRAW["Draw into Layer B"] --> CLIP["layers.library
clips to visible
ClipRects only"]
+ CLIP --> LB1
+
+ style LA fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style LB fill:#fff9c4,stroke:#f9a825,color:#333
+ style LC fill:#ffcdd2,stroke:#c62828,color:#333
+ style CLIP fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
## Layer Types
-| Flag | Type | Description |
-|---|---|---|
-| `LAYERSIMPLE` | Simple Refresh | Application must redraw damaged areas |
-| `LAYERSMART` | Smart Refresh | System saves obscured regions |
-| `LAYERSUPER` | Super BitMap | Application provides full-size bitmap |
-| `LAYERBACKDROP` | Backdrop | Behind all other layers |
+| Flag | Type | Backing Store | Damage Handling | Memory Cost |
+|---|---|---|---|---|
+| `LAYERSIMPLE` | Simple Refresh | None | App must redraw on `IDCMP_REFRESHWINDOW` | Minimal |
+| `LAYERSMART` | Smart Refresh | Auto — obscured regions saved/restored | Automatic — OS handles damage | Moderate |
+| `LAYERSUPER` | Super BitMap | Full off-screen bitmap (app provides) | Full bitmap always valid | High |
+| `LAYERBACKDROP` | Backdrop | Modifier — behind all normal layers | Depends on refresh type | — |
+
+```mermaid
+flowchart LR
+ subgraph "Simple Refresh"
+ S1["Window obscured"] --> S2["Damage region
recorded"]
+ S2 --> S3["IDCMP_REFRESHWINDOW
sent to app"]
+ S3 --> S4["App redraws
between Begin/EndRefresh"]
+ end
+
+ subgraph "Smart Refresh"
+ M1["Window obscured"] --> M2["Obscured area
saved to buffer"]
+ M2 --> M3["Window revealed"] --> M4["OS restores
from buffer"]
+ end
+
+ style S3 fill:#ffcdd2,stroke:#c62828,color:#333
+ style M4 fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
---
-## Key Functions
+## ClipRects — The Clipping Engine
+
+Each layer maintains a linked list of **ClipRects** — rectangles that define how each region of the layer should be handled:
+
+```c
+struct ClipRect {
+ struct ClipRect *Next; /* next in chain */
+ struct ClipRect *prev; /* previous */
+ struct Layer *lobs; /* layer that obscures this rect (NULL if visible) */
+ struct BitMap *BitMap; /* backing store bitmap (Smart Refresh) */
+ LONG reserved;
+ LONG Flags;
+ struct Rectangle bounds; /* x1,y1,x2,y2 of this rect */
+};
+```
+
+When drawing to a partially obscured layer:
+1. The drawing call (e.g., `RectFill`) enters `graphics.library`
+2. Graphics detects `rp->Layer != NULL`
+3. For each **visible ClipRect**, the drawing is performed clipped to that rectangle
+4. For **obscured ClipRects** (Smart Refresh), drawing goes to the backing-store bitmap instead
+5. The application sees nothing — clipping is fully transparent
+
+---
+
+## Creating Layers Directly
+
+While Intuition normally creates layers for windows, you can create them manually for custom display systems:
```c
struct Layer_Info *li = NewLayerInfo();
-struct Layer *layer = CreateUpfrontLayer(li, bitmap,
- x1, y1, x2, y2, LAYERSMART, NULL);
+/* Create a layer on top: */
+struct Layer *frontLayer = CreateUpfrontLayer(li, screenBitMap,
+ 10, 10, 200, 100, /* bounds: x1, y1, x2, y2 */
+ LAYERSMART, /* type */
+ NULL); /* super bitmap (NULL for non-SUPER) */
-/* Lock before drawing: */
+/* Create behind existing layers: */
+struct Layer *backLayer = CreateBehindLayer(li, screenBitMap,
+ 50, 50, 250, 150,
+ LAYERSIMPLE | LAYERBACKDROP,
+ NULL);
+
+/* Get the RastPort for drawing: */
+struct RastPort *rp = frontLayer->rp;
+SetAPen(rp, 1);
+RectFill(rp, 0, 0, 190, 90); /* coordinates relative to layer */
+```
+
+---
+
+## Layer Operations
+
+### Locking
+
+**Always lock a layer before drawing** if other tasks might modify it simultaneously:
+
+```c
LockLayer(0, layer);
-/* ... draw into layer->rp ... */
+/* ... safe to draw ... */
UnlockLayer(layer);
-/* Move: */
+/* Lock ALL layers (for bulk operations): */
+LockLayers(layerInfo);
+UnlockLayers(layerInfo);
+
+/* Lock a layer for multiple operations: */
+LockLayerInfo(layerInfo);
+/* ... manipulate layer stack ... */
+UnlockLayerInfo(layerInfo);
+```
+
+### Moving and Resizing
+
+```c
+/* Move layer by delta: */
MoveLayer(0, layer, dx, dy);
-/* Resize: */
+/* Resize layer by delta: */
SizeLayer(0, layer, dw, dh);
-/* Cleanup: */
+/* Move to front/back of stack: */
+UpfrontLayer(0, layer);
+BehindLayer(0, layer);
+```
+
+### Damage and Refresh
+
+```c
+/* For Simple Refresh windows — handle IDCMP_REFRESHWINDOW: */
+BeginRefresh(window);
+/* ... redraw damaged area only ... */
+/* The ClipRect list is temporarily set to damaged regions only */
+EndRefresh(window, TRUE); /* TRUE = damage fully repaired */
+```
+
+### Install Backfill Hook
+
+```c
+/* Custom backfill instead of default (clear to pen 0): */
+struct Hook backfillHook;
+backfillHook.h_Entry = (HOOKFUNC)MyBackfillFunc;
+InstallLayerHook(layer, &backfillHook);
+
+/* The hook is called whenever a region needs to be filled
+ (e.g., when a window is moved and new area is exposed) */
+```
+
+---
+
+## Super BitMap Layers
+
+Super BitMap layers maintain a full-sized off-screen bitmap that the application owns. The visible portion is blitted to the screen; the rest is always valid in the off-screen buffer:
+
+```c
+/* Allocate the full bitmap: */
+struct BitMap *superBM = AllocBitMap(640, 480, depth,
+ BMF_CLEAR, NULL);
+
+/* Create super bitmap layer: */
+struct Layer *superLayer = CreateUpfrontLayer(li, screenBitMap,
+ 0, 0, 319, 255, /* visible area on screen */
+ LAYERSUPER,
+ superBM); /* the full-size bitmap */
+
+/* Scroll the view within the super bitmap: */
+ScrollLayer(0, superLayer, dx, dy);
+
+/* Sync super bitmap with display after drawing: */
+SyncSBitMap(superLayer);
+
+/* Copy display back to super bitmap before hiding: */
+CopySBitMap(superLayer);
+```
+
+---
+
+## Cleanup
+
+```c
+/* Remove layers in reverse order: */
DeleteLayer(0, layer);
+
+/* Dispose the layer info: */
DisposeLayerInfo(li);
+
+/* If using super bitmap, free it after DeleteLayer: */
+FreeBitMap(superBM);
```
---
## References
-- NDK39: `graphics/layers.h`, `graphics/clip.h`
+- NDK39: `graphics/layers.h`, `graphics/clip.h`, `graphics/gfx.h`
+- ADCD 2.1: layers.library autodocs
+- See also: [rastport.md](../08_graphics/rastport.md) — drawing through layers
+- See also: [screens.md](../09_intuition/screens.md) — Intuition screen/window layer management
diff --git a/11_libraries/locale.md b/11_libraries/locale.md
index f448429..74d7824 100644
--- a/11_libraries/locale.md
+++ b/11_libraries/locale.md
@@ -4,39 +4,180 @@
## Overview
-`locale.library` (OS 2.1+) provides language-aware string lookup, date/number formatting, and character classification for internationalised applications.
+`locale.library` (OS 2.1+) provides the Amiga's internationalisation (i18n) framework: language-aware string lookup via catalogues, locale-sensitive date/number/currency formatting, and character classification. Applications that use locale.library display in the user's preferred language automatically.
+
+```mermaid
+flowchart TD
+ APP["Application"] -->|"GetCatalogStr(cat, ID, fallback)"| LOC["locale.library"]
+ LOC -->|"Looks up string ID"| CAT["myapp.catalog
(LOCALE:Catalogs/deutsch/)"]
+ CAT -->|"Found"| DE["'Datei öffnen'"]
+ LOC -->|"Not found"| FB["Fallback:
'Open File'"]
+
+ style LOC fill:#e8f4fd,stroke:#2196f3,color:#333
+ style CAT fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
---
-## Core Pattern
+## Catalogue System
+
+### Creating String IDs
+
+```c
+/* Typically in a generated header (from CatComp or FlexCat): */
+#define MSG_OPEN_FILE 1
+#define MSG_SAVE_FILE 2
+#define MSG_QUIT 3
+#define MSG_ERROR_NOTFOUND 100
+
+/* Built-in English strings (fallback): */
+static const char *builtinStrings[] = {
+ [MSG_OPEN_FILE] = "Open File",
+ [MSG_SAVE_FILE] = "Save File",
+ [MSG_QUIT] = "Quit",
+ [MSG_ERROR_NOTFOUND] = "File not found",
+};
+```
+
+### Using Catalogues
```c
struct Library *LocaleBase = OpenLibrary("locale.library", 38);
+
+/* Open the application's catalogue: */
struct Catalog *cat = OpenCatalog(NULL, "myapp.catalog",
- OC_BuiltInLanguage, "english",
- TAG_DONE);
+ OC_BuiltInLanguage, (ULONG)"english",
+ TAG_DONE);
+/* NULL for first arg = use current locale */
-/* Get localised string: */
-STRPTR str = GetCatalogStr(cat, MSG_HELLO, "Hello"); /* fallback */
+/* Get localised string (with English fallback): */
+STRPTR openStr = GetCatalogStr(cat, MSG_OPEN_FILE, "Open File");
+/* Returns German "Datei öffnen" if German catalogue exists,
+ otherwise the fallback "Open File" */
+/* Use throughout the application: */
+Printf("%s\n", GetCatalogStr(cat, MSG_QUIT, "Quit"));
+
+/* Cleanup: */
CloseCatalog(cat);
CloseLibrary(LocaleBase);
```
+### Catalogue File Structure
+
+```
+LOCALE:Catalogs/deutsch/myapp.catalog ← German
+LOCALE:Catalogs/français/myapp.catalog ← French
+LOCALE:Catalogs/italiano/myapp.catalog ← Italian
+```
+
+Catalogues are compiled from `.cd` (catalogue description) and `.ct` (catalogue translation) files using **CatComp** or **FlexCat**:
+
+```
+; myapp.cd — catalogue description
+MSG_OPEN_FILE (1//)
+Open File
+;
+MSG_SAVE_FILE (2//)
+Save File
+;
+```
+
+```
+; myapp_deutsch.ct — German translation
+MSG_OPEN_FILE
+Datei öffnen
+;
+MSG_SAVE_FILE
+Datei speichern
+;
+```
+
---
## Locale-Aware Formatting
```c
-struct Locale *loc = OpenLocale(NULL); /* user's default */
+struct Locale *loc = OpenLocale(NULL); /* user's default locale */
-/* Format a date: */
-FormatDate(loc, "%A %e %B %Y", &datestamp, &hook);
+Printf("Country: %s\n", loc->loc_CountryName);
+Printf("Language: %s\n", loc->loc_PrefLanguages[0]);
+Printf("Decimal: '%s'\n", loc->loc_DecimalPoint); /* "." or "," */
+Printf("Grouping: '%s'\n", loc->loc_GroupSeparator); /* "," or "." */
+Printf("Currency: '%s'\n", loc->loc_MonCS); /* "$", "€", "£" */
+```
-/* Format a number: */
-/* Uses loc->loc_GroupSeparator, loc->loc_DecimalPoint */
+### Date Formatting
-CloseLocale(loc);
+```c
+/* Format a date stamp according to locale: */
+struct DateStamp ds;
+DateStamp(&ds);
+
+/* FormatDate uses a hook to receive characters: */
+char dateBuf[64];
+int pos = 0;
+
+/* Simple hook that fills a buffer: */
+void __saveds __asm DateHookFunc(
+ register __a0 struct Hook *hook,
+ register __a1 char ch)
+{
+ char *buf = hook->h_Data;
+ buf[pos++] = ch;
+ buf[pos] = 0;
+}
+
+struct Hook dateHook;
+dateHook.h_Entry = (HOOKFUNC)DateHookFunc;
+dateHook.h_Data = dateBuf;
+
+FormatDate(loc, "%A, %e %B %Y", &ds, &dateHook);
+/* Result (German locale): "Mittwoch, 23 April 2025" */
+/* Result (US locale): "Wednesday, 23 April 2025" */
+```
+
+### Format Codes
+
+| Code | Output | Example |
+|---|---|---|
+| `%A` | Full weekday name | "Wednesday" / "Mittwoch" |
+| `%a` | Abbreviated weekday | "Wed" / "Mi" |
+| `%B` | Full month name | "April" |
+| `%b` | Abbreviated month | "Apr" |
+| `%d` | Day (01–31) | "23" |
+| `%e` | Day (1–31, no leading zero) | "23" |
+| `%H` | Hour (00–23) | "14" |
+| `%I` | Hour (01–12) | "02" |
+| `%M` | Minute (00–59) | "30" |
+| `%S` | Second (00–59) | "00" |
+| `%p` | AM/PM | "PM" |
+| `%Y` | 4-digit year | "2025" |
+| `%y` | 2-digit year | "25" |
+
+---
+
+## Character Classification
+
+```c
+/* Locale-aware character checks: */
+if (IsAlpha(loc, ch)) /* alphabetic (language-aware) */
+if (IsUpper(loc, ch)) /* uppercase */
+if (IsLower(loc, ch)) /* lowercase */
+if (IsDigit(loc, ch)) /* digit */
+if (IsAlNum(loc, ch)) /* alphanumeric */
+if (IsPunct(loc, ch)) /* punctuation */
+if (IsSpace(loc, ch)) /* whitespace */
+
+/* Locale-aware case conversion: */
+char upper = ConvToUpper(loc, ch);
+char lower = ConvToLower(loc, ch);
+
+/* Locale-aware string comparison: */
+LONG result = StrnCmp(loc, str1, str2, -1, SC_COLLATE2);
+/* SC_ASCII = byte comparison */
+/* SC_COLLATE1 = primary collation (ignores accents) */
+/* SC_COLLATE2 = full collation */
```
---
@@ -44,3 +185,5 @@ CloseLocale(loc);
## References
- NDK39: `libraries/locale.h`
+- ADCD 2.1: locale.library autodocs
+- CatComp / FlexCat documentation for catalogue compilation
diff --git a/11_libraries/rexxsyslib.md b/11_libraries/rexxsyslib.md
index d7bc28a..a5bf537 100644
--- a/11_libraries/rexxsyslib.md
+++ b/11_libraries/rexxsyslib.md
@@ -1,32 +1,87 @@
[← Home](../README.md) · [Libraries](README.md)
-# rexxsyslib.library — ARexx Interface
+# rexxsyslib.library — ARexx Scripting Interface
## Overview
-ARexx is the Amiga's built-in macro/scripting language (based on REXX). `rexxsyslib.library` provides APIs for hosting ARexx ports, sending commands, and receiving results.
+ARexx is the Amiga's built-in macro/scripting language (based on IBM's REXX). Any application can host an **ARexx port** to receive commands from scripts and other applications — this is the Amiga's primary inter-application communication mechanism for user-facing scripting.
+
+```mermaid
+flowchart LR
+ SCRIPT["ARexx Script
(text file)"] -->|"Sends command
to named port"| PORT["Application
ARexx Port
(e.g., 'MYAPP')"]
+ PORT -->|"Parses command
returns result"| SCRIPT
+
+ APP2["Other Application"] -->|"FindPort + PutMsg"| PORT
+
+ style PORT fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
-## Adding an ARexx Port
+## Adding an ARexx Port to Your Application
```c
-struct MsgPort *arexxPort = CreateMsgPort();
-arexxPort->mp_Node.ln_Name = "MYAPP";
-arexxPort->mp_Node.ln_Pri = 0;
-AddPort(arexxPort);
+#include
+#include
-/* In event loop, check for ARexx messages: */
-struct RexxMsg *rmsg;
-while ((rmsg = (struct RexxMsg *)GetMsg(arexxPort))) {
- STRPTR cmd = ARG0(rmsg); /* the command string */
- /* Parse and execute command... */
- rmsg->rm_Result1 = 0; /* RC = 0 (success) */
- rmsg->rm_Result2 = 0;
- ReplyMsg((struct Message *)rmsg);
+struct MsgPort *arexxPort = CreateMsgPort();
+arexxPort->mp_Node.ln_Name = "MYAPP"; /* public port name */
+arexxPort->mp_Node.ln_Pri = 0;
+AddPort(arexxPort); /* make it publicly findable */
+
+/* In your main event loop, combine with IDCMP: */
+ULONG arexxSig = 1 << arexxPort->mp_SigBit;
+ULONG idcmpSig = 1 << window->UserPort->mp_SigBit;
+
+ULONG sigs = Wait(arexxSig | idcmpSig);
+
+if (sigs & arexxSig)
+{
+ struct RexxMsg *rmsg;
+ while ((rmsg = (struct RexxMsg *)GetMsg(arexxPort)))
+ {
+ STRPTR cmd = ARG0(rmsg); /* the command string */
+
+ /* Parse and execute: */
+ if (stricmp(cmd, "QUIT") == 0)
+ {
+ rmsg->rm_Result1 = 0; /* RC = 0 (success) */
+ rmsg->rm_Result2 = 0;
+ running = FALSE;
+ }
+ else if (strnicmp(cmd, "OPEN ", 5) == 0)
+ {
+ char *filename = cmd + 5;
+ LONG rc = OpenFile(filename);
+ rmsg->rm_Result1 = rc ? 0 : 10; /* 0=ok, 10=error */
+ rmsg->rm_Result2 = 0;
+ }
+ else if (stricmp(cmd, "VERSION") == 0)
+ {
+ rmsg->rm_Result1 = 0;
+ /* Return a string result: */
+ if (rmsg->rm_Action & RXFF_RESULT)
+ rmsg->rm_Result2 = (LONG)CreateArgstring("1.0", 3);
+ }
+ else
+ {
+ rmsg->rm_Result1 = 5; /* RC_WARN — unknown command */
+ rmsg->rm_Result2 = 0;
+ }
+
+ ReplyMsg((struct Message *)rmsg);
+ }
}
+/* Cleanup: */
RemPort(arexxPort);
+/* Drain remaining messages: */
+struct RexxMsg *rmsg;
+while ((rmsg = (struct RexxMsg *)GetMsg(arexxPort)))
+{
+ rmsg->rm_Result1 = 20; /* RC_FATAL */
+ ReplyMsg((struct Message *)rmsg);
+}
DeleteMsgPort(arexxPort);
```
@@ -35,17 +90,34 @@ DeleteMsgPort(arexxPort);
## Sending ARexx Commands
```c
-struct MsgPort *replyPort = CreateMsgPort();
-struct RexxMsg *rmsg = CreateRexxMsg(replyPort, NULL, NULL);
-rmsg->rm_Args[0] = CreateArgstring("QUIT", 4);
-rmsg->rm_Action = RXCOMM;
+struct Library *RexxSysBase = OpenLibrary("rexxsyslib.library", 0);
+struct MsgPort *replyPort = CreateMsgPort();
+struct RexxMsg *rmsg = CreateRexxMsg(replyPort, NULL, "MYAPP");
+rmsg->rm_Args[0] = (STRPTR)CreateArgstring("OPEN test.txt", 13);
+rmsg->rm_Action = RXCOMM | RXFF_RESULT;
+
+/* Find the target application's port: */
+Forbid();
struct MsgPort *target = FindPort("TARGETAPP");
-if (target) {
+if (target)
+{
PutMsg(target, &rmsg->rm_Node);
+ Permit();
WaitPort(replyPort);
GetMsg(replyPort);
- /* rmsg->rm_Result1 = return code */
+
+ Printf("Return code: %ld\n", rmsg->rm_Result1);
+ if (rmsg->rm_Result1 == 0 && rmsg->rm_Result2)
+ {
+ Printf("Result: %s\n", (char *)rmsg->rm_Result2);
+ DeleteArgstring((STRPTR)rmsg->rm_Result2);
+ }
+}
+else
+{
+ Permit();
+ Printf("Target port not found\n");
}
DeleteArgstring(rmsg->rm_Args[0]);
@@ -55,6 +127,50 @@ DeleteMsgPort(replyPort);
---
+## ARexx Script Example
+
+```rexx
+/* MyScript.rexx — control MYAPP from ARexx */
+ADDRESS 'MYAPP'
+
+OPEN 'work:data/image.iff'
+IF RC > 0 THEN DO
+ SAY 'Failed to open file, RC=' RC
+ EXIT 10
+END
+
+VERSION
+SAY 'App version:' RESULT
+QUIT
+```
+
+---
+
+## Return Codes
+
+| Value | Constant | Meaning |
+|---|---|---|
+| 0 | `RC_OK` | Success |
+| 5 | `RC_WARN` | Warning (command not understood, etc.) |
+| 10 | `RC_ERROR` | Error (command failed) |
+| 20 | `RC_FATAL` | Fatal error (port shutting down) |
+
+---
+
+## Common ARexx-Aware Applications
+
+| Application | Port Name | Example Commands |
+|---|---|---|
+| Workbench | `WORKBENCH` | `OPEN`, `CLOSE`, `WINDOW` |
+| MultiView | `MULTIVIEW.n` | `OPEN`, `PRINT`, `QUIT` |
+| IBrowse | `IBROWSE` | `GOTOURL`, `RELOAD` |
+| CygnusEd | `rexx_ced` | `OPEN`, `SAVE`, `MARK`, `CUT` |
+| TextPad | `TEXTPAD` | `LOAD`, `SAVE`, `FIND` |
+
+---
+
## References
- NDK39: `rexx/storage.h`, `rexx/rxslib.h`
+- ADCD 2.1: rexxsyslib.library autodocs
+- See also: [process_management.md](../07_dos/process_management.md) — process/task message ports
diff --git a/11_libraries/utility.md b/11_libraries/utility.md
index d368339..f97acc6 100644
--- a/11_libraries/utility.md
+++ b/11_libraries/utility.md
@@ -4,83 +4,200 @@
## Overview
-`utility.library` (OS 2.0+) provides the universal tag-based parameter passing system, callback hooks, and date/time utilities used throughout AmigaOS.
+`utility.library` (OS 2.0+) provides the universal tag-based parameter passing system, callback hooks, and date/time utilities used throughout AmigaOS. Nearly every OS 2.0+ API uses TagItem lists for extensible parameter passing — understanding this library is essential.
---
## TagItem System
+### Structure
+
```c
/* utility/tagitem.h — NDK39 */
struct TagItem {
ULONG ti_Tag; /* tag identifier */
- ULONG ti_Data; /* tag value */
+ ULONG ti_Data; /* tag value (ULONG — often cast from pointer) */
};
+```
-/* Special tag values: */
-#define TAG_DONE 0 /* end of tag list */
-#define TAG_END TAG_DONE
-#define TAG_IGNORE 1 /* skip this tag */
-#define TAG_MORE 2 /* ti_Data = pointer to another TagItem array */
-#define TAG_SKIP 3 /* skip next ti_Data tags */
-#define TAG_USER (1<<31) /* user-defined tags start here */
+### Special Tags
+
+| Tag | Value | Purpose |
+|---|---|---|
+| `TAG_DONE` | 0 | Terminates a tag list — must be the last entry |
+| `TAG_IGNORE` | 1 | Skip this tag (placeholder) |
+| `TAG_MORE` | 2 | `ti_Data` = pointer to another TagItem array (chain lists) |
+| `TAG_SKIP` | 3 | Skip next `ti_Data` tags in the list |
+| `TAG_USER` | `1<<31` | User-defined tags start at this value |
+
+### How Tag Lists Work
+
+```mermaid
+flowchart LR
+ subgraph "Tag List (array)"
+ T1["SA_Width
640"] --> T2["SA_Height
480"]
+ T2 --> T3["SA_Depth
8"]
+ T3 --> TM["TAG_MORE
→ extraTags"]
+ end
+
+ subgraph "Chained List"
+ E1["SA_Title
'My Screen'"] --> E2["TAG_DONE
0"]
+ end
+
+ TM --> E1
+
+ style TM fill:#fff9c4,stroke:#f9a825,color:#333
+ style E2 fill:#ffcdd2,stroke:#c62828,color:#333
+```
+
+```c
+/* Typical usage: */
+struct Screen *scr = OpenScreenTags(NULL,
+ SA_Width, 640,
+ SA_Height, 480,
+ SA_Depth, 8,
+ SA_Title, (ULONG)"My Screen",
+ SA_ShowTitle, TRUE,
+ TAG_DONE);
+
+/* Tag list as array: */
+struct TagItem tags[] = {
+ { SA_Width, 640 },
+ { SA_Height, 480 },
+ { SA_Depth, 8 },
+ { TAG_DONE, 0 }
+};
+struct Screen *scr = OpenScreenTagList(NULL, tags);
```
### Tag Utility Functions
| Function | Description |
|---|---|
-| `FindTagItem(tag, tagList)` | Find first matching tag |
-| `GetTagData(tag, default, tagList)` | Get tag value with default |
-| `NextTagItem(&tagListPtr)` | Iterate through tags |
-| `TagInArray(tag, array)` | Check if tag is in an array |
-| `FilterTagItems(tagList, filter, logic)` | Filter tag list |
-| `CloneTagItems(tagList)` | Allocate copy of tag list |
-| `FreeTagItems(tagList)` | Free cloned tag list |
-| `MapTags(tagList, mapList, flags)` | Remap tag IDs |
+| `FindTagItem(tag, tagList)` | Find first matching tag; returns `TagItem *` or NULL |
+| `GetTagData(tag, default, tagList)` | Get tag value, or default if tag not found |
+| `NextTagItem(&tagListPtr)` | Iterator — handles TAG_MORE, TAG_SKIP, TAG_IGNORE transparently |
+| `TagInArray(tag, array)` | Check if a tag ID is in a ULONG array |
+| `FilterTagItems(tagList, filter, logic)` | Remove/keep tags matching a filter array |
+| `CloneTagItems(tagList)` | Allocate a copy of the entire tag list |
+| `FreeTagItems(tagList)` | Free a cloned tag list |
+| `MapTags(tagList, mapList, flags)` | Remap tag IDs (for converting between APIs) |
+| `PackBoolTags(initialFlags, tagList, boolMap)` | Convert boolean tags to a flags word |
+
+### Iterating a Tag List
+
+```c
+/* The correct way to iterate — handles all special tags: */
+struct TagItem *tag;
+struct TagItem *tstate = tagList;
+
+while ((tag = NextTagItem(&tstate)))
+{
+ switch (tag->ti_Tag)
+ {
+ case MY_WIDTH: width = tag->ti_Data; break;
+ case MY_HEIGHT: height = tag->ti_Data; break;
+ case MY_TITLE: title = (char *)tag->ti_Data; break;
+ }
+}
+```
+
+> [!IMPORTANT]
+> **Never iterate tag lists manually** with `for` loops. Always use `NextTagItem()` — it correctly handles `TAG_MORE` chains, `TAG_SKIP`, and `TAG_IGNORE`. Manual iteration will break on chained or filtered lists.
---
## Hook System
+Hooks provide a standardised callback mechanism used throughout AmigaOS — Intuition, BOOPSI, layers.library, and locale.library all use them.
+
+### Structure
+
```c
/* utility/hooks.h */
struct Hook {
- struct MinNode h_MinNode;
+ struct MinNode h_MinNode; /* for linking into lists */
ULONG (*h_Entry)(void); /* assembler entry point */
ULONG (*h_SubEntry)(void); /* C function pointer */
APTR h_Data; /* user data */
};
```
-Convention: `h_Entry` receives `A0=hook, A2=object, A1=message`.
+### Register Convention
+
+When a hook is called, registers are set up as:
+- **A0** = pointer to the `Hook` itself
+- **A2** = the "object" (context-dependent)
+- **A1** = the "message" (context-dependent)
```c
-/* Convenience macro for SAS/C and GCC: */
-ULONG myHookFunc(struct Hook *hook __asm("a0"),
- Object *obj __asm("a2"),
- APTR msg __asm("a1"))
+/* SAS/C / GCC with register args: */
+ULONG __saveds __asm MyHookFunc(
+ register __a0 struct Hook *hook,
+ register __a2 APTR object,
+ register __a1 APTR message)
{
- /* ... */
+ struct MyData *data = (struct MyData *)hook->h_Data;
+ /* ... process callback ... */
return 0;
}
-struct Hook myHook = { {NULL}, (HOOKFUNC)myHookFunc, NULL, myData };
+/* Initialise the hook: */
+struct Hook myHook;
+myHook.h_Entry = (HOOKFUNC)HookEntry; /* utility.library glue */
+myHook.h_SubEntry = (HOOKFUNC)MyHookFunc;
+myHook.h_Data = myPrivateData;
```
+### Common Hook Uses
+
+| API | Object (A2) | Message (A1) |
+|---|---|---|
+| BOOPSI `OM_SET` | BOOPSI object | `opSet` message |
+| `InstallLayerHook` | Layer | `struct BackFillMessage` |
+| Locale `FormatString` | — | Format data |
+| `DoMethod` (MUI) | MUI object | Method message |
+
---
## Date Utilities
```c
+#include
+
+struct ClockData {
+ UWORD sec; /* 0–59 */
+ UWORD min; /* 0–59 */
+ UWORD hour; /* 0–23 */
+ UWORD mday; /* 1–31 */
+ UWORD month; /* 1–12 */
+ UWORD year; /* 1978+ */
+ UWORD wday; /* 0=Sunday */
+};
+
+/* Convert Amiga timestamp to date components: */
struct ClockData cd;
-Amiga2Date(seconds, &cd); /* seconds since 1.1.1978 → date */
-ULONG secs = Date2Amiga(&cd); /* date → seconds */
-ULONG secs = CheckDate(&cd); /* validate and convert */
+Amiga2Date(seconds, &cd);
+Printf("%02d/%02d/%04d %02d:%02d:%02d\n",
+ cd.mday, cd.month, cd.year,
+ cd.hour, cd.min, cd.sec);
+
+/* Convert date to Amiga timestamp: */
+cd.year = 2024; cd.month = 3; cd.mday = 15;
+cd.hour = 14; cd.min = 30; cd.sec = 0;
+ULONG secs = Date2Amiga(&cd);
+
+/* Validate a date and get timestamp: */
+ULONG secs = CheckDate(&cd); /* returns 0 if invalid */
```
+> [!NOTE]
+> The Amiga epoch is **January 1, 1978 00:00:00 UTC**. To convert to/from Unix timestamps (epoch 1970-01-01), add/subtract **252,460,800** seconds (8 years + 2 leap days).
+
---
## References
- NDK39: `utility/tagitem.h`, `utility/hooks.h`, `utility/date.h`
+- ADCD 2.1: utility.library autodocs
+- See also: [boopsi.md](../09_intuition/boopsi.md) — BOOPSI uses hooks extensively
diff --git a/11_libraries/workbench.md b/11_libraries/workbench.md
index 6259f89..f6cdcf7 100644
--- a/11_libraries/workbench.md
+++ b/11_libraries/workbench.md
@@ -4,67 +4,185 @@
## Overview
-`workbench.library` provides APIs for interacting with the Workbench desktop: AppWindows, AppIcons, AppMenuItems, and startup message handling.
+`workbench.library` provides APIs for interacting with the Workbench desktop environment: receiving startup arguments, registering **AppWindows** (drag-and-drop targets), **AppIcons** (desktop icons), and **AppMenuItems** (Workbench Tools menu entries).
+
+```mermaid
+flowchart TD
+ subgraph "Workbench"
+ WB["Workbench Desktop"]
+ WB --> AW["AppWindow
(file drop target)"]
+ WB --> AI["AppIcon
(custom desktop icon)"]
+ WB --> AM["AppMenuItem
(Tools menu entry)"]
+ end
+
+ subgraph "Application"
+ PORT["MsgPort"] --> LOOP["Event Loop"]
+ LOOP --> HANDLE["Handle AppMessage"]
+ end
+
+ AW -->|"User drops file"| PORT
+ AI -->|"User clicks icon"| PORT
+ AM -->|"User selects menu"| PORT
+
+ style AW fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style AI fill:#fff9c4,stroke:#f9a825,color:#333
+ style AM fill:#e8f4fd,stroke:#2196f3,color:#333
+```
---
## WBStartup Message
-When launched from Workbench, a program receives a `WBStartup` message instead of CLI arguments:
+When launched from Workbench (double-click an icon), the program receives a `WBStartup` message instead of CLI arguments:
```c
struct WBStartup {
struct Message sm_Message;
struct MsgPort *sm_Process; /* process that sent us */
- BPTR sm_Segment; /* our loaded segment */
- LONG sm_NumArgs; /* number of arguments */
- char *sm_ToolWindow; /* tool window spec */
- struct WBArg *sm_ArgList; /* argument array */
+ BPTR sm_Segment; /* our loaded segment list */
+ LONG sm_NumArgs; /* number of arguments (1 = just the tool) */
+ char *sm_ToolWindow; /* tool window spec (CON: string) */
+ struct WBArg *sm_ArgList; /* array of arguments */
};
struct WBArg {
- BPTR wa_Lock; /* directory lock */
- BYTE *wa_Name; /* filename */
+ BPTR wa_Lock; /* directory lock (BPTR) */
+ BYTE *wa_Name; /* filename (relative to wa_Lock) */
};
```
### Handling WBStartup
```c
+/* In main(): */
struct WBStartup *wbmsg = NULL;
-if (!argc) { /* launched from Workbench */
- WaitPort(&((struct Process *)FindTask(NULL))->pr_MsgPort);
- wbmsg = (struct WBStartup *)GetMsg(
- &((struct Process *)FindTask(NULL))->pr_MsgPort);
- /* Process wbmsg->sm_ArgList for file arguments */
+
+if (argc == 0)
+{
+ /* Launched from Workbench — get the startup message: */
+ struct Process *me = (struct Process *)FindTask(NULL);
+ WaitPort(&me->pr_MsgPort);
+ wbmsg = (struct WBStartup *)GetMsg(&me->pr_MsgPort);
+
+ /* ArgList[0] = the tool itself (our icon) */
+ /* ArgList[1..n] = files the user shift-clicked or dropped on us */
+
+ /* CD to the tool's directory for file access: */
+ BPTR oldDir = CurrentDir(wbmsg->sm_ArgList[0].wa_Lock);
+
+ /* Process dropped files: */
+ for (int i = 1; i < wbmsg->sm_NumArgs; i++)
+ {
+ BPTR dir = wbmsg->sm_ArgList[i].wa_Lock;
+ char *name = wbmsg->sm_ArgList[i].wa_Name;
+ BPTR old = CurrentDir(dir);
+ /* ... open and process file 'name' ... */
+ CurrentDir(old);
+ }
+
+ /* Read our ToolTypes: */
+ struct DiskObject *dobj = GetDiskObject(wbmsg->sm_ArgList[0].wa_Name);
+ if (dobj)
+ {
+ char *pubscreen = FindToolType(dobj->do_ToolTypes, "PUBSCREEN");
+ FreeDiskObject(dobj);
+ }
+
+ CurrentDir(oldDir);
}
-/* ... do work ... */
-if (wbmsg) {
+
+/* ... main program logic ... */
+
+/* MUST reply before exiting: */
+if (wbmsg)
+{
Forbid();
ReplyMsg((struct Message *)wbmsg);
}
```
+> [!CAUTION]
+> You **must** reply the WBStartup message before exiting, and you **must** call `Forbid()` before `ReplyMsg()`. If you reply without Forbid, Workbench may unload your code segment before your process finishes exiting → crash.
+
---
-## AppWindow / AppIcon / AppMenuItem
+## AppWindow — File Drop Target
+
+Register a window to receive files when the user drags icons to it:
```c
-/* Register a window to receive file drops: */
-struct AppWindow *appwin = AddAppWindow(1, 0, win, port, NULL);
-/* When user drags files to the window, receive AppMessage on port */
+struct MsgPort *appPort = CreateMsgPort();
-struct AppMessage {
- struct Message am_Message;
- UWORD am_Type; /* AMTYPE_APPWINDOW, etc. */
- ULONG am_UserData;
- ULONG am_ID;
- LONG am_NumArgs; /* number of files dropped */
- struct WBArg *am_ArgList; /* array of file references */
- /* ... */
-};
+/* Register the window: */
+struct AppWindow *appWin = AddAppWindow(
+ 1, /* ID (your choice — returned in AppMessage) */
+ 0, /* user data */
+ window, /* the Intuition Window */
+ appPort, /* port for AppMessages */
+ NULL); /* tags (NULL = defaults) */
-RemoveAppWindow(appwin);
+/* In event loop: */
+struct AppMessage *amsg;
+while ((amsg = (struct AppMessage *)GetMsg(appPort)))
+{
+ for (int i = 0; i < amsg->am_NumArgs; i++)
+ {
+ BPTR old = CurrentDir(amsg->am_ArgList[i].wa_Lock);
+ Printf("Dropped: %s\n", amsg->am_ArgList[i].wa_Name);
+ /* ... open and process file ... */
+ CurrentDir(old);
+ }
+ ReplyMsg((struct Message *)amsg);
+}
+
+/* Cleanup: */
+RemoveAppWindow(appWin);
+DeleteMsgPort(appPort);
+```
+
+---
+
+## AppIcon — Desktop Icon
+
+Place a custom icon on the Workbench desktop:
+
+```c
+struct DiskObject *icon = GetDiskObject("PROGDIR:myapp");
+
+struct AppIcon *appIcon = AddAppIcon(
+ 1, /* ID */
+ 0, /* user data */
+ "My App", /* label under the icon */
+ appPort, /* port for messages */
+ NULL, /* lock (NULL = Workbench root) */
+ icon, /* the icon imagery */
+ NULL); /* tags */
+
+/* When user double-clicks or drops files on the AppIcon: */
+/* → AppMessage received on appPort (same as AppWindow) */
+
+RemoveAppIcon(appIcon);
+FreeDiskObject(icon);
+```
+
+---
+
+## AppMenuItem — Tools Menu Entry
+
+Add an entry to the Workbench "Tools" menu:
+
+```c
+struct AppMenuItem *appMenu = AddAppMenuItem(
+ 1, /* ID */
+ 0, /* user data */
+ "My Tool", /* menu text */
+ appPort, /* port for messages */
+ NULL); /* tags */
+
+/* When user selects the menu item: */
+/* → AppMessage received, am_NumArgs=0 (no file args) */
+
+RemoveAppMenuItem(appMenu);
```
---
@@ -72,3 +190,5 @@ RemoveAppWindow(appwin);
## References
- NDK39: `workbench/workbench.h`, `workbench/startup.h`
+- ADCD 2.1: workbench.library autodocs
+- See also: [icon.md](icon.md) — icon.library for reading .info files and ToolTypes
diff --git a/12_networking/README.md b/12_networking/README.md
index c46512c..c15bc6c 100644
--- a/12_networking/README.md
+++ b/12_networking/README.md
@@ -2,11 +2,13 @@
# Networking — Overview
+AmigaOS has no built-in TCP/IP stack. Third-party stacks (AmiTCP, Miami, Roadshow) provide `bsdsocket.library` — a BSD-compatible socket API in user-space. Network hardware is abstracted via the SANA-II device driver standard.
+
## Section Index
| File | Description |
|---|---|
-| [bsdsocket.md](bsdsocket.md) | bsdsocket.library — BSD socket API |
-| [sana2.md](sana2.md) | SANA-II — standard network device driver interface |
-| [tcp_ip_stacks.md](tcp_ip_stacks.md) | Stack comparison: AmiTCP, Miami, Roadshow |
-| [protocols.md](protocols.md) | Protocol implementation: DHCP, DNS, HTTP |
+| [tcp_ip_stacks.md](tcp_ip_stacks.md) | Stack architecture: Amiga vs Unix model, SANA-II integration, PPP/SLIP dial-up, modem setup, Ethernet cards, MiSTer virtual NIC, detailed stack comparison |
+| [bsdsocket.md](bsdsocket.md) | BSD socket API: per-task library base, LVO table, WaitSelect with Exec signals, error handling, BSD/POSIX differences |
+| [sana2.md](sana2.md) | SANA-II driver specification: buffer management hooks, send/receive patterns, device query, driver requirements |
+| [protocols.md](protocols.md) | Protocol implementation: DNS resolution, TCP client/server, WaitSelect integration, UDP, DHCP sequence |
diff --git a/12_networking/bsdsocket.md b/12_networking/bsdsocket.md
index e6f3c9d..d2681c5 100644
--- a/12_networking/bsdsocket.md
+++ b/12_networking/bsdsocket.md
@@ -1,23 +1,39 @@
[← Home](../README.md) · [Networking](README.md)
-# bsdsocket.library — BSD Socket API
+# bsdsocket.library — BSD Socket API Reference
## Overview
-`bsdsocket.library` is the AmigaOS implementation of the BSD socket API. It is provided by the active TCP/IP stack (AmiTCP, Miami, Roadshow) and presents a POSIX-like socket interface adapted to the Amiga's library-based architecture.
+`bsdsocket.library` is the AmigaOS implementation of the BSD socket API. It is provided by the active TCP/IP stack (AmiTCP, Miami, Roadshow). See [TCP/IP Stacks](tcp_ip_stacks.md) for how the stack architecture works and how it differs from Unix. See [Protocols](protocols.md) for working code examples.
---
-## Opening
+## Per-Task Library Base
+
+Unlike Unix (kernel-managed fd table), each Amiga task must open its own private `SocketBase`:
```c
struct Library *SocketBase = OpenLibrary("bsdsocket.library", 4);
-if (!SocketBase) { /* no TCP/IP stack running */ }
+if (!SocketBase)
+{
+ Printf("No TCP/IP stack running\n");
+ return;
+}
+
+/* Configure per-task error variable: */
+LONG errno;
+SocketBaseTags(
+ SBTM_SETVAL(SBTC_ERRNOPTR(sizeof(LONG))), (ULONG)&errno,
+ SBTM_SETVAL(SBTC_LOGTAGPTR), (ULONG)"myapp",
+ TAG_DONE);
```
+> [!CAUTION]
+> **Never share `SocketBase` between tasks.** Each task MUST `OpenLibrary` its own copy. Sharing causes socket state corruption and random crashes. This is the #1 Amiga networking bug.
+
---
-## Core Functions (LVO Mapping)
+## Function Table (LVO Mapping)
| LVO | Function | BSD Equivalent |
|---|---|---|
@@ -36,58 +52,83 @@ if (!SocketBase) { /* no TCP/IP stack running */ }
| −102 | `gethostbyname(name)` | `gethostbyname()` |
| −108 | `gethostbyaddr(addr, len, type)` | `gethostbyaddr()` |
| −114 | `getnetbyname(name)` | `getnetbyname()` |
-| −168 | `Errno()` | `errno` (returns last error) |
+| −168 | `Errno()` | `errno` |
| −174 | `CloseSocket(sock)` | `close()` |
| −180 | `WaitSelect(nfds, rd, wr, ex, timeout, sigmask)` | `select()` + signals |
| −210 | `inet_addr(cp)` | `inet_addr()` |
| −216 | `Inet_NtoA(in)` | `inet_ntoa()` |
| −222 | `inet_makeaddr(net, host)` | `inet_makeaddr()` |
| −252 | `getservbyname(name, proto)` | `getservbyname()` |
+| −270 | `SocketBaseTagList(tags)` | (Amiga-specific) |
---
## WaitSelect — Amiga-Enhanced select()
-Unlike BSD `select()`, `WaitSelect` integrates with Exec signals:
+The key Amiga-specific extension: `WaitSelect` simultaneously waits on socket file descriptors **and** Exec signal bits. This replaces the need for separate threads — a single event loop can handle sockets, windows, timers, and ARexx:
```c
-fd_set rdset;
-FD_ZERO(&rdset);
-FD_SET(sock, &rdset);
-
-ULONG sigmask = SIGBREAKF_CTRL_C; /* also wait for Ctrl-C */
-struct timeval tv = { 5, 0 }; /* 5 second timeout */
-
-LONG n = WaitSelect(sock + 1, &rdset, NULL, NULL, &tv, &sigmask);
-if (n > 0 && FD_ISSET(sock, &rdset)) { /* data ready */ }
-if (sigmask & SIGBREAKF_CTRL_C) { /* user pressed Ctrl-C */ }
+LONG WaitSelect(LONG nfds,
+ fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
+ struct timeval *timeout,
+ ULONG *sigmask);
+/* sigmask: on entry = signals to also wait for
+ on exit = which signals fired */
```
+See [protocols.md](protocols.md) for a complete working example combining socket I/O with Intuition window events.
+
---
-## Simple TCP Client
+## Error Handling
```c
+/* Amiga sockets use Errno() instead of global errno: */
LONG sock = socket(AF_INET, SOCK_STREAM, 0);
-struct sockaddr_in addr;
-addr.sin_family = AF_INET;
-addr.sin_port = htons(80);
-addr.sin_addr.s_addr = inet_addr("93.184.216.34");
-
-if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
- send(sock, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n", 38, 0);
- char buf[4096];
- LONG n = recv(sock, buf, sizeof(buf) - 1, 0);
- buf[n] = 0;
- Printf("%s\n", buf);
+if (sock < 0)
+{
+ LONG err = Errno();
+ Printf("socket() failed: error %ld\n", err);
}
-CloseSocket(sock);
+
+/* Or set up a per-task errno pointer (recommended): */
+LONG myErrno;
+SocketBaseTags(
+ SBTM_SETVAL(SBTC_ERRNOPTR(sizeof(LONG))), (ULONG)&myErrno,
+ TAG_DONE);
+/* Now myErrno is updated automatically after each call */
```
+### Common Error Codes
+
+| Value | Name | Meaning |
+|---|---|---|
+| 48 | `EADDRINUSE` | Address already in use |
+| 60 | `ETIMEDOUT` | Connection timed out |
+| 61 | `ECONNREFUSED` | Connection refused |
+| 64 | `EHOSTDOWN` | Host is down |
+| 65 | `EHOSTUNREACH` | No route to host |
+
+---
+
+## Differences from BSD/POSIX Sockets
+
+| Aspect | BSD/POSIX | Amiga bsdsocket.library |
+|---|---|---|
+| Close socket | `close(fd)` | `CloseSocket(sock)` — `close()` is AmigaDOS |
+| Error variable | Global `errno` | Call `Errno()` or set `SBTC_ERRNOPTR` |
+| Multiplexing | `select()` / `poll()` / `epoll()` | `WaitSelect()` — also waits on Exec signals |
+| Headers | `` | `` or stack-specific |
+| Lifecycle | Kernel-managed | Must `OpenLibrary`/`CloseLibrary` per task |
+| fd namespace | Shared with files | Sockets are separate from DOS file handles |
+| Multi-thread | fd shared across threads | `SocketBase` is per-task, not shareable |
+
---
## References
-- NDK39: `libraries/bsdsocket.h` (stack-specific)
- Roadshow SDK documentation
-- [sana2.md](sana2.md) — network device driver layer
+- AmiTCP SDK: `bsdsocket.h` and autodocs
+- See also: [tcp_ip_stacks.md](tcp_ip_stacks.md) — stack architecture and configuration
+- See also: [protocols.md](protocols.md) — working code examples (TCP, UDP, DNS)
+- See also: [sana2.md](sana2.md) — network device driver layer below the stack
diff --git a/12_networking/protocols.md b/12_networking/protocols.md
index 787208d..eaadce7 100644
--- a/12_networking/protocols.md
+++ b/12_networking/protocols.md
@@ -1,67 +1,245 @@
[← Home](../README.md) · [Networking](README.md)
-# Protocol Implementation — DHCP, DNS, HTTP
+# Protocol Implementation — DNS, TCP, UDP, WaitSelect
## Overview
-With `bsdsocket.library` providing a BSD-compatible API, standard network protocols can be implemented using familiar patterns adapted for the Amiga's single-address-space, library-based architecture.
+Working code examples for common network protocols on AmigaOS using [bsdsocket.library](bsdsocket.md). See [TCP/IP Stacks](tcp_ip_stacks.md) for the architecture that makes this possible and how it differs from Unix.
+
+> [!NOTE]
+> Amiga socket functions are **library calls** (via `bsdsocket.library`), not system calls. You must `OpenLibrary("bsdsocket.library", ...)` before using any socket function. Use `CloseSocket()` instead of `close()`, and `Errno()` instead of `errno`.
---
## DNS Resolution
```c
-/* gethostbyname — provided by bsdsocket.library: */
+struct Library *SocketBase = OpenLibrary("bsdsocket.library", 4);
+if (!SocketBase) return;
+
struct hostent *he = gethostbyname("www.amiga.org");
-if (he) {
- struct in_addr addr = *(struct in_addr *)he->h_addr;
- Printf("IP: %s\n", Inet_NtoA(addr.s_addr));
+if (he)
+{
+ struct in_addr addr;
+ CopyMem(he->h_addr, &addr, sizeof(addr));
+ Printf("Host: %s\n", he->h_name);
+ Printf("IP: %s\n", Inet_NtoA(addr.s_addr));
+
+ /* May have multiple addresses: */
+ char **p;
+ for (p = he->h_addr_list; *p; p++)
+ {
+ CopyMem(*p, &addr, sizeof(addr));
+ Printf(" Addr: %s\n", Inet_NtoA(addr.s_addr));
+ }
}
+else
+{
+ Printf("DNS lookup failed\n");
+}
+
+CloseLibrary(SocketBase);
```
---
-## HTTP Client (Minimal)
+## TCP Client
```c
+struct Library *SocketBase = OpenLibrary("bsdsocket.library", 4);
+if (!SocketBase) return;
+
LONG sock = socket(AF_INET, SOCK_STREAM, 0);
+if (sock < 0) { Printf("socket() failed\n"); goto out; }
+
struct hostent *he = gethostbyname("www.example.com");
+if (!he) { Printf("DNS failed\n"); goto close; }
+
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(80);
CopyMem(he->h_addr, &sa.sin_addr, he->h_length);
-connect(sock, (struct sockaddr *)&sa, sizeof(sa));
+if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
+{
+ Printf("connect failed: %ld\n", Errno());
+ goto close;
+}
+/* Send HTTP request: */
char req[] = "GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n";
send(sock, req, strlen(req), 0);
-char buf[8192];
-LONG total = 0, n;
-while ((n = recv(sock, buf + total, sizeof(buf) - total - 1, 0)) > 0)
- total += n;
-buf[total] = 0;
-Printf("%s\n", buf);
+/* Receive response: */
+char buf[4096];
+LONG n;
+while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0)
+{
+ buf[n] = 0;
+ Printf("%s", buf);
+}
-CloseSocket(sock);
+close:
+ CloseSocket(sock);
+out:
+ CloseLibrary(SocketBase);
```
---
-## DHCP Overview
+## TCP Server
-DHCP on Amiga is typically handled by the TCP/IP stack itself (Roadshow, Miami) or an external client (AmiTCP + `dhclient`). The sequence is standard:
+```c
+LONG listenSock = socket(AF_INET, SOCK_STREAM, 0);
-1. `DHCPDISCOVER` — broadcast on port 67
+struct sockaddr_in sa;
+sa.sin_family = AF_INET;
+sa.sin_port = htons(8080);
+sa.sin_addr.s_addr = INADDR_ANY;
+
+bind(listenSock, (struct sockaddr *)&sa, sizeof(sa));
+listen(listenSock, 5);
+
+Printf("Listening on port 8080...\n");
+
+while (running)
+{
+ struct sockaddr_in clientAddr;
+ LONG addrLen = sizeof(clientAddr);
+ LONG clientSock = accept(listenSock,
+ (struct sockaddr *)&clientAddr,
+ &addrLen);
+ if (clientSock >= 0)
+ {
+ Printf("Connection from %s\n",
+ Inet_NtoA(clientAddr.sin_addr.s_addr));
+
+ char response[] = "HTTP/1.0 200 OK\r\n"
+ "Content-Type: text/html\r\n\r\n"
+ "Hello from Amiga!
\n";
+ send(clientSock, response, strlen(response), 0);
+ CloseSocket(clientSock);
+ }
+}
+
+CloseSocket(listenSock);
+```
+
+---
+
+## UDP Datagram
+
+```c
+LONG udpSock = socket(AF_INET, SOCK_DGRAM, 0);
+
+/* Send: */
+struct sockaddr_in dest;
+dest.sin_family = AF_INET;
+dest.sin_port = htons(9999);
+dest.sin_addr.s_addr = inet_addr("192.168.1.255");
+
+char msg[] = "Hello UDP";
+sendto(udpSock, msg, strlen(msg), 0,
+ (struct sockaddr *)&dest, sizeof(dest));
+
+/* Receive: */
+char buf[1024];
+struct sockaddr_in from;
+LONG fromLen = sizeof(from);
+LONG n = recvfrom(udpSock, buf, sizeof(buf), 0,
+ (struct sockaddr *)&from, &fromLen);
+buf[n] = 0;
+Printf("From %s: %s\n", Inet_NtoA(from.sin_addr.s_addr), buf);
+
+CloseSocket(udpSock);
+```
+
+---
+
+## WaitSelect — Combined Socket + GUI Event Loop
+
+The key pattern for responsive Amiga network applications. `WaitSelect` simultaneously waits on sockets **and** Exec signals (windows, ARexx, timers) — no threads needed:
+
+```c
+/* Set up signal masks: */
+ULONG winSig = 1 << window->UserPort->mp_SigBit;
+ULONG ctrlSig = SIGBREAKF_CTRL_C;
+
+fd_set readFDs;
+struct timeval tv;
+
+BOOL running = TRUE;
+while (running)
+{
+ FD_ZERO(&readFDs);
+ FD_SET(sock, &readFDs);
+ tv.tv_secs = 1;
+ tv.tv_micro = 0;
+
+ ULONG sigmask = winSig | ctrlSig;
+
+ LONG result = WaitSelect(sock + 1, &readFDs, NULL, NULL,
+ &tv, &sigmask);
+
+ /* Socket data ready? */
+ if (result > 0 && FD_ISSET(sock, &readFDs))
+ {
+ LONG n = recv(sock, buffer, sizeof(buffer), 0);
+ if (n > 0)
+ {
+ /* process network data */
+ }
+ else
+ {
+ /* connection closed or error */
+ running = FALSE;
+ }
+ }
+
+ /* Window event? */
+ if (sigmask & winSig)
+ {
+ struct IntuiMessage *imsg;
+ while ((imsg = (struct IntuiMessage *)GetMsg(window->UserPort)))
+ {
+ switch (imsg->Class)
+ {
+ case IDCMP_CLOSEWINDOW:
+ running = FALSE;
+ break;
+ /* ... handle other IDCMP events ... */
+ }
+ ReplyMsg((struct Message *)imsg);
+ }
+ }
+
+ /* Ctrl-C? */
+ if (sigmask & ctrlSig)
+ {
+ Printf("*** Break\n");
+ running = FALSE;
+ }
+}
+```
+
+> [!TIP]
+> `WaitSelect` eliminates the need for multi-threading in most Amiga network apps. A single event loop handles sockets, GUI, timers, and ARexx simultaneously — simpler and safer than threads in a non-protected memory environment.
+
+---
+
+## DHCP
+
+DHCP is handled by the TCP/IP stack, not by applications. See [TCP/IP Stacks](tcp_ip_stacks.md) for configuration. The standard sequence is:
+
+1. `DHCPDISCOVER` — stack broadcasts on port 67
2. `DHCPOFFER` — server responds with IP offer
-3. `DHCPREQUEST` — client accepts
-4. `DHCPACK` — server confirms; lease begins
-
-For MiSTer/FPGA cores with custom SANA-II drivers, DHCP is handled automatically once the SANA-II driver is online and the stack is configured for `IPTYPE=DHCP`.
+3. `DHCPREQUEST` — stack accepts
+4. `DHCPACK` — server confirms; interface is configured
---
## References
-- [bsdsocket.md](bsdsocket.md) — socket API reference
-- [tcp_ip_stacks.md](tcp_ip_stacks.md) — stack configuration
+- [bsdsocket.md](bsdsocket.md) — API reference and LVO table
+- [tcp_ip_stacks.md](tcp_ip_stacks.md) — stack architecture and configuration
+- [sana2.md](sana2.md) — SANA-II driver layer
diff --git a/12_networking/sana2.md b/12_networking/sana2.md
index 7f4a509..a2f022c 100644
--- a/12_networking/sana2.md
+++ b/12_networking/sana2.md
@@ -4,75 +4,198 @@
## Overview
-SANA-II is the standard device driver interface for network hardware on AmigaOS. It defines a uniform API that TCP/IP stacks use to communicate with any network card (Ethernet, WiFi, PPP, etc.).
+SANA-II (Standard Amiga Networking Architecture, version II) is the standardised `exec.device` interface between TCP/IP stacks and network hardware drivers. Any SANA-II compliant driver works with any SANA-II compliant stack — providing hardware independence analogous to NDIS on Windows or the Linux kernel network driver model.
----
+See [TCP/IP Stacks](tcp_ip_stacks.md) for how SANA-II fits into the full networking architecture.
-## Architecture
+```mermaid
+flowchart TD
+ subgraph "TCP/IP Stacks (any)"
+ AT["AmiTCP"]
+ RS["Roadshow"]
+ MI["Miami"]
+ end
-```
-Application
- ↓
-bsdsocket.library (TCP/IP stack)
- ↓
-SANA-II device driver (e.g., prism2.device, a2065.device)
- ↓
-Network hardware
+ subgraph "SANA-II API (standardised)"
+ API["CMD_READ / CMD_WRITE
S2_ONLINE / S2_OFFLINE
S2_DEVICEQUERY
Buffer Management Hooks"]
+ end
+
+ subgraph "SANA-II Drivers (any)"
+ A2065["a2065.device
(Ethernet)"]
+ ARIADNE["ariadne.device
(Ethernet)"]
+ PPP["ppp.device
(dial-up)"]
+ MISTER["mister_eth.device
(MiSTer virtual)"]
+ end
+
+ AT --> API
+ RS --> API
+ MI --> API
+ API --> A2065
+ API --> ARIADNE
+ API --> PPP
+ API --> MISTER
+
+ style API fill:#fff9c4,stroke:#f9a825,color:#333
```
---
## Opening a SANA-II Device
+SANA-II drivers require **buffer management hooks** — callback functions the driver uses to copy packet data to/from the application's buffers:
+
```c
+#include
+
+struct MsgPort *port = CreateMsgPort();
struct IOSana2Req *s2req = (struct IOSana2Req *)
CreateIORequest(port, sizeof(struct IOSana2Req));
-/* Provide buffer management hooks: */
-static struct TagItem s2tags[] = {
- { S2_CopyToBuff, (ULONG)CopyToBuff },
- { S2_CopyFromBuff, (ULONG)CopyFromBuff },
+/* Buffer management hooks (required): */
+static struct TagItem bufferTags[] = {
+ { S2_CopyToBuff, (ULONG)MyCopyToBuff },
+ { S2_CopyFromBuff, (ULONG)MyCopyFromBuff },
{ TAG_DONE, 0 }
};
-s2req->ios2_BufferManagement = s2tags;
+s2req->ios2_BufferManagement = bufferTags;
-OpenDevice("a2065.device", 0, (struct IORequest *)s2req, 0);
+if (OpenDevice("a2065.device", 0, (struct IORequest *)s2req, 0))
+{
+ Printf("Cannot open network device\n");
+}
```
+### Buffer Management Hooks
+
+```c
+/* Called by the driver to copy received data into your buffer: */
+BOOL __asm MyCopyToBuff(register __a0 APTR to,
+ register __a1 APTR from,
+ register __d0 ULONG length)
+{
+ CopyMem(from, to, length);
+ return TRUE;
+}
+
+/* Called by the driver to copy transmit data from your buffer: */
+BOOL __asm MyCopyFromBuff(register __a0 APTR to,
+ register __a1 APTR from,
+ register __d0 ULONG length)
+{
+ CopyMem(from, to, length);
+ return TRUE;
+}
+```
+
+> [!NOTE]
+> Buffer management hooks exist because SANA-II drivers may use DMA or special memory regions. The hooks let the driver control how data is copied — the stack never accesses driver buffers directly.
+
---
## Commands
-| Code | Constant | Description |
-|---|---|---|
-| 2 | `CMD_READ` | Read a packet |
-| 3 | `CMD_WRITE` | Send a packet |
-| 9 | `S2_DEVICEQUERY` | Query hardware capabilities |
-| 10 | `S2_GETSTATIONADDRESS` | Get MAC address |
-| 11 | `S2_CONFIGINTERFACE` | Configure interface (set MAC) |
-| 14 | `S2_ONLINE` | Bring interface online |
-| 15 | `S2_OFFLINE` | Take interface offline |
-| 21 | `S2_GETGLOBALSTATS` | Get packet statistics |
-| 16 | `S2_ADDMULTICASTADDRESS` | Add multicast address |
-| 17 | `S2_DELMULTICASTADDRESS` | Remove multicast address |
+| Code | Constant | Direction | Description |
+|---|---|---|---|
+| 2 | `CMD_READ` | Receive | Read a packet (kept outstanding; completed when packet arrives) |
+| 3 | `CMD_WRITE` | Transmit | Send a packet |
+| 9 | `S2_DEVICEQUERY` | Query | Get hardware capabilities (type, MTU, speed) |
+| 10 | `S2_GETSTATIONADDRESS` | Query | Get hardware (MAC) address |
+| 11 | `S2_CONFIGINTERFACE` | Config | Set the hardware address |
+| 14 | `S2_ONLINE` | Control | Bring interface up (start receiving) |
+| 15 | `S2_OFFLINE` | Control | Take interface down |
+| 16 | `S2_ADDMULTICASTADDRESS` | Config | Subscribe to multicast address |
+| 17 | `S2_DELMULTICASTADDRESS` | Config | Unsubscribe from multicast |
+| 21 | `S2_GETGLOBALSTATS` | Query | Get packet/byte counters |
+| 22 | `S2_GETSPECIALSTATS` | Query | Get driver-specific statistics |
---
-## Packet Reading
+## Sending a Packet
```c
+/* Prepare an Ethernet frame: */
+s2req->ios2_Req.io_Command = CMD_WRITE;
+s2req->ios2_WireError = 0;
+s2req->ios2_PacketType = 0x0800; /* IPv4 EtherType */
+s2req->ios2_DataLength = packetLength;
+
+/* Set destination MAC: */
+CopyMem(destMAC, s2req->ios2_DstAddr, 6);
+
+/* ios2_Data points to the packet payload (after Ethernet header): */
+s2req->ios2_Data = packetData;
+
+DoIO((struct IORequest *)s2req);
+if (s2req->ios2_Req.io_Error)
+ Printf("Send error: %ld (wire: %ld)\n",
+ s2req->ios2_Req.io_Error, s2req->ios2_WireError);
+```
+
+---
+
+## Receiving Packets
+
+The stack posts **multiple read requests** to keep a pipeline full:
+
+```c
+/* Post a read request (non-blocking): */
s2req->ios2_Req.io_Command = CMD_READ;
s2req->ios2_WireError = 0;
-s2req->ios2_PacketType = 0x0800; /* IPv4 */
+s2req->ios2_PacketType = 0x0800; /* only IPv4 packets */
SendIO((struct IORequest *)s2req);
-/* wait for packet... */
+
+/* Wait for a packet: */
WaitIO((struct IORequest *)s2req);
-/* s2req->ios2_Data* contains the received packet */
+
+Printf("Received %ld bytes from %02x:%02x:%02x:%02x:%02x:%02x\n",
+ s2req->ios2_DataLength,
+ s2req->ios2_SrcAddr[0], s2req->ios2_SrcAddr[1],
+ s2req->ios2_SrcAddr[2], s2req->ios2_SrcAddr[3],
+ s2req->ios2_SrcAddr[4], s2req->ios2_SrcAddr[5]);
+
+/* Immediately post another read to keep the pipeline full: */
+s2req->ios2_Req.io_Command = CMD_READ;
+SendIO((struct IORequest *)s2req);
```
---
+## Querying Device Capabilities
+
+```c
+struct Sana2DeviceQuery query;
+query.SizeAvailable = sizeof(query);
+
+s2req->ios2_Req.io_Command = S2_DEVICEQUERY;
+s2req->ios2_StatData = &query;
+DoIO((struct IORequest *)s2req);
+
+Printf("Hardware type: %ld\n", query.HardwareType); /* 1 = Ethernet */
+Printf("MTU: %ld bytes\n", query.MTU); /* 1500 for Ethernet */
+Printf("Speed: %ld bps\n", query.BPS); /* 10000000 for 10 Mbps */
+Printf("Address size: %ld bits\n", query.AddrFieldSize); /* 48 for Ethernet MAC */
+```
+
+---
+
+## Writing a SANA-II Driver
+
+A SANA-II driver is a standard exec.device that implements the SANA-II command set. Key requirements:
+
+| Requirement | Detail |
+|---|---|
+| Must be an exec.device | Standard `Open/Close/BeginIO/AbortIO` entry points |
+| Buffer management | Must use the caller's buffer hooks — never copy directly |
+| Multiple openers | Must support multiple tasks opening the device simultaneously |
+| Promiscuous mode | Should support `S2_ONEVENT` for link-level monitoring |
+| Error reporting | Must set both `io_Error` and `ios2_WireError` |
+
+---
+
## References
-- SANA-II Network Device Driver Specification (Commodore)
+- SANA-II Network Device Driver Specification (Commodore, 1992)
- NDK39: `devices/sana2.h`
+- Aminet: `docs/hard/sana2.lha` — specification document
+- See also: [tcp_ip_stacks.md](tcp_ip_stacks.md) — stack architecture
+- See also: [bsdsocket.md](bsdsocket.md) — socket API above SANA-II
diff --git a/12_networking/tcp_ip_stacks.md b/12_networking/tcp_ip_stacks.md
index a50783a..ecfd436 100644
--- a/12_networking/tcp_ip_stacks.md
+++ b/12_networking/tcp_ip_stacks.md
@@ -1,66 +1,426 @@
[← Home](../README.md) · [Networking](README.md)
-# TCP/IP Stacks — AmiTCP, Miami, Roadshow
+# TCP/IP Stacks — Architecture, Integration, and Configuration
## Overview
-AmigaOS has no built-in TCP/IP stack. Third-party stacks provide `bsdsocket.library`. All stacks present the same API to applications — only configuration and driver support differ.
+AmigaOS has **no built-in TCP/IP stack** — networking is entirely provided by third-party software that installs as an Amiga shared library (`bsdsocket.library`). This is fundamentally different from every other major OS where TCP/IP is a kernel subsystem.
---
-## Stack Comparison
+## Architecture — How Amiga TCP/IP Differs
-| Feature | AmiTCP 3.0b2 | Miami 3.2 | Roadshow 1.15 |
-|---|---|---|---|
-| License | Free (Genesis fork) | Commercial | Commercial (demo available) |
-| API version | bsdsocket.library v3 | v4 | v4 |
-| IPv6 | No | No | No |
-| PPP | Via serial | Built-in | Via driver |
-| DHCP | External (dhclient) | Built-in | Built-in |
-| DNS cache | No | Yes | Yes |
-| SANA-II | Yes | Yes | Yes |
-| GUI config | MUI prefs | Miami prefs | Roadshow prefs |
-| Active development | No | No | Yes (Olaf Barthel) |
-| MiSTer recommended | ✅ (free) | — | ✅ (most capable) |
+### The Library Model vs. Kernel Sockets
----
+```mermaid
+flowchart TD
+ subgraph "Unix / Windows / macOS"
+ UAPP["Application"] -->|"syscall boundary
(kernel trap)"| KERN["Kernel TCP/IP Stack"]
+ KERN --> KDRV["Kernel NIC Driver"]
+ KDRV --> HW1["Network Hardware"]
+ end
-## Configuration (Roadshow)
+ subgraph "AmigaOS"
+ AAPP["Application"] -->|"Library call
(JSR through LVO)"| BSL["bsdsocket.library
(user-space process)"]
+ BSL -->|"SANA-II IORequests"| SANA["SANA-II Driver
(exec device)"]
+ SANA --> HW2["Network Hardware"]
+ end
-```
-; DEVS:NetInterfaces/prism2
-DEVICE=prism2.device
-UNIT=0
-IPTYPE=DHCP
-; or:
-; ADDRESS=192.168.1.100
-; NETMASK=255.255.255.0
-; GATEWAY=192.168.1.1
-
-; DEVS:NetInterfaces/lo0
-DEVICE=lo0.device
-UNIT=0
-ADDRESS=127.0.0.1
-NETMASK=255.0.0.0
+ style KERN fill:#ffcdd2,stroke:#c62828,color:#333
+ style BSL fill:#c8e6c9,stroke:#2e7d32,color:#333
```
+| Aspect | Unix/Linux/Windows | AmigaOS |
+|---|---|---|
+| **Stack location** | Kernel (ring 0) | User-space process (same address space) |
+| **Socket API** | System calls (trap to kernel) | Library vector calls (JSR) — no context switch |
+| **Driver model** | Kernel module / NDIS | SANA-II exec.device (user-space) |
+| **Multiple stacks** | One per kernel | Multiple possible (only one active) |
+| **Per-process state** | fd table in kernel | Socket "library base" per opener |
+| **Protection** | Full memory protection | None — any process can corrupt stack state |
+| **Signal integration** | select/poll/epoll | WaitSelect + Exec signal bits |
+| **Performance** | Optimised kernel path | No syscall overhead, but no DMA offload |
+
+### The Full Network Stack
+
+```mermaid
+flowchart TD
+ subgraph "Applications"
+ IBR["IBrowse
(HTTP)"]
+ FTP["AmiFTP
(FTP)"]
+ IRC["AmiRC
(IRC)"]
+ PING["ping
(ICMP)"]
+ end
+
+ subgraph "bsdsocket.library"
+ SOCK["Socket Layer
(socket, bind, connect,
send, recv, select)"]
+ TCP["TCP"]
+ UDP["UDP"]
+ ICMP["ICMP"]
+ IP["IP Layer
(routing, fragmentation)"]
+ ARP["ARP
(address resolution)"]
+ end
+
+ subgraph "Link Layer"
+ SANA2["SANA-II Interface
(standardised IORequest API)"]
+ end
+
+ subgraph "Hardware Drivers"
+ ETH["Ethernet Driver
(e.g., ariadne.device)"]
+ PPP["PPP Driver
(e.g., ppp.device)"]
+ SLIP["SLIP Driver"]
+ LO["Loopback
(lo0.device)"]
+ end
+
+ subgraph "Physical"
+ NIC["Ethernet NIC
(Zorro/PCMCIA)"]
+ MODEM["Serial Modem
(via serial.device)"]
+ LOOP["Internal loopback"]
+ end
+
+ IBR --> SOCK
+ FTP --> SOCK
+ IRC --> SOCK
+ PING --> SOCK
+
+ SOCK --> TCP --> IP
+ SOCK --> UDP --> IP
+ SOCK --> ICMP --> IP
+ IP --> ARP --> SANA2
+ IP --> SANA2
+
+ SANA2 --> ETH --> NIC
+ SANA2 --> PPP --> MODEM
+ SANA2 --> SLIP --> MODEM
+ SANA2 --> LO --> LOOP
+
+ style SOCK fill:#e8f4fd,stroke:#2196f3,color:#333
+ style SANA2 fill:#fff9c4,stroke:#f9a825,color:#333
+ style IP fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+### Per-Opener Library Base
+
+Each Amiga task that opens `bsdsocket.library` gets its own **private library base** with isolated socket state — there is no kernel fd table. This means `SocketBase` must **never** be shared between tasks. See [bsdsocket.md](bsdsocket.md) for the full API reference and per-task setup pattern.
+
---
-## Configuration (AmiTCP)
+## SANA-II — The Network Driver Layer
+
+SANA-II (Standard Amiga Networking Architecture, version II) is the standardised interface between TCP/IP stacks and network hardware drivers. It's an `exec.device` protocol — drivers communicate via IORequests.
+
+```mermaid
+flowchart LR
+ subgraph "TCP/IP Stack"
+ IP["IP Layer"]
+ end
+
+ subgraph "SANA-II API"
+ direction TB
+ OPEN["OpenDevice
(driver, unit)"]
+ WRITE["CMD_WRITE
(send frame)"]
+ READ["CMD_READ
(receive frame)"]
+ ONLINE["S2_ONLINE
(bring up link)"]
+ CONFIG["S2_CONFIGINTERFACE
(set HW address)"]
+ QUERY["S2_DEVICEQUERY
(get capabilities)"]
+ end
+
+ subgraph "Drivers"
+ A2065["a2065.device
(Commodore Ethernet)"]
+ ARIADNE["ariadne.device
(Village Tronic)"]
+ CNET["cnet.device
(Hydra/X-Surf)"]
+ PPPDEV["ppp.device
(dial-up modem)"]
+ end
+
+ IP --> OPEN
+ IP --> WRITE
+ IP --> READ
+ OPEN --> A2065
+ OPEN --> ARIADNE
+ OPEN --> CNET
+ OPEN --> PPPDEV
+
+ style OPEN fill:#e8f4fd,stroke:#2196f3,color:#333
+```
+
+### How the Stack Sends a Packet
+
+1. Application calls `send(sock, data, len, 0)`
+2. TCP/IP stack builds TCP segment + IP header
+3. ARP resolves destination MAC address (or uses cached entry)
+4. Stack fills a `struct IOSana2Req` with the complete Ethernet frame
+5. `DoIO` / `SendIO` to the SANA-II driver
+6. Driver writes frame to hardware registers / DMA buffer
+
+### How the Stack Receives a Packet
+
+1. Stack posts **read requests** (`CMD_READ`) to the SANA-II driver (kept outstanding)
+2. When a frame arrives, the driver completes the IORequest
+3. Stack parses the Ethernet frame → IP → TCP/UDP → delivers to socket buffer
+4. Application's `recv()` or `WaitSelect()` returns the data
+5. Stack immediately posts a new `CMD_READ` to keep the pipeline full
+
+---
+
+## Dial-Up Networking — PPP and SLIP
+
+Before broadband, most Amiga internet connections were via **serial modem** using PPP or SLIP over `serial.device`:
+
+```mermaid
+flowchart LR
+ subgraph "Amiga"
+ BSL2["bsdsocket.library"] --> PPP2["PPP/SLIP Driver"]
+ PPP2 --> SER["serial.device
(CIA UART)"]
+ end
+
+ SER -->|"RS-232
up to 115200 baud"| MODEM["External Modem
(AT commands)"]
+ MODEM -->|"Phone line
V.34/V.90"| ISP["ISP
PPP Server"]
+
+ style PPP2 fill:#fff9c4,stroke:#f9a825,color:#333
+ style MODEM fill:#ffcdd2,stroke:#c62828,color:#333
+```
+
+### PPP Connection Sequence
+
+```
+1. Modem initialisation:
+ → ATZ (reset modem)
+ ← OK
+ → AT&F1 (factory defaults, hardware flow control)
+ ← OK
+
+2. Dial ISP:
+ → ATDT 555-1234 (tone dial)
+ ← CONNECT 33600 (connected at 33.6 kbps)
+
+3. PPP negotiation (automatic):
+ ← LCP Configure-Request
+ → LCP Configure-Ack
+ → LCP Configure-Request
+ ← LCP Configure-Ack
+ → PAP Authenticate (username/password)
+ ← PAP Authenticate-Ack
+ → IPCP Configure-Request
+ ← IPCP Configure-Ack (IP=203.0.113.42, DNS=203.0.113.1)
+
+4. Link up — IP traffic flows over PPP frames on serial line
+```
+
+### PPP Configuration (Miami)
+
+Miami had the most user-friendly dial-up setup:
+
+| Setting | Value | Notes |
+|---|---|---|
+| Serial device | `serial.device` | Or `duart.device` for A2232 multi-port |
+| Unit | 0 | |
+| Baud rate | 57600 or 115200 | Must match modem's DTE rate |
+| Flow control | RTS/CTS (hardware) | **Required** — XON/XOFF causes corruption |
+| Init string | `ATZ` then `AT&F1` | Hardware flow control defaults |
+| Dial command | `ATDT ` | |
+| Login | PAP or script-based | |
+| VJ compression | Yes | Van Jacobson TCP header compression |
+
+### PPP Configuration (AmiTCP)
```
; AmiTCP:db/interfaces
-prism2 DEV=DEVS:Networks/prism2.device UNIT=0 IP=DHCP
+ppp0 DEV=DEVS:Networks/ppp.device UNIT=0
+; AmiTCP:bin/startnet script must:
+; 1. Open serial port
+; 2. Send modem commands
+; 3. Wait for CONNECT
+; 4. Hand off to PPP
+```
+
+### SLIP (Serial Line IP)
+
+SLIP is the simpler, older protocol — no authentication, no compression, no error detection:
+
+```
+; AmiTCP:db/interfaces
+sl0 DEV=DEVS:Networks/slip.device UNIT=0
+
+; SLIP frames:
+; Each IP packet is framed with END bytes (0xC0)
+; ESC (0xDB) used to escape END and ESC within data
+```
+
+| Feature | SLIP | PPP |
+|---|---|---|
+| Authentication | None | PAP, CHAP |
+| IP negotiation | Manual (both sides must know IPs) | Automatic (IPCP) |
+| Compression | None | VJ header compression |
+| Error detection | None | FCS (frame check) |
+| Multi-protocol | IP only | IP, IPX, AppleTalk |
+
+---
+
+## Stack Comparison (Detailed)
+
+| Feature | AmiTCP 3.0b2 / Genesis | Miami 3.2 | Roadshow 1.15 |
+|---|---|---|---|
+| **License** | Free (Genesis fork) | Commercial ($30) | Commercial (demo: 5-min limit) |
+| **API version** | bsdsocket.library v3 | v4 | v4 |
+| **Based on** | NetBSD TCP/IP stack | Custom implementation | Custom implementation |
+| **IPv6** | No | No | No |
+| **PPP** | External (AmiPPP) | Built-in dialer + PPP | Via SANA-II PPP driver |
+| **SLIP** | Yes | Yes | Via SANA-II SLIP driver |
+| **DHCP** | External (dhclient) | Built-in | Built-in |
+| **DNS cache** | No | Yes | Yes |
+| **DNS over TCP** | No | Yes | Yes |
+| **Multi-homing** | Limited | Yes | Yes (multiple interfaces) |
+| **SANA-II v2** | Yes | Yes | Yes |
+| **GUI config** | Genesis MUI prefs | Miami prefs GUI | Roadshow prefs editor |
+| **CLI config** | `ifconfig`, `route` | Via GUI only | Text file + CLI tools |
+| **Syslog** | Yes | Yes | Yes |
+| **Active development** | No (last: ~2002) | No (last: ~2003) | Yes (Olaf Barthel, ongoing) |
+| **Stability** | Good | Good | Excellent |
+| **MiSTer notes** | ✅ Free, easy to set up | ❌ Requires registration | ✅ Most capable, demo limit |
+
+### Which Stack for MiSTer?
+
+- **AmiTCP / Genesis**: Free, works well for basic networking. Use `Genesis.lha` from Aminet.
+- **Roadshow**: Most capable and actively maintained. Demo works for 5 minutes at a time — sufficient for testing. Full license recommended for permanent setups.
+
+---
+
+## Ethernet Cards (SANA-II Hardware)
+
+| Card | Device | Bus | Chipset | Speed |
+|---|---|---|---|---|
+| Commodore A2065 | `a2065.device` | Zorro II | AMD LANCE | 10 Mbps |
+| Village Tronic Ariadne | `ariadne.device` | Zorro II | AMD PCnet | 10 Mbps |
+| Village Tronic Ariadne II | `ariadne2.device` | Zorro II | SMC 91C94 | 10 Mbps |
+| Hydra Systems Amiganet | `amiganet.device` | Zorro II | AMD LANCE | 10 Mbps |
+| X-Surf | `xsurf.device` | Clock port | RTL8019AS | 10 Mbps |
+| X-Surf 100 | `xsurf100.device` | Zorro II/III | AX88796B | 100 Mbps |
+| PCMCIA Ethernet | `cnet.device` | PCMCIA (A600/A1200) | Various | 10 Mbps |
+
+### MiSTer Virtual NIC
+
+MiSTer emulates a network interface that bridges to the Linux HPS network:
+
+```
+; DEVS:NetInterfaces/mister0 (Roadshow)
+DEVICE=mister_eth.device
+UNIT=0
+IPTYPE=DHCP
+MTU=1500
+```
+
+The virtual driver presents a standard SANA-II interface — the TCP/IP stack sees no difference from a real Ethernet card.
+
+---
+
+## Configuration — Roadshow
+
+### Network Interface
+
+```
+; DEVS:NetInterfaces/eth0
+DEVICE=ariadne.device ; or your card's device name
+UNIT=0
+IPTYPE=DHCP ; or STATIC for manual
+
+; Static configuration:
+; ADDRESS=192.168.1.100
+; NETMASK=255.255.255.0
+```
+
+### Name Resolution
+
+```
+; DEVS:Internet/name_resolution
+NAMESERVER 8.8.8.8
+NAMESERVER 8.8.4.4
+DOMAIN local
+SEARCH local
+```
+
+### Default Gateway
+
+```
+; DEVS:Internet/default_gateway
+DEVICE=ariadne.device
+UNIT=0
+GATEWAY=192.168.1.1
+```
+
+### Startup
+
+```
+; S:User-Startup
+Run >NIL: C:AddNetInterface eth0
+WaitForPort AMITCP
+; Stack is now ready
+```
+
+---
+
+## Configuration — AmiTCP / Genesis
+
+### Interface Configuration
+
+```
+; AmiTCP:db/interfaces
+eth0 DEV=DEVS:Networks/ariadne.device UNIT=0 IP=DHCP
+
+; Static:
+; eth0 DEV=DEVS:Networks/ariadne.device UNIT=0 IP=192.168.1.100 NETMASK=255.255.255.0
+```
+
+### DNS and Hosts
+
+```
; AmiTCP:db/netdb-myhost
HOST 127.0.0.1 localhost
+HOST 192.168.1.1 gateway
NAMESERVER 8.8.8.8
DOMAIN local
```
+### Startup
+
+```
+; S:User-Startup
+Run >NIL: AmiTCP:AmiTCP
+WaitForPort AMITCP
+; bsdsocket.library is now available
+```
+
+---
+
+## Verifying the Network
+
+```
+; CLI commands (provided by the stack):
+ifconfig ; show interface configuration
+netstat -r ; show routing table
+netstat -a ; show active connections
+ping 8.8.8.8 ; test connectivity
+nslookup amiga.org ; test DNS resolution
+traceroute 8.8.8.8 ; trace route to destination
+```
+
+```c
+/* Programmatic check: */
+struct Library *SocketBase = OpenLibrary("bsdsocket.library", 4);
+if (!SocketBase)
+ Printf("No TCP/IP stack running — start AmiTCP or Roadshow\n");
+else
+ CloseLibrary(SocketBase);
+```
+
---
## References
- Roadshow SDK: http://roadshow.apc-tcp.de/
- AmiTCP SDK: Aminet `comm/tcp/AmiTCP-SDK-4.3.lha`
+- Genesis (free AmiTCP fork): Aminet `comm/tcp/Genesis.lha`
+- SANA-II specification: Aminet `docs/hard/sana2.lha`
+- See also: [bsdsocket.md](bsdsocket.md) — socket API reference
+- See also: [sana2.md](sana2.md) — SANA-II driver specification
+- See also: [protocols.md](protocols.md) — DNS, HTTP, UDP implementation examples
diff --git a/13_toolchain/README.md b/13_toolchain/README.md
index 4549e8a..df9c8ef 100644
--- a/13_toolchain/README.md
+++ b/13_toolchain/README.md
@@ -2,15 +2,18 @@
# Toolchain — Overview
+Development tools for building Amiga software, from native compilers to modern cross-compilation environments.
+
## Section Index
| File | Description |
|---|---|
+| [gcc_amiga.md](gcc_amiga.md) | m68k-amigaos-gcc cross-compiler: bebbo's toolchain, Docker setup, CPU targets, libnix/ixemul startup |
+| [sasc.md](sasc.md) | SAS/C 6.x: pragma format with register encoding, compiler/linker flags, __saveds/__asm idioms, SAS/C vs GCC comparison |
+| [stormc.md](stormc.md) | StormC native IDE: C/C++ with exceptions, integrated debugger, PowerPC support, version history |
| [vasm_vlink.md](vasm_vlink.md) | vasm assembler and vlink linker |
-| [gcc_amiga.md](gcc_amiga.md) | m68k-amigaos-gcc cross-compiler |
-| [sasc.md](sasc.md) | SAS/C 6.x compiler |
| [fd_files.md](fd_files.md) | FD/SFD file format and LVO generation |
-| [pragmas.md](pragmas.md) | Compiler pragmas and inline stubs |
-| [ndk.md](ndk.md) | NDK versions, contents, and where to get them |
-| [makefiles.md](makefiles.md) | Makefile patterns for Amiga cross-compilation |
-| [debugging.md](debugging.md) | Debugging tools: BareFoot, wack, gdb remote |
+| [pragmas.md](pragmas.md) | Compiler pragmas and inline stubs: SAS/C pragmas, GCC inline asm, proto headers, fd2pragma |
+| [ndk.md](ndk.md) | NDK versions (3.1/3.9/3.2): contents, downloads, cross-compiler integration |
+| [makefiles.md](makefiles.md) | Makefile patterns for GCC cross-compilation, vasm/vlink assembly, mixed C+asm projects |
+| [debugging.md](debugging.md) | Debugging tools: Enforcer/MuForce memory watchdog, SnoopDOS tracing, FS-UAE GDB remote, kprintf, debugging checklist |
diff --git a/13_toolchain/debugging.md b/13_toolchain/debugging.md
index 9e07030..6636882 100644
--- a/13_toolchain/debugging.md
+++ b/13_toolchain/debugging.md
@@ -1,62 +1,191 @@
[← Home](../README.md) · [Toolchain](README.md)
-# Debugging Tools — BareFoot, wack, GDB Remote
+# Debugging Tools — Enforcer, BareFoot, GDB Remote
## Overview
-Debugging Amiga programs ranges from ROM-resident kernel debuggers (wack/SAD) through software monitors to modern cross-debugging via GDB stubs.
+Debugging Amiga programs spans from hardware-level ROM debuggers through MMU-based memory watchers to modern cross-debugging via GDB stubs. The unique Amiga challenges — no memory protection, shared address space, custom chip state — require specialised tools.
+
+```mermaid
+flowchart TD
+ subgraph "Development Host (Linux/macOS)"
+ GDB["m68k-elf-gdb
(source-level)"]
+ end
+
+ subgraph "Amiga / Emulator"
+ ENF["Enforcer/MuForce
(MMU memory watch)"]
+ MON["MonAm/Barfly
(software debugger)"]
+ SERD["BareFoot/SAD
(serial ROM debugger)"]
+ APP["Application
under test"]
+ end
+
+ GDB -->|"TCP/Serial
GDB Remote Protocol"| APP
+ ENF -->|"Traps illegal
memory access"| APP
+ MON -->|"Breakpoints,
disassembly"| APP
+
+ style ENF fill:#ffcdd2,stroke:#c62828,color:#333
+ style GDB fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
---
## Tool Comparison
-| Tool | Type | Target | Best for |
+| Tool | Type | Requires | Best For |
|---|---|---|---|
-| BareFoot | Serial debugger | Real hardware / UAE | OS-level, Exec kernel |
-| wack/SAD | ROM debugger | Real hardware | Boot-time, crash analysis |
-| MonAm | Software monitor | AmigaOS | Breakpoints, disassembly |
-| IDA Pro | Static + remote | Cross-platform | Static RE, decompilation |
-| GDB (m68k-elf-gdb) | Cross-debugger | FS-UAE / Basilisk | Source-level C debugging |
-| Enforcer | Memory watchdog | AmigaOS (68020+) | Illegal memory access detection |
-| MuForce | Memory watchdog | AmigaOS (68040+) | Same as Enforcer for 040/060 |
+| **Enforcer** | MMU memory watchdog | 68020+ with MMU | Catching NULL ptr, illegal reads/writes |
+| **MuForce** | MMU memory watchdog | 68040/060 | Same as Enforcer, optimised for 040/060 |
+| **MuGuardianAngel** | Stack overflow detector | 68040/060 | Catching stack overflow crashes |
+| **BareFoot** | Serial kernel debugger | Serial cable + terminal | Exec internals, boot crashes, system hangs |
+| **wack / SAD** | ROM-resident debugger | Guru Meditation (crash) | Post-crash analysis, ROM debugging |
+| **MonAm** | Software monitor/debugger | AmigaOS | Interactive breakpoints, register inspection |
+| **SnoopDOS** | DOS call tracer | AmigaOS 2.0+ | Tracing file/library opens, locks |
+| **CPR** | Debug output viewer | AmigaOS | Capturing `kprintf` and `RawPutChar` output |
+| **m68k-elf-gdb** | Cross-debugger | FS-UAE or vamos | Source-level C debugging from host |
+| **IDA Pro** | Static + remote | Cross-platform | Static RE, decompilation |
---
-## Enforcer / MuForce
+## Enforcer / MuForce — Memory Access Watchdog
-Detects invalid memory accesses using MMU:
+The most essential Amiga debugging tool. Uses the MMU to trap **all invalid memory accesses** — reading from address 0 (NULL pointer), writing to ROM, accessing non-existent memory, etc.
```
-Enforcer Hit! $0000000C read by task "myapp" at $00020456
-```
-
-```
-; Install:
+; Start Enforcer (runs in background):
run >NIL: Enforcer
-; or for 040/060:
+
+; For 040/060 systems:
run >NIL: MuForce
+
+; Output goes to the serial port by default.
+; Redirect to a file or console:
+run >NIL: Enforcer STDOUT
+run >NIL: Enforcer FILE RAM:enforcer.log
+```
+
+### Reading Enforcer Hits
+
+```
+WORD-READ from $00000004 by "myapp" at $00020456
+Data: $00000000 USP: $0007FFF0 PC: $00020456
+
+ --- Loss of Register Dump ---
+ D0 00000000 D1 0003A2F0 D2 00000010 D3 00000001
+ D4 00000000 D5 FFFFFFFF D6 00000000 D7 00000000
+ A0 00000000 A1 0003A2F0 A2 00070000 A3 00040000
+ A4 00020000 A5 00000000 A6 0003F000 A7 0007FFF0
+```
+
+| Field | Meaning |
+|---|---|
+| `WORD-READ from $00000004` | Tried to read a WORD from address 4 (low memory = NULL struct deref) |
+| `by "myapp"` | Task name that caused the hit |
+| `at $00020456` | PC (program counter) where the access occurred |
+
+> [!TIP]
+> **Address $00000004 = reading `ExecBase` through NULL pointer.** This is the #1 Enforcer hit — it means code is dereferencing a NULL pointer to a structure and accessing field at offset 4. Find the instruction at the PC address in your disassembly.
+
+### Common Enforcer Hit Patterns
+
+| Address Range | Likely Cause |
+|---|---|
+| `$00000000–$000003FF` | NULL pointer dereference (struct field at offset N) |
+| `$00F00000–$00FFFFFF` | Reading from ROM area (write-to-ROM bug) |
+| `$00C00000–$00DFFFFF` | Accessing custom chip area with bad address |
+| `$01000000+` | Accessing memory beyond physical RAM |
+
+---
+
+## SnoopDOS — System Call Tracer
+
+Monitors all DOS and library calls system-wide — essential for understanding why programs fail to find files or libraries:
+
+```
+; Start SnoopDOS (MUI GUI):
+SnoopDos
+
+; Typical output:
+Open "myapp" LIBS:mytool.library OK (Lock=$0003A000)
+Open "myapp" DEVS:Keymaps/usa FAIL (Object not found)
+Lock "myapp" SYS:Prefs/Env-Archive OK (Lock=$00042000)
+OpenLibrary "myapp" intuition.library v39 OK (Base=$0003F000)
```
---
-## FS-UAE GDB Debugging
+## FS-UAE GDB Remote Debugging
+
+Modern source-level debugging from your development host:
```bash
# In fs-uae.conf:
remote_debugger = 1
remote_debugger_port = 6860
-# Connect from host:
+# Start FS-UAE, then from your development machine:
m68k-elf-gdb myapp
+
(gdb) target remote localhost:6860
+(gdb) symbol-file myapp # load debug symbols
(gdb) break main
(gdb) continue
+(gdb) print myVariable
+(gdb) info registers
+(gdb) x/10i $pc # disassemble at PC
+(gdb) backtrace # show call stack
```
+### Building with Debug Info
+
+```bash
+# GCC — include DWARF debug info in hunk format:
+m68k-amigaos-gcc -g -noixemul -o myapp main.c
+
+# SAS/C — include SAS debug info:
+lc -d2 main.c
+blink main.o TO myapp LIB lib:sc.lib lib:amiga.lib DEBUG
+```
+
+---
+
+## kprintf / RawPutChar — Debug Output
+
+For printf-style debugging without a console:
+
+```c
+/* kprintf writes directly to the serial port,
+ bypassing all OS output — works even during interrupts: */
+#include
+
+kprintf("Value: %ld at %lx\n", value, address);
+
+/* RawPutChar — single character to serial: */
+RawPutChar('X');
+```
+
+Capture with a terminal emulator on the serial port, or use **CPR** (Capture Raw Putchar) to redirect to a window or file.
+
+---
+
+## Debugging Checklist
+
+| Symptom | Tool | Action |
+|---|---|---|
+| Guru Meditation | wack/SAD | Note crash address, check code at that PC |
+| Random crashes | Enforcer | Run Enforcer, look for illegal accesses before the crash |
+| "Can't open library" | SnoopDOS | See what path the open is trying |
+| Memory leak | MungWall | Tracks AllocMem/FreeMem, reports leaks at exit |
+| Stack overflow | MuGuardianAngel | Detects stack underflow on 040/060 |
+| Performance issue | sprof (SAS/C) | Profile C functions |
+| File not found | SnoopDOS | See where the OS is looking for the file |
+
---
## References
-- BareFoot: Aminet `dev/debug/BareFoot.lha`
- Enforcer: Aminet `dev/debug/Enforcer.lha`
- MuForce: Aminet `dev/debug/MuForce.lha`
+- SnoopDOS: Aminet `util/monitor/SnoopDos.lha`
+- BareFoot: Aminet `dev/debug/BareFoot.lha`
+- MungWall: Aminet `dev/debug/MungWall.lha`
+- CPR: Aminet `dev/debug/cpr.lha`
diff --git a/13_toolchain/sasc.md b/13_toolchain/sasc.md
index 6e67c2f..dad95b0 100644
--- a/13_toolchain/sasc.md
+++ b/13_toolchain/sasc.md
@@ -4,7 +4,9 @@
## Overview
-SAS/C (originally Lattice C) was the dominant commercial C compiler for AmigaOS. Version 6.58 is the final release. It produces optimised 68k code with full AmigaOS integration, including pragma-based system calls and SAS-specific debug formats.
+SAS/C (originally Lattice C) was the dominant commercial C compiler for AmigaOS from the mid-1980s through the mid-1990s. Version **6.58** is the final release. It produces highly optimised 68k code with deep AmigaOS integration, including pragma-based system calls, SAS-specific debug formats, and a built-in profiler.
+
+Most existing Amiga source code and documentation assumes SAS/C conventions. Understanding its idioms is essential for working with legacy codebases.
---
@@ -12,52 +14,143 @@ SAS/C (originally Lattice C) was the dominant commercial C compiler for AmigaOS.
| Feature | Description |
|---|---|
-| Register args | Automatic register parameter passing |
-| Pragmas | `#pragma libcall` for direct library LVO calls |
-| Small data | A4-relative addressing for globals |
-| Profiling | Built-in `sprof` profiler |
-| Debug format | SAS stabs (`=APS` tag in HUNK_DEBUG) |
-| Optimizer | Global optimizer (`lc -O`) with peephole |
+| **Register args** | Automatic register parameter passing for small functions |
+| **Pragmas** | `#pragma libcall` for direct library LVO calls — no stub library needed |
+| **Small data model** | A4-relative addressing for global variables (faster, smaller code) |
+| **Large data model** | Absolute addressing for globals (no 64 KB limit) |
+| **Profiler** | Built-in `sprof` function-level profiler |
+| **Debug format** | SAS stabs (`=APS` tag in `HUNK_DEBUG`) |
+| **Global optimizer** | `lc -O` with peephole, dead code elimination, CSE |
+| **Code generation** | 68000 through 68060 + 68881/68882 FPU |
+| **Integrated linker** | `blink` — faster than generic linkers, understands Amiga hunks |
---
## Pragma Format
+Pragmas tell the compiler how to call AmigaOS library functions directly via JSR through the library base, with arguments in specific registers:
+
```c
/* dos_pragmas.h — generated from FD files: */
#pragma libcall DOSBase Open 1e 2102
-/* ^^ ^^ ^^^^
- name LVO reg-encoding
- reg-encoding: 2=D2, 1=D1, 0=result in D0, 2 args */
+/* ^^ ^^^^
+ LVO register encoding
+
+ LVO $1E = -30 (decimal) = offset in jump table
+ Register encoding: read RIGHT to LEFT:
+ 2 = D2 (not used here)
+ 0 = D0 (return value)
+ 1 = D1 (first arg)
+ 2 = D2 (second arg)
+ Last digit = number of arguments */
#pragma libcall DOSBase Close 24 101
+/* LVO -$24 (-36), 1 arg in D1, returns in D0 */
+
#pragma libcall DOSBase Read 2a 32103
+/* LVO -$2A (-42), 3 args: D1=fh, D2=buffer, D3=length */
```
-Register encoding: digits map to registers (1=D0, 2=D1, ... 9=A0, A=A1, etc.)
+### Register Encoding
+
+| Digit | Register | Digit | Register |
+|---|---|---|---|
+| 0 | D0 | 8 | (unused) |
+| 1 | D1 | 9 | A0 |
+| 2 | D2 | A | A1 |
+| 3 | D3 | B | A2 |
+| 4 | D4 | C | A3 |
+| 5 | D5 | D | A4 |
+| 6 | D6 | E | A5 |
+| 7 | D7 | F | (unused) |
+
+The rightmost digit is always the result register, next digit is number of args, then args are listed left-to-right.
---
-## Compilation
+## Compilation Workflow
```
-lc -v -O -b0 -j73 hello.c ; compile
-blink hello.o TO hello LIB lib:sc.lib lib:amiga.lib ; link
+Source (.c) → lc (compile) → Object (.o) → blink (link) → Executable
```
+```bash
+# Compile a single file:
+lc -v -O -b0 main.c
+
+# Link:
+blink main.o util.o TO myapp LIB lib:sc.lib lib:amiga.lib
+
+# Compile + link in one step:
+lc -v -O -b0 -j73 main.c util.c LINK TO myapp
+```
+
+### Compiler Flags
+
| Flag | Meaning |
|---|---|
-| `-v` | Verbose |
-| `-O` | Optimise |
-| `-b0` | Small data model (A4-relative) |
-| `-b1` | Large data model |
-| `-j73` | Generate 68020/68881 code |
+| `-v` | Verbose output |
+| `-O` | Enable global optimiser |
+| `-b0` | Small data model (A4-relative, max 64 KB globals) |
+| `-b1` | Large data model (absolute addressing) |
+| `-j30` | Generate 68030 code |
+| `-j40` | Generate 68040 code |
+| `-j73` | Generate 68020 + 68881 FPU code |
| `-d0` | No debug info |
-| `-d2` | Full debug info |
+| `-d2` | Full debug info (SAS stabs) |
+| `-r` | Generate resident/reentrant code |
+| `-L` | Disable auto-open of amiga.lib |
+| `-w` | Suppress warnings |
+| `-E` | Preprocess only |
+
+### Linker Flags (blink)
+
+| Flag | Meaning |
+|---|---|
+| `TO name` | Output file name |
+| `LIB libs` | Link libraries |
+| `DEBUG` | Include debug hunks |
+| `STRIPDEBUG` | Remove debug hunks |
+| `SMALLCODE` | PC-relative addressing |
+| `SMALLDATA` | A4-relative data |
+| `MAP file` | Generate link map |
+| `NODEBUG` | Omit all debug info |
+
+---
+
+## SAS/C-Specific Idioms
+
+```c
+/* Register parameters: */
+ULONG __asm MyFunc(register __d0 ULONG arg1,
+ register __a0 APTR arg2);
+
+/* Interrupt-safe function: */
+void __interrupt __saveds MyInterrupt(void);
+
+/* Resident code: */
+LONG __saveds __asm LibOpen(register __a6 struct Library *base);
+
+/* __saveds: saves/restores A4 (small data base) on entry/exit */
+/* __interrupt: preserves all registers */
+/* __asm: use register calling convention */
+```
+
+### Differences from GCC
+
+| Feature | SAS/C | GCC (m68k-amigaos) |
+|---|---|---|
+| Register args | `register __d0 ULONG x` | `ULONG x __asm("d0")` |
+| Small data base | A4 (automatic with `-b0`) | `-fbaserel` (A4) |
+| Library pragmas | `#pragma libcall` | Inline asm stubs in `` |
+| Startup | `cres.o` (resident) / `c.o` (standard) | `libnix` or `ixemul` |
+| String constants | Pooled by default | `-fmerge-constants` |
---
## References
-- SAS/C 6.x User Manual
+- SAS/C 6.x User Manual and Reference Manual
- NDK39 pragma files: `NDK_3.9/Include/pragmas/`
+- See also: [pragmas.md](pragmas.md) — pragma/inline mechanism in depth
+- See also: [gcc_amiga.md](gcc_amiga.md) — GCC cross-compiler (modern alternative)
diff --git a/13_toolchain/stormc.md b/13_toolchain/stormc.md
new file mode 100644
index 0000000..8e7c444
--- /dev/null
+++ b/13_toolchain/stormc.md
@@ -0,0 +1,106 @@
+[← Home](../README.md) · [Toolchain](README.md)
+
+# StormC — Native IDE and Compiler Suite
+
+## Overview
+
+**StormC** was a native Amiga integrated development environment (IDE) developed by **Haage & Partner**. Unlike SAS/C (command-line focused) or GCC (cross-compilation), StormC provided a modern GUI-based IDE running directly on the Amiga with project management, integrated editor, debugger, and compiler — similar to what Borland C++ and Visual Studio offered on PC.
+
+---
+
+## Version History
+
+| Version | Year | Key Features |
+|---|---|---|
+| StormC 1.0 | 1996 | Initial release, C compiler, basic IDE |
+| StormC 2.0 | 1997 | C++ support, improved optimiser |
+| StormC 3.0 | 1998 | Full C++ with exceptions, STL, PowerPC support |
+| StormC 4.0 | 1999 | Final version, OS 3.5 integration |
+
+---
+
+## Key Features
+
+| Feature | Description |
+|---|---|
+| **Native IDE** | GUI editor + project manager running on AmigaOS itself |
+| **C and C++** | Full C89/C90, C++ with exceptions and RTTI |
+| **PowerPC** | StormC 3+ could target PPC (for CyberStorm PPC, BlizzardPPC) |
+| **68k code gen** | 68000 through 68060 target support |
+| **Debugger** | Integrated source-level debugger with breakpoints and watch |
+| **Linker** | StormLink — Amiga hunk format output |
+| **Profiler** | Built-in function-level profiler |
+| **AmigaOS integration** | Full NDK headers, pragma support, Amiga library call stubs |
+| **MUI support** | Built-in MUI class creation wizards (later versions) |
+
+---
+
+## Project Structure
+
+StormC used `.storm` project files (proprietary format) containing:
+- Source file list and compilation order
+- Compiler flags per file or project-wide
+- Include paths and library search paths
+- Debug/Release build configurations
+- Target CPU selection
+
+---
+
+## Compilation
+
+```
+; From the IDE:
+; Project → Build (or press Ctrl+B)
+
+; Command-line compiler also available:
+stormc -O2 -m68020 -o myapp main.c util.c
+
+; Typical flags:
+; -m68000 Target 68000
+; -m68020 Target 68020+
+; -m68040 Target 68040
+; -m68060 Target 68060
+; -O0 to -O3 Optimisation level
+; -g Debug info
+; -c Compile only (no link)
+; -I Include path
+; -L Library path
+; -l Link library
+```
+
+---
+
+## StormC vs. Other Amiga Compilers
+
+| Feature | SAS/C 6.58 | GCC (bebbo) | StormC 4.0 |
+|---|---|---|---|
+| **C++ support** | No (C only) | Yes (GCC 6.5) | Yes (with exceptions) |
+| **IDE** | No (CLI + editor) | No (CLI + any editor) | **Yes (native GUI)** |
+| **Debugger** | External (CodeProbe) | GDB remote | **Integrated** |
+| **Cross-compile** | No (native only) | **Yes (Linux/macOS host)** | No (native only) |
+| **Optimiser quality** | Excellent | Good | Good |
+| **PowerPC** | No | No | Yes (v3+) |
+| **Availability** | Abandonware | Free / open source | Abandonware |
+| **Legacy code compat** | High (dominant compiler) | Moderate (GCC differences) | Moderate |
+| **Pragma support** | Native `#pragma libcall` | Inline asm stubs | Pragma compatible |
+
+---
+
+## Limitations and Legacy
+
+- **Proprietary project format**: `.storm` files can't be converted to Makefiles easily
+- **No cross-compilation**: Must run on a real Amiga or emulator
+- **C++ ABI**: StormC's C++ name mangling and vtable layout differ from GCC — libraries compiled with StormC can't be linked with GCC C++ code
+- **Abandoned**: Haage & Partner ceased operations; no source release
+- **PowerPC path abandoned**: The WarpOS/PowerUP split made PPC support fragmented
+
+Despite these issues, StormC was the most productive **native** Amiga development environment, and many late-era Amiga applications (1996–2000) were developed with it.
+
+---
+
+## References
+
+- Haage & Partner: historical website (archived)
+- Aminet: `dev/c/StormC` — various StormC patches and updates
+- See also: [sasc.md](sasc.md) — SAS/C (dominant legacy compiler)
+- See also: [gcc_amiga.md](gcc_amiga.md) — GCC cross-compiler (modern standard)
diff --git a/14_references/README.md b/14_references/README.md
index 005beff..baf332a 100644
--- a/14_references/README.md
+++ b/14_references/README.md
@@ -2,11 +2,13 @@
# References — Quick Lookup Tables
+Condensed reference tables for frequently needed constants, register addresses, LVO offsets, and error codes.
+
## Section Index
| File | Description |
|---|---|
-| [custom_chip_registers.md](custom_chip_registers.md) | Complete custom chip register map |
+| [custom_chip_registers.md](custom_chip_registers.md) | Complete custom chip register map ($DFF000–$DFF1FE) |
| [exec_lvo_table.md](exec_lvo_table.md) | exec.library LVO offset table |
| [dos_lvo_table.md](dos_lvo_table.md) | dos.library LVO offset table |
-| [error_codes.md](error_codes.md) | Complete DOS error code reference |
+| [error_codes.md](error_codes.md) | DOS error codes, Exec device errors, trackdisk errors, Intuition errors |
diff --git a/14_references/error_codes.md b/14_references/error_codes.md
index 9c035fb..c86296a 100644
--- a/14_references/error_codes.md
+++ b/14_references/error_codes.md
@@ -1,9 +1,93 @@
[← Home](../README.md) · [References](README.md)
-# DOS Error Codes — Complete Reference
+# Error Codes — Complete Reference
-Duplicate of [error_handling.md](../07_dos/error_handling.md) error table — see that file for the full list with descriptions.
+## DOS Error Codes
-This file is kept as a quick cross-reference entry point.
+Full reference in [error_handling.md](../07_dos/error_handling.md). Summary of the most commonly encountered:
-→ [Full error code table](../07_dos/error_handling.md)
+| Code | Constant | Description |
+|---|---|---|
+| 103 | `ERROR_INSUFFICIENT_FREE_STORE` | Out of memory |
+| 114 | `ERROR_BAD_TEMPLATE` | Command-line template parse error |
+| 121 | `ERROR_FILE_NOT_OBJECT` | Not a valid executable |
+| 202 | `ERROR_OBJECT_IN_USE` | File/dir locked by another process |
+| 203 | `ERROR_OBJECT_EXISTS` | File already exists (can't create) |
+| 204 | `ERROR_DIR_NOT_FOUND` | Directory path not found |
+| 205 | `ERROR_OBJECT_NOT_FOUND` | File not found |
+| 209 | `ERROR_ACTION_NOT_KNOWN` | Packet type not supported by handler |
+| 210 | `ERROR_INVALID_COMPONENT_NAME` | Illegal character in filename |
+| 212 | `ERROR_OBJECT_WRONG_TYPE` | e.g., tried to Enter a file |
+| 213 | `ERROR_DISK_NOT_VALIDATED` | Disk not yet validated after insert |
+| 214 | `ERROR_DISK_WRITE_PROTECTED` | Write-protected media |
+| 216 | `ERROR_DIRECTORY_NOT_EMPTY` | Can't delete non-empty directory |
+| 218 | `ERROR_DEVICE_NOT_MOUNTED` | Device/volume not mounted |
+| 221 | `ERROR_DISK_FULL` | No free space |
+| 222 | `ERROR_DELETE_PROTECTED` | File has delete protection |
+| 223 | `ERROR_WRITE_PROTECTED` | File has write protection |
+| 224 | `ERROR_READ_PROTECTED` | File has read protection |
+| 225 | `ERROR_NOT_A_DOS_DISK` | Unrecognised disk format |
+| 226 | `ERROR_NO_DISK` | No disk in drive |
+| 232 | `ERROR_NO_MORE_ENTRIES` | End of directory scan (ExNext) |
+| 233 | `ERROR_IS_SOFT_LINK` | Object is a soft link |
+| 303 | `ERROR_BUFFER_OVERFLOW` | Buffer too small |
+
+---
+
+## Exec Device Errors
+
+Returned in `io_Error` field of IORequest:
+
+| Code | Constant | Description |
+|---|---|---|
+| 0 | — | Success |
+| -1 | `IOERR_OPENFAIL` | OpenDevice failed |
+| -2 | `IOERR_ABORTED` | I/O request was AbortIO'd |
+| -3 | `IOERR_NOCMD` | Command not supported by this device |
+| -4 | `IOERR_BADLENGTH` | Invalid length parameter |
+| -5 | `IOERR_BADADDRESS` | Invalid address parameter |
+| -6 | `IOERR_UNITBUSY` | Unit is busy (exclusive access) |
+| -7 | `IOERR_SELFTEST` | Hardware self-test failure |
+
+---
+
+## Trackdisk / SCSI Errors
+
+| Code | Constant | Description |
+|---|---|---|
+| 20 | `TDERR_NotSpecified` | General hardware error |
+| 21 | `TDERR_NoSecHdr` | Sector header not found |
+| 22 | `TDERR_BadSecPreamble` | Bad sector preamble |
+| 23 | `TDERR_BadSecID` | Bad sector ID |
+| 24 | `TDERR_BadHdrSum` | Header checksum error |
+| 25 | `TDERR_BadSecSum` | Sector checksum error |
+| 26 | `TDERR_TooFewSecs` | Too few sectors found |
+| 27 | `TDERR_BadSecHdr` | Bad sector header |
+| 28 | `TDERR_WriteProt` | Disk is write protected |
+| 29 | `TDERR_DiskChanged` | Disk was changed |
+| 30 | `TDERR_SeekError` | Seek error |
+| 31 | `TDERR_NoMem` | Not enough memory |
+| 32 | `TDERR_BadUnitNum` | Invalid unit number |
+| 33 | `TDERR_BadDriveType` | Bad drive type |
+| 34 | `TDERR_DriveInUse` | Drive in use |
+| 35 | `TDERR_PostReset` | Post-reset error |
+
+---
+
+## Intuition/Workbench Error Codes
+
+| Value | Meaning |
+|---|---|
+| 0 | Success |
+| 1 | Not enough memory |
+| 2 | No chip memory |
+| 3 | No free store |
+| 4 | Screen too big |
+| 5 | No screen |
+
+---
+
+## References
+
+- NDK39: `dos/dos.h`, `exec/errors.h`, `devices/trackdisk.h`
+- See: [error_handling.md](../07_dos/error_handling.md) — DOS error handling patterns
diff --git a/16_driver_development/README.md b/16_driver_development/README.md
index 5cde509..9080485 100644
--- a/16_driver_development/README.md
+++ b/16_driver_development/README.md
@@ -2,11 +2,13 @@
# Driver Development — Overview
+Technical reference for developing hardware drivers on AmigaOS. Covers the exec.device framework, RTG graphics card drivers (Picasso96/CyberGraphX), SANA-II network drivers, and AHI audio drivers.
+
## Section Index
| File | Description |
|---|---|
-| [sana2_driver.md](sana2_driver.md) | Writing a SANA-II network device driver |
-| [rtg_driver.md](rtg_driver.md) | Writing Picasso96/RTG display card drivers |
-| [ahi_driver.md](ahi_driver.md) | Writing AHI audio drivers |
-| [device_driver_basics.md](device_driver_basics.md) | exec.device framework — how Amiga devices work |
+| [device_driver_basics.md](device_driver_basics.md) | exec.device framework — IORequest lifecycle, BeginIO/AbortIO, unit management |
+| [rtg_driver.md](rtg_driver.md) | Picasso96/RTG driver architecture — SetFunction patching, BoardInfo hooks, VRAM/CRTC management, signal routing, Native driver, compatibility issues, system tuning |
+| [sana2_driver.md](sana2_driver.md) | SANA-II network device driver specification |
+| [ahi_driver.md](ahi_driver.md) | AHI retargetable audio driver interface |
diff --git a/16_driver_development/rtg_driver.md b/16_driver_development/rtg_driver.md
index 6c27846..63fdf0a 100644
--- a/16_driver_development/rtg_driver.md
+++ b/16_driver_development/rtg_driver.md
@@ -1,176 +1,309 @@
[← Home](../README.md) · [Driver Development](README.md)
-# Writing Picasso96/RTG Display Drivers
+# RTG — Retargetable Graphics: Architecture, Hardware, and Driver Development
## Overview
-**RTG** (Retargetable Graphics) allows the Amiga to use non-native display hardware (graphics cards). The two major RTG systems are **Picasso96** and **CyberGraphX**. Both use a board driver model where a `.card` file provides hardware-specific acceleration.
+**RTG** (Retargetable Graphics) is the framework that allows AmigaOS to use external graphics cards instead of, or alongside, the native custom chipset (OCS/ECS/AGA). RTG was essential for the Amiga's evolution beyond the 15 kHz PAL/NTSC display limitations, enabling VGA-class resolutions (up to 1600×1200) and true-colour (16/24/32-bit) output.
+
+The two major RTG systems are:
+- **Picasso96 (P96)** — the de facto standard, now maintained by Individual Computers
+- **CyberGraphX (CGX)** — the earlier competitor, used on CyberVision cards
+
+Both share the same fundamental architecture: a **board driver** model where `.card` library files provide hardware-specific functions called by the RTG system library.
---
-## Architecture
+## The Problem RTG Solves
+
+```mermaid
+graph LR
+ subgraph "Native Chipset (OCS/ECS/AGA)"
+ direction TB
+ NC_BW["Max: 1280×512 (AGA)"]
+ NC_CD["Max: 256 colours (8-bit)"]
+ NC_FM["Planar framebuffer"]
+ NC_FR["15 kHz / 31 kHz PAL/NTSC"]
+ end
+
+ subgraph "RTG Graphics Card"
+ direction TB
+ RTG_BW["Up to: 1600×1200"]
+ RTG_CD["16/24/32-bit True Colour"]
+ RTG_FM["Chunky (linear) framebuffer"]
+ RTG_FR["31–100+ kHz VGA"]
+ end
+
+ NC_BW -.->|"RTG bypasses
all limitations"| RTG_BW
+
+ style NC_BW fill:#ffcdd2,stroke:#c62828,color:#333
+ style RTG_BW fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+### Planar vs Chunky — The Fundamental Difference
+
+The native Amiga chipset uses **planar** pixel storage — colour bits are spread across separate bitplanes. RTG cards use **chunky** (linear, packed) pixel storage:
```
-Application
- ↓ graphics.library / cybergraphics.library / Picasso96API
- ↓
-RTG System (P96 or CGX)
- ↓ Calls board driver functions
- ↓
-Board Driver (.card file)
- ↓ Programs hardware registers
- ↓
-Graphics Card Hardware (Cirrus, S3, Permedia, FPGA framebuffer...)
+PLANAR (Native Amiga — 4 bitplanes = 16 colours):
+ Bitplane 0: 10110010... ← bit 0 of each pixel's colour
+ Bitplane 1: 01100101... ← bit 1
+ Bitplane 2: 11010001... ← bit 2
+ Bitplane 3: 00101100... ← bit 3
+
+ To read pixel 0's colour: read bit 0 from each plane → combine
+
+CHUNKY (RTG Card — 8-bit indexed):
+ Pixel 0: $0D Pixel 1: $05 Pixel 2: $1B Pixel 3: $0A ...
+
+ Each pixel's colour stored contiguously — one byte per pixel (8-bit)
+ Or: two bytes per pixel (16-bit), three (24-bit), four (32-bit)
```
---
-## Picasso96 Board Driver Interface
+## System Architecture
-A P96 board driver is a shared library exposing specific functions:
+```mermaid
+flowchart TD
+ APP["Application"] --> GFX["graphics.library
(patched by RTG)"]
+ APP --> P96API["Picasso96API.library
/ cybergraphics.library"]
-### Required Functions
+ GFX --> RTG["rtg.library
(P96 core)"]
+ P96API --> RTG
-| Function | Description |
-|---|---|
-| `FindCard(boardInfo)` | Detect and identify the graphics card |
-| `InitCard(boardInfo)` | Initialise card, set up memory map |
-| `SetSwitch(boardInfo, state)` | Switch between Amiga native and RTG display |
-| `SetColorArray(boardInfo, start, count)` | Set palette entries |
-| `SetDAC(boardInfo, type)` | Configure DAC mode |
-| `SetGC(boardInfo, modeInfo, border)` | Set graphics context (resolution, depth) |
-| `SetPanning(boardInfo, addr, w, x, y)` | Set display start address (scrolling) |
-| `SetDisplay(boardInfo, state)` | Enable/disable display output |
+ RTG --> CARD["Board Driver
(mycard.card)"]
+ CARD --> CHIP["Chip Driver
(e.g. CirrusGD.chip)"]
-### Optional Acceleration Functions
+ CHIP --> HW["Graphics Card
Hardware"]
-| Function | Description |
-|---|---|
-| `BlitRect(boardInfo, ri, x1, y1, x2, y2, w, h, mask, mode)` | Accelerated rectangle blit |
-| `FillRect(boardInfo, ri, x, y, w, h, pen, mask, mode)` | Accelerated rectangle fill |
-| `InvertRect(boardInfo, ri, x, y, w, h, mask, mode)` | Accelerated rectangle invert |
-| `BlitRectNoMaskComplete(...)` | Blit without mask |
-| `BlitTemplate(...)` | Text/pattern blit |
-| `DrawLine(...)` | Accelerated line drawing |
-| `SetSprite(boardInfo, state)` | Hardware sprite control |
-| `SetSpritePosition(boardInfo, x, y)` | Move hardware sprite |
-| `SetSpriteImage(boardInfo, ...)` | Set sprite image data |
-| `SetSpriteColor(boardInfo, idx, r, g, b)` | Set sprite palette |
+ GFX --> NATIVE["Native Chipset
(OCS/ECS/AGA)"]
+
+ RTG -.->|"SetSwitch()"| SW{{"Display Switch"}}
+ NATIVE -.-> SW
+ SW --> MONITOR["Monitor"]
+
+ style RTG fill:#e8f4fd,stroke:#2196f3,color:#333
+ style CARD fill:#fff9c4,stroke:#f9a825,color:#333
+ style CHIP fill:#fff3e0,stroke:#ff9800,color:#333
+```
+
+### How P96 Integrates with the OS — The SetFunction Patch
+
+P96's core trick is **patching the OS itself**. At startup, `rtg.library` uses `exec.library/SetFunction()` to replace function vectors inside `graphics.library` and `intuition.library` with its own versions. After patching, every application that calls standard OS drawing functions is **transparently redirected** to the RTG card — no application changes needed.
+
+```mermaid
+flowchart LR
+ subgraph "Before P96 (native only)"
+ APP1["App calls
BltBitMap()"] --> GFX1["graphics.library
original vector"]
+ GFX1 --> CHIP1["Custom Chips
(Blitter DMA)"]
+ end
+
+ subgraph "After P96 patches"
+ APP2["App calls
BltBitMap()"] --> GFX2["graphics.library
PATCHED vector"]
+ GFX2 --> CHECK{"Is target bitmap
in VRAM?"}
+ CHECK -->|"Yes (RTG)"| P96["rtg.library
→ card driver
→ HW blitter"]
+ CHECK -->|"No (Chip RAM)"| CHIP2["Original function
→ Custom Chips"]
+ end
+
+ style GFX2 fill:#fff9c4,stroke:#f9a825,color:#333
+ style CHECK fill:#e8f4fd,stroke:#2196f3,color:#333
+```
+
+#### What Gets Patched — Hooked Function Vectors
+
+| Library | Function (LVO) | Original (Native) | RTG Reimplementation |
+|---|---|---|---|
+| `graphics.library` | `BltBitMap` (−30) | Programs the Blitter DMA to copy rectangular regions between planar bitmaps in Chip RAM. Uses hardware minterm logic for raster ops (AND/OR/XOR). | Checks source/dest bitmap memory addresses. If either is in card VRAM, calls `BoardInfo->BlitRect()` which programs the card's 2D engine to do a VRAM-to-VRAM rectangle copy. If both are Chip RAM, falls through to original Blitter path. Mixed (Chip↔VRAM) requires CPU-mediated copy across the bus. |
+| `graphics.library` | `BltBitMapRastPort` (−606) | Blits a bitmap into a RastPort (with clipping). Uses native Blitter with layer clip rectangles. | Same VRAM detection. For RTG RastPorts, iterates the ClipRect list and calls `BlitRect` for each visible clip rectangle individually, handling layer obscuring. |
+| `graphics.library` | `RectFill` (−306) | Programs native Blitter to fill a rectangular region with the RastPort's foreground pen using planar fill mode. | Calls `BoardInfo->FillRect()` which sends a solid-fill command to the card's 2D engine with the pen colour converted to the screen's `RGBFTYPE`. Single register write + blit trigger — extremely fast. |
+| `graphics.library` | `Move`/`Draw` (−240/−246) | Uses the Blitter's line-draw mode (BLTCON1 bit 0) to draw a one-pixel-wide line between two points in planar memory. | Calls `BoardInfo->DrawLine()` which programs the card's hardware Bresenham line engine. If DrawLine is NULL, falls back to CPU-computed pixel-by-pixel `WritePixel` into VRAM. |
+| `graphics.library` | `WritePixel` (−324) | Computes bitplane offsets, sets/clears the appropriate bit in each plane for the pen colour. | Computes byte offset in chunky VRAM: `offset = y * BytesPerRow + x * BPP`. Writes the pen colour directly as 1/2/3/4 bytes depending on depth. No hardware assist needed — just a bus write. |
+| `graphics.library` | `ReadPixel` (−318) | Reads the corresponding bit from each bitplane, assembles the colour index. | Reads 1/2/3/4 bytes from VRAM at the pixel offset. **Very slow on Zorro II** — each read stalls the CPU for a full bus cycle (~1 µs). Avoid in loops. |
+| `graphics.library` | `ScrollRaster` (−396) | Uses native Blitter to shift a rectangular region, then clears the exposed strip. | Calls `BlitRect` to shift the rectangle within VRAM, then `FillRect` to clear the newly exposed area. Alternatively, if the entire screen scrolls, uses `SetPanning()` to simply change the display start address — zero-copy scroll. |
+| `graphics.library` | `AllocBitMap` (−918) | Allocates a planar bitmap in Chip RAM (`MEMF_CHIP`). Creates N bitplane pointers, one per plane. | If the bitmap is for an RTG screen (friend bitmap is RTG), allocates a contiguous chunky buffer from card VRAM instead. Sets a private flag so all subsequent drawing ops route to the card. The bitmap struct's Planes[0] points to VRAM; Planes[1..7] are NULL (chunky = one plane). |
+| `graphics.library` | `FreeBitMap` (−924) | Frees planar bitplane memory back to Chip RAM pool. | If bitmap is in VRAM, frees it from the card's VRAM allocator (managed by `rtg.library`). Clears the RTG flag. |
+| `graphics.library` | `SetAPen`/`SetBPen` | Stores pen index in RastPort for subsequent draw calls. | Same — pen storage is RastPort-local. But for hi/true-colour RTG screens, P96 must also resolve the pen to an RGB value via the screen's colour table when the actual draw call happens. |
+| `graphics.library` | `LoadRGB32` (−882) | Programs the native colour palette registers ($DFF180–$DFF1BE for OCS, or AGA bank registers). | Calls `BoardInfo->SetColorArray()` which programs the card's RAMDAC palette registers. For 8-bit CLUT screens, this updates the hardware palette. For 16/24/32-bit screens, this updates a software lookup table only. |
+| `intuition.library` | `OpenScreen` (−198) | Creates a ViewPort, allocates Chip RAM bitplanes, builds a Copper list for the display, and programs Denise/Lisa registers. | Intercepts the mode ID. If it's an RTG mode: allocates VRAM for the framebuffer, calls `SetGC()` to program the card's CRTC timing registers, calls `SetDAC()` for pixel format, and calls `SetSwitch(TRUE)` to route the monitor to the card's output. No Copper list is built. |
+| `intuition.library` | `CloseScreen` (−66) | Tears down ViewPort, frees Copper lists and Chip RAM bitplanes. | Frees VRAM allocations. If this was the last RTG screen, calls `SetSwitch(FALSE)` to return the monitor to native chipset output. |
+| `intuition.library` | `ScreenToFront` (−252) | Reorders the screen list and rebuilds the Copper list to display this screen on top. | If the new front screen is on different hardware than the current display (e.g., switching from native to RTG), calls `SetSwitch()` to toggle the monitor. If same hardware, just reorders the screen depth. |
+| `intuition.library` | `MoveScreen` (−162) | Adjusts the screen's ViewPort Y-offset for drag-scrolling; Copper list is rebuilt to show the new position. | For RTG screens, calls `SetPanning()` with the new display start offset. The card's CRTC start address register changes — zero-copy, instant scroll. |
+
+The critical decision happens at every call: **is the target bitmap in VRAM (RTG) or Chip RAM (native)?** If the bitmap was allocated by P96 on the graphics card, the call goes to the card driver. If it's a regular Chip RAM bitmap, the original unpatched function is called — the native Blitter handles it as usual.
+
+> This is why RTG is "transparent" — old applications that use proper OS calls (not direct hardware banging) work on RTG screens without modification. The OS is doing the same thing it always did; P96 just redirected where "the same thing" happens.
+
+### Two-Driver Model
+
+P96 splits hardware support into two independent drivers:
+
+| Driver Type | File Extension | Responsibility | Example |
+|---|---|---|---|
+| **Card Driver** | `.card` | Board identification, Zorro/PCI bus interface, memory mapping, interrupt handling | `PicassoIV.card` |
+| **Chip Driver** | `.chip` | VGA/graphics controller programming: CRTC timing, DAC, blitter commands | `CirrusGD.chip` |
+
+This separation means one chip driver can support multiple boards using the same VGA controller:
+
+```
+PicassoII.card ─┐
+PicassoIV.card ─┤── CirrusGD.chip (shared — Cirrus Logic GD542x/5446)
+Spectrum.card ─┘
+
+CyberVision.card ── S3Virge.chip
+Mediator_PCI.card ─── Permedia2.chip
+```
---
-## struct BoardInfo
+## Graphics Card Hardware
+
+### Major Amiga Graphics Cards
+
+| Card | Controller | Bus | VRAM | Acceleration | Notes |
+|---|---|---|---|---|---|
+| **Picasso II** | Cirrus GD5426/28 | Zorro II | 1–2 MB DRAM | 2D blitter | Most popular; reliable |
+| **Picasso IV** | Cirrus GD5446 | Zorro II/III | 4 MB EDO | 64-bit blitter, 180 MB/s fill | Integrated flicker-fixer + video scaler |
+| **CyberVision 64** | S3 Trio64 | Zorro III | 2–4 MB | S3 2D engine | First Zorro III gfx card; Roxxler C2P chip |
+| **CyberVision 64/3D** | S3 ViRGE | Zorro III | 4 MB | 2D + basic 3D | ViRGE "decelerator" — 3D was slow |
+| **Merlin** | Tseng ET4000W32 | Zorro II/III | 2–4 MB | Tseng blitter | Very fast 2D |
+| **Retina Z3** | NCR 77C32BLT | Zorro III | 4 MB | NCR blitter | High-end |
+| **uaegfx** | Virtual (UAE) | Virtual | Configurable | Software | Emulator virtual card |
+| **MiSTer RTG** | FPGA | Virtual | Shared DDR | Software/HW | MiSTer Amiga core RTG |
+
+### Zorro Bus Memory Mapping
+
+The graphics card's VRAM and registers are mapped into the Amiga's address space via the Zorro expansion bus:
+
+```
+Amiga Address Map:
+ $0000_0000 ─ $001F_FFFF : Chip RAM (2 MB)
+ $00BF_D000 ─ $00BF_DFFF : CIA registers
+ $00C0_0000 ─ $00D7_FFFF : Slow RAM (A500 trapdoor)
+ $00DC_0000 ─ $00DC_FFFF : RTC
+ $00DF_F000 ─ $00DF_FFFF : Custom chip registers (Agnus/Denise/Paula)
+ ┌─────────────────────────────────────────────┐
+ │ $0020_0000 ─ $009F_FFFF : Zorro II space │ ← Picasso II maps here
+ │ Card VRAM: $0020_0000 (2 MB window) │
+ │ Card Regs: $0022_0000 (MMIO) │
+ └─────────────────────────────────────────────┘
+ ┌─────────────────────────────────────────────┐
+ │ $1000_0000 ─ $7FFF_FFFF : Zorro III space │ ← Picasso IV (Z3 mode)
+ │ Card VRAM: $4000_0000 (4 MB linear) │
+ │ Card Regs: $4040_0000 (MMIO) │
+ └─────────────────────────────────────────────┘
+```
+
+> [!IMPORTANT]
+> **Zorro II limitation**: The 8 MB Zorro II address space is shared by all expansion cards *and* autoconfig Fast RAM. A 2 MB graphics card consumes 25% of available Zorro II space. This is why Zorro III (A3000/A4000) was critical for high-end graphics.
+
+---
+
+## Framebuffer Architecture
+
+### VRAM Layout
+
+The card's VRAM is managed by `rtg.library`. The driver reports available memory via `BoardInfo.MemorySize`, and P96 handles allocation:
+
+```
+┌───────────────────────────────────────┐ $0000_0000 (VRAM start)
+│ Screen 0 (Workbench) │
+│ 1024 × 768 × 16bpp = 1,572,864 bytes │
+├───────────────────────────────────────┤ $0018_0000
+│ Screen 1 (Application) │
+│ 800 × 600 × 32bpp = 1,920,000 bytes │
+├───────────────────────────────────────┤ $0035_4E00
+│ Hardware Sprite Data │
+│ 64 × 64 × 2bpp cursor │
+├───────────────────────────────────────┤
+│ Off-screen bitmap cache │
+│ (window backing store, icons, etc.) │
+├───────────────────────────────────────┤
+│ Free VRAM │
+└───────────────────────────────────────┘ MemorySize
+
+Total: 2 MB (Picasso II) / 4 MB (Picasso IV)
+```
+
+### Screen Modes and CRTC Timing
+
+The chip driver programs the **CRTC** (CRT Controller) registers to set display timing:
```c
-/* P96 BoardInfo — key fields */
-struct BoardInfo {
- UBYTE *MemoryBase; /* card framebuffer base */
- ULONG MemorySize; /* framebuffer size */
- UBYTE *RegisterBase; /* MMIO register base */
- ULONG BoardType; /* board type ID */
- UBYTE *ChipBase; /* chip register base */
-
- /* Function pointers (set by driver): */
- APTR SetSwitch;
- APTR SetColorArray;
- APTR SetDAC;
- APTR SetGC;
- APTR SetPanning;
- APTR SetDisplay;
- APTR WaitVerticalSync;
-
- /* Acceleration (NULL = software fallback): */
- APTR BlitRect;
- APTR FillRect;
- APTR InvertRect;
- APTR DrawLine;
- APTR BlitTemplate;
- APTR BlitRectNoMaskComplete;
-
- /* Sprite: */
- APTR SetSprite;
- APTR SetSpritePosition;
- APTR SetSpriteImage;
- APTR SetSpriteColor;
-
- /* Display mode database: */
- struct ModeInfo *ModeInfoList;
-
- /* Color palette (256 entries): */
- UBYTE CLUT[256 * 3];
-
- /* ... many more fields ... */
-};
-```
-
----
-
-## Minimal FindCard Implementation
-
-```c
-BOOL FindCard(struct BoardInfo *bi)
+/* SetGC — called when switching to a new display mode: */
+BOOL MySetGC(struct BoardInfo *bi, struct ModeInfo *mi, BOOL border)
{
- struct ConfigDev *cd = NULL;
-
- /* Scan Zorro bus for our card: */
- cd = FindConfigDev(cd, MY_MANUFACTURER_ID, MY_PRODUCT_ID);
- if (!cd) return FALSE;
-
- bi->MemoryBase = cd->cd_BoardAddr;
- bi->MemorySize = cd->cd_BoardSize;
- bi->RegisterBase = cd->cd_BoardAddr + REGISTER_OFFSET;
-
+ /* Program CRTC timing registers: */
+ WriteRegister(bi, CRTC_HTOTAL, mi->HorTotal);
+ WriteRegister(bi, CRTC_HDISPEND, mi->HorDispEnd);
+ WriteRegister(bi, CRTC_HBLANKSTART, mi->HorBlankStart);
+ WriteRegister(bi, CRTC_HBLANKEND, mi->HorBlankEnd);
+ WriteRegister(bi, CRTC_HSYNCSTART, mi->HorSyncStart);
+ WriteRegister(bi, CRTC_HSYNCEND, mi->HorSyncEnd);
+
+ WriteRegister(bi, CRTC_VTOTAL, mi->VerTotal);
+ WriteRegister(bi, CRTC_VDISPEND, mi->VerDispEnd);
+ WriteRegister(bi, CRTC_VSYNCSTART, mi->VerSyncStart);
+ WriteRegister(bi, CRTC_VSYNCEND, mi->VerSyncEnd);
+
+ /* Set pixel depth: */
+ SetBitsPerPixel(bi, mi->Depth); /* 8, 15, 16, 24, 32 */
+
+ /* Set pixel clock (dot clock): */
+ SetClock(bi, mi->PixelClock);
+
return TRUE;
}
```
---
-## Minimal InitCard Implementation
+## Display Switching — Native ↔ RTG
-```c
-BOOL InitCard(struct BoardInfo *bi)
-{
- /* Set up function pointers: */
- bi->SetSwitch = (APTR)MySetSwitch;
- bi->SetColorArray = (APTR)MySetColorArray;
- bi->SetDAC = (APTR)MySetDAC;
- bi->SetGC = (APTR)MySetGC;
- bi->SetPanning = (APTR)MySetPanning;
- bi->SetDisplay = (APTR)MySetDisplay;
-
- /* Optional acceleration: */
- bi->FillRect = (APTR)MyFillRect;
- bi->BlitRect = (APTR)MyBlitRect;
-
- /* Register display modes: */
- AddResolution(bi, 640, 480, 8); /* 640x480 256 colours */
- AddResolution(bi, 800, 600, 16); /* 800x600 16-bit */
- AddResolution(bi, 1024, 768, 24); /* 1024x768 24-bit */
-
- /* Reset hardware: */
- HW_Reset(bi->RegisterBase);
-
- return TRUE;
-}
+This is the most visible RTG operation for users: switching the monitor between the native chipset and the graphics card.
+
+```mermaid
+stateDiagram-v2
+ [*] --> Native : Boot
+ Native --> RTG : User drags to RTG screen
SetSwitch(TRUE)
+ RTG --> Native : User drags to native screen
SetSwitch(FALSE)
+ RTG --> RTG : Switch between RTG modes
SetGC()
```
----
+### Physical Signal Routing
-## SetSwitch — Native ↔ RTG Toggle
+On real hardware, display switching works through one of these mechanisms:
+
+| Method | How It Works | Cards |
+|---|---|---|
+| **VGA pass-through cable** | Card has VGA input + output. Native signal passes through card; card switches to its own output when active | Picasso II, Merlin |
+| **Integrated scan-doubler** | Card accepts native Amiga video and can mix/switch internally | Picasso IV (built-in flicker-fixer) |
+| **Monitor with dual input** | Two cables, user switches monitor input | Any card |
+| **Software framebuffer copy** | RTG system copies native display to card's VRAM (slow, emulator approach) | UAE/MiSTer (virtual) |
```c
+/* SetSwitch — toggle display source: */
BOOL MySetSwitch(struct BoardInfo *bi, BOOL state)
{
- if (state) {
- /* Switch TO RTG display */
- HW_EnableOutput(bi->RegisterBase);
- /* Disable Amiga native display if using pass-through: */
- custom.dmacon = DMAF_RASTER; /* turn off bitplane DMA */
- } else {
- /* Switch BACK to Amiga native */
- HW_DisableOutput(bi->RegisterBase);
- custom.dmacon = DMAF_SETCLR | DMAF_RASTER;
+ if (state)
+ {
+ /* Switching TO RTG: */
+ /* 1. Enable graphics card output */
+ HW_EnableDAC(bi->RegisterBase);
+ HW_SetVGAPassthrough(bi->RegisterBase, FALSE); /* card output */
+
+ /* 2. Optionally disable native display DMA to save bandwidth */
+ custom->dmacon = DMAF_RASTER; /* turn off bitplane DMA */
+ }
+ else
+ {
+ /* Switching BACK to native: */
+ /* 1. Disable graphics card output */
+ HW_DisableDAC(bi->RegisterBase);
+ HW_SetVGAPassthrough(bi->RegisterBase, TRUE); /* pass-through */
+
+ /* 2. Re-enable native display */
+ custom->dmacon = DMAF_SETCLR | DMAF_RASTER;
}
return state;
}
@@ -178,17 +311,710 @@ BOOL MySetSwitch(struct BoardInfo *bi, BOOL state)
---
-## Installation
+## Hardware Acceleration
+
+### What Can Be Accelerated?
+
+RTG cards with a 2D engine can offload these operations from the CPU:
+
+| Operation | Function | Description | Speedup |
+|---|---|---|---|
+| **Rect Fill** | `FillRect()` | Fill rectangle with solid colour | 10–50× |
+| **Rect Blit** | `BlitRect()` | Copy rectangle (VRAM→VRAM) | 5–20× |
+| **Screen Scroll** | `BlitRect()` + `SetPanning()` | Scroll display contents | 5–20× |
+| **Pattern Fill** | `BlitTemplate()` | Fill with pattern/text glyph | 5–15× |
+| **Line Draw** | `DrawLine()` | Bresenham line | 2–10× |
+| **Invert Rect** | `InvertRect()` | XOR rectangle (selection highlight) | 5–20× |
+| **HW Cursor** | `SetSprite*()` | Hardware sprite cursor | ∞ (zero CPU) |
+
+### Acceleration Implementation Example
+
+```c
+/* FillRect — hardware-accelerated solid fill: */
+BOOL MyFillRect(struct BoardInfo *bi,
+ struct RenderInfo *ri,
+ WORD x, WORD y, WORD w, WORD h,
+ ULONG pen, UBYTE mask, RGBFTYPE fmt)
+{
+ /* Calculate VRAM offset: */
+ ULONG offset = (ULONG)ri->Memory - (ULONG)bi->MemoryBase;
+ offset += y * ri->BytesPerRow + x * GetBytesPerPixel(fmt);
+
+ /* Program the blitter engine: */
+ WaitBlitter(bi); /* wait for previous blit to complete */
+
+ HW_SetBlitDest(bi->RegisterBase, offset);
+ HW_SetBlitPitch(bi->RegisterBase, ri->BytesPerRow);
+ HW_SetBlitSize(bi->RegisterBase, w, h);
+ HW_SetBlitColour(bi->RegisterBase, pen);
+ HW_SetBlitMode(bi->RegisterBase, BLIT_FILL);
+ HW_StartBlit(bi->RegisterBase); /* fire! */
+
+ return TRUE;
+}
+```
+
+### Software Fallback
+
+If a function pointer in `BoardInfo` is NULL, P96 uses a software implementation. This means a minimal driver only needs `FindCard`, `InitCard`, `SetSwitch`, `SetGC`, `SetPanning`, `SetColorArray`, `SetDAC`, and `SetDisplay` — everything else falls back to CPU rendering.
+
+---
+
+## Planar-to-Chunky Conversion (C2P)
+
+When legacy planar software runs on an RTG screen, the system must convert:
```
-DEVS:Monitors/mycard.card ; the board driver
-DEVS:Monitors/mycard ; monitor file (text config)
+Planar Data (4 planes): Chunky Output (8bpp):
+ Plane 0: 1 0 1 1 0 0 1 0 Pixel 0: 0101 = $05
+ Plane 1: 0 1 0 1 1 0 0 1 Pixel 1: 1010 = $0A
+ Plane 2: 1 1 0 0 0 1 1 0 → Pixel 2: 0100 = $04
+ Plane 3: 0 1 1 0 1 1 0 0 Pixel 3: 1011 = $0B
+ ...
```
+This is **extremely CPU-intensive**. The CyberVision 64 included a dedicated **Roxxler** chip for hardware C2P acceleration. Without hardware help, C2P runs at ~2–5 FPS for fullscreen games — which is why RTG was primarily for productivity, not gaming.
+
+> [!WARNING]
+> Old games that bang hardware registers directly (writing to `$DFF1xx` colour registers, custom chip DMA) will **never** work on RTG. They must be run on the native chipset display.
+
+---
+
+## The Picasso96 BoardInfo Structure
+
+```c
+/* boardinfo.h — key fields from P96 SDK */
+struct BoardInfo {
+ /* Memory layout: */
+ UBYTE *MemoryBase; /* VRAM base address (Zorro-mapped) */
+ ULONG MemorySize; /* Total VRAM available to P96 */
+ UBYTE *RegisterBase; /* MMIO register base */
+ UBYTE *ChipBase; /* VGA I/O register base */
+ APTR CardData; /* Driver private data (per-instance) */
+
+ /* Board identity: */
+ ULONG BoardType; /* Board ID */
+ char BoardName[32]; /* Human-readable name */
+
+ /* Display state: */
+ struct ModeInfo *ModeInfoList; /* Linked list of supported modes */
+ UBYTE CLUT[256 * 3]; /* Current palette (RGB triplets) */
+ BOOL SoftSpriteWA; /* TRUE = no HW sprite available */
+
+ /* Required function hooks: */
+ APTR FindCard; /* LVO -$1E */
+ APTR InitCard; /* LVO -$24 */
+ APTR SetSwitch;
+ APTR SetColorArray;
+ APTR SetDAC;
+ APTR SetGC;
+ APTR SetPanning;
+ APTR SetDisplay;
+ APTR WaitVerticalSync;
+
+ /* Optional acceleration hooks (NULL = software fallback): */
+ APTR BlitRect;
+ APTR FillRect;
+ APTR InvertRect;
+ APTR DrawLine;
+ APTR BlitTemplate;
+ APTR BlitRectNoMaskComplete;
+ APTR BlitPlanar2Chunky; /* C2P acceleration */
+
+ /* Hardware sprite hooks: */
+ APTR SetSprite;
+ APTR SetSpritePosition;
+ APTR SetSpriteImage;
+ APTR SetSpriteColor;
+
+ /* Interrupt hooks: */
+ APTR SetInterrupt; /* VBlank interrupt setup */
+ APTR WaitBlitter; /* Wait for HW blitter idle */
+
+ /* ... many more fields (see boardinfo.h) ... */
+};
+```
+
+> Each board instance gets its own `BoardInfo`. Multi-card setups (e.g., two Picasso IVs) each have separate structures. Per-instance state must go in `bi->CardData`, not in driver globals.
+
+---
+
+## Driver Development — FindCard and InitCard
+
+### FindCard
+
+```c
+/* Scan Zorro bus for our graphics card: */
+BOOL __saveds __asm FindCard(register __a0 struct BoardInfo *bi)
+{
+ struct ConfigDev *cd = NULL;
+
+ /* Search for our Zorro board: */
+ cd = FindConfigDev(cd, MANUFACTURER_VILLAGE_TRONIC, PRODUCT_PICASSO_IV);
+ if (!cd) return FALSE;
+
+ /* Map card memory into BoardInfo: */
+ bi->MemoryBase = (UBYTE *)cd->cd_BoardAddr;
+ bi->MemorySize = cd->cd_BoardSize; /* 4 MB for P-IV */
+ bi->RegisterBase = (UBYTE *)cd->cd_BoardAddr + 0x00400000;
+
+ /* Mark the ConfigDev as "driver loaded": */
+ cd->cd_Flags |= CDF_CONFIGME;
+
+ return TRUE;
+}
+```
+
+### InitCard
+
+```c
+BOOL __saveds __asm InitCard(register __a0 struct BoardInfo *bi)
+{
+ /* Register required function hooks: */
+ bi->SetSwitch = (APTR)MySetSwitch;
+ bi->SetColorArray = (APTR)MySetColorArray;
+ bi->SetDAC = (APTR)MySetDAC;
+ bi->SetGC = (APTR)MySetGC;
+ bi->SetPanning = (APTR)MySetPanning;
+ bi->SetDisplay = (APTR)MySetDisplay;
+ bi->WaitVerticalSync = (APTR)MyWaitVSync;
+
+ /* Register optional acceleration: */
+ bi->FillRect = (APTR)MyFillRect;
+ bi->BlitRect = (APTR)MyBlitRect;
+ bi->DrawLine = (APTR)MyDrawLine;
+ bi->BlitTemplate = (APTR)MyBlitTemplate;
+
+ /* Hardware sprite: */
+ bi->SetSprite = (APTR)MySetSprite;
+ bi->SetSpritePosition = (APTR)MySetSpritePos;
+ bi->SetSpriteImage = (APTR)MySetSpriteImg;
+ bi->SetSpriteColor = (APTR)MySetSpriteColor;
+ bi->SoftSpriteWA = FALSE; /* we have HW sprite */
+
+ /* Build resolution list: */
+ struct ModeInfo *mi;
+ /* 640×480 @ 8, 16, 24, 32 bpp */
+ mi = AllocModeInfo(640, 480, 8); AddResolution(bi, mi);
+ mi = AllocModeInfo(640, 480, 16); AddResolution(bi, mi);
+ mi = AllocModeInfo(640, 480, 24); AddResolution(bi, mi);
+ mi = AllocModeInfo(640, 480, 32); AddResolution(bi, mi);
+ /* 800×600, 1024×768, 1280×1024... */
+
+ /* Reset the graphics controller: */
+ ChipReset(bi->RegisterBase);
+
+ return TRUE;
+}
+```
+
+---
+
+## MiSTer FPGA RTG Implementation
+
+The MiSTer Amiga core implements RTG as a virtual graphics card:
+
+| Aspect | Implementation |
+|---|---|
+| **Card identity** | Virtual `uaegfx.card` compatible |
+| **VRAM location** | Mapped into shared DDR memory (not Chip RAM) |
+| **Acceleration** | FillRect and BlitRect in FPGA logic; rest in software |
+| **Display output** | FPGA scaler renders RTG framebuffer to HDMI |
+| **Mode switching** | Software-controlled: OSD or automatic (screen drag) |
+| **Resolution** | Up to 1920×1080 (limited by scaler) |
+| **Colour depth** | 8/16/32-bit |
+
+### Display Pipeline
+
+```mermaid
+flowchart LR
+ subgraph "68020/040 CPU"
+ APP["Application"] --> GFX["P96 rtg.library"]
+ GFX --> UAEGFX["uaegfx.card"]
+ end
+
+ subgraph "FPGA Fabric"
+ UAEGFX --> DDR["DDR3 SDRAM
(RTG framebuffer)"]
+ CHIPSET["AGA Chipset
(native display)"] --> MUX{{"Video MUX"}}
+ DDR --> SCALER["FPGA Scaler"]
+ SCALER --> MUX
+ MUX --> HDMI["HDMI Output"]
+ end
+
+ style DDR fill:#e8f4fd,stroke:#2196f3,color:#333
+ style SCALER fill:#fff9c4,stroke:#f9a825,color:#333
+```
+
+---
+
+## Installation and Configuration
+
+```
+DEVS:Monitors/
+ PicassoIV.card ; board driver library
+ PicassoIV ; monitor definition file (text)
+
+LIBS:Picasso96/
+ CirrusGD.chip ; chip driver
+ rtg.library ; P96 core
+
+SYS:Prefs/Picasso96Mode ; mode editor — select resolutions and refresh rates
+
+; In startup-sequence or user-startup:
+C:AddMonitor DEVS:Monitors/PicassoIV
+```
+
+---
+
+## RGB Pixel Formats
+
+RTG cards support multiple chunky pixel formats. The `RGBFTYPE` enum defines how colour channels are packed:
+
+| Format | Constant | BPP | Layout | Byte Order |
+|---|---|---|---|---|
+| CLUT (indexed) | `RGBFB_CLUT` | 8 | Palette index | `[index]` |
+| RGB15 | `RGBFB_R5G5B5` | 16 | 1-5-5-5 | `0RRRRRGGGGGBBBBB` |
+| RGB16 | `RGBFB_R5G6B5` | 16 | 5-6-5 | `RRRRRGGGGGGBBBBB` |
+| BGR15 | `RGBFB_B5G5R5` | 16 | Swapped | `0BBBBBGGGGGRRRRR` |
+| RGB24 | `RGBFB_R8G8B8` | 24 | 8-8-8 | `RR GG BB` |
+| BGR24 | `RGBFB_B8G8R8` | 24 | Swapped | `BB GG RR` |
+| ARGB32 | `RGBFB_A8R8G8B8` | 32 | 8-8-8-8 | `AA RR GG BB` |
+| BGRA32 | `RGBFB_B8G8R8A8` | 32 | Swapped | `BB GG RR AA` |
+| RGBA32 | `RGBFB_R8G8B8A8` | 32 | Alpha last | `RR GG BB AA` |
+
+> [!IMPORTANT]
+> Different VGA controllers prefer different byte orders. Cirrus Logic uses BGR; S3 uses RGB. The chip driver must handle the correct format for its hardware. P96 normalises this through the `RGBFTYPE` system — applications specify the format and the driver translates.
+
+### VRAM Bandwidth Calculations
+
+| Resolution | Depth | Frame Size | @ 60 Hz Bandwidth |
+|---|---|---|---|
+| 640×480 | 8-bit | 300 KB | 18 MB/s |
+| 800×600 | 16-bit | 937 KB | 56 MB/s |
+| 1024×768 | 16-bit | 1.5 MB | 90 MB/s |
+| 1024×768 | 32-bit | 3 MB | 180 MB/s |
+| 1280×1024 | 16-bit | 2.5 MB | 150 MB/s |
+
+> Zorro II bus bandwidth: ~3–5 MB/s (shared). Zorro III: ~37 MB/s (burst). This is why Zorro II cards struggle above 800×600 — the CPU can't write pixels fast enough through the bus bottleneck.
+
+---
+
+## Application-Side API — Using RTG Screens
+
+### Picasso96API.library
+
+```c
+/* Open a 16-bit true-colour screen: */
+#include
+
+struct Screen *scr = p96OpenScreenTags(
+ P96SA_Width, 800,
+ P96SA_Height, 600,
+ P96SA_Depth, 16,
+ P96SA_RGBFormat, RGBFB_R5G6B5,
+ P96SA_AutoScroll, TRUE,
+ P96SA_Title, "My RTG Screen",
+ TAG_DONE);
+
+/* Lock the bitmap for direct pixel access: */
+struct RenderInfo ri;
+APTR lock = p96LockBitMap(scr->RastPort.BitMap, (UBYTE *)&ri, sizeof(ri));
+if (lock)
+{
+ /* ri.Memory = pointer to pixel data in VRAM */
+ /* ri.BytesPerRow = stride */
+ UWORD *pixels = (UWORD *)ri.Memory;
+
+ /* Write a red pixel at (100, 50): */
+ pixels[50 * (ri.BytesPerRow / 2) + 100] = 0xF800; /* RGB565 red */
+
+ p96UnlockBitMap(scr->RastPort.BitMap, lock);
+}
+
+/* Clean up: */
+p96CloseScreen(scr);
+```
+
+### CyberGraphX API (cgx)
+
+```c
+#include
+
+/* Check if a bitmap is on RTG hardware: */
+BOOL isRTG = GetCyberMapAttr(bitmap, CYBRMATTR_ISRTG);
+ULONG depth = GetCyberMapAttr(bitmap, CYBRMATTR_DEPTH);
+ULONG pixfmt = GetCyberMapAttr(bitmap, CYBRMATTR_PIXFMT);
+
+/* Lock bitmap for direct access: */
+APTR handle = LockBitMapTags(bitmap,
+ LBMI_BASEADDRESS, &baseAddr,
+ LBMI_BYTESPERROW, &bytesPerRow,
+ LBMI_PIXFMT, &pixfmt,
+ TAG_DONE);
+
+/* ... write pixels directly ... */
+
+UnLockBitMap(handle);
+
+/* Blit a chunky buffer to RTG screen: */
+WritePixelArray(chunkyData, 0, 0, srcBytesPerRow,
+ rp, destX, destY, width, height,
+ RECTFMT_RGB);
+```
+
+### Opening RTG Screens with Standard Intuition API
+
+Applications don't need P96API for basic RTG. Standard `OpenScreen()` works if the mode ID is an RTG mode:
+
+```c
+/* Query available RTG modes: */
+ULONG modeID = BestModeID(
+ BIDTAG_NominalWidth, 1024,
+ BIDTAG_NominalHeight, 768,
+ BIDTAG_Depth, 16,
+ BIDTAG_MonitorID, 0, /* any monitor */
+ TAG_DONE);
+
+if (modeID != INVALID_ID)
+{
+ struct Screen *scr = OpenScreenTags(NULL,
+ SA_DisplayID, modeID,
+ SA_Width, 1024,
+ SA_Height, 768,
+ SA_Depth, 16,
+ SA_Title, "RTG via Intuition",
+ SA_ShowTitle, TRUE,
+ TAG_DONE);
+}
+```
+
+---
+
+## Video Output — Signal Routing and Monitor Connections
+
+### Dual-Output Architecture
+
+On real Amiga hardware with an RTG card, two independent video signals exist simultaneously:
+
+```mermaid
+flowchart TD
+ subgraph "Amiga Motherboard"
+ DENISE["Denise/Lisa
(Native Chipset)"] --> DB23["DB-23 Video Out
15 kHz RGB"]
+ end
+
+ subgraph "Graphics Card (Zorro slot)"
+ GPU["Cirrus/S3
VGA Controller"] --> VGA15["VGA Port
31–100 kHz"]
+ end
+
+ DB23 --> PASS["Pass-through cable
(VGA input on card)"]
+ PASS --> GPU
+
+ GPU --> MONITOR["Monitor"]
+
+ style DENISE fill:#ffcdd2,stroke:#c62828,color:#333
+ style GPU fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+### How the Pass-Through Cable Works
+
+1. The Amiga's native DB-23 video output connects to the card's **VGA input** via a pass-through adapter
+2. When displaying a **native** screen, the card passes the Amiga video signal straight through to the monitor
+3. When `SetSwitch(TRUE)` activates RTG, the card **switches** its video multiplexer to output its own VRAM instead
+4. The monitor sees a seamless transition — no cable swapping needed
+
+### Picasso IV — Integrated Video Processing
+
+The Picasso IV was unique in having a built-in **scan-doubler/flicker-fixer** and **video scaler**:
+
+```mermaid
+flowchart LR
+ subgraph "Picasso IV Board"
+ NATIVE_IN["Native Amiga
15 kHz RGB input"] --> SCANDBL["Scan Doubler
(15→31 kHz)"]
+ SCANDBL --> MUX{{"Video MUX"}}
+ GD5446["Cirrus GD5446
RTG Output"] --> MUX
+ MUX --> RAMDAC["RAMDAC
(D/A Converter)"]
+ RAMDAC --> VGAOUT["VGA Output
31+ kHz"]
+ end
+
+ style SCANDBL fill:#fff9c4,stroke:#f9a825,color:#333
+ style GD5446 fill:#c8e6c9,stroke:#2e7d32,color:#333
+```
+
+This means the Picasso IV can:
+- Display native Amiga screens on a standard VGA monitor (scan-doubled from 15 kHz → 31 kHz)
+- Seamlessly switch between native and RTG without any video glitching
+- Even **overlay** native video onto RTG screens in some configurations (genlock-like)
+
+---
+
+## Screen Management — Multiple Screens on Different Hardware
+
+Intuition manages all screens in a single depth-arranged list, regardless of which hardware renders them:
+
+```
+Screen Stack (front to back):
+ ┌─ Screen "Workbench" → RTG: 1024×768×16bpp (Picasso IV)
+ ├─ Screen "Deluxe Paint" → Native: 640×256×32col (AGA)
+ └─ Screen "Terminal" → RTG: 800×600×8bpp (Picasso IV)
+```
+
+When the user drags a screen to reveal the one behind it:
+- If both screens are on the **same hardware** → smooth drag, hardware handles it
+- If screens are on **different hardware** → `SetSwitch()` fires to toggle the monitor between outputs. The "drag to reveal" becomes a hard **cut** — the monitor switches from VGA to native (or vice versa)
+
+> [!WARNING]
+> There is no compositing or blending between native and RTG. They are completely independent video pipelines. You see one or the other, never both simultaneously on one monitor.
+
+---
+
+## Performance Considerations
+
+### Bus Bandwidth — The Critical Bottleneck
+
+| Bus | Peak Bandwidth | Practical | Impact |
+|---|---|---|---|
+| Zorro II | 7.14 MB/s (16-bit) | ~3–5 MB/s | Limits RTG to ~800×600×8bpp at usable speed |
+| Zorro III | 37 MB/s (32-bit burst) | ~20–25 MB/s | 1024×768×16bpp comfortable |
+| PCI (Mediator) | 133 MB/s | ~80 MB/s | Full-speed modern RTG |
+| MiSTer (DDR) | 800+ MB/s | ~400 MB/s | No bottleneck |
+
+### Optimisation Patterns
+
+| Pattern | Description |
+|---|---|
+| **Minimise CPU VRAM writes** | Only write changed regions, not full frames |
+| **Use hardware acceleration** | FillRect/BlitRect are 10–50× faster than CPU |
+| **Batch operations** | Queue multiple blits; WaitBlitter once at the end |
+| **Avoid ReadPixel on VRAM** | Reads across the bus are extremely slow (stalls CPU) |
+| **Use off-screen bitmaps** | Double-buffer: draw in off-screen VRAM, then blit to visible |
+| **Match pixel format** | Use the card's native RGBF to avoid format conversion |
+
+### Anti-Patterns
+
+| Anti-Pattern | Problem |
+|---|---|
+| Reading every pixel from VRAM | Zorro II read = ~1 µs per word; full screen read = seconds |
+| Mixing planar and chunky | Forces expensive C2P conversion |
+| Direct hardware register access | Bypasses RTG — only works on native chipset |
+| Allocating bitmaps in Chip RAM | RTG bitmaps must be in VRAM for acceleration |
+| Not checking `GetCyberMapAttr` | Assuming all bitmaps are planar breaks on RTG |
+
+---
+
+## Troubleshooting
+
+| Symptom | Cause | Fix |
+|---|---|---|
+| Black screen on mode switch | SetSwitch not toggling pass-through correctly | Check VGA cable; verify DAC enable/disable |
+| Corrupted display | CRTC timing wrong | Verify HTotal/VTotal/sync values in SetGC |
+| Garbled colours | Wrong RGBFTYPE (BGR vs RGB swap) | Match format to VGA controller's native order |
+| Slow scrolling | No BlitRect acceleration | Implement hardware blit; check VRAM alignment |
+| Cursor flickers | SoftSpriteWA=TRUE | Implement HW cursor (SetSprite*) |
+| Guru on screen close | Driver doesn't handle CloseScreen cleanup | Free VRAM allocations in board cleanup |
+| Two monitors needed | No pass-through cable or scan doubler | Get Picasso IV or use pass-through adapter |
+
+
+---
+
+## The "Native" Driver — P96 Without a Graphics Card
+
+Thomas Richter's **Native** driver (Aminet: `driver/moni/Native`) reveals a crucial aspect of P96's architecture: the `rtg.library` can operate **without any graphics card at all**, providing CPU-based blitter replacement for the native chipset display.
+
+### What It Does
+
+The Native driver is deceptively simple — all it does is:
+1. Set the environment variable `Picasso96/DisableAmigaBlitter` to `"Yes"`
+2. Load `rtg.library`
+
+This is sufficient for P96 to intercept all Blitter operations and redirect them through the CPU, providing the same benefits as the older **FBlit** utility but through the standard P96 framework.
+
+### Why You'd Want This
+
+The native Amiga Blitter can only access **Chip RAM** (the first 2 MB). This forces all bitmaps — window buffers, icons, fonts — to live in Chip RAM, which is precious and limited. With the Native driver:
+
+```mermaid
+flowchart LR
+ subgraph "Without Native driver"
+ direction TB
+ BM1["All bitmaps"] --> CHIP["Chip RAM
(2 MB limit)"]
+ BLIT["Amiga Blitter"] --> CHIP
+ end
+
+ subgraph "With Native driver"
+ direction TB
+ BM2["Off-screen bitmaps"] --> FAST["Fast RAM
(unlimited)"]
+ BM3["Display bitplanes"] --> CHIP2["Chip RAM
(display only)"]
+ CPU["CPU blitter
(68030+)"] --> FAST
+ CPU --> CHIP2
+ end
+
+ style FAST fill:#c8e6c9,stroke:#2e7d32,color:#333
+ style CHIP fill:#ffcdd2,stroke:#c62828,color:#333
+```
+
+- **Off-screen bitmaps** (window backing store, icons, gadget imagery) can move to Fast RAM
+- Only the **visible display bitplanes** must remain in Chip RAM (for DMA)
+- On a 68030+, the CPU is typically **faster** than the aging OCS/ECS Blitter anyway
+
+### The NOBLITTER Tooltype
+
+The Native driver's icon supports the `NOBLITTER` tooltype, which controls how native Amiga blitter operations are handled:
+
+| Setting | Behaviour | When to Use |
+|---|---|---|
+| `NOBLITTER=YES` (default) | **All** blits go through CPU. Native Amiga Blitter is completely bypassed. | Default. Best for accelerated systems (68030+). Frees Chip RAM. |
+| `NOBLITTER=NO` | Native Blitter is used when **both** source and destination are in Chip RAM. Falls back to CPU only for Fast RAM bitmaps. | Use when you want hardware Blitter for DMA-visible operations (e.g., playfield scrolling) but CPU for off-screen work. |
+
+> [!IMPORTANT]
+> `NOBLITTER=NO` requires **P96 3.3.3 or later** — earlier versions had a bug where Bobs (Workbench icons, which are Blitter Objects) in Fast RAM would crash if the native Blitter was still enabled, because the Blitter DMA engine physically cannot read Fast RAM addresses.
+
+---
+
+## Compatibility Issues — The RTG Porting Minefield
+
+### The Chip RAM / Fast RAM Split
+
+The most fundamental RTG compatibility issue stems from the memory architecture:
+
+```
+Native Amiga:
+ Chip RAM ($000000–$1FFFFF): DMA-accessible by all custom chips
+ → ALL bitmaps must be here (Blitter, display DMA, sprites all need it)
+
+RTG:
+ Card VRAM ($xxxxxxxx): Only accessible by the graphics card
+ Fast RAM: CPU-only, no DMA access
+ Chip RAM: Still needed for native display, but NOT for RTG bitmaps
+```
+
+| Problem | Cause | Symptom |
+|---|---|---|
+| Application assumes `AllocBitMap` returns Chip RAM | RTG returns VRAM pointer instead | Direct pixel math gives wrong addresses |
+| Code reads bitplane pointers directly | RTG bitmap has Planes[0]=VRAM, Planes[1..7]=NULL (chunky) | Writes to garbage addresses, crash |
+| `MEMF_CHIP` allocation for graphics buffers | Unnecessary; wastes Chip RAM | Works but defeats RTG's memory benefit |
+| Using `BltBitMap` between Chip RAM and VRAM | Requires CPU-mediated cross-bus copy | Very slow on Zorro II — stalls system |
+
+### Software That Breaks on RTG
+
+| Category | Example | Root Cause | Fix |
+|---|---|---|---|
+| **Hardware banging** | Game writes directly to `$DFF180` colour registers | Bypasses OS entirely | Must run on native screen — unfixable for RTG |
+| **Direct bitplane access** | Demo reads `rp->BitMap->Planes[3]` | Assumes planar layout | Use `ReadPixelArray`/`WritePixelArray` |
+| **Copper effects** | Colour cycling via Copper list | Copper is native-chipset only | Must use `LoadRGB32` with timer |
+| **HAM mode** | HAM6/HAM8 encoding | HAM is a planar encoding trick | No RTG equivalent; must use true-colour |
+| **Sprite tricks** | Multiplexed sprites, sprite-field overlays | Hardware sprites are native only | RTG uses software cursor or HW sprite emulation |
+| **DMA-dependent timing** | Code waits for Blitter by polling `DMACONR` | RTG blits are CPU, not DMA | Use `WaitBlit()` or P96's `WaitBlitter` |
+| **Fixed palette assumptions** | Expects 4-colour Workbench pen mapping | RTG may have 256+ pens | Use `ObtainBestPen()` |
+
+### The FBlit Conflict
+
+**FBlit** (by Greed) and **FText** were pre-RTG utilities that patched `graphics.library` to move bitmaps to Fast RAM. If used alongside P96:
+
+- Both FBlit and P96 try to patch the same library vectors → **double patching** → system crash
+- FBlit's bitmap allocator doesn't know about P96's VRAM management → memory corruption
+- The Native driver explicitly replaces FBlit's functionality within the P96 framework
+
+> [!CAUTION]
+> **Never run FBlit alongside P96.** Remove FBlit and FText from your startup sequence if P96 is installed. The Native driver provides the same benefits without the conflict.
+
+---
+
+## System Tuning — Best Practices
+
+### For Systems WITH an RTG Card
+
+| Setting | Recommendation | Why |
+|---|---|---|
+| FBlit/FText | **Remove** | Conflicts with P96's own library patching |
+| `NOBLITTER` in card driver | `YES` (default) | Card has its own blitter; native Blitter not needed |
+| Monitor tooltypes | Match card's native RGB format | Avoids per-pixel format conversion overhead |
+| Screen depth | 16-bit for speed, 32-bit for quality | 16-bit = half the bus traffic vs 32-bit |
+| Double-buffer | Use off-screen VRAM bitmaps | Prevents tearing; card blit is fast |
+| `Picasso96/DisableAmigaBlitter` | `YES` | Ensures all blits route through card, not through slow Chip RAM Blitter |
+
+### For Systems WITHOUT an RTG Card (Native Driver)
+
+| Setting | Recommendation | Why |
+|---|---|---|
+| Native driver in `DEVS:Monitors` | **Install** | Enables CPU blitter replacement |
+| CPU | 68030+ strongly recommended | CPU must be faster than native Blitter (~3.58 MHz DMA) for net benefit |
+| `NOBLITTER` in Native icon | `YES` for 68040+, try `NO` for 68030 | 68040/060 CPU blitting is 5–20× faster than hardware Blitter |
+| Chip RAM | Reserve for display and audio DMA only | With Native driver, window bitmaps live in Fast RAM |
+| Cache | **Enable** CPU data cache | CPU blitting benefits enormously from cache |
+
+### For Emulators (UAE/MiSTer)
+
+| Setting | Recommendation | Why |
+|---|---|---|
+| `NOBLITTER` | `YES` | Emulated Blitter has overhead; direct CPU write to VRAM is faster in emulation |
+| JIT/CPU speed | Maximum | CPU blitting speed scales directly with emulation speed |
+| RTG VRAM | 16–32 MB | Generous VRAM avoids allocation failures on high-res modes |
+| `uaegfx.card` | Use latest version | Fixes for edge cases in emulated VRAM access |
+| Native screens | Use separate screen for Chip-only software | Don't force C2P — let native software use native display |
+
+### Application Developer Best Practices
+
+```c
+/* DO: use system bitmap allocation — lets P96 choose Chip vs VRAM */
+struct BitMap *bm = AllocBitMap(width, height, depth,
+ BMF_MINPLANES,
+ screen->RastPort.BitMap); /* friend bitmap! */
+
+/* DON'T: force Chip RAM when not needed */
+struct BitMap *bm = AllocBitMap(width, height, depth,
+ BMF_MINPLANES | BMF_DISPLAYABLE,
+ NULL); /* no friend → defaults to Chip RAM */
+
+/* DO: check if bitmap is RTG before direct access */
+if (GetCyberMapAttr(bm, CYBRMATTR_ISRTG))
+{
+ /* Lock for direct pixel access: */
+ APTR handle = LockBitMapTags(bm, ...);
+ /* ... use pixel pointer ... */
+ UnLockBitMap(handle);
+}
+else
+{
+ /* Native planar bitmap — use standard bitplane access */
+}
+
+/* DO: use friend bitmaps for compatibility */
+/* A "friend" bitmap ensures the same format as the screen's bitmap.
+ On native screens → planar in Chip RAM.
+ On RTG screens → chunky in VRAM.
+ The application doesn't need to know which. */
+```
+
+---
+
+## Troubleshooting
+
+| Symptom | Cause | Fix |
+|---|---|---|
+| Black screen on mode switch | SetSwitch not toggling pass-through correctly | Check VGA cable; verify DAC enable/disable |
+| Corrupted display | CRTC timing wrong | Verify HTotal/VTotal/sync values in SetGC |
+| Garbled colours | Wrong RGBFTYPE (BGR vs RGB swap) | Match format to VGA controller's native order |
+| Slow scrolling | No BlitRect acceleration | Implement hardware blit; check VRAM alignment |
+| Cursor flickers | SoftSpriteWA=TRUE | Implement HW cursor (SetSprite*) |
+| Guru on screen close | Driver doesn't handle CloseScreen cleanup | Free VRAM allocations in board cleanup |
+| Two monitors needed | No pass-through cable or scan doubler | Get Picasso IV or use pass-through adapter |
+| Icons disappear/corrupt | FBlit conflict with P96 | Remove FBlit; use Native driver instead |
+| Crash on Bob display | `NOBLITTER=NO` with P96 < 3.3.3 | Upgrade P96 or use `NOBLITTER=YES` |
+| Workbench slow despite card | Bitmap allocated in Chip RAM, not VRAM | Use `AllocBitMap` with friend bitmap from RTG screen |
+| Game crashes on RTG screen | Game writes directly to custom chip registers | Run game on native screen; don't force RTG |
+
---
## References
-- Picasso96 SDK (P96 developer documentation)
-- CyberGraphX SDK
-- Example: UAE RTG driver (`uaegfx.card` source)
+- Picasso96 SDK: [iComp P96 Wiki](https://wiki.icomp.de/wiki/P96) — `boardinfo.h`, example drivers
+- Aminet: `dev/misc/P96CardDevelop.lha` — official driver development kit
+- Aminet: `driver/moni/Native` — Thomas Richter's CPU blitter replacement driver
+- CyberGraphX SDK — CGX-specific board driver interface
+- UAE RTG source: `uaegfx.card` — reference virtual card implementation
+- Village Tronic Picasso IV documentation — scan doubler and video scaler internals
+- See also: [display_modes.md](../08_graphics/display_modes.md) — native display modes
+- See also: [blitter.md](../08_graphics/blitter.md) — native Blitter (not RTG blitter)
+- See also: [device_driver_basics.md](device_driver_basics.md) — general Amiga device driver framework
+- See also: [screens.md](../09_intuition/screens.md) — Intuition screen management
+
diff --git a/README.md b/README.md
index a2dc811..71fd70b 100644
--- a/README.md
+++ b/README.md
@@ -245,11 +245,12 @@ The Amiga's documentation was scattered across out-of-print manuals, Usenet post
| [vasm_vlink.md](13_toolchain/vasm_vlink.md) | vasm assembler, vlink linker |
| [gcc_amiga.md](13_toolchain/gcc_amiga.md) | m68k-amigaos-gcc (bebbo fork, Codeberg) |
| [sasc.md](13_toolchain/sasc.md) | SAS/C 6.x compiler |
+| [stormc.md](13_toolchain/stormc.md) | StormC native IDE and C/C++ compiler |
| [fd_files.md](13_toolchain/fd_files.md) | FD/SFD file format, Python parser |
| [pragmas.md](13_toolchain/pragmas.md) | Compiler pragmas, inline stubs |
| [ndk.md](13_toolchain/ndk.md) | NDK versions, contents, downloads |
| [makefiles.md](13_toolchain/makefiles.md) | Cross-compilation Makefile patterns |
-| [debugging.md](13_toolchain/debugging.md) | Enforcer, GDB remote, BareFoot |
+| [debugging.md](13_toolchain/debugging.md) | Enforcer, SnoopDOS, GDB remote, kprintf |
### 14 — References
| File | Topic |