Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

CGA Graphics Memory: Scanline Banks, Packed Pixels, and Safe Page Writes

Address IBM CGA 320-by-200 and 640-by-200 graphics correctly by accounting for packed pixels, alternating scanline banks, palette state, and adapter limits.

IBM Color/Graphics Adapter (CGA) graphics memory is not a linear array of rows. In its 200-line graphics modes, even and odd scanlines occupy separate banks within the 16 KiB display-memory aperture. A program that calculates y * bytes_per_row + x without the bank offset will draw alternating rows in the wrong place. Packed pixels introduce a second boundary: one byte represents multiple neighboring pixels, so updating one pixel requires a mask that preserves the others.

This article covers the original IBM CGA-compatible graphics layout and the BIOS mode interface around it. It does not describe VGA planar memory or VBE linear framebuffers. A clone may reproduce CGA modes but differ in composite output, timing, snow behavior, and extended palette features; validate claims on the exact adapter or emulator.

Select a known mode before touching the aperture

BIOS video mode selection is the portable first step. The common CGA graphics modes are BIOS mode 04h and 05h for 320-by-200, four-color output, and mode 06h for 640-by-200, two-color output. Modes 4 and 5 differ in palette/intensity selection rather than the basic two-bit pixel packing. Video BIOS calls establish adapter state that a direct writer otherwise must configure itself.

The conventional CGA color-memory aperture begins at physical address B8000h and covers 16 KiB. That base is not permission to write there on any display adapter: verify the active mode and memory map first. A monochrome adapter commonly uses another aperture, and EGA/VGA compatibility modes add their own register state. If an application changes mode, launches a child, or returns from a library that manages display hardware, re-establish ownership before resuming direct writes.

Even and odd scanlines use separate banks

In the standard 320-by-200 mode, each line contains 320 pixels at two bits per pixel, or 80 bytes of packed data. The display stores all even scanlines in the first 8 KiB region and all odd scanlines in the second 8 KiB region. The byte offset for a pixel at (x, y) is therefore:

bank_offset = (y & 1) ? 2000h : 0000h
row_offset  = (y >> 1) * 80
byte_offset = bank_offset + row_offset + (x >> 2)
pixel_slot  = x & 3

The four two-bit pixels in a byte are packed from the high-order pair toward the low-order pair in the CGA convention. Verify the exact bit ordering against the IBM adapter reference before implementing the mask. A safe setter reads the byte, clears only the target pair, inserts the new color bits, then writes the result. A direct byte store would erase the other three pixels on that byte.

The 640-by-200 mode uses one bit per pixel and packs eight pixels per byte, with the same even/odd scanline banking principle. Each line is 80 bytes. For pixel x, select its byte with x >> 3 and its bit position according to the documented bit order. The value chooses foreground versus background intensity; it is not an arbitrary four-bit RGB color. Do not reuse the 320-mode color setter for mode 6.

A bounded pixel update

This C-like routine is illustrative pseudocode. It assumes the caller has already selected the correct mode, mapped the aperture, and chosen the color bits for that mode:

void cga_putpixel_320(uint8_t far *video, unsigned x, unsigned y,
                      unsigned color) {
    if (x >= 320 || y >= 200 || color > 3)
        return;

    unsigned bank = (y & 1) ? 0x2000 : 0;
    unsigned offset = bank + (y >> 1) * 80 + (x >> 2);
    unsigned shift = 6 - 2 * (x & 3); /* verify bit order on target */
    uint8_t mask = (uint8_t)(3u << shift);
    uint8_t old = video[offset];
    video[offset] = (uint8_t)((old & ~mask) | ((color & 3u) << shift));
}

The function demonstrates address calculation and preservation of neighboring pixels. It is not a compiled or hardware-tested driver, and C memory-model syntax differs among 16-bit compilers. On a protected-mode extender, the address must be mapped through its supported physical-memory mechanism. A far pointer alone does not create a mapping or guarantee that writes reach CGA memory.

For a production routine, calculate the maximum offset and confirm it stays inside the intended aperture before dereferencing. Clip lines and rectangles at the screen edges with overflow-safe arithmetic. When drawing spans, process aligned groups of pixels to reduce read-modify-write operations, but keep edge masks so neighboring pixels survive. Do not allow caller-supplied coordinates to wrap a 16-bit offset.

Palette and mode state are separate from pixel storage

The pixel value indexes or selects a display color according to the active CGA mode and color-control state. Mode set, background selection, palette selection, intensity, and monitor type influence what the same memory value looks like. The CGA can drive a digital RGBI monitor or composite output, and the visible colors can differ. A memory dump alone cannot establish the displayed color without the adapter’s palette and output path.

Prefer BIOS mode and palette calls if the application needs compatibility. If the program writes I/O registers directly, record the exact register values it changes and restore them on exit. Do not use a guessed “default palette” after returning from a child program. A child’s BIOS call may have changed both memory and palette state.

Timing and CGA snow

The original CGA display shares memory access between the CPU and display logic. On some original hardware, writes made during active display can cause visible transient artifacts often called snow. This is a timing characteristic of particular adapters, not a guarantee that every CGA clone or emulator exhibits the same behavior. Some programs wait for a display status bit or use BIOS services to avoid writes during sensitive intervals; those methods have timing and compatibility costs of their own.

Do not assume a fixed delay loop synchronizes with vertical retrace. CPU frequency, wait states, emulator scheduling, and the adapter’s status implementation change timing. If artifact-free output is a requirement, test the exact hardware and choose a documented synchronization method. Otherwise, avoid promising a snow-free display on all clones.

Pages, buffers, and restoration

CGA graphics memory is limited compared with later adapters. The two scanline banks together form the visible 200-line image; do not assume there is a second full off-screen page. A program can allocate a software back buffer in conventional memory, but copying it to video memory still takes time and must respect the layout and any display timing constraints. The BIOS page-selection API for text modes does not create arbitrary full graphics pages in the classic 16 KiB CGA aperture.

Before entering graphics mode, save the previous BIOS mode and any state the program modifies. On normal exit, restore through the BIOS where possible and verify text display, cursor, and palette behavior. If the program crashes after direct register changes, a reboot may be the only reliable reset. Provide an escape path and keep recovery instructions visible in the startup profile.

Acceptance tests

Use a known IBM-compatible CGA mode. Draw a different pattern on an even line and adjacent odd line, then verify that the two patterns occupy the expected separate banks. Exercise first and last pixels of a row, all packed pixel positions within one byte, and both ends of the 200-line image. Confirm that changing one pixel leaves its neighbors unchanged. Test mode 4, mode 5, and mode 6 separately if the application claims support for all three.

Repeat on original CGA hardware or a named emulator configuration if color output, snow behavior, or timing is part of the claim. Record video mode, adapter/clone, monitor or composite path, BIOS, and test pattern. A VGA card’s CGA-compatible mode can verify the basic memory algorithm but cannot establish analog composite behavior of an original CGA board.

The critical invariant is that row parity selects the 8 KiB bank before the row-within-bank calculation. Combine that with packed-pixel masks and explicit mode/palette ownership, and the graphics path becomes predictable. A simple linear framebuffer formula is wrong for the original CGA layout.

Related:

Sources:

Comments