Implemented basic graphics, firmware card
This commit is contained in:
parent
1d5e822fe8
commit
95f22d22b8
|
@ -0,0 +1,19 @@
|
||||||
|
package cards
|
||||||
|
|
||||||
|
type Card interface {
|
||||||
|
String() string // The name of the card, for debug/display purposes
|
||||||
|
Read16(address byte) byte // Read from the $C0(8+slot)X addresses
|
||||||
|
Write16(address byte, value byte) // Write to the $C0(8+slot)X addresses
|
||||||
|
Slot() byte // Get the card's slot 0-7
|
||||||
|
ROMDisabled() // Tell the card that its handling of $C(8-F)xx was disabled
|
||||||
|
Read256(address byte) byte // Read from the $C(slot)XX addresses
|
||||||
|
Write256(address byte, value byte) // Write to the $C(slot)XX addresses
|
||||||
|
Read(address uint16) byte // Read from any address ($C800-$FFFF)
|
||||||
|
Write(address uint16, value byte) // Write to any address ($C800-$FFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CardManager interface {
|
||||||
|
HandleROM(onOff bool, slot byte)
|
||||||
|
Handle12k(onOff bool, slot byte)
|
||||||
|
EmptyRead() byte
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package cards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FirmwareCard struct {
|
||||||
|
name string
|
||||||
|
rom [12288]byte
|
||||||
|
cm CardManager
|
||||||
|
slot byte
|
||||||
|
slotbit byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFirmwareCard(rom []byte, name string, slot byte, cm CardManager) (*FirmwareCard, error) {
|
||||||
|
if len(rom) != 12288 {
|
||||||
|
return nil, fmt.Errorf("Wrong size ROM: expected 12288, got %d", len(rom))
|
||||||
|
}
|
||||||
|
fc := &FirmwareCard{name: name, cm: cm, slot: slot, slotbit: 1 << slot}
|
||||||
|
copy(fc.rom[:], rom)
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) String() string {
|
||||||
|
return fmt.Sprintf("%s (slot %d)", fc.name, fc.slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Slot() byte {
|
||||||
|
return fc.slot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) ROMDisabled() {
|
||||||
|
// Firmware card doesn't have a $C(8-F)xx ROM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) handleAccess(address byte) {
|
||||||
|
if address%2 == 1 {
|
||||||
|
// Card off
|
||||||
|
fc.cm.HandleROM(false, fc.slotbit)
|
||||||
|
} else {
|
||||||
|
// Card on
|
||||||
|
fc.cm.HandleROM(true, fc.slotbit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Read16(address byte) byte {
|
||||||
|
fc.handleAccess(address)
|
||||||
|
return fc.cm.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Write16(address byte, value byte) {
|
||||||
|
fc.handleAccess(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Read(address uint16) byte {
|
||||||
|
if address < 0xD000 {
|
||||||
|
panic(fmt.Sprintf("%s got read to $%04X (<$D000)", fc.String(), address))
|
||||||
|
}
|
||||||
|
return fc.rom[address-0xD000]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Write(address uint16, value byte) {
|
||||||
|
// Firmware is ROM: do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Read256(address byte) byte {
|
||||||
|
return fc.cm.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *FirmwareCard) Write256(address byte, value byte) {
|
||||||
|
// Firmware is ROM: do nothing
|
||||||
|
}
|
|
@ -468,3 +468,20 @@ sdlEventLoop():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* Firmware card
|
||||||
|
$D000-$FFFF
|
||||||
|
B2-B set (motherboard ROM)
|
||||||
|
- RESET+switch down
|
||||||
|
- odd addresses between $C081 and $C08F (slot 0)
|
||||||
|
B2-B reset (firmware card ROM)
|
||||||
|
- RESET+switch up
|
||||||
|
- even addresses between $C080 and $C08E
|
||||||
|
|
||||||
|
F8 jumper jumpered: use F8 ROM on card when card is enabled.
|
||||||
|
F8 jumper not jumpered: never use F8 ROM on card: use motherboard.
|
||||||
|
(Apple sold jumpered+autostart ROM, non-jumpered+no-rom. [UtA2: 6-11])
|
||||||
|
|
||||||
|
* Questions
|
||||||
|
** Color
|
||||||
|
Can a previously rendered dot change colors? Gray?
|
||||||
|
|
237
goapple2.go
237
goapple2.go
|
@ -1,57 +1,64 @@
|
||||||
package goapple2
|
package goapple2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/zellyn/go6502/cpu"
|
"github.com/zellyn/go6502/cpu"
|
||||||
|
"github.com/zellyn/goapple2/cards"
|
||||||
"github.com/zellyn/goapple2/videoscan"
|
"github.com/zellyn/goapple2/videoscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Memory for the tests. Satisfies the cpu.Memory interface.
|
// Memory for the tests. Satisfies the cpu.Memory interface.
|
||||||
type Apple2 struct {
|
type Apple2 struct {
|
||||||
mem [65536]byte
|
mem [65536]byte
|
||||||
cpu cpu.Cpu
|
cpu cpu.Cpu
|
||||||
key byte // BUG(zellyn): make reads/writes atomic
|
key byte // BUG(zellyn): make reads/writes atomic
|
||||||
keys chan byte
|
keys chan byte
|
||||||
plotter videoscan.Plotter
|
plotter videoscan.Plotter
|
||||||
scanner *videoscan.Scanner
|
scanner *videoscan.Scanner
|
||||||
done bool
|
Done bool
|
||||||
tw TickWaiter
|
lastRead byte
|
||||||
}
|
cards [8]cards.Card "Peripheral cards"
|
||||||
|
cardMask byte
|
||||||
// Cycle counter. Satisfies the cpu.Ticker interface.
|
cardRomMask byte
|
||||||
type TickWaiter struct {
|
cardRomConflict bool "True if more than one card is handling the 2k ROM area"
|
||||||
Wait chan byte
|
cardRomHandler byte
|
||||||
}
|
card12kMask byte
|
||||||
|
card12kConflict bool "True if more than one card is handling the 12k ROM area"
|
||||||
func (t TickWaiter) Tick() {
|
card12kHandler byte
|
||||||
<-t.Wait
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTickWaiter() TickWaiter {
|
|
||||||
return TickWaiter{Wait: make(chan byte)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApple2(p videoscan.Plotter, rom []byte, charRom [2048]byte) *Apple2 {
|
func NewApple2(p videoscan.Plotter, rom []byte, charRom [2048]byte) *Apple2 {
|
||||||
tw := NewTickWaiter()
|
|
||||||
a2 := Apple2{
|
a2 := Apple2{
|
||||||
|
// BUG(zellyn): this is not how the apple2 keyboard actually works
|
||||||
keys: make(chan byte, 16),
|
keys: make(chan byte, 16),
|
||||||
tw: tw,
|
|
||||||
}
|
}
|
||||||
copy(a2.mem[len(a2.mem)-len(rom):len(a2.mem)], rom)
|
copy(a2.mem[len(a2.mem)-len(rom):len(a2.mem)], rom)
|
||||||
a2.scanner = videoscan.NewScanner(&a2, p, charRom)
|
a2.scanner = videoscan.NewScanner(&a2, p, charRom)
|
||||||
a2.cpu = cpu.NewCPU(&a2, tw, cpu.VERSION_6502)
|
a2.cpu = cpu.NewCPU(&a2, &a2, cpu.VERSION_6502)
|
||||||
a2.cpu.Reset()
|
a2.cpu.Reset()
|
||||||
go func() {
|
|
||||||
tw.Tick()
|
|
||||||
for !a2.done {
|
|
||||||
a2.cpu.Step()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return &a2
|
return &a2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a2 *Apple2) Read(address uint16) byte {
|
func (a2 *Apple2) AddCard(card cards.Card) error {
|
||||||
// Keyboard read
|
slot := card.Slot()
|
||||||
if address == 0xC000 {
|
slotbit := byte(1 << slot)
|
||||||
|
if slotbit&a2.cardMask > 0 {
|
||||||
|
return fmt.Errorf("Slot %d already has a card: %s", slot, a2.cards[slot])
|
||||||
|
}
|
||||||
|
a2.cardMask |= slotbit
|
||||||
|
a2.cards[slot] = card
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) handleCardRom(address uint16, value byte, write bool) byte {
|
||||||
|
return a2.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) handleC00X(address uint16, value byte, write bool) byte {
|
||||||
|
switch address & 0xC0F0 {
|
||||||
|
// $C00X: Read keyboard
|
||||||
|
case 0xC000:
|
||||||
if a2.key&0x80 == 0 {
|
if a2.key&0x80 == 0 {
|
||||||
select {
|
select {
|
||||||
case key := <-a2.keys:
|
case key := <-a2.keys:
|
||||||
|
@ -60,23 +67,128 @@ func (a2 *Apple2) Read(address uint16) byte {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return a2.key
|
return a2.key
|
||||||
}
|
// $C01X: Reset keyboard
|
||||||
if address == 0xC010 {
|
case 0xC010:
|
||||||
a2.key &= 0x7F
|
a2.key &= 0x7F
|
||||||
return 0 // BUG(zellyn): return proper value (keydown on IIe, not sure on II+)
|
return a2.EmptyRead()
|
||||||
}
|
}
|
||||||
return a2.mem[address]
|
switch address {
|
||||||
|
case 0xC050: // GRAPHICS
|
||||||
|
a2.scanner.SetGraphics(true)
|
||||||
|
case 0xC051: // TEXT
|
||||||
|
a2.scanner.SetGraphics(false)
|
||||||
|
case 0xC052: // NOMIX
|
||||||
|
a2.scanner.SetMix(false)
|
||||||
|
case 0xC053: // MIX
|
||||||
|
a2.scanner.SetMix(true)
|
||||||
|
case 0xC054: // PAGE 1
|
||||||
|
a2.scanner.SetPage(1)
|
||||||
|
case 0xC055: // PAGE 2
|
||||||
|
a2.scanner.SetPage(2)
|
||||||
|
case 0xC056: // LORES
|
||||||
|
a2.scanner.SetHires(false)
|
||||||
|
case 0xC057: // HIRES
|
||||||
|
a2.scanner.SetHires(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if address < 0xC080 {
|
||||||
|
return a2.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
if address < 0xC100 {
|
||||||
|
slot := byte((address - 0xC080) >> 4)
|
||||||
|
if a2.cards[slot] != nil {
|
||||||
|
if write {
|
||||||
|
a2.cards[slot].Write16(byte(address&0xF), value)
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return a2.cards[slot].Read16(byte(address & 0xF))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a2.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
if address < 0xC800 {
|
||||||
|
slot := byte((address - 0xC000) >> 8)
|
||||||
|
if a2.cards[slot] != nil {
|
||||||
|
if write {
|
||||||
|
a2.cards[slot].Write256(byte(address&0xFF), value)
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return a2.cards[slot].Read256(byte(address & 0xFF))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a2.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0xCFFF disables 2k on all cards
|
||||||
|
if address == 0xCFFF {
|
||||||
|
for i := 0; a2.cardMask > 0; a2.cardMask >>= 1 {
|
||||||
|
if a2.cardMask&1 > 0 {
|
||||||
|
a2.cards[i].ROMDisabled()
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return a2.EmptyRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only addresses left are 0xC800-0xCFFE
|
||||||
|
if a2.cardRomMask == 0 {
|
||||||
|
return a2.EmptyRead()
|
||||||
|
}
|
||||||
|
if a2.cardRomConflict {
|
||||||
|
panic(fmt.Sprintf("More than one card trying to provide 2K ROM: Mask=$%02X", a2.cardRomMask))
|
||||||
|
}
|
||||||
|
|
||||||
|
if write {
|
||||||
|
a2.cards[a2.cardRomHandler].Write(address, value)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return a2.cards[a2.cardRomHandler].Read(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyRead returns the value last read from RAM, lingering on the bus.
|
||||||
|
func (a2 *Apple2) EmptyRead() byte {
|
||||||
|
return a2.lastRead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) Read(address uint16) byte {
|
||||||
|
if address&0xF000 == 0xC000 {
|
||||||
|
return a2.handleC00X(address, 0, false)
|
||||||
|
}
|
||||||
|
if address >= 0xD000 && a2.cardRomMask > 0 {
|
||||||
|
if a2.card12kConflict {
|
||||||
|
panic(fmt.Sprintf("More than one card trying to provide 12K ROM: Mask=$%02X", a2.card12kMask))
|
||||||
|
}
|
||||||
|
a2.lastRead = a2.cards[a2.card12kHandler].Read(address)
|
||||||
|
return a2.lastRead
|
||||||
|
}
|
||||||
|
|
||||||
|
a2.lastRead = a2.mem[address]
|
||||||
|
return a2.lastRead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) RamRead(address uint16) byte {
|
||||||
|
a2.lastRead = a2.mem[address]
|
||||||
|
return a2.lastRead
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a2 *Apple2) Write(address uint16, value byte) {
|
func (a2 *Apple2) Write(address uint16, value byte) {
|
||||||
if address >= 0xD000 {
|
if address >= 0xD000 {
|
||||||
|
if a2.cardRomMask > 0 {
|
||||||
|
if a2.card12kConflict {
|
||||||
|
panic(fmt.Sprintf("More than one card trying to provide 12K ROM: Mask=$%02X", a2.card12kMask))
|
||||||
|
}
|
||||||
|
a2.cards[a2.card12kHandler].Write(address, value)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if address == 0xC010 {
|
if address&0xF000 == 0xC000 {
|
||||||
// Clear keyboard strobe
|
a2.handleC00X(address, value, true)
|
||||||
a2.key &= 0x7F
|
return
|
||||||
}
|
}
|
||||||
a2.mem[address] = value
|
a2.mem[address] = value
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a2 *Apple2) Keypress(key byte) {
|
func (a2 *Apple2) Keypress(key byte) {
|
||||||
|
@ -84,11 +196,50 @@ func (a2 *Apple2) Keypress(key byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a2 *Apple2) Step() error {
|
func (a2 *Apple2) Step() error {
|
||||||
a2.tw.Wait <- 0
|
return a2.cpu.Step()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) Tick() {
|
||||||
a2.scanner.Scan1()
|
a2.scanner.Scan1()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a2 *Apple2) Quit() {
|
func (a2 *Apple2) Quit() {
|
||||||
a2.done = true
|
a2.Done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) HandleROM(onOff bool, slot byte) {
|
||||||
|
if onOff {
|
||||||
|
a2.cardRomMask |= (1 << slot)
|
||||||
|
a2.cardRomHandler = slot
|
||||||
|
} else {
|
||||||
|
a2.cardRomMask &^= (1 << slot)
|
||||||
|
}
|
||||||
|
a2.cardRomConflict = a2.cardRomMask&(a2.cardRomMask-1) > 0
|
||||||
|
if !onOff && !a2.cardRomConflict && a2.cardRomMask > 0 {
|
||||||
|
// Removed a card: figure out new handler
|
||||||
|
for i := byte(0); i < 7; i++ {
|
||||||
|
if 1<<i == a2.cardRomMask {
|
||||||
|
a2.cardRomHandler = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a2 *Apple2) Handle12k(onOff bool, slot byte) {
|
||||||
|
if onOff {
|
||||||
|
a2.card12kMask |= slot
|
||||||
|
} else {
|
||||||
|
a2.card12kMask &^= slot
|
||||||
|
}
|
||||||
|
a2.card12kConflict = a2.card12kMask&(a2.card12kMask-1) > 0
|
||||||
|
if !onOff && !a2.card12kConflict && a2.card12kMask > 0 {
|
||||||
|
// Removed a card: figure out new handler
|
||||||
|
for i := byte(0); i < 7; i++ {
|
||||||
|
if 1<<i == a2.card12kMask {
|
||||||
|
a2.card12kHandler = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
SDL_VIDEODRIVER=x11 go run sdl.go
|
SDL_VIDEODRIVER=x11 go run sdl.go $@
|
||||||
|
|
106
sdl/sdl.go
106
sdl/sdl.go
|
@ -2,16 +2,26 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"log"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/0xe2-0x9a-0x9b/Go-SDL/sdl"
|
"github.com/0xe2-0x9a-0x9b/Go-SDL/sdl"
|
||||||
"github.com/zellyn/goapple2"
|
"github.com/zellyn/goapple2"
|
||||||
|
"github.com/zellyn/goapple2/cards"
|
||||||
"github.com/zellyn/goapple2/util"
|
"github.com/zellyn/goapple2/util"
|
||||||
"github.com/zellyn/goapple2/videoscan"
|
"github.com/zellyn/goapple2/videoscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||||
|
steplimit = flag.Uint64("steplimit", 0, "limit on number of steps to take")
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BORDER_H = 20
|
BORDER_H = 20
|
||||||
BORDER_W = 20
|
BORDER_W = 20
|
||||||
|
@ -133,16 +143,16 @@ var KeyToApple = map[Key]byte{
|
||||||
Key{sdl.K_8, M_NONE}: '8',
|
Key{sdl.K_8, M_NONE}: '8',
|
||||||
Key{sdl.K_9, M_NONE}: '9',
|
Key{sdl.K_9, M_NONE}: '9',
|
||||||
|
|
||||||
Key{sdl.K_0, M_SHIFT}: '!',
|
Key{sdl.K_1, M_SHIFT}: '!',
|
||||||
Key{sdl.K_1, M_SHIFT}: '@',
|
Key{sdl.K_2, M_SHIFT}: '@',
|
||||||
Key{sdl.K_2, M_SHIFT}: '#',
|
Key{sdl.K_3, M_SHIFT}: '#',
|
||||||
Key{sdl.K_3, M_SHIFT}: '$',
|
Key{sdl.K_4, M_SHIFT}: '$',
|
||||||
Key{sdl.K_4, M_SHIFT}: '%',
|
Key{sdl.K_5, M_SHIFT}: '%',
|
||||||
Key{sdl.K_5, M_SHIFT}: '^',
|
Key{sdl.K_6, M_SHIFT}: '^',
|
||||||
Key{sdl.K_6, M_SHIFT}: '&',
|
Key{sdl.K_7, M_SHIFT}: '&',
|
||||||
Key{sdl.K_7, M_SHIFT}: '*',
|
Key{sdl.K_8, M_SHIFT}: '*',
|
||||||
Key{sdl.K_8, M_SHIFT}: '(',
|
Key{sdl.K_9, M_SHIFT}: '(',
|
||||||
Key{sdl.K_9, M_SHIFT}: ')',
|
Key{sdl.K_0, M_SHIFT}: ')',
|
||||||
|
|
||||||
Key{sdl.K_MINUS, M_NONE}: '-',
|
Key{sdl.K_MINUS, M_NONE}: '-',
|
||||||
Key{sdl.K_MINUS, M_SHIFT}: '_',
|
Key{sdl.K_MINUS, M_SHIFT}: '_',
|
||||||
|
@ -223,7 +233,8 @@ func ProcessEvents(events <-chan interface{}, a2 *goapple2.Apple2) (done bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SdlPlotter struct {
|
type SdlPlotter struct {
|
||||||
screen *sdl.Surface
|
screen *sdl.Surface
|
||||||
|
oncePerFrame func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SdlPlotter) Plot(pd videoscan.PlotData) {
|
func (s SdlPlotter) Plot(pd videoscan.PlotData) {
|
||||||
|
@ -241,32 +252,91 @@ func (s SdlPlotter) Plot(pd videoscan.PlotData) {
|
||||||
plot(x+i, y*2+1, color2, s.screen)
|
plot(x+i, y*2+1, color2, s.screen)
|
||||||
data >>= 1
|
data >>= 1
|
||||||
}
|
}
|
||||||
if pd.Column == 39 && y == 191 {
|
}
|
||||||
s.screen.Flip()
|
|
||||||
|
func (s SdlPlotter) OncePerFrame() {
|
||||||
|
s.screen.Flip()
|
||||||
|
s.oncePerFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeProgram(a2 *goapple2.Apple2) {
|
||||||
|
lines := []string{
|
||||||
|
"10 GR",
|
||||||
|
"20 POKE -16302,0",
|
||||||
|
"30 FOR Y=0 TO 47",
|
||||||
|
"40 FOR X=0 TO 39",
|
||||||
|
"50 COLOR=INT(RND(1)*16)",
|
||||||
|
"60 PLOT X,Y",
|
||||||
|
"70 NEXT",
|
||||||
|
"80 NEXT",
|
||||||
|
"RUN",
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
for _, ch := range line {
|
||||||
|
a2.Keypress(byte(ch))
|
||||||
|
}
|
||||||
|
a2.Keypress(13)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the emulator
|
// Run the emulator
|
||||||
func RunEmulator() {
|
func RunEmulator() {
|
||||||
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
||||||
|
// charRom = util.ReadFullCharacterRomOrDie("../data/roms/apple2char.rom")
|
||||||
charRom := util.ReadSmallCharacterRomOrDie("../data/roms/apple2-chars.rom")
|
charRom := util.ReadSmallCharacterRomOrDie("../data/roms/apple2-chars.rom")
|
||||||
screen, err := Init()
|
screen, err := Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
plotter := SdlPlotter{screen}
|
var a2 *goapple2.Apple2
|
||||||
a2 := goapple2.NewApple2(plotter, rom, charRom)
|
oncePerFrame := func() {
|
||||||
for !ProcessEvents(sdl.Events, a2) {
|
a2.Done = a2.Done || ProcessEvents(sdl.Events, a2)
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
plotter := SdlPlotter{screen, oncePerFrame}
|
||||||
|
a2 = goapple2.NewApple2(plotter, rom, charRom)
|
||||||
|
|
||||||
|
intBasicRom := util.ReadRomOrDie("../data/roms/apple2.rom")
|
||||||
|
firmwareCard, err := cards.NewFirmwareCard(intBasicRom, "Intbasic Firmware Card", 0, a2)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := a2.AddCard(firmwareCard); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := *steplimit
|
||||||
|
|
||||||
|
if *cpuprofile != "" {
|
||||||
|
f, err := os.Create(*cpuprofile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
pprof.StartCPUProfile(f)
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
go typeProgram(a2)
|
||||||
|
|
||||||
|
for !a2.Done {
|
||||||
err := a2.Step()
|
err := a2.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
|
// runtime.Gosched() // So the keyboard-reading goroutines can run
|
||||||
|
if steps > 0 {
|
||||||
|
steps--
|
||||||
|
if steps == 0 {
|
||||||
|
a2.Quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
a2.Quit()
|
||||||
sdl.Quit()
|
sdl.Quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
RunEmulator()
|
RunEmulator()
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,16 +70,16 @@ func (p TextPlotter) Plot(data videoscan.PlotData) {
|
||||||
value := data.RawData
|
value := data.RawData
|
||||||
ch, fg, bg := translateToTermbox(value)
|
ch, fg, bg := translateToTermbox(value)
|
||||||
termbox.SetCell(x+1, y+1, ch, fg, bg)
|
termbox.SetCell(x+1, y+1, ch, fg, bg)
|
||||||
if x == 39 && data.Row == 191 {
|
}
|
||||||
termbox.Flush()
|
func (p TextPlotter) OncePerFrame() {
|
||||||
}
|
termbox.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the emulator
|
// Run the emulator
|
||||||
func RunEmulator() {
|
func RunEmulator() {
|
||||||
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
||||||
|
charRom := util.ReadSmallCharacterRomOrDie("../data/roms/apple2-chars.rom")
|
||||||
plotter := TextPlotter(0)
|
plotter := TextPlotter(0)
|
||||||
var charRom [2048]byte
|
|
||||||
a2 := goapple2.NewApple2(plotter, rom, charRom)
|
a2 := goapple2.NewApple2(plotter, rom, charRom)
|
||||||
if err := termbox.Init(); err != nil {
|
if err := termbox.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
10
util/util.go
10
util/util.go
|
@ -27,3 +27,13 @@ func ReadSmallCharacterRomOrDie(filename string) [2048]byte {
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadFullCharacterRomOrDie(filename string) [2048]byte {
|
||||||
|
bytes := ReadRomOrDie(filename)
|
||||||
|
if len(bytes) != 2048 {
|
||||||
|
panic(fmt.Sprintf("Got %d bytes (not 2048) from file '%s'", len(bytes), filename))
|
||||||
|
}
|
||||||
|
var value [2048]byte
|
||||||
|
copy(value[:], bytes)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
|
@ -2,11 +2,9 @@ package videoscan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/zellyn/go6502/cpu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const FLASH_CYCLES = 15 // 15/60 = four per second
|
const FLASH_INCREMENT = 8 // 128/8=16 / 60 // 15/60 = 3.75 per second
|
||||||
|
|
||||||
// PlotData is the struct that holds plotting
|
// PlotData is the struct that holds plotting
|
||||||
// information. Essentially, it holds the (binary) waveform of the
|
// information. Essentially, it holds the (binary) waveform of the
|
||||||
|
@ -16,15 +14,22 @@ type PlotData struct {
|
||||||
Column byte "The column (0-39)"
|
Column byte "The column (0-39)"
|
||||||
ColorBurst bool "Whether the color signal is active"
|
ColorBurst bool "Whether the color signal is active"
|
||||||
Data uint16 "14 half color cycles of information"
|
Data uint16 "14 half color cycles of information"
|
||||||
|
LastData uint16 "The previous 14 half color cycles of information"
|
||||||
RawData byte "The underlying raw byte"
|
RawData byte "The underlying raw byte"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plotter interface {
|
type Plotter interface {
|
||||||
Plot(PlotData)
|
Plot(PlotData)
|
||||||
|
OncePerFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAM memory interface
|
||||||
|
type RamReader interface {
|
||||||
|
RamRead(uint16) byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type Scanner struct {
|
type Scanner struct {
|
||||||
m cpu.Memory
|
m RamReader
|
||||||
h uint16
|
h uint16
|
||||||
v uint16
|
v uint16
|
||||||
plotter Plotter
|
plotter Plotter
|
||||||
|
@ -35,15 +40,17 @@ type Scanner struct {
|
||||||
hires bool // LORES/HIRES
|
hires bool // LORES/HIRES
|
||||||
page2 bool // PAGE1/PAGE2
|
page2 bool // PAGE1/PAGE2
|
||||||
|
|
||||||
hbl bool // Horizontal blanking
|
hbl bool // Horizontal blanking
|
||||||
vbl bool // Vertical blanking
|
vbl bool // Vertical blanking
|
||||||
lastFour bool // Are we in the last 4 lines of the screen?
|
lastFour bool // Are we in the last 4 lines of the screen?
|
||||||
lastBit uint16 // Last bit of previous 14-cycle color data
|
lastData uint16 // The previous 14 half color cycles of information
|
||||||
flasher byte // if high bit is set, invert flashing text
|
lastBit uint16 // Last bit of previous 14-cycle color data
|
||||||
flashCount int // count up, and toggle flasher
|
lastChange bool // Was there a change plotted last byte?
|
||||||
|
flasher byte // if high bit is set, invert flashing text
|
||||||
|
graphicsBit uint16 // Bit 14 high if we're in graphics mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScanner(m cpu.Memory, p Plotter, rom [2048]byte) *Scanner {
|
func NewScanner(m RamReader, p Plotter, rom [2048]byte) *Scanner {
|
||||||
s := &Scanner{
|
s := &Scanner{
|
||||||
m: m,
|
m: m,
|
||||||
plotter: p,
|
plotter: p,
|
||||||
|
@ -66,10 +73,7 @@ func (s *Scanner) inc() {
|
||||||
switch s.v {
|
switch s.v {
|
||||||
case 0x1ff:
|
case 0x1ff:
|
||||||
s.v = 250
|
s.v = 250
|
||||||
if s.flashCount++; s.flashCount >= FLASH_CYCLES {
|
s.flasher += FLASH_INCREMENT
|
||||||
s.flashCount = 0
|
|
||||||
s.flasher ^= 0x80
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
s.v++
|
s.v++
|
||||||
}
|
}
|
||||||
|
@ -87,12 +91,15 @@ func (s *Scanner) inc() {
|
||||||
s.hbl = ((s.h >> 3) & 7) <= 2
|
s.hbl = ((s.h >> 3) & 7) <= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The last-plotted color cycle information.
|
||||||
|
// Bits 0-13 are the color cycle waveform. Bit 14 is true if colorburst was on.
|
||||||
|
var last [192][40]uint16
|
||||||
|
|
||||||
func (s *Scanner) Scan1() {
|
func (s *Scanner) Scan1() {
|
||||||
m := s.m.Read(s.address())
|
m := s.m.RamRead(s.address())
|
||||||
row, column := s.row(), s.column()
|
row, column := s.row(), s.column()
|
||||||
_, _, _ = m, row, column
|
_, _, _ = m, row, column
|
||||||
var data uint16
|
var data uint16
|
||||||
color := s.graphics
|
|
||||||
switch {
|
switch {
|
||||||
case !s.graphics || (s.mix && s.lastFour):
|
case !s.graphics || (s.mix && s.lastFour):
|
||||||
data = s.textData(m, row, column)
|
data = s.textData(m, row, column)
|
||||||
|
@ -103,14 +110,24 @@ func (s *Scanner) Scan1() {
|
||||||
}
|
}
|
||||||
s.lastBit = (data >> 13) & 1
|
s.lastBit = (data >> 13) & 1
|
||||||
if !s.hbl && !s.vbl {
|
if !s.hbl && !s.vbl {
|
||||||
s.plotter.Plot(PlotData{
|
change := last[row][column] != (data | s.graphicsBit)
|
||||||
Row: byte(row),
|
if change || s.lastChange {
|
||||||
Column: byte(column),
|
s.plotter.Plot(PlotData{
|
||||||
ColorBurst: color,
|
Row: byte(row),
|
||||||
Data: data,
|
Column: byte(column),
|
||||||
RawData: m,
|
ColorBurst: s.graphics,
|
||||||
})
|
Data: data,
|
||||||
|
LastData: s.lastData,
|
||||||
|
RawData: m,
|
||||||
|
})
|
||||||
|
last[row][column] = data | s.graphicsBit
|
||||||
|
}
|
||||||
|
s.lastChange = change
|
||||||
|
if column == 39 && row == 191 {
|
||||||
|
s.plotter.OncePerFrame()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
s.lastData = data
|
||||||
s.inc()
|
s.inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,20 +139,28 @@ func (s *Scanner) row() int {
|
||||||
return int(s.v) - 0x100
|
return int(s.v) - 0x100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var SUMS = [32]uint16{
|
||||||
|
104, 112, 120, 0, 8, 16, 24, 32,
|
||||||
|
16, 24, 32, 40, 48, 56, 64, 72,
|
||||||
|
56, 64, 72, 80, 88, 96, 104, 112,
|
||||||
|
96, 104, 112, 120, 0, 8, 16, 24,
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Scanner) address() uint16 {
|
func (s *Scanner) address() uint16 {
|
||||||
// Low three bits are just H0-H2
|
// Low three bits are just H0-H2
|
||||||
addr := s.h & 7
|
addr := s.h & 7
|
||||||
|
|
||||||
// Next four bits are H5,H4,H3 + offset = SUM-A6,SUM-A5,SUM-A4,SUM-A3
|
// Next four bits are H5,H4,H3 + offset = SUM-A6,SUM-A5,SUM-A4,SUM-A3
|
||||||
bias := uint16(0xD) // 1 1 0 1
|
// bias := uint16(0xD) // 1 1 0 1
|
||||||
hsum := (s.h >> 3) & 7 // 0 H5 H4 H3
|
// hsum := (s.h >> 3) & 7 // 0 H5 H4 H3
|
||||||
vsum := (s.v >> 6) & 3 // V4 V3 V4 V3
|
// vsum := (s.v >> 6) & 3 // V4 V3 V4 V3
|
||||||
vsum = vsum | (vsum << 2)
|
// vsum = vsum | (vsum << 2)
|
||||||
suma36 := (bias + hsum + vsum) & 0xF
|
// suma36 := (bias + hsum + vsum) & 0xF
|
||||||
addr |= (suma36 << 3)
|
|
||||||
|
addr |= SUMS[(s.h>>3)&7+(s.v>>3)&24]
|
||||||
|
|
||||||
// Next three are V0,V1,V2
|
// Next three are V0,V1,V2
|
||||||
addr |= ((s.v >> 3 & 7) << 7)
|
addr |= (s.v << 4) & 0x380 // ((s.v >> 3 & 7) << 7)
|
||||||
|
|
||||||
page := uint16(1)
|
page := uint16(1)
|
||||||
if s.page2 {
|
if s.page2 {
|
||||||
|
@ -167,6 +192,11 @@ func (s *Scanner) address() uint16 {
|
||||||
|
|
||||||
func (s *Scanner) SetGraphics(graphics bool) {
|
func (s *Scanner) SetGraphics(graphics bool) {
|
||||||
s.graphics = graphics
|
s.graphics = graphics
|
||||||
|
if graphics {
|
||||||
|
s.graphicsBit = 1 << 14
|
||||||
|
} else {
|
||||||
|
s.graphicsBit = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) SetMix(mix bool) {
|
func (s *Scanner) SetMix(mix bool) {
|
||||||
|
@ -191,7 +221,7 @@ func (s *Scanner) SetPage(page int) {
|
||||||
func (s *Scanner) textData(m byte, row int, column int) uint16 {
|
func (s *Scanner) textData(m byte, row int, column int) uint16 {
|
||||||
line := s.rom[int(m)*8+((row+800)%8)]
|
line := s.rom[int(m)*8+((row+800)%8)]
|
||||||
// Invert if flash
|
// Invert if flash
|
||||||
if (m^0x80)&line&s.flasher > 0 {
|
if (m^0x80)&line&s.flasher > 127 {
|
||||||
line ^= 0xff
|
line ^= 0xff
|
||||||
}
|
}
|
||||||
line &= 0x7f // Mask out high bit
|
line &= 0x7f // Mask out high bit
|
||||||
|
@ -199,22 +229,31 @@ func (s *Scanner) textData(m byte, row int, column int) uint16 {
|
||||||
return s.hiresData(line, row, column)
|
return s.hiresData(line, row, column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Double each bit to go from pixel info to color info
|
||||||
|
var HIRES_DOUBLES = [128]uint16{
|
||||||
|
0x0, 0x3, 0xC, 0xF, 0x30, 0x33, 0x3C, 0x3F,
|
||||||
|
0xC0, 0xC3, 0xCC, 0xCF, 0xF0, 0xF3, 0xFC, 0xFF,
|
||||||
|
0x300, 0x303, 0x30C, 0x30F, 0x330, 0x333, 0x33C, 0x33F,
|
||||||
|
0x3C0, 0x3C3, 0x3CC, 0x3CF, 0x3F0, 0x3F3, 0x3FC, 0x3FF,
|
||||||
|
0xC00, 0xC03, 0xC0C, 0xC0F, 0xC30, 0xC33, 0xC3C, 0xC3F,
|
||||||
|
0xCC0, 0xCC3, 0xCCC, 0xCCF, 0xCF0, 0xCF3, 0xCFC, 0xCFF,
|
||||||
|
0xF00, 0xF03, 0xF0C, 0xF0F, 0xF30, 0xF33, 0xF3C, 0xF3F,
|
||||||
|
0xFC0, 0xFC3, 0xFCC, 0xFCF, 0xFF0, 0xFF3, 0xFFC, 0xFFF,
|
||||||
|
0x3000, 0x3003, 0x300C, 0x300F, 0x3030, 0x3033, 0x303C, 0x303F,
|
||||||
|
0x30C0, 0x30C3, 0x30CC, 0x30CF, 0x30F0, 0x30F3, 0x30FC, 0x30FF,
|
||||||
|
0x3300, 0x3303, 0x330C, 0x330F, 0x3330, 0x3333, 0x333C, 0x333F,
|
||||||
|
0x33C0, 0x33C3, 0x33CC, 0x33CF, 0x33F0, 0x33F3, 0x33FC, 0x33FF,
|
||||||
|
0x3C00, 0x3C03, 0x3C0C, 0x3C0F, 0x3C30, 0x3C33, 0x3C3C, 0x3C3F,
|
||||||
|
0x3CC0, 0x3CC3, 0x3CCC, 0x3CCF, 0x3CF0, 0x3CF3, 0x3CFC, 0x3CFF,
|
||||||
|
0x3F00, 0x3F03, 0x3F0C, 0x3F0F, 0x3F30, 0x3F33, 0x3F3C, 0x3F3F,
|
||||||
|
0x3FC0, 0x3FC3, 0x3FCC, 0x3FCF, 0x3FF0, 0x3FF3, 0x3FFC, 0x3FFF,
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Scanner) hiresData(m byte, row int, column int) uint16 {
|
func (s *Scanner) hiresData(m byte, row int, column int) uint16 {
|
||||||
// Double each bit
|
if m < 128 {
|
||||||
var data uint16
|
return HIRES_DOUBLES[m]
|
||||||
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,
|
return ((HIRES_DOUBLES[m&0x7f] << 1) & 0x3ff) | s.lastBit
|
||||||
// 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 {
|
func (s *Scanner) loresData(m byte, row int, column int) uint16 {
|
||||||
|
|
|
@ -2,19 +2,25 @@ package videoscan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/zellyn/go6502/tests"
|
|
||||||
apple2 "github.com/zellyn/goapple2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakePlotter struct {
|
type fakePlotter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakePlotter) Plot(apple2.PlotData) {
|
func (f *fakePlotter) Plot(PlotData) {
|
||||||
|
}
|
||||||
|
func (f *fakePlotter) OncePerFrame() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory for the tests. Satisfies the videoscan.RamReader interfaces.
|
||||||
|
type K64 [65536]byte
|
||||||
|
|
||||||
|
func (m *K64) RamRead(address uint16) byte {
|
||||||
|
return m[address]
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHorizontal(t *testing.T) {
|
func TestHorizontal(t *testing.T) {
|
||||||
var m tests.Memorizer
|
var m K64
|
||||||
var f fakePlotter
|
var f fakePlotter
|
||||||
s := NewScanner(&m, &f, [2048]byte{})
|
s := NewScanner(&m, &f, [2048]byte{})
|
||||||
|
|
||||||
|
@ -36,7 +42,7 @@ func TestHorizontal(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVertical(t *testing.T) {
|
func TestVertical(t *testing.T) {
|
||||||
var m tests.Memorizer
|
var m K64
|
||||||
var f fakePlotter
|
var f fakePlotter
|
||||||
s := NewScanner(&m, &f, [2048]byte{})
|
s := NewScanner(&m, &f, [2048]byte{})
|
||||||
scan65 := func() {
|
scan65 := func() {
|
||||||
|
@ -63,7 +69,7 @@ func TestVertical(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlanking(t *testing.T) {
|
func TestBlanking(t *testing.T) {
|
||||||
var m tests.Memorizer
|
var m K64
|
||||||
var f fakePlotter
|
var f fakePlotter
|
||||||
s := NewScanner(&m, &f, [2048]byte{})
|
s := NewScanner(&m, &f, [2048]byte{})
|
||||||
|
|
||||||
|
@ -92,7 +98,7 @@ func TestBlanking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSpecificLocationAddresses(t *testing.T) {
|
func TestSpecificLocationAddresses(t *testing.T) {
|
||||||
var m tests.Memorizer
|
var m K64
|
||||||
var f fakePlotter
|
var f fakePlotter
|
||||||
s := NewScanner(&m, &f, [2048]byte{})
|
s := NewScanner(&m, &f, [2048]byte{})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue