mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-12 16:16:28 +00:00
docs: expand 7 Tier 3 articles to Deep quality
- gcc_amiga.md: 101→606 lines — pipeline, Docker, platform builds, flags, antipatterns, FAQ - trackdisk.md: 178→428 lines — MFM encoding, 16-command reference, antipatterns, FPGA impact - console.md: 244→470 lines — decision guide, TUI/progress cookbooks, antipatterns, pitfalls - layers.md: 224→739 lines — ClipRect engine, LVO API, backfill hooks, 4 antipatterns, optimization - text_fonts.md: 215→708 lines — ColorFont, Compugraphic outlines, 3 cookbooks, 4 antipatterns - windows.md: 370→778 lines — 5 antipatterns, decision guide, 3 cookbooks, modern analogies - menus.md: 378→695 lines — render chain diagram, 5 antipatterns, lifecycle cookbook, 6 FAQ
This commit is contained in:
parent
ab88118dc1
commit
9f5d9de1ed
8 changed files with 3129 additions and 209 deletions
|
|
@ -4,22 +4,32 @@
|
|||
|
||||
## Overview
|
||||
|
||||
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).
|
||||
AmigaOS renders all text through **bitmap fonts** — each glyph is a pre-drawn grid of pixels stored in a single monolithic bitmap strip. The `graphics.library` `Text()` function blits these glyph pixels to the screen via the Blitter, making text rendering essentially free in CPU terms. This was a deliberate design: a 7 MHz 68000 could not rasterize TrueType curves at interactive speeds, so the Amiga pre-rendered everything.
|
||||
|
||||
Two fonts live in ROM — **topaz 8** and **topaz 9** — available instantly at boot with no disk access. Every other font (helvetica, times, courier, garnet, sapphire, third-party) lives on disk under the `FONTS:` assign and must be loaded via [diskfont.library](../11_libraries/diskfont.md).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
flowchart TD
|
||||
subgraph "Font Sources"
|
||||
ROM["ROM Fonts<br/>(topaz 8, topaz 9)"]
|
||||
DISK["Disk Fonts<br/>(FONTS: directory)"]
|
||||
OUTLINE["Compugraphic Outlines<br/>(OS 2.0+)"]
|
||||
end
|
||||
|
||||
ROM -->|"OpenFont"| TF["struct TextFont"]
|
||||
DISK -->|"OpenDiskFont"| TF
|
||||
OUTLINE -->|"OpenDiskFont\n(auto-rasterized)"| TF
|
||||
|
||||
TF -->|"SetFont(rp, font)"| RP["RastPort"]
|
||||
RP -->|"Text(rp, str, len)"| BM["BitMap<br/>(rendered text)"]
|
||||
RP -->|"Text(rp, str, len)"| BM["BitMap\n(Blitter blits glyph pixels)"]
|
||||
|
||||
STYLE["Algorithmic Styles\n(Bold/Italic/Underline)"] -->|"SetSoftStyle"| RP
|
||||
COLOR["Color Fonts\n(OS 3.0+, up to 8 bitplanes)"] --> TF
|
||||
|
||||
style TF fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style BM fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style STYLE fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style COLOR fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -34,7 +44,7 @@ struct TextAttr {
|
|||
STRPTR ta_Name; /* font name, e.g. "topaz.font" */
|
||||
UWORD ta_YSize; /* desired height in pixels */
|
||||
UBYTE ta_Style; /* FSF_BOLD, FSF_ITALIC, FSF_UNDERLINED */
|
||||
UBYTE ta_Flags; /* FPF_ROMFONT, FPF_DISKFONT, etc. */
|
||||
UBYTE ta_Flags; /* FPF_ROMFONT, FPF_DISKFONT, FPF_PROPORTIONAL, etc. */
|
||||
};
|
||||
|
||||
/* Loaded font instance: */
|
||||
|
|
@ -42,7 +52,7 @@ struct TextFont {
|
|||
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;
|
||||
UBYTE tf_Flags; /* FPF_ROMFONT, FPF_DISKFONT, FPF_PROPORTIONAL, etc. */
|
||||
UWORD tf_XSize; /* nominal character width */
|
||||
UWORD tf_Baseline; /* pixels from top to baseline */
|
||||
UWORD tf_BoldSmear; /* extra pixels for algorithmic bold */
|
||||
|
|
@ -57,6 +67,19 @@ struct TextFont {
|
|||
};
|
||||
```
|
||||
|
||||
### Field Reference
|
||||
|
||||
| Field | Purpose | Important Notes |
|
||||
|-------|---------|----------------|
|
||||
| `tf_YSize` | Font height in pixels | Amiga "point size" = pixel height, not typographic points |
|
||||
| `tf_Baseline` | Distance from top to baseline | Position text here — not at the top of the cell |
|
||||
| `tf_BoldSmear` | Pixels to smear for algorithmic bold | Usually 1; affects rendered width |
|
||||
| `tf_LoChar` / `tf_HiChar` | Character range (typically 32–127 or 32–255) | Characters outside range render as default glyph |
|
||||
| `tf_CharData` | Pointer to the monolithic glyph bitmap strip | 1 bitplane for monochrome; multiple for ColorFont |
|
||||
| `tf_CharLoc` | Array of `{offset, width}` pairs, one per character | `ULONG` per char: high word = bit offset, low word = pixel width |
|
||||
| `tf_CharSpace` | Per-character horizontal advance | `NULL` means fixed-width (`tf_XSize` used for all) |
|
||||
| `tf_CharKern` | Per-character kerning offset | `NULL` means no kerning |
|
||||
|
||||
### Font Bitmap Layout
|
||||
|
||||
All characters are stored in a single bitmap strip. The `tf_CharLoc` table tells the renderer where each character starts:
|
||||
|
|
@ -77,7 +100,28 @@ tf_CharSpace[ch - tf_LoChar]:
|
|||
|
||||
---
|
||||
|
||||
## Opening Fonts
|
||||
## Opening and Closing Fonts
|
||||
|
||||
### Font Opening Decision Guide
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
NEED{"What font?"} -->|"topaz 8 or 9"| ROM["OpenFont()\nNo disk access needed"]
|
||||
NEED -->|"Any other font"| DISK["OpenDiskFont()\nLoads from FONTS:"]
|
||||
NEED -->|"User's screen font"| SCR["Use screen->RastPort.Font\nAlready open, no loading"]
|
||||
DISK --> FOUND{"Found on disk?"}
|
||||
FOUND -->|"Yes"| LOAD["Load and return TextFont*"]
|
||||
FOUND -->|"No exact match"| SCALE{"Can scale existing?"}
|
||||
SCALE -->|"Yes"| SCALED["Scale bitmap and return\nQuality may be poor"]
|
||||
SCALE -->|"No"| FAIL["Return NULL"]
|
||||
|
||||
style ROM fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style LOAD fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style SCALED fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style FAIL fill:#ffcdd2,stroke:#c62828,color:#333
|
||||
```
|
||||
|
||||
### Code Examples
|
||||
|
||||
```c
|
||||
/* ROM font (topaz — always available, no disk access): */
|
||||
|
|
@ -93,17 +137,36 @@ struct TextFont *font2 = OpenDiskFont(&ta2);
|
|||
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);
|
||||
/* Use the screen's current font (respects user preferences): */
|
||||
struct TextFont *scrFont = screen->RastPort.Font;
|
||||
SetFont(rp, scrFont);
|
||||
```
|
||||
|
||||
### Font Flags
|
||||
|
||||
| Flag | Meaning | Notes |
|
||||
|------|---------|-------|
|
||||
| `FPF_ROMFONT` | Font lives in ROM | Only topaz 8 and topaz 9 |
|
||||
| `FPF_DISKFONT` | Font loaded from disk | Requires diskfont.library |
|
||||
| `FPF_PROPORTIONAL` | Variable-width characters | `tf_CharSpace != NULL` |
|
||||
| `FPF_DESIGNED` | Explicitly designed at this size | Set by the font designer; if absent, font was auto-scaled |
|
||||
| `FPF_TALLDOT` | Designed for HiRes (640×200 NTSC non-interlaced) | Taller pixel aspect |
|
||||
| `FPF_WIDEDOT` | Designed for LoRes interlaced (320×400 NTSC) | Wider pixel aspect |
|
||||
| `FPF_REVPATH` | Right-to-left rendering | For Hebrew, Arabic |
|
||||
|
||||
---
|
||||
|
||||
## Rendering Text
|
||||
|
||||
### Draw Mode Effects on Text
|
||||
|
||||
| Draw Mode | Effect on Text | FgPen | BgPen |
|
||||
|-----------|---------------|-------|-------|
|
||||
| `JAM1` | Draw foreground only — transparent background | Used | Ignored |
|
||||
| `JAM2` | Draw foreground + solid background | Used | Used |
|
||||
| `COMPLEMENT` | XOR text with destination | Ignored | Ignored |
|
||||
| `INVERSVID` | Invert video (swap fg/bg roles) | Modifier | Modifier |
|
||||
|
||||
```c
|
||||
/* Position cursor then render: */
|
||||
Move(rp, 10, 20 + rp->Font->tf_Baseline); /* baseline-relative! */
|
||||
|
|
@ -126,12 +189,28 @@ TextExtent(rp, "Hello", 5, &te);
|
|||
```
|
||||
|
||||
> [!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.
|
||||
> `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. If you position at the top instead, descenders (g, j, p, q, y) will clip.
|
||||
|
||||
### Rendering API Reference
|
||||
|
||||
| Function | Library | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `Text()` | graphics | Render `count` characters at current position |
|
||||
| `TextLength()` | graphics | Return pixel width of a string (no rendering) |
|
||||
| `TextExtent()` | graphics | Return width, height, and bounding rect |
|
||||
| `TextFit()` | graphics | Max characters that fit in a given width |
|
||||
| `SetFont()` | graphics | Set the RastPort's current font |
|
||||
| `OpenFont()` | graphics | Open a font already in system memory |
|
||||
| `CloseFont()` | graphics | Release a font (decrement accessor count) |
|
||||
| `OpenDiskFont()` | diskfont | Load a font from disk (or system memory) |
|
||||
| `AvailFonts()` | diskfont | Enumerate all available fonts |
|
||||
|
||||
---
|
||||
|
||||
## Algorithmic Styles
|
||||
|
||||
When a font doesn't have a built-in bold/italic variant, the Amiga can generate these styles **algorithmically** at render time:
|
||||
|
||||
```c
|
||||
/* Style flags: */
|
||||
#define FSF_UNDERLINED 0x01
|
||||
|
|
@ -150,12 +229,123 @@ Text(rp, "Bold Italic Text", 16);
|
|||
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 |
|
||||
| Style | Method | Visual Effect | Performance Impact |
|
||||
|-------|--------|--------------|-------------------|
|
||||
| Bold | Smear right by `tf_BoldSmear` pixels | Characters become slightly wider | Negligible — extra blit per scanline |
|
||||
| Italic | Shear top scanlines right | Fixed-angle slant (\~12°) | Negligible — per-scanline offset |
|
||||
| Underline | Draw 1-pixel line at descender level | Line below baseline | Negligible — one extra line draw |
|
||||
| Extended | Widen each character by 50% | Extra-wide spacing | Moderate — affects spacing calculation |
|
||||
|
||||
> [!NOTE]
|
||||
> Algorithmic styles work on **any** bitmap font. The `AskSoftStyle()` function returns which styles are supported — always check before applying. All four basic styles are supported by the standard renderer.
|
||||
|
||||
---
|
||||
|
||||
## Color Fonts (OS 3.0+)
|
||||
|
||||
OS 3.0 extended bitmap fonts with **multi-bitplane color support**. Instead of a single bitplane (1-bit: foreground or background), color fonts store up to 8 bitplanes per glyph, enabling 256-color text:
|
||||
|
||||
```c
|
||||
/* graphics/text.h — NDK39 */
|
||||
struct ColorTextFont {
|
||||
struct TextFont ctf_TF; /* standard TextFont (must be first!) */
|
||||
UWORD ctf_Flags; /* CT_COLORFONT, CT_GREYFONT */
|
||||
UBYTE ctf_Depth; /* number of bitplanes (1–8) */
|
||||
UBYTE ctf_FgColor; /* default foreground pen */
|
||||
UBYTE ctf_Low; /* lowest color register used */
|
||||
UBYTE ctf_High; /* highest color register used */
|
||||
APTR ctf_PlanePick; /* plane selection for rendering */
|
||||
APTR ctf_PlaneOnOff; /* plane on/off masks */
|
||||
struct ColorFontColors *ctf_ColorTable;
|
||||
APTR ctf_CharData[8]; /* per-plane glyph data pointers */
|
||||
};
|
||||
```
|
||||
|
||||
```
|
||||
Traditional font: 1 bitplane → FgPen or BgPen only
|
||||
Color font: up to 8 bitplanes → 256 colors per glyph pixel
|
||||
|
||||
Per-plane glyph storage:
|
||||
┌─────────────────┐
|
||||
│ ctf_CharData[0] │ bitplane 0 (LSB)
|
||||
├─────────────────┤
|
||||
│ ctf_CharData[1] │ bitplane 1
|
||||
├─────────────────┤
|
||||
│ ... │ ...
|
||||
├─────────────────┤
|
||||
│ ctf_CharData[N] │ bitplane N
|
||||
└─────────────────┘
|
||||
Each plane is a separate bitmap strip, same layout as tf_CharData.
|
||||
ctf_ColorTable maps pen indices to screen palette entries.
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Color fonts require **Chip RAM** for all glyph data planes. A 24-pixel color font at 8 bitplanes uses **8×** the memory of a monochrome bitmap font. On a 512 KB Chip RAM system, loading multiple color fonts can exhaust memory fast.
|
||||
|
||||
Color fonts render automatically with `Text()` — the renderer detects `FPF_COLORFONT` in `tf_Flags` and uses the multi-plane blit path. No special code is needed.
|
||||
|
||||
---
|
||||
|
||||
## Compugraphic Outline Fonts (OS 2.0+)
|
||||
|
||||
OS 2.0 added support for **AGFA Compugraphic** outline fonts via `diskfont.library`. These store mathematical curve descriptions instead of pre-drawn pixels. From the programmer's perspective, they work identically to bitmap fonts — `OpenDiskFont()` handles rasterization internally:
|
||||
|
||||
```c
|
||||
/* Same API — diskfont.library auto-detects outline vs bitmap: */
|
||||
struct TextAttr ta = {"CGTriumvirate.font", 36, 0, 0};
|
||||
struct TextFont *outlineFont = OpenDiskFont(&ta);
|
||||
/* If no 36-pixel bitmap exists but a CG outline does,
|
||||
diskfont.library rasterizes it to a bitmap automatically */
|
||||
```
|
||||
|
||||
| Aspect | Bitmap Font | Compugraphic Outline |
|
||||
|--------|-------------|---------------------|
|
||||
| Source | Pre-drawn pixels per size | Mathematical curves |
|
||||
| Scaling | Distorts at non-designed sizes | Clean at any size |
|
||||
| File format | `.font` + numbered descriptor files | `.font` with outline data |
|
||||
| API difference | None — same `OpenDiskFont()` | None — transparent to application |
|
||||
| Speed | Instant (blit pixels) | Slower (first rasterize, then blit) |
|
||||
| Availability | OS 1.0+ | OS 2.0+ (requires `diskfont.library` V36+) |
|
||||
|
||||
> [!NOTE]
|
||||
> Compugraphic outlines are **not** TrueType or PostScript. They are AGFA's proprietary curve format. The Amiga never gained native TrueType support; third-party libraries (like TrueDot) filled this gap.
|
||||
|
||||
---
|
||||
|
||||
## Font Scaling and Aspect Ratio
|
||||
|
||||
When `OpenDiskFont()` can't find an exact size match, it scales an existing bitmap. This produces usable but not beautiful results:
|
||||
|
||||
```c
|
||||
/* Request a size that doesn't exist on disk: */
|
||||
struct TextAttr ta = {"topaz.font", 15, 0, 0};
|
||||
/* diskfont.library scales topaz 8 or topaz 9 to 15 pixels */
|
||||
/* Result: usable but slightly blurry or jagged */
|
||||
struct TextFont *scaled = OpenDiskFont(&ta);
|
||||
```
|
||||
|
||||
The `FPF_DESIGNED` flag in `tf_Flags` indicates whether the font was explicitly designed at this size (clear) or auto-scaled (set). Always check:
|
||||
|
||||
```c
|
||||
if (font->tf_Flags & FPF_DESIGNED) {
|
||||
/* This size was designed — crisp rendering */
|
||||
} else {
|
||||
/* Auto-scaled — may look rough at large sizes */
|
||||
}
|
||||
```
|
||||
|
||||
### Aspect Ratio Mismatch
|
||||
|
||||
Fonts designed for one display mode may look wrong on another:
|
||||
|
||||
| Source Mode | Target Mode | Visual Effect |
|
||||
|-------------|-------------|--------------|
|
||||
| LoRes 320×256 | HiRes 640×256 | Font appears half-width (compressed) |
|
||||
| HiRes 640×256 | LoRes 320×256 | Font appears double-width (stretched) |
|
||||
| Non-interlaced | Interlaced | Font appears half-height (thin) |
|
||||
| Interlaced | Non-interlaced | Font appears double-height (fat) |
|
||||
|
||||
The `FPF_TALLDOT` and `FPF_WIDEDOT` flags indicate which pixel aspect the font was designed for. Check these when rendering across different screen modes.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -189,27 +379,330 @@ FreeMem(afh, bufSize);
|
|||
|
||||
---
|
||||
|
||||
## Font Preferences
|
||||
## Practical Cookbooks
|
||||
|
||||
The system font can be changed via Preferences. Applications should respect the user's choice:
|
||||
### Cookbook: Centered Title Bar
|
||||
|
||||
```c
|
||||
/* Get the current screen font (user's preference): */
|
||||
struct TextAttr *screenFont;
|
||||
struct Preferences prefs;
|
||||
GetPrefs(&prefs, sizeof(prefs));
|
||||
/* prefs contains font info */
|
||||
void DrawCenteredTitle(struct RastPort *rp, const char *title,
|
||||
UWORD width, UWORD y)
|
||||
{
|
||||
struct TextFont *font = rp->Font;
|
||||
UWORD textW = TextLength(rp, title, strlen(title));
|
||||
WORD x = (width - textW) / 2;
|
||||
|
||||
/* Better: use the screen's font directly: */
|
||||
struct TextFont *scrFont = screen->RastPort.Font;
|
||||
SetFont(myRastPort, scrFont);
|
||||
SetDrMd(rp, JAM1);
|
||||
SetAPen(rp, 1); /* foreground */
|
||||
Move(rp, x, y + font->tf_Baseline);
|
||||
Text(rp, title, strlen(title));
|
||||
}
|
||||
```
|
||||
|
||||
### Cookbook: Multi-Font Label + Value
|
||||
|
||||
```c
|
||||
void DrawLabelValue(struct RastPort *rp,
|
||||
struct TextFont *labelFont,
|
||||
struct TextFont *valueFont,
|
||||
const char *label, const char *value,
|
||||
UWORD x, UWORD y)
|
||||
{
|
||||
/* Draw label in small font */
|
||||
SetFont(rp, labelFont);
|
||||
SetAPen(rp, 2);
|
||||
SetDrMd(rp, JAM1);
|
||||
Move(rp, x, y + labelFont->tf_Baseline);
|
||||
Text(rp, label, strlen(label));
|
||||
|
||||
/* Draw value in large font, right after label */
|
||||
UWORD labelW = TextLength(rp, label, strlen(label));
|
||||
SetFont(rp, valueFont);
|
||||
SetAPen(rp, 1);
|
||||
Move(rp, x + labelW + 4, y + valueFont->tf_Baseline);
|
||||
Text(rp, value, strlen(value));
|
||||
}
|
||||
```
|
||||
|
||||
### Cookbook: Word-Wrap Text Block
|
||||
|
||||
```c
|
||||
/* Returns the number of lines rendered */
|
||||
UWORD DrawWrappedText(struct RastPort *rp, const char *text,
|
||||
UWORD maxWidth, UWORD startX, UWORD startY)
|
||||
{
|
||||
struct TextFont *font = rp->Font;
|
||||
UWORD y = startY;
|
||||
UWORD lineH = font->tf_YSize + 1; /* +1 for line spacing */
|
||||
const char *lineStart = text;
|
||||
const char *wordStart = text;
|
||||
const char *bestBreak = text;
|
||||
|
||||
while (*wordStart)
|
||||
{
|
||||
/* Skip to end of word */
|
||||
const char *wordEnd = wordStart;
|
||||
while (*wordEnd && *wordEnd != ' ' && *wordEnd != '\n') wordEnd++;
|
||||
|
||||
/* Measure from lineStart to wordEnd */
|
||||
UWORD w = TextLength(rp, lineStart, wordEnd - lineStart);
|
||||
|
||||
if (w > maxWidth && bestBreak > lineStart)
|
||||
{
|
||||
/* Break at the last good position */
|
||||
Move(rp, startX, y + font->tf_Baseline);
|
||||
Text(rp, lineStart, bestBreak - lineStart);
|
||||
y += lineH;
|
||||
lineStart = bestBreak + 1; /* skip the space */
|
||||
bestBreak = lineStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
bestBreak = wordEnd;
|
||||
}
|
||||
|
||||
if (*wordEnd == '\n')
|
||||
{
|
||||
Move(rp, startX, y + font->tf_Baseline);
|
||||
Text(rp, lineStart, wordEnd - lineStart);
|
||||
y += lineH;
|
||||
lineStart = wordEnd + 1;
|
||||
bestBreak = lineStart;
|
||||
wordEnd++;
|
||||
}
|
||||
|
||||
wordStart = wordEnd;
|
||||
while (*wordStart == ' ') wordStart++;
|
||||
}
|
||||
|
||||
/* Final line */
|
||||
if (lineStart < wordStart)
|
||||
{
|
||||
Move(rp, startX, y + font->tf_Baseline);
|
||||
Text(rp, lineStart, strlen(lineStart));
|
||||
y += lineH;
|
||||
}
|
||||
|
||||
return (y - startY) / lineH;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Historical Context & Modern Analogies
|
||||
|
||||
### Competitive Landscape
|
||||
|
||||
| Platform | Font System | Scalable? | Color Fonts? | Max Glyph Quality |
|
||||
|----------|-------------|-----------|-------------|-------------------|
|
||||
| **AmigaOS 1.x** | Bitmap (graphics.library) | No | No | Fixed-size pixel glyphs |
|
||||
| **AmigaOS 2.0+** | Bitmap + Compugraphic outlines | Yes (outlines) | No | Scalable via curves |
|
||||
| **AmigaOS 3.0+** | Bitmap + outlines + ColorFont | Yes | Yes (up to 8 planes) | Multi-color pixel glyphs |
|
||||
| **Mac System 1–6** | Bitmap "suitcase" + FOND resources | No | No | 72 DPI assumption |
|
||||
| **Mac System 7 (1991)** | TrueType + bitmap fallbacks | Yes | No | Full curve rasterization |
|
||||
| **Windows 3.0** | Bitmap `.FON` | No | No | System fixed-width |
|
||||
| **Windows 3.1 (1992)** | TrueType + bitmap | Yes | No | WYSIWYG via TrueType |
|
||||
| **Atari ST TOS** | 8×8 bitmap, GDOS optional | No (SpeedoGDOS in '91) | No | Minimal system font |
|
||||
|
||||
The Amiga was **ahead of Mac and Windows in color fonts** (OS 3.0, 1992) but **behind in scalable font technology** — Compugraphic outlines were less capable than Apple's TrueType.
|
||||
|
||||
### Modern Analogies
|
||||
|
||||
| Amiga Concept | Modern Equivalent | Notes |
|
||||
|--------------|-------------------|-------|
|
||||
| `TextFont` / `TextAttr` | `CTFont` (macOS) / `IDWriteFont` (Windows) / `PangoFontDescription` | Font description + loaded instance |
|
||||
| `SetFont()` | `CGContextSetFont()` / `SelectObject(hDC, hFont)` | Bind font to drawing context |
|
||||
| `Text()` | `CTLineDraw()` / `DrawText()` / `pango_layout_show()` | Render string at current position |
|
||||
| `TextLength()` | `CTLineGetTypographicBounds()` / `GetTextExtentPoint32()` | Measure without rendering |
|
||||
| `tf_CharData` bitmap strip | Glyph atlas texture (game engines) | Same concept: all glyphs in one bitmap |
|
||||
| `tf_CharLoc` table | Font atlas lookup table (UV coordinates) | Same concept: offset + width per glyph |
|
||||
| Algorithmic bold | `NSFontManager.convertWeight()` | Amiga smears pixels; modern re-strokes curves |
|
||||
| `AvailFonts()` | `NSFontManager.availableFonts` / `IDWriteFontCollection` | Enumerate installed fonts |
|
||||
| `FONTS:` assign | `/usr/share/fonts` / `C:\Windows\Fonts` | System font directory |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use `OpenDiskFont()` over `OpenFont()`** — it loads from disk if needed; `OpenFont()` only finds already-loaded fonts
|
||||
2. **Check the return value** — `OpenDiskFont()` returns `NULL` if the font doesn't exist; drawing with a NULL font crashes
|
||||
3. **Restore the original font** if you borrowed a shared RastPort (e.g., window's RPort)
|
||||
4. **Position at the baseline**, not the top — use `font->tf_Baseline` offset
|
||||
5. **Use `TextLength()` before `Text()`** for alignment and clipping
|
||||
6. **Respect user font preferences** — use `screen->RastPort.Font` as the default
|
||||
7. **Check `FPF_DESIGNED`** before relying on auto-scaled font quality
|
||||
8. **Open font, set font, draw, close font** — don't hold fonts open longer than needed
|
||||
9. **Use `JAM2` with a matching `BgPen`** for text that must be readable over any background
|
||||
10. **Don't assume ASCII 32–127** — check `tf_LoChar`/`tf_HiChar` before indexing
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Baseline Blind Spot" — Positioning at the Top Instead of Baseline
|
||||
|
||||
```c
|
||||
/* BAD: Positioning at the top of the character cell —
|
||||
descenders on g, j, p, q, y clip into the next line */
|
||||
Move(rp, 10, 50); /* y=50 is the TOP of the cell */
|
||||
Text(rp, "jumpy", 5);
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Position at the baseline */
|
||||
Move(rp, 10, 50 + font->tf_Baseline);
|
||||
Text(rp, "jumpy", 5);
|
||||
```
|
||||
|
||||
### "The Font Leak" — Forgetting CloseFont
|
||||
|
||||
```c
|
||||
/* BAD: OpenDiskFont increments the accessor count.
|
||||
Forgetting CloseFont leaks the font in memory forever. */
|
||||
struct TextFont *font = OpenDiskFont(&ta);
|
||||
SetFont(rp, font);
|
||||
/* ... draw stuff ... */
|
||||
/* Missing CloseFont()! Font stays loaded. */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Always pair OpenDiskFont/OpenFont with CloseFont */
|
||||
struct TextFont *font = OpenDiskFont(&ta);
|
||||
if (font) {
|
||||
struct TextFont *oldFont = rp->Font;
|
||||
SetFont(rp, font);
|
||||
/* ... draw stuff ... */
|
||||
SetFont(rp, oldFont); /* restore */
|
||||
CloseFont(font);
|
||||
}
|
||||
```
|
||||
|
||||
### "The Scale Surprise" — Assuming Auto-Scaled Fonts Look Good
|
||||
|
||||
```c
|
||||
/* BAD: Requesting a size that doesn't exist, assuming clean rendering */
|
||||
struct TextAttr ta = {"topaz.font", 48, 0, 0};
|
||||
struct TextFont *huge = OpenDiskFont(&ta);
|
||||
/* topaz has no 48-pixel design — you get a scaled-up 8-pixel bitmap.
|
||||
It looks like pixelated garbage. */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Check FPF_DESIGNED and warn/fallback */
|
||||
struct TextFont *font = OpenDiskFont(&ta);
|
||||
if (font && !(font->tf_Flags & FPF_DESIGNED)) {
|
||||
/* Font was auto-scaled — consider using a different size
|
||||
or an outline font (Compugraphic) for better quality */
|
||||
}
|
||||
```
|
||||
|
||||
### "The Proportional Trap" — Assuming Fixed Width
|
||||
|
||||
```c
|
||||
/* BAD: Calculating text width by multiplying character count */
|
||||
UWORD badWidth = strlen(title) * font->tf_XSize;
|
||||
/* WRONG for proportional fonts (helvetica, times, etc.) */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Always measure with TextLength */
|
||||
UWORD goodWidth = TextLength(rp, title, strlen(title));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. NULL Font After Failed OpenDiskFont
|
||||
|
||||
**Symptom:** System crash or garbage rendering after `SetFont(rp, NULL)`.
|
||||
|
||||
**Cause:** `OpenDiskFont()` returns `NULL` when the font doesn't exist. Passing `NULL` to `SetFont()` causes undefined behavior.
|
||||
|
||||
**Fix:** Always check the return value:
|
||||
```c
|
||||
struct TextFont *font = OpenDiskFont(&ta);
|
||||
if (!font) {
|
||||
/* Fallback to a ROM font */
|
||||
struct TextAttr fallback = {"topaz.font", 8, 0, FPF_ROMFONT};
|
||||
font = OpenFont(&fallback);
|
||||
}
|
||||
if (font) SetFont(rp, font);
|
||||
```
|
||||
|
||||
### 2. Color Font Memory Exhaustion
|
||||
|
||||
**Symptom:** System runs out of Chip RAM after loading several large color fonts.
|
||||
|
||||
**Cause:** Each color font bitplane is a full copy of the glyph bitmap. An 8-plane color font at 24 pixels with 256 characters uses roughly `24 × 256/8 × 8 = ~49 KB` of Chip RAM per font. Multiple fonts multiply this.
|
||||
|
||||
**Fix:** Load color fonts only when needed and close them immediately after use. Prefer monochrome fonts for large text blocks.
|
||||
|
||||
### 3. Aspect Ratio Distortion Across Screen Modes
|
||||
|
||||
**Symptom:** Text looks squished or stretched when opening a window on a different resolution screen than the font was designed for.
|
||||
|
||||
**Cause:** `FPF_TALLDOT`/`FPF_WIDEDOT` flags indicate the font's native pixel aspect. The renderer does not auto-compensate.
|
||||
|
||||
**Fix:** Check the screen mode's aspect ratio and choose a font designed for that mode, or accept the distortion.
|
||||
|
||||
### 4. Font Name Case Sensitivity
|
||||
|
||||
**Symptom:** `OpenDiskFont()` fails even though the font exists on disk.
|
||||
|
||||
**Cause:** Font names are case-sensitive on the Amiga filesystem. `"Topaz.font"` ≠ `"topaz.font"`.
|
||||
|
||||
**Fix:** Always use lowercase font names (the standard convention):
|
||||
```c
|
||||
/* WRONG: */
|
||||
struct TextAttr ta = {"Topaz.font", 8, 0, 0}; /* uppercase T */
|
||||
|
||||
/* CORRECT: */
|
||||
struct TextAttr ta = {"topaz.font", 8, 0, 0}; /* lowercase */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I render rotated text?**
|
||||
A: No — the `Text()` function only renders horizontally. Rotated text requires custom glyph-by-glyph blitting with coordinate transforms. Compugraphic outlines in OS 2.0+ were planned to support rotation but the API was never exposed.
|
||||
|
||||
**Q: What is `tf_BoldSmear`?**
|
||||
A: The number of pixels the renderer shifts and ORs the glyph rightward to produce algorithmic bold. Usually 1. The smear makes characters slightly wider — account for this in width calculations.
|
||||
|
||||
**Q: Can I use TrueType fonts?**
|
||||
A: Not natively. AmigaOS uses bitmap and Compugraphic outlines. Third-party solutions like TrueDot, TypeManager, and AmigaOS 4's bullet.library add TrueType support. For retro development, stick with bitmap fonts.
|
||||
|
||||
**Q: What happens if I call `Text()` with characters outside `tf_LoChar`–`tf_HiChar`?**
|
||||
A: The renderer substitutes a default glyph (usually the character for `tf_LoChar`). It does not crash, but you get wrong characters.
|
||||
|
||||
**Q: How do I change the system font for all new windows?**
|
||||
A: Use the Font Preferences editor (Prefs → Font). The user's selection is stored in `ENV:sys/font.prefs`. Applications should read `screen->RastPort.Font` rather than hardcoding a specific font.
|
||||
|
||||
**Q: What is the difference between `OpenFont()` and `OpenDiskFont()`?**
|
||||
A: `OpenFont()` (graphics.library) only finds fonts already loaded in memory. `OpenDiskFont()` (diskfont.library) can also load from disk and scale. Always prefer `OpenDiskFont()`.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- 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
|
||||
### NDK Headers
|
||||
|
||||
- `graphics/text.h` — `TextFont`, `TextAttr`, `TextExtent`, style/flag constants
|
||||
- `graphics/rastport.h` — RastPort font-related fields
|
||||
- `graphics/gfx.h` — `ColorTextFont`, `ColorFontColors`
|
||||
- `libraries/diskfont.h` — `DiskFontHeader`, `AvailFonts`
|
||||
|
||||
### Autodocs
|
||||
|
||||
- ADCD 2.1: graphics.library text functions
|
||||
- ADCD 2.1: diskfont.library font loading
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [diskfont.md](../11_libraries/diskfont.md) — font file format, disk loading pipeline, ColorFont memory layout
|
||||
- [rastport.md](rastport.md) — RastPort text rendering context and drawing modes
|
||||
- [bitmap.md](bitmap.md) — BitMap structure (font glyph data is a BitMap)
|
||||
- [blitter.md](blitter.md) — hardware that performs the actual glyph blitting
|
||||
- [console.md](../10_devices/console.md) — console text rendering uses these fonts
|
||||
- [utility.md](../11_libraries/utility.md) — `Hook` structure (used by font enumeration)
|
||||
|
|
|
|||
|
|
@ -367,12 +367,329 @@ Intuition automatically intercepts Right-Amiga+key combinations matching menu sh
|
|||
6. **Disable items** with `OffMenu()` when they don't apply — don't hide them
|
||||
7. **Clean up in order**: `ClearMenuStrip()` → `CloseWindow()` → `FreeMenus()` → `FreeVisualInfo()`
|
||||
8. **Use `GTMN_NewLookMenus, TRUE`** for the modern 3D menu appearance (OS 3.0+)
|
||||
9. **Keep menus shallow** — max 3 levels (menu → item → sub-item); deeper nesting confuses users
|
||||
10. **Disable rather than remove** — users learn where commands live; removing them breaks spatial memory
|
||||
11. **Use keyboard shortcuts for the top 10 commands** — the rest don't need them
|
||||
12. **Never modify menu structs while attached** — always `ClearMenuStrip()` first
|
||||
|
||||
---
|
||||
|
||||
## Menu Render Chain
|
||||
|
||||
When the user presses the right mouse button, Intuition renders the menu system through a specific pipeline:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Intuition
|
||||
participant MenuStrip
|
||||
participant RastPort
|
||||
participant Layer
|
||||
|
||||
Note over User,Layer: Right mouse button pressed
|
||||
User->>Intuition: RMB down event
|
||||
Intuition->>Intuition: Freeze window input (menu mode)
|
||||
Intuition->>Layer: Lock screen layers
|
||||
Intuition->>RastPort: Render menu strip across screen top
|
||||
Note over RastPort: All menu titles rendered in title bar area
|
||||
|
||||
User->>Intuition: Mouse moves to "Edit" title
|
||||
Intuition->>RastPort: Highlight "Edit" title
|
||||
Intuition->>RastPort: Render dropdown box with Edit items
|
||||
Note over RastPort: Items drawn with checkmarks, shortcuts, separators
|
||||
|
||||
User->>Intuition: Mouse moves to "Copy" item
|
||||
Intuition->>RastPort: Highlight "Copy" (invert/complement)
|
||||
|
||||
Note over User,Layer: Right mouse button released
|
||||
User->>Intuition: RMB up event
|
||||
Intuition->>RastPort: Erase all menu rendering
|
||||
Intuition->>Layer: Unlock screen layers
|
||||
Intuition->>Intuition: Send IDCMP_MENUPICK to window
|
||||
Note over Intuition: Code = packed menu/item/sub numbers
|
||||
```
|
||||
|
||||
### Render Details
|
||||
|
||||
| Phase | What Intuition Does | Memory Impact |
|
||||
|-------|--------------------|---------------|
|
||||
| **Strip rendering** | Draws all menu titles across the screen title bar | Backing store saved for Smart Refresh windows obscured by strip |
|
||||
| **Dropdown** | Draws a filled rectangle with items inside | Obscures part of the window beneath |
|
||||
| **Highlight** | `HIGHCOMP` = XOR complement, `HIGHBOX` = draw rectangle | Non-destructive — can be undone by same operation |
|
||||
| **Cleanup** | Restores all obscured content from backing store | Smart refresh handles this automatically |
|
||||
|
||||
> [!NOTE]
|
||||
> While the menu is active, Intuition **freezes input** to the window. No `IDCMP_RAWKEY`, `IDCMP_MOUSEBUTTONS`, or gadget events are delivered until the menu closes. The application cannot draw during menu mode because Intuition holds the layer lock.
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Multi-Select Ghost" — Ignoring NextSelect
|
||||
|
||||
```c
|
||||
/* BAD: Only processes the first item selected.
|
||||
If the user multi-selects (hold RMB, click several items),
|
||||
only the first one is handled — the rest silently vanish. */
|
||||
case IDCMP_MENUPICK:
|
||||
{
|
||||
struct MenuItem *item = ItemAddress(menuStrip, code);
|
||||
HandleCommand(GTMENUITEM_USERDATA(item));
|
||||
break; /* BUG: drops all subsequent selections */
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Walk the NextSelect chain */
|
||||
case IDCMP_MENUPICK:
|
||||
{
|
||||
UWORD menuCode = code;
|
||||
while (menuCode != MENUNULL)
|
||||
{
|
||||
struct MenuItem *item = ItemAddress(menuStrip, menuCode);
|
||||
HandleCommand(GTMENUITEM_USERDATA(item));
|
||||
menuCode = item->NextSelect;
|
||||
}
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### "The Stale Menu" — Modifying Items While Attached
|
||||
|
||||
```c
|
||||
/* BAD: Modifying MenuItem flags while the menu strip is active.
|
||||
If the user opens the menu at exactly this moment, Intuition
|
||||
reads partially-modified state — corrupted rendering or crash. */
|
||||
struct MenuItem *item = ItemAddress(menuStrip, FULLMENUNUM(0, 2, NOSUB));
|
||||
item->Flags |= CHECKED; /* RACE CONDITION */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Detach, modify, reattach */
|
||||
ClearMenuStrip(win);
|
||||
struct MenuItem *item = ItemAddress(menuStrip, FULLMENUNUM(0, 2, NOSUB));
|
||||
item->Flags |= CHECKED;
|
||||
SetMenuStrip(win, menuStrip);
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> For simple enable/disable or check/uncheck, use `OnMenu()`/`OffMenu()` instead — these are safe to call while the menu is attached.
|
||||
|
||||
### "The Cleanup Reversal" — Wrong Free Order
|
||||
|
||||
```c
|
||||
/* BAD: Freeing menus before clearing from window — Intuition
|
||||
may access freed memory on the next menu open attempt. */
|
||||
FreeMenus(menuStrip); /* freed while still attached */
|
||||
ClearMenuStrip(win); /* too late — dangling pointer */
|
||||
CloseWindow(win);
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: ClearMenuStrip → CloseWindow → FreeMenus */
|
||||
ClearMenuStrip(win);
|
||||
CloseWindow(win);
|
||||
FreeMenus(menuStrip); /* now safe — no window references it */
|
||||
FreeVisualInfo(vi);
|
||||
UnlockPubScreen(NULL, scr);
|
||||
```
|
||||
|
||||
### "The Shortcut Collision" — Duplicate CommKey Letters
|
||||
|
||||
```c
|
||||
/* BAD: Two items in the same menu share shortcut "S".
|
||||
Only the first match fires — "Save As" is unreachable via keyboard. */
|
||||
{ NM_ITEM, "Save", "S", 0, 0, (APTR)MENU_SAVE },
|
||||
{ NM_ITEM, "Save As...", "S", 0, 0, (APTR)MENU_SAVEAS },
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Each shortcut must be unique within its menu */
|
||||
{ NM_ITEM, "Save", "S", 0, 0, (APTR)MENU_SAVE },
|
||||
{ NM_ITEM, "Save As...", "A", 0, 0, (APTR)MENU_SAVEAS },
|
||||
```
|
||||
|
||||
### "The Phantom VisualInfo" — Wrong Screen
|
||||
|
||||
```c
|
||||
/* BAD: Getting VisualInfo from one screen, opening window on another.
|
||||
LayoutMenus calculates positions for the wrong font/resolution. */
|
||||
struct Screen *scr1 = LockPubScreen(NULL);
|
||||
APTR vi = GetVisualInfo(scr1, TAG_DONE);
|
||||
/* ... later, open window on a DIFFERENT screen ... */
|
||||
struct Screen *scr2 = /* custom screen */;
|
||||
struct Window *win = OpenWindowTags(NULL, WA_CustomScreen, scr2, TAG_DONE);
|
||||
SetMenuStrip(win, menuStrip); /* WRONG VI — menu positions are for scr1 */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Get VisualInfo from the SAME screen the window uses */
|
||||
struct Screen *scr = /* the screen your window is on */;
|
||||
APTR vi = GetVisualInfo(scr, TAG_DONE);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Cookbook: Complete Menu Lifecycle
|
||||
|
||||
```c
|
||||
#include <proto/exec.h>
|
||||
#include <proto/intuition.h>
|
||||
#include <proto/gadtools.h>
|
||||
#include <libraries/gadtools.h>
|
||||
|
||||
enum { MENU_QUIT = 1, MENU_NEW, MENU_OPEN, MENU_CUT, MENU_COPY, MENU_PASTE };
|
||||
|
||||
struct NewMenu menuDef[] = {
|
||||
{ NM_TITLE, "Project", NULL, 0, 0, NULL },
|
||||
{ NM_ITEM, "New", "N", 0, 0, (APTR)MENU_NEW },
|
||||
{ NM_ITEM, "Open...", "O", 0, 0, (APTR)MENU_OPEN },
|
||||
{ NM_ITEM, NM_BARLABEL, NULL, 0, 0, NULL },
|
||||
{ NM_ITEM, "Quit", "Q", 0, 0, (APTR)MENU_QUIT },
|
||||
{ NM_TITLE, "Edit", NULL, 0, 0, NULL },
|
||||
{ NM_ITEM, "Cut", "X", 0, 0, (APTR)MENU_CUT },
|
||||
{ NM_ITEM, "Copy", "C", 0, 0, (APTR)MENU_COPY },
|
||||
{ NM_ITEM, "Paste", "V", 0, 0, (APTR)MENU_PASTE },
|
||||
{ NM_END, NULL, NULL, 0, 0, NULL }
|
||||
};
|
||||
|
||||
struct Menu *CreateAndAttachMenus(struct Window *win)
|
||||
{
|
||||
struct Screen *scr = win->WScreen;
|
||||
APTR vi = GetVisualInfo(scr, TAG_DONE);
|
||||
if (!vi) return NULL;
|
||||
|
||||
struct Menu *menuStrip = CreateMenus(menuDef, TAG_DONE);
|
||||
if (!menuStrip) { FreeVisualInfo(vi); return NULL; }
|
||||
|
||||
if (!LayoutMenus(menuStrip, vi,
|
||||
GTMN_NewLookMenus, TRUE,
|
||||
TAG_DONE))
|
||||
{
|
||||
FreeMenus(menuStrip);
|
||||
FreeVisualInfo(vi);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SetMenuStrip(win, menuStrip);
|
||||
FreeVisualInfo(vi); /* safe to free after LayoutMenus + SetMenuStrip */
|
||||
return menuStrip;
|
||||
}
|
||||
|
||||
void ProcessMenuPick(struct Window *win, struct Menu *menuStrip, UWORD code)
|
||||
{
|
||||
while (code != MENUNULL)
|
||||
{
|
||||
struct MenuItem *item = ItemAddress(menuStrip, code);
|
||||
ULONG cmd = (ULONG)GTMENUITEM_USERDATA(item);
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case MENU_NEW: NewDocument(); break;
|
||||
case MENU_OPEN: OpenDocument(); break;
|
||||
case MENU_CUT: CutSelection(); break;
|
||||
case MENU_COPY: CopySelection(); break;
|
||||
case MENU_PASTE: PasteClipboard(); break;
|
||||
case MENU_QUIT: /* signal main loop to exit */ break;
|
||||
}
|
||||
code = item->NextSelect;
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupMenus(struct Window *win, struct Menu *menuStrip)
|
||||
{
|
||||
ClearMenuStrip(win);
|
||||
FreeMenus(menuStrip);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Historical Context & Modern Analogies
|
||||
|
||||
### Competitive Landscape
|
||||
|
||||
| Platform | Menu System | Keyboard Shortcuts | Dynamic Modification | Sub-Menus | Notes |
|
||||
|----------|------------|-------------------|---------------------|-----------|-------|
|
||||
| **AmigaOS Intuition** | Right-click pull-down | Right-Amiga+key | `OnMenu()`/`OffMenu()`, `SetMenuStrip()` | 1 level only | Multi-select is unique — no other platform supports it |
|
||||
| **Mac OS (Classic)** | Click-and-hold in menu bar | Command+key | `EnableMenuItem()`/`DisableMenuItem()` | Cascading | First platform with standard menu bar |
|
||||
| **Windows 3.x** | Click on menu bar | Alt+letter, Ctrl+key | `EnableMenuItem()` | Cascading | Menu bar in each window |
|
||||
| **Atari ST GEM** | Click on menu bar | Alt+key | Limited | No | Very basic — no checkmarks, no radio groups |
|
||||
| **X11/Motif** | Click on menu bar | Alt+key (motif) | `XtSetSensitive()` | Cascading | Server-side rendering |
|
||||
|
||||
The Amiga's **right-click-to-activate** menu system was unique. Every other platform used a left-click menu bar. The right-click approach meant menus never interfered with left-click window operations, but it confused users coming from Mac/Windows.
|
||||
|
||||
**Multi-select** (holding right button while clicking multiple items) was an Amiga-exclusive feature. No other platform supported selecting Cut, Copy, and Paste in a single menu interaction.
|
||||
|
||||
### Modern Analogies
|
||||
|
||||
| Amiga Concept | Modern Equivalent | Notes |
|
||||
|--------------|-------------------|-------|
|
||||
| Right-click menu activation | Right-click context menu (all platforms) | Amiga: right-click = global menu bar; Modern: right-click = context menu |
|
||||
| `struct NewMenu[]` array | XUL `<menubar>` / GTK `GtkUIManager` / Qt `QMenuBar` | Declarative menu definition |
|
||||
| `CreateMenus()` + `LayoutMenus()` | `gtk_menu_bar_new_from_model()` / Qt Designer | Build + layout in one step |
|
||||
| `SetMenuStrip()` | `gtk_application_set_menubar()` / macOS `NSMenu` | Attach to window |
|
||||
| `GTMENUITEM_USERDATA()` | `gtk_buildable_get_name()` / Qt `QObject::property()` | User data for dispatch |
|
||||
| `IDCMP_MENUPICK` | `activate` signal (GTK) / `triggered` (Qt) / `@IBAction` (macOS) | Selection event |
|
||||
| `MENUNUM/ITEMNUM/SUBNUM` | None — modern APIs pass the menu item object directly | Amiga packs 3 levels into one UWORD |
|
||||
| `NextSelect` chain | None — modern menus fire one event per selection | Amiga's multi-select is unique |
|
||||
| `OnMenu()`/`OffMenu()` | `gtk_widget_set_sensitive()` / `NSMenuItem.enabled` | Enable/disable |
|
||||
| `NM_BARLABEL` separator | GTK `gtk_separator_menu_item_new()` / Qt `addSeparator()` | Visual divider |
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Application | Menu Structure | Notable Features |
|
||||
|-------------|---------------|-----------------|
|
||||
| **Workbench** | System (disk operations), Special (snapshot, cleanup) | Backdrop menu — menus available from Workbench screen |
|
||||
| **Word processors (Final Writer, WordWorth)** | Project, Edit, Format, Tools, Print | Mutual exclusion for font size, toggle for bold/italic |
|
||||
| **Image editors (Deluxe Paint)** | Picture, Brush, Mode,Prefs | Dynamic menus — mode options change with tool |
|
||||
| **Terminal emulators** | Connect, Edit, Settings | Menu-only commands — no toolbar |
|
||||
| **IDE / dev tools (SAS/C, DevPac)** | Project, Edit, Search, Run, Debug, Options | Extensive menus with many disabled states |
|
||||
| **Games with GUI frontends** | Game, Options, Help | Simple 2-3 menu strip; menus disabled during gameplay |
|
||||
| **File managers (Directory Opus)** | File, Edit, View, Tools, Settings | Heavily customized menus with gadget-driven alternatives |
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I have more than 31 menus?**
|
||||
A: No — the menu number is packed into 5 bits (0–31). In practice, no application needs more than 10–12 top-level menus. The Mac's Human Interface Guidelines recommend 5–7.
|
||||
|
||||
**Q: Can menus use custom fonts?**
|
||||
A: No — Intuition renders menus using the screen's default font (set via Font Preferences). GadTools' `LayoutMenus()` uses the screen's `Font` field for sizing. There is no per-menu font override.
|
||||
|
||||
**Q: What happens if the user opens a menu while I'm drawing?**
|
||||
A: Intuition freezes input and locks the screen's layers during menu mode. Your drawing calls will block until the menu interaction completes (user releases right mouse button). This is why long drawing operations should be done in a separate task.
|
||||
|
||||
**Q: Can I add images/icons to menu items?**
|
||||
A: Yes — by manually constructing `struct MenuItem` with `ItemFill` pointing to a `struct Image`. GadTools' `CreateMenus()` does not support image items. This is rarely done in practice.
|
||||
|
||||
**Q: How do I detect keyboard shortcuts in my event loop?**
|
||||
A: Intuition automatically handles Right-Amiga+key combinations matching `nm_CommKey` and generates `IDCMP_MENUPICK`. You do NOT need to handle them in `IDCMP_RAWKEY` — in fact, Intuition intercepts those key combinations before your window sees them.
|
||||
|
||||
**Q: What is `NOSUB` in `FULLMENUNUM`?**
|
||||
A: `NOSUB` (value 0) indicates that the item has no sub-item. Use it when constructing menu numbers for `OnMenu()`/`OffMenu()` on regular (non-sub) items.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- NDK 3.9: `intuition/intuition.h`, `libraries/gadtools.h`
|
||||
### NDK Headers
|
||||
|
||||
- `intuition/intuition.h` — `struct Menu`, `struct MenuItem`, `MENUNUM/ITEMNUM/SUBNUM` macros
|
||||
- `libraries/gadtools.h` — `struct NewMenu`, `NM_*` types/flags, `GTMENUITEM_USERDATA()`
|
||||
|
||||
### Autodocs
|
||||
|
||||
- ADCD 2.1: `CreateMenus()`, `LayoutMenus()`, `SetMenuStrip()`, `ClearMenuStrip()`, `FreeMenus()`
|
||||
- AmigaOS Reference Manual (RKRM): Libraries, Chapter 6 — Menus
|
||||
- See also: [IDCMP](idcmp.md), [Windows](windows.md), [GadTools (Gadgets)](gadgets.md)
|
||||
- ADCD 2.1: `ItemAddress()`, `OnMenu()`, `OffMenu()`, `GetVisualInfoA()`, `FreeVisualInfo()`
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [IDCMP](idcmp.md) — `IDCMP_MENUPICK` event delivery
|
||||
- [Windows](windows.md) — windows host menu strips
|
||||
- [Gadgets](gadgets.md) — GadTools creates both gadgets and menus
|
||||
- [Screens](screens.md) — menu bar renders in the screen title area
|
||||
- [Intuition Base](intuition_base.md) — Intuition's global state
|
||||
|
|
|
|||
|
|
@ -359,12 +359,420 @@ If you cache a screen pointer and it becomes invalid, `OpenWindowTags()` will cr
|
|||
6. **Drain IDCMP messages** before calling `CloseWindow()`
|
||||
7. **Show a busy pointer** during long operations — it prevents user confusion
|
||||
8. **Use `WA_AutoAdjust, TRUE`** to handle cases where the window doesn't fit the screen
|
||||
9. **Use `WA_NewLookMenus, TRUE`** on OS 3.0+ for consistent 3D appearance
|
||||
10. **Never modify `struct Window` fields directly** — use the API functions
|
||||
11. **Lock the screen before opening a window** — `LockPubScreen(NULL)` prevents the screen from closing mid-open
|
||||
12. **Handle `IDCMP_CLOSEWINDOW` immediately** — don't defer shutdown if user expectations are involved
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Border Collision" — Drawing at (0,0) in a Normal Window
|
||||
|
||||
```c
|
||||
/* BAD: (0,0) is the top-left corner of the BORDER, not the content area.
|
||||
This overwrites the title bar and close gadget. */
|
||||
Move(win->RPort, 0, 0);
|
||||
RectFill(win->RPort, 0, 0, 100, 50);
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Offset all drawing by the border widths */
|
||||
WORD x0 = win->BorderLeft;
|
||||
WORD y0 = win->BorderTop;
|
||||
Move(win->RPort, x0, y0);
|
||||
RectFill(win->RPort, x0, y0, x0 + 100, y0 + 50);
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> If you find yourself adding border offsets everywhere, consider `WFLG_GIMMEZEROZERO` — the content RastPort starts at (0,0). But it costs an extra layer.
|
||||
|
||||
### "The Unresponsive Close" — Ignoring IDCMP_CLOSEWINDOW
|
||||
|
||||
```c
|
||||
/* BAD: No handler for IDCMP_CLOSEWINDOW — clicking close does nothing.
|
||||
The user thinks the application is frozen. */
|
||||
ULONG signals = Wait(1L << win->UserPort->mp_SigBit);
|
||||
struct IntuiMessage *msg;
|
||||
while ((msg = (struct IntuiMessage *)GetMsg(win->UserPort)))
|
||||
{
|
||||
/* Handle gadgets, keys, but NO close event... */
|
||||
ReplyMsg((struct Message *)msg);
|
||||
}
|
||||
/* Window never closes. User is stuck. */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Always handle IDCMP_CLOSEWINDOW */
|
||||
while ((msg = (struct IntuiMessage *)GetMsg(win->UserPort)))
|
||||
{
|
||||
ULONG class = msg->Class;
|
||||
ReplyMsg((struct Message *)msg); /* reply FIRST */
|
||||
|
||||
switch (class)
|
||||
{
|
||||
case IDCMP_CLOSEWINDOW:
|
||||
running = FALSE; /* exit the event loop */
|
||||
break;
|
||||
case IDCMP_GADGETUP:
|
||||
/* ... handle gadget ... */
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### "The Leaked Message" — Accessing IntuiMessage After ReplyMsg
|
||||
|
||||
```c
|
||||
/* BAD: Reading fields from the message AFTER replying.
|
||||
ReplyMsg() may free the message immediately — the pointer is stale. */
|
||||
struct IntuiMessage *msg = (struct IntuiMessage *)GetMsg(win->UserPort);
|
||||
ReplyMsg((struct Message *)msg); /* may invalidate msg */
|
||||
UWORD code = msg->Code; /* UNDEFINED BEHAVIOR — msg may be freed */
|
||||
APTR iaddr = msg->IAddress; /* ALSO DANGEROUS */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Copy what you need BEFORE replying */
|
||||
struct IntuiMessage *msg = (struct IntuiMessage *)GetMsg(win->UserPort);
|
||||
ULONG class = msg->Class;
|
||||
UWORD code = msg->Code;
|
||||
APTR iaddr = msg->IAddress;
|
||||
WORD mouseX = msg->MouseX;
|
||||
WORD mouseY = msg->MouseY;
|
||||
ReplyMsg((struct Message *)msg); /* NOW it's safe to reply */
|
||||
|
||||
/* Use the saved copies */
|
||||
```
|
||||
|
||||
### "The Refresh Loop" — Drawing Outside BeginRefresh/EndRefresh
|
||||
|
||||
```c
|
||||
/* BAD: Drawing on every REFRESHWINDOW without BeginRefresh().
|
||||
Drawing into the full layer (not just damaged areas) causes NEW damage,
|
||||
which triggers another REFRESHWINDOW, which draws again... infinite loop. */
|
||||
case IDCMP_REFRESHWINDOW:
|
||||
RedrawEverything(win); /* redraws entire window, causes new damage */
|
||||
break;
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Scope the redraw to damaged regions only */
|
||||
case IDCMP_REFRESHWINDOW:
|
||||
BeginRefresh(win);
|
||||
RedrawEverything(win); /* ClipRects restricted to damaged areas */
|
||||
EndRefresh(win, TRUE); /* TRUE = fully repaired, clear damage list */
|
||||
break;
|
||||
```
|
||||
|
||||
### "The Phantom Window" — Using a Screen Pointer After It Closes
|
||||
|
||||
```c
|
||||
/* BAD: Caching a screen pointer between operations.
|
||||
Another application can close the screen between your cache and use. */
|
||||
struct Screen *screen = LockPubScreen(NULL);
|
||||
UnlockPubScreen(NULL, screen);
|
||||
/* ... later ... */
|
||||
OpenWindowTags(NULL, WA_PubScreen, screen, TAG_DONE); /* screen may be gone! */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Lock, open, unlock — all in sequence */
|
||||
struct Screen *screen = LockPubScreen(NULL);
|
||||
if (screen) {
|
||||
struct Window *win = OpenWindowTags(NULL,
|
||||
WA_PubScreen, screen,
|
||||
WA_InnerWidth, 400,
|
||||
WA_InnerHeight, 300,
|
||||
TAG_DONE);
|
||||
UnlockPubScreen(NULL, screen);
|
||||
/* win is now valid — Intuition holds its own reference to the screen */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Window Type Decision Guide
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Q{"What do you need?"} -->|"Standard app window"| STD["Standard Window\nClose + Drag + Depth + Size"]
|
||||
Q -->|"Background / desktop"| BD["Backdrop Window\nWFLG_BACKDROP | WFLG_BORDERLESS"]
|
||||
Q -->|"Fullscreen overlay"| BL["Borderless Window\nWA_Borderless + WA_InnerWidth/Height"]
|
||||
Q -->|"Simple content,\nborder offset headaches"| GZZ["GimmeZeroZero\nContent RastPort at (0,0)"]
|
||||
Q -->|"Large scrollable canvas"| SUPER["SuperBitMap\nWA_SuperBitMap + BitMap"]
|
||||
|
||||
STD --> REF{"Refresh mode?"}
|
||||
REF -->|"Most apps"| SMART["Smart Refresh\n(WA_SmartRefresh)"]
|
||||
REF -->|"Memory-critical"| SIMPLE["Simple Refresh\n(WA_SimpleRefresh)"]
|
||||
REF -->|"Scrollable canvas"| SUPER
|
||||
|
||||
style STD fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style BD fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style BL fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style GZZ fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style SMART fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Cookbooks
|
||||
|
||||
### Cookbook: Resizable Window with Proper Relayout
|
||||
|
||||
```c
|
||||
#include <proto/exec.h>
|
||||
#include <proto/intuition.h>
|
||||
#include <proto/graphics.h>
|
||||
#include <intuition/intuition.h>
|
||||
|
||||
void RunResizableWindow(void)
|
||||
{
|
||||
struct Screen *screen = LockPubScreen(NULL);
|
||||
if (!screen) return;
|
||||
|
||||
struct Window *win = OpenWindowTags(NULL,
|
||||
WA_PubScreen, screen,
|
||||
WA_InnerWidth, 400,
|
||||
WA_InnerHeight, 300,
|
||||
WA_Title, "Resizable",
|
||||
WA_IDCMP, IDCMP_CLOSEWINDOW | IDCMP_NEWSIZE |
|
||||
IDCMP_REFRESHWINDOW,
|
||||
WA_Flags, WFLG_CLOSEGADGET | WFLG_DRAGBAR |
|
||||
WFLG_DEPTHGADGET | WFLG_SIZEGADGET |
|
||||
WFLG_SMART_REFRESH | WFLG_ACTIVATE,
|
||||
WA_MinWidth, 200,
|
||||
WA_MinHeight, 100,
|
||||
WA_MaxWidth, -1,
|
||||
WA_MaxHeight, -1,
|
||||
TAG_DONE);
|
||||
UnlockPubScreen(NULL, screen);
|
||||
|
||||
if (!win) return;
|
||||
|
||||
BOOL running = TRUE;
|
||||
while (running)
|
||||
{
|
||||
ULONG sigs = Wait(1L << win->UserPort->mp_SigBit);
|
||||
struct IntuiMessage *msg;
|
||||
|
||||
while ((msg = (struct IntuiMessage *)GetMsg(win->UserPort)))
|
||||
{
|
||||
ULONG class = msg->Class;
|
||||
ReplyMsg((struct Message *)msg);
|
||||
|
||||
switch (class)
|
||||
{
|
||||
case IDCMP_CLOSEWINDOW:
|
||||
running = FALSE;
|
||||
break;
|
||||
|
||||
case IDCMP_NEWSIZE:
|
||||
case IDCMP_REFRESHWINDOW:
|
||||
if (class == IDCMP_REFRESHWINDOW)
|
||||
BeginRefresh(win);
|
||||
|
||||
/* Recalculate layout and redraw */
|
||||
{
|
||||
WORD w = win->Width - win->BorderLeft - win->BorderRight;
|
||||
WORD h = win->Height - win->BorderTop - win->BorderBottom;
|
||||
SetRast(win->RPort, 0); /* clear */
|
||||
/* ... your layout code using w, h ... */
|
||||
}
|
||||
|
||||
if (class == IDCMP_REFRESHWINDOW)
|
||||
EndRefresh(win, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseWindow(win);
|
||||
}
|
||||
```
|
||||
|
||||
### Cookbook: Full-Screen Borderless Overlay
|
||||
|
||||
```c
|
||||
/* Open a borderless window covering the entire screen —
|
||||
useful for games, demos, or presentations */
|
||||
struct Screen *scr = LockPubScreen(NULL);
|
||||
struct Window *overlay = OpenWindowTags(NULL,
|
||||
WA_PubScreen, scr,
|
||||
WA_Left, 0,
|
||||
WA_Top, 0,
|
||||
WA_Width, scr->Width,
|
||||
WA_Height, scr->Height,
|
||||
WA_Borderless, TRUE,
|
||||
WA_SimpleRefresh, TRUE,
|
||||
WA_NoCareRefresh, TRUE,
|
||||
WA_RMBTrap, TRUE, /* capture right-click (no menu bar) */
|
||||
WA_Activate, TRUE,
|
||||
WA_IDCMP, IDCMP_RAWKEY | IDCMP_MOUSEBUTTONS |
|
||||
IDCMP_MOUSEMOVE,
|
||||
TAG_DONE);
|
||||
UnlockPubScreen(NULL, scr);
|
||||
|
||||
/* Draw directly at (0,0) — no borders */
|
||||
SetAPen(overlay->RPort, 1);
|
||||
RectFill(overlay->RPort, 0, 0, scr->Width - 1, scr->Height - 1);
|
||||
```
|
||||
|
||||
### Cookbook: Multi-Window Shared Message Port
|
||||
|
||||
```c
|
||||
/* For applications with many windows, sharing one MsgPort is more
|
||||
efficient than creating a port per window.
|
||||
See idcmp.md for the full explanation. */
|
||||
|
||||
struct MsgPort *sharedPort = CreateMsgPort();
|
||||
|
||||
/* Window A */
|
||||
struct Window *winA = OpenWindowTags(NULL,
|
||||
WA_Title, "Window A",
|
||||
WA_InnerWidth, 300, WA_InnerHeight, 200,
|
||||
WA_Flags, WFLG_CLOSEGADGET | WFLG_DRAGBAR | WFLG_ACTIVATE,
|
||||
WA_IDCMP, IDCMP_CLOSEWINDOW | IDCMP_RAWKEY,
|
||||
TAG_DONE);
|
||||
|
||||
/* Window B */
|
||||
struct Window *winB = OpenWindowTags(NULL,
|
||||
WA_Title, "Window B",
|
||||
WA_InnerWidth, 300, WA_InnerHeight, 200,
|
||||
WA_Flags, WFLG_CLOSEGADGET | WFLG_DRAGBAR,
|
||||
WA_IDCMP, IDCMP_CLOSEWINDOW | IDCMP_RAWKEY,
|
||||
TAG_DONE);
|
||||
|
||||
/* Redirect both to the shared port */
|
||||
winA->UserPort = sharedPort;
|
||||
winB->UserPort = sharedPort;
|
||||
ModifyIDCMP(winA, IDCMP_CLOSEWINDOW | IDCMP_RAWKEY);
|
||||
ModifyIDCMP(winB, IDCMP_CLOSEWINDOW | IDCMP_RAWKEY);
|
||||
|
||||
/* Event loop processes both windows from one port */
|
||||
BOOL running = TRUE;
|
||||
while (running)
|
||||
{
|
||||
Wait(1L << sharedPort->mp_SigBit);
|
||||
struct IntuiMessage *msg;
|
||||
while ((msg = (struct IntuiMessage *)GetMsg(sharedPort)))
|
||||
{
|
||||
struct Window *src = msg->IDCMPWindow; /* which window? */
|
||||
ULONG class = msg->Class;
|
||||
ReplyMsg((struct Message *)msg);
|
||||
|
||||
if (class == IDCMP_CLOSEWINDOW)
|
||||
{
|
||||
if (src == winA) { CloseWindow(winA); winA = NULL; }
|
||||
if (src == winB) { CloseWindow(winB); winB = NULL; }
|
||||
if (!winA && !winB) running = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
DeleteMsgPort(sharedPort);
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> When closing a window with a shared port, you **must** drain its messages, detach the port, and strip pending messages. See [idcmp.md](idcmp.md#multi-window-shared-port) for the full `Forbid()` protocol.
|
||||
|
||||
---
|
||||
|
||||
## Historical Context & Modern Analogies
|
||||
|
||||
### Competitive Landscape
|
||||
|
||||
| Platform | Overlapping Windows | Clipping System | Backing Store | Notes |
|
||||
|----------|---------------------|-----------------|---------------|-------|
|
||||
| **AmigaOS Intuition** | Yes — layered | layers.library ClipRect | Smart Refresh | Lightweight — just a Layer + RastPort |
|
||||
| **Mac OS (Classic)** | Yes — via Window Manager | Region-based | Window buffer (optional) | Heavyweight GrafPort per window |
|
||||
| **Windows 1.x** | No — tiled only | Rectangle clip | None | Could not overlap |
|
||||
| **Windows 2.x–3.x** | Yes — overlapping | Region clip | None (app redraws) | `WM_PAINT` driven |
|
||||
| **Atari ST GEM** | No — tiled (AES limits) | Rectangle only | None | Max 8 windows, no true overlap |
|
||||
| **X11** | Yes — server-side | Server region clip | Backing store (optional) | Remote protocol overhead |
|
||||
|
||||
The Amiga was the **only consumer platform in 1985** with true overlapping windows backed by automatic damage repair (Smart Refresh). The Mac had overlapping windows but required the application to handle all redraw until System 7 (1991).
|
||||
|
||||
### Modern Analogies
|
||||
|
||||
| Amiga Concept | Modern Equivalent | Notes |
|
||||
|--------------|-------------------|-------|
|
||||
| `OpenWindowTags()` | `CreateWindowEx()` (Win32) / `NSWindow()` (macOS) / `gtk_window_new()` | Tag-based extensible API was ahead of its time |
|
||||
| `struct Window` | `NSWindow` / `GtkWidget` / `wl_surface` | Amiga's is read-only; modern objects have methods |
|
||||
| `WA_InnerWidth/Height` | `contentRect` (macOS) / `client area` (Win32) | Same concept — content area minus chrome |
|
||||
| Smart Refresh | Compositor shadow buffer (Wayland) / `CA_LAYER` (macOS) | OS saves obscured content automatically |
|
||||
| Simple Refresh | `WM_PAINT` (Win32) / `expose-event` (X11/GTK2) | App must redraw damaged areas |
|
||||
| `IDCMP_CLOSEWINDOW` | `WM_CLOSE` (Win32) / `windowWillClose:` (macOS) | User clicked the close button |
|
||||
| `IDCMP_NEWSIZE` | `WM_SIZE` (Win32) / `resize-event` (GTK) | Window dimensions changed |
|
||||
| `WA_RMBTrap` | `button-press-event` right button (GTK) | Capture right-click instead of menu activation |
|
||||
| `SetWindowTitles()` | `window.title =` (macOS/GTK) | Change title at runtime |
|
||||
| `SetWindowPointer(WA_BusyPointer)` | `NSCursor.operationSetCursor()` / `gtk_window_set_cursor(GDK_WATCH)` | Hourglass / spinning beach ball |
|
||||
|
||||
The key architectural difference: Amiga windows are **immediate-mode** — you draw directly to the screen bitmap via the window's RastPort. Modern windows are **retained-mode** — you describe what to draw, and the compositor renders it off-screen, then presents it. This is why Amiga windows could be so lightweight — no compositor, no texture memory, just rectangle math.
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Software Type | Window Style | Refresh Mode | Notes |
|
||||
|--------------|-------------|--------------|-------|
|
||||
| Word processor (Final Writer) | Standard + gadgets | Smart | Full gadget toolbar, resizable |
|
||||
| Drawing program (Deluxe Paint) | Borderless or standard | SuperBitMap | Canvas in off-screen bitmap |
|
||||
| Terminal emulator (Term) | Standard | Smart | Scrollback via ClipBlit |
|
||||
| Spreadsheet (MaxiPlan) | Standard + lots of gadgets | Smart | Grid layout, cell gadgets |
|
||||
| Game (Lemmings) | Full-screen borderless | Simple | Full redraw every frame anyway |
|
||||
| Demo scene intro | Borderless + RMBTrap | Simple | No system gadgets, raw key input |
|
||||
| Workbench background | Backdrop + borderless | Simple | Behind everything, draws desktop pattern |
|
||||
| File requester (ASL) | Standard + borderless | Smart | Modal dialog pattern |
|
||||
| System monitor (SysMon) | Standard, small | Smart | Periodic timer-driven redraw |
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: What happens if `OpenWindowTags()` returns `NULL`?**
|
||||
A: Either the screen is locked by another application, the system is out of memory (Chip RAM for Smart refresh), or the requested flags are contradictory. Check `IoErr()` for the specific error.
|
||||
|
||||
**Q: Can I open a window without a screen?**
|
||||
A: No — every window must belong to a screen. Use `WA_PubScreen, NULL` for the default (Workbench) screen, or `WA_CustomScreen, myScreen` for your own.
|
||||
|
||||
**Q: What's the minimum set of IDCMP flags I should request?**
|
||||
A: At minimum: `IDCMP_CLOSEWINDOW` (if you have a close gadget) + `IDCMP_GADGETUP` (if you have gadgets). Never request all flags — it floods your message port with mouse move events.
|
||||
|
||||
**Q: How do I make a window that can't be moved?**
|
||||
A: Don't set `WFLG_DRAGBAR`. The window will have no title bar drag area.
|
||||
|
||||
**Q: What is GimmeZeroZero good for?**
|
||||
A: It creates a separate layer for borders so your content RastPort starts at (0,0) — no need to add `BorderLeft`/`BorderTop` offsets to every draw call. The cost is an extra layer (memory + blitter overhead). Useful for applications with complex drawing that don't want to track border offsets.
|
||||
|
||||
**Q: Can I change the refresh mode after opening?**
|
||||
A: No — the refresh mode is determined at open time by the `WFLG_SMART_REFRESH` / `WFLG_SIMPLE_REFRESH` / `WFLG_SUPER_BITMAP` flags. You must close and reopen the window to change it.
|
||||
|
||||
**Q: What is `WA_AutoAdjust`?**
|
||||
A: If the requested window dimensions don't fit on the screen, `WA_AutoAdjust, TRUE` tells Intuition to shrink or reposition the window to make it fit. Without it, `OpenWindowTags()` may fail if the window is too large.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- NDK 3.9: `intuition/intuition.h`, `intuition/screens.h`
|
||||
- ADCD 2.1: `OpenWindowTagList()`, `CloseWindow()`, `ModifyIDCMP()`
|
||||
- AmigaOS Reference Manual (RKRM): Libraries, Chapter 4 — Intuition Windows
|
||||
- See also: [IDCMP](idcmp.md), [Screens](screens.md), [Gadgets](gadgets.md)
|
||||
### NDK Headers
|
||||
|
||||
- `intuition/intuition.h` — `struct Window`, `struct NewWindow`, `WA_*` tags, `WFLG_*` flags
|
||||
- `intuition/screens.h` — `LockPubScreen()`, `UnlockPubScreen()`
|
||||
- `intuition/intuitionbase.h` — IntuitionBase
|
||||
|
||||
### Autodocs
|
||||
|
||||
- ADCD 2.1: `OpenWindowTagList()`, `CloseWindow()`, `MoveWindow()`, `SizeWindow()`, `ChangeWindowBox()`
|
||||
- ADCD 2.1: `SetWindowTitles()`, `SetWindowPointer()`, `WindowToFront()`, `WindowToBack()`
|
||||
- ADCD 2.1: `ModifyIDCMP()`, `RefreshWindowFrame()`
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [IDCMP](idcmp.md) — event delivery mechanism, shared message port protocol
|
||||
- [Screens](screens.md) — screen architecture, Copper-based display switching
|
||||
- [Gadgets](gadgets.md) — button, string, slider, and custom BOOPSI gadgets
|
||||
- [Menus](menus.md) — menu bar structure and event handling
|
||||
- [Layers](../11_libraries/layers.md) — the clipping engine behind every window
|
||||
- [RastPort](../08_graphics/rastport.md) — drawing context, the window's `RPort`
|
||||
- [Requesters](requesters.md) — modal and async dialog boxes
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@
|
|||
|
||||
# console.device — Text Terminal I/O
|
||||
|
||||
## Overview
|
||||
Every CLI/Shell window on the Amiga is powered by `console.device`. It is a software terminal emulator that sits between raw keyboard input and Intuition window rendering: it translates keycodes into ASCII characters, parses ANSI escape sequences for cursor control and color, and renders text through the window's RastPort. If you need a text interface in an Intuition window — a debugger console, a text editor, a terminal emulator — console.device is the foundation.
|
||||
|
||||
`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, color, 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.
|
||||
> [!NOTE]
|
||||
> For simple file I/O, use [dos.library](../07_dos/file_io.md) with `CON:` windows instead of opening console.device directly. The `CON:` and `RAW:` handlers wrap console.device and provide buffered line editing, window management, and automatic cleanup.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
|
@ -238,7 +234,237 @@ BPTR raw = Open("RAW:0/0/640/200/Raw Input", MODE_OLDFILE);
|
|||
|
||||
## References
|
||||
|
||||
- NDK39: `devices/conunit.h`, `devices/console.h`
|
||||
### NDK Headers
|
||||
|
||||
- `devices/conunit.h` — unit type constants (`CONU_STANDARD`, etc.)
|
||||
- `devices/console.h` — console-specific structures
|
||||
|
||||
### Documentation
|
||||
|
||||
- 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
|
||||
- *Amiga ROM Kernel Reference Manual: Devices* — console chapter
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [keyboard.md](keyboard.md) — raw keycode to console.device pipeline
|
||||
- [input.md](input.md) — input handler chain
|
||||
- [cli_shell.md](../07_dos/cli_shell.md) — Shell architecture built on console.device
|
||||
- [windows.md](../09_intuition/windows.md) — Intuition window management
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide: How to Render Text
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START{"Need text output?"} --> WHERE{"Where?"}
|
||||
WHERE -->|"Shell window"| CON{"Use CON: handler<br>via dos.library"}
|
||||
WHERE -->|"Own Intuition window"| COMPLEX{"Need cursor, colors,<br>line editing?"}
|
||||
COMPLEX -->|Yes| CD{"Use console.device"}
|
||||
COMPLEX -->|No| RP{"Use RastPort text<br>functions directly"}
|
||||
WHERE -->|"Full-screen TUI"| CD
|
||||
WHERE -->|"Custom text editor"| CD
|
||||
```
|
||||
|
||||
| Approach | Use When | Pros | Cons |
|
||||
|----------|----------|------|------|
|
||||
| **CON: / RAW:** | Simple text I/O in a window | Easiest — just `Open()` / `Write()` | Limited control over rendering |
|
||||
| **console.device** | Full-screen TUI, text editor | ANSI escape sequences, cursor, colors | Must manage I/O requests manually |
|
||||
| **RastPort text** | Custom text rendering, game UI | Full pixel-level control | No built-in cursor, scrolling, or input handling |
|
||||
| **Intuition gadgets** | Forms, menus, buttons | Standard UI elements | Not suitable for free-form text |
|
||||
|
||||
---
|
||||
|
||||
## Use-Case Cookbook
|
||||
|
||||
### Full-Screen Text UI (Status Display)
|
||||
|
||||
```c
|
||||
/* con_tui.c — full-screen TUI using console.device */
|
||||
#include <proto/exec.h>
|
||||
#include <proto/intuition.h>
|
||||
#include <proto/dos.h>
|
||||
|
||||
void ConPuts(struct IOStdReq *con, const char *str)
|
||||
{
|
||||
con->io_Command = CMD_WRITE;
|
||||
con->io_Data = (APTR)str;
|
||||
con->io_Length = -1;
|
||||
DoIO((struct IORequest *)con);
|
||||
}
|
||||
|
||||
void run_tui(struct Window *win, struct IOStdReq *con)
|
||||
{
|
||||
/* Clear screen and draw a frame */
|
||||
ConPuts(con, "\033[2J"); /* clear screen */
|
||||
ConPuts(con, "\033[1;1H"); /* home cursor */
|
||||
ConPuts(con, "\033[7m Status Monitor \033[0m\n"); /* inverse title */
|
||||
ConPuts(con, "\033[3;1H\033[33mCPU: \033[32m7.14 MHz\033[0m");
|
||||
ConPuts(con, "\033[4;1H\033[33mChip: \033[32m512 KB\033[0m");
|
||||
ConPuts(con, "\033[20;1H\033[7m Press ESC to exit \033[0m");
|
||||
|
||||
/* Main loop — wait for keypress */
|
||||
char buf[8];
|
||||
con->io_Command = CMD_READ;
|
||||
con->io_Data = (APTR)buf;
|
||||
con->io_Length = sizeof(buf);
|
||||
DoIO((struct IORequest *)con);
|
||||
/* Check if ESC was pressed (0x1B) */
|
||||
}
|
||||
```
|
||||
|
||||
### Progress Bar Using Escape Sequences
|
||||
|
||||
```c
|
||||
void DrawProgressBar(struct IOStdReq *con, int row, int percent)
|
||||
{
|
||||
char buf[80];
|
||||
int bar_width = 40;
|
||||
int filled = (percent * bar_width) / 100;
|
||||
|
||||
/* Move to row, column 10 */
|
||||
RawDoFmt("\033[%ld;10H", (RAWARG)&row, NULL, buf);
|
||||
ConPuts(con, buf);
|
||||
|
||||
/* Draw filled portion in green, empty in black */
|
||||
ConPuts(con, "\033[42m"); /* green background */
|
||||
for (int i = 0; i < filled; i++) ConPuts(con, " ");
|
||||
ConPuts(con, "\033[40m"); /* black background */
|
||||
for (int i = filled; i < bar_width; i++) ConPuts(con, " ");
|
||||
ConPuts(con, "\033[0m"); /* reset */
|
||||
|
||||
/* Percentage text */
|
||||
RawDoFmt(" %ld%%", (RAWARG)&percent, NULL, buf);
|
||||
ConPuts(con, buf);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use `CON:` via dos.library for simple text I/O — avoid opening console.device directly unless you need escape sequence control
|
||||
2. Always save/restore cursor position around multi-step output operations
|
||||
3. Reset all text attributes (`\033[0m`) at the end of every output operation — stale attributes cause rendering bugs
|
||||
4. Use `CONU_SNIPMAP` (OS 3.0+) for clipboard support in text editors
|
||||
5. Set scroll margins with `\033[t` / `\033[b` for status bars that stay fixed
|
||||
6. Always abort pending reads before closing the device — see Proper Shutdown above
|
||||
7. Handle both ASCII and escape-sequence input when reading — cursor keys arrive as `\033[A` etc.
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Leaking Read" — Pending I/O on Shutdown
|
||||
|
||||
```c
|
||||
/* BAD: Pending async read never completed */
|
||||
con->io_Command = CMD_READ;
|
||||
con->io_Data = (APTR)buffer;
|
||||
con->io_Length = 256;
|
||||
SendIO((struct IORequest *)con);
|
||||
|
||||
/* ... user closes window ... */
|
||||
CloseDevice((struct IORequest *)con); /* CRASH: pending I/O */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Abort pending I/O before closing */
|
||||
if (!CheckIO((struct IORequest *)con))
|
||||
{
|
||||
AbortIO((struct IORequest *)con);
|
||||
WaitIO((struct IORequest *)con);
|
||||
}
|
||||
CloseDevice((struct IORequest *)con);
|
||||
```
|
||||
|
||||
### "The Attribute Leak" — Stale Colors After Output
|
||||
|
||||
```c
|
||||
/* BAD: Set color but never reset — next output inherits it */
|
||||
ConPuts(con, "\033[31mError!");
|
||||
/* Next write is still red! */
|
||||
ConPuts(con, "Normal text"); /* Still red! */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Always reset attributes */
|
||||
ConPuts(con, "\033[31mError!\033[0m");
|
||||
ConPuts(con, "Normal text"); /* Correctly default color */
|
||||
```
|
||||
|
||||
### "The Blocking Shell" — DoIO Read Freezes UI
|
||||
|
||||
```c
|
||||
/* BAD: DoIO on CMD_READ blocks forever if no input comes */
|
||||
con->io_Command = CMD_READ;
|
||||
con->io_Length = 1;
|
||||
DoIO((struct IORequest *)con); /* Frozen — can't handle IDCMP events */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Use SendIO + Wait with IDCMP signals */
|
||||
con->io_Command = CMD_READ;
|
||||
con->io_Length = 1;
|
||||
SendIO((struct IORequest *)con);
|
||||
|
||||
ULONG conSig = 1 << conPort->mp_SigBit;
|
||||
ULONG winSig = 1 << window->UserPort->mp_SigBit;
|
||||
ULONG sigs = Wait(conSig | winSig);
|
||||
|
||||
if (sigs & winSig) {
|
||||
/* Handle IDCMP event (close window, etc.) */
|
||||
AbortIO((struct IORequest *)con);
|
||||
WaitIO((struct IORequest *)con);
|
||||
}
|
||||
if (sigs & conSig) {
|
||||
WaitIO((struct IORequest *)con);
|
||||
/* Process input character */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Console Position is 1-Based, Not 0-Based
|
||||
|
||||
**Symptom:** Text appears one row/column off from expected position.
|
||||
|
||||
**Cause:** ANSI `H` command uses 1-based coordinates: `\033[1;1H` is the top-left corner, not `\033[0;0H`.
|
||||
|
||||
**Fix:** Always add 1 to your 0-based coordinates.
|
||||
|
||||
### 2. Writing to a Closed Window
|
||||
|
||||
**Symptom:** Guru meditation or silent crash.
|
||||
|
||||
**Cause:** Console.device renders through the window's RastPort. If the window is closed (user clicked close gadget), writing to the console device dereferences a stale window pointer.
|
||||
|
||||
**Fix:** Monitor `IDCMP_CLOSEWINDOW` and abort all console I/O before closing.
|
||||
|
||||
### 3. Buffer Overrun on Read
|
||||
|
||||
**Symptom:** Memory corruption.
|
||||
|
||||
**Cause:** `CMD_READ` returns up to `io_Length` bytes. If you allocated a smaller buffer, the device writes past the end.
|
||||
|
||||
**Fix:** Always ensure `io_Length <= sizeof(buffer)`.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: What is the difference between CON: and RAW:?**
|
||||
A: `CON:` is line-buffered — input is collected until the user presses Enter, with line editing (backspace, cursor keys). `RAW:` delivers each keypress immediately with no editing. Use `RAW:` for games and interactive TUIs; use `CON:` for command-line tools.
|
||||
|
||||
**Q: Can I use console.device without an Intuition window?**
|
||||
A: Yes — open with `CONU_LIBRARY` unit type. This gives access to the keymap translation without rendering. Useful for translating raw keycodes to ASCII.
|
||||
|
||||
**Q: How do I create a console with a specific font size?**
|
||||
A: Open the window with the desired font, then open console.device on that window. The console uses the window's RastPort font. Set the font with `SetFont(window->RPort, myFont)` before opening the console.
|
||||
|
||||
**Q: Why does my text look wrong after a screen drag?**
|
||||
A: Console.device renders into the window's RastPort. When the screen is dragged, the console does not automatically redraw. You must handle `IDCMP_NEWSIZE` and redraw the console content.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
[← Home](../README.md) · [Devices](README.md)
|
||||
|
||||
# trackdisk.device — Floppy Disk DMA Controller
|
||||
# trackdisk.device — Floppy Disk Controller
|
||||
|
||||
## Overview
|
||||
Every Amiga shipped with a floppy drive — and every Amiga program that loaded from disk went through `trackdisk.device`. It is the lowest-level software interface to the Amiga's custom-chip floppy DMA controller, providing sector-level read/write access to 3.5" double-density disks. The Amiga's floppy controller is unusual for its era: it reads and writes **raw MFM bitstreams** rather than decoded data, giving software complete control over on-disk format. This is why the Amiga could read PC, Mac, and ST disks — and why copy-protected Amiga disks are so hard to duplicate.
|
||||
|
||||
`trackdisk.device` interfaces with the Amiga's floppy disk controller — a custom DMA engine that reads and writes raw MFM-encoded data from double-density 3.5" disks. It provides block-level access (512 bytes/sector, 11 sectors/track, 80 tracks × 2 sides = 1,760 sectors = 880 KB per disk).
|
||||
> [!NOTE]
|
||||
> Most application developers should use [dos.library](../07_dos/file_io.md) (`Open()`, `Read()`, `Write()`) instead of trackdisk.device directly. The filesystem handles sector-to-file mapping, buffering, and error recovery. Use trackdisk.device only when you need raw disk access: disk duplication, custom bootblocks, copy-protection analysis, or low-level diagnostics.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -42,17 +43,22 @@ graph LR
|
|||
|
||||
### MFM Encoding
|
||||
|
||||
The disk stores data in **Modified Frequency Modulation** format. Each byte becomes 16 bits on disk (clock + data interleaved):
|
||||
The disk stores data in **Modified Frequency Modulation** format. Each data byte becomes **16 bits** on disk — a clock bit and a data bit interleaved. The clock bit is determined by a simple rule: write a `1` clock only when both the previous data bit and current data bit are `0`.
|
||||
|
||||
```
|
||||
Data bit: 1 0 1 1 0 0 0 1
|
||||
MFM: 01 10 01 01 10 10 10 01
|
||||
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
|
||||
c/d pairs (clock bit inserted before each data bit)
|
||||
Data byte: $A1 = 1010 0001
|
||||
|
||||
MFM encoding (data bits underlined, clock bits computed):
|
||||
Data: 1 0 1 0 0 0 0 1
|
||||
Clock: 0 1 0 1 1 1 1 0
|
||||
MFM: 01 10 01 10 10 10 10 01
|
||||
= $4489 (the famous Amiga sync word)
|
||||
```
|
||||
|
||||
A raw track is ~12,668 bytes of MFM data (including gaps, sync words, and sector headers).
|
||||
|
||||
> **Key insight**: The Amiga floppy controller does **not** decode MFM in hardware — it DMA-transfers the raw MFM bitstream directly to Chip RAM. Decoding is done in software by `trackdisk.device`. This design choice is what gives the Amiga its unique ability to read and write non-standard disk formats.
|
||||
|
||||
### Track Format (AmigaDOS)
|
||||
|
||||
```
|
||||
|
|
@ -67,6 +73,20 @@ Track = 11 sectors, each containing:
|
|||
Gaps between sectors: variable-length padding
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph TRACK["One Track = 11 Sectors + Gaps"]
|
||||
direction LR
|
||||
GAP1["Gap"] --> S0["Sector 0<br>Sync+Header+Data"] --> GAP2["Gap"] --> S1["Sector 1"] --> S2["..."] --> S10["Sector 10"] --> GAP3["Gap"]
|
||||
end
|
||||
subgraph SECTOR["Single Sector Layout"]
|
||||
direction LR
|
||||
SYNC["$4489 $4489<br>Sync"] --> HDR["4 longs<br>Header"] --> HCRC["Checksum<br>1 long"] --> DATA["512 bytes<br>Data"] --> DCRC["Checksum<br>1 long"]
|
||||
end
|
||||
```
|
||||
|
||||
> **Why sync words matter**: The controller synchronizes to the MFM bitstream by looking for the pattern `$4489 $4489`, which violates normal MFM encoding rules (a missing clock pulse). This makes sync words unambiguous — they can never appear in normal data. Copy protection schemes exploit this by placing non-standard sync patterns that normal disk copiers cannot read.
|
||||
|
||||
---
|
||||
|
||||
## Using trackdisk.device
|
||||
|
|
@ -172,7 +192,237 @@ Games often bypass trackdisk.device for speed and copy protection:
|
|||
|
||||
## References
|
||||
|
||||
- NDK39: `devices/trackdisk.h`, `resources/disk.h`
|
||||
### NDK Headers
|
||||
|
||||
- `devices/trackdisk.h` — `struct IOExtTD`, command constants
|
||||
- `resources/disk.h` — disk resource management
|
||||
|
||||
### Documentation
|
||||
|
||||
- HRM: *Amiga Hardware Reference Manual* — Disk Controller chapter
|
||||
- ADCD 2.1: trackdisk.device autodocs
|
||||
- See also: [filesystem.md](../07_dos/filesystem.md) — FFS/OFS block format on top of trackdisk
|
||||
- *Amiga ROM Kernel Reference Manual: Devices* — trackdisk chapter
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [filesystem.md](../07_dos/filesystem.md) — FFS/OFS block format on top of trackdisk
|
||||
- [boot_block](../02_boot_sequence/dos_boot.md) — bootblock format and loading sequence
|
||||
- [custom_loaders_and_drm.md](../05_reversing/custom_loaders_and_drm.md) — copy protection schemes that exploit raw disk access
|
||||
- [io_requests.md](../06_exec_os/io_requests.md) — IORequest, DoIO, SendIO protocol
|
||||
|
||||
---
|
||||
|
||||
## trackdisk vs. dos.library — Decision Guide
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START{"Need disk access?"} --> WHAT{"What level?"}
|
||||
WHAT -->|"File read/write"| DOS{"Use dos.library<br>Open/Read/Write"}
|
||||
WHAT -->|"Raw sector access"| WHY{"Why?"}
|
||||
WHY -->|"Bootblock, disk tool"| TD{"Use trackdisk.device"}
|
||||
WHY -->|"Copy protection analysis"| TD
|
||||
WHY -->|"Non-Amiga disk format"| RAW{"Use raw disk DMA"}
|
||||
WHY -->|"Disk duplication"| TD
|
||||
```
|
||||
|
||||
| Criterion | dos.library | trackdisk.device | Raw DMA (CIA+custom) |
|
||||
|-----------|-------------|------------------|----------------------|
|
||||
| **Level** | File I/O | Sector I/O | MFM bitstream |
|
||||
| **Buffer** | Any RAM | Chip RAM only | Chip RAM only |
|
||||
| **Error recovery** | Automatic (retries) | Built-in (retries) | Your responsibility |
|
||||
| **Filesystem aware** | Yes | No | No |
|
||||
| **Write protection** | Respects WP tab | Respects WP tab | Must check yourself |
|
||||
| **Disk change detection** | Automatic | TD_CHANGENUM | Must poll CIA pins |
|
||||
| **Motor control** | Automatic | Manual (TD_MOTOR) | Manual (CIA PRB) |
|
||||
| **Typical use** | Applications | Disk tools, bootblocks | Games, demos, protection |
|
||||
| **FPGA impact** | None — OS handles it | Must emulate whole-track cache | Must emulate exact DMA timing |
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | LVO | Description |
|
||||
|---------|-----|-------------|
|
||||
| `CMD_READ` | 0 | Read sectors from disk |
|
||||
| `CMD_WRITE` | 1 | Write sectors to disk |
|
||||
| `CMD_UPDATE` | 2 | Flush write buffer to disk |
|
||||
| `CMD_CLEAR` | 3 | Clear read-ahead buffer |
|
||||
| `TD_CHANGENUM` | 6 | Get disk change counter |
|
||||
| `TD_CHANGESTATE` | 7 | Check if disk is in drive (0=in, 1=empty) |
|
||||
| `TD_PROTSTATUS` | 8 | Check write-protect status |
|
||||
| `TD_ADDCHANGEINT` 9 | Add disk-change interrupt handler |
|
||||
| `TD_REMCHANGEINT` 10 | Remove disk-change interrupt handler |
|
||||
| `TD_GETNUMTRACKS` 12 | Get total number of tracks |
|
||||
| `TD_ADDTRACK` | 13 | Add track buffer (AmigaOS 2.0+) |
|
||||
| `TD_FORMAT` | 4 | Format a track (write raw MFM) |
|
||||
| `TD_RAWREAD` | 10 | Read raw MFM track (AmigaOS 2.0+) |
|
||||
| `TD_RAWWRITE` | 11 | Write raw MFM track (AmigaOS 2.0+) |
|
||||
| `TD_GETDRIVETYPE` 15 | Get drive type (AmigaOS 3.0+) |
|
||||
|
||||
> [!WARNING]
|
||||
> All I/O buffers passed to `CMD_READ` and `CMD_WRITE` **must be in Chip RAM**. The floppy DMA engine can only access Chip RAM. Using Fast RAM causes silent data corruption.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always use `MEMF_CHIP` for trackdisk I/O buffers — the floppy DMA engine cannot reach Fast RAM
|
||||
2. Call `CMD_UPDATE` after writing — the device buffers writes internally
|
||||
3. Check `TD_CHANGESTATE` before critical operations — the user may have ejected the disk
|
||||
4. Turn off the motor with `TD_MOTOR(0)` when done — leaving it on wears out the disk and drive
|
||||
5. Use `DoIO()` (synchronous) for simple operations; `SendIO()` only when overlapping disk I/O with computation
|
||||
6. Handle `TDERR_DiskChange` errors — always verify the disk hasn't been swapped mid-operation
|
||||
7. For disk-to-disk copy, read a full track then write a full track — not sector by sector
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Fast RAM Buffer" — DMA-Corrupted Reads
|
||||
|
||||
```c
|
||||
/* BAD: AllocMem without MEMF_CHIP returns Fast RAM on expanded systems */
|
||||
UBYTE *buf = AllocMem(512, 0); /* could be Fast RAM */
|
||||
diskReq->iotd_Req.io_Data = buf;
|
||||
diskReq->iotd_Req.io_Command = CMD_READ;
|
||||
DoIO((struct IORequest *)diskReq);
|
||||
/* DMA writes to Chip RAM, but buf points to Fast RAM — silent garbage! */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Always use MEMF_CHIP */
|
||||
UBYTE *buf = AllocMem(512, MEMF_CHIP | MEMF_CLEAR);
|
||||
```
|
||||
|
||||
### "The Motor Hog" — Wearing Out the Drive
|
||||
|
||||
```c
|
||||
/* BAD: Motor left running forever */
|
||||
diskReq->iotd_Req.io_Command = TD_MOTOR;
|
||||
diskReq->iotd_Req.io_Length = 1; /* motor on */
|
||||
DoIO((struct IORequest *)diskReq);
|
||||
/* ... read disk ... */
|
||||
/* ... forget to turn motor off ... */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Turn off motor after use */
|
||||
/* ... after all disk operations ... */
|
||||
diskReq->iotd_Req.io_Command = TD_MOTOR;
|
||||
diskReq->iotd_Req.io_Length = 0; /* motor off */
|
||||
DoIO((struct IORequest *)diskReq);
|
||||
```
|
||||
|
||||
### "The Stale Disk" — Ignoring Disk Changes
|
||||
|
||||
```c
|
||||
/* BAD: Assumes the same disk is still in the drive */
|
||||
/* User swaps disk between reads — data is from wrong disk! */
|
||||
ULONG oldCount = diskReq->iotd_Req.io_Actual;
|
||||
/* ... much later ... */
|
||||
ReadSector(buf, 0);
|
||||
/* No check if disk was changed */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Check change counter before each operation */
|
||||
diskReq->iotd_Req.io_Command = TD_CHANGENUM;
|
||||
DoIO((struct IORequest *)diskReq);
|
||||
if (diskReq->iotd_Req.io_Actual != oldCount) {
|
||||
/* Disk was changed — re-read disk info */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Forgetting CMD_UPDATE After Writes
|
||||
|
||||
**Symptom:** Written data disappears after reboot.
|
||||
|
||||
**Cause:** trackdisk.device buffers writes internally. `CMD_WRITE` stores data in the device's track buffer, but does not flush to physical disk until `CMD_UPDATE` is issued.
|
||||
|
||||
**Fix:**
|
||||
```c
|
||||
diskReq->iotd_Req.io_Command = CMD_WRITE;
|
||||
DoIO((struct IORequest *)diskReq);
|
||||
|
||||
/* MUST call CMD_UPDATE to flush: */
|
||||
diskReq->iotd_Req.io_Command = CMD_UPDATE;
|
||||
DoIO((struct IORequest *)diskReq);
|
||||
```
|
||||
|
||||
### 2. Wrong Byte Offset Calculation
|
||||
|
||||
**Symptom:** Reading wrong sectors, corrupt data.
|
||||
|
||||
**Cause:** The `io_Offset` field uses **byte offsets from disk start**, not track/sector numbers.
|
||||
|
||||
**Fix:** Calculate offset correctly:
|
||||
```c
|
||||
/* Linear byte offset for track T, side S, sector SEC: */
|
||||
ULONG offset = ((T * 2 + S) * 11 + SEC) * 512;
|
||||
/* Example: Track 5, side 1, sector 3 = ((5*2+1)*11 + 3) * 512 = 63,488 */
|
||||
```
|
||||
|
||||
### 3. Unit Number Out of Range
|
||||
|
||||
**Symptom:** `OpenDevice()` succeeds but operations fail or access wrong drive.
|
||||
|
||||
**Cause:** Unit numbers are DF0:=0, DF1:=1, DF2:=2, DF3:=3. Higher numbers are invalid.
|
||||
|
||||
**Fix:** Always validate the unit number and handle `OpenDevice()` errors.
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Use Case | Approach | Notes |
|
||||
|----------|----------|-------|
|
||||
| Loading files | Use [dos.library](../07_dos/file_io.md) | Never use trackdisk directly |
|
||||
| Bootblock installation | `CMD_WRITE` sector 0 | Must be Chip RAM buffer |
|
||||
| Disk duplication | Read full track, write full track | Track-at-once is faster than sector-at-once |
|
||||
| Copy protection detection | `TD_RAWREAD` (OS 2.0+) or raw DMA | Analyze non-standard sync words |
|
||||
| Disk format tool | `TD_FORMAT` | Writes MFM sector headers + data |
|
||||
| Non-Amiga disk (PC/ST) | `TD_RAWREAD` + custom MFM decode | PC uses different sector format |
|
||||
| Write-protect detection | `TD_PROTSTATUS` | Returns 0=writable, 1=protected |
|
||||
|
||||
---
|
||||
|
||||
## Impact on FPGA / Emulation
|
||||
|
||||
The floppy controller is one of the trickiest subsystems to emulate accurately:
|
||||
|
||||
| Aspect | FPGA/Emulation Challenge |
|
||||
|--------|--------------------------|
|
||||
| **Raw MFM DMA** | Must transfer the complete raw MFM bitstream to/from memory, not decoded data |
|
||||
| **Whole-track caching** | trackdisk.device caches full tracks — emulation must preserve this for correct seek timing |
|
||||
| **CIA-B floppy control** | Motor, step, side select via CIA-B PRA/PRB registers — must be cycle-accurate |
|
||||
| **Sync word detection** | Hardware searches for `$4489` sync in the MFM stream — must handle missing-clock encoding correctly |
|
||||
| **Copy protection** | Non-standard sync words, weak bits, extra-long tracks — many games depend on exact timing |
|
||||
| **Write-speed mismatch** | MFM encoding runs at 250 kbit/s regardless of CPU speed — emulation must throttle DMA |
|
||||
| **TrackMotor delay** | Motor spin-up takes ~500 ms — games check this delay for protection |
|
||||
|
||||
> **Practical impact**: The MiSTer Amiga (Minimig) core emulates the floppy controller at the MFM level, reading ADF images and reconstructing the raw MFM bitstream. This is why copy-protected disks require special formats (IPF/CTRaw) that preserve the exact MFM encoding.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Why is the Amiga floppy format different from PC?**
|
||||
A: The Amiga uses a unique sector format with `$4489` sync words and XOR checksums, whereas PCs use IBM MFM format with `$A1` address marks and CRC-16. The Amiga controller is more flexible because it processes raw MFM — the PC's uPD765 controller decodes MFM in hardware.
|
||||
|
||||
**Q: Can I read PC disks on an Amiga?**
|
||||
A: Yes — the Amiga's raw MFM controller can read any MFM format. The `CrossDOS` filesystem (built into AmigaOS 2.1+) decodes PC MFM sectors. Hardware-wise, the drive mechanism is identical to PC double-density drives.
|
||||
|
||||
**Q: Why does a full disk copy take so long?**
|
||||
A: 80 tracks × 2 sides × 200 ms/revolution = 32 seconds minimum (one revolution per track). With seek time (~3 ms per step) and motor spin-up (~500 ms), a full 880 KB copy takes 45–90 seconds in practice.
|
||||
|
||||
**Q: What is the difference between ADF and IPF?**
|
||||
A: ADF stores decoded sector data (880 KB). IPF stores the raw MFM bitstream with timing information, preserving copy protection, weak bits, and non-standard formatting. See [custom_loaders_and_drm.md](../05_reversing/custom_loaders_and_drm.md).
|
||||
|
||||
**Q: What happens if I use Fast RAM for a DMA buffer?**
|
||||
A: The DMA engine writes to the physical Chip RAM address. If your pointer is in Fast RAM, the DMA data goes to Chip RAM (wrong location) while your code reads from Fast RAM (stale or uninitialized data). The result is silent corruption — no error is reported.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,36 +1,27 @@
|
|||
[← Home](../README.md) · [Libraries](README.md)
|
||||
|
||||
# layers.library — Window Clipping and Damage Repair
|
||||
# layers.library — Window Clipping, Damage Repair, and Layer Management
|
||||
|
||||
## Overview
|
||||
|
||||
`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.
|
||||
`layers.library` is the invisible engine behind every Intuition window. It manages overlapping rectangular regions on a shared `BitMap`, clips drawing operations to visible areas, tracks damage when windows move or resize, and optionally restores obscured content from backing store. Intuition builds its entire windowing system on top of layers — every window is a layer, and every layer is a linked list of `ClipRect` structures.
|
||||
|
||||
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.
|
||||
The library is small (~14 KB in ROM, 60th library initialized at boot) but its impact is enormous. Without layers, there would be no overlapping windows, no drag-to-reveal, no multi-tasking GUI on a 7 MHz 68000 with 512 KB of RAM.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Screen BitMap"
|
||||
subgraph "Layer A (front)"
|
||||
LA["Fully visible<br/>ClipRect covers<br/>entire layer"]
|
||||
end
|
||||
subgraph "Layer B (middle)"
|
||||
LB["Partially obscured<br/>by Layer A"]
|
||||
LB1["ClipRect 1<br/>(visible)"]
|
||||
LB2["ClipRect 2<br/>(obscured)"]
|
||||
end
|
||||
subgraph "Layer C (back)"
|
||||
LC["Mostly obscured"]
|
||||
end
|
||||
end
|
||||
APP["Application Code"] -->|"Draw via RastPort"| GFX["graphics.library"]
|
||||
GFX -->|"rp->Layer != NULL?"| LAY["layers.library"]
|
||||
LAY -->|"Clip to visible ClipRects"| BM["Screen BitMap"]
|
||||
LAY -->|"Obscured regions → backing store"| BS["Backing Store BitMap"]
|
||||
INT["Intuition"] -->|"Move/Size/Depth-arrange"| LAY
|
||||
LAY -->|"DamageList updated"| DMG["Damage Region"]
|
||||
DMG -->|"IDCMP_REFRESHWINDOW"| APP
|
||||
|
||||
DRAW["Draw into Layer B"] --> CLIP["layers.library<br/>clips to visible<br/>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
|
||||
style LAY fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style BM fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style BS fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style DMG fill:#ffcdd2,stroke:#c62828,color:#333
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -38,120 +29,304 @@ flowchart TD
|
|||
## Layer Types
|
||||
|
||||
| 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 | — |
|
||||
| `LAYERSMART` | Smart Refresh | Auto — obscured regions saved/restored | Automatic — OS handles damage (except resize) | Moderate |
|
||||
| `LAYERSUPER` | Super BitMap | Full off-screen bitmap (app provides) | Full bitmap always valid — OS restores automatically | High |
|
||||
| `LAYERBACKDROP` | Backdrop | Modifier — behind all normal layers | Depends on base refresh type | — |
|
||||
|
||||
> [!WARNING]
|
||||
> The three layer-type flags (`LAYERSIMPLE`, `LAYERSMART`, `LAYERSUPER`) are **mutually exclusive**. Only one may be specified per layer. `LAYERBACKDROP` is a modifier that can be OR'd with any of them.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "Simple Refresh"
|
||||
S1["Window obscured"] --> S2["Damage region<br/>recorded"]
|
||||
S2 --> S3["IDCMP_REFRESHWINDOW<br/>sent to app"]
|
||||
S3 --> S4["App redraws<br/>between Begin/EndRefresh"]
|
||||
S1["Window obscured"] --> S2["Damage region\nrecorded"]
|
||||
S2 --> S3["IDCMP_REFRESHWINDOW\nsent to app"]
|
||||
S3 --> S4["App redraws\nbetween Begin/EndRefresh"]
|
||||
end
|
||||
|
||||
subgraph "Smart Refresh"
|
||||
M1["Window obscured"] --> M2["Obscured area<br/>saved to buffer"]
|
||||
M2 --> M3["Window revealed"] --> M4["OS restores<br/>from buffer"]
|
||||
M1["Window obscured"] --> M2["Obscured area\nsaved to buffer"]
|
||||
M2 --> M3["Window revealed"] --> M4["OS restores\nfrom buffer\nautomatically"]
|
||||
end
|
||||
|
||||
subgraph "Super BitMap"
|
||||
U1["Full off-screen bitmap\nalways valid"] --> U2["Any reveal →\nOS blits from SuperBitMap"]
|
||||
U2 --> U3["No app refresh\nneeded ever"]
|
||||
end
|
||||
|
||||
style S3 fill:#ffcdd2,stroke:#c62828,color:#333
|
||||
style M4 fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style U3 fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
```
|
||||
|
||||
### Refresh Type Decision Guide
|
||||
|
||||
| Scenario | Recommended Type | Rationale |
|
||||
|----------|-----------------|-----------|
|
||||
| Standard Intuition window (menus, gadgets) | `LAYERSMART` | OS handles most damage automatically |
|
||||
| Custom display with full content regeneration | `LAYERSIMPLE` | Lowest memory — app redraws everything anyway |
|
||||
| Scrollable viewport (maps, documents) | `LAYERSUPER` | Full off-screen buffer enables ScrollLayer() without damage |
|
||||
| Backdrop window (WB pattern) | `LAYERSIMPLE \| LAYERBACKDROP` | Behind everything, rarely damaged |
|
||||
| Memory-constrained game | `LAYERSIMPLE` | Zero backing-store overhead |
|
||||
| Status bar / clock on screen | `LAYERSIMPLE \| LAYERBACKDROP` | Minimal content, easy to redraw |
|
||||
|
||||
---
|
||||
|
||||
## ClipRects — The Clipping Engine
|
||||
## Core Data Structures
|
||||
|
||||
Each layer maintains a linked list of **ClipRects** — rectangles that define how each region of the layer should be handled:
|
||||
### struct Layer
|
||||
|
||||
```c
|
||||
/* graphics/clip.h — NDK39 */
|
||||
struct Layer {
|
||||
struct Layer *front; /* next layer towards front (higher priority) */
|
||||
struct Layer *back; /* next layer towards back */
|
||||
struct ClipRect *ClipRect; /* head of ClipRect list for this layer */
|
||||
struct RastPort *rp; /* RastPort for drawing into this layer */
|
||||
struct Rectangle bounds; /* layer bounds (x1,y1,x2,y2) in screen coords */
|
||||
/* ... internal fields ... */
|
||||
UWORD Flags; /* LAYERSIMPLE/LAYERSMART/LAYERSUPER/LAYERBACKDROP */
|
||||
struct BitMap *SuperBitMap; /* non-NULL for LAYERSUPER */
|
||||
/* ... internal fields ... */
|
||||
struct Region *DamageList; /* damaged rectangles needing refresh */
|
||||
/* ... more internal fields ... */
|
||||
};
|
||||
```
|
||||
|
||||
**Readable fields** (read-only — never modify directly):
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `front`, `back` | Doubly-linked layer stack — front = higher priority (drawn on top) |
|
||||
| `ClipRect` | Head of the clipping rectangle list — graphics.library walks this |
|
||||
| `rp` | The RastPort for drawing — coordinates are relative to the layer's top-left |
|
||||
| `bounds` | Absolute screen coordinates of the layer |
|
||||
| `Flags` | Layer type + state bits (`LAYERREFRESH`, `LAYERUPDATING`, etc.) |
|
||||
| `SuperBitMap` | The app-provided off-screen bitmap (`LAYERSUPER` only) |
|
||||
| `DamageList` | Region list of rectangles needing redraw |
|
||||
|
||||
### struct ClipRect
|
||||
|
||||
```c
|
||||
/* graphics/clip.h — NDK39 */
|
||||
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) */
|
||||
struct Layer *lobs; /* layer that obscures this rect (NULL = visible) */
|
||||
struct BitMap *BitMap; /* backing store bitmap (Smart refresh, obscured only) */
|
||||
LONG reserved;
|
||||
LONG Flags;
|
||||
LONG Flags; /* CR_NEEDS_NO_CONCEALED_RASTERS, etc. */
|
||||
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`
|
||||
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
|
||||
3. For each **visible ClipRect** (`lobs == NULL`), drawing is performed clipped to that rectangle
|
||||
4. For **obscured ClipRects** (`lobs != NULL`, Smart refresh), drawing goes to the backing-store bitmap instead
|
||||
5. The application sees nothing — clipping is fully transparent
|
||||
|
||||
### struct Layer_Info
|
||||
|
||||
```c
|
||||
/* graphics/layers.h — NDK39 */
|
||||
struct Layer_Info {
|
||||
struct Layer *top_layer; /* frontmost layer */
|
||||
struct Layer *check_layer; /* layer being examined (internal) */
|
||||
struct ClipRect *obs; /* accumulation of obscured rectangles (internal) */
|
||||
struct Region *damage_list; /* layers with damage (internal) */
|
||||
struct RastPort *blank_hook; /* backfill RastPort (internal) */
|
||||
struct Layer *save_layer; /* layer saved for BeginRefresh (internal) */
|
||||
UWORD Flags; /* LIFLG_SUPPORT_IOFFERS, etc. */
|
||||
BOOL fatten_count; /* nesting level of FattenLayerInfo() */
|
||||
struct SignalSemaphore Lock; /* V39: semaphore for intertask safety */
|
||||
/* ... more fields ... */
|
||||
};
|
||||
```
|
||||
|
||||
Every display shared by multiple layers requires **one** `Layer_Info`. Intuition screens already have one at `screen->LayerInfo`. For custom displays, allocate with `NewLayerInfo()`.
|
||||
|
||||
---
|
||||
|
||||
## Creating Layers Directly
|
||||
|
||||
While Intuition normally creates layers for windows, you can create them manually for custom display systems:
|
||||
> [!WARNING]
|
||||
> **Never create layers on an Intuition screen directly.** Intuition windows are the only supported method of adding layers to Intuition screens. Only the locking/unlocking functions are safe to use with Intuition-managed layers. Create your own `View` if you need direct layer control.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Q{"Need layers?"} -->|"Using Intuition?"| INT["Use OpenWindow()\nIntuition creates the layer"]
|
||||
Q -->|"Custom display?"| CUSTOM["Create your own View + BitMap"]
|
||||
CUSTOM --> LI["NewLayerInfo()"]
|
||||
LI --> CL["CreateUpfrontLayer() /\nCreateBehindLayer()"]
|
||||
CL --> DRAW["Draw via layer->rp"]
|
||||
DRAW --> DL["DeleteLayer() + DisposeLayerInfo()"]
|
||||
|
||||
INT -->|"Never call layers directly"| X["X"]
|
||||
|
||||
style INT fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style CUSTOM fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style X fill:#ffcdd2,stroke:#c62828,color:#333
|
||||
```
|
||||
|
||||
### Full Lifecycle Example
|
||||
|
||||
```c
|
||||
struct Layer_Info *li = NewLayerInfo();
|
||||
#include <proto/exec.h>
|
||||
#include <proto/graphics.h>
|
||||
#include <proto/layers.h>
|
||||
#include <graphics/gfx.h>
|
||||
#include <graphics/gfxbase.h>
|
||||
#include <graphics/layers.h>
|
||||
#include <graphics/clip.h>
|
||||
#include <graphics/rastport.h>
|
||||
#include <graphics/view.h>
|
||||
|
||||
/* 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) */
|
||||
void custom_layer_demo(void)
|
||||
{
|
||||
struct BitMap screenBM;
|
||||
struct View view;
|
||||
struct ViewPort vport;
|
||||
|
||||
/* Create behind existing layers: */
|
||||
struct Layer *backLayer = CreateBehindLayer(li, screenBitMap,
|
||||
50, 50, 250, 150,
|
||||
LAYERSIMPLE | LAYERBACKDROP,
|
||||
NULL);
|
||||
/* 1. Set up your own View + ViewPort + BitMap (not shown —
|
||||
see graphics.library View system docs for full setup) */
|
||||
InitBitMap(&screenBM, 4, 640, 256); /* 4 bitplanes, 640x256 */
|
||||
for (int i = 0; i < 4; i++)
|
||||
screenBM.Planes[i] = AllocRaster(640, 256);
|
||||
|
||||
/* Get the RastPort for drawing: */
|
||||
struct RastPort *rp = frontLayer->rp;
|
||||
SetAPen(rp, 1);
|
||||
RectFill(rp, 0, 0, 190, 90); /* coordinates relative to layer */
|
||||
/* 2. Allocate Layer_Info */
|
||||
struct Layer_Info *li = NewLayerInfo();
|
||||
if (!li) goto cleanup_bitmap;
|
||||
|
||||
/* 3. Create layers */
|
||||
struct Layer *front = CreateUpfrontLayer(li, &screenBM,
|
||||
10, 10, 300, 200,
|
||||
LAYERSMART, NULL);
|
||||
|
||||
struct Layer *back = CreateBehindLayer(li, &screenBM,
|
||||
50, 50, 350, 250,
|
||||
LAYERSIMPLE, NULL);
|
||||
|
||||
if (!front || !back) goto cleanup_layers;
|
||||
|
||||
/* 4. Draw into front layer */
|
||||
struct RastPort *rp = front->rp;
|
||||
SetAPen(rp, 1);
|
||||
SetDrMd(rp, JAM2);
|
||||
RectFill(rp, 0, 0, 290, 190); /* coords relative to layer */
|
||||
|
||||
/* 5. Move front layer — back layer takes damage */
|
||||
MoveLayer(0, front, 50, 30);
|
||||
|
||||
/* 6. Check back layer for damage */
|
||||
if (back->Flags & LAYERREFRESH) {
|
||||
BeginRefresh(back->rp); /* sets ClipRects to damaged regions only */
|
||||
SetAPen(back->rp, 2);
|
||||
RectFill(back->rp, 0, 0, 9999, 9999); /* fills damaged area */
|
||||
EndRefresh(back->rp, TRUE);
|
||||
}
|
||||
|
||||
cleanup_layers:
|
||||
/* 7. Delete layers in reverse order (back first, then front) */
|
||||
if (back) DeleteLayer(0, back);
|
||||
if (front) DeleteLayer(0, front);
|
||||
DisposeLayerInfo(li);
|
||||
|
||||
cleanup_bitmap:
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (screenBM.Planes[i])
|
||||
FreeRaster(screenBM.Planes[i], 640, 256);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer Operations
|
||||
|
||||
### Locking
|
||||
### API Reference
|
||||
|
||||
#### Creating and Destroying
|
||||
|
||||
| Function | LVO | Purpose |
|
||||
|----------|-----|---------|
|
||||
| `CreateUpfrontLayer()` | -30 | Create layer in front of all others |
|
||||
| `CreateBehindLayer()` | -36 | Create layer behind all others |
|
||||
| `DeleteLayer()` | -42 | Remove a layer and free its resources |
|
||||
| `NewLayerInfo()` | -66 | Allocate a new `Layer_Info` structure |
|
||||
| `DisposeLayerInfo()` | -72 | Free a `Layer_Info` |
|
||||
| `FattenLayerInfo()` | -78 | Allocate internal pools for clip regions |
|
||||
| `ThinLayerInfo()` | -84 | Release pools allocated by FattenLayerInfo |
|
||||
|
||||
#### Moving, Sizing, Reordering
|
||||
|
||||
| Function | LVO | Purpose |
|
||||
|----------|-----|---------|
|
||||
| `MoveLayer()` | -48 | Move layer by delta (dx, dy) |
|
||||
| `SizeLayer()` | -54 | Resize layer by delta (dw, dh) |
|
||||
| `UpfrontLayer()` | -24 | Move layer to front of stack |
|
||||
| `BehindLayer()` | -18 | Move layer behind all others |
|
||||
| `MoveLayerInFrontOf()` | -90 | Move layer to specific position in stack |
|
||||
| `ScrollLayer()` | -60 | Scroll viewport within a Super BitMap layer |
|
||||
|
||||
#### Locking
|
||||
|
||||
| Function | LVO | Purpose |
|
||||
|----------|-----|---------|
|
||||
| `LockLayer()` | -6 | Lock a single layer for exclusive access |
|
||||
| `UnlockLayer()` | -12 | Unlock a previously locked layer |
|
||||
| `LockLayers()` | -96 | Lock all layers in the `Layer_Info` |
|
||||
| `UnlockLayers()` | -102 | Unlock all layers |
|
||||
| `LockLayerInfo()` | -108 | Lock the `Layer_Info` for layer stack operations |
|
||||
| `UnlockLayerInfo()` | -114 | Unlock the `Layer_Info` |
|
||||
|
||||
#### Damage and Refresh
|
||||
|
||||
| Function | LVO | Purpose |
|
||||
|----------|-----|---------|
|
||||
| `BeginRefresh()` | -126 | Set ClipRects to damaged regions only |
|
||||
| `EndRefresh()` | -132 | Complete refresh, clear damage list |
|
||||
| `InstallLayerHook()` | -138 | Set custom backfill hook for a layer |
|
||||
| `InstallClipRegion()` | -150 | Install a graphics Region for additional clipping |
|
||||
|
||||
### Locking Strategy
|
||||
|
||||
**Always lock a layer before drawing** if other tasks might modify it simultaneously:
|
||||
|
||||
```c
|
||||
/* Lock a single layer for drawing: */
|
||||
LockLayer(0, layer);
|
||||
/* ... safe to draw ... */
|
||||
UnlockLayer(layer);
|
||||
|
||||
/* Lock ALL layers (for bulk operations): */
|
||||
/* Lock ALL layers (for bulk operations like screen capture): */
|
||||
LockLayers(layerInfo);
|
||||
/* ... no layer can move, resize, or be deleted ... */
|
||||
UnlockLayers(layerInfo);
|
||||
|
||||
/* Lock a layer for multiple operations: */
|
||||
/* Lock the Layer_Info itself (for stack manipulation): */
|
||||
LockLayerInfo(layerInfo);
|
||||
/* ... manipulate layer stack ... */
|
||||
/* ... UpfrontLayer, BehindLayer, MoveLayerInFrontOf ... */
|
||||
UnlockLayerInfo(layerInfo);
|
||||
```
|
||||
|
||||
### Moving and Resizing
|
||||
```mermaid
|
||||
flowchart TD
|
||||
NEED{"What are you doing?"} -->|"Drawing into a layer"| SINGLE["LockLayer() / UnlockLayer()"]
|
||||
NEED -->|"Reordering layers"| INFO["LockLayerInfo() / UnlockLayerInfo()"]
|
||||
NEED -->|"Screen capture,\nbulk operation"| ALL["LockLayers() / UnlockLayers()"]
|
||||
|
||||
```c
|
||||
/* Move layer by delta: */
|
||||
MoveLayer(0, layer, dx, dy);
|
||||
|
||||
/* Resize layer by delta: */
|
||||
SizeLayer(0, layer, dw, dh);
|
||||
|
||||
/* Move to front/back of stack: */
|
||||
UpfrontLayer(0, layer);
|
||||
BehindLayer(0, layer);
|
||||
style SINGLE fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
style INFO fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style ALL fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
```
|
||||
|
||||
### Damage and Refresh
|
||||
|
||||
When a layer operation reveals previously obscured areas, the newly exposed region is **damaged** and needs repair:
|
||||
|
||||
```c
|
||||
/* For Simple Refresh windows — handle IDCMP_REFRESHWINDOW: */
|
||||
BeginRefresh(window);
|
||||
|
|
@ -160,18 +335,60 @@ BeginRefresh(window);
|
|||
EndRefresh(window, TRUE); /* TRUE = damage fully repaired */
|
||||
```
|
||||
|
||||
### Install Backfill Hook
|
||||
The `BeginRefresh()` / `EndRefresh()` pair is critical:
|
||||
1. `BeginRefresh()` restructures the ClipRect list to cover **only damaged rectangles** — your redraw only touches areas that actually need repair
|
||||
2. During refresh, other tasks cannot modify the layer (it is implicitly locked)
|
||||
3. `EndRefresh(TRUE)` clears the damage list and restores normal ClipRects
|
||||
4. `EndRefresh(FALSE)` means "I didn't finish" — damage remains for the next refresh cycle
|
||||
|
||||
---
|
||||
|
||||
## Backfill Hooks
|
||||
|
||||
When a region of a layer is exposed (e.g., another window moves away), the exposed area must be **filled** before the application redraws content. By default, layers clears the area to pen 0 (background color). A custom backfill hook lets you fill with a pattern, gradient, or bitmap instead.
|
||||
|
||||
```c
|
||||
/* Custom backfill instead of default (clear to pen 0): */
|
||||
/* Custom backfill hook — fills exposed areas with a tiled pattern */
|
||||
LONG ASM MyBackfillFunc(
|
||||
REG(a0, struct Hook *hook),
|
||||
REG(a1, struct Layer *layer),
|
||||
REG(a2, struct RastPort *rp),
|
||||
REG(a3, struct BackFillMsg *msg)
|
||||
{
|
||||
/* msg->Bounds contains the rectangle to fill (layer-relative) */
|
||||
/* msg->Layer is the layer being refilled */
|
||||
/* rp is a special RastPort for backfill — use it, not layer->rp */
|
||||
|
||||
if (msg->OffsetX == 0 && msg->OffsetY == 0) {
|
||||
/* Full layer clear — use simple fill */
|
||||
SetAPen(rp, 0);
|
||||
RectFill(rp,
|
||||
msg->Bounds.MinX, msg->Bounds.MinY,
|
||||
msg->Bounds.MaxX, msg->Bounds.MaxY);
|
||||
} else {
|
||||
/* Partial exposure — tile a pattern at the offset */
|
||||
SetAPen(rp, 1);
|
||||
SetDrMd(rp, JAM2);
|
||||
RectFill(rp,
|
||||
msg->Bounds.MinX, msg->Bounds.MinY,
|
||||
msg->Bounds.MaxX, msg->Bounds.MaxY);
|
||||
}
|
||||
|
||||
return 0; /* always return 0 */
|
||||
}
|
||||
|
||||
/* Installation: */
|
||||
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) */
|
||||
/* To restore default backfill: */
|
||||
InstallLayerHook(layer, LAYERS_BACKFILL); /* NULL also works */
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> The backfill hook receives a **special RastPort** (parameter `a2`) — not the layer's normal `rp`. Always use this RastPort for drawing inside the hook. Drawing to the layer's normal `rp` from a backfill hook corrupts the ClipRect state.
|
||||
|
||||
---
|
||||
|
||||
## Super BitMap Layers
|
||||
|
|
@ -199,26 +416,324 @@ SyncSBitMap(superLayer);
|
|||
CopySBitMap(superLayer);
|
||||
```
|
||||
|
||||
### Super BitMap Use Cases
|
||||
|
||||
| Use Case | Why Super BitMap | Example |
|
||||
|----------|-----------------|---------|
|
||||
| Scrolling map viewer | Smooth scroll without redraw penalty | Strategy games (Cannon Fodder map) |
|
||||
| Large document viewer | Full content always in memory | Text editors, image viewers |
|
||||
| Virtual desktop | Screen shows a viewport into a larger space | DTP software (PageStream) |
|
||||
| Undo buffer | Keep previous state in off-screen bitmap | Drawing programs (Deluxe Paint) |
|
||||
|
||||
---
|
||||
|
||||
## Cleanup
|
||||
## Optimized Refresh Techniques
|
||||
|
||||
### ClipBlit vs ScrollRaster for Scrolling
|
||||
|
||||
For scrolling content within a Smart Refresh layer, `ClipBlit()` provides better CPU/Blitter overlap than `ScrollRaster()`:
|
||||
|
||||
| Operation | `ScrollRaster()` | `ClipBlit()` |
|
||||
|-----------|------------------|-------------|
|
||||
| Blitter ops | 2: scroll + clear | 1: scroll only |
|
||||
| CPU overlap | Waits for blit to finish | Returns while blit runs |
|
||||
| Damage | Generates damage in cleared region | No damage (no clear) |
|
||||
| Memory | No extra allocation | No extra allocation |
|
||||
|
||||
```c
|
||||
/* Remove layers in reverse order: */
|
||||
DeleteLayer(0, layer);
|
||||
/* BAD: ScrollRaster waits for blit, then clears — two blits */
|
||||
ScrollRaster(rp, 0, -16, 0, 0, 639, 255);
|
||||
/* Must also handle IDCMP_REFRESHWINDOW for the cleared strip */
|
||||
|
||||
/* Dispose the layer info: */
|
||||
DisposeLayerInfo(li);
|
||||
|
||||
/* If using super bitmap, free it after DeleteLayer: */
|
||||
FreeBitMap(superBM);
|
||||
/* BETTER: ClipBlit overlaps with CPU — one blit, no damage */
|
||||
ClipBlit(rp, 0, 16, rp, 0, 0, 640, 240, 0xC0);
|
||||
/* Now fill the bottom 16 lines with new content */
|
||||
SetAPen(rp, 0);
|
||||
RectFill(rp, 0, 240, 639, 255);
|
||||
```
|
||||
|
||||
### Using Multiple RastPorts
|
||||
|
||||
For complex windows with multiple drawing regions, allocate separate `RastPort` structures that share the same `BitMap`. This avoids repeated `SetAPen`/`SetDrMd` calls when switching between regions:
|
||||
|
||||
```c
|
||||
struct RastPort headerRP, contentRP, statusRP;
|
||||
|
||||
InitRastPort(&headerRP);
|
||||
InitRastPort(&contentRP);
|
||||
InitRastPort(&statusRP);
|
||||
|
||||
/* All share the window's bitmap but have independent pens/modes */
|
||||
headerRP.BitMap = window->RPort->BitMap;
|
||||
contentRP.BitMap = window->RPort->BitMap;
|
||||
statusRP.BitMap = window->RPort->BitMap;
|
||||
|
||||
/* Set up once: */
|
||||
SetAPen(&headerRP, 2); SetDrMd(&headerRP, JAM2);
|
||||
SetAPen(&contentRP, 1); SetDrMd(&contentRP, JAM1);
|
||||
SetAPen(&statusRP, 3); SetDrMd(&statusRP, JAM2);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Historical Context & Modern Analogies
|
||||
|
||||
### Amiga Layers vs. Contemporaries
|
||||
|
||||
| Platform | Window Clipping | Backing Store | Damage Tracking | Overlapping Windows |
|
||||
|----------|----------------|---------------|-----------------|---------------------|
|
||||
| **Amiga layers.library** | ClipRect linked list | Smart refresh auto-saves | `DamageList` + `LAYERREFRESH` bit | Yes, hardware-agnostic |
|
||||
| **Mac OS (Classic)** | Region-based clipping | Off-screen port (manual) | Window Manager event | Yes |
|
||||
| **Atari ST GEM** | Rectangle clipping only | None | Full redraw on expose | Limited (not truly overlapping) |
|
||||
| **Windows 1.x–3.x** | Region clipping | None (3.x added PATBLT) | `WM_PAINT` messages | Tiled (1.x), overlapping (2.x+) |
|
||||
| **X11** | Server-side region clipping | Backing store (optional) | Expose events | Yes |
|
||||
|
||||
The Amiga's approach was notable for its **automatic backing store** (Smart refresh) at a time when most platforms required the application to handle all redraw. This came at a memory cost — significant on a 512 KB machine.
|
||||
|
||||
### Modern Analogies
|
||||
|
||||
| Amiga Concept | Modern Equivalent | Notes |
|
||||
|--------------|-------------------|-------|
|
||||
| `Layer` | compositor surface (Wayland `wl_surface`, macOS `CALayer`) | Amiga layers are region-clipped; modern compositors texture-sample |
|
||||
| `ClipRect` | damage region (Wayland `wl_surface.damage`) | Same concept: track what needs redraw |
|
||||
| `LAYERSMART` backing store | Wayland compositor shadow buffer | OS saves obscured content automatically |
|
||||
| `LAYERSUPER` | macOS `NSBitmapImageRep` + `NSImageView` | Full off-screen buffer, viewport scrolls through it |
|
||||
| `BeginRefresh` / `EndRefresh` | `drawRect:` (macOS) / `WM_PAINT` (Windows) | Scope the damage region for targeted redraw |
|
||||
| `LAYERBACKDROP` | desktop wallpaper layer | Always behind normal windows |
|
||||
| `InstallLayerHook` | `drawsBackground` delegate (macOS/iOS) | Customize how exposed regions are filled |
|
||||
|
||||
The key difference: modern compositors use **full-screen textures** and a GPU to composite. Amiga layers use **region math** and the Blitter — no GPU, no texture memory, just rectangle algebra on a single framebuffer.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `LAYERSMART` for Intuition windows** — the OS handles most damage automatically, and the memory cost is modest
|
||||
2. **Always lock before drawing** — `LockLayer()` prevents other tasks from moving your window mid-draw
|
||||
3. **Use `BeginRefresh()` / `EndRefresh()` for damage repair** — never redraw outside this pair
|
||||
4. **Pass `TRUE` to `EndRefresh()`** unless you genuinely couldn't complete the redraw
|
||||
5. **Use `ClipBlit()` over `ScrollRaster()`** for scrolling — better CPU/Blitter overlap
|
||||
6. **Never modify `Layer` struct fields directly** — use the API functions
|
||||
7. **Delete layers in reverse creation order** — back layers first, front layers last
|
||||
8. **Don't hold layer locks across I/O or `Wait()` calls** — lock, draw, unlock
|
||||
9. **Use `InstallClipRegion()`** for complex non-rectangular clipping within a layer
|
||||
10. **Test with multiple overlapping windows** — damage bugs only appear when windows overlap
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Intuition Layer Creep" — Creating Layers on an Intuition Screen
|
||||
|
||||
```c
|
||||
/* BAD: Creating a raw layer on an Intuition screen's BitMap */
|
||||
struct Layer *bad = CreateUpfrontLayer(
|
||||
&screen->LayerInfo, /* Intuition owns this LayerInfo! */
|
||||
screen->RastPort.BitMap,
|
||||
10, 10, 200, 100,
|
||||
LAYERSMART, NULL);
|
||||
/* Undefined behavior — corrupts Intuition's layer tracking */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Use OpenWindow() for Intuition screens */
|
||||
struct Window *win = OpenWindowTags(NULL,
|
||||
WA_Left, 10,
|
||||
WA_Top, 10,
|
||||
WA_Width, 190,
|
||||
WA_Height, 90,
|
||||
WA_Flags, WFLG_SIZEGADGET | WFLG_DRAGBAR | WFLG_DEPTHGADGET,
|
||||
WA_IDCMP, IDCMP_REFRESHWINDOW,
|
||||
TAG_DONE);
|
||||
```
|
||||
|
||||
### "The Phantom Refresh" — Drawing Outside BeginRefresh/EndRefresh
|
||||
|
||||
```c
|
||||
/* BAD: Drawing directly on damage without BeginRefresh() —
|
||||
draws to ALL ClipRects (visible AND obscured), causing
|
||||
double-drawing and corruption */
|
||||
if (layer->Flags & LAYERREFRESH) {
|
||||
SetAPen(layer->rp, 1);
|
||||
RectFill(layer->rp, 0, 0, 9999, 9999); /* redraws EVERYTHING */
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: BeginRefresh() restricts ClipRects to damaged regions only */
|
||||
if (layer->Flags & LAYERREFRESH) {
|
||||
BeginRefresh(layer->rp);
|
||||
SetAPen(layer->rp, 1);
|
||||
RectFill(layer->rp, 0, 0, 9999, 9999); /* only touches damaged areas */
|
||||
EndRefresh(layer->rp, TRUE);
|
||||
}
|
||||
```
|
||||
|
||||
### "The Backfill RastPort Mix-Up" — Using the Wrong RastPort in a Hook
|
||||
|
||||
```c
|
||||
/* BAD: Using layer->rp inside a backfill hook — corrupts ClipRect state */
|
||||
LONG ASM MyBadBackfill(
|
||||
REG(a0, struct Hook *hook),
|
||||
REG(a1, struct Layer *layer),
|
||||
REG(a2, struct RastPort *rp), /* THIS is the correct RastPort */
|
||||
REG(a3, struct BackFillMsg *msg))
|
||||
{
|
||||
SetAPen(layer->rp, 1); /* WRONG! */
|
||||
RectFill(layer->rp, /* WRONG! */
|
||||
msg->Bounds.MinX, msg->Bounds.MinY,
|
||||
msg->Bounds.MaxX, msg->Bounds.MaxY);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Always use the RastPort passed as parameter a2 */
|
||||
LONG ASM MyGoodBackfill(
|
||||
REG(a0, struct Hook *hook),
|
||||
REG(a1, struct Layer *layer),
|
||||
REG(a2, struct RastPort *rp), /* use this one */
|
||||
REG(a3, struct BackFillMsg *msg))
|
||||
{
|
||||
SetAPen(rp, 1); /* CORRECT */
|
||||
RectFill(rp, /* CORRECT */
|
||||
msg->Bounds.MinX, msg->Bounds.MinY,
|
||||
msg->Bounds.MaxX, msg->Bounds.MaxY);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### "The Stale Lock" — Holding a Layer Lock Across Wait()
|
||||
|
||||
```c
|
||||
/* BAD: Locking a layer, then sleeping — other tasks are blocked */
|
||||
LockLayer(0, layer);
|
||||
DrawMyContent(layer->rp);
|
||||
Wait(SIGF_INTERRUPT); /* task sleeps while holding the lock! */
|
||||
UnlockLayer(layer); /* other tasks were frozen this whole time */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Lock, draw, unlock — then wait */
|
||||
LockLayer(0, layer);
|
||||
DrawMyContent(layer->rp);
|
||||
UnlockLayer(layer); /* release immediately */
|
||||
|
||||
Wait(SIGF_INTERRUPT); /* safe to sleep now */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Infinite Refresh Loop
|
||||
|
||||
**Symptom:** `IDCMP_REFRESHWINDOW` messages arrive endlessly; CPU at 100%.
|
||||
|
||||
**Cause:** Drawing outside the damaged region causes *new* damage, which triggers another `IDCMP_REFRESHWINDOW`, which causes more drawing, which causes more damage...
|
||||
|
||||
**Fix:** Only draw between `BeginRefresh()` and `EndRefresh()` — and always pass `TRUE` to `EndRefresh()` when done:
|
||||
|
||||
```c
|
||||
/* In the IDCMP event loop: */
|
||||
case IDCMP_REFRESHWINDOW:
|
||||
BeginRefresh(win);
|
||||
RedrawContent(win->RPort, win);
|
||||
EndRefresh(win, TRUE); /* must be TRUE to clear damage */
|
||||
break;
|
||||
```
|
||||
|
||||
### 2. LAYERREFRESH Bit Stays Set
|
||||
|
||||
**Symptom:** The window flickers or the refresh message keeps firing even after redrawing.
|
||||
|
||||
**Cause:** You called `EndRefresh()` with `FALSE` (indicating incomplete refresh) but never followed up with a second refresh pass. Or you drew directly without `BeginRefresh()` so the damage list was never cleared.
|
||||
|
||||
**Fix:** Always pair `BeginRefresh()` + `EndRefresh(TRUE)`. If you must use `EndRefresh(FALSE)`, schedule another refresh immediately.
|
||||
|
||||
### 3. Smart Refresh Resize Damage
|
||||
|
||||
**Symptom:** Garbage appears in the newly exposed area when a Smart Refresh window is made larger.
|
||||
|
||||
**Cause:** Smart refresh only saves **obscured** regions — when the window grows, the new area was never obscured, so there's no backing store to restore from. The `LAYERREFRESH` bit is set, and the app must fill the new area.
|
||||
|
||||
**Fix:** Check for `IDCMP_NEWSIZE` alongside `IDCMP_REFRESHWINDOW`:
|
||||
|
||||
```c
|
||||
case IDCMP_NEWSIZE:
|
||||
/* Window resized — new area needs drawing */
|
||||
RedrawContent(win->RPort, win);
|
||||
break;
|
||||
```
|
||||
|
||||
### 4. LayerInfo Deadlock with Intuition
|
||||
|
||||
**Symptom:** System hangs when opening a window.
|
||||
|
||||
**Cause:** You called `LockLayerInfo(&screen->LayerInfo)` before `OpenWindow()`. Intuition also locks the same `LayerInfo` internally, causing a deadlock.
|
||||
|
||||
**Fix:** Never lock Intuition's `LayerInfo` manually. See the warning in [screens.md](../09_intuition/screens.md).
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Software Type | Layer Usage | Refresh Type | Notes |
|
||||
|--------------|-------------|--------------|-------|
|
||||
| Workbench (OS) | One layer per window | `LAYERSMART` | Default for all WB windows |
|
||||
| Productivity apps (Final Writer, PageStream) | Multiple windows, some scrolling | `LAYERSMART` + `LAYERSUPER` for scroll views | Backfill hooks for custom backgrounds |
|
||||
| Games (strategy / GUI-based) | Custom layers for panels | `LAYERSIMPLE` | Games typically bypass Intuition entirely |
|
||||
| Terminal emulators | Scrolling text layer | `LAYERSMART` with `ClipBlit()` scroll | Backfill hook for background color |
|
||||
| Drawing programs (Deluxe Paint) | Canvas as Super BitMap | `LAYERSUPER` | Undo via off-screen buffer swap |
|
||||
| Screen grabbers | Lock all layers, read BitMap | Lock only | Must lock to get consistent snapshot |
|
||||
| Demo scene (GUI intros) | Overlapping effect regions | `LAYERSIMPLE` | Minimal overhead, full content regeneration |
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Do I need to open layers.library explicitly?**
|
||||
A: Intuition opens it automatically. If you call `NewLayerInfo()` or `CreateUpfrontLayer()` for a custom display, open it with `OpenLibrary("layers.library", 0)`.
|
||||
|
||||
**Q: What happens if I draw to a layer without locking it?**
|
||||
A: On a single-tasking system, nothing visible. In a multitasking environment, another task could move or resize the layer while you're mid-draw, causing tearing, partial draws, or corruption. Always lock.
|
||||
|
||||
**Q: Can I have more than one `Layer_Info` per display?**
|
||||
A: No — each display (each `BitMap` shared by layers) needs exactly one `Layer_Info`. Multiple `Layer_Info` structures would each track overlapping independently.
|
||||
|
||||
**Q: What's the performance cost of Smart refresh?**
|
||||
A: Each obscured ClipRect requires a backing-store `BitMap` allocation. For a typical Workbench window partially obscured by one other window, this costs ~10–20 KB. The Blitter handles the copy, so the CPU cost is minimal. The real cost is memory, not time.
|
||||
|
||||
**Q: When should I use `InstallClipRegion()` vs. `BeginRefresh()`?**
|
||||
A: `InstallClipRegion()` installs a **permanent** clipping region (like cutting a hole in your window). `BeginRefresh()` temporarily restricts drawing to **damage** regions only. They serve different purposes.
|
||||
|
||||
**Q: What does `FattenLayerInfo()` do?**
|
||||
A: It pre-allocates internal memory pools for ClipRect and Region operations. Without it, every layer operation allocates and frees memory dynamically. Call it once after `NewLayerInfo()` for performance-critical custom displays.
|
||||
|
||||
**Q: Can layers.library be used for off-screen rendering?**
|
||||
A: No — layers manage overlapping regions on a single `BitMap`. For off-screen rendering, use `AllocBitMap()` + `InitRastPort()` directly, or use a `LAYERSUPER` layer with a custom bitmap that's never displayed.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- NDK39: `graphics/layers.h`, `graphics/clip.h`, `graphics/gfx.h`
|
||||
### NDK Headers
|
||||
|
||||
- `graphics/layers.h` — `Layer_Info`, function prototypes
|
||||
- `graphics/clip.h` — `Layer`, `ClipRect` structures
|
||||
- `graphics/gfx.h` — `BitMap`, `Rectangle`
|
||||
- `graphics/rastport.h` — `RastPort` structure
|
||||
|
||||
### Autodocs
|
||||
|
||||
- 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
|
||||
- ADCD 2.1: graphics.library (drawing through layers)
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [RastPort](../08_graphics/rastport.md) — drawing through layers, the Layer field
|
||||
- [Screens](../09_intuition/screens.md) — Intuition screen layer management, LayerInfo deadlock
|
||||
- [Windows](../09_intuition/windows.md) — every window is a layer
|
||||
- [Bitmaps](../08_graphics/bitmap.md) — the pixel storage that layers clip onto
|
||||
- [Blitter](../08_graphics/blitter.md) — hardware that performs the actual copy operations for layers
|
||||
- [Utility](utility.md) — `Hook` structure used by backfill hooks
|
||||
|
|
|
|||
|
|
@ -1,101 +1,812 @@
|
|||
[← Home](../README.md) · [Toolchain](README.md)
|
||||
|
||||
# m68k-amigaos-gcc — Cross-Compiler
|
||||
# m68k-amigaos-gcc — GCC Cross-Compiler for AmigaOS
|
||||
|
||||
## Overview
|
||||
In 2025, building Amiga software looks nothing like 1993. The **bebbo toolchain** — GCC 6.5 retargeted for m68k-amigaos — cross-compiles on Linux, macOS, and Windows, producing native Amiga hunk executables from modern C and C++ source. It replaced the ancient GCC 2.95 cross-compiler that lived on Aminet for two decades, bringing modern optimization, C++14 support, and a Docker-based workflow that takes **under five minutes** to set up.
|
||||
|
||||
`m68k-amigaos-gcc` is the GCC-based cross-compiler for AmigaOS, typically based on GCC 2.95 (legacy) or GCC 6.5 (bebbo's fork). It produces Amiga hunk-format executables and supports all 68k variants. By default, bebbo's toolchain uses [vasm](vasm_vlink.md) as the assembler and [vlink](vasm_vlink.md) as the linker — the compiler emits assembly, vasm assembles it, and vlink produces the final hunk executable.
|
||||
The toolchain ships with [vasm](vasm_vlink.md) as its default assembler and [vlink](vasm_vlink.md) as its linker. GCC emits assembly, vasm assembles it, and vlink produces the final hunk executable. This replaces the aging GNU `as` and `ld` that produced unreliable Amiga binaries.
|
||||
|
||||
---
|
||||
|
||||
## Installation (bebbo's toolchain)
|
||||
## Architecture
|
||||
|
||||
### Compiler Pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "Frontend"
|
||||
SRC["Source (.c/.cc)"]
|
||||
GCC["m68k-amigaos-gcc<br>GCC 6.5"]
|
||||
PP["Preprocessor (cpp)"]
|
||||
end
|
||||
|
||||
subgraph "Backend"
|
||||
ASM["Assembly (.s)"]
|
||||
VASM["vasmm68k_mot"]
|
||||
OBJ["Object (.o)"]
|
||||
end
|
||||
|
||||
subgraph "Linker"
|
||||
VL["vlink"]
|
||||
EXE["Amiga HUNK"]
|
||||
end
|
||||
|
||||
SRC --> GCC
|
||||
GCC --> PP --> ASM --> VASM --> OBJ
|
||||
OBJ --> VL --> EXE
|
||||
```
|
||||
|
||||
bebbo's GCC is based on **GCC 6.5.0** with a custom m68k-amigaos backend. Unlike the original GCC 2.95 cross-compiler (which used GNU `as` and `ld`), bebbo's fork integrates [vasm](vasm_vlink.md) and [vlink](vasm_vlink.md) as the default assembler and linker. This produces more reliable Amiga hunk-format executables.
|
||||
|
||||
### What Ships in the Toolchain
|
||||
|
||||
| Component | Binary | Purpose |
|
||||
|-----------|--------|----------|
|
||||
| GCC C compiler | `m68k-amigaos-gcc` | C frontend, optimizer, code generator |
|
||||
| GCC C++ compiler | `m68k-amigaos-g++` | C++ frontend (supports C++14) |
|
||||
| Assembler | `vasmm68k_mot` | Motorola-syntax assembler (replaces GNU `as`) |
|
||||
| Linker | `vlink` | Multi-format linker (replaces GNU `ld`) |
|
||||
| Archiver | `m68k-amigaos-ar` | Static library (.a) creation |
|
||||
| Strip | `m68k-amigaos-strip` | Symbol removal |
|
||||
| objdump | `m68k-amigaos-objdump` | Disassembly and inspection |
|
||||
| NDK headers | `/opt/amiga/m68k-amigaos/ndk-include/` | AmigaOS API definitions |
|
||||
| libnix | `/opt/amiga/m68k-amigaos/lib/` | Lightweight C runtime |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Start: Docker (All Platforms)
|
||||
|
||||
The fastest path to a working toolchain on any OS:
|
||||
|
||||
```bash
|
||||
# Docker-based (recommended):
|
||||
docker pull bebbo/amiga-gcc
|
||||
docker run -v $(pwd):/work bebbo/amiga-gcc m68k-amigaos-gcc -o hello hello.c
|
||||
# Pull the pre-built image:
|
||||
docker pull amigadev/m68k-amigaos-gcc
|
||||
|
||||
# Native build (Linux/macOS):
|
||||
# NOTE: bebbo removed his repos from GitHub. Use Codeberg or his personal git:
|
||||
git clone https://codeberg.org/bebbo/amiga-gcc.git
|
||||
# Mirror: https://franke.ms/git/bebbo/amiga-gcc
|
||||
# Compile a single file:
|
||||
docker run --rm -v "$(pwd):/work" amigadev/m68k-amigaos-gcc \
|
||||
m68k-amigaos-gcc -noixemul -o hello hello.c
|
||||
|
||||
# Interactive shell with toolchain available:
|
||||
docker run --rm -it -v "$(pwd):/work" amigadev/m68k-amigaos-gcc bash
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The Docker image is maintained at [hub.docker.com/r/amigadev/m68k-amigaos-gcc](https://hub.docker.com/r/amigadev/m68k-amigaos-gcc). It includes the full toolchain, NDK headers, and libnix runtime.
|
||||
|
||||
### Source Repositories
|
||||
|
||||
bebbo's original repository was removed from GitHub. Use the maintained forks:
|
||||
|
||||
| Mirror | URL | Notes |
|
||||
|--------|-----|-------|
|
||||
| BlitterStudio | https://github.com/BlitterStudio/amiga-gcc | Most active, primary fork |
|
||||
| Codeberg | https://codeberg.org/bebbo/amiga-gcc | Official mirror |
|
||||
| Pre-built Windows installer | http://franke.ms/download/setup-amiga-gcc.exe | MSYS2-based, includes GUI installer |
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
#### Ubuntu / Debian — Prerequisites
|
||||
|
||||
```bash
|
||||
sudo apt install make wget git gcc g++ lhasa libgmp-dev libmpfr-dev \
|
||||
libmpc-dev flex bison gettext texinfo ncurses-dev autoconf rsync \
|
||||
libreadline-dev
|
||||
```
|
||||
|
||||
#### Fedora — Prerequisites
|
||||
|
||||
```bash
|
||||
sudo dnf install wget gcc gcc-c++ python git perl-Pod-Simple gperf patch \
|
||||
autoconf automake make makedepend bison flex ncurses-devel gmp-devel \
|
||||
mpfr-devel libmpc-devel gettext-devel texinfo rsync readline-devel
|
||||
```
|
||||
|
||||
#### CentOS / RHEL — Prerequisites
|
||||
|
||||
```bash
|
||||
sudo yum install wget gcc gcc-c++ python git perl-Pod-Simple gperf patch \
|
||||
autoconf automake make makedepend bison flex ncurses-devel gmp-devel \
|
||||
mpfr-devel libmpc-devel gettext-devel texinfo rsync readline-devel
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
git clone https://github.com/BlitterStudio/amiga-gcc.git
|
||||
cd amiga-gcc
|
||||
make update
|
||||
|
||||
# Build everything (adjust -j to your core count):
|
||||
make all -j$(nproc)
|
||||
# Installs to /opt/amiga/
|
||||
# Installs to /opt/amiga/ by default
|
||||
|
||||
# If /opt/amiga is not writable by your user:
|
||||
sudo mkdir /opt/amiga
|
||||
sudo chgrp users /opt/amiga
|
||||
sudo chmod 775 /opt/amiga
|
||||
sudo usermod -a -G users $USER # then log out and back in
|
||||
|
||||
# Or install to a user-writable location:
|
||||
make all -j$(nproc) PREFIX=$HOME/amiga
|
||||
```
|
||||
|
||||
Add to your shell profile:
|
||||
```bash
|
||||
export PATH=/opt/amiga/bin:$PATH
|
||||
```
|
||||
|
||||
Build time: **~10 minutes** on a modern Linux box with `-j4` or higher.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
### macOS
|
||||
|
||||
macOS requires **Xcode Command Line Tools** and **Homebrew**. The default `/bin/bash` is too old (macOS ships bash 3.2); you must use Homebrew's bash.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
```bash
|
||||
# Compile and link:
|
||||
m68k-amigaos-gcc -noixemul -o hello hello.c
|
||||
# Install Xcode CLI tools (if not already installed):
|
||||
xcode-select --install
|
||||
|
||||
# Common flags:
|
||||
# -noixemul — use libnix (no ixemul.library dependency)
|
||||
# -m68000 — target 68000 (default)
|
||||
# -m68020 — target 68020+
|
||||
# -m68040 — target 68040
|
||||
# -m68060 — target 68060
|
||||
# -m68881 — use 68881/68882 FPU
|
||||
# -Os — optimize for size
|
||||
# -O2 — optimize for speed
|
||||
# -fomit-frame-pointer — free up A5
|
||||
# -fbaserel — base-relative addressing (small data model)
|
||||
# -resident — generate resident-capable code
|
||||
# -g — include debug info (HUNK_DEBUG)
|
||||
# -s — strip symbols
|
||||
# Install Homebrew (https://brew.sh) if not already installed,
|
||||
# then install the required packages:
|
||||
brew install bash wget make lhasa gmp mpfr libmpc flex gettext \
|
||||
gnu-sed texinfo gcc@12 make autoconf bison
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
git clone https://github.com/BlitterStudio/amiga-gcc.git
|
||||
cd amiga-gcc
|
||||
make update
|
||||
|
||||
# CRITICAL: macOS default bash is too old — always use Homebrew's bash:
|
||||
make all -j$(sysctl -n hw.ncpu) SHELL=$(brew --prefix)/bin/bash
|
||||
|
||||
# If the build fails with system clang errors, force Homebrew's GCC:
|
||||
CC=gcc-12 CXX=g++-12 make all -j$(sysctl -n hw.ncpu) SHELL=$(brew --prefix)/bin/bash
|
||||
```
|
||||
|
||||
Add to your shell profile:
|
||||
```bash
|
||||
export PATH=/opt/amiga/bin:$PATH
|
||||
```
|
||||
|
||||
#### Apple Silicon (M1 / M2 / M3 / M4)
|
||||
|
||||
Native builds on Apple Silicon are **directly supported** — no Rosetta needed. The build compiles the m68k cross-compiler toolchain and does not run any m68k code on the host, so the host architecture is irrelevant.
|
||||
|
||||
The only Apple Silicon caveat: some Homebrew packages install into `/opt/homebrew/` instead of `/usr/local/`. The `$(brew --prefix)` calls above handle this automatically.
|
||||
|
||||
#### Building GDB on macOS
|
||||
|
||||
GDB requires a newer `bison` than macOS provides. Use the Homebrew version:
|
||||
|
||||
```bash
|
||||
export PATH=$(brew --prefix bison)/bin:$PATH
|
||||
make gdb SHELL=$(brew --prefix)/bin/bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Startup Code
|
||||
### Windows
|
||||
|
||||
| Startup | Description |
|
||||
|---|---|
|
||||
| `libnix` | Minimal C runtime, no shared library dependency |
|
||||
| `ixemul` | Unix-like C runtime (requires ixemul.library) |
|
||||
| `crt0.o` | Raw startup — no C runtime at all |
|
||||
Three options, in order of recommendation:
|
||||
|
||||
#### MSYS2 (Recommended)
|
||||
|
||||
[MSYS2](https://www.msys2.org/) provides a Unix-like build environment on Windows with native performance (no virtualization).
|
||||
|
||||
```bash
|
||||
# Install MSYS2 from https://www.msys2.org/, then open an MSYS2 terminal:
|
||||
pacman -S git base-devel gcc flex gmp-devel mpc-devel mpfr-devel \
|
||||
ncurses-devel rsync autoconf automake
|
||||
|
||||
# Clone and build:
|
||||
git clone https://github.com/BlitterStudio/amiga-gcc.git
|
||||
cd amiga-gcc
|
||||
make update
|
||||
|
||||
# IMPORTANT: cd into an absolute path — MSYS2 has a bug where
|
||||
# relative paths can cause file-not-found errors during build:
|
||||
cd /c/Users/you/amiga-gcc
|
||||
make all -j4
|
||||
```
|
||||
|
||||
Add to your Windows `PATH`: `%USERPROFILE%\msys64\opt\amiga\bin` (adjust if MSYS2 is installed elsewhere).
|
||||
|
||||
> [!WARNING]
|
||||
> You **must** `cd` into an absolute MSYS2 path (e.g. `/c/msys64/home/test/amiga-gcc/`) before running `make`. Building from a relative path fails because some source files aren't found correctly — this is an MSYS2 path translation bug.
|
||||
|
||||
#### WSL — Ubuntu on Windows
|
||||
|
||||
If you already use WSL (Windows Subsystem for Linux), the build is identical to Ubuntu:
|
||||
|
||||
```bash
|
||||
# In WSL Ubuntu terminal:
|
||||
sudo apt install make wget git gcc g++ lhasa libgmp-dev libmpfr-dev \
|
||||
libmpc-dev flex bison gettext texinfo ncurses-dev autoconf rsync \
|
||||
libreadline-dev
|
||||
|
||||
git clone https://github.com/BlitterStudio/amiga-gcc.git
|
||||
cd amiga-gcc && make update && make all -j$(nproc)
|
||||
```
|
||||
|
||||
The toolchain ends up in `/opt/amiga/` inside WSL. To access compiled binaries from Windows Explorer, look in `\\wsl$\\Ubuntu\\opt\\amiga\\bin`.
|
||||
|
||||
#### Cygwin (Legacy)
|
||||
|
||||
Cygwin works but is **significantly slower** than MSYS2 or WSL. The build can take over an hour on the same hardware that completes in 10 minutes under MSYS2.
|
||||
|
||||
```bash
|
||||
# Install Cygwin from https://cygwin.com/, then in the Cygwin terminal:
|
||||
wget https://raw.githubusercontent.com/transcode-open/apt-cyg/master/apt-cyg
|
||||
install apt-cyg /bin
|
||||
apt-cyg install gcc-core gcc-g++ python git perl-Pod-Simple gperf patch \
|
||||
automake make makedepend bison flex libncurses-devel python-devel \
|
||||
gettext-devel libgmp-devel libmpc-devel libmpfr-devel rsync
|
||||
|
||||
git clone https://github.com/BlitterStudio/amiga-gcc.git
|
||||
cd amiga-gcc && make update && make all -j$(nproc)
|
||||
```
|
||||
|
||||
#### Pre-built Installer
|
||||
|
||||
For those who want a zero-build option, there is a community-maintained Windows installer:
|
||||
|
||||
- **Download:** http://franke.ms/download/setup-amiga-gcc.exe
|
||||
- Includes the full toolchain, NDK headers, libnix, and an MSYS2-based runtime
|
||||
- Install and add the `bin/` directory to your `PATH`
|
||||
|
||||
> [!NOTE]
|
||||
> The pre-built installer may lag behind the latest Git commits. Check the version after installing with `m68k-amigaos-gcc --version`.
|
||||
|
||||
---
|
||||
|
||||
### Post-Install Verification
|
||||
|
||||
After building or installing by any method, verify the toolchain:
|
||||
|
||||
```bash
|
||||
# Check GCC version:
|
||||
m68k-amigaos-gcc --version
|
||||
# Expected: gcc (GCC) 6.5.0 + Amiga patches
|
||||
|
||||
# Check assembler:
|
||||
vasmm68k_mot -V
|
||||
|
||||
# Check linker:
|
||||
vlink -V
|
||||
|
||||
# Compile and inspect a test binary:
|
||||
echo 'int main(void) { return 0; }' > test.c
|
||||
m68k-amigaos-gcc -noixemul -m68000 -o test test.c
|
||||
m68k-amigaos-objdump -d test | head -20
|
||||
rm -f test test.c
|
||||
```
|
||||
|
||||
### Platform Comparison
|
||||
|
||||
| Aspect | Linux | macOS | Windows MSYS2 | Windows WSL | Docker |
|
||||
|--------|-------|-------|---------------|-------------|--------|
|
||||
| **Build time** | ~10 min | ~15 min | ~15-20 min | ~10 min | N/A (pre-built) |
|
||||
| **Host compiler** | System GCC/Clang | Homebrew GCC | MSYS2 GCC | Ubuntu GCC | N/A |
|
||||
| **Shell requirement** | Any bash | Homebrew bash | MSYS2 bash | Any bash | Any |
|
||||
| **Path quirks** | None | `/opt/homebrew` on ARM | Absolute paths only | None | Volume mounts |
|
||||
| **Apple Silicon** | N/A | Native, no Rosetta | N/A | N/A | Via Rosetta |
|
||||
| **Setups for beginners** | Easy | Medium | Medium | Easy | Easiest |
|
||||
| **Updates** | `make update && make all` | Same + `SHELL=` | Same + absolute `cd` | Same as Linux | `docker pull` |
|
||||
|
||||
---
|
||||
|
||||
## Compilation Flags
|
||||
|
||||
### CPU Target Flags
|
||||
|
||||
| Flag | Target | Instructions Available | Typical Use |
|
||||
|------|--------|----------------------|-------------|
|
||||
| `-m68000` | 68000 | Baseline | A500, A1000, CDTV — smallest code |
|
||||
| `-m68020` | 68020 | `MULS.L`, `DIVS.L`, 32-bit math | A1200, A3000 — best default |
|
||||
| `-m68030` | 68030 | + MMU instructions | Accelerated A1200 |
|
||||
| `-m68040` | 68040 | + hardware FPU | A4000, CyberStorm |
|
||||
| `-m68060` | 68060 | + superscalar | 060 accelerators |
|
||||
| `-m68881` | 68881/68882 | FPU coprocessor | For 68000/020 with FPU |
|
||||
|
||||
### Amiga-Specific Flags
|
||||
|
||||
| Flag | Purpose |
|
||||
|------|----------|
|
||||
| `-noixemul` | Use libnix runtime — no `ixemul.library` dependency. **Always use this** unless you need POSIX compatibility. |
|
||||
| `-fbaserel` | Base-relative addressing (small data model). Reduces code size, uses A4 as data base pointer. |
|
||||
| `-resident` | Generate resident-capable code (for libraries and devices). |
|
||||
| `-fomit-frame-pointer` | Frees A5 from frame-pointer duty. Safe for AmigaOS code — do not use with `-pg` profiling. |
|
||||
| `-malways-restore-a4` | Force A4 save/restore in every function (needed for small data model with callbacks). |
|
||||
| `-mrestore-a4` | Save/restore A4 only when needed (less conservative than `-malways-restore-a4`). |
|
||||
|
||||
### Optimization Flags
|
||||
|
||||
| Flag | Effect | Use When |
|
||||
|------|--------|----------|
|
||||
| `-O0` | No optimization | Debugging — all variables in memory |
|
||||
| `-O1` | Basic optimization | Development — fast compile, some speedup |
|
||||
| `-O2` | Full optimization | **Release** — best speed/size balance |
|
||||
| `-O3` | Aggressive optimization | Inner loops — may increase code size |
|
||||
| `-Os` | Optimize for size | Memory-constrained — smallest binary |
|
||||
| `-fomit-frame-pointer` | Eliminate frame pointer | All release builds — frees A5 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Startup Code & Runtime Libraries
|
||||
|
||||
### libnix (Recommended)
|
||||
|
||||
**libnix** is a minimal C runtime that requires no shared library on the target Amiga. It is the default when using `-noixemul`. Programs linked with libnix run on any Amiga with no extra dependencies.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
ENTRY["_start entry"] --> CRT0["crt0.o"] --> PREMAIN["__main()"] --> MAIN["main()"] --> EXIT["exit() / _exit()"]
|
||||
style CRT0 fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
```
|
||||
|
||||
| Startup Module | Use For | Auto-Opens Libraries |
|
||||
|---------------|---------|---------------------|
|
||||
| `libnix` (default) | CLI programs | None — minimal overhead |
|
||||
| `libnix` + `-resident` | Shared libraries / devices | None |
|
||||
| Custom `crt0.o` | Bootblock, tracker-loader | None — you own the entry point |
|
||||
|
||||
### ixemul (Legacy)
|
||||
|
||||
**ixemul** provides a Unix-like C runtime (`malloc()`, `printf()`, POSIX file I/O) but requires the `ixemul.library` to be installed on the target Amiga. This is useful for porting Unix software but creates a runtime dependency.
|
||||
|
||||
```c
|
||||
/* WITHOUT -noixemul (ixemul mode): */
|
||||
#include <stdio.h> /* ixemul provides standard C I/O */
|
||||
|
||||
int main(void) {
|
||||
printf("Hello via ixemul\n"); /* Works like any Unix program */
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Never ship ixemul-linked programs to end users unless you also ship `ixemul.library`. For standalone Amiga programs, always use `-noixemul`.
|
||||
|
||||
### Startup Decision Guide
|
||||
|
||||
| Scenario | Startup | Flags |
|
||||
|----------|---------|-------|
|
||||
| CLI tool, standalone | libnix CLI | `-noixemul` |
|
||||
| Workbench tool | libnix WB startup | `-noixemul` + handle `WBStartup` message |
|
||||
| Shared library | libnix resident | `-noixemul -resident` |
|
||||
| Bootblock / custom loader | No runtime | `-noixemul -nostartfiles` + custom `crt0.o` |
|
||||
| Unix port | ixemul | Omit `-noixemul` |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Inline/Pragma System Calls
|
||||
|
||||
```c
|
||||
/* Use inline headers to call library functions via registers: */
|
||||
#include <inline/exec.h>
|
||||
#include <inline/dos.h>
|
||||
GCC uses **inline assembly stubs** in `<inline/*.h>` headers to map C function calls directly onto the AmigaOS register-based calling convention. The `<proto/*.h>` headers include the appropriate inline or pragma version automatically.
|
||||
|
||||
/* Or use proto headers (auto-select inline vs pragma): */
|
||||
```c
|
||||
/* Use proto headers (recommended — auto-selects best available mechanism): */
|
||||
#include <proto/exec.h>
|
||||
#include <proto/dos.h>
|
||||
#include <proto/graphics.h>
|
||||
|
||||
/* The proto header expands Open() into an inline asm stub like: */
|
||||
/* MOVE.L name, D1
|
||||
MOVE.L mode, D2
|
||||
MOVEA.L _DOSBase, A6
|
||||
JSR -30(A6) */
|
||||
```
|
||||
|
||||
### How Inline Stubs Work
|
||||
|
||||
GCC's inline headers use `__asm()` extensions to place arguments in the correct registers:
|
||||
|
||||
```c
|
||||
/* Simplified version of what inline/dos.h contains: */
|
||||
static __inline BPTR Open(CONST_STRPTR name, LONG accessMode)
|
||||
{
|
||||
register BPTR res __asm("d0"); /* return value in D0 */
|
||||
register CONST_STRPTR r_d1 __asm("d1") = name;
|
||||
register LONG r_d2 __asm("d2") = accessMode;
|
||||
__asm volatile (
|
||||
"movea.l %1,%%a6\n\t"
|
||||
"jsr -30(%%a6)"
|
||||
: "=r"(res)
|
||||
: "r"(DOSBase), "r"(r_d1), "r"(r_d2)
|
||||
: "d0", "d1", "d2", "a0", "a1", "memory"
|
||||
);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
See [compiler_stubs.md](../04_linking_and_libraries/compiler_stubs.md) for a cross-compiler comparison of stub generation.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Minimal CLI Program
|
||||
|
||||
```c
|
||||
/* hello.c — minimal Amiga CLI program with libnix */
|
||||
#include <proto/dos.h>
|
||||
#include <proto/exec.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
BPTR output = Output(); /* get stdout handle */
|
||||
char msg[] = "Hello, Amiga!\n";
|
||||
Write(output, msg, sizeof(msg) - 1);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
m68k-amigaos-gcc -noixemul -m68000 -Os -o hello hello.c
|
||||
```
|
||||
|
||||
### Workbench Program with Tooltypes
|
||||
|
||||
```c
|
||||
/* wbapp.c — program that runs from both CLI and Workbench */
|
||||
#include <proto/dos.h>
|
||||
#include <proto/exec.h>
|
||||
#include <workbench/startup.h>
|
||||
#include <stdio.h> /* only with ixemul; use RawDoFmt() with libnix */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc == 0)
|
||||
{
|
||||
/* Launched from Workbench — argv points to WBStartup */
|
||||
struct WBStartup *wb = (struct WBStartup *)argv;
|
||||
struct WBArg *arg = &wb->sm_ArgList[0];
|
||||
/* arg->wa_ToolTypes contains the tooltype array */
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Launched from CLI */
|
||||
Printf("Running from CLI with %ld arguments\n", argc);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
m68k-amigaos-gcc -noixemul -m68000 -O2 -o wbapp wbapp.c
|
||||
```
|
||||
|
||||
### Shared Library Skeleton
|
||||
|
||||
```c
|
||||
/* mylib.c — minimal shared library for AmigaOS */
|
||||
#include <proto/exec.h>
|
||||
#include <exec/libraries.h>
|
||||
#include <exec/resident.h>
|
||||
|
||||
/* The library function table */
|
||||
LONG MyLibFunction(ULONG val)
|
||||
{
|
||||
return val * 2;
|
||||
}
|
||||
|
||||
/* Function table — NULL terminated */
|
||||
static const APTR funcTable[] = {
|
||||
(APTR)LIBENT_0, /* Open() — auto-generated */
|
||||
(APTR)LIBENT_1, /* Close() — auto-generated */
|
||||
(APTR)LIBENT_2, /* Expunge() — auto-generated */
|
||||
(APTR)LIBENT_3, /* Reserved */
|
||||
(APTR)MyLibFunction,
|
||||
(APTR)-1 /* end of table */
|
||||
};
|
||||
|
||||
/* Library init table */
|
||||
static const struct LibInitTable {
|
||||
APTR *libBase;
|
||||
APTR *funcTable;
|
||||
APTR *dataTable;
|
||||
APTR (*init)(APTR libBase, BPTR segList, struct ExecBase *sysBase);
|
||||
} libInit = {
|
||||
(APTR *)NULL, /* filled by MakeLibrary */
|
||||
(APTR *)funcTable,
|
||||
(APTR *)NULL, /* data table */
|
||||
NULL /* init function */
|
||||
};
|
||||
```
|
||||
|
||||
```bash
|
||||
m68k-amigaos-gcc -noixemul -resident -m68000 -O2 -o mylib.library mylib.c
|
||||
```
|
||||
|
||||
### CMake Integration
|
||||
|
||||
```cmake
|
||||
# CMakeLists.txt for m68k-amigaos-gcc
|
||||
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(MyAmigaApp C)
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Generic)
|
||||
set(CMAKE_SYSTEM_PROCESSOR m68k)
|
||||
|
||||
set(TOOLPREFIX "m68k-amigaos-")
|
||||
set(CMAKE_C_COMPILER "${TOOLPREFIX}gcc")
|
||||
set(CMAKE_AR "${TOOLPREFIX}ar")
|
||||
set(CMAKE_STRIP "${TOOLPREFIX}strip")
|
||||
|
||||
# No standard library — we use libnix
|
||||
set(CMAKE_C_FLAGS "-noixemul -m68000 -O2 -Wall -Wextra")
|
||||
|
||||
add_executable(myapp main.c util.c)
|
||||
|
||||
# Strip the final binary
|
||||
add_custom_command(TARGET myapp POST_BUILD
|
||||
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:myapp>)
|
||||
```
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-m68k-amigaos.cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Toolchain Integration — vasm & vlink
|
||||
## GCC vs VBCC Decision Guide
|
||||
|
||||
bebbo's GCC uses **[vasm](vasm_vlink.md)** and **[vlink](vasm_vlink.md)** as its default assembler and linker. The compiler emits assembly (`.s`), vasm assembles it to hunk-format object files, and vlink links the final executable. This is fully configurable — `-Wa,...` passes flags to the assembler, `-Wl,...` passes flags to the linker.
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START{"Starting a new project?"} --> CPP{"Need C++?"}
|
||||
CPP -->|Yes| GCC{"Use GCC"}
|
||||
CPP -->|No| GNU{"Need GNU extensions?\n(__attribute__, typeof, etc.)"}
|
||||
GNU -->|Yes| GCC
|
||||
GNU -->|No| SPEED{"Compile speed matters?"}
|
||||
SPEED -->|Yes| VBCC{"Use VBCC"}
|
||||
SPEED -->|No| REG{"Need explicit register control?"}
|
||||
REG -->|Yes| VBCC
|
||||
REG -->|No| EITHER{"Either compiler works\nUse what the project already uses"}
|
||||
```
|
||||
|
||||
| Criterion | GCC bebbo | VBCC |
|
||||
|-----------|-----------|------|
|
||||
| **C++ support** | Yes (C++14) | No |
|
||||
| **C standard** | C11 | C89 (+ partial C99) |
|
||||
| **GNU extensions** | Yes | No |
|
||||
| **Compile speed** | Slow (large optimizer) | Fast |
|
||||
| **Code quality** | Good (mature backend) | Excellent (tight m68k code) |
|
||||
| **Register control** | `__asm("d1")` inline | `__reg("d1")` storage class |
|
||||
| **Linker** | vlink (default) | vlink |
|
||||
| **Debug info** | HUNK_DEBUG + GDB remote | HUNK_DEBUG |
|
||||
| **Active maintenance** | Yes (BlitterStudio) | Yes (Volker Barthelmann) |
|
||||
| **AmigaOS 4 / MorphOS** | Yes | Yes |
|
||||
| **Smallest binary** | With `-Os` | With `-size` (often smaller) |
|
||||
|
||||
See [vbcc.md](vbcc.md) for the VBCC deep dive.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always use `-noixemul` for standalone programs — ixemul requires a runtime library
|
||||
2. Target `-m68000` unless you know your audience has an accelerator — A500 is the largest installed base
|
||||
3. Use `-Os` for size-constrained programs; `-O2` for performance-critical code
|
||||
4. Add `-fomit-frame-pointer` in release builds to free A5 for general use
|
||||
5. Strip release binaries with `-s` or `m68k-amigaos-strip`
|
||||
6. Use `proto/*.h` headers — never call raw LVO offsets manually in C
|
||||
7. Set up a Makefile or CMake build — see [makefiles.md](makefiles.md) for templates
|
||||
8. Keep NDK headers up to date — NDK 3.9 is the last official release
|
||||
9. Test on both FS-UAE and real hardware — timing-sensitive code behaves differently
|
||||
10. For mixed C + assembly, use GCC for C and [vasm](vasm_vlink.md) for asm, link with vlink
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The ixemul Dependence" — Shipping Without -noixemul
|
||||
|
||||
```c
|
||||
/* BAD: Compiles fine, but requires ixemul.library on the target */
|
||||
#include <stdio.h>
|
||||
int main(void) {
|
||||
printf("Hello\n"); /* ixemul provides printf */
|
||||
return 0;
|
||||
}
|
||||
/* Build: m68k-amigaos-gcc -o hello hello.c (missing -noixemul!) */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Uses AmigaOS native I/O, no dependencies */
|
||||
#include <proto/dos.h>
|
||||
int main(void) {
|
||||
Write(Output(), "Hello\n", 6);
|
||||
return 0;
|
||||
}
|
||||
/* Build: m68k-amigaos-gcc -noixemul -o hello hello.c */
|
||||
```
|
||||
|
||||
### "The Phantom Frame Pointer" — A5 Corruption
|
||||
|
||||
```c
|
||||
/* BAD: Without -fomit-frame-pointer, A5 is used as frame pointer.
|
||||
If your code or an OS callback corrupts A5, every local variable
|
||||
in the calling chain becomes garbage. */
|
||||
void callback(void) {
|
||||
/* A5 points to caller's frame — but we were called from OS code */
|
||||
int x = 42; /* stored at A5+offset — WRONG frame! */
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: Use -fomit-frame-pointer so A5 is free,
|
||||
and explicitly save/restore registers in callbacks */
|
||||
void callback(void) {
|
||||
register int x __asm("d3") = 42; /* in register, not on frame */
|
||||
/* ... */
|
||||
}
|
||||
/* Build: m68k-amigaos-gcc -noixemul -fomit-frame-pointer ... */
|
||||
```
|
||||
|
||||
### "The Wrong CPU" — 68020 Code on 68000
|
||||
|
||||
```bash
|
||||
# Single command — GCC orchestrates assembler and linker internally:
|
||||
m68k-amigaos-gcc -noixemul -m68020 -O2 -o myapp myapp.c
|
||||
# Internal pipeline: gcc → .s → vasmm68k_mot → .o → vlink → executable
|
||||
|
||||
# Pass assembler-specific flags:
|
||||
m68k-amigaos-gcc -Wa,-m68060,-devpac -o myapp myapp.c
|
||||
|
||||
# Pass linker-specific flags:
|
||||
m68k-amigaos-gcc -Wl,-Map=myapp.map -o myapp myapp.c
|
||||
# BAD: Compiles with 68020 instructions, crashes on A500
|
||||
m68k-amigaos-gcc -noixemul -m68020 -O2 -o game game.c
|
||||
# The binary contains MULS.L, DIVS.L — illegal on 68000
|
||||
```
|
||||
|
||||
```bash
|
||||
# CORRECT: Target the lowest common denominator
|
||||
m68k-amigaos-gcc -noixemul -m68000 -O2 -o game game.c
|
||||
# Or use runtime detection and provide two code paths
|
||||
```
|
||||
|
||||
### "The Chip RAM Blind Spot" — Allocating Fast for DMA
|
||||
|
||||
```c
|
||||
/* BAD: AllocMem with MEMF_FAST — not accessible by custom chip DMA */
|
||||
APTR bitmap_data = AllocMem(width * height / 8, MEMF_FAST);
|
||||
/* Blitter cannot reach this memory! Silent corruption or crash. */
|
||||
```
|
||||
|
||||
```c
|
||||
/* CORRECT: DMA-visible memory must be Chip RAM */
|
||||
APTR bitmap_data = AllocMem(width * height / 8, MEMF_CHIP | MEMF_CLEAR);
|
||||
/* Now Blitter, Copper, and bitplane DMA can access it */
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> This is not a GCC-specific bug — it is the single most common Amiga programming mistake. **Any data touched by custom chip DMA must be in Chip RAM.** See [memory_types.md](../01_hardware/common/memory_types.md).
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Stack Size Too Small
|
||||
|
||||
**Symptom:** Random crashes after running for a while, especially in functions with large local arrays.
|
||||
|
||||
**Cause:** AmigaOS default stack size is only **4,096 bytes** for CLI programs and **4,000 bytes** for Workbench programs. GCC does not warn about stack overflow.
|
||||
|
||||
**Fix:** Set stack size explicitly in the icon or via the `Stack` command:
|
||||
```bash
|
||||
# In CLI:
|
||||
Stack 10000
|
||||
myapp
|
||||
|
||||
# Or embed in .info icon with icon tool type:
|
||||
STACK=10000
|
||||
```
|
||||
|
||||
### 2. Volatile Hardware Registers
|
||||
|
||||
**Symptom:** Blitter or Copper register writes disappear or happen in the wrong order.
|
||||
|
||||
**Cause:** GCC's optimizer eliminates or reorders memory accesses to non-volatile pointers.
|
||||
|
||||
**Fix:** Always use `volatile` for hardware registers:
|
||||
```c
|
||||
#include <hardware/custom.h>
|
||||
extern volatile struct Custom custom; /* NDK does this for you */
|
||||
|
||||
/* For custom register pointers: */
|
||||
volatile UWORD *myReg = (volatile UWORD *)0xDFF100;
|
||||
*myReg = 0xFF; /* guaranteed emit */
|
||||
```
|
||||
|
||||
### 3. Big-Endian Data in File Formats
|
||||
|
||||
**Symptom:** File I/O reads wrong values on modern x86 development machines.
|
||||
|
||||
**Cause:** The 68000 is **big-endian**. When testing file parsing code on the host (x86), byte order is reversed. Cross-compiled code runs correctly on Amiga, but unit tests on the host may fail.
|
||||
|
||||
**Fix:** Use explicit byte-swap functions or test only on the target:
|
||||
```c
|
||||
#include <exec/types.h> /* UWORD, ULONG are defined here */
|
||||
|
||||
/* Reading a big-endian ULONG from a file — works on both host and target */
|
||||
ULONG read_be32(UBYTE *buf) {
|
||||
return ((ULONG)buf[0] << 24) | ((ULONG)buf[1] << 16) |
|
||||
((ULONG)buf[2] << 8) | (ULONG)buf[3];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Missing NDK Include Paths
|
||||
|
||||
**Symptom:** `fatal error: proto/exec.h: No such file or directory`
|
||||
|
||||
**Cause:** bebbo's toolchain installs NDK headers in `/opt/amiga/m68k-amigaos/ndk-include/`, which is not a default GCC search path.
|
||||
|
||||
**Fix:** Add the include path explicitly:
|
||||
```bash
|
||||
m68k-amigaos-gcc -noixemul -I/opt/amiga/m68k-amigaos/ndk-include -o app app.c
|
||||
# Or use the Makefile template from makefiles.md
|
||||
```
|
||||
|
||||
### 5. Mixing GCC and VBCC Object Files
|
||||
|
||||
**Symptom:** Link errors, undefined references to internal runtime functions.
|
||||
|
||||
**Cause:** GCC and VBCC use different internal calling conventions for their runtime helpers (division, stack checking, etc.).
|
||||
|
||||
**Fix:** Compile the entire project with one compiler. If mixing assembly, use [vasm](vasm_vlink.md) and link with `vlink`.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: What version of GCC does bebbo's toolchain use?**
|
||||
A: GCC 6.5.0 with a custom m68k-amigaos backend. This is much newer than the ancient GCC 2.95 that was the standard for decades.
|
||||
|
||||
**Q: Can I compile C++ for Amiga?**
|
||||
A: Yes — use `m68k-amigaos-g++` with the same flags. C++14 is supported. Note that C++ exceptions and RTTI increase code size significantly on m68k.
|
||||
|
||||
**Q: Where are bebbo's original repos?**
|
||||
A: bebbo removed his repositories from GitHub. Use the [BlitterStudio fork](https://github.com/BlitterStudio/amiga-gcc) or the [Codeberg mirror](https://codeberg.org/bebbo/amiga-gcc).
|
||||
|
||||
**Q: Can I use GCC inline assembly for OS calls?**
|
||||
A: Yes, but prefer `<proto/*.h>` headers — they handle the register allocation correctly. See [compiler_stubs.md](../04_linking_and_libraries/compiler_stubs.md) for the full comparison.
|
||||
|
||||
**Q: How do I debug cross-compiled programs?**
|
||||
A: Use FS-UAE with GDB remote debugging (`-s` flag), or add `kprintf()` / `Printf()` tracing. The `-g` flag emits HUNK_DEBUG symbols.
|
||||
|
||||
**Q: Does GCC support AmigaOS 4 or MorphOS?**
|
||||
A: GCC bebbo targets AmigaOS 3.x (m68k). For AmigaOS 4 (PPC), use the official SDK. For MorphOS, use the MorphOS SDK with GCC.
|
||||
|
||||
**Q: What is the difference between `-fbaserel` and small data model?**
|
||||
A: `-fbaserel` enables base-relative addressing where frequently-accessed global data is accessed relative to A4. This reduces code size but requires A4 to be set up correctly — handled by libnix startup code.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Toolchain
|
||||
|
||||
- BlitterStudio fork: https://github.com/BlitterStudio/amiga-gcc
|
||||
- Codeberg mirror: https://codeberg.org/bebbo/amiga-gcc
|
||||
- Docker image: https://hub.docker.com/r/amigadev/m68k-amigaos-gcc
|
||||
|
||||
### Related Knowledge Base Articles
|
||||
|
||||
- [vasm & vlink](vasm_vlink.md) — assembler and linker used by this toolchain
|
||||
- Codeberg: https://codeberg.org/bebbo/amiga-gcc
|
||||
- Mirror: https://franke.ms/git/bebbo/amiga-gcc
|
||||
- GCC 6.5 m68k cross-compiler documentation
|
||||
- [VBCC](vbcc.md) — alternative cross-compiler comparison
|
||||
- [SAS/C](sasc.md) — native AmigaOS compiler (historical)
|
||||
- [Makefiles](makefiles.md) — build templates for GCC cross-compilation
|
||||
- [NDK](ndk.md) — NDK versions and include paths
|
||||
- [Compiler Stubs](../04_linking_and_libraries/compiler_stubs.md) — how GCC generates library call stubs
|
||||
- [Register Conventions](../04_linking_and_libraries/register_conventions.md) — AmigaOS register ABI
|
||||
- [Compiler Fingerprints](../05_reversing/compiler_fingerprints.md) — recognizing GCC output in disassembly
|
||||
|
|
|
|||
20
TODO.md
20
TODO.md
|
|
@ -53,14 +53,14 @@ Articles were scored against [AGENTS.md](../amiga/AGENTS.md) "Deep" criteria:
|
|||
| # | File | Was | Now | Status |
|
||||
|---|---|---|---|---|
|
||||
| 15 | `08_graphics/sprites.md` | 306 | 306 | ❌ **Pending** — Named antipatterns, pitfalls comparing hardware sprites vs SimpleSprite, FPGA sprite timing |
|
||||
| 16 | `08_graphics/text_fonts.md` | 215 | 215 | ❌ **Pending** — Cookbook (bitmap vs outline fonts), pitfalls with font spacing, ColorFont memory layout |
|
||||
| 16 | `08_graphics/text_fonts.md` | 215 | 708 | ✅ **Complete** — Expanded with ColorTextFont layout, Compugraphic outlines, font scaling/aspect ratio, draw mode effects, 3 cookbooks (centered title, multi-font label, word wrap), 4 antipatterns, 4 pitfalls, historical comparison, modern analogies, 6 FAQ |
|
||||
| 17 | `09_intuition/screens.md` | 582 | 582 | ❌ **Pending** — Antipatterns, cookbook (screen flipping, borderless, PAL→NTSC handling) |
|
||||
| 18 | `09_intuition/windows.md` | 370 | 370 | ❌ **Pending** — Named antipatterns ("The Borderless Too Soon"), FAQ, cookbook (custom window dragging, backdrop to screen flip) |
|
||||
| 19 | `09_intuition/menus.md` | 378 | 378 | ❌ **Pending** — Menu render chain diagram, pitfalls with RemoveMenu/FreeMenu ordering, AmigaGuide help links |
|
||||
| 18 | `09_intuition/windows.md` | 370 | 778 | ✅ **Complete** — Added 5 named antipatterns (Border Collision, Unresponsive Close, Leaked Message, Refresh Loop, Phantom Window), window type decision guide with Mermaid, 3 cookbooks (resizable, borderless overlay, multi-window shared port), historical comparison, modern analogies, use cases, 7 FAQ |
|
||||
| 19 | `09_intuition/menus.md` | 378 | 695 | ✅ **Complete** — Added render chain sequence diagram, 5 named antipatterns (Multi-Select Ghost, Stale Menu, Cleanup Reversal, Shortcut Collision, Phantom VisualInfo), complete lifecycle cookbook, historical comparison, modern analogies, use cases, 6 FAQ |
|
||||
| 20 | `09_intuition/gadgets.md` | 403 | 403 | ❌ **Pending** — Named antipatterns, BOOPSI command flow, GadTools→BOOPSI migration guide |
|
||||
| 21 | `11_libraries/iffparse.md` | 271 | 1031 | ✅ **DONE** — Nesting & chunk hierarchy diagrams, ILBM/EHB pitfalls, PBM read patterns, cross-reference to Datatypes |
|
||||
| 22 | `13_toolchain/gcc_amiga.md` | 82 | 82 | ❌ **Pending** — Extremely thin stub. Needs full build pipeline, Docker cross-compilation guide, linker scripts, amiga-gcc specifics |
|
||||
| 23 | `11_libraries/layers.md` | 224 | 224 | ❌ **Pending** — Mermaid layer stacking diagram, pitfalls with layer ordering and damage regions |
|
||||
| 23 | `11_libraries/layers.md` | 224 | 739 | ✅ **Complete** — Expanded with ClipRect engine deep-dive, API reference with LVOs, backfill hook cookbook, refresh type decision guide, 4 named antipatterns, 4 pitfalls, ClipBlit vs ScrollRaster optimization, historical comparison table, modern analogies, 7 FAQ |
|
||||
| 24 | `10_devices/console.md` | 244 | 244 | ❌ **Pending** — Escape sequence table, cookbook for raw console I/O, pitfalls with buffering |
|
||||
| 25 | `10_devices/trackdisk.md` | 178 | 178 | ❌ **Pending** — Sector-level format diagram, MFM encoding basics, pitfalls with trackdisk vs filesystem access |
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ Articles were scored against [AGENTS.md](../amiga/AGENTS.md) "Deep" criteria:
|
|||
| `cia_chips.md` | common | 218 | ✅ Adequate | 8520 CIA: timers, I/O ports, TOD clock |
|
||||
| `address_space.md` | common | 175 | ✅ Adequate | 24-bit memory map: Chip, Fast, ROM, I/O, Zorro II |
|
||||
| `m68k_cpu.md` | common | 141 | ✅ Adequate | 68000 architecture baseline: registers, exceptions |
|
||||
| `zorro_bus.md` | common | 139 | ✅ Adequate | Zorro II/III expansion bus: AutoConfig, bus arbitration |
|
||||
| `zorro_bus.md` | common | 110 | ✅ Adequate | Zorro II/III expansion bus: electrical specs, bandwidth, Buster, PCI bridges; links to autoconfig.md for protocol details |
|
||||
| `autoconfig.md` | common | 662 | ✅ Deep | AutoConfig protocol: CFGIN/CFGOUT chain, ExpansionRom encoding, nibble-pair map, size codes, shut-up mechanism, Z2/Z3 differences, antipatterns, FPGA notes, boot sequence positioning |
|
||||
| `dma_architecture.md` | common | 581 | ✅ Deep | Scanline slot allocation, even/odd interleaving, DMA channel priority, bitplane DMA budget (DDFSTRT/DDFSTOP/BPLxMOD), bus arbitration (Agnus as bus master, DTACK, wait states), Blitter-Nasty/BLTPRI, per-model bus table (A1000→CD32), Buster/Ramsey/SDMAC/Gayle glue chips, AGA FMODE bandwidth equation, alignment requirements, FPGA implementation notes (Minimig state machine, SDRAM timing, common bugs), bandwidth calculation cookbook with 4 worked examples, best practices, FAQ |
|
||||
| `video_timing.md` | common | 736 | ✅ Deep | Crystal-to-CPU clock tree (PAL/NTSC), scanline anatomy, frame structure, interlace LOF/SHF, beam counters (VPOSR/VHPOSR), BEAMCON0 programmable sync, 23-pin video connector pinout, analog/digital RGB output, video slot (A2000/A3000/A4000), genlock/XCLK/PIXELSW overlay, Video Toaster, scandoublers/flicker fixers (historical + modern Indivision/RGB2HDMI/OSSC), sync→async CPU evolution, per-frame time budgets, best practices, FAQ |
|
||||
|
|
@ -256,7 +256,7 @@ Articles were scored against [AGENTS.md](../amiga/AGENTS.md) "Deep" criteria:
|
|||
| `copper_programming.md` | 319 | ✅ Deep | Copper deep dive: copper list construction, gradient, raster effects |
|
||||
| `sprites.md` | 582 | ✅ Deep | Tier 3 #15 upgrade: DMA timing, CLXCON/CLXDAT collision, V39 ExtSprite API, decision flowchart, 5 named antipatterns, multiplexing techniques, real-world use cases, FPGA notes, competitive landscape |
|
||||
| `gfx_base.md` | 237 | ✅ Adequate | GfxBase, chipset detection, display pipeline overview |
|
||||
| `text_fonts.md` | 215 | ❌ Pending | Tier 3 #16: needs cookbook (bitmap vs outline), font spacing pitfalls, ColorFont layout |
|
||||
| `text_fonts.md` | 708 | ✅ Deep | ColorFont layout, Compugraphic outlines, font scaling/aspect, 3 cookbooks, 4 antipatterns, 4 pitfalls, historical + modern analogies |
|
||||
| `copper.md` | 124 | ✅ Adequate | Copper coprocessor basics, instruction format, UCopList |
|
||||
| `blitter.md` | 109 | ✅ Adequate | Blitter DMA engine basics, minterms, BltBitMap |
|
||||
|
||||
|
|
@ -270,8 +270,8 @@ Articles were scored against [AGENTS.md](../amiga/AGENTS.md) "Deep" criteria:
|
|||
| `screens.md` | 582 | ❌ Pending | Tier 3 #17: needs antipatterns, cookbook (screen flipping, borderless, PAL→NTSC) |
|
||||
| `boopsi.md` | 505 | ✅ Adequate | OOP dispatcher, ICA interconnection, custom class tutorial |
|
||||
| `gadgets.md` | 403 | ❌ Pending | Tier 3 #20: needs antipatterns, BOOPSI command flow, GadTools→BOOPSI guide |
|
||||
| `menus.md` | 378 | ❌ Pending | Tier 3 #19: needs menu render chain diagram, RemoveMenu/FreeMenu pitfalls |
|
||||
| `windows.md` | 370 | ❌ Pending | Tier 3 #18: needs antipatterns ("The Borderless Too Soon"), FAQ, cookbook |
|
||||
| `menus.md` | 695 | ✅ Deep | Render chain diagram, 5 antipatterns, lifecycle cookbook, historical + modern analogies, use cases, 6 FAQ |
|
||||
| `windows.md` | 778 | ✅ Deep | 5 named antipatterns, window type decision guide, 3 cookbooks (resizable, borderless, shared port), historical + modern analogies, use cases, 7 FAQ |
|
||||
| `requesters.md` | 370 | ✅ Adequate | EasyRequest, ASL file/font/screenmode dialogs |
|
||||
| `intuition_base.md` | 267 | ✅ Adequate | IntuitionBase, ViewLord, LockIBase |
|
||||
|
||||
|
|
@ -319,8 +319,8 @@ Articles were scored against [AGENTS.md](../amiga/AGENTS.md) "Deep" criteria:
|
|||
| `translator.md` | 536 | ✅ Deep | Tier 1 creation: phonetic translation, narrator.device |
|
||||
| `mathffp.md` | 468 | ✅ Adequate | Motorola FFP and IEEE 754 floating point formats |
|
||||
| `locale.md` | 265 | ✅ Adequate | .cd/.ct catalog system, locale-aware formatting |
|
||||
| `layers.md` | 224 | ❌ Pending | Tier 3 #23: needs Mermaid layer stacking diagram, damage region pitfalls |
|
||||
| `expansion.md` | 806 | ✅ Deep | Zorro II/III, AutoConfig ROM layout, FPGA/Emulator implementation guide, firmware complexity tiers, SCSI boot use-case walkthrough (Phase 1-4) |
|
||||
| `layers.md` | 739 | ✅ Deep | ClipRect engine, LVO API reference, backfill hooks, refresh decision guide, 4 antipatterns, ClipBlit vs ScrollRaster, historical + modern analogies |
|
||||
| `expansion.md` | 781 | ✅ Deep | Zorro II/III, expansion.library API, ConfigDev/ExpansionRom structs, DiagArea boot ROM execution, manufacturer IDs, FPGA/Emulator implementation guide, firmware complexity tiers, SCSI boot use-case walkthrough (Phase 1-4) |
|
||||
| `utility.md` | 203 | ✅ Adequate | TagItem lists, callback hooks, date/time utilities |
|
||||
| `workbench.md` | 194 | ✅ Adequate | WBStartup, AppWindow, AppIcon, AppMenuItem |
|
||||
| `icon.md` | 188 | ✅ Adequate | .info format, DiskObject, ToolTypes, true-color icons |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue