Example program for the Cidco MailStation Z80 computer
1; vim:syntax=z8a:ts=8
2;
3; putchar for 5x8 font
4; Copyright (c) 2019-2021 joshua stein <jcs@jcs.org>
5;
6; Permission to use, copy, modify, and distribute this software for any
7; purpose with or without fee is hereby granted, provided that the above
8; copyright notice and this permission notice appear in all copies.
9;
10; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11; WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12; MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13; ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15; ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16; OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17;
18
19 .module putchar
20
21 .include "mailstation.inc"
22
23 ; screen contents (characters) array in upper memory
24 .equ _screenbuf, #0xc000
25 .equ _screenbufend, #0xc3ff
26
27 ; per-character attributes array in upper memory
28 .equ _screenattrs, #0xc400
29 .equ _screenattrsend, #0xc7ff
30
31 .area _DATA
32
33font_data::
34 .include "font/spleen-5x8.inc"
35
36 ; lookup table for putchar
37 ; left-most 5 bits are col group for lcd_cas
38 ; last 3 bits are offset into col group
39cursorx_lookup_data::
40 .include "cursorx_lookup.inc"
41
42_cursorx:: ; cursor x position, 0-indexed
43 .db #0
44_cursory:: ; cursor y position, 0-indexed
45 .db #0
46_saved_cursorx:: ; cursor x position, 0-indexed
47 .db #0
48_saved_cursory:: ; cursor y position, 0-indexed
49 .db #0
50_putchar_sgr:: ; current SGR for putchar()
51 .db #0
52
53
54 .area _CODE
55
56; void screen_init(void)
57; quick initialization of the screen
58_screen_init::
59 push hl
60 call _clear_screen_bufs
61 call _clear_screen
62 call _recursor
63 ld hl, #0
64 push hl
65 push hl
66 call _stamp_char
67 pop hl
68 pop hl
69 pop hl
70 ret
71
72
73; void lcd_cas(unsigned char col)
74; enable CAS, address the LCD column col (in h), and disable CAS
75_lcd_cas::
76 push ix
77 ld ix, #0
78 add ix, sp
79 push de
80 push hl
81 ld hl, (p2shadow)
82 ld a, (hl)
83 and #0b11110111 ; CAS(0) - turn port2 bit 3 off
84 ld (hl), a
85 out (#0x02), a ; write p2shadow to port2
86 ld de, #LCD_START
87 ld a, 4(ix)
88 ld (de), a ; write col argument
89 ld a, (hl)
90 or #0b00001000 ; CAS(1) - turn port2 bit 3 on
91 ld (hl), a
92 out (#0x02), a
93 pop hl
94 pop de
95 ld sp, ix
96 pop ix
97 ret
98
99
100; void clear_screen(void)
101_clear_screen::
102 di
103 push hl
104 in a, (#SLOT_DEVICE)
105 ld h, a
106 in a, (#SLOT_PAGE)
107 ld l, a
108 push hl
109 ld a, #DEVICE_LCD_RIGHT
110 out (#SLOT_DEVICE), a
111 call _clear_lcd_half
112 ld a, #DEVICE_LCD_LEFT
113 out (#SLOT_DEVICE), a
114 call _clear_lcd_half
115 pop hl
116 ld a, h
117 out (#SLOT_DEVICE), a
118 ld a, l
119 out (#SLOT_PAGE), a
120 pop hl
121 ei
122 ret
123
124_clear_screen_bufs::
125 di
126 push bc
127 push de
128 push hl
129 xor a
130 ld (_cursorx), a
131 ld (_cursory), a
132 ld (_saved_cursorx), a
133 ld (_saved_cursory), a
134 ld (_putchar_sgr), a
135zero_screenbuf:
136 ld hl, #_screenbuf
137 ld de, #_screenbuf + 1
138 ld bc, #_screenbufend - _screenbuf
139 ld (hl), #' '
140 ldir
141zero_screenattrs:
142 ld hl, #_screenattrs
143 ld de, #_screenattrs + 1
144 ld bc, #_screenattrsend - _screenattrs
145 ld (hl), #0
146 ldir
147clear_screen_out:
148 pop hl
149 pop de
150 pop bc
151 ei
152 ret
153
154
155; void clear_lcd_half(void)
156; zero out the current LCD module (must already be in SLOT_DEVICE)
157; from v2.54 firmware at 0x2490
158_clear_lcd_half::
159 push bc
160 push de
161 ld b, #20 ; do 20 columns total
162clear_lcd_column:
163 ld h, #0
164 ld a, b
165 dec a ; columns are 0-based
166 ld l, a
167 push hl
168 call _lcd_cas
169 pop hl
170 push bc ; preserve our column counter
171 ld hl, #LCD_START
172 ld (hl), #0 ; zero out hl, then copy it to de
173 ld de, #LCD_START + 1 ; de will always be the next line
174 ld bc, #128 - 1 ; iterate (LCD_HEIGHT - 1) times
175 ldir ; ld (de), (hl), bc-- until 0
176 pop bc ; restore column counter
177 djnz clear_lcd_column ; column--, if not zero keep going
178clear_done:
179 pop de
180 pop bc
181 ret
182
183
184; void redraw_screen(void)
185_redraw_screen::
186 push bc
187 push de
188 push hl
189 ld b, #0
190redraw_rows:
191 ld d, b ; store rows in d
192 ld b, #0
193redraw_cols:
194 push bc ; XXX figure out what is corrupting
195 push de ; bc and de in stamp_char, these shouldn't be needed
196 push hl
197 ld h, #0 ; cols
198 ld l, b
199 push hl
200 ld h, #0 ; rows
201 ld l, d
202 push hl
203 call _stamp_char
204 pop hl
205 pop hl
206 pop hl
207 pop de
208 pop bc
209redraw_cols_next:
210 inc hl
211 inc b
212 ld a, b
213 cp #LCD_COLS
214 jr nz, redraw_cols
215 ld b, d
216 inc b
217 ld a, b
218 cp #LCD_ROWS
219 jr nz, redraw_rows
220redraw_screen_out:
221 pop hl
222 pop de
223 pop bc
224 ret
225
226
227; void scroll_lcd(void)
228; scroll entire screen up by FONT_HEIGHT rows, minus statusbar
229_scroll_lcd::
230 di
231 push bc
232 push de
233 push hl
234 in a, (#SLOT_DEVICE)
235 ld h, a
236 in a, (#SLOT_PAGE)
237 ld l, a
238 push hl
239 ld a, #DEVICE_LCD_LEFT
240 out (#SLOT_DEVICE), a
241 call _scroll_lcd_half
242 ld a, #DEVICE_LCD_RIGHT
243 out (#SLOT_DEVICE), a
244 call _scroll_lcd_half
245 pop hl
246 ld a, h
247 out (#SLOT_DEVICE), a
248 ld a, l
249 out (#SLOT_PAGE), a
250shift_bufs:
251 ld b, #0
252screenbuf_shift_loop:
253 ld h, b
254 ld l, #0
255 call screenbuf_offset
256 ld de, #_screenbuf
257 add hl, de ; hl = screenbuf[b * LCD_COLS]
258 push hl
259 ld de, #LCD_COLS
260 add hl, de ; hl += LCD_COLS
261 pop de ; de = screenbuf[b * LCD_COLS]
262 push bc
263 ld bc, #LCD_COLS
264 ldir ; ld (de), (hl), de++, hl++, bc--
265 pop bc
266 inc b
267 ld a, b
268 cp #TEXT_ROWS - 1
269 jr nz, screenbuf_shift_loop
270screenattrs_shift:
271 ld b, #0
272screenattrs_shift_loop:
273 ld h, b
274 ld l, #0
275 call screenbuf_offset
276 ld de, #_screenattrs
277 add hl, de ; hl = screenattrs[b * LCD_COLS]
278 push hl
279 ld de, #LCD_COLS
280 add hl, de
281 pop de
282 push bc
283 ld bc, #LCD_COLS
284 ldir
285 pop bc
286 inc b
287 ld a, b
288 cp #TEXT_ROWS - 1
289 jr nz, screenattrs_shift_loop
290last_row_zero:
291 ld a, #TEXT_ROWS - 1
292 ld h, a
293 ld l, #0
294 call screenbuf_offset
295 ld de, #_screenbuf
296 add hl, de
297 ld d, #0
298 ld e, #LCD_COLS - 1
299 add hl, de
300 ld b, #LCD_COLS
301 ld a, (_putchar_sgr)
302last_row_zero_loop:
303 ld (hl), #' '
304 dec hl
305 djnz last_row_zero_loop
306scroll_lcd_out:
307 pop hl
308 pop de
309 pop bc
310 ei
311 ret
312
313
314; void scroll_lcd_half(void)
315; scroll current LCD module up by FONT_HEIGHT rows, minus statusbar and
316; zero out the last line of text (only to the LCD)
317_scroll_lcd_half::
318 push ix
319 ld ix, #0
320 add ix, sp
321 push bc
322 push de
323 push hl
324 ; alloc 2 bytes on the stack for local storage
325 push hl
326 ld a, #LCD_HEIGHT - (FONT_HEIGHT * 2) ; iterations of pixel row moves
327scroll_init:
328 ld -1(ix), a ; store iterations
329 ld b, #20 ; do 20 columns total
330scroll_lcd_column:
331 ld -2(ix), b ; store new column counter
332 ld a, b
333 sub #1 ; columns are 0-based
334 ld h, #0
335 ld l, a
336 push hl
337 call _lcd_cas
338 pop hl
339scroll_rows:
340 ld b, #0
341 ld c, -1(ix) ; bc = row counter
342 ld hl, #LCD_START + 8 ; start of next line
343 ld de, #LCD_START
344 ldir ; ld (de), (hl), bc-- until 0
345scroll_zerolast:
346 ld hl, #LCD_START
347 ld d, #0
348 ld e, -1(ix)
349 add hl, de
350 ld b, #FONT_HEIGHT
351scroll_zerolastloop: ; 8 times: zero hl, hl++
352 ld (hl), #0
353 inc hl
354 djnz scroll_zerolastloop
355 ld b, -2(ix)
356 djnz scroll_lcd_column ; column--, if not zero keep going
357 pop hl
358 pop de
359 pop bc
360 ld sp, ix
361 pop ix
362 ret
363
364
365; address of screenbuf or screenattrs offset for a row/col in hl, returns in hl
366screenbuf_offset:
367 push bc
368 push de
369 ; uses hl
370 ex de, hl
371 ld hl, #0
372 ld a, d ; row
373 cp #0
374 jr z, multiply_srow_out ; only add rows if > 0
375 ld bc, #LCD_COLS
376multiply_srow:
377 add hl, bc
378 dec a
379 cp #0
380 jr nz, multiply_srow
381multiply_srow_out:
382 ld d, #0 ; col in e
383 add hl, de ; hl = (row * LCD_COLS) + col
384 pop de
385 pop bc
386 ret ; hl
387
388
389; void stamp_char(unsigned int row, unsigned int col)
390; row at 4(ix), col at 6(ix)
391_stamp_char::
392 push ix
393 ld ix, #0
394 add ix, sp
395 push bc
396 push de
397 push hl
398 ld hl, #-15 ; stack bytes for local storage
399 add hl, sp
400 ld sp, hl
401 in a, (#SLOT_DEVICE)
402 ld -3(ix), a ; stack[-3] = old slot device
403 in a, (#SLOT_PAGE)
404 ld -4(ix), a ; stack[-4] = old slot page
405find_char:
406 ld h, 4(ix)
407 ld l, 6(ix)
408 call screenbuf_offset
409 push hl
410 ld de, #_screenbuf
411 add hl, de ; hl = screenbuf[(row * LCD_COLS) + col]
412 ld a, (hl)
413 ld -5(ix), a ; stack[-5] = character to stamp
414 pop hl
415 ld de, #_screenattrs
416 add hl, de ; hl = screenattrs[(row * LCD_COLS) + col]
417 ld a, (hl)
418 ld -6(ix), a ; stack[-6] = character attrs
419calc_font_data_base:
420 ld h, #0
421 ld l, -5(ix) ; char
422 add hl, hl ; hl = char * FONT_HEIGHT (8)
423 add hl, hl
424 add hl, hl
425 ld de, #font_data
426 add hl, de
427 ld -7(ix), l
428 ld -8(ix), h ; stack[-8,-7] = char font data base addr
429calc_char_cell_base:
430 ld h, #0
431 ld l, 4(ix) ; row
432 add hl, hl
433 add hl, hl
434 add hl, hl ; hl = row * FONT_HEIGHT (8)
435 ld de, #LCD_START
436 add hl, de ; hl = 4038 + (row * FONT_HEIGHT)
437 ld -9(ix), l
438 ld -10(ix), h ; stack[-10,-9] = lcd char cell base
439fetch_from_table:
440 ld a, 6(ix) ; col
441 ld hl, #cursorx_lookup_data
442 ld b, #0
443 ld c, a
444 add hl, bc
445 ld b, (hl)
446 ld a, b
447pluck_col_group:
448 and #0b11111000 ; upper 5 bits are col group
449 srl a
450 srl a
451 srl a
452 ld -11(ix), a ; stack[-11] = col group
453pluck_offset:
454 ld a, b
455 and #0b00000111 ; lower 3 bits are offset
456 ld -12(ix), a ; stack[-12] = offset
457 ld -15(ix), #0 ; stack[-15] = previous lcd col
458 ld d, #FONT_HEIGHT ; for (row = FONT_HEIGHT; row >= 0; row--)
459next_char_row:
460 ld a, d
461 dec a
462 ld h, -8(ix) ; char font data base
463 ld l, -7(ix)
464 ld b, #0
465 ld c, a
466 add hl, bc
467 ld a, (hl) ; font_addr + (char * FONT_HEIGHT) + row
468 ld b, -6(ix)
469 bit #ATTR_BIT_REVERSE, b
470 jr nz, reverse
471 bit #ATTR_BIT_CURSOR, b
472 jr nz, reverse
473 jr not_reverse
474reverse:
475 cpl ; flip em
476 and #0b00011111 ; mask off bits not within FONT_WIDTH
477not_reverse:
478 ld -13(ix), a ; stack[-13] = working font data
479 ld a, -6(ix)
480 bit #ATTR_BIT_UNDERLINE, a
481 jr z, not_underline
482 ld a, d
483 cp #FONT_HEIGHT
484 jr nz, not_underline
485underline:
486 ld -13(ix), #0xff
487not_underline:
488 ld a, 6(ix) ; col
489 cp #LCD_COLS / 2 ; assume a char never spans both LCD sides
490 jr nc, rightside
491leftside:
492 ld a, #DEVICE_LCD_LEFT
493 jr swap_lcd
494rightside:
495 ld a, #DEVICE_LCD_RIGHT
496swap_lcd:
497 out (#SLOT_DEVICE), a
498 ld e, #FONT_WIDTH ; for (col = FONT_WIDTH; col > 0; col--)
499next_char_col: ; inner loop, each col of each row
500 ld -14(ix), #0b00011111 ; font data mask that will get shifted
501determine_cas:
502 ld c, #0
503 ld b, -11(ix) ; col group
504 ld a, -12(ix) ; bit offset
505 add #FONT_WIDTH
506 sub e ; if offset+(5-col) is >= 8, advance col
507 cp #LCD_COL_GROUP_WIDTH
508 jr c, skip_advance ; if a >= 8, advance (dec b)
509 dec b
510 ld c, -12(ix) ; bit offset
511 ld a, #LCD_COL_GROUP_WIDTH
512 sub c
513 ld c, a ; c = number of right shifts
514skip_advance:
515do_lcd_cas:
516 ld a, -15(ix) ; previous lcd cas
517 cp b
518 jr z, prep_right_shift
519 ld h, #0
520 ld l, b
521 push hl
522 call _lcd_cas
523 pop hl
524 ld -15(ix), b ; store lcd col for next round
525 ; if this character doesn't fit entirely in one lcd column, we need to
526 ; span two of them and on the left one, shift font data and masks right
527 ; to remove right-most bits that will be on the next column
528prep_right_shift:
529 ld a, c
530 cp #0
531 jr z, prep_left_shift
532 ld b, c
533 ld c, -14(ix) ; matching mask 00011111
534 ld a, -13(ix) ; load font data like 00010101
535right_shift:
536 srl a ; shift font data right #b times
537 srl c ; and mask to match
538 djnz right_shift ; -> 10101000
539 ld -14(ix), c
540 jr done_left_shift
541prep_left_shift:
542 ld c, -14(ix) ; mask
543 ld a, -12(ix) ; (bit offset) times, shift font data
544 cp #0
545 ld b, a
546 ld a, -13(ix) ; read new font data
547 jr z, done_left_shift
548left_shift:
549 sla a
550 sla c
551 djnz left_shift
552done_left_shift:
553 ld b, a
554 ld a, c
555 cpl
556 ld -14(ix), a ; store inverted mask
557 ld a, b
558read_lcd_data:
559 ld h, -10(ix)
560 ld l, -9(ix)
561 ld b, a
562 ld a, d
563 dec a
564 ld c, a
565 ld a, b
566 ld b, #0
567 add hl, bc ; hl = 4038 + (row * FONT_HEIGHT) + row - 1
568 ld b, a ; store new font data
569 ld a, (hl) ; read existing cell data
570 and -14(ix) ; mask off new char cell
571 or b ; combine data into cell
572 ld (hl), a
573 dec e
574 jp nz, next_char_col
575 dec d
576 jp nz, next_char_row
577stamp_char_out:
578 ld a, -3(ix) ; restore old slot device
579 out (#SLOT_DEVICE), a
580 ld a, -4(ix) ; restore old slot page
581 out (#SLOT_PAGE), a
582 ld hl, #15 ; remove stack bytes
583 add hl, sp
584 ld sp, hl
585 pop hl
586 pop de
587 pop bc
588 ld sp, ix
589 pop ix
590 ret
591
592
593; void uncursor(void)
594; remove cursor attribute from old cursor position
595_uncursor::
596 push de
597 push hl
598 ld a, (_cursory)
599 ld h, a
600 ld a, (_cursorx)
601 ld l, a
602 call screenbuf_offset
603 ld de, #_screenattrs
604 add hl, de ; screenattrs[(cursory * TEXT_COLS) + cursorx]
605 ld a, (hl)
606 res #ATTR_BIT_CURSOR, a ; &= ~(ATTR_CURSOR)
607 ld (hl), a
608 ld a, (_cursorx)
609 ld l, a
610 push hl
611 ld a, (_cursory)
612 ld l, a
613 push hl
614 call _stamp_char
615 pop hl
616 pop hl
617 pop hl
618 pop de
619 ret
620
621; void recursor(void)
622; force-set cursor attribute
623_recursor::
624 push de
625 push hl
626 ld a, (_cursory)
627 ld h, a
628 ld a, (_cursorx)
629 ld l, a
630 call screenbuf_offset
631 ld de, #_screenattrs
632 add hl, de ; screenattrs[(cursory * TEXT_COLS) + cursorx]
633 ld a, (hl)
634 set #ATTR_BIT_CURSOR, a
635 ld (hl), a
636 pop hl
637 pop de
638 ret
639
640
641; int putchar(int c)
642_putchar::
643 push ix
644 ld ix, #0
645 add ix, sp ; char to print is at 4(ix)
646 push de
647 push hl
648 call _uncursor
649 ld a, 4(ix)
650 cp #'\b' ; backspace
651 jr nz, not_backspace
652backspace:
653 ld a, (_cursorx)
654 cp #0
655 jr nz, cursorx_not_zero
656 ld a, (_cursory)
657 cp #0
658 jp z, putchar_fastout ; cursorx/y at 0,0, nothing to do
659 dec a
660 ld (_cursory), a ; cursory--
661 ld a, #LCD_COLS - 2
662 ld (_cursorx), a
663 jp putchar_draw_cursor
664cursorx_not_zero:
665 dec a
666 ld (_cursorx), a ; cursorx--;
667 jp putchar_draw_cursor
668not_backspace:
669 cp #'\r'
670 jr nz, not_cr
671 xor a
672 ld (_cursorx), a ; cursorx = 0
673 jr not_crlf
674not_cr:
675 cp #'\n'
676 jr nz, not_crlf
677 xor a
678 ld (_cursorx), a ; cursorx = 0
679 ld a, (_cursory)
680 inc a
681 ld (_cursory), a ; cursory++
682not_crlf:
683 ld a, (_cursorx)
684 cp #LCD_COLS
685 jr c, not_longer_text_cols ; cursorx < TEXT_COLS
686 xor a
687 ld (_cursorx), a ; cursorx = 0
688 ld a, (_cursory)
689 inc a
690 ld (_cursory), a
691not_longer_text_cols:
692 ld a, (_cursory)
693 cp #TEXT_ROWS
694 jr c, scroll_out
695scroll_up_screen:
696 call _scroll_lcd
697 xor a
698 ld (_cursorx), a
699 ld a, #TEXT_ROWS - 1
700 ld (_cursory), a ; cursory = TEXT_ROWS - 1
701scroll_out:
702 ld a, 4(ix)
703 cp a, #'\r'
704 jr z, cr_or_lf
705 cp a, #'\n'
706 jr z, cr_or_lf
707 jr store_char_in_buf
708cr_or_lf:
709 jp putchar_draw_cursor
710store_char_in_buf:
711 ld a, (_cursory)
712 ld h, a
713 ld a, (_cursorx)
714 ld l, a
715 call screenbuf_offset
716 push hl
717 ld de, #_screenbuf
718 add hl, de ; hl = screenbuf[(cursory * LCD_COLS) + cursorx]
719 ld a, 4(ix)
720 ld (hl), a ; store character
721 pop hl
722 ld de, #_screenattrs
723 add hl, de ; hl = screenattrs[(cursory * LCD_COLS) + cursorx]
724 ld a, (_putchar_sgr)
725 ld (hl), a ; = putchar_sgr
726 ld a, (_cursorx)
727 ld l, a
728 push hl
729 ld a, (_cursory)
730 ld l, a
731 push hl
732 call _stamp_char
733 pop hl
734 pop hl
735advance_cursorx:
736 ld a, (_cursorx)
737 inc a
738 ld (_cursorx), a
739 cp #LCD_COLS ; if (cursorx >= LCD_COLS)
740 jr c, putchar_draw_cursor
741 xor a
742 ld (_cursorx), a
743 ld a, (_cursory)
744 inc a
745 ld (_cursory), a
746check_cursory:
747 cp #TEXT_ROWS ; and if (cursory >= TEXT_ROWS)
748 jr c, putchar_draw_cursor
749 call _scroll_lcd
750 ld a, #TEXT_ROWS - 1
751 ld (_cursory), a ; cursory = TEXT_ROWS - 1
752putchar_draw_cursor:
753 ld a, (_cursory)
754 ld h, a
755 ld a, (_cursorx)
756 ld l, a
757 call screenbuf_offset
758 ld de, #_screenattrs
759 add hl, de ; hl = screenattrs[(cursory * LCD_COLS) + cursorx]
760 ld a, (hl) ; read existing attrs
761 set #ATTR_BIT_CURSOR, a
762 ld (hl), a ; = putchar_sgr | ATTR_CURSOR
763 ld a, (_cursorx)
764 ld l, a
765 push hl
766 ld a, (_cursory)
767 ld l, a
768 push hl
769 call _stamp_char
770 pop hl
771 pop hl
772putchar_fastout:
773 pop hl
774 pop de
775 ld sp, ix
776 pop ix
777 ret
778
779
780; void putchar_attr(unsigned char row, unsigned char col, char c, char attr)
781; directly manipulates screenbuf/attrs without scrolling or length checks
782; row at 4(ix), col at 5(ix), c at 6(ix), attr at 7(ix)
783_putchar_attr::
784 push ix
785 ld ix, #0
786 add ix, sp
787 push de
788 push hl
789store_char:
790 ld h, 4(ix)
791 ld l, 5(ix)
792 call screenbuf_offset
793 push hl
794 ld de, #_screenbuf
795 add hl, de ; screenbuf[(row * TEXT_COLS) + col]
796 ld a, 6(ix)
797 ld (hl), a
798store_attrs:
799 pop hl
800 ld de, #_screenattrs
801 add hl, de ; screenattrs[(row * TEXT_COLS) + col]
802 ld a, 7(ix)
803 ld (hl), a
804 ld l, 5(ix)
805 push hl
806 ld l, 4(ix)
807 push hl
808 call _stamp_char
809 pop hl
810 pop hl
811 pop hl
812 pop de
813 ld sp, ix
814 pop ix
815 ret