Apple ][ //e HGR Font Tutorial
Go to file
2016-01-09 18:27:36 -08:00
Apple2eFont7x8.png Added Apple 2e Font 7x8 2016-01-09 11:48:01 -08:00
README.md Change subsection func we want & will write 2016-01-09 18:27:36 -08:00

#Apple ][ HGR Font Tutorial

Table of Contents

Introduction

Functions we want & will write

Hard-Coded: A

Quirks of the Apple HGR screen

Non-Linear Memory

No FONT data in ROM

HGR bytes are reversed

Half-pixel shift

Font Data

Raw Font Data

Image to Font Data (Javascript)

Font -> Screen Memory Trace

DrawChar version 1

X Cursor Position

CursorCol()

DrawChar() version 2

DrawChar() version 3

Character Inspector

Character Inspector version 2

Character Inspector version 3

Y Cursor Position

Natural Params CursorColRow()

DrawString()

Recap

Copy text screen to HGR

Exercise 1: ScrollHgrUpLine()

Exercise 2: ScrollHgrUpPixel()

Solution 1: ScrollHgrUpLine()

Solution 2: ScrollHgrUpPixel()

References

Introduction

A lot of people in comp.sys.apple2.programmer and other places on the internet have wondered how to "print" text onto the Apple's High Resolution Graphics (HGR) screen. Here's a tutorial on "6502 Font Blitting."

Note: We will prefix hex numbers with $ (or C's notation of 0x). We will prefix binary numbers with %.

Fire up your favorite Apple emulator (cough AppleWin) or real hardware.

Note: If you use:

  • AppleWin press F2 (to reboot), Ctrl-F2 to Ctrl-Reset, and then press F9 until you get a Monochrome screen.

  • Apple 2 js make sure you select:

    Options, [x] Green Screen

  • If you use Virtual II press Ctrl-F12 to reset.

Functions we want & will write

When we are done we will have 6502 assembly code that implements the equivalent of these C functions names:

    void DrawChar();
    void DrawCharCol( char c, int col )
    void DrawCharColRow( char c, int col, int row );
    void SetCursorRow( int row );
    void SetCursorColRow( int col, int row );
    void SetCursorCol( int col );
    void IncCursorCol();
    void DrawHexByte( char c );
    void DrawString( char *text );
    void CopyTextToHGR();

Hard-Coded: A

When you are at the Applesoft ] prompt type (or paste) in the following:

(If you use AppleWin, select the lines, copy, switch back to the emulator, and press Shift-Insert to paste)

HGR
CALL-151
2000:4
2400:A
2800:11
2C00:11
3000:1F
3400:11
3800:11

Voila!

You should see an uppercase A appear in the top left of the HGR screen.

Magic? :-)

Nah, just Computer Science. :-)

The first question you probalby have is "How did I know what bytes to use?" We'll get to that in a second.

Quirks of the Apple HGR screen

There are couple of things we need to discuss first. The preceeding example showed that the Apple's HGR screen behaves a little "funky." The Apple's, shall we say, esoteric use of hardware, is one of the reasons us fans love (or hate) it.

Non-Linear Memory

First, we should notice that video memory is non-linear. :-( You'll want to get familiar with the HGR address for the various Y scanlines:

Y Address
0 $2000
1 $2400
2 $2800
3 $2C00
4 $3000
5 $3400
6 $3800
7 $3C00
8 $2080
: :
64 $2028
128 $2050
191 $3FD0

Don't worry if the address pattern makes no sense right now -- we'll reveal that later.

No FONT data in ROM

Second, each glyph in the Apple font is in a 7x8 cell -- the leading line on the bottom is usually blank but we'll store that too so that we have a true "underline" and bottom descender on 'j', 'y', etc.

Unfortunately, the data for the TEXT ROM 25123 hardware chip is not accessible from the 6502. :-/ This means you will need to manually enter in the 8 bytes/character. :-( The good news is that I've already done this so you can copy / paste. :-)

You can find a picture of the Apple ][ ROM text font on Page 8-9, diagram 8.4 of "Understanding the Apple ][" https://archive.org/stream/understanding_the_apple_ii#page/n203/mode/2up

We're actually going to use the Apple //e ROM text font since it has lower case and the famous "Mouse Text" glyphs.

HGR bytes are reversed

Third, the video scanner for HGR mode scans bits in reverse. :-/ This means that we need to "flip" the bits in a byte if we want it to appear properly. Not hard, just inconvenient. We'll store the pre-flipped bits so we don't have to do this at run-time. :-)

For example, If we want these 4 scan-lines of \:

X___
_X__
__X_
___X_

You would normally encode the pixels in binary as:

%1000_0000 = $80
%0100_0000 = $40
%0010_0000 = $20
%0001_0000 = $10

And if we tried entering in:

2100:80
2500:40
2900:20
2D00:10

We would only get:

  • 3 scanlines intead of the expected 4 (see the next point), and
  • the image would be flipped along the left-right (X axis) like this: /

On the Apple we need to flip each byte:

%0000_0001 = $01
%0000_0010 = $02
%0000_0100 = $04
%0000_1000 = $08

Enter in:

2200:1
2600:2
2A00:4
2E00:8

And we see the correct: \

Half-pixel shift

Fourth, we mentioned above that when we entered in $80 that the Apple didn't display any pixels for this byte. This is because the Apple uses the high-bit as a flag to shift that group of 7 pixels over HALF a pixel. (Yes, half a pixel.) This means the monochrome effective resolution is a pseudo 560x192. We can't individually access every 560 pixels, only part of them so it is not a "true" 560 resolution. :-( What this means in practice is that we can use this half-pixel shift / byte to get very smooth slopes for Y, etc. :-)

For example this will give us a "sharp" Y:

2300:22
2700:22
2B00:14
2F00:8
3300:8
3700:8
3B00:8

If we change the 2nd and 4th scan line to:

2700:92
2F00:8C

We'll get a "smooth" Y.

Note: The emulators Virtual ][ and Apple2js are broken emulators. They do not emulate the half-pixel shift of real hardware at all. This is another reason we won't worry about it for now.

We're going to ignore the half-pixel shift since it is easy to touch up the font data later if we wish:

Font Data

Alrighty then, let's get the font data!

Here is a picture of the Apple //e character set:

  • Apple //e character set

If we wanted only uppercase ASCII we could get away with 64 glyphs:

64 glyphs * 8 bytes/glyph = 512 bytes.

Since the font data chews up memory anyways we'll "splurge" and use the full 128 ASCII glyphs:

128 glyphs * 8 bytes/glyph = 1024 bytes = 1K of data.

Ouch 1K of our precious 64K! Now we know why all this data was in ROM.

Raw Font Data

I've saved you the trouble of converting all the pixels to hex. You may want to mute your sound since the Apple will beep at the semi-colon "comments".

Enter in:

6000:10 08 36 7F 3F 3F 7E 36  ; ^@
6008:10 08 36 41 21 21 4A 36  ; ^A
6010:00 00 02 06 0E 1E 36 42  ; ^B
6018:7F 22 14 08 08 14 2A 7F  ; ^C
6020:00 40 20 11 0A 04 04 00  ; ^D
6028:7F 3F 5F 6C 75 7B 7B 7F  ; ^E
6030:70 60 7E 31 79 30 3F 02  ; ^F
6038:00 18 07 00 07 0C 08 70  ; ^G
6040:08 04 02 7F 02 04 08 00  ; ^H
6048:00 00 00 00 00 00 00 2A  ; ^I
6050:08 08 08 08 49 2A 1C 08  ; ^J
6058:08 1C 2A 49 08 08 08 08  ; ^K
6060:7F 00 00 00 00 00 00 00  ; ^L
6068:40 40 40 44 46 7F 06 04  ; ^M
6070:3F 3F 3F 3F 3F 3F 3F 3F  ; ^N
6078:13 18 1C 7E 1C 18 10 6F  ; ^O
6080:64 0C 1C 3F 1C 0C 04 7B  ; ^P
6088:40 48 08 7F 3E 1C 48 40  ; ^Q
6090:40 48 1C 3E 7E 08 48 40  ; ^R
6098:00 00 00 7F 00 00 00 00  ; ^S
60A0:01 01 01 01 01 01 01 7F  ; ^T
60A8:08 10 20 7F 20 10 08 00  ; ^U
60B0:2A 55 2A 55 2A 55 2A 55  ; ^V
60B8:55 2A 55 2A 55 2A 55 2A  ; ^W
60C0:00 3E 41 01 01 01 7F 00  ; ^X
60C8:00 00 3F 40 40 40 7F 00  ; ^Y
60D0:40 40 40 40 40 40 40 40  ; ^Z
60D8:08 1C 3E 7F 3E 1C 08 00  ; ^[
60E0:7F 00 00 00 00 00 00 7F  ; ^\
60E8:14 14 77 00 77 14 14 00  ; ^]
60F0:7F 40 40 4C 4C 40 40 7F  ; ^^
60F8:01 01 01 01 01 01 01 01  ; ^_
6100:00 00 00 00 00 00 00 00  ;  
6108:08 08 08 08 08 00 08 00  ; !
6110:14 14 14 00 00 00 00 00  ; "
6118:14 14 3E 14 3E 14 14 00  ; #
6120:08 3C 0A 1C 28 1E 08 00  ; $
6128:06 26 10 08 04 32 30 00  ; %
6130:04 0A 0A 04 2A 12 2C 00  ; &
6138:08 08 08 00 00 00 00 00  ; '
6140:08 04 02 02 02 04 08 00  ; (
6148:08 10 20 20 20 10 08 00  ; )
6150:08 2A 1C 08 1C 2A 08 00  ; *
6158:00 08 08 3E 08 08 00 00  ; +
6160:00 00 00 00 08 08 04 00  ; ,
6168:00 00 00 3E 00 00 00 00  ; -
6170:00 00 00 00 00 00 08 00  ; .
6178:00 20 10 08 04 02 00 00  ; /
6180:1C 22 32 2A 26 22 1C 00  ; 0
6188:08 0C 08 08 08 08 1C 00  ; 1
6190:1C 22 20 18 04 02 3E 00  ; 2
6198:3E 20 10 18 20 22 1C 00  ; 3
61A0:10 18 14 12 3E 10 10 00  ; 4
61A8:3E 02 1E 20 20 22 1C 00  ; 5
61B0:38 04 02 1E 22 22 1C 00  ; 6
61B8:3E 20 10 08 04 04 04 00  ; 7
61C0:1C 22 22 1C 22 22 1C 00  ; 8
61C8:1C 22 22 3C 20 10 0E 00  ; 9
61D0:00 00 08 00 08 00 00 00  ; :
61D8:00 00 08 00 08 08 04 00  ; ;
61E0:10 08 04 02 04 08 10 00  ; <
61E8:00 00 3E 00 3E 00 00 00  ; =
61F0:04 08 10 20 10 08 04 00  ; >
61F8:1C 22 10 08 08 00 08 00  ; ?
6200:1C 22 2A 3A 1A 02 3C 00  ; @
6208:08 14 22 22 3E 22 22 00  ; A
6210:1E 22 22 1E 22 22 1E 00  ; B
6218:1C 22 02 02 02 22 1C 00  ; C
6220:1E 22 22 22 22 22 1E 00  ; D
6228:3E 02 02 1E 02 02 3E 00  ; E
6230:3E 02 02 1E 02 02 02 00  ; F
6238:3C 02 02 02 32 22 3C 00  ; G
6240:22 22 22 3E 22 22 22 00  ; H
6248:1C 08 08 08 08 08 1C 00  ; I
6250:20 20 20 20 20 22 1C 00  ; J
6258:22 12 0A 06 0A 12 22 00  ; K
6260:02 02 02 02 02 02 3E 00  ; L
6268:22 36 2A 2A 22 22 22 00  ; M
6270:22 22 26 2A 32 22 22 00  ; N
6278:1C 22 22 22 22 22 1C 00  ; O
6280:1E 22 22 1E 02 02 02 00  ; P
6288:1C 22 22 22 2A 12 2C 00  ; Q
6290:1E 22 22 1E 0A 12 22 00  ; R
6298:1C 22 02 1C 20 22 1C 00  ; S
62A0:3E 08 08 08 08 08 08 00  ; T
62A8:22 22 22 22 22 22 1C 00  ; U
62B0:22 22 22 22 22 14 08 00  ; V
62B8:22 22 22 2A 2A 36 22 00  ; W
62C0:22 22 14 08 14 22 22 00  ; X
62C8:22 22 14 08 08 08 08 00  ; Y
62D0:3E 20 10 08 04 02 3E 00  ; Z
62D8:3E 06 06 06 06 06 3E 00  ; [
62E0:00 02 04 08 10 20 00 00  ; \
62E8:3E 30 30 30 30 30 3E 00  ; ]
62F0:00 00 08 14 22 00 00 00  ; ^
62F8:00 00 00 00 00 00 00 7F  ; _
6300:04 08 10 00 00 00 00 00  ; `
6308:00 00 1C 20 3C 22 3C 00  ; a
6310:02 02 1E 22 22 22 1E 00  ; b
6318:00 00 3C 02 02 02 3C 00  ; c
6320:20 20 3C 22 22 22 3C 00  ; d
6328:00 00 1C 22 3E 02 3C 00  ; e
6330:18 24 04 1E 04 04 04 00  ; f
6338:00 00 1C 22 22 3C 20 1C  ; g
6340:02 02 1E 22 22 22 22 00  ; h
6348:08 00 0C 08 08 08 1C 00  ; i
6350:10 00 18 10 10 10 12 0C  ; j
6358:02 02 22 12 0E 12 22 00  ; k
6360:0C 08 08 08 08 08 1C 00  ; l
6368:00 00 36 2A 2A 2A 22 00  ; m
6370:00 00 1E 22 22 22 22 00  ; n
6378:00 00 1C 22 22 22 1C 00  ; o
6380:00 00 1E 22 22 1E 02 02  ; p
6388:00 00 3C 22 22 3C 20 20  ; q
6390:00 00 3A 06 02 02 02 00  ; r
6398:00 00 3C 02 1C 20 1E 00  ; s
63A0:04 04 1E 04 04 24 18 00  ; t
63A8:00 00 22 22 22 32 2C 00  ; u
63B0:00 00 22 22 22 14 08 00  ; v
63B8:00 00 22 22 2A 2A 36 00  ; w
63C0:00 00 22 14 08 14 22 00  ; x
63C8:00 00 22 22 22 3C 20 1C  ; y
63D0:00 00 3E 10 08 04 3E 00  ; z
63D8:38 0C 0C 06 0C 0C 38 00  ; {
63E0:08 08 08 08 08 08 08 08  ; |
63E8:0E 18 18 30 18 18 0E 00  ; }
63F0:2C 1A 00 00 00 00 00 00  ; ~
63F8:00 2A 14 2A 14 2A 00 00  ; 

Image to Font Data (Javascript)

If you were wondering how this data was generated, you see the great thing about computers is that they can automate all the tedious and boring crap, er, calculations for us. Here's a HTML + Javascript program I wrote to convert the image to HEX:

    <html>
        <script>
            function byte2hex$( byte )
            {
                return ("0" + byte.toString(16)).toUpperCase().substr(-2)
            }

            function OnLoad()
            {
                var image   = document.getElementById( "Apple2eFont7x8" );
                var canvas  = document.createElement( "Canvas" );
                var context = canvas.getContext( "2d" );

                canvas.width  = image.width;
                canvas.height = image.height;
                context.drawImage( image, 0, 0 );

                var CW = 7, CH = 8; // Cell Width Height
                var address = 0x6000, pixel, rgba, lines = "";
                for( var ty = 0; ty < image.height/CH; ++ty )
                {
                    for( var tx = 0; tx < image.width/CW; ++tx )
                    {
                        var text = "";
                        for( var y = 0; y < CH; ++y )
                        {
                            var hex = 0, mask = 0x1;
                            for( var x = 0; x < CW; ++x, mask <<= 1 )
                            {
                                pixel = context.getImageData( tx*CW+x, ty*CH+y, 1, 1 );
                                rgba  = pixel.data;
                                hex  += rgba[0] ? mask : 0; // assume R=G=B
                            }
                            text += byte2hex$( hex ) + " ";
                        }
                        var c = (16*ty)+tx, d =       String.fromCharCode( c );
                        if (c < 32)         d = "^" + String.fromCharCode( c + 0x40 );
                        text += "; " + d + "\n";
                        lines += "" + address.toString(16).toUpperCase() + ":" + text;
                        address += 8;
                    }
                }
                console.log( lines );
                var pre = document.getElementById( "hexdump" );
                pre.innerHTML = lines;
            }
        </script>
    <body onload="OnLoad()">
        <img id="Apple2eFont7x8" src="Apple2eFont7x8.png">
        <hr>
        <pre id="hexdump"></pre>
    </body>
    </html>

Font -> Screen Memory Trace

OK, so now that we have the font data, how do we draw a character "on screen" ?

Remember we need to transfer 8 consecutive bytes (1 byte / scanline) to 8 different scanlines.

Assuming we want to draw the A glyph at the top-left of the screen we would need to transfer bytes from the (source) font glyph memory locations to the (destination) screen memory locations:

($6208) -> $2000
($6209) -> $2400
($620A) -> $2800
($620B) -> $2C00
($620C) -> $3000
($620D) -> $3400
($620E) -> $3800
($620F) -> $3C00

For simplicity, we're going to "quantize" our destination Y so that we render font glyphs only on the start of every 8 rows and every 7 pixel columns. If we then had the starting address we simply could move to the next scan line by successively adding $0400 to our destination screen pointer.

How did I know $0400? One quirk of the HGR screen is that every 8 successive scan lines start this many bytes away.

DrawChar() version 1

Before we can start a simple DrawChar(char c) function, we also first need to assign some zero page memory locations for our static and temporary variables:

$E5 Low  byte (16-bit address) Pointer to screen destination
$E6 High byte (16-bit Address) Pointer to screen destination
$F5 Low  byte (16-bit address) Working pointer to screen byte
$F6 High byte (16-bit address) Working pointer to screen byte

Here's the disassembly of our (hard-coded) DrawChar() program:

    ; FUNC: DrawChar() = $0300
    ; NOTES: A, X, Y is destroyed
    300:    JSR ScreenPtrToTempPtr
    303:    LDA #00      ; glyph 'c' to draw (not used yet)
    305:    LDY #00      ; Y = column to draw at (hard-coded)
    307:    JMP _DrawChar

    307     .ORG $0352
    352: _DrawChar:
    352:    LDX #0
    354: .1 LDA $6200,X  ; A = font[ offset + i ]
    357:    STA ($F5),Y  ; screen[col] = A
    359:    CLC
    35A:    LDA $F6
    35C:    ADC #4
    35E:    STA $F6
    360:    INX
    361:    CPX #8
    363:    BNE .1
    365:    RTS

    ; FUNC: ScreenPtrToTempPtr() = $0366
    366:    LDA $E5      ; Copy initial screen
    368:    STA $F5      ; destination pointer
    36A:    LDA $E6      ; to working pointer
    36C:    STA $F6
    36D:    RTS

Enter in:

300:20 66 03 A9 00 A0 00 4C 52 03
352:A2 00 BD 00 62 91
358:F5 18 A5 F6 69 04 85 F6
360:E8 E0 08 D0 EF 60
366:A5 E5 85 F5 A5 E6 85 F6 60

We're almost ready to run this! We just need to initialize one variable -- where to draw the glyph at:

E5:00 20
300G

And with any luck you should see the at sign @ in the top-left.

X Cursor Position

If we wanted to draw in columns 1 and 2 instead of column 0 then we need to set the Y register which controls which "column" we'll draw at.

Enter in:

306:1
300G
306:2
300G

This works because we are using the 6502 Indirect Zero-Page Y addressing mode to store the destination pixels with the STA instruction. Since the Y-register must always be used in this addressing mode we get a column offset "for free." :-)

357: STA ($F5),Y  ; screen[col] = A

Here's the C pseudo-code of the assembly code:

    char  c      = '@'; // 0x40;
    int   col    = 0;
    char  FONT[] = { ... }; // our font data glyphs
    char *screen = 0x2000 + col; // destination
    char *font   = 0x6200;       // eventually want: &FONT[ c*8 ]
    for( y = 0; y < 8; y++, screen += 0x400 )
       *screen = *font++;

CursorCol( col )

Since the Y-register controls the column we can inline this function and have the caller take care of setting the Y-Register before calling DrawChar().

LDY #column

After DrawChar() it is handy if we can advance both:

  • the column of the cursor
  • the pointer to the screen where the next glyph will be drawn
    ; FUNC: IncCursorCol() = $0370
    ; OUTPUT: Y-Register (column) is incremented
    ; Increment the cursor column and move the destination screen pointer back
    ; up 8 scan lines previously to what it was when DrawChar() was called.
    370:C8     INY
    371:18     CLC
    372:A5 F6  LDA $F6
    374:E9 1F  SBC #1F
    376:85 F6  STA $F6
    378:60     RTS

Enter in:

370:C8 18 A5 F6 E9 1F 85 F6 60

DrawChar() version 2

The glyph to draw is currently hard-coded to $40 (@). The pointer to the start of this glyph is located at:

source = $6000 + ($40*8) = $6000 + $200 = $6200

If we wanted to draw a different glyph, say D we would need to modify the source pointer of the font glyph data.

Recall that our font has this memory layout:

Char Index Address
^@ $00 $6000
^A $01 $6008
^B $02 $6010
^C $03 $6018
: :
Spc $20 $6100
! $21 $6108
: :
0 $30 $6180
1 $31 $6188
2 $32 $6190
3 $33 $6198
: :
? $3F $61F8
@ $40 $6200
A $41 $6208
B $42 $6210
C $43 $6218
D $44 $6220
: : :
_ $5F $62F8

The 6502 stores and loads 16-bit addresses in little-endian format so for glyph D, we need to store the bytes of the address $6220 in reverse order.

Enter in:

355:20 62

And to draw the new glyph, enter in:

300G

We should see the last character of the 3 @@@ change to D.

DrawChar() version 3

Let's remove the hard-coded printing of the glyph and use the character data we really want to draw. This means we need to "fix-up" the temporary source pointer to the font glyph data. Since we have 8 bytes/glyph we need to manually calculate the array offset.

Our array offset for the source glyph data is:

address = $6000 + (c * 8)

Some C pseudo-code would be:

    char c       = 'D'; // 0x44
    int  offset  = c * 8;
    int  address = 0x6000 + offset;

Since we are dealing with a 16-bit address offset it is simpler to break this down into a low-byte and high-byte calculation for the 6502 since it can't natively do 16-bit offsets. Every 32 characters we need to offset 256 bytes.

    int AddressHi = 0x60 + (c / 32)

But since the 6502 doesn't have a division instruction we need to use bit-shifts instead. The calculation c / 32 is the same as c >> 5.

    char c        = 'D'; // 0x44
    char *Font    = 0x6000;
    int FontHi    = (Font >> 8) & 0xFF;
    int FontLo    = (Font >> 0) & 0xFF;
    int AddressHi = FontHi + ((c >> 5) & 0x07);
    int AddressLo = FontLo + ((c << 3) & 0xF8);

A naive glyph/32 calculation would be to use 5 shift right bit-shifts:

    68       PLA      ; pop c  = %PQRSTUVW to draw
    29 60    AND #60  ;        = %PQR00000 S=0, Optimization: implicit CLC
    4A       LSR      ; c /  2 = %0PQRSTUV
    4A       LSR      ; c /  4 = %00PQRSTU
    4A       LSR      ; c /  8 = %000PQRST
    4A       LSR      ; c / 16 = %0000PQRS
    4A       LSR      ; c / 32 = %00000PQR

However we can save one instruction (and 2 cycles) if we optimize c/32 to use the counter-intutive 6502's ROL instruction -- which only requires 4 instructions instead:

    68       PLA      ; pop c  = %PQRSTUVW to draw
    29 60    AND #C0  ;        = %PQR00000 S=0, Optimization: implicit CLC
    2A       ROL      ;        = %QR000000 C=P
    2A       ROL      ;        = %R000000P C=Q
    2A       ROL      ;        = %000000PQ C=R
    2A       ROL      ; c / 32 = %00000PQR C=0

Our prefix code to setup the source address becomes:

    ; FUNC: DrawCharCol( c, col ) = $03BB
    ; FUNC: DrawCharCol( c, col ) = $03BB
    ; PARAM: A = glyph to draw
    ; PARAM: Y = column to draw at; $0 .. $27 (Columns 0 .. 39) (not modified)
    ; NOTES: X is destroyed
    33B:48       PHA      ; push c = %PQRSTUVW to draw
    33C:29 1F    AND #1F  ;        = %000STUVW R=0, Optimization: implicit CLC
    33E:0A       ASL      ; c * 2    %00STUVW0
    33F:0A       ASL      ; c * 4    %0STUVW00
    340:0A       ASL      ; c * 8    %STUVW000
    341:69 00    ADC #00  ; += FontLo; Carry = 0 since R=0 from above
    343:8D 55 03 STA $355 ; AddressLo = FontLo + (c*8)
    346:68       PLA      ; pop c  = %PQRSTUVW to draw
    347:29 60    AND #60  ;        = %PQR00000 S=0, Optimization: implicit CLC
    349:2A       ROL      ;        = %QR000000 C=P
    34A:2A       ROL      ;        = %R000000P C=Q
    34B:2A       ROL      ;        = %000000PQ C=R
    34C:2A       ROL      ; c / 32 = %00000PQR C=0 and one more to get R
    34D:69 60    ADC #60  ; += FontHi; Carry = 0 since S=0 from above
    34F:8D 56 03 STA $356 ; AddressHi = FontHi + (c/32)

Recall we'll re-use our existing font drawing code at $0352:

    352:A2 00    LDX #0
    354:BD 00 00 LDA $0000,X  ; A = font[ offset + i ]
    357:91 F5    STA ($F5),Y  ; screen[col] = A
    359:18       CLC
    35A:A5 F6    LDA $F6
    35C:69 04    ADC #4       ; screen += 0x400
    35E:85 F6    STA $F6
    360:E8       INX
    361:E0 08    CPX #8
    363:D0 EF    BNE $304
    365:60       RTS

We just need to touch up our entry point from $0352 ScreenPtrToTempPtr() to $033B DrawCharCol():

    307:4C 3B 03    JMP $033B   ; DrawCharCol()

Enter in:

300:20 66 03 A9 00 A0 00 4C 3B 03
33B:48 29 1F 0A 0A
340:0A 69 00 8D 55 03 68 29
348:60 2A 2A 2A 2A 69 60 8D
350:56 03
300G

We should now see an closed apple glyph!

To change which glyph is printed:

304:41
300G

And we should see an A printed.

We now have the ability to print any of the 128 ASCII characters!

Character Inspector

Let's verify this by writing a character inspector. We'll use the arrow keys to select the glyph and ESC to exit.

    ; FUNC: DemoCharInspect() = $1000
    1000:A9 00       LDA #0    ; c=0
    1002:85 FE       STA $FE   ; save which glyph to draw
    1004:A9 00    .1 LDA #0    ; screen = 0x2000
    1006:85 F5       STA $F5   ;
    1008:A9 20       LDA #20   ;
    100A:85 F6       STA $F6   ;
    100C:A5 FE       LDA $FE   ; A = glyph c
    100E:A0 00       LDY #00   ; Y = col
    1010:20 3B 03    JSR $033B ; DrawCharCol()
    1013:AD 00 C0 .2 LDA $C000 ; read A=key
    1016:10 FB       BMI .2    ; no key?
    1018:8D 10 C0    STA $C010 ; debounce key
    101B:C9 88       CMP #88   ; key == <-- ?
    101D:D0 0A       BNE .4    ;
    101F:C6 FE       DEC $FE   ; yes, --c
    1021:A5 FE    .3 LDA $FE   ; c &= 0x7F
    1023:29 7F       AND #7F
    1025:85 FE       STA $FE
    1027:10 DB       BPL .1    ; allways branch, draw prev char
    1029:C9 95    .4 CMP #95   ; key == --> ?
    102B:D0 05       BNE .5    ;
    102D:E6 FE       INC $FE   ; yes, ++c
    102F:18          CLC
    1030:90 EF       BCC .3    ; allways branch, draw prev char
    1032:C9 9B    .5 CMP #9B   ; key == ESC ?
    1034:D0 DD       BNE .2    ;
    1036:60          RTS       ; yes, exit

Enter in this code:

1000:A9 00 85 FE A9 00 85 F5
1008:A9 20 85 F6 A5 FE A0 00
1010:20 3B 03 AD 00 C0 10 FB
1018:8D 10 C0 C9 88 D0 0A C6
1020:FE A5 FE 29 7F 85 FE 10
1028:DB C9 95 D0 05 E6 FE 18
1030:90 EF C9 9B D0 DD 60
1000G

We now have an ASCII char inspector!

Character Inspector version 2

Let's fix it up to print the hex value of the current character we are inspecting:

    1010:20 37 10    JSR $1037

    1037:48          PHA            ; save c
    1038:20 3B 03    JSR $033B      ; DrawCharCol()
    103B:68          PLA            ; restore c so we can print it in hex

    ; FUNC: DrawHexByte( c ) = $103C
    ; PARAM: A = byte to print in hex
    103C:48          PHA            ; save low nibble
    103D:6A          ROR            ; shift high nibble
    103E:6A          ROR            ; to low nibble
    103F:6A          ROR
    1040:6A          ROR
    1041:20 48 10    JSR DrawHexNib ; print high nib in hex
    1044:68          PLA            ; pop low nibble
    1045:4C 48 10    JMP DrawHexNib ; print low nib in hex

    ; FUNC: DrawHexNib() = $1048
    ; PARAM: A = nibble to print as hex char
    1048:29 0F       AND #F         ; base 16
    104A:AA          TAX            ;
    104B:20 66 03    JSR $0366      ; update dest screen pointer
    104E:BD 58 10    LDA $1058,X    ; nibble to ASCII
    1051:C8          INY            ; IncCursorCol()
    1052:20 3B 03    JSR $033B      ; DrawCharCol()
    1055:60          RTS
    1058:30 31 32 33 ASC "0123456789ABCDEF"
    105C:34 35 36 37
    1060:38 39 41 42
    1064:43 44 45 46

Enter in:

1010:20 37 10
1037:48
1038:20 3B 03 68 48 6A 6A 6A
1040:6A 20 48 10 68 4C 48 10
1048:29 0F AA 20 66 03 BD 58
1050:10 C8 20 3B 03 60
1058:30 31 32 33 34 35 36 37
1060:38 39 41 42 43 44 45 46
1000G

And now we have our own DrawHexByte() function.

Character Inspector version 3

Let's use IncCursorCol() to automatically advance the cusor. We'll also add a space after the character but before the hex value to improve readability of the output.

    ; FUNC: PrintChar() = $0310
    ; PARAM: A = glyph to draw
    ; PARAM: Y = column to draw at; $0 .. $27 (Columns 0 .. 39) (not modified)
    ; INPUT : $F5,$F6 pointer to the destination screen scanline
    ;         Must start at every 8 scanlines.
    ; OUTPUT: The Y-Register (cursor column) is automatically incremented.
    310:20 3B 03     JSR DrawCharCol
    313:4C 70 03     JMP IncCursorCol

    1010:20 37 10    JSR $1037

    1037:48          PHA            ; save c
    1038:20 10 03    JSR PrintChar  ;
    103B:A9 20       LDA ' '        ; Draw whitespace
    103D:20 10 03    JSR PrintChar  ;
    1040:68          PLA            ; restore c so we can print it in hex

    ; FUNC: DrawHexByte( c )
    ; PARAM: A = byte to print in hex
    1041:48          PHA            ; save low nibble
    1042:6A          ROR            ; shift high nibble
    1043:6A          ROR            ; to low nibble
    1044:6A          ROR
    1045:6A          ROR
    1046:20 4D 10    JSR DrawHexNib ; print high nib in hex
    1049:68          PLA            ; pop low nibble
    104A:4C 4D 10    JMP DrawHexNib ; print low nib in hex

    ; FUNC: $1048 = DrawHexNib()
    ; PARAM: A = nibble to print as hex char
    104D:29 0F       AND #F         ; base 16
    104F:AA          TAX            ;
    1050:BD 58 10    LDA $1058,X    ; nibble to ASCII
    1053:4C 10 03    JMP PrintChar  ;
    1058:30 31 32 33 ASC "0123456789ABCDEF"
    105C:34 35 36 37
    1060:38 39 41 42
    1064:43 44 45 46

Enter in:

310:20 3B 03 4C 70 03
1010:20 37 10
1037:48
1038:20 10 03 A9 20 20 10 03
1040:68 48 6A 6A 6A 6A 20 4D
1048:10 68 4C 4D 10 29 0F AA
1050:BD 58 10 4C 10 03
1058:30 31 32 33 34 35 36 37
1060:38 39 41 42 43 44 45 46
1000G

Y Cursor Position

Right now the line we "print" to is hard-coded since we are using a screen address of $2000 with the pointer at $E5, $E6.

We're going to digress slightly before we fix this.

The secret to getting high speed graphics rendering on the Apple is to use a look-up table. We're going to have a 16-bit address lookup table for Y=0, Y=8, Y=16, .. Y = 184

The HGR screen address is broken up a triad. Every 64 scan lines the offset change by $28.

Y Address Hi Lo
0 $2000 $20 $00
8 $2080 $20 $80
16 $2100 $21 $00
24 $2180 $21 $80
32 $2200 $22 $00
40 $2280 $22 $80
48 $2300 $23 $00
56 $2380 $23 $80
- ----- - -
64 $2028 $20 $28
72 $20A8 $20 $A8
80 $2128 $21 $28
88 $21A8 $21 $A8
96 $2228 $22 $28
104 $22A8 $22 $A8
112 $2328 $23 $28
120 $23A8 $23 $A8
- ----- - -
128 $2050 $20 $50
136 $20D0 $20 $D0
144 $2150 $21 $50
152 $21D0 $21 $D0
160 $2250 $22 $50
168 $22D0 $22 $D0
176 $2350 $23 $50
184 $23D0 $23 $D0

We'll split the table of addresses into Low and High bytes for easier access. We'll also subtract off the hard-coded graphics page 1 high byte = $20 and instead use relative offsets to make it work with either graphics page 1 or 2.

This is our mini HGR Y Address look-up table. "Funny" that it has 24 entries -- the same height as our text screen. :-)

Enter these bytes:

Our HgrLo table:

6400:00 80 00 80 00 80 00 80
6408:28 A8 28 A8 28 A8 28 A8
6410:50 D0 50 D0 50 D0 50 D0

Our HgrHi table:

6418:00 00 01 01 02 02 03 03
6420:00 00 01 01 02 02 03 03
6428:00 00 01 01 02 02 03 03

To select which row to draw at we'll pass that in the X register to our DrawCharColRow() routine:

    ; FUNC: DrawCharColRow() = $0320
    ; PARAM: A = glyph to draw
    ; PARAM: Y = column to draw at; $0 .. $27 (Columns 0 .. 39) (not modified)
    ; PARAM: X = row    to draw at; $0 .. $17 (Rows 0 .. 23) (destroyed)
    320:48        PHA
    321:20 28 03  JSR CursorRow()
    324:68        PLA
    325:4C 3B 03  JMP DrawCharCol()

    ; FUNC: CursorRow( row ) = $0328
    ; PARAM: X = row    to draw at; $0 .. $17 (Rows 0 .. 23) (not modified)
    ; INPUT : $E5,$E6 initial pointer to the destination screen scanline
    ;         Note: Must start at every 8 scanlines.
    ; OUTPUT: $F5,$F5 working pointer to the destination screen scanline
    328:BD 00 64  LDA $6400,X   ; HgrLo[ row ]
    32B:18        CLC
    32C:65 E5     ADC $E5
    32E:85 F5     STA $F5
    330:BD 18 64  LDA $6418,X   ; HgrHi[ row ]
    333:18        CLC
    334:65 E6     ADC $E6
    336:85 F6     STA $F6
    338:60        RTS

Enter in:

320:48 20 28 03 68 4C 3B 03
328:BD 00 64 18 65 E5 85 F5
330:BD 18 64 18 65 E6 85 F6 60

Now we can print a char at any location:

    1100:A9 41    ; A-register = char
    1102:A0 01    ; Y-register = col 1 (2nd column)
    1104:A2 02    ; X-register = row 2 (3rd row)
    1106:4C 20 03 ; DrawCharColRow( c, col )

Enter in:

1100:A9 41 A0 01 A2 02 4C 20 03
1100G

Natural Params CursorColRow()

Unfortunately, our usage of the X and Y registers are not intuitive. This is due to the limited addressing modes of the 6502. :-/ If the 6502 had a symmetrical indirect zero-page X addressing mode:

 LDA ($ZP),X

We could map the X-register to the natural column (x-axis), and the Y-register to the natural row (y-axis). Alas, we're stuck with the X=row and Y=col unless we wanted to add extra code to "swap" the two.

    ; FUNC: CursorColRow() = $0379
    ; PARAM: Y = col
    ; PARAM: X = row
    379:20 28 03  JSR CursorRow
    37C:18        CLC
    37D:98        TYA
    37E:65 F5     ADC $F5
    381:85 F5     STA $F5
    383:60

Enter:

379:20 28 03 18 98 65 F5 85 F5 60

Or are? Since we're using a function to calculate the destinatin address let's fix the order.

We'll need to change the X offset in CursorRow() to Y;

    ; FUNC: CursorRow2( row ) = $033B
    ; PARAM: Y = row
    ; NOTES: Version 2 !
    328:B9 00 64  LDA $6400,Y ; changed from: ,X
    32B:18        CLC
    32C:65 E5     ADC $E5
    32E:85 F5     STA $F5
    330:B9 18 64  LDA $6418,Y ; changed from: ,X
    333:18        CLC
    334:65 E6     ADC $E6
    336:85 F6     STA $F6
    338:60        RTS

And change the low byte to add X instead:

    ; FUNC: CursorColRow2( col, row ) = $0379
    ; PARAM: X = col
    ; PARAM: Y = row
    ; NOTES: Version 2 !
    379:20 28 03  JSR CursorRow
    37C:18        CLC
    37D:88        TXA         ; changed from: TYA
    37E:65 F5     ADC $F5
    381:85 F5     STA $F5
    383:60

This is a little clunky but it is progress. Let's write the new CursorColRow() version with the CursorRow() inlined so we don't have to use a JSR.

    ; FUNC: CursorColRow3( col, row ) = $0379
    ; PARAM: X = column to draw at; $0 .. $27 (Columns 0 .. 39) (not modified)
    ; PARAM: Y = row    to draw at; $0 .. $17 (Rows 0 .. 23) (not modified)
    ; NOTES: Version 3! X and Y is swapped from earlier version!
    ; [$F5] = HgrLo[ Y ] + [$E5] + X
    379:86 F5     STX $F5
    37B:B9 00 64  LDA $6400,Y ; HgrLo[ row ]
    37E:18        CLC
    37F:65 E5     ADC $E5
    381:65 F5     ADC $F5
    383:85 F5     STA $F5
    385:B9 18 64  LDA $6418,Y ; HgrHi[ row ]
    388:18        CLC
    389:65 E6     ADC $E6
    38B:85 F6     STA $F6
    38D:60        RTS

Enter in:

379:   86 F5 B9 00 64 18 65
380:E5 65 F5 85 F5 B9 18 64
388:18 65 E6 85 F6 60

DrawString()

Now that we have the basic print char working lets extend it to print a C-style string (one that is zero terminated.)

    ; FUNC: DrawString( *text ) = $038E
    ; PARAM: X = High byte of string address
    ; PARAM: Y = Low  byte of string address
    38E:84 F0        STY $F0
    390:86 F1        STX $F1
    392:A0 00        LDY #0
    394:B1 F0     .1 LDA ($F0),Y
    396:F0 07        BEQ .2      ; null byte? Done
    398:20 10 03     JSR PrintChar
    39B:C0 28        CPY 40      ; col < 40?
    39D:90 F5        BCC .1
    39F:60        .2 RTS

And our example to verify that it works:

    ; FUNC: DemoDrawString()
    1200:A2 03        LDX #3      ; col = 3
    1202:A0 02        LDY #2      ; row = 2
    1204:20 79 03     JSR CursorColRow3
    1207:A2 12        LDX >.3     ; High
    1209:A0 0E        LDY <.3     ; Low
    120B:4C 8E 03     JMP DrawString
    120E:          .3 ASC "Hello World",0
    120E:48 65 6C 6C 6F 20 57 6F 72 6C 64 00

Enter:

38E:84 F0 86 F1 A0 00 B1 F0
396:F0 07 20 10 03 C0 28 90 F5 60

1200:A2 03 A0 02 20 79 03
1207:A2 12 A0 0E 4C 8E 03
120E:48 65 6C 6C 6F 20 57 6F 72 6C 64 00
1200G

Note: An easy way to get the hex bytes for a string is to use this tiny Javascript snippet to convert a text string to hex:

    var txt = "Hello World";
    for( var i=0; i < txt.length; ++i )
        console.log( txt.charCodeAt(i).toString(16) );

Recap

Here are all the routines we've entered in so far:

300:20 66 03 A9 00 A0 00 4C 3B 03 
310:20 3B 03 4C 70 03
320:48 20 28 03 68 4C 3B 03
328:BD 00 64 18 65 E5 85 F5
330:BD 18 64 18 65 E6 85 F6
33A:60       48 29 1F 0A 0A
340:0A 69 00 8D 55 03 68 29
348:60 2A 2A 2A 2A 69 60 8D
350:56 03 A2 00 BD 00 62 91
358:F5 18 A5 F6 69 04 85 F6
360:E8 E0 08 D0 EF 60 A5 E5
368:85 F5 A5 E6 85 F6 60
370:C8 18 A5 F6 E9 1F 85 F6
378:60 86 F5 B9 00 64 18 65
380:E5 65 F5 85 F5 B9 18 64
388:18 65 E6 85 F6 60 84 F0
390:86 F1 A0 00 B1 F0 F0 07
398:20 10 03 C0 28 90 F5 60

We also have a mini HGR Y address lookup table:

6400:00 80 00 80 00 80 00 80
6408:28 A8 28 A8 28 A8 28 A8
6410:50 D0 50 D0 50 D0 50 D0
6418:00 00 01 01 02 02 03 03
6420:00 00 01 01 02 02 03 03
6428:00 00 01 01 02 02 03 03

What's left?

Copy text screen to HGR

For our final trick we are going to copy the characters off the text screen onto the HGR screen. More magic? Nah, just bit-shuffling.

The text screen, like the HGR screen, is also non-linear, and also broken up into a triad:

Row Row2TextAddr Row2HgrAddr
0 $400 $2000
1 $480 $2080
2 $500 $2100
3 $580 $2180
4 $600 $2200
5 $600 $2280
6 $700 $2300
7 $780 $2380
- ---- -----
8 $428 $2028
9 $4A8 $20A8
10 $528 $2128
11 $5A8 $21A8
12 $628 $2228
13 $6A8 $22A8
14 $728 $2328
15 $7A8 $23A8
- ---- -----
16 $450 $2050
17 $4D0 $20D0
18 $550 $2150
19 $5D0 $21D0
20 $650 $2250
21 $6D0 $22D0
22 $750 $2350
23 $7D0 $23D0

While the Apple's memory layout seems esoteric it has beautiful symmetry. For any given text row notice that:

  • the low byte of the text address is the same low byte of the HGR address
  • the high byte of the text addres is 0x1C less then the high byte of the HGR address

Since we already have a HGR 16-bit address table we can re-use it.

Here's the Pseudo-code to copy the text screen to the HGR Screen:

    for( row = 0; row < 24; row++ )
    {
       SrcTextLo = HgrLo[ row ];
       SrcTextHi = HgrHi[ row ] - 0x1C;
    // CursorColRow( 0, row ) which does:
       DstHgrLo  = HgrLo[ row ]
       DstHgrHi  = HgrHi[ row ]

       for( col = 0; col < 40; col++ )
       {
           c = SrcText[ col ]
           PrintChar( c );
           IncCursorCol();
       }
    }

And here is the assembly:

    ; FUNC: CopyTextToHGR() = $1300
    ; DATA:
    ;    $6000.$63FF  Font 7x8 Data
    ;    $6400.$642F  HgrLo, HgrHi table for every 8 scanlines
    1300:A9 00      LDA #0
    1302:85 F3      STA row
    1304:85 E5      STA $E5
    1306:A9 20      LDA #20           ; Dest = HGR1 = $2000
    1308:85 E6      STA $E6
    130A:A4 F3   .1 LDY row
    130C:C0 18      CPY #$18          ; 24 is #$18
    130E:B0 20      BCS .3            ; Y >= 24
    1310:A2 00      LDX #0
    1312:86 F2      STX col
    1314:20 79 03   JSR CursorColRow3 ; A = HgrHi[ row ]
    1317:18         CLC
    1318:E9 1B      SBC #$1B          ; A -= 0x1C
    131A:85 F8      STA $F8
    131C:B9 00 64   LDA $6400, Y      ; A = HgrLo[ row ]
    131F:85 F7      STA $F7
    1321:A4 F2      LDY col
    1323:B1 F7   .2 LDA ($F7),Y
    1325:20 10 03   JSR PrintChar
    1328:C0 28      CPY #$28          ; 40 is #$18
    132A:90 F7      BCC .2            ; Y < 40
    132C:E6 F3      INC row
    133E:D0 DA      BNE .1            ; allways
    1330:60      .3 RTS

Enter in:

1300:A9 00 85 F3 85 E5 A9 20
1308:85 E6 A4 F3 C0 18 B0 20
1310:A2 00 86 F2 20 79 03 18
1318:E9 1B 85 F8 B9 00 64 85
1320:F7 A4 F2 B1 F7 20 10 03
1328:C0 28 90 F7 E6 F3 D0 DA
1330:60

And now for the moment of truth! Don't worry if you can't see what you are typing.

FC58G
1300L
1300G

Voila!

And just to prove that it copied the bottom 4 text rows as well:

C052

And to restore the bottom 4 text rows

C053

Exercise 1: ScrollHgrUpPixel()

Hey! Homework? Yes, the only (true) way to demonstrate you understand the theory is with implementation:

Write a function to "scroll" the HGR screen up:

* one "text line" (8 pixels), and

Hint: This is basically a gloried and specialized `memcpy()`.

Exercise 2: ScrollHgrUpLine()

Write a function to "scroll" the HGR screen up:

* one scan line (1 pixel)


For scrolling up one pixel we can spot the patter if we inspect the memory flow of how pixels get shuffled around:

    40 bytes from $2400.$2427 -> $2000.$2027
    40 bytes from $2800.$2827 -> $2400.$2427
    etc

Hope this tutorial helped you understand the inner workings of a font blitter!

Happy (Apple ][ //e //c) Hacking! Michael "AppleWin Debug Dev"

Solution 1: ScrollHgrUpLine()

Figure it out ! You have all the tools and knowledge.

Solution 2:: ScrollHgrUpPixel()

There are many different ways to solve this depending if we want to prioritize space or speed.

We could manually unroll every loop such as this monstrosity (we trade space for speed):

Enter this:

1400:A2 27
1402:BD 00 24 9D 00 20
1408:BD 00 28 9D 00 24
140E:BD 00 2C 9D 00 28
1414:BD 00 30 9D 00 2C
141A:BD 00 34 9D 00 30
1420:BD 00 38 9D 00 34
1426:BD 00 3C 9D 00 38
142C:BD 80 20 9D 00 3C
1432:BD 80 24 9D 80 20
1438:BD 80 28 9D 80 24
143E:BD 80 2C 9D 80 28
1444:BD 80 30 9D 80 2C
144A:BD 80 34 9D 80 30
1450:BD 80 38 9D 80 34
1456:BD 80 3C 9D 80 38
145C:BD 00 21 9D 80 3C
1462:BD 00 25 9D 00 21
1468:BD 00 29 9D 00 25
146E:BD 00 2D 9D 00 29
1474:BD 00 31 9D 00 2D
147A:BD 00 35 9D 00 31
1480:BD 00 39 9D 00 35
1486:BD 00 3D 9D 00 39
148C:BD 80 21 9D 00 3D
1492:BD 80 25 9D 80 21
1498:BD 80 29 9D 80 25
149E:BD 80 2D 9D 80 29
14A4:BD 80 31 9D 80 2D
14AA:BD 80 35 9D 80 31
14B0:BD 80 39 9D 80 35
14B6:BD 80 3D 9D 80 39
14BC:BD 00 22 9D 80 3D
14C2:BD 00 26 9D 00 22
14C8:BD 00 2A 9D 00 26
14CE:BD 00 2E 9D 00 2A
14D4:BD 00 32 9D 00 2E
14DA:BD 00 36 9D 00 32
14E0:BD 00 3A 9D 00 36
14E6:BD 00 3E 9D 00 3A
14EC:BD 80 22 9D 00 3E
14F2:BD 80 26 9D 80 22
14F8:BD 80 2A 9D 80 26
14FE:BD 80 2E 9D 80 2A
1504:BD 80 32 9D 80 2E
150A:BD 80 36 9D 80 32
1510:BD 80 3A 9D 80 36
1516:BD 80 3E 9D 80 3A
151C:BD 00 23 9D 80 3E
1522:BD 00 27 9D 00 23
1528:BD 00 2B 9D 00 27
152E:BD 00 2F 9D 00 2B
1534:BD 00 33 9D 00 2F
153A:BD 00 37 9D 00 33
1540:BD 00 3B 9D 00 37
1546:BD 00 3F 9D 00 3B
154C:BD 80 23 9D 00 3F
1552:BD 80 27 9D 80 23
1558:BD 80 2B 9D 80 27
155E:BD 80 2F 9D 80 2B
1564:BD 80 33 9D 80 2F
156A:BD 80 37 9D 80 33
1570:BD 80 3B 9D 80 37
1576:BD 80 3F 9D 80 3B
157C:BD 28 20 9D 80 3F
1582:BD 28 24 9D 28 20
1588:BD 28 28 9D 28 24
158E:BD 28 2C 9D 28 28
1594:BD 28 30 9D 28 2C
159A:BD 28 34 9D 28 30
15A0:BD 28 38 9D 28 34
15A6:BD 28 3C 9D 28 38
15AC:BD A8 20 9D 28 3C
15B2:BD A8 24 9D A8 20
15B8:BD A8 28 9D A8 24
15BE:BD A8 2C 9D A8 28
15C4:BD A8 30 9D A8 2C
15CA:BD A8 34 9D A8 30
15D0:BD A8 38 9D A8 34
15D6:BD A8 3C 9D A8 38
15DC:BD 28 21 9D A8 3C
15E2:BD 28 25 9D 28 21
15E8:BD 28 29 9D 28 25
15EE:BD 28 2D 9D 28 29
15F4:BD 28 31 9D 28 2D
15FA:BD 28 35 9D 28 31
1600:BD 28 39 9D 28 35
1606:BD 28 3D 9D 28 39
160C:BD A8 21 9D 28 3D
1612:BD A8 25 9D A8 21
1618:BD A8 29 9D A8 25
161E:BD A8 2D 9D A8 29
1624:BD A8 31 9D A8 2D
162A:BD A8 35 9D A8 31
1630:BD A8 39 9D A8 35
1636:BD A8 3D 9D A8 39
163C:BD 28 22 9D A8 3D
1642:BD 28 26 9D 28 22
1648:BD 28 2A 9D 28 26
164E:BD 28 2E 9D 28 2A
1654:BD 28 32 9D 28 2E
165A:BD 28 36 9D 28 32
1660:BD 28 3A 9D 28 36
1666:BD 28 3E 9D 28 3A
166C:BD A8 22 9D 28 3E
1672:BD A8 26 9D A8 22
1678:BD A8 2A 9D A8 26
167E:BD A8 2E 9D A8 2A
1684:BD A8 32 9D A8 2E
168A:BD A8 36 9D A8 32
1690:BD A8 3A 9D A8 36
1696:BD A8 3E 9D A8 3A
169C:BD 28 23 9D A8 3E
16A2:BD 28 27 9D 28 23
16A8:BD 28 2B 9D 28 27
16AE:BD 28 2F 9D 28 2B
16B4:BD 28 33 9D 28 2F
16BA:BD 28 37 9D 28 33
16C0:BD 28 3B 9D 28 37
16C6:BD 28 3F 9D 28 3B
16CC:BD A8 23 9D 28 3F
16D2:BD A8 27 9D A8 23
16D8:BD A8 2B 9D A8 27
16DE:BD A8 2F 9D A8 2B
16E4:BD A8 33 9D A8 2F
16EA:BD A8 37 9D A8 33
16F0:BD A8 3B 9D A8 37
16F6:BD A8 3F 9D A8 3B
16FC:BD 50 20 9D A8 3F
1702:BD 50 24 9D 50 20
1708:BD 50 28 9D 50 24
170E:BD 50 2C 9D 50 28
1714:BD 50 30 9D 50 2C
171A:BD 50 34 9D 50 30
1720:BD 50 38 9D 50 34
1726:BD 50 3C 9D 50 38
172C:BD D0 20 9D 50 3C
1732:BD D0 24 9D D0 20
1738:BD D0 28 9D D0 24
173E:BD D0 2C 9D D0 28
1744:BD D0 30 9D D0 2C
174A:BD D0 34 9D D0 30
1750:BD D0 38 9D D0 34
1756:BD D0 3C 9D D0 38
175C:BD 50 21 9D D0 3C
1762:BD 50 25 9D 50 21
1768:BD 50 29 9D 50 25
176E:BD 50 2D 9D 50 29
1774:BD 50 31 9D 50 2D
177A:BD 50 35 9D 50 31
1780:BD 50 39 9D 50 35
1786:BD 50 3D 9D 50 39
178C:BD D0 21 9D 50 3D
1792:BD D0 25 9D D0 21
1798:BD D0 29 9D D0 25
179E:BD D0 2D 9D D0 29
17A4:BD D0 31 9D D0 2D
17AA:BD D0 35 9D D0 31
17B0:BD D0 39 9D D0 35
17B6:BD D0 3D 9D D0 39
17BC:BD 50 22 9D D0 3D
17C2:BD 50 26 9D 50 22
17C8:BD 50 2A 9D 50 26
17CE:BD 50 2E 9D 50 2A
17D4:BD 50 32 9D 50 2E
17DA:BD 50 36 9D 50 32
17E0:BD 50 3A 9D 50 36
17E6:BD 50 3E 9D 50 3A
17EC:BD D0 22 9D 50 3E
17F2:BD D0 26 9D D0 22
17F8:BD D0 2A 9D D0 26
17FE:BD D0 2E 9D D0 2A
1804:BD D0 32 9D D0 2E
180A:BD D0 36 9D D0 32
1810:BD D0 3A 9D D0 36
1816:BD D0 3E 9D D0 3A
181C:BD 50 23 9D D0 3E
1822:BD 50 27 9D 50 23
1828:BD 50 2B 9D 50 27
182E:BD 50 2F 9D 50 2B
1834:BD 50 33 9D 50 2F
183A:BD 50 37 9D 50 33
1840:BD 50 3B 9D 50 37
1846:BD 50 3F 9D 50 3B
184C:BD D0 23 9D 50 3F
1852:BD D0 27 9D D0 23
1858:BD D0 2B 9D D0 27
185E:BD D0 2F 9D D0 2B
1864:BD D0 33 9D D0 2F
186A:BD D0 37 9D D0 33
1870:BD D0 3B 9D D0 37
1876:BD D0 3F 9D D0 3B
187C:A9 00    9D D0 3F
1881:CA 30 03 4C 02 14
1887:60

And let's write a little demo ...

    1380:A0 C0        LDY #C0
    1382:20 00 14  .1 JSR ScrollHgrUpPixel
    1385:88           DEY
    1386:D0 FA        BNE .1
    1388:60           RTS

Enter in:

1380:A0 C0 20 00 14 88 D0 FA 60

And let's try it out:

1300L
1300G
1380G

Sweet !

Here's the assembly to scroll the HGR screen up one pixel:

    ; FUNC: ScrollHgrUpPixel() = $1400
    1400:     LDX #27 ; 39 columns
    1402:  .1 LDA $2400,X : STA $2000,X  ; [  1] -> [  0]
    1408:     LDA $2800,X : STA $2400,X  ; [  2] -> [  1]
    140E:     LDA $2C00,X : STA $2800,X  ; [  3] -> [  2]
    1414:     LDA $3000,X : STA $2C00,X  ; [  4] -> [  3]
    141A:     LDA $3400,X : STA $3000,X  ; [  5] -> [  4]
    1420:     LDA $3800,X : STA $3400,X  ; [  6] -> [  5]
    1426:     LDA $3C00,X : STA $3800,X  ; [  7] -> [  6]
    142C:     LDA $2080,X : STA $3C00,X  ; [  8] -> [  7]
    1432:     LDA $2480,X : STA $2080,X  ; [  9] -> [  8]
    1438:     LDA $2880,X : STA $2480,X  ; [ 10] -> [  9]
    143E:     LDA $2C80,X : STA $2880,X  ; [ 11] -> [ 10]
    1444:     LDA $3080,X : STA $2C80,X  ; [ 12] -> [ 11]
    144A:     LDA $3480,X : STA $3080,X  ; [ 13] -> [ 12]
    1450:     LDA $3880,X : STA $3480,X  ; [ 14] -> [ 13]
    1456:     LDA $3C80,X : STA $3880,X  ; [ 15] -> [ 14]
    145C:     LDA $2100,X : STA $3C80,X  ; [ 16] -> [ 15]
    1462:     LDA $2500,X : STA $2100,X  ; [ 17] -> [ 16]
    1468:     LDA $2900,X : STA $2500,X  ; [ 18] -> [ 17]
    146E:     LDA $2D00,X : STA $2900,X  ; [ 19] -> [ 18]
    1474:     LDA $3100,X : STA $2D00,X  ; [ 20] -> [ 19]
    147A:     LDA $3500,X : STA $3100,X  ; [ 21] -> [ 20]
    1480:     LDA $3900,X : STA $3500,X  ; [ 22] -> [ 21]
    1486:     LDA $3D00,X : STA $3900,X  ; [ 23] -> [ 22]
    148C:     LDA $2180,X : STA $3D00,X  ; [ 24] -> [ 23]
    1492:     LDA $2580,X : STA $2180,X  ; [ 25] -> [ 24]
    1498:     LDA $2980,X : STA $2580,X  ; [ 26] -> [ 25]
    149E:     LDA $2D80,X : STA $2980,X  ; [ 27] -> [ 26]
    14A4:     LDA $3180,X : STA $2D80,X  ; [ 28] -> [ 27]
    14AA:     LDA $3580,X : STA $3180,X  ; [ 29] -> [ 28]
    14B0:     LDA $3980,X : STA $3580,X  ; [ 30] -> [ 29]
    14B6:     LDA $3D80,X : STA $3980,X  ; [ 31] -> [ 30]
    14BC:     LDA $2200,X : STA $3D80,X  ; [ 32] -> [ 31]
    14C2:     LDA $2600,X : STA $2200,X  ; [ 33] -> [ 32]
    14C8:     LDA $2A00,X : STA $2600,X  ; [ 34] -> [ 33]
    14CE:     LDA $2E00,X : STA $2A00,X  ; [ 35] -> [ 34]
    14D4:     LDA $3200,X : STA $2E00,X  ; [ 36] -> [ 35]
    14DA:     LDA $3600,X : STA $3200,X  ; [ 37] -> [ 36]
    14E0:     LDA $3A00,X : STA $3600,X  ; [ 38] -> [ 37]
    14E6:     LDA $3E00,X : STA $3A00,X  ; [ 39] -> [ 38]
    14EC:     LDA $2280,X : STA $3E00,X  ; [ 40] -> [ 39]
    14F2:     LDA $2680,X : STA $2280,X  ; [ 41] -> [ 40]
    14F8:     LDA $2A80,X : STA $2680,X  ; [ 42] -> [ 41]
    14FE:     LDA $2E80,X : STA $2A80,X  ; [ 43] -> [ 42]
    1504:     LDA $3280,X : STA $2E80,X  ; [ 44] -> [ 43]
    150A:     LDA $3680,X : STA $3280,X  ; [ 45] -> [ 44]
    1510:     LDA $3A80,X : STA $3680,X  ; [ 46] -> [ 45]
    1516:     LDA $3E80,X : STA $3A80,X  ; [ 47] -> [ 46]
    151C:     LDA $2300,X : STA $3E80,X  ; [ 48] -> [ 47]
    1522:     LDA $2700,X : STA $2300,X  ; [ 49] -> [ 48]
    1528:     LDA $2B00,X : STA $2700,X  ; [ 50] -> [ 49]
    152E:     LDA $2F00,X : STA $2B00,X  ; [ 51] -> [ 50]
    1534:     LDA $3300,X : STA $2F00,X  ; [ 52] -> [ 51]
    153A:     LDA $3700,X : STA $3300,X  ; [ 53] -> [ 52]
    1540:     LDA $3B00,X : STA $3700,X  ; [ 54] -> [ 53]
    1546:     LDA $3F00,X : STA $3B00,X  ; [ 55] -> [ 54]
    154C:     LDA $2380,X : STA $3F00,X  ; [ 56] -> [ 55]
    1552:     LDA $2780,X : STA $2380,X  ; [ 57] -> [ 56]
    1558:     LDA $2B80,X : STA $2780,X  ; [ 58] -> [ 57]
    155E:     LDA $2F80,X : STA $2B80,X  ; [ 59] -> [ 58]
    1564:     LDA $3380,X : STA $2F80,X  ; [ 60] -> [ 59]
    156A:     LDA $3780,X : STA $3380,X  ; [ 61] -> [ 60]
    1570:     LDA $3B80,X : STA $3780,X  ; [ 62] -> [ 61]
    1576:     LDA $3F80,X : STA $3B80,X  ; [ 63] -> [ 62]
    157C:     LDA $2028,X : STA $3F80,X  ; [ 64] -> [ 63]
    1582:     LDA $2428,X : STA $2028,X  ; [ 65] -> [ 64]
    1588:     LDA $2828,X : STA $2428,X  ; [ 66] -> [ 65]
    158E:     LDA $2C28,X : STA $2828,X  ; [ 67] -> [ 66]
    1594:     LDA $3028,X : STA $2C28,X  ; [ 68] -> [ 67]
    159A:     LDA $3428,X : STA $3028,X  ; [ 69] -> [ 68]
    15A0:     LDA $3828,X : STA $3428,X  ; [ 70] -> [ 69]
    15A6:     LDA $3C28,X : STA $3828,X  ; [ 71] -> [ 70]
    15AC:     LDA $20A8,X : STA $3C28,X  ; [ 72] -> [ 71]
    15B2:     LDA $24A8,X : STA $20A8,X  ; [ 73] -> [ 72]
    15B8:     LDA $28A8,X : STA $24A8,X  ; [ 74] -> [ 73]
    15BE:     LDA $2CA8,X : STA $28A8,X  ; [ 75] -> [ 74]
    15C4:     LDA $30A8,X : STA $2CA8,X  ; [ 76] -> [ 75]
    15CA:     LDA $34A8,X : STA $30A8,X  ; [ 77] -> [ 76]
    15D0:     LDA $38A8,X : STA $34A8,X  ; [ 78] -> [ 77]
    15D6:     LDA $3CA8,X : STA $38A8,X  ; [ 79] -> [ 78]
    15DC:     LDA $2128,X : STA $3CA8,X  ; [ 80] -> [ 79]
    15E2:     LDA $2528,X : STA $2128,X  ; [ 81] -> [ 80]
    15E8:     LDA $2928,X : STA $2528,X  ; [ 82] -> [ 81]
    15EE:     LDA $2D28,X : STA $2928,X  ; [ 83] -> [ 82]
    15F4:     LDA $3128,X : STA $2D28,X  ; [ 84] -> [ 83]
    15FA:     LDA $3528,X : STA $3128,X  ; [ 85] -> [ 84]
    1600:     LDA $3928,X : STA $3528,X  ; [ 86] -> [ 85]
    1606:     LDA $3D28,X : STA $3928,X  ; [ 87] -> [ 86]
    160C:     LDA $21A8,X : STA $3D28,X  ; [ 88] -> [ 87]
    1612:     LDA $25A8,X : STA $21A8,X  ; [ 89] -> [ 88]
    1618:     LDA $29A8,X : STA $25A8,X  ; [ 90] -> [ 89]
    161E:     LDA $2DA8,X : STA $29A8,X  ; [ 91] -> [ 90]
    1624:     LDA $31A8,X : STA $2DA8,X  ; [ 92] -> [ 91]
    162A:     LDA $35A8,X : STA $31A8,X  ; [ 93] -> [ 92]
    1630:     LDA $39A8,X : STA $35A8,X  ; [ 94] -> [ 93]
    1636:     LDA $3DA8,X : STA $39A8,X  ; [ 95] -> [ 94]
    163C:     LDA $2228,X : STA $3DA8,X  ; [ 96] -> [ 95]
    1642:     LDA $2628,X : STA $2228,X  ; [ 97] -> [ 96]
    1648:     LDA $2A28,X : STA $2628,X  ; [ 98] -> [ 97]
    164E:     LDA $2E28,X : STA $2A28,X  ; [ 99] -> [ 98]
    1654:     LDA $3228,X : STA $2E28,X  ; [100] -> [ 99]
    165A:     LDA $3628,X : STA $3228,X  ; [101] -> [100]
    1660:     LDA $3A28,X : STA $3628,X  ; [102] -> [101]
    1666:     LDA $3E28,X : STA $3A28,X  ; [103] -> [102]
    166C:     LDA $22A8,X : STA $3E28,X  ; [104] -> [103]
    1672:     LDA $26A8,X : STA $22A8,X  ; [105] -> [104]
    1678:     LDA $2AA8,X : STA $26A8,X  ; [106] -> [105]
    167E:     LDA $2EA8,X : STA $2AA8,X  ; [107] -> [106]
    1684:     LDA $32A8,X : STA $2EA8,X  ; [108] -> [107]
    168A:     LDA $36A8,X : STA $32A8,X  ; [109] -> [108]
    1690:     LDA $3AA8,X : STA $36A8,X  ; [110] -> [109]
    1696:     LDA $3EA8,X : STA $3AA8,X  ; [111] -> [110]
    169C:     LDA $2328,X : STA $3EA8,X  ; [112] -> [111]
    16A2:     LDA $2728,X : STA $2328,X  ; [113] -> [112]
    16A8:     LDA $2B28,X : STA $2728,X  ; [114] -> [113]
    16AE:     LDA $2F28,X : STA $2B28,X  ; [115] -> [114]
    16B4:     LDA $3328,X : STA $2F28,X  ; [116] -> [115]
    16BA:     LDA $3728,X : STA $3328,X  ; [117] -> [116]
    16C0:     LDA $3B28,X : STA $3728,X  ; [118] -> [117]
    16C6:     LDA $3F28,X : STA $3B28,X  ; [119] -> [118]
    16CC:     LDA $23A8,X : STA $3F28,X  ; [120] -> [119]
    16D2:     LDA $27A8,X : STA $23A8,X  ; [121] -> [120]
    16D8:     LDA $2BA8,X : STA $27A8,X  ; [122] -> [121]
    16DE:     LDA $2FA8,X : STA $2BA8,X  ; [123] -> [122]
    16E4:     LDA $33A8,X : STA $2FA8,X  ; [124] -> [123]
    16EA:     LDA $37A8,X : STA $33A8,X  ; [125] -> [124]
    16F0:     LDA $3BA8,X : STA $37A8,X  ; [126] -> [125]
    16F6:     LDA $3FA8,X : STA $3BA8,X  ; [127] -> [126]
    16FC:     LDA $2050,X : STA $3FA8,X  ; [128] -> [127]
    1702:     LDA $2450,X : STA $2050,X  ; [129] -> [128]
    1708:     LDA $2850,X : STA $2450,X  ; [130] -> [129]
    170E:     LDA $2C50,X : STA $2850,X  ; [131] -> [130]
    1714:     LDA $3050,X : STA $2C50,X  ; [132] -> [131]
    171A:     LDA $3450,X : STA $3050,X  ; [133] -> [132]
    1720:     LDA $3850,X : STA $3450,X  ; [134] -> [133]
    1726:     LDA $3C50,X : STA $3850,X  ; [135] -> [134]
    172C:     LDA $20D0,X : STA $3C50,X  ; [136] -> [135]
    1732:     LDA $24D0,X : STA $20D0,X  ; [137] -> [136]
    1738:     LDA $28D0,X : STA $24D0,X  ; [138] -> [137]
    173E:     LDA $2CD0,X : STA $28D0,X  ; [139] -> [138]
    1744:     LDA $30D0,X : STA $2CD0,X  ; [140] -> [139]
    174A:     LDA $34D0,X : STA $30D0,X  ; [141] -> [140]
    1750:     LDA $38D0,X : STA $34D0,X  ; [142] -> [141]
    1756:     LDA $3CD0,X : STA $38D0,X  ; [143] -> [142]
    175C:     LDA $2150,X : STA $3CD0,X  ; [144] -> [143]
    1762:     LDA $2550,X : STA $2150,X  ; [145] -> [144]
    1768:     LDA $2950,X : STA $2550,X  ; [146] -> [145]
    176E:     LDA $2D50,X : STA $2950,X  ; [147] -> [146]
    1774:     LDA $3150,X : STA $2D50,X  ; [148] -> [147]
    177A:     LDA $3550,X : STA $3150,X  ; [149] -> [148]
    1780:     LDA $3950,X : STA $3550,X  ; [150] -> [149]
    1786:     LDA $3D50,X : STA $3950,X  ; [151] -> [150]
    178C:     LDA $21D0,X : STA $3D50,X  ; [152] -> [151]
    1792:     LDA $25D0,X : STA $21D0,X  ; [153] -> [152]
    1798:     LDA $29D0,X : STA $25D0,X  ; [154] -> [153]
    179E:     LDA $2DD0,X : STA $29D0,X  ; [155] -> [154]
    17A4:     LDA $31D0,X : STA $2DD0,X  ; [156] -> [155]
    17AA:     LDA $35D0,X : STA $31D0,X  ; [157] -> [156]
    17B0:     LDA $39D0,X : STA $35D0,X  ; [158] -> [157]
    17B6:     LDA $3DD0,X : STA $39D0,X  ; [159] -> [158]
    17BC:     LDA $2250,X : STA $3DD0,X  ; [160] -> [159]
    17C2:     LDA $2650,X : STA $2250,X  ; [161] -> [160]
    17C8:     LDA $2A50,X : STA $2650,X  ; [162] -> [161]
    17CE:     LDA $2E50,X : STA $2A50,X  ; [163] -> [162]
    17D4:     LDA $3250,X : STA $2E50,X  ; [164] -> [163]
    17DA:     LDA $3650,X : STA $3250,X  ; [165] -> [164]
    17E0:     LDA $3A50,X : STA $3650,X  ; [166] -> [165]
    17E6:     LDA $3E50,X : STA $3A50,X  ; [167] -> [166]
    17EC:     LDA $22D0,X : STA $3E50,X  ; [168] -> [167]
    17F2:     LDA $26D0,X : STA $22D0,X  ; [169] -> [168]
    17F8:     LDA $2AD0,X : STA $26D0,X  ; [170] -> [169]
    17FE:     LDA $2ED0,X : STA $2AD0,X  ; [171] -> [170]
    1804:     LDA $32D0,X : STA $2ED0,X  ; [172] -> [171]
    180A:     LDA $36D0,X : STA $32D0,X  ; [173] -> [172]
    1810:     LDA $3AD0,X : STA $36D0,X  ; [174] -> [173]
    1816:     LDA $3ED0,X : STA $3AD0,X  ; [175] -> [174]
    181C:     LDA $2350,X : STA $3ED0,X  ; [176] -> [175]
    1822:     LDA $2750,X : STA $2350,X  ; [177] -> [176]
    1828:     LDA $2B50,X : STA $2750,X  ; [178] -> [177]
    182E:     LDA $2F50,X : STA $2B50,X  ; [179] -> [178]
    1834:     LDA $3350,X : STA $2F50,X  ; [180] -> [179]
    183A:     LDA $3750,X : STA $3350,X  ; [181] -> [180]
    1840:     LDA $3B50,X : STA $3750,X  ; [182] -> [181]
    1846:     LDA $3F50,X : STA $3B50,X  ; [183] -> [182]
    184C:     LDA $23D0,X : STA $3F50,X  ; [184] -> [183]
    1852:     LDA $27D0,X : STA $23D0,X  ; [185] -> [184]
    1858:     LDA $2BD0,X : STA $27D0,X  ; [186] -> [185]
    185E:     LDA $2FD0,X : STA $2BD0,X  ; [187] -> [186]
    1864:     LDA $33D0,X : STA $2FD0,X  ; [188] -> [187]
    186A:     LDA $37D0,X : STA $33D0,X  ; [189] -> [188]
    1870:     LDA $3BD0,X : STA $37D0,X  ; [190] -> [189]
    1876:     LDA $3FD0,X : STA $3BD0,X  ; [191] -> [190]
    187C:     LDA #00     : STA $3FD0,X  ; zero  -> [191]
    1881:     DEX
    1882:     BMI .2   ; x < 0 ?
    1884:     JMP .1
    1887:  .2 RTS

The bulk of the ScrollHgrUpPixel() was generated with this Javascript program:

    var hgr = [];
    for( var y = 0; y < 193; ++y ) // Intentional 1 scanline too many!
        hgr[ y ] = 0x2000 + ((y/64)|0)*0x28 + ((y%8)|0)*0x400 + ((y/8)&7)*0x80;

    for( var y = 0; y < 192; ++y )
        console.log( "[" + y + "]: " + hgr[y].toString(16).toUpperCase() );

    function byte2hex$( byte )
    {
        return ("0" + byte.toString(16)).toUpperCase().substr(-2)
    }

    var useX = false;

    var address = 0x1402, out = "";
    for( var y = 0; y < 192/8; ++y )
        for( var x = 0; x < 8; ++x )
        {
            var row = y*8 + x; // Assumes hgr[] has a dummy 193rd scanline!
            var src = hgr[ row + 1 ];
            var dst = hgr[ row + 0 ];
            var mem = (useX ? "BD " : "?? " )
                    + byte2hex$( (src >> 0) & 0xFF ) + " "
                    + byte2hex$( (src >> 8) & 0xFF ) + " "
                    + (useX ? "9D "
                    + byte2hex$( (dst >> 0) & 0xFF ) + " "
                    + byte2hex$( (dst >> 8) & 0xFF ) + " "
            var txt = "  "
                 + "  LDA $" + src.toString(16).toUpperCase() + ",Y "
                 + ": STA $" + dst.toString(16).toUpperCase() + ",Y "
                 + " ; ["   + ("  " + (row+1)).substr(-3) 
                 + "] -> [" + ("  " + (row  )).substr(-3) + "]"  + "\n";
            if (row != 191)
                out += address.toString(16).toUpperCase() + ":" + mem + txt;
            address += 6; // 6 bytes per line
        }
    console.log( out );

And who said Javascript was a useless language? :-)

That's all folks! No go write some cool font blitter code.

References