Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor
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.
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.
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.
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.
| 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.
| 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 |
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 is still the VIC-IV's most important mode. The screen editor uses it, and most MEGA65 games build their graphics from characters.
| 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.
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).
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.
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.
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.
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.
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:
$0 is the background colour, as $00 is
in FCM, and a nybble of $F shows the character's full
colour value (mega65-core, viciv.vhdl, September 2026).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 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.
| 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.
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).
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.
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.
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.
| 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.
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.
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.
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.
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.
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 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).
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).
Three ways of arranging a row are common:
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).
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.
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.
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.
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.
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.
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
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.
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
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
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
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.
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.
$D011,
$D016, $D018, $D031 and
$DD00 overwrite the VIC-IV pointers when written; clear
HOTREG, bit 7 of $D05D, to take full control.$FFFF ends the
row, and the next row starts right after it.$D100-$D3FF, in four banks: text and bitplanes share one,
sprites can use another, and characters can switch to an alternate.$D06C-$D06E.Chapter 13 turns to sound.