From 3dbb90d0caba1383595882f4412d5d6e7b1e5095 Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Fri, 8 Nov 2019 23:56:54 +0100 Subject: [PATCH] 80 columns support --- README.md | 17 ++++++--- memoryManager.go | 74 +++++++++++++++++++-------------------- screen.go | 16 ++++++--- screenLoRes.go | 78 +++++++++++------------------------------ screenText.go | 89 ++++++++++++++++++++++++++++++++++------------- softSwitches2e.go | 3 +- 6 files changed, 146 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index beed202..a099e65 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ Portable emulator of an Apple II+. Written in Go. ## 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 - 16 Sector diskettes in DSK format - ProDos hard disk @@ -16,8 +20,13 @@ Portable emulator of an Apple II+. Written in Go. - 256Kb Saturn RAM - ThunderClock Plus real time clock - Simulated bootable hard disk card + - Apple //e 80 columns with 64Kb - Graphic modes: - - Text, Lores and Hires + - Text 40 columns + - text 80 columns (Apple //e only) + - Low-Resolution graphics + - High-Resolution graphics + - Mixed mode - Displays: - Green monochrome monitor with half width pixel support - 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. - Fast disk mode to set max speed while using the disks. - 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. @@ -95,6 +103,7 @@ Line: ### Keys - 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 - F7: Save current state to disk - F8: Restore state from disk @@ -128,7 +137,7 @@ Only valid on SDL mode -mhz float cpu speed in Mhz, use 0 for full speed. Use F5 to toggle. (default 1.0227142857142857) -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 emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle. -panicSS diff --git a/memoryManager.go b/memoryManager.go index ce1ff41..15e16ea 100644 --- a/memoryManager.go +++ b/memoryManager.go @@ -36,6 +36,7 @@ type memoryManager struct { 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 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 intCxROMActive bool // Apple2e slots internal ROM shadow activeSlot uint8 // Active slot owner of 0xc800 to 0xcfff @@ -47,6 +48,10 @@ type memoryManager struct { const ( ioC8Off uint16 = 0xcfff addressLimitZero uint16 = 0x01ff + addressStartText uint16 = 0x0400 + addressLimitText uint16 = 0x07ff + addressStartHgr uint16 = 0x2000 + addressLimitHgr uint16 = 0x3fff addressLimitMainRAM uint16 = 0xbfff addressLimitIO uint16 = 0xc0ff addressLimitSlots uint16 = 0xc7ff @@ -106,75 +111,68 @@ func (mmu *memoryManager) accessLCArea(address uint16) memoryHandler { 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 { - // First two pages, $00xx and $01xx if address <= addressLimitZero { - if mmu.altZeroPage { - return mmu.physicalMainRAMAlt - } - return mmu.physicalMainRAM + return mmu.getPhysicalMainRAM(mmu.altZeroPage) + } + if mmu.store80Active && address <= addressLimitHgr { + 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 mmu.altMainRAMActiveRead { - return mmu.physicalMainRAMAlt - } - return mmu.physicalMainRAM + return mmu.getPhysicalMainRAM(mmu.altMainRAMActiveRead) } - - // IO section, $C0cc if address <= addressLimitIO { return mmu.apple2.io } - - // Slots sections, $Cxxx if address <= addressLimitSlotsExtra { return mmu.accessCArea(address) } - - // Upper address area if mmu.lcActiveRead { return mmu.accessLCArea(address) } - - // Use ROM return mmu.physicalROM[mmu.romPage] } func (mmu *memoryManager) accessWrite(address uint16) memoryHandler { - // First two pages, $00xx and $01xx if address <= addressLimitZero { - if mmu.altZeroPage { - return mmu.physicalMainRAMAlt - } - return mmu.physicalMainRAM + return mmu.getPhysicalMainRAM(mmu.altZeroPage) + } + if address <= addressLimitHgr && mmu.store80Active { + 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 mmu.altMainRAMActiveWrite { - return mmu.physicalMainRAMAlt - } - return mmu.physicalMainRAM + return mmu.getPhysicalMainRAM(mmu.altMainRAMActiveWrite) } - - // IO section, $C0xx if address <= addressLimitIO { return mmu.apple2.io } - - // Slots sections, $Cxxx if address <= addressLimitSlotsExtra { return mmu.accessCArea(address) } - - // Upper address area if mmu.lcActiveWrite { return mmu.accessLCArea(address) } - - // Use ROM return mmu.physicalROM[mmu.romPage] } diff --git a/screen.go b/screen.go index 8dc3738..31acf6e 100644 --- a/screen.go +++ b/screen.go @@ -21,9 +21,12 @@ func Snapshot(a *Apple2) *image.RGBA { isTextMode := a.io.isSoftSwitchActive(ioFlagText) isHiResMode := a.io.isSoftSwitchActive(ioFlagHiRes) 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 - if a.io.isSoftSwitchActive(ioFlagSecondPage) { + if isSecondPage { pageIndex = 1 } @@ -39,16 +42,21 @@ func Snapshot(a *Apple2) *image.RGBA { var snap *image.RGBA if isTextMode { - snap = snapshotTextMode(a, pageIndex, false, lightColor) + snap = snapshotTextMode(a, is80Columns, isSecondPage, false /*isMixMode*/, lightColor) } else { if isHiResMode { snap = snapshotHiResModeMonoShift(a, pageIndex, isMixMode, lightColor) } 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 { - snapText := snapshotTextMode(a, pageIndex, isMixMode, lightColor) + snapText := snapshotTextMode(a, is80Columns, false /*isSecondPage*/, true /*isMixMode*/, lightColor) snap = mixSnapshots(snap, snapText) } if isColor { diff --git a/screenLoRes.go b/screenLoRes.go index e7dc8ac..86ca64a 100644 --- a/screenLoRes.go +++ b/screenLoRes.go @@ -6,53 +6,10 @@ import ( ) const ( - loResPixelWidth = charWidth + loResPixelWidth = charWidth * 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 { /* 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 { - // As described in "Undertanding the Apple II", with half pixel shifts - - height := loResHeight - if mixedMode { - height = loResHeightMixed +func snapshotLoResModeMono(a *Apple2, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA { + text, columns, lines := getActiveText(a, false, isSecondPage, false) + if isMixMode { + lines -= textLinesMix } + 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) 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 - for i := 0; i < loResPixelWidth*2; i++ { - v := patterns[c][i+offset] + for i := 0; i < loResPixelWidth; i++ { + v := patterns[grPixel][i+offset] // Repeat the same color for 4 lines 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 } diff --git a/screenText.go b/screenText.go index fdc0b9c..95b7383 100644 --- a/screenText.go +++ b/screenText.go @@ -16,6 +16,7 @@ const ( textLinesMix = 4 textPage1Address = uint16(0x0400) textPage2Address = uint16(0x0800) + textPageSize = uint16(0x0400) ) 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) } -func getTextChar(a *Apple2, col int, line int, page int) uint8 { - address := textPage1Address - if page == 1 { - address = textPage2Address - } - address += getTextCharOffset(col, line) - return a.mmu.physicalMainRAM.subRange(address, address+1)[0] +func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA { + text, columns, lines := getActiveText(a, is80Columns, isSecondPage, isMixMode) + return renderTextMode(a, text, columns, lines, light) } -func snapshotTextMode(a *Apple2, page int, mixMode bool, light color.Color) *image.RGBA { - // Flash mode is 2Hz - isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2) +func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool) ([]uint8, int, int) { + 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 - if mixMode { + if isMixMode { lineStart = textLines - textLinesMix } - width := textColumns * charWidth - height := (textLines - lineStart) * charHeight + addressStart := textPage1Address + 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) img := image.NewRGBA(size) for x := 0; x < width; x++ { for y := 0; y < height; y++ { - line := y/charHeight + lineStart + line := y / charHeight col := x / charWidth rowInChar := y % charHeight colInChar := x % charWidth - char := getTextChar(a, col, line, page) + char := text[line*columns+col] var pixel bool if a.isApple2e { 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 // for reverse and flash func DumpTextModeAnsi(a *Apple2) string { - content := "\n" - content += fmt.Sprintln(strings.Repeat("#", textColumns+4)) + is80Columns := a.io.isSoftSwitchActive(ioFlag80Col) + 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) - for l := 0; l < textLines; l++ { + for l := 0; l < lines; l++ { line := "" - for c := 0; c < textColumns; c++ { - char := getTextChar(a, c, l, pageIndex) + for c := 0; c < columns; c++ { + char := text[l*columns+c] line += textMemoryByteToString(char, isAltText) } content += fmt.Sprintf("# %v #\n", line) } - content += fmt.Sprintln(strings.Repeat("#", textColumns+4)) + content += fmt.Sprintln(strings.Repeat("#", columns+4)) return content } diff --git a/softSwitches2e.go b/softSwitches2e.go index c9c5e1d..5fef5b0 100644 --- a/softSwitches2e.go +++ b/softSwitches2e.go @@ -6,7 +6,6 @@ package apple2 */ const ( - ioFlag80Store uint8 = 0x18 ioFlagAltChar uint8 = 0x1E ioFlag80Col uint8 = 0x1F // ??? ioVertBlank uin8 = 0x19 @@ -20,9 +19,9 @@ func addApple2ESoftSwitches(io *ioC0Page) { addSoftSwitchesMmu(io, 0x06, 0x07, 0x15, &mmu.intCxROMActive, "INTCXROM") addSoftSwitchesMmu(io, 0x08, 0x09, 0x16, &mmu.altZeroPage, "ALTZP") addSoftSwitchesMmu(io, 0x0a, 0x0b, 0x17, &mmu.slotC3ROMActive, "SLOTC3ROM") + addSoftSwitchesMmu(io, 0x00, 0x01, 0x18, &mmu.store80Active, "80STORE") // New IOU read softswithes - addSoftSwitchesIou(io, 0x00, 0x01, 0x18, ioFlag80Store, "80STORE") addSoftSwitchesIou(io, 0x0c, 0x0d, 0x1f, ioFlag80Col, "80COL") addSoftSwitchesIou(io, 0x0e, 0x0f, 0x1e, ioFlagAltChar, "ALTCHARSET")