2019-05-03 18:09:53 +00:00
|
|
|
package apple2
|
|
|
|
|
|
|
|
import (
|
2019-06-01 15:11:25 +00:00
|
|
|
"fmt"
|
2019-05-03 18:09:53 +00:00
|
|
|
"image"
|
|
|
|
"image/color"
|
2019-06-01 15:11:25 +00:00
|
|
|
"strings"
|
2019-05-03 18:09:53 +00:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-08-08 11:44:45 +00:00
|
|
|
charWidth = 7
|
|
|
|
charHeight = 8
|
|
|
|
textColumns = 40
|
|
|
|
textLines = 24
|
|
|
|
|
2019-05-03 19:45:29 +00:00
|
|
|
textPage1Address = uint16(0x0400)
|
|
|
|
textPage2Address = uint16(0x0800)
|
2019-11-08 22:56:54 +00:00
|
|
|
textPageSize = uint16(0x0400)
|
2019-05-03 18:09:53 +00:00
|
|
|
)
|
|
|
|
|
2020-08-08 17:23:35 +00:00
|
|
|
func snapshotText40Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA {
|
|
|
|
text := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
|
|
|
return renderTextMode(a, text, nil /*colorMap*/, light)
|
|
|
|
}
|
2019-05-03 18:09:53 +00:00
|
|
|
|
2020-08-08 17:23:35 +00:00
|
|
|
func snapshotText80Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA {
|
|
|
|
text := getText80FromMemory(a, isSecondPage)
|
|
|
|
return renderTextMode(a, text, nil /*colorMap*/, light)
|
|
|
|
}
|
|
|
|
|
|
|
|
func snapshotText40RGBMode(a *Apple2, isSecondPage bool) *image.RGBA {
|
|
|
|
text := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
|
|
|
colorMap := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage)
|
|
|
|
return renderTextMode(a, text, colorMap, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func snapshotText40RGBModeColors(a *Apple2, isSecondPage bool) *image.RGBA {
|
|
|
|
colorMap := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage)
|
|
|
|
return renderTextMode(a, nil /*text*/, colorMap, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getTextCharOffset(col int, line int) uint16 {
|
2019-05-03 18:09:53 +00:00
|
|
|
// See "Understanding the Apple II", page 5-10
|
|
|
|
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
|
|
|
|
section := line / 8 // Top, middle and bottom
|
2020-08-08 11:44:45 +00:00
|
|
|
eighth := line % 8
|
|
|
|
return uint16(section*40 + eighth*0x80 + col)
|
2019-05-03 18:09:53 +00:00
|
|
|
}
|
|
|
|
|
2020-08-08 17:23:35 +00:00
|
|
|
func getText80FromMemory(a *Apple2, isSecondPage bool) []uint8 {
|
2020-08-08 11:44:45 +00:00
|
|
|
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
|
|
|
text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage)
|
2020-08-08 17:23:35 +00:00
|
|
|
|
2019-11-08 22:56:54 +00:00
|
|
|
// Merge the two 40 cols to return 80 cols
|
|
|
|
text80Columns := make([]uint8, 2*len(text40Columns))
|
|
|
|
for i := 0; i < len(text40Columns); i++ {
|
|
|
|
text80Columns[2*i] = text40ColumnsAlt[i]
|
|
|
|
text80Columns[2*i+1] = text40Columns[i]
|
|
|
|
}
|
2020-08-08 17:23:35 +00:00
|
|
|
|
|
|
|
return text80Columns
|
2019-11-08 22:56:54 +00:00
|
|
|
}
|
2019-05-03 18:09:53 +00:00
|
|
|
|
2020-08-08 11:44:45 +00:00
|
|
|
func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 {
|
2019-11-08 22:56:54 +00:00
|
|
|
addressStart := textPage1Address
|
|
|
|
if isSecondPage {
|
|
|
|
addressStart = textPage2Address
|
|
|
|
}
|
|
|
|
addressEnd := addressStart + textPageSize
|
|
|
|
data := mem.subRange(addressStart, addressEnd)
|
|
|
|
|
2020-08-08 11:44:45 +00:00
|
|
|
text := make([]uint8, textLines*textColumns)
|
|
|
|
for l := 0; l < textLines; l++ {
|
2019-11-08 22:56:54 +00:00
|
|
|
for c := 0; c < textColumns; c++ {
|
2020-08-08 11:44:45 +00:00
|
|
|
char := data[getTextCharOffset(c, l)]
|
2019-11-08 22:56:54 +00:00
|
|
|
text[textColumns*l+c] = char
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return text
|
|
|
|
}
|
|
|
|
|
2020-08-08 17:23:35 +00:00
|
|
|
func getRGBTextColor(pixel bool, colorKey uint8) color.Color {
|
|
|
|
if pixel {
|
|
|
|
colorKey >>= 4
|
|
|
|
}
|
|
|
|
colorKey &= 0x0f
|
|
|
|
return rgbColorMap[colorKey]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func renderTextMode(a *Apple2, text []uint8, colorMap []uint8, light color.Color) *image.RGBA {
|
2020-08-08 11:44:45 +00:00
|
|
|
// Flash mode is 2Hz (host time)
|
2019-11-08 22:56:54 +00:00
|
|
|
isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2)
|
2020-08-08 17:23:35 +00:00
|
|
|
isAltText := a.io.isSoftSwitchActive(ioFlagAltChar)
|
2019-11-08 22:56:54 +00:00
|
|
|
|
2020-08-08 17:23:35 +00:00
|
|
|
columns := len(text) / textLines
|
|
|
|
if text == nil {
|
|
|
|
columns = textColumns
|
|
|
|
}
|
2019-11-08 22:56:54 +00:00
|
|
|
width := columns * charWidth
|
2020-08-08 17:23:35 +00:00
|
|
|
height := textLines * charHeight
|
2019-05-03 18:09:53 +00:00
|
|
|
size := image.Rect(0, 0, width, height)
|
|
|
|
img := image.NewRGBA(size)
|
|
|
|
|
|
|
|
for x := 0; x < width; x++ {
|
|
|
|
for y := 0; y < height; y++ {
|
2019-11-08 22:56:54 +00:00
|
|
|
line := y / charHeight
|
2019-05-03 18:09:53 +00:00
|
|
|
col := x / charWidth
|
|
|
|
rowInChar := y % charHeight
|
|
|
|
colInChar := x % charWidth
|
2020-08-08 17:23:35 +00:00
|
|
|
charIndex := line*columns + col
|
|
|
|
var char uint8
|
|
|
|
if text != nil {
|
|
|
|
char = text[charIndex]
|
|
|
|
} else {
|
|
|
|
char = 79 + 128 // Debug screen filed with O
|
|
|
|
}
|
|
|
|
|
2019-10-12 21:55:53 +00:00
|
|
|
var pixel bool
|
|
|
|
if a.isApple2e {
|
|
|
|
vid6 := (char & 0x40) != 0
|
|
|
|
vid7 := (char & 0x80) != 0
|
|
|
|
char := char & 0x3f
|
|
|
|
if vid6 && (vid7 || isAltText) {
|
|
|
|
char += 0x40
|
|
|
|
}
|
|
|
|
if vid7 || (vid6 && isFlashedFrame && !isAltText) {
|
|
|
|
char += 0x80
|
|
|
|
}
|
|
|
|
pixel = !a.cg.getPixel(char, rowInChar, colInChar)
|
|
|
|
} else {
|
|
|
|
pixel = a.cg.getPixel(char, rowInChar, colInChar)
|
|
|
|
topBits := char >> 6
|
|
|
|
isInverse := topBits == 0
|
|
|
|
isFlash := topBits == 1
|
|
|
|
|
|
|
|
pixel = pixel != (isInverse || (isFlash && isFlashedFrame))
|
|
|
|
}
|
2020-08-08 17:23:35 +00:00
|
|
|
|
2019-05-03 18:09:53 +00:00
|
|
|
var colour color.Color
|
2020-08-08 17:23:35 +00:00
|
|
|
if colorMap != nil {
|
|
|
|
colour = getRGBTextColor(pixel, colorMap[charIndex])
|
|
|
|
} else if pixel {
|
2019-05-03 19:45:29 +00:00
|
|
|
colour = light
|
2019-05-03 18:09:53 +00:00
|
|
|
} else {
|
|
|
|
colour = color.Black
|
|
|
|
}
|
2020-08-08 17:23:35 +00:00
|
|
|
|
2019-05-03 18:09:53 +00:00
|
|
|
img.Set(x, y, colour)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return img
|
|
|
|
}
|
2019-06-01 15:11:25 +00:00
|
|
|
|
|
|
|
// DumpTextModeAnsi returns the text mode contents using ANSI escape codes
|
|
|
|
// for reverse and flash
|
|
|
|
func DumpTextModeAnsi(a *Apple2) string {
|
2019-11-08 22:56:54 +00:00
|
|
|
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
|
|
|
|
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
|
2020-08-08 17:23:35 +00:00
|
|
|
isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar)
|
|
|
|
|
|
|
|
var text []uint8
|
|
|
|
if is80Columns {
|
|
|
|
text = getText80FromMemory(a, isSecondPage)
|
|
|
|
} else {
|
|
|
|
text = getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
|
|
|
}
|
|
|
|
columns := len(text) / textLines
|
2019-11-08 22:56:54 +00:00
|
|
|
|
2019-06-01 15:11:25 +00:00
|
|
|
content := "\n"
|
2019-11-08 22:56:54 +00:00
|
|
|
content += fmt.Sprintln(strings.Repeat("#", columns+4))
|
2020-08-08 17:23:35 +00:00
|
|
|
for l := 0; l < textLines; l++ {
|
2019-06-01 15:11:25 +00:00
|
|
|
line := ""
|
2019-11-08 22:56:54 +00:00
|
|
|
for c := 0; c < columns; c++ {
|
|
|
|
char := text[l*columns+c]
|
2019-06-01 15:11:25 +00:00
|
|
|
line += textMemoryByteToString(char, isAltText)
|
|
|
|
}
|
|
|
|
content += fmt.Sprintf("# %v #\n", line)
|
|
|
|
}
|
|
|
|
|
2019-11-08 22:56:54 +00:00
|
|
|
content += fmt.Sprintln(strings.Repeat("#", columns+4))
|
2019-06-01 15:11:25 +00:00
|
|
|
return content
|
|
|
|
}
|
|
|
|
|
|
|
|
func textMemoryByteToString(value uint8, isAltCharSet bool) string {
|
|
|
|
// See https://en.wikipedia.org/wiki/Apple_II_character_set
|
|
|
|
// Supports the new lowercase characters in the Apple2e
|
|
|
|
// Only ascii from 0x20 to 0x5F is visible
|
|
|
|
topBits := value >> 6
|
|
|
|
isInverse := topBits == 0
|
|
|
|
isFlash := topBits == 1
|
|
|
|
if isFlash && isAltCharSet {
|
|
|
|
// On the Apple2e with lowercase chars there is not flash mode.
|
|
|
|
isFlash = false
|
|
|
|
isInverse = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if isAltCharSet {
|
|
|
|
value = value & 0x7F
|
|
|
|
} else {
|
|
|
|
value = value & 0x3F
|
|
|
|
}
|
|
|
|
|
|
|
|
if value < 0x20 {
|
|
|
|
value += 0x40
|
|
|
|
}
|
|
|
|
|
|
|
|
if value == 0x7f {
|
|
|
|
// DEL is full box
|
|
|
|
value = '_'
|
|
|
|
}
|
|
|
|
|
|
|
|
if isFlash {
|
|
|
|
if value == ' ' {
|
|
|
|
// Flashing space in Apple is the full box. It can't be done with ANSI codes
|
|
|
|
value = '_'
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("\033[5m%v\033[0m", string(value))
|
|
|
|
} else if isInverse {
|
|
|
|
return fmt.Sprintf("\033[7m%v\033[0m", string(value))
|
|
|
|
} else {
|
|
|
|
return string(value)
|
|
|
|
}
|
|
|
|
}
|