mirror of
https://github.com/ivanizag/izapple2.git
synced 2025-02-19 00:31:00 +00:00
80 columns support
This commit is contained in:
parent
4d6b02e4d6
commit
3dbb90d0ca
17
README.md
17
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
|
||||
|
@ -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]
|
||||
}
|
||||
|
||||
|
16
screen.go
16
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user