mirror of
https://github.com/zellyn/goapple2.git
synced 2024-12-28 13:30:39 +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.textfiles.com/apple/peeks.pokes.2
|
||||||
- http://www.applefritter.com/node/24236
|
- http://www.applefritter.com/node/24236
|
||||||
- http://www.easy68k.com/paulrsm/6502/index.html - Information about firmware
|
- 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
|
* Language card
|
||||||
Install in slot 0.
|
Install in slot 0.
|
||||||
@ -326,3 +327,144 @@ and selects the second 4K bank for $D000-$DFFF.
|
|||||||
| Z | DA | 9A | | |
|
| Z | DA | 9A | | |
|
||||||
| ESC | 9B | 9B | | |
|
| 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
|
||||||
|
}
|
164
texty/texty.go
164
texty/texty.go
@ -3,24 +3,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nsf/termbox-go"
|
"github.com/nsf/termbox-go"
|
||||||
"github.com/zellyn/go6502/asm"
|
"github.com/zellyn/goapple2"
|
||||||
"github.com/zellyn/go6502/cpu"
|
"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
|
// Mapping of screen bytes to character values
|
||||||
var AppleChars = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?"
|
var AppleChars = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?"
|
||||||
|
|
||||||
@ -55,136 +45,76 @@ func termboxToAppleKeyboard(ev termbox.Event) (key byte, err error) {
|
|||||||
return 0, fmt.Errorf("hi")
|
return 0, fmt.Errorf("hi")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a2 *Apple2) Read(address uint16) byte {
|
// func Write(address uint16, value byte) {
|
||||||
// Keyboard read
|
// a2.mem[address] = value
|
||||||
if address == 0xC000 {
|
// if address >= 0x0400 && address < 0x0800 {
|
||||||
return a2.key
|
// offset := int(address - 0x0400)
|
||||||
}
|
// count := offset & 0x7f
|
||||||
if address == 0xC010 {
|
// if count <= 119 {
|
||||||
a2.key &= 0x7F
|
// x := count % 40
|
||||||
return 0 // BUG(zellyn): return proper value (keydown on IIe, not sure on II+)
|
// segment := offset / 128
|
||||||
}
|
// which40 := count / 40
|
||||||
return a2.mem[address]
|
// 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) {
|
func ProcessEvents(events chan termbox.Event, a2 *goapple2.Apple2) bool {
|
||||||
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() {
|
|
||||||
select {
|
select {
|
||||||
case ev := <-a2.events:
|
case ev := <-events:
|
||||||
if ev.Type == termbox.EventKey && ev.Ch == '~' {
|
if ev.Type == termbox.EventKey && ev.Ch == '~' {
|
||||||
a2.done = true
|
return true
|
||||||
}
|
}
|
||||||
if ev.Type == termbox.EventKey {
|
if ev.Type == termbox.EventKey {
|
||||||
if key, err := termboxToAppleKeyboard(ev); err == nil {
|
if key, err := termboxToAppleKeyboard(ev); err == nil {
|
||||||
a2.keys <- key | 0x80
|
a2.Keypress(key | 0x80)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
if a2.key&0x80 == 0 {
|
return false
|
||||||
select {
|
|
||||||
case key := <-a2.keys:
|
|
||||||
a2.key = key
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cycle counter for the tests. Satisfies the cpu.Ticker interface.
|
type TextPlotter int
|
||||||
type CycleCount uint64
|
|
||||||
|
|
||||||
func (c *CycleCount) Tick() {
|
func (p TextPlotter) Plot(data videoscan.PlotData) {
|
||||||
*c += 1
|
y := int(data.Row / 8)
|
||||||
}
|
x := int(data.Column)
|
||||||
|
value := data.RawData
|
||||||
// printStatus prints out the current CPU instruction and register status.
|
ch, fg, bg := translateToTermbox(value)
|
||||||
func printStatus(c cpu.Cpu, m *[65536]byte) {
|
termbox.SetCell(x+1, y+1, ch, fg, bg)
|
||||||
bytes, text, _ := asm.Disasm(c.PC(), m[c.PC()], m[c.PC()+1], m[c.PC()+2])
|
if x == 39 && data.Row == 191 {
|
||||||
fmt.Printf("$%04X: %s %s A=$%02X X=$%02X Y=$%02X SP=$%02X P=$%08b\n",
|
termbox.Flush()
|
||||||
c.PC(), bytes, text, c.A(), c.X(), c.Y(), c.SP(), c.P())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the emulator
|
// Run the emulator
|
||||||
func RunEmulator() {
|
func RunEmulator() {
|
||||||
bytes, err := ioutil.ReadFile("../data/roms/apple2+.rom")
|
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
||||||
if err != nil {
|
plotter := TextPlotter(0)
|
||||||
panic("Cannot read ROM file")
|
a2 := goapple2.NewApple2(plotter, rom)
|
||||||
|
if err := termbox.Init(); err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
var a2 Apple2
|
events := make(chan termbox.Event)
|
||||||
ROM_OFFSET := 0xD000
|
go func() {
|
||||||
copy(a2.mem[ROM_OFFSET:ROM_OFFSET+len(bytes)], bytes)
|
for {
|
||||||
a2.Init()
|
events <- termbox.PollEvent()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
if a2.debug {
|
}()
|
||||||
printStatus(c, &a2.mem)
|
for !ProcessEvents(events, a2) {
|
||||||
}
|
err := a2.Step()
|
||||||
err := c.Step()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Nanosecond) // So the keyboard-reading goroutines can run
|
time.Sleep(1 * time.Nanosecond) // So the keyboard-reading goroutines can run
|
||||||
}
|
}
|
||||||
if !a2.debug {
|
termbox.Close()
|
||||||
a2.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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