goapple2/videoscan/videoscan.go

280 lines
6.5 KiB
Go

package videoscan
import (
"fmt"
)
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
// 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"
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 RamReader
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?
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
graphicsBit uint16 // Bit 14 high if we're in graphics mode
}
func NewScanner(m RamReader, 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
s.flasher += FLASH_INCREMENT
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
}
// 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() {
address := s.address()
if address >= 0xC000 {
fmt.Printf("\n\n\nWOAH! $%04X\n\n\n", address)
}
m := s.m.RamRead(address)
row, column := s.row(), s.column()
_, _, _ = m, row, column
var data uint16
switch {
case !s.graphics || (s.mix && s.lastFour):
data = s.textData(m, row)
case s.hires:
data = s.hiresData(m)
default: // lores
data = s.loresData(m, row, column)
}
s.lastBit = (data >> 13) & 1
if !s.hbl && !s.vbl {
change := last[row][column] != (data | s.graphicsBit)
if change || s.lastChange {
// if row <= 8 && column == 0 {
// fmt.Printf("%d,%d: RawData=%02X, Data=%04X\n", row, column, m, data)
// }
s.plotter.Plot(PlotData{
Row: byte(row),
Column: byte(column),
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()
}
func (s *Scanner) column() int {
return int(s.h) - 0x58 // 0x1011000
}
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 |= SUMS[(s.h>>3)&7+(s.v>>3)&24]
// Next three are V0,V1,V2
addr |= (s.v << 4) & 0x380 // ((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
if graphics {
s.graphicsBit = 1 << 14
} else {
s.graphicsBit = 0
}
}
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) uint16 {
line := s.rom[int(m)*8+((row+800)%8)]
// Invert if flash
if (m^0x80)&line&s.flasher > 127 {
line ^= 0xff
}
line &= 0x7f // Mask out high bit
// Now it's just like hires data
return s.hiresData(line)
}
// 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) uint16 {
if m < 128 {
return HIRES_DOUBLES[m]
}
return ((HIRES_DOUBLES[m&0x7f] << 1) & 0x3fff) | s.lastBit
}
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
}