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
- 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

View File

@ -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]
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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")