From 0ef160a56e37d9f952390ca0aca076e3b5f16c65 Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Sat, 8 Aug 2020 19:23:35 +0200 Subject: [PATCH] RGB card 40 columns text with 16 colors video mode --- README.md | 1 + memoryManager.go | 2 +- screen.go | 32 +++++++++---- screenDebugParts.go | 21 ++++---- screenLoRes.go | 18 +++++-- screenText.go | 114 ++++++++++++++++++++++++++++++++++---------- 6 files changed, 140 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 99ae277..03f6ab1 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Portable emulator of an Apple II+ or //e. Written in Go. - Double-Width High-Resolution graphics (Apple //e only) - Super High Resolution (VidHD only) - Mixed mode + - RGB card text 40 columns with 16 colors for foreground and background - RGB card mode 11, mono 560x192 - RGB card mode 13, ntsc 140*192 (regular DHGR) - RGB card mode 14, mix of modes 11 and 13 on the fly diff --git a/memoryManager.go b/memoryManager.go index 4e9be16..fa13713 100644 --- a/memoryManager.go +++ b/memoryManager.go @@ -123,7 +123,7 @@ func (mmu *memoryManager) accessRead(address uint16) memoryHandler { return mmu.getPhysicalMainRAM(mmu.altZeroPage) } 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 { return mmu.getPhysicalMainRAM(altPage) } diff --git a/screen.go b/screen.go index 0e52982..eeeb195 100644 --- a/screen.go +++ b/screen.go @@ -24,9 +24,10 @@ const ( videoDGR uint8 = 0x09 videoDHGR uint8 = 0x0a - videoMono560 uint8 = 0x10 - videoRGBMix uint8 = 0x11 - videoSHR uint8 = 0x12 + videoRGBText40 uint8 = 0x10 + videoMono560 uint8 = 0x11 + videoRGBMix uint8 = 0x12 + videoSHR uint8 = 0x13 // Modifiers videoBaseMask uint8 = 0x1f @@ -56,7 +57,11 @@ func getCurrentVideoMode(a *Apple2) uint8 { if is80Columns { mode = videoText80 } else { - mode = videoText40 + if a.mmu.store80Active { + mode = videoRGBText40 + } else { + mode = videoText40 + } } isMixMode = false } else if isHiResMode { @@ -111,15 +116,18 @@ func snapshotByMode(a *Apple2, videoMode uint8) *image.RGBA { var ntscMask *image.Alpha switch videoBase { case videoText40: - snap = snapshotTextMode(a, false /*is80Columns*/, isSecondPage, lightColor) + snap = snapshotText40Mode(a, isSecondPage, lightColor) applyNTSCFilter = false case videoText80: - snap = snapshotTextMode(a, true /*is80Columns*/, isSecondPage, lightColor) + snap = snapshotText80Mode(a, isSecondPage, lightColor) + applyNTSCFilter = false + case videoRGBText40: + snap = snapshotText40RGBMode(a, isSecondPage) applyNTSCFilter = false case videoGR: - snap = snapshotLoResModeMono(a, false /*isDoubleResMode*/, isSecondPage, lightColor) + snap = snapshotLoResModeMono(a, isSecondPage, lightColor) case videoDGR: - snap = snapshotLoResModeMono(a, true /*isDoubleResMode*/, isSecondPage, lightColor) + snap = snapshotMeResModeMono(a, isSecondPage, lightColor) case videoHGR: snap = snapshotHiResModeMono(a, isSecondPage, lightColor) case videoDHGR: @@ -135,8 +143,12 @@ func snapshotByMode(a *Apple2, videoMode uint8) *image.RGBA { } if isMixMode { - isMix80 := (videoMode & videoMixText80) != 0 - bottom := snapshotTextMode(a, isMix80, isSecondPage, lightColor) + var bottom *image.RGBA + if (videoMode & videoMixText40) != 0 { + bottom = snapshotText40Mode(a, isSecondPage, lightColor) + } else { + bottom = snapshotText80Mode(a, isSecondPage, lightColor) + } snap = mixSnapshots(snap, bottom) } diff --git a/screenDebugParts.go b/screenDebugParts.go index 10dfde2..739e785 100644 --- a/screenDebugParts.go +++ b/screenDebugParts.go @@ -7,23 +7,25 @@ import ( // SnapshotParts the currently visible screen func (a *Apple2) SnapshotParts() *image.RGBA { videoMode := getCurrentVideoMode(a) + isSecondPage := (videoMode & videoSecondPage) != 0 + videoBase := videoMode & videoBaseMask + snapScreen := snapshotByMode(a, videoMode) snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage) snapPage2 := snapshotByMode(a, videoMode|videoSecondPage) var snapAux *image.RGBA - /*videoBase := videoMode & videoBaseMask - if videoBase == videoRGBMix { - isSecondPage := (videoMode & videoSecondPage) != 0 - _, mask := snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/ /*, color.White) + /* + if videoBase == videoRGBMix { + _, mask := snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/ /*, color.White) 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) - } - - if snapAux == nil { + } else { snapAux = snapshotByMode(a, videoText40) } @@ -45,6 +47,9 @@ func (a *Apple2) VideoModeName() string { case videoText80: name = "TEXT80COL" applyNTSCFilter = false + case videoRGBText40: + name = "TEXT40COLRGB" + applyNTSCFilter = false case videoGR: name = "GR" case videoDGR: diff --git a/screenLoRes.go b/screenLoRes.go index 21c51dc..c95af85 100644 --- a/screenLoRes.go +++ b/screenLoRes.go @@ -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 { - text, columns, lines := getActiveText(a, isDoubleResMode, isSecondPage) - grLines := lines * 2 +func snapshotLoResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { + data := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage) + 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 if isDoubleResMode { pixelWidth = doubleLoResPixelWidth @@ -52,7 +62,7 @@ func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, l patterns := getColorPatterns(light) for l := 0; l < grLines; l++ { for c := 0; c < columns; c++ { - char := text[(l/2)*columns+c] + char := data[(l/2)*columns+c] grPixel := char >> 4 if l%2 == 0 { grPixel = char & 0xf diff --git a/screenText.go b/screenText.go index ee49786..ba3b1c7 100644 --- a/screenText.go +++ b/screenText.go @@ -19,8 +19,28 @@ const ( 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 // http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf 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) } -func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, light color.Color) *image.RGBA { - 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 - } - +func getText80FromMemory(a *Apple2, isSecondPage bool) []uint8 { text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage) text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage) + // 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] } - return text80Columns, textColumns * 2, textLines + + return text80Columns } func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 { @@ -68,12 +80,50 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 { 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) 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 - height := lines * charHeight + height := textLines * charHeight size := image.Rect(0, 0, width, height) img := image.NewRGBA(size) @@ -83,13 +133,18 @@ func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color col := x / charWidth rowInChar := y % charHeight 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 if a.isApple2e { - isAltText := a.io.isSoftSwitchActive(ioFlagAltChar) vid6 := (char & 0x40) != 0 vid7 := (char & 0x80) != 0 - char := char & 0x3f if vid6 && (vid7 || isAltText) { char += 0x40 @@ -106,12 +161,16 @@ func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color pixel = pixel != (isInverse || (isFlash && isFlashedFrame)) } + var colour color.Color - if pixel { + if colorMap != nil { + colour = getRGBTextColor(pixel, colorMap[charIndex]) + } else if pixel { colour = light } else { colour = color.Black } + 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 { is80Columns := a.io.isSoftSwitchActive(ioFlag80Col) 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) - 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 := "" for c := 0; c < columns; c++ { char := text[l*columns+c]