Programming the MEGA65 — Chapter 12: Graphics

Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor

Introduction

The VIC-IV is three video chips in one. It behaves as a VIC-II for C64 software, adds the C65's VIC-III features of 80 columns, bitplanes and a 256-colour palette, and then goes much further with its own modes. Like its ancestors it has no frame buffer: it builds each line of the display as it is drawn, from screen memory, character data and colour RAM. That design is why character graphics remain central on the MEGA65, even though it can display far more than a C64.

The native display is 720 by 576 pixels, and the chip has four banks of 256 palette entries: one for text and bitmap graphics, one for sprites, and an alternate bank that individual characters can select, so more than one palette's worth of colours can be on screen at once (mega65-core, viciv.vhdl, September 2026).

This chapter works from the easiest methods to the most detailed:

The VIC-II registers at $D000-$D02E work as on the C64, and the techniques in any C64 graphics reference still apply. This chapter concentrates on what is new. Most of the registers described here exist only in the VIC-IV personality, so a program starting in C64 mode must perform the knock from Chapter 5 first.

Graphics from BASIC 65

BASIC 65 has a full set of graphics commands, inherited from the C65's BASIC 10 and extended since. They draw on a bitplane screen held in banks 4 and 5, which is why Chapter 6 advised reserving those banks with MEM before using them for anything else.

Group Commands Purpose
Screens SCREEN, SCREEN CLOSE Open a graphics screen of a given size and colour depth, and close it
Drawing PEN, DOT, LINE, BOX, CIRCLE, ELLIPSE, POLYGON, PAINT Choose a colour and draw shapes and fills
Text and blocks CHAR, CUT, PASTE Draw text on a graphics screen; copy rectangular areas
Colour PALETTE, PALETTE RESTORE, BACKGROUND, BORDER, FOREGROUND Change palette entries and screen colours
Sprites SPRITE, MOVSPR, SPRCOLOR, SPRSAV Enable, move, colour and load sprites
Registers SETBIT, CLRBIT, WPOKE, WPEEK Change single bits and 16-bit values directly

A short program shows the pattern. It opens a 320 by 200 screen, draws, waits for a key, and returns to text:

10 SCREEN 320,200,4
20 PEN 1
30 LINE 0,0,319,199
40 CIRCLE 160,100,50
50 GETKEY A$
60 SCREEN CLOSE

Chapter 3 gives SCREEN's full syntax.

When to go further

The graphics commands are ideal for charts, diagrams, title screens and games where drawing speed is not critical. For fast animation, scrolling, or the VIC-IV's character-based colour modes, programs work with the registers directly. BASIC 65 makes that practical too: SETBIT, CLRBIT, WPOKE and WPEEK change bits and 16-bit registers in one statement, and many of the register examples in the rest of this chapter can be typed in as BASIC.

PALETTE RESTORE deserves a mention: it returns the system palette to its defaults, so any program that changes the palette can put things back before it ends. Every example in this chapter that changes VIC-IV state should likewise restore it, or the screen editor may be left unusable.

Where the Display Lives

The VIC-II found its screen and character set through a few bits in $D018 and the bank bits of CIA 2, which restricted them to a 16KB window. The VIC-IV replaces that with pointers that can address memory directly, at byte granularity, anywhere the chip can see.

Layout registers

Register Name Purpose
$D058-$D059 LINESTEP Bytes of screen and colour memory per character row
$D05E CHRCOUNT Characters drawn per row, low 8 bits; bits 9-8 are in $D063 bits 4-5
$D060-$D062 SCRNPTR Address of screen memory
$D064-$D065 COLPTR Offset of colour memory from $FF80000
$D068-$D06A CHARPTR Address of the character set
$D06C-$D06E SPRPTRADR Address of the sprite pointer list
$D07B DISPROWS Number of character rows drawn

LINESTEP, CHRCOUNT, SCRNPTR and COLPTR are described in Dan Sanderson's and Retrocogs' VIC-IV tutorials; check the CHARPTR and DISPROWS addresses against the MEGA65 Book (Dan Sanderson, Retrocogs).

SCRNPTR can point anywhere with byte precision, which makes coarse scrolling trivial: adding one to SCRNPTR scrolls the screen one character left. COLPTR is an offset into colour RAM rather than a full address (Retrocogs). It is 16 bits wide, but colour RAM is 32KB, so the VIC-IV uses only its low 15 bits: an offset of $8000 or more wraps round to the start of colour RAM (mega65-core, viciv.vhdl, September 2026).

LINESTEP and CHRCOUNT are separate because they answer different questions. CHRCOUNT says how many characters to draw; LINESTEP says how far apart rows are in memory. Setting LINESTEP larger than CHRCOUNT creates a screen wider than the display, which can then be scrolled horizontally by moving SCRNPTR.

Mode registers

Register Bit Name Effect when set
$D031 7 H640 80-column, 640-pixel-wide display
$D031 5 ATTR Enables VIC-III text attributes; clear it to suppress them
$D031 4 BPM Bitplane mode
$D031 3 V400 400-line vertical resolution
$D054 0 CHR16 Two bytes of screen and colour memory per character (SEAM)
$D054 1 FCLRLO Full-colour mode for screen codes 0-255
$D054 2 FCLRHI Full-colour mode for screen codes 256 and up

Hot registers

When a program writes to the C64-style registers that choose a screen or character set location, the VIC-IV calculates the equivalent address and updates its own pointers, such as CHARPTR, automatically. Five registers behave this way: $D011, $D016, $D018, $D031, and the VIC bank bits of $DD00. A write to any one of them propagates all five, so a program that has set CHARPTR directly can lose that setting simply by turning on multicolour mode in $D016 (Dan Sanderson).

Programs that manage the VIC-IV pointers themselves should turn this off by clearing HOTREG, bit 7 of $D05D:

CLRBIT $D05D,7

With HOTREG clear, the C64-style registers no longer update the VIC-IV pointers, and the program has full control. One side effect is worth knowing: the C65 shifted 80-column text slightly and compensated through the horizontal scroll bits, and the MEGA65 reproduces this. After clearing HOTREG and choosing a width with H640, set TEXTXPOS at $D04C to its standard value of 80 to place the text correctly (Dan Sanderson).

Text Modes

Text is still the VIC-IV's most important mode. The screen editor uses it, and most MEGA65 games build their graphics from characters.

Sizes

Columns × rows H640 V400 Bytes per screen (normal mode)
40 × 25 0 0 1,000
80 × 25 1 0 2,000
40 × 50 0 1 2,000
80 × 50 1 1 4,000

When changing size by hand, set LINESTEP and CHRCOUNT to match, and make sure screen memory and colour RAM are large enough. From BASIC, the editor's own screen-size commands handle this for you.

Attributes

In normal text mode, each character has one byte of screen memory, its screen code, and one byte of colour RAM. The low four bits of colour RAM are the colour. The high four bits are VIC-III attributes: blink (bit 4), reverse (bit 5), bold (bit 6) and underline (bit 7), so text can be styled without a different character set. Clearing ATTR, bit 5 of $D031, switches the attributes off system-wide (Dan Sanderson).

SEAM: two bytes per character

Setting CHR16, bit 0 of $D054, switches the VIC-IV into what the MEGA65 calls Super-Extended Attribute Mode, or SEAM. Every character now takes two bytes of screen memory and two bytes of colour RAM (Retrocogs).

The extra screen byte extends the character number to 13 bits, so a screen can use up to 8,192 different characters instead of 256. The extra colour byte carries further per-character settings, and the second colour byte holds the colour itself. Among those settings, bit 7 of the first colour byte flips the character vertically and bit 6 flips it horizontally, so one character definition can be reused facing the other way (mega65-core, viciv.vhdl). Ordinary monochrome characters can then take their foreground from the first 32 palette entries, and the full-colour modes below reach the whole palette (Dan Sanderson).

Because each character now takes two bytes, LINESTEP must be at least twice CHRCOUNT: 160 for an 80-column row. The screen editor's memory is not laid out for this, so SEAM programs move SCRNPTR to their own area.

One 16-bit value is not a character. In SEAM, a screen word of $FFFF marks the end of a row: the VIC-IV stops drawing the row there, and the next row starts straight after the marker instead of LINESTEP bytes after the start of this one. Rows ended this way can therefore have different lengths, and LINESTEP does not matter for them. That makes it easy to build a screen one line at a time: write each line's characters followed by $FFFF, and the next line follows on directly. This works in release cores as well as development. A program that only wants ordinary characters must never store $FFFF in screen memory, so character $1FFF cannot be combined with all of the high screen-byte bits set (mega65-core, viciv.vhdl, September 2026).

In BASIC:

100 S1=WPEEK($D060) : S2=WPEEK($D062)  : REM SAVE SCRNPTR
110 SETBIT $D054,0                      : REM CHR16 ON
120 WPOKE $D058,160                     : REM LINESTEP 80 X 2
130 WPOKE $D060,$0000 : WPOKE $D062,$05 : REM SCREEN AT $50000
...
900 CLRBIT $D054,0 : WPOKE $D058,80     : REM RESTORE
910 WPOKE $D060,S1 : WPOKE $D062,S2

In SEAM, screen memory is written with WPOKE, low byte first. A screen code below 256 still refers to the character set at CHARPTR, so ordinary PETSCII text continues to work alongside the new modes in the next section.

Full-Colour and Nybble-Colour Characters

A C64 character has one bit per pixel: foreground or background. The VIC-IV can give a character one byte per pixel, choosing every pixel's colour directly from the palette. This is Full Colour Mode, or FCM. Nybble Colour Mode, NCM, is a lighter variant using four bits per pixel. Both require SEAM.

Full Colour Mode

In FCM each character is 8 by 8 pixels and takes 64 bytes, one per pixel. A pixel value of $00 shows the background colour, $FF shows the character's colour from colour RAM, and $01-$FE select palette entries directly (Dan Sanderson).

The most important fact about FCM, and the most common stumbling block, is that screen memory no longer holds a screen code. It holds the address of the character's 64 bytes of data divided by 64, and CHARPTR is ignored. Screen value $1000 therefore means the data at $1000 × $40 = $40000. With 13 bits available, FCM data can lie anywhere in chip RAM, aligned to a 64-byte boundary (Dan Sanderson).

Two bits in $D054 decide which screen values are treated this way:

Bit Name FCM applies to
1 FCLRLO Screen values 0-255
2 FCLRHI Screen values 256 and above

The usual choice is FCLRHI alone. Values below 256 remain ordinary characters from the character set at CHARPTR, so text and full-colour graphics share the screen, while values from 256 up point at FCM data from $4000 upward. FCLRLO would make the first 16KB of memory the only place for full-colour data, at the cost of losing the normal font, so it is rarely useful (Dan Sanderson).

A full 80 by 25 screen of unique FCM characters needs 2,000 × 64 = 128,000 bytes of data, which fits in banks 4 and 5. In return, every pixel on the screen can be any palette colour, and scrolling or animating still means changing only screen memory.

Setting up FCM in machine language

        lda #%00000001
        tsb $d054       ; CHR16: SEAM on
        lda #%00000100
        tsb $d054       ; FCLRHI: FCM for values 256 and up
        lda #<160
        sta $d058
        lda #>160
        sta $d059       ; LINESTEP = 80 characters x 2 bytes

Then point SCRNPTR at a free area, fill it with 16-bit values from $0100 upward, and place the character data at the matching addresses.

The attribute bits still apply in SEAM, in the high nybble of the second colour byte. In FCM they cut the colour that $FF pixels show to the first 16 palette entries and can make characters blink or underline; in NCM the same bits select the sub-palette, so they must not also trigger blinking or underlining (mega65-core, viciv.vhdl, September 2026). Clear ATTR, bit 5 of $D031, to suppress the attributes system-wide (Dan Sanderson). Because $D031 is a hot register, either clear HOTREG first or change $D031 before setting the VIC-IV pointers.

Nybble Colour Mode

NCM is not a separate screen mode but a property of individual SEAM characters, selected by bit 3 of the first byte of the character's colour RAM entry. Setting that bit makes the character a 4-bit full-colour character whatever FCLRLO and FCLRHI say (mega65-core, viciv.vhdl, September 2026; Dan Sanderson). Each byte of character data then holds two pixels of four bits each, and three details follow from that:

Because NCM characters are twice as wide, a row of them needs half as many characters to fill the screen. A full row of NCM characters is 40 characters, or 80 bytes, so LINESTEP can be 80 instead of 160. Mixing NCM and ordinary characters on one row is allowed, which is the first sign that a VIC-IV row is not a fixed grid of cells.

The Palette

The C64 had sixteen fixed colours. The MEGA65 has palettes of 256 entries, each entry an arbitrary colour chosen by its red, green and blue values. Every colour number used anywhere on the display, from border to sprite pixel, is an index into a palette.

Palette registers

Registers Contents
$D100-$D1FF Red value for entries 0-255
$D200-$D2FF Green value for entries 0-255
$D300-$D3FF Blue value for entries 0-255

Each value is 8 bits, but with its two nybbles swapped: the high nybble of the colour component is stored in the low four bits of the register, and the low nybble in the high four bits (Dan Sanderson). A red value of $A5 is therefore written as $5A. Converting at the point of storage keeps source data readable:

10 C=1 : R=255 : G=128 : B=0          : REM ENTRY 1 = ORANGE
20 POKE $D100+C, ((R AND 15)<<4) OR (R>>4)
30 POKE $D200+C, ((G AND 15)<<4) OR (G>>4)
40 POKE $D300+C, ((B AND 15)<<4) OR (B>>4)

In machine language, a 256-byte lookup table of swapped values, or a short shift-and-rotate sequence, does the same job. Tools that convert images for the MEGA65 usually swap the nybbles when they write the palette file, so the bytes can be copied straight into the registers.

The first 16 entries default to the C64's colours, so programs that use only colours 0-15 look as they would on a C64. BASIC 65's PALETTE COLOR command changes an entry without any register work, and PALETTE RESTORE puts the defaults back.

Several palettes

The VIC-IV keeps four palette banks of 256 entries. $D070 chooses which bank the $D100-$D3FF registers edit (bits 7-6), which bank text and bitmap graphics use (bits 5-4), which bank sprites use (bits 3-2), and an alternate bank (bits 1-0) that a character selects with the bold and reverse attributes. Text and bitplanes share a bank; sprites can use a different one, which allows more than 256 different colours on screen at once without changing the palette mid-frame (mega65-core, viciv.vhdl, September 2026).

Development cores add BOLDISALT, bit 4 of $D053. When it is set, the bold attribute alone selects the alternate palette, without reverse, and bold no longer adds 16 to the colour. It is not in release 0.97 cores, and it is cleared at reset (mega65-core, viciv.vhdl, development).

Palette effects

Because the display reads the palette as it draws, changing an entry changes every pixel of that colour instantly. This makes some effects nearly free: fading a screen means adjusting a few dozen palette entries rather than rewriting graphics, and colour cycling, the classic technique for animating water, fire or conveyor belts, means rotating a range of entries each frame. Changing palette entries from a raster interrupt partway down the screen gives a different set of colours above and below the split.

Sprites

The VIC-IV keeps the VIC-II's eight hardware sprites and every one of their C64 registers, so $D000-$D02E sprite code works unchanged. The MEGA65 adds new options on top, all of which can be enabled per sprite.

Where the pointers are

On the C64, the eight sprite pointers sit in the last eight bytes of the screen, at $07F8 by default. In MEGA65 mode the screen is elsewhere, and at power-on the pointers are at $0FF8 (Dan Sanderson). C64 type-in programs that POKE pointers at 2040 therefore need changing, as do programs that put sprite data in memory the MEGA65 uses for other things, such as the C64's cassette buffer at $0340.

The VIC-IV can also move the pointer list anywhere: $D06C-$D06E hold its address. That is useful when screen memory is full of FCM data and the default pointer location falls in the middle of a tile (MEGA65 blog). The VIC-IV also offers 16-bit sprite pointers, so sprite data is not confined to one 16KB bank as on the C64.

New sprite features

Feature What it gives
16-colour sprites 15 colours plus a transparent one chosen per sprite; sprite n uses palette entries n×16 to n×16+15, or 128 higher with bitplane mode on (mega65-core, sprite.vhdl)
64-pixel-wide sprites Enabled per sprite in SPRX64EN, $D057: 64, 32 and 16 pixels across for hi-res, multicolour and 16-colour sprites (MEGA65 blog)
Variable height Sprites taller or shorter than 21 lines
High-resolution positioning Coordinates and pixel sizes matching the 640-pixel display
Palette banks A choice of four palette banks for sprite colours (Dan Sanderson)
Bitplane modification A sprite can modify one or more bitplanes of the output rather than simply covering it (MEGA65 blog)

The 16-colour mode is the biggest change for game graphics. With the 64-pixel-wide option enabled, a 16-colour sprite is 16 pixels across, each pixel any of 15 colours or transparent. Combined with variable height and an eight-sprite multiplexer driven by raster interrupts, it gives arcade-quality moving objects.

The MEGA65 Book lists the enable registers for 16-colour mode, height and high-resolution positioning.

Sprites from BASIC

BASIC 65's SPRITE command turns sprites on and sets their colour, size and mode; MOVSPR moves them, optionally along a path at a set speed; SPRCOLOR sets the shared multicolour registers; and SPRSAV copies sprite data to and from strings. SPRITE switches a sprite between monochrome and VIC-II multicolour, and from ROM 920415 SPRITE SYS R turns on high-resolution sprites (MEGA65 ROM change log). BASIC does not support 16-colour and the other VIC-IV sprite modes (Dan Sanderson), so for those, BASIC programs set the registers directly with SETBIT and POKE.

Bitplanes

The C65 introduced bitplane graphics, the Amiga's way of building a colour image: several one-bit-per-pixel images, the bitplanes, are overlaid, and the bits at each pixel position combine into a palette index. With n bitplanes there are 2ⁿ colours, up to 8 bitplanes and 256 colours.

Setting Options
Mode BPM, bit 4 of $D031, selects bitplane graphics
Width 320 or 640 pixels, by H640
Height 200 or 400 lines, by V400
Depth 1 to 8 bitplanes

On the C65, $D032 chooses which bitplanes are displayed and $D033-$D03A hold their addresses. These are VIC-III registers, so they are reached in the VIC-III or VIC-IV personality.

Memory

Each bitplane of a 320 by 200 screen takes 8,000 bytes, and memory grows with each step up in size or depth:

Screen Bytes per bitplane 4 bitplanes (16 colours) 8 bitplanes (256 colours)
320 × 200 8,000 32,000 64,000
640 × 200 16,000 64,000 128,000
640 × 400 32,000 128,000 256,000

The larger combinations exceed the 128KB of banks 4 and 5, so high resolutions are normally used with fewer colours. BASIC 65's graphics commands draw on bitplane screens in banks 4 and 5, which is why SCREEN takes a depth argument.

Bitplanes or full-colour characters?

For a static picture, either works. For anything that moves, full-colour characters are usually better. Moving an object across a bitplane screen means rewriting bits in several planes; moving it across a character screen means changing a few screen codes. FCM also lets repeated tiles share one copy of their data, while bitplanes store every pixel individually. Bitplanes suit drawing programs, line graphics and images with no repetition; characters suit games, scrolling and user interfaces.

Raster Techniques

Chapter 8 showed raster interrupts with the VIC-II registers. On the MEGA65 they power the same effects as on the C64, such as split screens, colour bars and sprite multiplexing, and at 40.5 MHz there is far more time to do work between interrupts. The VIC-IV also offers something the C64 never had: a way to draw characters on top of other characters, at any horizontal position, without using sprites.

The Raster Rewrite Buffer

The VIC-IV builds each line of the display in an internal buffer before sending it to the screen. The bytes of a row in screen and colour memory act as a list of drawing instructions: fetch this character, paint its pixels, move right by its width. Usually each instruction draws a character. A special instruction instead moves the drawing position, and because it can move backwards, later characters can be drawn over earlier ones. That buffer is called the Raster Rewrite Buffer, or RRB, and it is the basis of layered, parallax and many-object graphics on the MEGA65 (Dan Sanderson).

GOTOX

The position-moving instruction is called GOTOX. It occupies one character slot in SEAM, two bytes of screen memory and two of colour memory, and is marked by bit 4 of its first colour byte:

Byte Bits Meaning in a GOTOX entry
Colour byte 0 4 GOTOX flag: this entry is an instruction, not a character
Colour byte 0 7 Transparency: colour 0 in following characters does not overwrite the buffer
Screen byte 0 7-0 New X position, bits 7-0, in pixels from the left border
Screen byte 1 1-0 New X position, bits 9-8
Screen byte 1 7-5 Y offset of 0 to 7 pixels for the following characters, drawing each one that many rows higher
Screen byte 1 4 Subtract the Y offset instead, drawing the following characters lower (core 0.97 and later)
Colour byte 0 3 ROWMASK: draw the following characters only on the pixel rows whose bits are set in colour byte 1
Colour byte 1 7-0 The row mask, one bit per pixel row, used when ROWMASK is set
Colour byte 0 6 Treat the following characters as background, for sprite priority
Colour byte 0 2 Treat the following characters as foreground, for sprite priority

To draw a soldier on a patch of flowers, a row contains three entries: the flower character, a GOTOX back to the flower's X position with transparency set, and the soldier character. The soldier's colour-0 pixels let the flowers show through. Transparency stays on for the rest of the row, or until another GOTOX changes it (Dan Sanderson).

The Y offset and the row mask together let an object move vertically by single pixels within a character row: offset its character, and mask off the rows that would otherwise spill into the row above or below. The offset, row mask and priority settings, like transparency, last for the rest of the row or until the next GOTOX (mega65-core, viciv.vhdl, release 0.97). On release 0.97 cores, a GOTOX with ROWMASK set whose row mask has bits 5 and 6 both set also switches the following characters to the alternate palette, as bold and reverse do; development cores fix this (mega65-core PR #928).

Organising layers

Three ways of arranging a row are common:

  1. Stacks per position. At each place on the row, draw every layer's character with a GOTOX between them. Compact, but changing one object means rebuilding the row.
  2. Full layers. Draw a whole row of background, GOTOX to 0 with transparency, a whole row of the next layer, and so on. Simple to address, since every layer has a fixed offset, but sparse foreground layers waste memory on empty characters.
  3. Background plus draw list. Draw the full background, then a list of GOTOX and character pairs for the objects on that row. Economical, at the cost of rebuilding each row's list when objects move.

Whichever is used, the row must end with a GOTOX to the right border followed by a character, or the VIC-IV treats the last drawing position as the end of the line and leaves the rest of it blank (Dan Sanderson).

Sizing rows

GOTOX entries use character slots, so CHRCOUNT must cover every character and every GOTOX in the busiest row, and LINESTEP must be at least twice CHRCOUNT in SEAM. Making LINESTEP a power of two, such as 128 rather than the minimum 124, wastes a few bytes but makes row addresses quick to compute in machine language.

Time limits

The VIC-IV has only so long to fill the buffer for each line. Its "double buffering", which prepares one line while drawing the previous one, should be left on by keeping NORRDEL, bit 7 of $D051, clear. At 200 lines, the time available can be doubled by setting V400, setting CHRYSCL at $D05B to 0, setting DBLRR, bit 6 of $D051, and moving TEXTYPOS at $D04E up by two pixels. In one test, twelve rows of 20 NCM characters handled 11 full layers before lines started to go missing (Dan Sanderson).

The limits differ slightly between real hardware and the Xemu emulator, so test RRB-heavy code on both. Bit 5 of $D60F, REALHW, reads 1 on a real MEGA65 and 0 in Xemu, if a program needs to adjust.

Smooth scrolling

The VIC-IV also scrolls more freely than the VIC-II. Coarse scrolling is a matter of changing SCRNPTR and COLPTR, which can point anywhere, so a map wider than the screen needs only a larger LINESTEP and a moving pointer. Fine scrolling uses the pixel-level text position registers, TEXTXPOS at $D04C and TEXTYPOS at $D04E, together with one extra character per row so the partly visible column at the edge is filled (Retrocogs). Parallax layers combine scrolling with the RRB: each layer is a section of the row whose GOTOX position is offset by a different amount.

Plotting Pixels and Drawing Lines

On bitplane screens opened by BASIC 65, DOT plots a pixel and LINE draws a line, and nothing more is needed. This section gives machine-language equivalents for every mode in this chapter: one plot routine per mode, and a single line routine that works with all of them by calling the plot routine through a vector.

The routines favour clarity over speed. Each plot computes its address from scratch, using the hardware multiplier from Chapter 5 for the row and the Q register from Chapter 7 for the 32-bit sum, then writes through a flat [ptr],Z pointer so the bitmap can be anywhere in memory. A faster version would precompute a table of row addresses.

Address formulas

All of these modes store the screen in 8-line cell rows, so each formula has the same shape: a cell-row offset, plus an offset within the row. Here cw is the cell width in pixels and rowbytes the bytes per cell row.

Mode Bits per pixel Cell width (cw) rowbytes at 320 wide
VIC-II hi-res bitmap, bitplane, monochrome SEAM quilt 1 8 320
VIC-II multicolour bitmap 2 4 320
FCM quilt 8 8 2,560
NCM quilt 4 16 1,280

The byte offset of pixel (x, y) in each mode:

1 bit:    (y/8)*rowbytes + (x AND $FFF8) + (y AND 7)
          bit mask = $80 >> (x AND 7)

2 bits:   (y/8)*rowbytes + (x/4)*8 + (y AND 7)
          bits 7-6 hold the pixel where x AND 3 = 0

FCM:      (y/8)*rowbytes + (x/8)*64 + (y AND 7)*8 + (x AND 7)

NCM:      (y/8)*rowbytes + (x/16)*64 + (y AND 7)*8 + (x AND 15)/2
          low nybble for even x, high nybble for odd x

Bitplanes use the same cell order as the C64 bitmap: the address of a 320-wide bitplane byte is INT(Y/8)*320 + (Y AND 7) + INT(X/8)*8 plus the plane's start (MEGA65 blog). At 640 wide, rowbytes doubles. The 400-line interlaced modes split even and odd lines between two plane addresses and are not covered here.

A "quilt" is a SEAM screen whose screen memory is filled row by row with ascending character values, as in Dan Sanderson's full-screen FCM example, so that the character data forms a bitmap in cell order (Dan Sanderson). For FCM or NCM data at $40000, the screen values run from $1000 upward; for a monochrome quilt, they run upward from the character at CHARPTR.

Shared variables and row address

The routines share these variables. ptr and the line variables must be in the base page; the rest can be anywhere. A private base page, as in Chapter 9, avoids conflicts with the KERNAL.

Variable Size Contents
px 2 X coordinate
py 1 Y coordinate
pcol 1 Colour: 0 or 1, 0-3, 0-255 or 0-15 by mode
bmbase 4 28-bit address of the bitmap or character data
rowbytes 2 Bytes per cell row, from the table above
xyoff 4 Offset within the cell row, computed by each plot routine
ptr 4 Flat pointer to the target byte (base page)
tmp 1 Scratch
; rowaddr: ptr = bmbase + (py / 8) * rowbytes + xyoff
; Uses the hardware multiplier and Q. Returns with Z = 0.
rowaddr lda py
        lsr
        lsr
        lsr             ; cell row = py / 8
        sta $d770       ; MULTINA
        lda #0
        sta $d771
        sta $d772
        sta $d773
        sta $d776
        sta $d777
        lda rowbytes
        sta $d774       ; MULTINB = rowbytes
        lda rowbytes+1
        sta $d775
        ldq $d778       ; Q = cell row * rowbytes
        clc
        adcq bmbase
        clc
        adcq xyoff
        stq ptr
        ldz #0
        rts

1 bit per pixel: hi-res bitmap, bitplanes, monochrome quilt

A pcol of 0 clears the pixel; any other value sets it.

plot1   lda px
        and #$f8
        sta xyoff
        lda py
        and #$07
        ora xyoff       ; (x AND $F8) OR (y AND 7)
        sta xyoff
        lda px+1
        sta xyoff+1
        lda #0
        sta xyoff+2
        sta xyoff+3
        jsr rowaddr
        lda px
        and #$07
        tax
        lda pcol
        beq clr1
        lda [ptr],z
        ora bit1,x
        sta [ptr],z
        rts
clr1    lda bit1,x
        eor #$ff
        and [ptr],z
        sta [ptr],z
        rts

bit1    .byte $80, $40, $20, $10, $08, $04, $02, $01

For a bitplane screen, plot once per plane, passing each bit of the colour in turn and each plane's address as bmbase:

; plotbp: plot colour in pcol on nplanes bitplanes
; planes: table of 4-byte plane addresses
plotbp  lda pcol
        sta cbits
        lda #0
        sta plane
bploop  lda plane
        asl
        asl
        tay             ; 4 bytes per table entry
        ldx #0
copy    lda planes,y
        sta bmbase,x
        iny
        inx
        cpx #4
        bne copy
        lda cbits
        and #1
        sta pcol        ; this plane's bit
        jsr plot1
        lsr cbits
        inc plane
        lda plane
        cmp #nplanes
        bne bploop
        rts

plotbp uses pcol as working space, so reload pcol before each call.

2 bits per pixel: multicolour bitmap

Multicolour pixels are twice as wide, so x runs from 0 to 159 and four pixels share a byte. pcol is 0 to 3.

A mask table and a table of pre-shifted colours avoid shifting bits at run time:

plotmc  lda px
        and #$fc
        asl             ; (x / 4) * 8 = (x AND $FC) * 2
        sta xyoff
        lda #0
        rol
        sta xyoff+1
        lda py
        and #$07
        ora xyoff
        sta xyoff
        lda #0
        sta xyoff+2
        sta xyoff+3
        jsr rowaddr
        lda px
        and #$03
        asl
        asl
        ora pcol        ; index = (x AND 3) * 4 + colour
        tay
        lda px
        and #$03
        tax
        lda [ptr],z
        and mcmask,x    ; clear the pixel's two bits
        ora mccol,y     ; insert the colour
        sta [ptr],z
        rts

mcmask  .byte $3f, $cf, $f3, $fc
mccol   .byte $00, $40, $80, $c0   ; x AND 3 = 0
        .byte $00, $10, $20, $30   ; x AND 3 = 1
        .byte $00, $04, $08, $0c   ; x AND 3 = 2
        .byte $00, $01, $02, $03   ; x AND 3 = 3

8 bits per pixel: FCM quilt

plotfcm lda px
        and #$f8
        sta xyoff
        lda px+1
        sta xyoff+1
        lda #0
        sta xyoff+2
        sta xyoff+3
        asl xyoff
        rol xyoff+1
        asl xyoff
        rol xyoff+1
        asl xyoff
        rol xyoff+1     ; (x AND $FFF8) * 8 = (x / 8) * 64
        lda py
        and #$07
        asl
        asl
        asl
        sta tmp         ; (y AND 7) * 8
        lda px
        and #$07
        ora tmp
        ora xyoff       ; low 6 bits of xyoff are zero
        sta xyoff
        jsr rowaddr
        lda pcol
        sta [ptr],z
        rts

4 bits per pixel: NCM quilt

NCM characters are 16 pixels wide; the left pixel of each byte is the low nybble. pcol is 0 to 15, the index within the character's 16-colour group; 0 leaves the pixel showing the background.

plotncm lda px
        and #$f0
        sta xyoff
        lda px+1
        sta xyoff+1
        lda #0
        sta xyoff+2
        sta xyoff+3
        asl xyoff
        rol xyoff+1
        asl xyoff
        rol xyoff+1     ; (x AND $FFF0) * 4 = (x / 16) * 64
        lda py
        and #$07
        asl
        asl
        asl
        sta tmp         ; (y AND 7) * 8
        lda px
        and #$0e
        lsr             ; (x AND 15) / 2
        ora tmp
        ora xyoff
        sta xyoff
        jsr rowaddr
        lda px
        lsr             ; C = x bit 0
        bcs ncmr
        lda [ptr],z     ; even x: low nybble
        and #$f0
        ora pcol
        sta [ptr],z
        rts
ncmr    lda pcol        ; odd x: high nybble
        asl
        asl
        asl
        asl
        sta tmp
        lda [ptr],z
        and #$0f
        ora tmp
        sta [ptr],z
        rts

Drawing lines

The line routine is Bresenham's algorithm in its all-octant form. It works in whole pixels for any slope, calls the plot routine whose address is in plotvec for each point, and draws both end points. x0, y0, x1, y1, dx, dy, err and e2 are 16-bit base-page variables; sx and sy are single bytes holding 1 or -1. Coordinates on the MEGA65 are small enough that no intermediate value can overflow 16 bits, so the signed comparisons test N alone.

; line: draw from (x0,y0) to (x1,y1) with the routine in plotvec
line    sec             ; dx = |x1 - x0|, sx = direction
        lda x1
        sbc x0
        sta dx
        lda x1+1
        sbc x0+1
        sta dx+1
        lda #1
        sta sx
        bit dx+1
        bpl dxok
        lda #$ff
        sta sx
        sec             ; dx = -dx
        lda #0
        sbc dx
        sta dx
        lda #0
        sbc dx+1
        sta dx+1
dxok    sec             ; dy = -|y1 - y0|, sy = direction
        lda y1
        sbc y0
        sta dy
        lda y1+1
        sbc y0+1
        sta dy+1
        lda #$ff
        sta sy
        bit dy+1
        bmi dyok        ; y1 < y0: dy is already negative
        lda #1
        sta sy
        sec             ; dy = -dy
        lda #0
        sbc dy
        sta dy
        lda #0
        sbc dy+1
        sta dy+1
dyok    clc             ; err = dx + dy
        lda dx
        adc dy
        sta err
        lda dx+1
        adc dy+1
        sta err+1

loop    lda x0          ; plot (x0, y0)
        sta px
        lda x0+1
        sta px+1
        lda y0
        sta py
        jsr plot
        lda x0          ; done when (x0, y0) = (x1, y1)
        cmp x1
        bne step
        lda x0+1
        cmp x1+1
        bne step
        lda y0
        cmp y1
        bne step
        lda y0+1
        cmp y1+1
        bne step
        rts

step    lda err         ; e2 = 2 * err
        asl
        sta e2
        lda err+1
        rol
        sta e2+1
        sec             ; if e2 >= dy: err += dy, x0 += sx
        lda e2
        sbc dy
        lda e2+1
        sbc dy+1
        bmi noxstep
        clc
        lda err
        adc dy
        sta err
        lda err+1
        adc dy+1
        sta err+1
        lda sx
        bmi xdec
        inw x0
        bra noxstep
xdec    dew x0
noxstep sec             ; if e2 <= dx: err += dx, y0 += sy
        lda dx
        sbc e2
        lda dx+1
        sbc e2+1
        bmi loop
        clc
        lda err
        adc dx
        sta err
        lda err+1
        adc dx+1
        sta err+1
        lda sy
        bmi ydec
        inw y0
        bra loop
ydec    dew y0
        bra loop

plot    jmp (plotvec)

INW and DEW step the coordinates in one instruction each, which is why the line variables live in the base page.

Using the routines

Select a mode by setting bmbase, rowbytes and plotvec, then draw:

        lda #<plotfcm
        sta plotvec
        lda #>plotfcm
        sta plotvec+1
        lda #$00        ; bmbase = $00040000
        sta bmbase
        sta bmbase+1
        sta bmbase+3
        lda #$04
        sta bmbase+2
        lda #<2560      ; 40 FCM characters x 64 bytes
        sta rowbytes
        lda #>2560
        sta rowbytes+1
        lda #5
        sta pcol        ; palette entry 5
        ; set x0, y0, x1, y1 ...
        jsr line

The plot routines change A, X, Y and Z, since Q includes all four, so a caller must not expect any register to survive. They also leave the hardware multiplier holding their last result, which matters only to code that uses the multiplier itself from an interrupt.

Summary

Chapter 13 turns to sound.