mirror of
https://github.com/zellyn/goapple2.git
synced 2024-12-06 23:51:05 +00:00
280 lines
6.5 KiB
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
|
|
}
|