RGB card 40 columns text with 16 colors video mode
This commit is contained in:
parent
aa198877ec
commit
0ef160a56e
|
@ -34,6 +34,7 @@ Portable emulator of an Apple II+ or //e. Written in Go.
|
||||||
- Double-Width High-Resolution graphics (Apple //e only)
|
- Double-Width High-Resolution graphics (Apple //e only)
|
||||||
- Super High Resolution (VidHD only)
|
- Super High Resolution (VidHD only)
|
||||||
- Mixed mode
|
- Mixed mode
|
||||||
|
- RGB card text 40 columns with 16 colors for foreground and background
|
||||||
- RGB card mode 11, mono 560x192
|
- RGB card mode 11, mono 560x192
|
||||||
- RGB card mode 13, ntsc 140*192 (regular DHGR)
|
- RGB card mode 13, ntsc 140*192 (regular DHGR)
|
||||||
- RGB card mode 14, mix of modes 11 and 13 on the fly
|
- RGB card mode 14, mix of modes 11 and 13 on the fly
|
||||||
|
|
|
@ -123,7 +123,7 @@ func (mmu *memoryManager) accessRead(address uint16) memoryHandler {
|
||||||
return mmu.getPhysicalMainRAM(mmu.altZeroPage)
|
return mmu.getPhysicalMainRAM(mmu.altZeroPage)
|
||||||
}
|
}
|
||||||
if mmu.store80Active && address <= addressLimitHgr {
|
if mmu.store80Active && address <= addressLimitHgr {
|
||||||
altPage := mmu.apple2.io.isSoftSwitchActive(ioFlagSecondPage)
|
altPage := mmu.apple2.io.isSoftSwitchActive(ioFlagSecondPage) // TODO: move flag to mmu property like the store80
|
||||||
if address >= addressStartText && address <= addressLimitText {
|
if address >= addressStartText && address <= addressLimitText {
|
||||||
return mmu.getPhysicalMainRAM(altPage)
|
return mmu.getPhysicalMainRAM(altPage)
|
||||||
}
|
}
|
||||||
|
|
32
screen.go
32
screen.go
|
@ -24,9 +24,10 @@ const (
|
||||||
videoDGR uint8 = 0x09
|
videoDGR uint8 = 0x09
|
||||||
videoDHGR uint8 = 0x0a
|
videoDHGR uint8 = 0x0a
|
||||||
|
|
||||||
videoMono560 uint8 = 0x10
|
videoRGBText40 uint8 = 0x10
|
||||||
videoRGBMix uint8 = 0x11
|
videoMono560 uint8 = 0x11
|
||||||
videoSHR uint8 = 0x12
|
videoRGBMix uint8 = 0x12
|
||||||
|
videoSHR uint8 = 0x13
|
||||||
|
|
||||||
// Modifiers
|
// Modifiers
|
||||||
videoBaseMask uint8 = 0x1f
|
videoBaseMask uint8 = 0x1f
|
||||||
|
@ -56,7 +57,11 @@ func getCurrentVideoMode(a *Apple2) uint8 {
|
||||||
if is80Columns {
|
if is80Columns {
|
||||||
mode = videoText80
|
mode = videoText80
|
||||||
} else {
|
} else {
|
||||||
mode = videoText40
|
if a.mmu.store80Active {
|
||||||
|
mode = videoRGBText40
|
||||||
|
} else {
|
||||||
|
mode = videoText40
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isMixMode = false
|
isMixMode = false
|
||||||
} else if isHiResMode {
|
} else if isHiResMode {
|
||||||
|
@ -111,15 +116,18 @@ func snapshotByMode(a *Apple2, videoMode uint8) *image.RGBA {
|
||||||
var ntscMask *image.Alpha
|
var ntscMask *image.Alpha
|
||||||
switch videoBase {
|
switch videoBase {
|
||||||
case videoText40:
|
case videoText40:
|
||||||
snap = snapshotTextMode(a, false /*is80Columns*/, isSecondPage, lightColor)
|
snap = snapshotText40Mode(a, isSecondPage, lightColor)
|
||||||
applyNTSCFilter = false
|
applyNTSCFilter = false
|
||||||
case videoText80:
|
case videoText80:
|
||||||
snap = snapshotTextMode(a, true /*is80Columns*/, isSecondPage, lightColor)
|
snap = snapshotText80Mode(a, isSecondPage, lightColor)
|
||||||
|
applyNTSCFilter = false
|
||||||
|
case videoRGBText40:
|
||||||
|
snap = snapshotText40RGBMode(a, isSecondPage)
|
||||||
applyNTSCFilter = false
|
applyNTSCFilter = false
|
||||||
case videoGR:
|
case videoGR:
|
||||||
snap = snapshotLoResModeMono(a, false /*isDoubleResMode*/, isSecondPage, lightColor)
|
snap = snapshotLoResModeMono(a, isSecondPage, lightColor)
|
||||||
case videoDGR:
|
case videoDGR:
|
||||||
snap = snapshotLoResModeMono(a, true /*isDoubleResMode*/, isSecondPage, lightColor)
|
snap = snapshotMeResModeMono(a, isSecondPage, lightColor)
|
||||||
case videoHGR:
|
case videoHGR:
|
||||||
snap = snapshotHiResModeMono(a, isSecondPage, lightColor)
|
snap = snapshotHiResModeMono(a, isSecondPage, lightColor)
|
||||||
case videoDHGR:
|
case videoDHGR:
|
||||||
|
@ -135,8 +143,12 @@ func snapshotByMode(a *Apple2, videoMode uint8) *image.RGBA {
|
||||||
}
|
}
|
||||||
|
|
||||||
if isMixMode {
|
if isMixMode {
|
||||||
isMix80 := (videoMode & videoMixText80) != 0
|
var bottom *image.RGBA
|
||||||
bottom := snapshotTextMode(a, isMix80, isSecondPage, lightColor)
|
if (videoMode & videoMixText40) != 0 {
|
||||||
|
bottom = snapshotText40Mode(a, isSecondPage, lightColor)
|
||||||
|
} else {
|
||||||
|
bottom = snapshotText80Mode(a, isSecondPage, lightColor)
|
||||||
|
}
|
||||||
snap = mixSnapshots(snap, bottom)
|
snap = mixSnapshots(snap, bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,23 +7,25 @@ import (
|
||||||
// SnapshotParts the currently visible screen
|
// SnapshotParts the currently visible screen
|
||||||
func (a *Apple2) SnapshotParts() *image.RGBA {
|
func (a *Apple2) SnapshotParts() *image.RGBA {
|
||||||
videoMode := getCurrentVideoMode(a)
|
videoMode := getCurrentVideoMode(a)
|
||||||
|
isSecondPage := (videoMode & videoSecondPage) != 0
|
||||||
|
videoBase := videoMode & videoBaseMask
|
||||||
|
|
||||||
snapScreen := snapshotByMode(a, videoMode)
|
snapScreen := snapshotByMode(a, videoMode)
|
||||||
snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage)
|
snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage)
|
||||||
snapPage2 := snapshotByMode(a, videoMode|videoSecondPage)
|
snapPage2 := snapshotByMode(a, videoMode|videoSecondPage)
|
||||||
var snapAux *image.RGBA
|
var snapAux *image.RGBA
|
||||||
|
|
||||||
/*videoBase := videoMode & videoBaseMask
|
/*
|
||||||
if videoBase == videoRGBMix {
|
if videoBase == videoRGBMix {
|
||||||
isSecondPage := (videoMode & videoSecondPage) != 0
|
_, mask := snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/ /*, color.White)
|
||||||
_, mask := snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/ /*, color.White)
|
|
||||||
snapAux = filterMask(mask)
|
snapAux = filterMask(mask)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
if snapAux == nil && (videoMode&videoMixText80) != 0 {
|
if videoBase == videoRGBText40 {
|
||||||
|
snapAux = snapshotText40RGBModeColors(a, isSecondPage)
|
||||||
|
} else if (videoMode & videoMixText80) != 0 {
|
||||||
snapAux = snapshotByMode(a, videoText80)
|
snapAux = snapshotByMode(a, videoText80)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if snapAux == nil {
|
|
||||||
snapAux = snapshotByMode(a, videoText40)
|
snapAux = snapshotByMode(a, videoText40)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +47,9 @@ func (a *Apple2) VideoModeName() string {
|
||||||
case videoText80:
|
case videoText80:
|
||||||
name = "TEXT80COL"
|
name = "TEXT80COL"
|
||||||
applyNTSCFilter = false
|
applyNTSCFilter = false
|
||||||
|
case videoRGBText40:
|
||||||
|
name = "TEXT40COLRGB"
|
||||||
|
applyNTSCFilter = false
|
||||||
case videoGR:
|
case videoGR:
|
||||||
name = "GR"
|
name = "GR"
|
||||||
case videoDGR:
|
case videoDGR:
|
||||||
|
|
|
@ -38,9 +38,19 @@ func getColorPatterns(light color.Color) [16][16]color.Color {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, light color.Color) *image.RGBA {
|
func snapshotLoResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA {
|
||||||
text, columns, lines := getActiveText(a, isDoubleResMode, isSecondPage)
|
data := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
||||||
grLines := lines * 2
|
return renderGrMode(data, false /*isMeres*/, light)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotMeResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA {
|
||||||
|
data := getText80FromMemory(a, isSecondPage)
|
||||||
|
return renderGrMode(data, true /*isMeres*/, light)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderGrMode(data []uint8, isDoubleResMode bool, light color.Color) *image.RGBA {
|
||||||
|
grLines := textLines * 2
|
||||||
|
columns := len(data) / textLines
|
||||||
pixelWidth := loResPixelWidth
|
pixelWidth := loResPixelWidth
|
||||||
if isDoubleResMode {
|
if isDoubleResMode {
|
||||||
pixelWidth = doubleLoResPixelWidth
|
pixelWidth = doubleLoResPixelWidth
|
||||||
|
@ -52,7 +62,7 @@ func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, l
|
||||||
patterns := getColorPatterns(light)
|
patterns := getColorPatterns(light)
|
||||||
for l := 0; l < grLines; l++ {
|
for l := 0; l < grLines; l++ {
|
||||||
for c := 0; c < columns; c++ {
|
for c := 0; c < columns; c++ {
|
||||||
char := text[(l/2)*columns+c]
|
char := data[(l/2)*columns+c]
|
||||||
grPixel := char >> 4
|
grPixel := char >> 4
|
||||||
if l%2 == 0 {
|
if l%2 == 0 {
|
||||||
grPixel = char & 0xf
|
grPixel = char & 0xf
|
||||||
|
|
114
screenText.go
114
screenText.go
|
@ -19,8 +19,28 @@ const (
|
||||||
textPageSize = uint16(0x0400)
|
textPageSize = uint16(0x0400)
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTextCharOffset(col int, line int) uint16 {
|
func snapshotText40Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA {
|
||||||
|
text := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
||||||
|
return renderTextMode(a, text, nil /*colorMap*/, light)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
// See "Understanding the Apple II", page 5-10
|
// See "Understanding the Apple II", page 5-10
|
||||||
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
|
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
|
||||||
section := line / 8 // Top, middle and bottom
|
section := line / 8 // Top, middle and bottom
|
||||||
|
@ -28,26 +48,18 @@ func getTextCharOffset(col int, line int) uint16 {
|
||||||
return uint16(section*40 + eighth*0x80 + col)
|
return uint16(section*40 + eighth*0x80 + col)
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, light color.Color) *image.RGBA {
|
func getText80FromMemory(a *Apple2, isSecondPage bool) []uint8 {
|
||||||
text, columns, lines := getActiveText(a, is80Columns, isSecondPage)
|
|
||||||
return renderTextMode(a, text, columns, lines, light)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool) ([]uint8, int, int) {
|
|
||||||
if !is80Columns {
|
|
||||||
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
|
||||||
return text40Columns, textColumns, textLines
|
|
||||||
}
|
|
||||||
|
|
||||||
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
||||||
text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage)
|
text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage)
|
||||||
|
|
||||||
// Merge the two 40 cols to return 80 cols
|
// Merge the two 40 cols to return 80 cols
|
||||||
text80Columns := make([]uint8, 2*len(text40Columns))
|
text80Columns := make([]uint8, 2*len(text40Columns))
|
||||||
for i := 0; i < len(text40Columns); i++ {
|
for i := 0; i < len(text40Columns); i++ {
|
||||||
text80Columns[2*i] = text40ColumnsAlt[i]
|
text80Columns[2*i] = text40ColumnsAlt[i]
|
||||||
text80Columns[2*i+1] = text40Columns[i]
|
text80Columns[2*i+1] = text40Columns[i]
|
||||||
}
|
}
|
||||||
return text80Columns, textColumns * 2, textLines
|
|
||||||
|
return text80Columns
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 {
|
func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 {
|
||||||
|
@ -68,12 +80,50 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color.Color) *image.RGBA {
|
/*
|
||||||
|
See:
|
||||||
|
https://mrob.com/pub/xapple2/colors.html
|
||||||
|
https://archive.org/details/IIgs_2523063_Master_Color_Values
|
||||||
|
*/
|
||||||
|
var rgbColorMap = [16]color.Color{
|
||||||
|
color.RGBA{0, 0, 0, 255}, // Black
|
||||||
|
color.RGBA{221, 0, 51, 255}, // Magenta
|
||||||
|
color.RGBA{0, 0, 153, 255}, // Dark Blue
|
||||||
|
color.RGBA{221, 34, 221, 255}, // Purple
|
||||||
|
color.RGBA{0, 119, 34, 255}, // Dark Green
|
||||||
|
color.RGBA{85, 85, 85, 255}, // Grey 1
|
||||||
|
color.RGBA{34, 34, 255, 255}, // Medium Blue
|
||||||
|
color.RGBA{102, 170, 255, 255}, // Light Blue
|
||||||
|
color.RGBA{136, 85, 0, 255}, // Brown
|
||||||
|
color.RGBA{255, 102, 0, 255}, // Orange
|
||||||
|
color.RGBA{170, 170, 170, 255}, // Grey 2
|
||||||
|
color.RGBA{255, 153, 136, 255}, // Pink
|
||||||
|
color.RGBA{17, 221, 0, 255}, // Green
|
||||||
|
color.RGBA{255, 255, 0, 255}, // Yellow
|
||||||
|
color.RGBA{68, 255, 153, 255}, // Aquamarine
|
||||||
|
color.RGBA{255, 255, 255, 255}, // White
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
// Flash mode is 2Hz (host time)
|
// Flash mode is 2Hz (host time)
|
||||||
isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2)
|
isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2)
|
||||||
|
isAltText := a.io.isSoftSwitchActive(ioFlagAltChar)
|
||||||
|
|
||||||
|
columns := len(text) / textLines
|
||||||
|
if text == nil {
|
||||||
|
columns = textColumns
|
||||||
|
}
|
||||||
width := columns * charWidth
|
width := columns * charWidth
|
||||||
height := lines * charHeight
|
height := textLines * charHeight
|
||||||
size := image.Rect(0, 0, width, height)
|
size := image.Rect(0, 0, width, height)
|
||||||
img := image.NewRGBA(size)
|
img := image.NewRGBA(size)
|
||||||
|
|
||||||
|
@ -83,13 +133,18 @@ func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color
|
||||||
col := x / charWidth
|
col := x / charWidth
|
||||||
rowInChar := y % charHeight
|
rowInChar := y % charHeight
|
||||||
colInChar := x % charWidth
|
colInChar := x % charWidth
|
||||||
char := text[line*columns+col]
|
charIndex := line*columns + col
|
||||||
|
var char uint8
|
||||||
|
if text != nil {
|
||||||
|
char = text[charIndex]
|
||||||
|
} else {
|
||||||
|
char = 79 + 128 // Debug screen filed with O
|
||||||
|
}
|
||||||
|
|
||||||
var pixel bool
|
var pixel bool
|
||||||
if a.isApple2e {
|
if a.isApple2e {
|
||||||
isAltText := a.io.isSoftSwitchActive(ioFlagAltChar)
|
|
||||||
vid6 := (char & 0x40) != 0
|
vid6 := (char & 0x40) != 0
|
||||||
vid7 := (char & 0x80) != 0
|
vid7 := (char & 0x80) != 0
|
||||||
|
|
||||||
char := char & 0x3f
|
char := char & 0x3f
|
||||||
if vid6 && (vid7 || isAltText) {
|
if vid6 && (vid7 || isAltText) {
|
||||||
char += 0x40
|
char += 0x40
|
||||||
|
@ -106,12 +161,16 @@ func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color
|
||||||
|
|
||||||
pixel = pixel != (isInverse || (isFlash && isFlashedFrame))
|
pixel = pixel != (isInverse || (isFlash && isFlashedFrame))
|
||||||
}
|
}
|
||||||
|
|
||||||
var colour color.Color
|
var colour color.Color
|
||||||
if pixel {
|
if colorMap != nil {
|
||||||
|
colour = getRGBTextColor(pixel, colorMap[charIndex])
|
||||||
|
} else if pixel {
|
||||||
colour = light
|
colour = light
|
||||||
} else {
|
} else {
|
||||||
colour = color.Black
|
colour = color.Black
|
||||||
}
|
}
|
||||||
|
|
||||||
img.Set(x, y, colour)
|
img.Set(x, y, colour)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,14 +183,19 @@ func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color
|
||||||
func DumpTextModeAnsi(a *Apple2) string {
|
func DumpTextModeAnsi(a *Apple2) string {
|
||||||
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
|
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
|
||||||
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
|
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
|
||||||
|
|
||||||
text, columns, lines := getActiveText(a, is80Columns, isSecondPage)
|
|
||||||
content := "\n"
|
|
||||||
content += fmt.Sprintln(strings.Repeat("#", columns+4))
|
|
||||||
|
|
||||||
isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar)
|
isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar)
|
||||||
|
|
||||||
for l := 0; l < lines; l++ {
|
var text []uint8
|
||||||
|
if is80Columns {
|
||||||
|
text = getText80FromMemory(a, isSecondPage)
|
||||||
|
} else {
|
||||||
|
text = getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage)
|
||||||
|
}
|
||||||
|
columns := len(text) / textLines
|
||||||
|
|
||||||
|
content := "\n"
|
||||||
|
content += fmt.Sprintln(strings.Repeat("#", columns+4))
|
||||||
|
for l := 0; l < textLines; l++ {
|
||||||
line := ""
|
line := ""
|
||||||
for c := 0; c < columns; c++ {
|
for c := 0; c < columns; c++ {
|
||||||
char := text[l*columns+c]
|
char := text[l*columns+c]
|
||||||
|
|
Loading…
Reference in New Issue