From 95c9b2c3ee21365ea027ec481ccb1ef9909f8139 Mon Sep 17 00:00:00 2001 From: Zellyn Hunter Date: Wed, 27 Mar 2013 21:49:38 -0700 Subject: [PATCH] Reorganizing things so texty is more like the real thing --- docs/apple2.org | 142 ++++++++++++++++++++++ goapple2.go | 77 ++++++++++++ texty/texty.go | 168 ++++++++------------------ util/util.go | 13 ++ videoscan/videoscan.go | 233 ++++++++++++++++++++++++++++++++++++ videoscan/videoscan_test.go | 224 ++++++++++++++++++++++++++++++++++ 6 files changed, 738 insertions(+), 119 deletions(-) create mode 100644 goapple2.go create mode 100644 util/util.go create mode 100644 videoscan/videoscan.go create mode 100644 videoscan/videoscan_test.go diff --git a/docs/apple2.org b/docs/apple2.org index 2c6e90f..228fe45 100644 --- a/docs/apple2.org +++ b/docs/apple2.org @@ -217,6 +217,7 @@ Apple II notes - http://www.textfiles.com/apple/peeks.pokes.2 - http://www.applefritter.com/node/24236 - http://www.easy68k.com/paulrsm/6502/index.html - Information about firmware +- http://www-personal.umich.edu/~mressl/a2dp/index.html - Apple II documentation project * Language card Install in slot 0. @@ -326,3 +327,144 @@ and selects the second 4K bank for $D000-$DFFF. | Z | DA | 9A | | | | ESC | 9B | 9B | | | + +* Character set +@ABCDEFGHIJKLMNO +PQRSTUVWXYZ[\]^_ + !"#$%&'()*+,-./ +0123456789:;<=>? + +64 * 8 = 512 +Revision 7: 256 * 8 = 2048 + +* Video scanning +H0,H1,H2,H3,H4,H5,HPE' +VA,VB,VC,V0,V1,V2,V3,V4,V5 + +** Horizontal scanning: H0-H5,HPE' +7 bits, 65 states. 0000000,1000000-1111111. HPE' is low only for one cycle, the long cycle. +40/65 states send picture output. 25/65 are blanked. + +** Vertical scanning: VA-VC,V0-V5 +Increments on overflow from the horizontal section. +9 bits, 262 states. 011111010-111111111. +192/262 states send picture information, 70/262 are blanked. +V4V3 divides the screen into thirds: +- V4'V3' - Top third +- V4'V3 - Middle third +- V4 V3' - Bottom third +- V4 V3 - Undisplayed (VBL) +6 cycles blanked: 011111010-011111111 +64 cycles blanked: 111000000-111111111 + +| Location on TV screen | Least significant address bits | +|-----------------------+--------------------------------| +| Top | 0000000 - 0100111 (First 40) | +| Middle | 0101000 - 1001111 (Second 40) | +| Bottom | 1010000 - 1110111 (Third 40) | + +Lowest 3 bits are easy: H2,H1,H0 == A2,A1,A0 +Next four bits are H5-H4-H3 + offset = SUM-A{3..6} +- 0000-0100, 0101-1001, 1010-1110. +H5-H4-H3: 000..010 are undisplayed: right margin, retrace, left margin. 011 first displayed. +H543 -3 in First 40, +2 in second 40, +7 in Third 40. + + 1 1 0 1 ++ H5 H4 H3 ++ V4 V3 V4 V3 +--------------------------- +SUM-A6 SUM-A5 SUM-A4 SUM-A3 + +** Pages: +$0000-$3FFF - ! HIRES PAGE 2 +$4000-$7FFF - HIRES PAGE 2 + +HIRES TIME: +- high when in HIRES, GRAPHICS, NO MIX mode +- high when in HIRES, GRAPHICS, MIX, !(V4•V2) (·) + + + +| A0 | H0 | +| A1 | H1 | +| A2 | H2 | +| A3 | SUM-A3 | +| A4 | SUM-A4 | +| A5 | SUM-A5 | +| A6 | SUM-A6 | +| A7 | V0 | +| A8 | V1 | +| A9 | V2 | +| A10 | HIRES•VA + TEXT/LORES•PAGE1 | +| A11 | HIRES•VB + TEXT/LORES•PAGE2 | +| A12 | HIRES•VC + TEXT/LORES•HBL | +| A13 | HIRES•PAGE1 | +| A14 | HIRES•PAGE2 | +| A15 | - | + +VA,VB,VC determine which part of text characters to draw. +VC determines which of two LORES blocks to draw. + + + +* Colors + +| Color Ref | 0011 | +| Dark Magenta | 0001 | +| Light Magenta | 1011 | +| HIRES Violet | 0011 | +| Dark Blue | 0010 | +| Light Blue | 0111 | +| HIRES Blue | 0110 | +| Dark Blue-Green | 0100 | +| Light Blue-Green | 1110 | +| HIRES Green | 1100 | +| Dark Brown | 1000 | +| Light Brown | 1101 | +| HIRES Orange | 1001 | +| $5 Grey | 0101 | +| $A Grey | 1010 | +| White | 1111 | +| Black | 0000 | + +* Design + +Event loop runs everything. +Key/mouse/resize/quit events come from somewhere (sdl) +Key events go to CPU. +Event loop can pause emulator and bring up menu. +- change disks +- save state, etc. +- change parameters +Video scanner scanning memory -> chroma/luma signal -> color interpreter -> plotting +Event loop hands colorplotter a pixelplotter +colorplotter does green / b/w / color choice +Some kind of menu subsystem for paused/config mode. + + +* Gospeccy design + +spectrum.Application +- HasTerminated chan +- RequestExit() +spectrum.Spectrum48k (app, *rom) +- EmulatorLoop() +- CommandChannel +- speccy.Keyboard.Key{Down,Up} + +sdl_output.Main(): +- Get a spectrum.Application +- Get a spectrum.Spectrum48k +- r = NewSDLRenderer(app, speccy, ...) +- setUI(r) +- initCLI() +- go sdlEventLoop(app, speccy) +- wait for shutdown +- sdl.Quit() + +sdlEventLoop(): +- evtLoop = app.NewEventLoop() + + + + diff --git a/goapple2.go b/goapple2.go new file mode 100644 index 0000000..3d30df6 --- /dev/null +++ b/goapple2.go @@ -0,0 +1,77 @@ +package goapple2 + +import ( + "github.com/zellyn/go6502/cpu" + "github.com/zellyn/goapple2/videoscan" +) + +// Memory for the tests. Satisfies the cpu.Memory interface. +type Apple2 struct { + mem [65536]byte + cpu cpu.Cpu + key byte // BUG(zellyn): make reads/writes atomic + keys chan byte + plotter videoscan.Plotter + scanner *videoscan.Scanner +} + +// Cycle counter. Satisfies the cpu.Ticker interface. +type CycleCount uint64 + +func (c *CycleCount) Tick() { + *c += 1 +} + +func NewApple2(p videoscan.Plotter, rom []byte) *Apple2 { + var cc CycleCount + a2 := Apple2{ + keys: make(chan byte, 16), + } + copy(a2.mem[len(a2.mem)-len(rom):len(a2.mem)], rom) + a2.scanner = videoscan.NewScanner(&a2, p, [2048]byte{}) + a2.cpu = cpu.NewCPU(&a2, &cc, cpu.VERSION_6502) + a2.cpu.Reset() + return &a2 +} + +func (a2 *Apple2) Read(address uint16) byte { + // Keyboard read + if address == 0xC000 { + if a2.key&0x80 == 0 { + select { + case key := <-a2.keys: + a2.key = key + default: + } + } + return a2.key + } + if address == 0xC010 { + a2.key &= 0x7F + return 0 // BUG(zellyn): return proper value (keydown on IIe, not sure on II+) + } + return a2.mem[address] +} + +func (a2 *Apple2) Write(address uint16, value byte) { + if address >= 0xD000 { + return + } + if address == 0xC010 { + // Clear keyboard strobe + a2.key &= 0x7F + } + a2.mem[address] = value +} + +func (a2 *Apple2) Keypress(key byte) { + a2.keys <- key +} + +func (a2 *Apple2) Step() error { + if err := a2.cpu.Step(); err != nil { + return err + } + a2.scanner.Scan1() + return nil +} diff --git a/texty/texty.go b/texty/texty.go index 2c12935..6390f23 100644 --- a/texty/texty.go +++ b/texty/texty.go @@ -3,24 +3,14 @@ package main import ( "fmt" - "io/ioutil" "time" "github.com/nsf/termbox-go" - "github.com/zellyn/go6502/asm" - "github.com/zellyn/go6502/cpu" + "github.com/zellyn/goapple2" + "github.com/zellyn/goapple2/util" + "github.com/zellyn/goapple2/videoscan" ) -// Memory for the tests. Satisfies the cpu.Memory interface. -type Apple2 struct { - mem [65536]byte - events chan termbox.Event - done bool - key byte // BUG(zellyn): make reads/writes atomic - keys chan byte - debug bool // Set true and close termbox to start tracing out instructions -} - // Mapping of screen bytes to character values var AppleChars = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" @@ -55,136 +45,76 @@ func termboxToAppleKeyboard(ev termbox.Event) (key byte, err error) { return 0, fmt.Errorf("hi") } -func (a2 *Apple2) Read(address uint16) byte { - // Keyboard read - if address == 0xC000 { - return a2.key - } - if address == 0xC010 { - a2.key &= 0x7F - return 0 // BUG(zellyn): return proper value (keydown on IIe, not sure on II+) - } - return a2.mem[address] -} +// func Write(address uint16, value byte) { +// a2.mem[address] = value +// if address >= 0x0400 && address < 0x0800 { +// offset := int(address - 0x0400) +// count := offset & 0x7f +// if count <= 119 { +// x := count % 40 +// segment := offset / 128 +// which40 := count / 40 +// y := which40*8 + segment +// ch, fg, bg := translateToTermbox(value) +// termbox.SetCell(x+1, y+1, ch, fg, bg) +// termbox.Flush() +// } +// } +// } -func (a2 *Apple2) Write(address uint16, value byte) { - if address >= 0xD000 { - termbox.Close() - panic(fmt.Sprintln("Write to ROM at address $%04X", address)) - } - if address == 0xC010 { - // Clear keyboard strobe - a2.key &= 0x7F - } - a2.mem[address] = value - if !a2.debug && address >= 0x0400 && address < 0x0800 { - offset := int(address - 0x0400) - count := offset & 0x7f - if count <= 119 { - x := count % 40 - segment := offset / 128 - which40 := count / 40 - y := which40*8 + segment - ch, fg, bg := translateToTermbox(value) - termbox.SetCell(x+1, y+1, ch, fg, bg) - termbox.Flush() - } - } -} - -func (a2 *Apple2) Init() error { - if err := termbox.Init(); err != nil { - return err - } - a2.events = make(chan termbox.Event) - a2.keys = make(chan byte, 16) - go func() { - for { - a2.events <- termbox.PollEvent() - } - }() - return nil -} -func (a2 *Apple2) Close() { - termbox.Close() -} - -func (a2 *Apple2) ProcessEvents() { +func ProcessEvents(events chan termbox.Event, a2 *goapple2.Apple2) bool { select { - case ev := <-a2.events: + case ev := <-events: if ev.Type == termbox.EventKey && ev.Ch == '~' { - a2.done = true + return true } if ev.Type == termbox.EventKey { if key, err := termboxToAppleKeyboard(ev); err == nil { - a2.keys <- key | 0x80 + a2.Keypress(key | 0x80) } } default: } - if a2.key&0x80 == 0 { - select { - case key := <-a2.keys: - a2.key = key - default: - } + return false +} + +type TextPlotter int + +func (p TextPlotter) Plot(data videoscan.PlotData) { + y := int(data.Row / 8) + x := int(data.Column) + value := data.RawData + ch, fg, bg := translateToTermbox(value) + termbox.SetCell(x+1, y+1, ch, fg, bg) + if x == 39 && data.Row == 191 { + termbox.Flush() } } -// Cycle counter for the tests. Satisfies the cpu.Ticker interface. -type CycleCount uint64 - -func (c *CycleCount) Tick() { - *c += 1 -} - -// printStatus prints out the current CPU instruction and register status. -func printStatus(c cpu.Cpu, m *[65536]byte) { - bytes, text, _ := asm.Disasm(c.PC(), m[c.PC()], m[c.PC()+1], m[c.PC()+2]) - fmt.Printf("$%04X: %s %s A=$%02X X=$%02X Y=$%02X SP=$%02X P=$%08b\n", - c.PC(), bytes, text, c.A(), c.X(), c.Y(), c.SP(), c.P()) -} - // Run the emulator func RunEmulator() { - bytes, err := ioutil.ReadFile("../data/roms/apple2+.rom") - if err != nil { - panic("Cannot read ROM file") + rom := util.ReadRomOrDie("../data/roms/apple2+.rom") + plotter := TextPlotter(0) + a2 := goapple2.NewApple2(plotter, rom) + if err := termbox.Init(); err != nil { + panic(err) } - var a2 Apple2 - ROM_OFFSET := 0xD000 - copy(a2.mem[ROM_OFFSET:ROM_OFFSET+len(bytes)], bytes) - a2.Init() - var cc CycleCount - c := cpu.NewCPU(&a2, &cc, cpu.VERSION_6502) - c.Reset() - for !a2.done { - // // LIST - // if c.PC() == 0xD6A5 { - // termbox.Close() - // a2.debug = true - // } - // // End of LIST - // if c.PC() == 0xD729 { - // break - // } - if !a2.debug { - a2.ProcessEvents() + events := make(chan termbox.Event) + go func() { + for { + events <- termbox.PollEvent() } - if a2.debug { - printStatus(c, &a2.mem) - } - err := c.Step() + }() + for !ProcessEvents(events, a2) { + err := a2.Step() if err != nil { fmt.Println(err) break } time.Sleep(1 * time.Nanosecond) // So the keyboard-reading goroutines can run } - if !a2.debug { - a2.Close() - } + termbox.Close() } func main() { diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..8c275db --- /dev/null +++ b/util/util.go @@ -0,0 +1,13 @@ +package util + +import ( + "io/ioutil" +) + +func ReadRomOrDie(filename string) []byte { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + panic("Cannot read ROM file: " + filename) + } + return bytes +} diff --git a/videoscan/videoscan.go b/videoscan/videoscan.go new file mode 100644 index 0000000..93b4a9b --- /dev/null +++ b/videoscan/videoscan.go @@ -0,0 +1,233 @@ +package videoscan + +import ( + "fmt" + + "github.com/zellyn/go6502/cpu" +) + +const FLASH_CYCLES = 15 // 15/60 = four per second + +// PlotData is the struct that holds plotting +// information. Essentially, it holds the (binary) waveform of the +// color pattern. +type PlotData struct { + Row byte "The row (0-191)" + Column byte "The column (0-39)" + ColorBurst bool "Whether the color signal is active" + Data uint16 "14 half color cycles of information" + RawData byte "The underlying raw byte" +} + +type Plotter interface { + Plot(PlotData) +} + +type Scanner struct { + m cpu.Memory + h uint16 + v uint16 + plotter Plotter + rom [2048]byte + + graphics bool // TEXT/GRAPHICS + mix bool // NOMIX/MIX + hires bool // LORES/HIRES + page2 bool // PAGE1/PAGE2 + + hbl bool // Horizontal blanking + vbl bool // Vertical blanking + lastFour bool // Are we in the last 4 lines of the screen? + lastBit uint16 // Last bit of previous 14-cycle color data + flasher byte // if high bit is set, invert flashing text + flashCount int // count up, and toggle flasher +} + +func NewScanner(m cpu.Memory, p Plotter, rom [2048]byte) *Scanner { + s := &Scanner{ + m: m, + plotter: p, + rom: rom, + h: 0x7f, + v: 0x1ff, + } + s.inc() + return s +} + +func (s *Scanner) inc() { + // Increment H0..H5,HPE' + switch s.h { + case 0: + s.h = 0x40 + case 0x7f: + s.h = 0 + // Increment VA-VC,V0-V5 + switch s.v { + case 0x1ff: + s.v = 250 + if s.flashCount++; s.flashCount >= FLASH_CYCLES { + s.flashCount = 0 + s.flasher ^= 0x80 + } + default: + s.v++ + } + + // VBL = V4 & V3 + s.vbl = (s.v>>6)&3 == 3 + + // Last four lines of the screen? + s.lastFour = ((s.v>>5)&5 == 5) + default: + s.h++ + } + + // HBL = H5' & (H3' + H4') + s.hbl = ((s.h >> 3) & 7) <= 2 +} + +func (s *Scanner) Scan1() { + m := s.m.Read(s.address()) + row, column := s.row(), s.column() + _, _, _ = m, row, column + var data uint16 + color := s.graphics + switch { + case !s.graphics || (s.mix && s.lastFour): + data = s.textData(m, row, column) + case s.hires: + data = s.hiresData(m, row, column) + default: // lores + data = s.loresData(m, row, column) + } + s.lastBit = (data >> 13) & 1 + if !s.hbl && !s.vbl { + s.plotter.Plot(PlotData{ + Row: byte(row), + Column: byte(column), + ColorBurst: color, + Data: data, + RawData: m, + }) + } + s.inc() +} + +func (s *Scanner) column() int { + return int(s.h) - 0x58 // 0x1011000 +} + +func (s *Scanner) row() int { + return int(s.v) - 0x100 +} + +func (s *Scanner) address() uint16 { + // Low three bits are just H0-H2 + addr := s.h & 7 + + // Next four bits are H5,H4,H3 + offset = SUM-A6,SUM-A5,SUM-A4,SUM-A3 + bias := uint16(0xD) // 1 1 0 1 + hsum := (s.h >> 3) & 7 // 0 H5 H4 H3 + vsum := (s.v >> 6) & 3 // V4 V3 V4 V3 + vsum = vsum | (vsum << 2) + suma36 := (bias + hsum + vsum) & 0xF + addr |= (suma36 << 3) + + // Next three are V0,V1,V2 + addr |= ((s.v >> 3 & 7) << 7) + + page := uint16(1) + if s.page2 { + page = 2 + } + + // HIRES TIME when HIRES,GRAPHICS,NOMIX or HIRES,GRAPHICS,MIX,!(V4&V2) + hiresTime := s.hires && s.graphics + if hiresTime && s.mix && s.lastFour { + hiresTime = false + } + + if hiresTime { + // A10-A12 = VA-VC + addr |= ((s.v & 7) << 10) + // A13=PAGE1, A14=PAGE2 + addr |= page << 13 + } else { + // A10=PAGE1, A11=PAGE2 + addr |= page << 10 + // A12 = HBL + if s.hbl { + addr |= (1 << 12) + } + } + + return addr +} + +func (s *Scanner) SetGraphics(graphics bool) { + s.graphics = graphics +} + +func (s *Scanner) SetMix(mix bool) { + s.mix = mix +} + +func (s *Scanner) SetHires(hires bool) { + s.hires = hires +} + +func (s *Scanner) SetPage(page int) { + switch page { + case 1: + s.page2 = false + case 2: + s.page2 = true + default: + panic(fmt.Sprint("Page must be 1 or 2, got", page)) + } +} + +func (s *Scanner) textData(m byte, row int, column int) uint16 { + line := s.rom[int(m)*8+((row+800)%8)] + // Invert if flash + if (m^0x80)&line&s.flasher > 0 { + line ^= 0xff + } + line &= 0x7f // Mask out high bit + // Now it's just like hires data + return s.hiresData(line, row, column) +} + +func (s *Scanner) hiresData(m byte, row int, column int) uint16 { + // Double each bit + var data uint16 + mm := uint16(m) + // BUG(zellyn): Use bitmagic to do this without looping + for i := byte(6); i != 0xff; i-- { + data |= ((mm >> i) & 1) * 3 + data <<= 2 + } + // High bit set delays the signal by 1/4 color cycle = 1 bit, + // and extends the last bit to fill in the delay. + if m > 127 { + data <<= 1 + data |= s.lastBit + } + return data & 0x3fff +} + +func (s *Scanner) loresData(m byte, row int, column int) uint16 { + var data uint16 + // First four rows get low nybble, second four high + if row%8 < 4 { + data = uint16(m & 0x0f) + } else { + data = uint16(m >> 4) + } + data = data * 0x1111 // Repeat lower nybble four times + if column%2 == 1 { + data >>= 2 + } + return data & 0x3fff +} diff --git a/videoscan/videoscan_test.go b/videoscan/videoscan_test.go new file mode 100644 index 0000000..3c721de --- /dev/null +++ b/videoscan/videoscan_test.go @@ -0,0 +1,224 @@ +package videoscan + +import ( + "testing" + + "github.com/zellyn/go6502/tests" + apple2 "github.com/zellyn/goapple2" +) + +type fakePlotter struct { +} + +func (f *fakePlotter) Plot(apple2.PlotData) { +} + +func TestHorizontal(t *testing.T) { + var m tests.Memorizer + var f fakePlotter + s := NewScanner(&m, &f, [2048]byte{}) + + // 1000 Increments + for i := 0; i < 1000; i++ { + for j := 0; j < 25; j++ { + if s.column() >= 0 { + t.Errorf("Expected s.column() < 0, got %v", s.column()) + } + s.Scan1() + } + for j := 0; j < 40; j++ { + if s.column() != j { + t.Errorf("Expected s.column()==%d, got %v", j, s.column()) + } + s.Scan1() + } + } +} + +func TestVertical(t *testing.T) { + var m tests.Memorizer + var f fakePlotter + s := NewScanner(&m, &f, [2048]byte{}) + scan65 := func() { + for i := 0; i < 65; i++ { + s.Scan1() + } + } + + // 1000 * 65 (per row) Increments + for i := 0; i < 1000; i++ { + for j := 0; j < 6; j++ { + if s.row() > 0 { + t.Errorf("Expected s.row() <= 0, got %v", s.row()) + } + scan65() + } + for j := 0; j < 256; j++ { + if s.row() != j { + t.Errorf("Expected s.row() == %d, got %v", j, s.row()) + } + scan65() + } + } +} + +func TestBlanking(t *testing.T) { + var m tests.Memorizer + var f fakePlotter + s := NewScanner(&m, &f, [2048]byte{}) + + for i := 0; i < 1000000; i++ { + if s.column() < 0 || s.column() >= 40 { + if !s.hbl { + t.Errorf("s.column()==%d, but s.hbl=%v", s.column(), s.hbl) + } + } else { + if s.hbl { + t.Errorf("s.column()==%d, but s.hbl=%v", s.column(), s.hbl) + } + } + + if s.row() < 0 || s.row() >= 192 { + if !s.vbl { + t.Errorf("s.row()==%d, but s.vbl=%v", s.row(), s.vbl) + } + } else { + if s.vbl { + t.Errorf("s.row()==%d, but s.vbl=%v", s.row(), s.vbl) + } + } + s.Scan1() + } +} + +func TestSpecificLocationAddresses(t *testing.T) { + var m tests.Memorizer + var f fakePlotter + s := NewScanner(&m, &f, [2048]byte{}) + + // TEXT/LORES Page 1 + text_lores_page1 := map[int]uint16{ + 0: 0x400, + 5: 0x680, + 8: 0x428, + 16: 0x450, + 14: 0x728, + 19: 0x5D0, + 20: 0x650, + 21: 0x6D0, + 22: 0x750, + 23: 0x7D0, + } + + // HIRES Page 1 + hires := map[int]uint16{ + 0: 0x2000, + 8: 0x2080, + 64: 0x2028, + 99: 0x2E28, + 127: 0x3FA8, + 128: 0x2050, + 100: 0x3228, + 159: 0x3DD0, + 160: 0x2250, + 170: 0x2AD0, + 183: 0x3F50, + 191: 0x3FD0, + } + + s.SetMix(false) + s.SetGraphics(false) + s.SetHires(false) + for i := 0; i < 1000000; i++ { + s.Scan1() + row := s.row() + if addr, ok := text_lores_page1[row/8]; ok { + col := s.column() + if row >= 0 && col >= 0 && col < 40 { + addrplus := addr + uint16(col) + s.SetPage(1) + s.SetGraphics(false) + if addrplus != s.address() { + t.Fatalf("Row %d (%d), col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, row/8, col, addr, col, addrplus, s.address()) + } + s.SetGraphics(true) + if addrplus != s.address() { + t.Fatalf("Row %d (%d), col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, row/8, col, addr, col, addrplus, s.address()) + } + s.SetMix(true) + if addrplus != s.address() { + t.Fatalf("Row %d (%d), col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, row/8, col, addr, col, addrplus, s.address()) + } + addr += 0x400 + addrplus += 0x400 + s.SetPage(2) + if addrplus != s.address() { + t.Fatalf("Row %d (%d), col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, row/8, col, addr, col, addrplus, s.address()) + } + } + } + } + + s.SetMix(false) + s.SetGraphics(true) + s.SetHires(true) + for i := 0; i < 1000000; i++ { + s.Scan1() + row := s.row() + if addr, ok := hires[row]; ok { + col := s.column() + if col >= 0 && col < 40 { + addrplus := addr + uint16(col) + s.SetPage(1) + if addrplus != s.address() { + t.Fatalf("Row %d, col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, col, addr, col, addrplus, s.address()) + } + s.SetPage(2) + addr += 0x2000 + addrplus += 0x2000 + if addrplus != s.address() { + t.Fatalf("Row %d, col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, col, addr, col, addrplus, s.address()) + } + } + } + } + + // Mixed hires/text + s.SetPage(1) + s.SetMix(true) + s.SetGraphics(true) + s.SetHires(true) + for i := 0; i < 1000000; i++ { + s.Scan1() + row := s.row() + if row >= 160 { + if addr, ok := text_lores_page1[row/8]; ok { + col := s.column() + if row >= 0 && col >= 0 && col < 40 { + addrplus := addr + uint16(col) + if addrplus != s.address() { + t.Fatalf("Row %d (%d), col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, row/8, col, addr, col, addrplus, s.address()) + } + } + } + } else { + if addr, ok := hires[row]; ok { + col := s.column() + if col >= 0 && col < 40 { + addrplus := addr + uint16(col) + if addrplus != s.address() { + t.Fatalf("Row %d, col %d, expected $%04X+$%02X=$%04X, got $%04X", + row, col, addr, col, addrplus, s.address()) + } + } + } + } + } +}