80 columns support

This commit is contained in:
Ivan Izaguirre 2019-11-08 23:56:54 +01:00 committed by Iván Izaguirre
parent 4d6b02e4d6
commit 3dbb90d0ca
6 changed files with 146 additions and 131 deletions

View File

@ -6,7 +6,11 @@ Portable emulator of an Apple II+. Written in Go.
## Features ## Features
- Apple II+ with 48Kb of base RAM - Models:
- Apple ][+ with 48Kb of base RAM
- Apple //e with 128Kb of RAM
- Apple //e enhanced with 128Kb of RAM
- Base64A clone with 48Kb of base RAM and paginated ROM
- Sound - Sound
- 16 Sector diskettes in DSK format - 16 Sector diskettes in DSK format
- ProDos hard disk - ProDos hard disk
@ -16,8 +20,13 @@ Portable emulator of an Apple II+. Written in Go.
- 256Kb Saturn RAM - 256Kb Saturn RAM
- ThunderClock Plus real time clock - ThunderClock Plus real time clock
- Simulated bootable hard disk card - Simulated bootable hard disk card
- Apple //e 80 columns with 64Kb
- Graphic modes: - Graphic modes:
- Text, Lores and Hires - Text 40 columns
- text 80 columns (Apple //e only)
- Low-Resolution graphics
- High-Resolution graphics
- Mixed mode
- Displays: - Displays:
- Green monochrome monitor with half width pixel support - Green monochrome monitor with half width pixel support
- NTSC Color TV (extracting the phase from the mono signal) - NTSC Color TV (extracting the phase from the mono signal)
@ -25,7 +34,6 @@ Portable emulator of an Apple II+. Written in Go.
- Adjustable speed. - Adjustable speed.
- Fast disk mode to set max speed while using the disks. - Fast disk mode to set max speed while using the disks.
- Single file executable with embedded ROMs and DOS 3.3 - Single file executable with embedded ROMs and DOS 3.3
- Optional emulation of the clone Base64A by Copam
- Joystick support. Up to two joysticks or four paddles. - Joystick support. Up to two joysticks or four paddles.
@ -95,6 +103,7 @@ Line:
### Keys ### Keys
- F5: Toggle speed between real and fastest - F5: Toggle speed between real and fastest
- Ctrl F5: Show current speed in Mhz
- F6: Toggle between NTSC color TV and green phosphor monochrome monitor - F6: Toggle between NTSC color TV and green phosphor monochrome monitor
- F7: Save current state to disk - F7: Save current state to disk
- F8: Restore state from disk - F8: Restore state from disk
@ -128,7 +137,7 @@ Only valid on SDL mode
-mhz float -mhz float
cpu speed in Mhz, use 0 for full speed. Use F5 to toggle. (default 1.0227142857142857) cpu speed in Mhz, use 0 for full speed. Use F5 to toggle. (default 1.0227142857142857)
-model string -model string
set base model. Models available 2plus, 2e, 2enh, base64a (default "2e") set base model. Models available 2plus, 2e, 2enh, base64a (default "2enh")
-mono -mono
emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle. emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.
-panicSS -panicSS

View File

@ -36,6 +36,7 @@ type memoryManager struct {
altZeroPage bool // Use extra RAM from 0x0000 to 0x01ff. And additional language card block altZeroPage bool // Use extra RAM from 0x0000 to 0x01ff. And additional language card block
altMainRAMActiveRead bool // Use extra RAM from 0x0200 to 0xbfff for read altMainRAMActiveRead bool // Use extra RAM from 0x0200 to 0xbfff for read
altMainRAMActiveWrite bool // Use extra RAM from 0x0200 to 0xbfff for write altMainRAMActiveWrite bool // Use extra RAM from 0x0200 to 0xbfff for write
store80Active bool // Special pagination for text and graphics areas
slotC3ROMActive bool // Apple2e slot 3 ROM shadow slotC3ROMActive bool // Apple2e slot 3 ROM shadow
intCxROMActive bool // Apple2e slots internal ROM shadow intCxROMActive bool // Apple2e slots internal ROM shadow
activeSlot uint8 // Active slot owner of 0xc800 to 0xcfff activeSlot uint8 // Active slot owner of 0xc800 to 0xcfff
@ -47,6 +48,10 @@ type memoryManager struct {
const ( const (
ioC8Off uint16 = 0xcfff ioC8Off uint16 = 0xcfff
addressLimitZero uint16 = 0x01ff addressLimitZero uint16 = 0x01ff
addressStartText uint16 = 0x0400
addressLimitText uint16 = 0x07ff
addressStartHgr uint16 = 0x2000
addressLimitHgr uint16 = 0x3fff
addressLimitMainRAM uint16 = 0xbfff addressLimitMainRAM uint16 = 0xbfff
addressLimitIO uint16 = 0xc0ff addressLimitIO uint16 = 0xc0ff
addressLimitSlots uint16 = 0xc7ff addressLimitSlots uint16 = 0xc7ff
@ -106,75 +111,68 @@ func (mmu *memoryManager) accessLCArea(address uint16) memoryHandler {
return mmu.physicalEFRAM[block] return mmu.physicalEFRAM[block]
} }
func (mmu *memoryManager) getPhysicalMainRAM(alt bool) *memoryRange {
if alt {
return mmu.physicalMainRAMAlt
}
return mmu.physicalMainRAM
}
func (mmu *memoryManager) accessRead(address uint16) memoryHandler { func (mmu *memoryManager) accessRead(address uint16) memoryHandler {
// First two pages, $00xx and $01xx
if address <= addressLimitZero { if address <= addressLimitZero {
if mmu.altZeroPage { return mmu.getPhysicalMainRAM(mmu.altZeroPage)
return mmu.physicalMainRAMAlt }
} if mmu.store80Active && address <= addressLimitHgr {
return mmu.physicalMainRAM altPage := mmu.apple2.io.isSoftSwitchActive(ioFlagSecondPage)
if address >= addressStartText && address <= addressLimitText {
return mmu.getPhysicalMainRAM(altPage)
}
hires := mmu.apple2.io.isSoftSwitchActive(ioFlagHiRes)
if hires && address >= addressStartHgr && address <= addressLimitHgr {
return mmu.getPhysicalMainRAM(altPage)
}
} }
// Main RAM area
if address <= addressLimitMainRAM { if address <= addressLimitMainRAM {
if mmu.altMainRAMActiveRead { return mmu.getPhysicalMainRAM(mmu.altMainRAMActiveRead)
return mmu.physicalMainRAMAlt
}
return mmu.physicalMainRAM
} }
// IO section, $C0cc
if address <= addressLimitIO { if address <= addressLimitIO {
return mmu.apple2.io return mmu.apple2.io
} }
// Slots sections, $Cxxx
if address <= addressLimitSlotsExtra { if address <= addressLimitSlotsExtra {
return mmu.accessCArea(address) return mmu.accessCArea(address)
} }
// Upper address area
if mmu.lcActiveRead { if mmu.lcActiveRead {
return mmu.accessLCArea(address) return mmu.accessLCArea(address)
} }
// Use ROM
return mmu.physicalROM[mmu.romPage] return mmu.physicalROM[mmu.romPage]
} }
func (mmu *memoryManager) accessWrite(address uint16) memoryHandler { func (mmu *memoryManager) accessWrite(address uint16) memoryHandler {
// First two pages, $00xx and $01xx
if address <= addressLimitZero { if address <= addressLimitZero {
if mmu.altZeroPage { return mmu.getPhysicalMainRAM(mmu.altZeroPage)
return mmu.physicalMainRAMAlt }
} if address <= addressLimitHgr && mmu.store80Active {
return mmu.physicalMainRAM altPage := mmu.apple2.io.isSoftSwitchActive(ioFlagSecondPage)
if address >= addressStartText && address <= addressLimitText {
return mmu.getPhysicalMainRAM(altPage)
}
hires := mmu.apple2.io.isSoftSwitchActive(ioFlagHiRes)
if false && hires && address >= addressStartHgr && address <= addressLimitHgr {
return mmu.getPhysicalMainRAM(altPage)
}
} }
// Main RAM area
if address <= addressLimitMainRAM { if address <= addressLimitMainRAM {
if mmu.altMainRAMActiveWrite { return mmu.getPhysicalMainRAM(mmu.altMainRAMActiveWrite)
return mmu.physicalMainRAMAlt
}
return mmu.physicalMainRAM
} }
// IO section, $C0xx
if address <= addressLimitIO { if address <= addressLimitIO {
return mmu.apple2.io return mmu.apple2.io
} }
// Slots sections, $Cxxx
if address <= addressLimitSlotsExtra { if address <= addressLimitSlotsExtra {
return mmu.accessCArea(address) return mmu.accessCArea(address)
} }
// Upper address area
if mmu.lcActiveWrite { if mmu.lcActiveWrite {
return mmu.accessLCArea(address) return mmu.accessLCArea(address)
} }
// Use ROM
return mmu.physicalROM[mmu.romPage] return mmu.physicalROM[mmu.romPage]
} }

View File

@ -21,9 +21,12 @@ func Snapshot(a *Apple2) *image.RGBA {
isTextMode := a.io.isSoftSwitchActive(ioFlagText) isTextMode := a.io.isSoftSwitchActive(ioFlagText)
isHiResMode := a.io.isSoftSwitchActive(ioFlagHiRes) isHiResMode := a.io.isSoftSwitchActive(ioFlagHiRes)
isMixMode := a.io.isSoftSwitchActive(ioFlagMixed) isMixMode := a.io.isSoftSwitchActive(ioFlagMixed)
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
isDoubleResMode := !isTextMode && is80Columns && a.io.isSoftSwitchActive(ioFlagAnnunciator3)
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
pageIndex := 0 pageIndex := 0
if a.io.isSoftSwitchActive(ioFlagSecondPage) { if isSecondPage {
pageIndex = 1 pageIndex = 1
} }
@ -39,16 +42,21 @@ func Snapshot(a *Apple2) *image.RGBA {
var snap *image.RGBA var snap *image.RGBA
if isTextMode { if isTextMode {
snap = snapshotTextMode(a, pageIndex, false, lightColor) snap = snapshotTextMode(a, is80Columns, isSecondPage, false /*isMixMode*/, lightColor)
} else { } else {
if isHiResMode { if isHiResMode {
snap = snapshotHiResModeMonoShift(a, pageIndex, isMixMode, lightColor) snap = snapshotHiResModeMonoShift(a, pageIndex, isMixMode, lightColor)
} else { } else {
snap = snapshotLoResModeMonoShift(a, pageIndex, isMixMode, lightColor) if isDoubleResMode {
//snap = snapshotLoResDoubleModeMono(a, false /*isSecondPage*/, isMixMode, lightColor)
snap = snapshotLoResModeMono(a, isSecondPage, isMixMode, lightColor)
} else {
snap = snapshotLoResModeMono(a, isSecondPage, isMixMode, lightColor)
}
} }
if isMixMode { if isMixMode {
snapText := snapshotTextMode(a, pageIndex, isMixMode, lightColor) snapText := snapshotTextMode(a, is80Columns, false /*isSecondPage*/, true /*isMixMode*/, lightColor)
snap = mixSnapshots(snap, snapText) snap = mixSnapshots(snap, snapText)
} }
if isColor { if isColor {

View File

@ -6,53 +6,10 @@ import (
) )
const ( const (
loResPixelWidth = charWidth loResPixelWidth = charWidth * 2
loResPixelHeight = charHeight / 2 loResPixelHeight = charHeight / 2
loResWidth = textColumns
loResHeight = textLines * 2
loResHeightMixed = (textLines - textLinesMix) * 2
loRes
loResPage1Address = textPage1Address
loResPage2Address = textPage2Address
) )
func getLoResPixel(a *Apple2, x int, y int, page int) uint8 {
// Each text mode char encodes two pixels
char := getTextChar(a, x, y/2, page)
if y%2 == 0 {
// Top pixel in char
return char & 0xf
}
// Bottom pixel in char
return char >> 4
}
func snapshotLoResModeReferenceColor(a *Apple2, page int, mixedMode bool) *image.RGBA {
// As defined on "Apple II Reference Manual"
height := loResHeight
if mixedMode {
height = loResHeightMixed
}
size := image.Rect(0, 0, loResWidth, height)
img := image.NewRGBA(size)
// Lores colors correspond to the NTSC 4 bit patterns reversed
colorMap := getNTSCColorMap()
reversedNibble := []uint8{0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}
for x := 0; x < loResWidth; x++ {
for y := 0; y < height; y++ {
v := getLoResPixel(a, x, y, page)
img.Set(x, y, colorMap[reversedNibble[v]])
}
}
return img
}
func getColorPatterns(light color.Color) [16][16]color.Color { func getColorPatterns(light color.Color) [16][16]color.Color {
/* /*
For each lores pixel we have to fill 14 half mono pixels with For each lores pixel we have to fill 14 half mono pixels with
@ -80,32 +37,37 @@ func getColorPatterns(light color.Color) [16][16]color.Color {
} }
func snapshotLoResModeMonoShift(a *Apple2, page int, mixedMode bool, light color.Color) *image.RGBA { func snapshotLoResModeMono(a *Apple2, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA {
// As described in "Undertanding the Apple II", with half pixel shifts text, columns, lines := getActiveText(a, false, isSecondPage, false)
if isMixMode {
height := loResHeight lines -= textLinesMix
if mixedMode {
height = loResHeightMixed
} }
grLines := lines * 2
size := image.Rect(0, 0, 2*loResWidth*loResPixelWidth, height*loResPixelHeight) size := image.Rect(0, 0, columns*loResPixelWidth, grLines*loResPixelHeight)
img := image.NewRGBA(size) img := image.NewRGBA(size)
patterns := getColorPatterns(light) patterns := getColorPatterns(light)
for l := 0; l < grLines; l++ {
for c := 0; c < columns; c++ {
char := text[(l/2)*columns+c]
grPixel := char >> 4
if l%2 == 0 {
grPixel = char & 0xf
}
offset := (c % 2) * 2 // 2 pixel offset for odd lores pixels, 0 for even pixels
for x := 0; x < loResWidth; x++ {
for y := 0; y < height; y++ {
offset := (x % 2) * 2 // 2 pixel offset for odd lores pixels, 0 for even pixels
c := getLoResPixel(a, x, y, page)
// Insert the 14 half pixels required // Insert the 14 half pixels required
for i := 0; i < loResPixelWidth*2; i++ { for i := 0; i < loResPixelWidth; i++ {
v := patterns[c][i+offset] v := patterns[grPixel][i+offset]
// Repeat the same color for 4 lines // Repeat the same color for 4 lines
for r := 0; r < loResPixelHeight; r++ { for r := 0; r < loResPixelHeight; r++ {
img.Set(x*loResPixelWidth*2+i, y*4+r, v) img.Set(c*loResPixelWidth+i, l*4+r, v)
} }
} }
} }
} }
return img return img
} }

View File

@ -16,6 +16,7 @@ const (
textLinesMix = 4 textLinesMix = 4
textPage1Address = uint16(0x0400) textPage1Address = uint16(0x0400)
textPage2Address = uint16(0x0800) textPage2Address = uint16(0x0800)
textPageSize = uint16(0x0400)
) )
func getTextCharOffset(col int, line int) uint16 { func getTextCharOffset(col int, line int) uint16 {
@ -27,36 +28,74 @@ func getTextCharOffset(col int, line int) uint16 {
return uint16(section*40 + eigth*0x80 + col) return uint16(section*40 + eigth*0x80 + col)
} }
func getTextChar(a *Apple2, col int, line int, page int) uint8 { func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA {
address := textPage1Address text, columns, lines := getActiveText(a, is80Columns, isSecondPage, isMixMode)
if page == 1 { return renderTextMode(a, text, columns, lines, light)
address = textPage2Address
}
address += getTextCharOffset(col, line)
return a.mmu.physicalMainRAM.subRange(address, address+1)[0]
} }
func snapshotTextMode(a *Apple2, page int, mixMode bool, light color.Color) *image.RGBA { func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool) ([]uint8, int, int) {
// Flash mode is 2Hz
isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2)
lines := textLines
if isMixMode {
lines = textLinesMix
}
if !is80Columns {
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode)
return text40Columns, textColumns, lines
}
text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode)
text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage, isMixMode)
// 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, lines
}
func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []uint8 {
lineStart := 0 lineStart := 0
if mixMode { if isMixMode {
lineStart = textLines - textLinesMix lineStart = textLines - textLinesMix
} }
width := textColumns * charWidth addressStart := textPage1Address
height := (textLines - lineStart) * charHeight if isSecondPage {
addressStart = textPage2Address
}
addressEnd := addressStart + textPageSize
data := mem.subRange(addressStart, addressEnd)
lines := textLines - lineStart
text := make([]uint8, lines*textColumns)
for l := 0; l < lines; l++ {
for c := 0; c < textColumns; c++ {
char := data[getTextCharOffset(c, l+lineStart)]
text[textColumns*l+c] = char
}
}
return text
}
func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color.Color) *image.RGBA {
// Flash mode is 2Hz
isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2)
width := columns * charWidth
height := lines * charHeight
size := image.Rect(0, 0, width, height) size := image.Rect(0, 0, width, height)
img := image.NewRGBA(size) img := image.NewRGBA(size)
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
line := y/charHeight + lineStart line := y / charHeight
col := x / charWidth col := x / charWidth
rowInChar := y % charHeight rowInChar := y % charHeight
colInChar := x % charWidth colInChar := x % charWidth
char := getTextChar(a, col, line, page) char := text[line*columns+col]
var pixel bool var pixel bool
if a.isApple2e { if a.isApple2e {
isAltText := a.io.isSoftSwitchActive(ioFlagAltChar) isAltText := a.io.isSoftSwitchActive(ioFlagAltChar)
@ -95,25 +134,25 @@ func snapshotTextMode(a *Apple2, page int, mixMode bool, light color.Color) *ima
// DumpTextModeAnsi returns the text mode contents using ANSI escape codes // DumpTextModeAnsi returns the text mode contents using ANSI escape codes
// for reverse and flash // for reverse and flash
func DumpTextModeAnsi(a *Apple2) string { func DumpTextModeAnsi(a *Apple2) string {
content := "\n" is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
content += fmt.Sprintln(strings.Repeat("#", textColumns+4)) isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
text, columns, lines := getActiveText(a, is80Columns, isSecondPage, false /*isMixedMode*/)
content := "\n"
content += fmt.Sprintln(strings.Repeat("#", columns+4))
pageIndex := 0
if a.io.isSoftSwitchActive(ioFlagSecondPage) {
pageIndex = 1
}
isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar) isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar)
for l := 0; l < textLines; l++ { for l := 0; l < lines; l++ {
line := "" line := ""
for c := 0; c < textColumns; c++ { for c := 0; c < columns; c++ {
char := getTextChar(a, c, l, pageIndex) char := text[l*columns+c]
line += textMemoryByteToString(char, isAltText) line += textMemoryByteToString(char, isAltText)
} }
content += fmt.Sprintf("# %v #\n", line) content += fmt.Sprintf("# %v #\n", line)
} }
content += fmt.Sprintln(strings.Repeat("#", textColumns+4)) content += fmt.Sprintln(strings.Repeat("#", columns+4))
return content return content
} }

View File

@ -6,7 +6,6 @@ package apple2
*/ */
const ( const (
ioFlag80Store uint8 = 0x18
ioFlagAltChar uint8 = 0x1E ioFlagAltChar uint8 = 0x1E
ioFlag80Col uint8 = 0x1F ioFlag80Col uint8 = 0x1F
// ??? ioVertBlank uin8 = 0x19 // ??? ioVertBlank uin8 = 0x19
@ -20,9 +19,9 @@ func addApple2ESoftSwitches(io *ioC0Page) {
addSoftSwitchesMmu(io, 0x06, 0x07, 0x15, &mmu.intCxROMActive, "INTCXROM") addSoftSwitchesMmu(io, 0x06, 0x07, 0x15, &mmu.intCxROMActive, "INTCXROM")
addSoftSwitchesMmu(io, 0x08, 0x09, 0x16, &mmu.altZeroPage, "ALTZP") addSoftSwitchesMmu(io, 0x08, 0x09, 0x16, &mmu.altZeroPage, "ALTZP")
addSoftSwitchesMmu(io, 0x0a, 0x0b, 0x17, &mmu.slotC3ROMActive, "SLOTC3ROM") addSoftSwitchesMmu(io, 0x0a, 0x0b, 0x17, &mmu.slotC3ROMActive, "SLOTC3ROM")
addSoftSwitchesMmu(io, 0x00, 0x01, 0x18, &mmu.store80Active, "80STORE")
// New IOU read softswithes // New IOU read softswithes
addSoftSwitchesIou(io, 0x00, 0x01, 0x18, ioFlag80Store, "80STORE")
addSoftSwitchesIou(io, 0x0c, 0x0d, 0x1f, ioFlag80Col, "80COL") addSoftSwitchesIou(io, 0x0c, 0x0d, 0x1f, ioFlag80Col, "80COL")
addSoftSwitchesIou(io, 0x0e, 0x0f, 0x1e, ioFlagAltChar, "ALTCHARSET") addSoftSwitchesIou(io, 0x0e, 0x0f, 0x1e, ioFlagAltChar, "ALTCHARSET")