mirror of
https://github.com/zellyn/goapple2.git
synced 2024-12-27 07:31:23 +00:00
Reorganizing things so texty is more like the real thing
This commit is contained in:
parent
b9603add19
commit
95c9b2c3ee
142
docs/apple2.org
142
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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
77
goapple2.go
Normal file
77
goapple2.go
Normal file
@ -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
|
||||
}
|
168
texty/texty.go
168
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() {
|
||||
|
13
util/util.go
Normal file
13
util/util.go
Normal file
@ -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
|
||||
}
|
233
videoscan/videoscan.go
Normal file
233
videoscan/videoscan.go
Normal file
@ -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
|
||||
}
|
224
videoscan/videoscan_test.go
Normal file
224
videoscan/videoscan_test.go
Normal file
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user