mirror of
https://github.com/zellyn/goapple2.git
synced 2024-11-26 03:49:17 +00:00
Implemented basic graphics, firmware card
This commit is contained in:
parent
1d5e822fe8
commit
95f22d22b8
19
cards/cards.go
Normal file
19
cards/cards.go
Normal file
@ -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
|
||||
}
|
72
cards/firmware.go
Normal file
72
cards/firmware.go
Normal file
@ -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?
|
||||
|
225
goapple2.go
225
goapple2.go
@ -1,7 +1,10 @@
|
||||
package goapple2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zellyn/go6502/cpu"
|
||||
"github.com/zellyn/goapple2/cards"
|
||||
"github.com/zellyn/goapple2/videoscan"
|
||||
)
|
||||
|
||||
@ -13,45 +16,49 @@ type Apple2 struct {
|
||||
keys chan byte
|
||||
plotter videoscan.Plotter
|
||||
scanner *videoscan.Scanner
|
||||
done bool
|
||||
tw TickWaiter
|
||||
}
|
||||
|
||||
// Cycle counter. Satisfies the cpu.Ticker interface.
|
||||
type TickWaiter struct {
|
||||
Wait chan byte
|
||||
}
|
||||
|
||||
func (t TickWaiter) Tick() {
|
||||
<-t.Wait
|
||||
}
|
||||
|
||||
func NewTickWaiter() TickWaiter {
|
||||
return TickWaiter{Wait: make(chan byte)}
|
||||
Done bool
|
||||
lastRead byte
|
||||
cards [8]cards.Card "Peripheral cards"
|
||||
cardMask byte
|
||||
cardRomMask byte
|
||||
cardRomConflict bool "True if more than one card is handling the 2k ROM area"
|
||||
cardRomHandler byte
|
||||
card12kMask byte
|
||||
card12kConflict bool "True if more than one card is handling the 12k ROM area"
|
||||
card12kHandler byte
|
||||
}
|
||||
|
||||
func NewApple2(p videoscan.Plotter, rom []byte, charRom [2048]byte) *Apple2 {
|
||||
tw := NewTickWaiter()
|
||||
a2 := Apple2{
|
||||
// BUG(zellyn): this is not how the apple2 keyboard actually works
|
||||
keys: make(chan byte, 16),
|
||||
tw: tw,
|
||||
}
|
||||
copy(a2.mem[len(a2.mem)-len(rom):len(a2.mem)], rom)
|
||||
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()
|
||||
go func() {
|
||||
tw.Tick()
|
||||
for !a2.done {
|
||||
a2.cpu.Step()
|
||||
}
|
||||
}()
|
||||
return &a2
|
||||
}
|
||||
|
||||
func (a2 *Apple2) Read(address uint16) byte {
|
||||
// Keyboard read
|
||||
if address == 0xC000 {
|
||||
func (a2 *Apple2) AddCard(card cards.Card) error {
|
||||
slot := card.Slot()
|
||||
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 {
|
||||
select {
|
||||
case key := <-a2.keys:
|
||||
@ -60,23 +67,128 @@ func (a2 *Apple2) Read(address uint16) byte {
|
||||
}
|
||||
}
|
||||
return a2.key
|
||||
}
|
||||
if address == 0xC010 {
|
||||
// $C01X: Reset keyboard
|
||||
case 0xC010:
|
||||
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) {
|
||||
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
|
||||
}
|
||||
if address == 0xC010 {
|
||||
// Clear keyboard strobe
|
||||
a2.key &= 0x7F
|
||||
if address&0xF000 == 0xC000 {
|
||||
a2.handleC00X(address, value, true)
|
||||
return
|
||||
}
|
||||
a2.mem[address] = value
|
||||
|
||||
}
|
||||
|
||||
func (a2 *Apple2) Keypress(key byte) {
|
||||
@ -84,11 +196,50 @@ func (a2 *Apple2) Keypress(key byte) {
|
||||
}
|
||||
|
||||
func (a2 *Apple2) Step() error {
|
||||
a2.tw.Wait <- 0
|
||||
return a2.cpu.Step()
|
||||
}
|
||||
|
||||
func (a2 *Apple2) Tick() {
|
||||
a2.scanner.Scan1()
|
||||
return nil
|
||||
}
|
||||
|
||||
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 $@
|
||||
|
102
sdl/sdl.go
102
sdl/sdl.go
@ -2,16 +2,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"unsafe"
|
||||
|
||||
"github.com/0xe2-0x9a-0x9b/Go-SDL/sdl"
|
||||
"github.com/zellyn/goapple2"
|
||||
"github.com/zellyn/goapple2/cards"
|
||||
"github.com/zellyn/goapple2/util"
|
||||
"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 (
|
||||
BORDER_H = 20
|
||||
BORDER_W = 20
|
||||
@ -133,16 +143,16 @@ var KeyToApple = map[Key]byte{
|
||||
Key{sdl.K_8, M_NONE}: '8',
|
||||
Key{sdl.K_9, M_NONE}: '9',
|
||||
|
||||
Key{sdl.K_0, M_SHIFT}: '!',
|
||||
Key{sdl.K_1, M_SHIFT}: '@',
|
||||
Key{sdl.K_2, M_SHIFT}: '#',
|
||||
Key{sdl.K_3, M_SHIFT}: '$',
|
||||
Key{sdl.K_4, M_SHIFT}: '%',
|
||||
Key{sdl.K_5, M_SHIFT}: '^',
|
||||
Key{sdl.K_6, M_SHIFT}: '&',
|
||||
Key{sdl.K_7, M_SHIFT}: '*',
|
||||
Key{sdl.K_8, M_SHIFT}: '(',
|
||||
Key{sdl.K_9, M_SHIFT}: ')',
|
||||
Key{sdl.K_1, M_SHIFT}: '!',
|
||||
Key{sdl.K_2, M_SHIFT}: '@',
|
||||
Key{sdl.K_3, M_SHIFT}: '#',
|
||||
Key{sdl.K_4, M_SHIFT}: '$',
|
||||
Key{sdl.K_5, M_SHIFT}: '%',
|
||||
Key{sdl.K_6, M_SHIFT}: '^',
|
||||
Key{sdl.K_7, M_SHIFT}: '&',
|
||||
Key{sdl.K_8, 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_SHIFT}: '_',
|
||||
@ -224,6 +234,7 @@ func ProcessEvents(events <-chan interface{}, a2 *goapple2.Apple2) (done bool) {
|
||||
|
||||
type SdlPlotter struct {
|
||||
screen *sdl.Surface
|
||||
oncePerFrame func()
|
||||
}
|
||||
|
||||
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)
|
||||
data >>= 1
|
||||
}
|
||||
if pd.Column == 39 && y == 191 {
|
||||
}
|
||||
|
||||
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
|
||||
func RunEmulator() {
|
||||
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
||||
// charRom = util.ReadFullCharacterRomOrDie("../data/roms/apple2char.rom")
|
||||
charRom := util.ReadSmallCharacterRomOrDie("../data/roms/apple2-chars.rom")
|
||||
screen, err := Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
plotter := SdlPlotter{screen}
|
||||
a2 := goapple2.NewApple2(plotter, rom, charRom)
|
||||
for !ProcessEvents(sdl.Events, a2) {
|
||||
var a2 *goapple2.Apple2
|
||||
oncePerFrame := func() {
|
||||
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()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
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()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
RunEmulator()
|
||||
}
|
||||
|
@ -70,16 +70,16 @@ func (p TextPlotter) Plot(data videoscan.PlotData) {
|
||||
value := data.RawData
|
||||
ch, fg, bg := translateToTermbox(value)
|
||||
termbox.SetCell(x+1, y+1, ch, fg, bg)
|
||||
if x == 39 && data.Row == 191 {
|
||||
}
|
||||
func (p TextPlotter) OncePerFrame() {
|
||||
termbox.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Run the emulator
|
||||
func RunEmulator() {
|
||||
rom := util.ReadRomOrDie("../data/roms/apple2+.rom")
|
||||
charRom := util.ReadSmallCharacterRomOrDie("../data/roms/apple2-chars.rom")
|
||||
plotter := TextPlotter(0)
|
||||
var charRom [2048]byte
|
||||
a2 := goapple2.NewApple2(plotter, rom, charRom)
|
||||
if err := termbox.Init(); err != nil {
|
||||
panic(err)
|
||||
|
10
util/util.go
10
util/util.go
@ -27,3 +27,13 @@ func ReadSmallCharacterRomOrDie(filename string) [2048]byte {
|
||||
}
|
||||
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 (
|
||||
"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
|
||||
// information. Essentially, it holds the (binary) waveform of the
|
||||
@ -16,15 +14,22 @@ type PlotData struct {
|
||||
Column byte "The column (0-39)"
|
||||
ColorBurst bool "Whether the color signal is active"
|
||||
Data uint16 "14 half color cycles of information"
|
||||
LastData uint16 "The previous 14 half color cycles of information"
|
||||
RawData byte "The underlying raw byte"
|
||||
}
|
||||
|
||||
type Plotter interface {
|
||||
Plot(PlotData)
|
||||
OncePerFrame()
|
||||
}
|
||||
|
||||
// RAM memory interface
|
||||
type RamReader interface {
|
||||
RamRead(uint16) byte
|
||||
}
|
||||
|
||||
type Scanner struct {
|
||||
m cpu.Memory
|
||||
m RamReader
|
||||
h uint16
|
||||
v uint16
|
||||
plotter Plotter
|
||||
@ -38,12 +43,14 @@ type Scanner struct {
|
||||
hbl bool // Horizontal blanking
|
||||
vbl bool // Vertical blanking
|
||||
lastFour bool // Are we in the last 4 lines of the screen?
|
||||
lastData uint16 // The previous 14 half color cycles of information
|
||||
lastBit uint16 // Last bit of previous 14-cycle color data
|
||||
lastChange bool // Was there a change plotted last byte?
|
||||
flasher byte // if high bit is set, invert flashing text
|
||||
flashCount int // count up, and toggle flasher
|
||||
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{
|
||||
m: m,
|
||||
plotter: p,
|
||||
@ -66,10 +73,7 @@ func (s *Scanner) inc() {
|
||||
switch s.v {
|
||||
case 0x1ff:
|
||||
s.v = 250
|
||||
if s.flashCount++; s.flashCount >= FLASH_CYCLES {
|
||||
s.flashCount = 0
|
||||
s.flasher ^= 0x80
|
||||
}
|
||||
s.flasher += FLASH_INCREMENT
|
||||
default:
|
||||
s.v++
|
||||
}
|
||||
@ -87,12 +91,15 @@ func (s *Scanner) inc() {
|
||||
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() {
|
||||
m := s.m.Read(s.address())
|
||||
m := s.m.RamRead(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)
|
||||
@ -103,14 +110,24 @@ func (s *Scanner) Scan1() {
|
||||
}
|
||||
s.lastBit = (data >> 13) & 1
|
||||
if !s.hbl && !s.vbl {
|
||||
change := last[row][column] != (data | s.graphicsBit)
|
||||
if change || s.lastChange {
|
||||
s.plotter.Plot(PlotData{
|
||||
Row: byte(row),
|
||||
Column: byte(column),
|
||||
ColorBurst: color,
|
||||
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()
|
||||
}
|
||||
|
||||
@ -122,20 +139,28 @@ func (s *Scanner) row() int {
|
||||
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 {
|
||||
// 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)
|
||||
// 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 |= SUMS[(s.h>>3)&7+(s.v>>3)&24]
|
||||
|
||||
// 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)
|
||||
if s.page2 {
|
||||
@ -167,6 +192,11 @@ func (s *Scanner) address() uint16 {
|
||||
|
||||
func (s *Scanner) SetGraphics(graphics bool) {
|
||||
s.graphics = graphics
|
||||
if graphics {
|
||||
s.graphicsBit = 1 << 14
|
||||
} else {
|
||||
s.graphicsBit = 0
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
line := s.rom[int(m)*8+((row+800)%8)]
|
||||
// Invert if flash
|
||||
if (m^0x80)&line&s.flasher > 0 {
|
||||
if (m^0x80)&line&s.flasher > 127 {
|
||||
line ^= 0xff
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
if m < 128 {
|
||||
return HIRES_DOUBLES[m]
|
||||
}
|
||||
// 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
|
||||
return ((HIRES_DOUBLES[m&0x7f] << 1) & 0x3ff) | s.lastBit
|
||||
}
|
||||
|
||||
func (s *Scanner) loresData(m byte, row int, column int) uint16 {
|
||||
|
@ -2,19 +2,25 @@ package videoscan
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zellyn/go6502/tests"
|
||||
apple2 "github.com/zellyn/goapple2"
|
||||
)
|
||||
|
||||
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) {
|
||||
var m tests.Memorizer
|
||||
var m K64
|
||||
var f fakePlotter
|
||||
s := NewScanner(&m, &f, [2048]byte{})
|
||||
|
||||
@ -36,7 +42,7 @@ func TestHorizontal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVertical(t *testing.T) {
|
||||
var m tests.Memorizer
|
||||
var m K64
|
||||
var f fakePlotter
|
||||
s := NewScanner(&m, &f, [2048]byte{})
|
||||
scan65 := func() {
|
||||
@ -63,7 +69,7 @@ func TestVertical(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBlanking(t *testing.T) {
|
||||
var m tests.Memorizer
|
||||
var m K64
|
||||
var f fakePlotter
|
||||
s := NewScanner(&m, &f, [2048]byte{})
|
||||
|
||||
@ -92,7 +98,7 @@ func TestBlanking(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSpecificLocationAddresses(t *testing.T) {
|
||||
var m tests.Memorizer
|
||||
var m K64
|
||||
var f fakePlotter
|
||||
s := NewScanner(&m, &f, [2048]byte{})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user