Partial support for the Basis 108 clone

This commit is contained in:
Iván Izaguirre 2024-07-28 22:37:48 +02:00
parent bd707227f0
commit 1938b9072b
44 changed files with 528 additions and 219 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ frontend/*/*.dsk
frontend/*/*.po frontend/*/*.po
frontend/*/*.2mg frontend/*/*.2mg
frontend/*/*.hdv frontend/*/*.hdv
frontend/*/*.zip
frontend/a2fyne/a2fyne frontend/a2fyne/a2fyne
frontend/headless/headless frontend/headless/headless
frontend/*/snapshot.gif frontend/*/snapshot.gif

View File

@ -9,6 +9,7 @@ Portable emulator of an Apple II+ or //e. Written in Go.
- Apple //e with 128Kb of RAM - Apple //e with 128Kb of RAM
- Apple //e enhanced with 128Kb of RAM - Apple //e enhanced with 128Kb of RAM
- Base64A clone with 48Kb of base RAM and paged ROM - Base64A clone with 48Kb of base RAM and paged ROM
- Basis 108 clone (partial)
- Storage - Storage
- 16 Sector 5 1/4 diskettes. Uncompressed or compressed witth gzip or zip. Supported formats: - 16 Sector 5 1/4 diskettes. Uncompressed or compressed witth gzip or zip. Supported formats:
- NIB (read only) - NIB (read only)
@ -228,6 +229,7 @@ The available pre-configured models are:
2enh: Apple //e 2enh: Apple //e
2plus: Apple ][+ 2plus: Apple ][+
base64a: Base 64A base64a: Base 64A
basis108: Basis 108
dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x
swyft: swyft swyft: swyft

View File

@ -22,10 +22,10 @@ func testA2AuditInternal(t *testing.T, model string, removeLangCard bool, cycles
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
at.terminateCondition = buildTerminateConditionTexts(at, messages, false, cycles) at.terminateCondition = buildTerminateConditionTexts(messages, testTextMode40, cycles)
at.run() at.run()
text := at.getText() text := at.getText(testTextMode40)
for _, message := range messages { for _, message := range messages {
if !strings.Contains(text, message) { if !strings.Contains(text, message) {
t.Errorf("Expected '%s', got '%s'", message, text) t.Errorf("Expected '%s', got '%s'", message, text)

View File

@ -4,6 +4,7 @@ import (
"sync/atomic" "sync/atomic"
"github.com/ivanizag/iz6502" "github.com/ivanizag/iz6502"
"github.com/ivanizag/izapple2/screen"
) )
// Apple2 represents all the components and state of the emulated machine // Apple2 represents all the components and state of the emulated machine
@ -12,6 +13,7 @@ type Apple2 struct {
cpu *iz6502.State cpu *iz6502.State
mmu *memoryManager mmu *memoryManager
io *ioC0Page io *ioC0Page
video screen.VideoSource
cg *CharacterGenerator cg *CharacterGenerator
cards [8]Card cards [8]Card
tracers []executionTracer tracers []executionTracer
@ -88,6 +90,10 @@ func (a *Apple2) IsForceCaps() bool {
return a.forceCaps return a.forceCaps
} }
func (a *Apple2) GetCgPageInfo() (int, int) {
return a.cg.getPage(), a.cg.getPages()
}
func (a *Apple2) RequestFastMode() { func (a *Apple2) RequestFastMode() {
// Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain. // Note: if the fastMode is shorter than maxWaitDuration, there won't be any gain.
atomic.AddInt32(&a.fastRequestsCounter, 1) atomic.AddInt32(&a.fastRequestsCounter, 1)
@ -100,3 +106,7 @@ func (a *Apple2) ReleaseFastMode() {
func (a *Apple2) registerRemovableMediaDrive(d drive) { func (a *Apple2) registerRemovableMediaDrive(d drive) {
a.removableMediaDrives = append(a.removableMediaDrives, d) a.removableMediaDrives = append(a.removableMediaDrives, d)
} }
func (a *Apple2) GetVideoSource() screen.VideoSource {
return a.video
}

View File

@ -132,7 +132,7 @@ func (a *Apple2) executionTrace() {
func (a *Apple2) dumpDebugInfo() { func (a *Apple2) dumpDebugInfo() {
// See "Apple II Monitors Peeled" // See "Apple II Monitors Peeled"
pageZeroSymbols := map[int]string{ pageZeroSymbols := map[uint16]string{
0x36: "CSWL", 0x36: "CSWL",
0x37: "CSWH", 0x37: "CSWH",
0x38: "KSWL", 0x38: "KSWL",
@ -145,8 +145,8 @@ func (a *Apple2) dumpDebugInfo() {
0xef: "JVAFOLDH", // Apple Pascal 0xef: "JVAFOLDH", // Apple Pascal
} }
fmt.Printf("Page zero values:\n") fmt.Printf("Page zero values:\n")
for _, k := range []int{0x36, 0x37, 0x38, 0x39, 0xe2, 0xe3, 0xec, 0xed, 0xee, 0xef} { for _, k := range []uint16{0x36, 0x37, 0x38, 0x39, 0xe2, 0xe3, 0xec, 0xed, 0xee, 0xef} {
d := a.mmu.physicalMainRAM.data[k] d := a.mmu.physicalMainRAM.peek(k)
fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d) fmt.Printf(" %v(0x%x): 0x%02x\n", pageZeroSymbols[k], k, d)
} }

View File

@ -48,12 +48,22 @@ func (at *apple2Tester) run() {
at.a.Run() at.a.Run()
} }
func (at *apple2Tester) getText() string { type testTextModeFunc func(a *Apple2) string
return screen.RenderTextModeString(at.a, false, false, false, at.a.isApple2e)
var testTextMode40 testTextModeFunc = func(a *Apple2) string {
return screen.RenderTextModeString(a.video, false, false, false, a.hasLowerCase, false)
} }
func (at *apple2Tester) getText80() string { var testTextMode80 testTextModeFunc = func(a *Apple2) string {
return screen.RenderTextModeString(at.a, true, false, false, at.a.isApple2e) return screen.RenderTextModeString(a.video, true, false, false, a.hasLowerCase, false)
}
var testTextMode80AltOrder testTextModeFunc = func(a *Apple2) string {
return screen.RenderTextModeString(a.video, true, false, false, a.hasLowerCase, true)
}
func (at *apple2Tester) getText(textMode testTextModeFunc) string {
return textMode(at.a)
} }
/* /*
@ -66,12 +76,12 @@ func (at *apple2Tester) getText80() string {
const textCheckInterval = uint64(100_000) const textCheckInterval = uint64(100_000)
func buildTerminateConditionText(at *apple2Tester, needle string, col80 bool, timeoutCycles uint64) terminateConditionFunc { func buildTerminateConditionText(needle string, textMode testTextModeFunc, timeoutCycles uint64) terminateConditionFunc {
needles := []string{needle} needles := []string{needle}
return buildTerminateConditionTexts(at, needles, col80, timeoutCycles) return buildTerminateConditionTexts(needles, textMode, timeoutCycles)
} }
func buildTerminateConditionTexts(at *apple2Tester, needles []string, col80 bool, timeoutCycles uint64) terminateConditionFunc { func buildTerminateConditionTexts(needles []string, textMode testTextModeFunc, timeoutCycles uint64) terminateConditionFunc {
lastCheck := uint64(0) lastCheck := uint64(0)
found := false found := false
return func(a *Apple2) bool { return func(a *Apple2) bool {
@ -81,12 +91,7 @@ func buildTerminateConditionTexts(at *apple2Tester, needles []string, col80 bool
} }
if cycles-lastCheck > textCheckInterval { if cycles-lastCheck > textCheckInterval {
lastCheck = cycles lastCheck = cycles
var text string text := textMode(a)
if col80 {
text = at.getText80()
} else {
text = at.getText()
}
for _, needle := range needles { for _, needle := range needles {
if !strings.Contains(text, needle) { if !strings.Contains(text, needle) {
return false return false

View File

@ -1,7 +1,5 @@
package izapple2 package izapple2
import "fmt"
/* /*
Copam BASE64A adaptation. Copam BASE64A adaptation.
*/ */
@ -16,35 +14,14 @@ const (
) )
func loadBase64aRom(a *Apple2) error { func loadBase64aRom(a *Apple2) error {
// Load the 6 PROM dumps return loadMultiPageRom(a, []string{
romBanksBytes := make([][]uint8, base64aRomBankCount) "<internal>/BASE64A_D0.BIN",
for j := range romBanksBytes { "<internal>/BASE64A_D8.BIN",
romBanksBytes[j] = make([]uint8, 0, base64aRomBankSize) "<internal>/BASE64A_E0.BIN",
} "<internal>/BASE64A_E8.BIN",
"<internal>/BASE64A_F0.BIN",
for i := 0; i < base64aRomChipCount; i++ { "<internal>/BASE64A_F8.BIN",
filename := fmt.Sprintf("<internal>/BASE64A_%X.BIN", 0xd0+i*0x08) })
data, _, err := LoadResource(filename)
if err != nil {
return err
}
for j := range romBanksBytes {
start := (j * base64aRomWindowSize) % len(data)
romBanksBytes[j] = append(romBanksBytes[j], data[start:start+base64aRomWindowSize]...)
}
}
// Create paged ROM
romData := make([]uint8, 0, base64aRomBankSize*base64aRomBankCount)
for _, bank := range romBanksBytes {
romData = append(romData, bank...)
}
rom := newMemoryRangePagedROM(0xd000, romData, "Base64 ROM", base64aRomBankCount)
// Start with first bank active
rom.setPage(0)
a.mmu.physicalROM = rom
return nil
} }
func addBase64aSoftSwitches(io *ioC0Page) { func addBase64aSoftSwitches(io *ioC0Page) {

126
boardBasis108.go Normal file
View File

@ -0,0 +1,126 @@
package izapple2
import "github.com/ivanizag/izapple2/screen"
/*
Basis 108 clone
Manual: https://www.applefritter.com/files/Basis%201982%20basis%20108%20instruction%20manual.pdf
ROM: Two pages of 12 KB each. Page 0 sets the 80 column mode. Page 1 starts in 40 column mode.
Character ROM: Four pages, the inverse and flash characters are built from the normal ones. Pages:
0: Apple II characters (no lowercase)
1: German ASCII
2: ASCII (default)
3: APL symbols
Memory: Has a full 64KB extra RAM replacing both main and LC RAM. It can be mapped on 8kb blocks with
new softswitches.
Video: 80 columns are made by having a sideways static RAM.
The keyboard can generate interrupts.
Missing: second 64kb block, keyboard interrupts, Z80 emulation, Parallel an Serial.
*/
func loadBasis108Rom(a *Apple2) error {
return loadMultiPageRom(a, []string{
"<internal>/Basis108_D83_D0.BIN",
"<internal>/Basis108_D70_D8.BIN",
"<internal>/Basis108_D56_E0.BIN",
"<internal>/Basis108_D40_E8.BIN",
"<internal>/Basis108_D39_F0.BIN",
"<internal>/Basis108_D25_F8.BIN",
})
}
type videoBasis108 struct {
video
ram *memoryRangeBasis108
col80 bool
}
func newVideoBasis108(a *Apple2, ram *memoryRangeBasis108) *videoBasis108 {
var v videoBasis108
v.video = *newVideo(a)
v.ram = ram
return &v
}
// GetCurrentVideoMode returns the active video mode
func (v *videoBasis108) GetCurrentVideoMode() uint32 {
if v.col80 {
mode := screen.VideoText80AltOrder
isTextMode := v.a.io.isSoftSwitchActive(ioFlagText)
isHiResMode := v.a.io.isSoftSwitchActive(ioFlagHiRes)
if isTextMode {
mode |= screen.VideoText80
} else if isHiResMode {
mode |= screen.VideoHGR
} else {
mode |= screen.VideoDGR
}
isSecondPage := v.a.io.isSoftSwitchActive(ioFlagSecondPage)
if isSecondPage {
mode |= screen.VideoSecondPage
}
return mode
}
return v.video.GetCurrentVideoMode()
}
// GetTextMemory returns a slice to the text memory pages
func (v *videoBasis108) GetTextMemory(secondPage bool, ext bool) []uint8 {
return v.ram.getTextMemory(secondPage, ext)
}
func addBasis108SoftSwitches(io *ioC0Page, ram *memoryRangeBasis108, video *videoBasis108, cg *CharacterGenerator) {
// Character generator softswitches
io.addSoftSwitchW(0x00, buildNotImplementedSoftSwitchW(io), "BASIS108-CG-SW0-OFF") // Inverse?
io.addSoftSwitchW(0x01, buildNotImplementedSoftSwitchW(io), "BASIS108-CG-SW0-ON") // Flash?
io.addSoftSwitchW(0x02, func(_ uint8) { cg.setPage(cg.page & 0x02) }, "BASIS108-CG-SW2-OFF")
io.addSoftSwitchW(0x03, func(_ uint8) { cg.setPage(cg.page | 0x01) }, "BASIS108-CG-SW2-ON")
io.addSoftSwitchW(0x04, func(_ uint8) { cg.setPage(cg.page & 0x01) }, "BASIS108-CG-SW1-OFF")
io.addSoftSwitchW(0x05, func(_ uint8) { cg.setPage(cg.page | 0x02) }, "BASIS108-CG-SW1-ON")
io.addSoftSwitchW(0x06, buildNotImplementedSoftSwitchW(io), "BASIS108-CG-SW0-OFF")
io.addSoftSwitchW(0x07, buildNotImplementedSoftSwitchW(io), "BASIS108-CG-SW0-ON")
// Keyboard interrupts
io.addSoftSwitchW(0x08, buildNotImplementedSoftSwitchW(io), "BASIS108-KBDINT-OFF")
io.addSoftSwitchW(0x09, buildNotImplementedSoftSwitchW(io), "BASIS108-KBDINT-ON")
// 80 column softswitches
io.addSoftSwitchW(0x0A, func(_ uint8) { video.col80 = false }, "BASIS108-80COL-OFF")
io.addSoftSwitchW(0x0B, func(_ uint8) { video.col80 = true }, "BASIS108-80COL-ON")
io.addSoftSwitchW(0x0C, func(_ uint8) { ram.staticRam = false }, "BASIS108-STATICRAM-OFF")
io.addSoftSwitchW(0x0D, func(_ uint8) { ram.staticRam = true }, "BASIS108-STATICRAM-ON")
// Language card configuration
io.addSoftSwitchW(0x0E, buildNotImplementedSoftSwitchW(io), "BASIS108-LANG-ON")
io.addSoftSwitchW(0x0F, buildNotImplementedSoftSwitchW(io), "BASIS108-LANG-OFF")
// RAM bank softswitches
io.addSoftSwitchW(0x60, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM0000-BANK0")
io.addSoftSwitchW(0x61, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM0000-BANK1")
io.addSoftSwitchW(0x62, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM2000-BANK0")
io.addSoftSwitchW(0x63, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM2000-BANK1")
io.addSoftSwitchW(0x64, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM4000-BANK0")
io.addSoftSwitchW(0x65, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM4000-BANK1")
io.addSoftSwitchW(0x66, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM6000-BANK0")
io.addSoftSwitchW(0x67, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM6000-BANK1")
io.addSoftSwitchW(0x68, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM8000-BANK0")
io.addSoftSwitchW(0x69, buildNotImplementedSoftSwitchW(io), "BASIS108-RAM8000-BANK1")
io.addSoftSwitchW(0x6A, buildNotImplementedSoftSwitchW(io), "BASIS108-RAMA000-BANK0")
io.addSoftSwitchW(0x6B, buildNotImplementedSoftSwitchW(io), "BASIS108-RAMA000-BANK1")
io.addSoftSwitchW(0x6C, buildNotImplementedSoftSwitchW(io), "BASIS108-RAMD000-BANK0")
io.addSoftSwitchW(0x6D, buildNotImplementedSoftSwitchW(io), "BASIS108-RAMD000-BANK1")
io.addSoftSwitchW(0x6E, buildNotImplementedSoftSwitchW(io), "BASIS108-RAME000-BANK0")
io.addSoftSwitchW(0x6F, buildNotImplementedSoftSwitchW(io), "BASIS108-RAME000-BANK1")
}

View File

@ -29,9 +29,9 @@ func TestBrainBoardCardWozaniam(t *testing.T) {
} }
at.run() at.run()
at.terminateCondition = buildTerminateConditionText(at, "_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@", false, 100_000) at.terminateCondition = buildTerminateConditionText("_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@", testTextMode40, 100_000)
text := at.getText() text := at.getText(testTextMode40)
if !strings.Contains(text, "_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@") { if !strings.Contains(text, "_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@_@") {
t.Errorf("Expected screen filled with _@_@', got '%s'", text) t.Errorf("Expected screen filled with _@_@', got '%s'", text)
} }
@ -40,10 +40,10 @@ func TestBrainBoardCardWozaniam(t *testing.T) {
func TestBrainBoardCardIntegerBasic(t *testing.T) { func TestBrainBoardCardIntegerBasic(t *testing.T) {
at := buildBrainBoardTester(t, "brainboard,switch=down") at := buildBrainBoardTester(t, "brainboard,switch=down")
at.terminateCondition = buildTerminateConditionText(at, "APPLE ][\n>", false, 1_000_000) at.terminateCondition = buildTerminateConditionText("APPLE ][\n>", testTextMode40, 1_000_000)
at.run() at.run()
text := at.getText() text := at.getText(testTextMode40)
if !strings.Contains(text, "APPLE ][\n>") { if !strings.Contains(text, "APPLE ][\n>") {
t.Errorf("Expected APPLE ][' and '>', got '%s'", text) t.Errorf("Expected APPLE ][' and '>', got '%s'", text)
} }

View File

@ -19,10 +19,10 @@ func testCardDetectedInternal(t *testing.T, model string, card string, cycles ui
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
at.terminateCondition = buildTerminateConditionText(at, banner, true, cycles) at.terminateCondition = buildTerminateConditionText(banner, testTextMode80, cycles)
at.run() at.run()
text := at.getText80() text := at.getText(testTextMode80)
if !strings.Contains(text, banner) { if !strings.Contains(text, banner) {
t.Errorf("Expected '%s', got '%s'", banner, text) t.Errorf("Expected '%s', got '%s'", banner, text)
} }

View File

@ -195,7 +195,7 @@ func (s *cardDan2ControllerSlot) openFile() (*os.File, error) {
return nil, err return nil, err
} }
func (s *cardDan2ControllerSlot) status(unit uint8) error { func (s *cardDan2ControllerSlot) status(_ uint8) error {
file, err := s.openFile() file, err := s.openFile()
if err != nil { if err != nil {
return err return err

View File

@ -14,11 +14,11 @@ func TestDan2Controller(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
at.terminateCondition = buildTerminateConditionText(at, "NEW VOL", true, 10_000_000) at.terminateCondition = buildTerminateConditionText("NEW VOL", testTextMode40, 10_000_000)
at.run() at.run()
text := at.getText() text := at.getText(testTextMode40)
if !strings.Contains(text, "NEW VOL") { if !strings.Contains(text, "NEW VOL") {
t.Errorf("Expected Bitsy Bye screen, got '%s'", text) t.Errorf("Expected Bitsy Bye screen, got '%s'", text)
} }

View File

@ -52,10 +52,11 @@ func newCardProDOSRomCard3Builder() *cardBuilder {
} }
} }
//lint:ignore U1000 this is used to write debug code
func newCardProDOSNVRAMDriveBuilder() *cardBuilder { func newCardProDOSNVRAMDriveBuilder() *cardBuilder {
return &cardBuilder{ return &cardBuilder{
name: "ProDOS 4MB NVRAM DRive", name: "ProDOS 4MB NVRAM DRive",
description: "A bootable 4 MB NVRAM card by Ralle Palaveev", description: "A bootable 4 MB NVRAM card by Ralle Palaveev, WIP",
defaultParams: &[]paramSpec{ defaultParams: &[]paramSpec{
{"image", "ROM image with the ProDOS volume", ""}, {"image", "ROM image with the ProDOS volume", ""},
}, },

View File

@ -11,11 +11,11 @@ func TestSwyftTutorial(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
at.terminateCondition = buildTerminateConditionText(at, "HOW TO USE SWYFTCARD", true, 10_000_000) at.terminateCondition = buildTerminateConditionText("HOW TO USE SWYFTCARD", testTextMode80, 10_000_000)
at.run() at.run()
text := at.getText80() text := at.getText(testTextMode80)
if !strings.Contains(text, "HOW TO USE SWYFTCARD") { if !strings.Contains(text, "HOW TO USE SWYFTCARD") {
t.Errorf("Expected 'HOW TO USE SWYFTCARD', got '%s'", text) t.Errorf("Expected 'HOW TO USE SWYFTCARD', got '%s'", text)
} }

View File

@ -29,8 +29,9 @@ func charGenColumnsMap2e(column int) int {
} }
const ( const (
charGenPageSize2Plus = 2048 charGenPageSize2Plus = 2048
charGenPageSize2E = 2048 * 2 charGenPageSize2E = 2048 * 2
charGenPageSizeBasis108 = 1024
) )
// NewCharacterGenerator instantiates a new Character Generator with the rom on the file given // NewCharacterGenerator instantiates a new Character Generator with the rom on the file given
@ -59,9 +60,13 @@ func (cg *CharacterGenerator) load(filename string) error {
return nil return nil
} }
func (cg *CharacterGenerator) getPages() int {
return len(cg.data) / cg.pageSize
}
func (cg *CharacterGenerator) setPage(page int) { func (cg *CharacterGenerator) setPage(page int) {
// Some clones had a switch to change codepage with extra characters // Some clones had a switch to change codepage with extra characters
pages := len(cg.data) / cg.pageSize pages := cg.getPages()
cg.page = page % pages cg.page = page % pages
} }
@ -74,7 +79,8 @@ func (cg *CharacterGenerator) nextPage() {
} }
func (cg *CharacterGenerator) getPixel(char uint8, row int, column int) bool { func (cg *CharacterGenerator) getPixel(char uint8, row int, column int) bool {
bits := cg.data[int(char)*8+row+cg.page*cg.pageSize] rowPos := (int(char)*8 + row) % cg.pageSize
bits := cg.data[rowPos+cg.page*cg.pageSize]
bit := cg.columnMap(column) bit := cg.columnMap(column)
value := bits >> uint(bit) & 1 value := bits >> uint(bit) & 1
return value == 1 return value == 1
@ -87,6 +93,10 @@ func setupCharactedGenerator(a *Apple2, board string, charRomFile string) error
switch board { switch board {
case "2plus": case "2plus":
charGenMap = charGenColumnsMap2Plus charGenMap = charGenColumnsMap2Plus
case "basis108":
charGenMap = charGenColumnsMap2Plus
pageSize = charGenPageSizeBasis108
initialCharGenPage = 2
case "2e": case "2e":
charGenMap = charGenColumnsMap2e charGenMap = charGenColumnsMap2e
pageSize = charGenPageSize2E pageSize = charGenPageSize2E
@ -94,7 +104,7 @@ func setupCharactedGenerator(a *Apple2, board string, charRomFile string) error
charGenMap = charGenColumnsMapBase64a charGenMap = charGenColumnsMapBase64a
initialCharGenPage = 1 initialCharGenPage = 1
default: default:
return fmt.Errorf("board %s not supported it must be '2plus', '2e' or 'base64a'", board) return fmt.Errorf("board %s not supported it must be '2plus', '2e', 'base64a', 'basis108", board)
} }
cg, err := newCharacterGenerator(charRomFile, charGenMap, pageSize) cg, err := newCharacterGenerator(charRomFile, charGenMap, pageSize)

8
configs/basis108.cfg Normal file
View File

@ -0,0 +1,8 @@
name: Basis 108
parent: _base
board: basis108
rom: <custom>
charrom: <internal>/D29_basis_cg_2532.rom.BIN
s0: language
s3: videx
s6: diskii,disk1=<internal>/dos33.dsk

View File

@ -50,6 +50,7 @@ The available pre-configured models are:
2enh: Apple //e 2enh: Apple //e
2plus: Apple ][+ 2plus: Apple ][+
base64a: Base 64A base64a: Base 64A
basis108: Basis 108
dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x
swyft: swyft swyft: swyft

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
) )
func testBoots(t *testing.T, model string, disk string, overrides *configuration, cycles uint64, banner string, prompt string, col80 bool) { func testBoots(t *testing.T, model string, disk string, overrides *configuration, cycles uint64, banner string, prompt string, textMode testTextModeFunc) {
if overrides == nil { if overrides == nil {
overrides = newConfiguration() overrides = newConfiguration()
} }
@ -23,15 +23,10 @@ func testBoots(t *testing.T, model string, disk string, overrides *configuration
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
at.terminateCondition = buildTerminateConditionTexts(at, []string{banner, prompt}, col80, cycles) at.terminateCondition = buildTerminateConditionTexts([]string{banner, prompt}, textMode, cycles)
at.run() at.run()
var text string text := at.getText(textMode)
if col80 {
text = at.getText80()
} else {
text = at.getText()
}
if !strings.Contains(text, banner) { if !strings.Contains(text, banner) {
t.Errorf("Expected '%s', got '%s'", banner, text) t.Errorf("Expected '%s', got '%s'", banner, text)
} }
@ -42,36 +37,40 @@ func testBoots(t *testing.T, model string, disk string, overrides *configuration
} }
func TestPlusBoots(t *testing.T) { func TestPlusBoots(t *testing.T) {
testBoots(t, "2plus", "", nil, 200_000, "APPLE ][", "\n]", false) testBoots(t, "2plus", "", nil, 200_000, "APPLE ][", "\n]", testTextMode40)
} }
func Test2EBoots(t *testing.T) { func Test2EBoots(t *testing.T) {
testBoots(t, "2e", "", nil, 200_000, "Apple ][", "\n]", false) testBoots(t, "2e", "", nil, 200_000, "Apple ][", "\n]", testTextMode40)
} }
func Test2EnhancedBoots(t *testing.T) { func Test2EnhancedBoots(t *testing.T) {
testBoots(t, "2enh", "", nil, 200_000, "Apple //e", "\n]", false) testBoots(t, "2enh", "", nil, 200_000, "Apple //e", "\n]", testTextMode40)
} }
func TestBase64Boots(t *testing.T) { func TestBase64Boots(t *testing.T) {
testBoots(t, "base64a", "", nil, 1_000_000, "BASE 64A", "\n]", false) testBoots(t, "base64a", "", nil, 1_000_000, "BASE 64A", "\n]", testTextMode40)
}
func TestBasis108Boots(t *testing.T) {
testBoots(t, "basis108", "", nil, 1_000_000, "B a s i s 1 0 8", "\n]", testTextMode80AltOrder)
} }
func TestPlusDOS32Boots(t *testing.T) { func TestPlusDOS32Boots(t *testing.T) {
overrides := newConfiguration() overrides := newConfiguration()
overrides.set(confS0, "multirom,bank=7,basic=0") overrides.set(confS0, "multirom,bank=7,basic=0")
overrides.set(confS6, "diskii,sectors13,disk1=<internal>/dos32.nib") overrides.set(confS6, "diskii,sectors13,disk1=<internal>/dos32.nib")
testBoots(t, "2plus", "", overrides, 100_000_000, "MASTER DISKETTE VERSION 3.2 STANDARD", "\n>", false) testBoots(t, "2plus", "", overrides, 100_000_000, "MASTER DISKETTE VERSION 3.2 STANDARD", "\n>", testTextMode40)
} }
func TestPlusDOS33Boots(t *testing.T) { func TestPlusDOS33Boots(t *testing.T) {
testBoots(t, "2plus", "<internal>/dos33.dsk", nil, 100_000_000, "DOS VERSION 3.3", "\n]", false) testBoots(t, "2plus", "<internal>/dos33.dsk", nil, 100_000_000, "DOS VERSION 3.3", "\n]", testTextMode40)
} }
func TestProdDOSBoots(t *testing.T) { func TestProdDOSBoots(t *testing.T) {
testBoots(t, "2enh", "<internal>/ProDOS_2_4_3.po", nil, 100_000_000, "BITSY BYE", "NEW VOL", false) testBoots(t, "2enh", "<internal>/ProDOS_2_4_3.po", nil, 100_000_000, "BITSY BYE", "NEW VOL", testTextMode40)
} }
func TestCPM65Boots(t *testing.T) { func TestCPM65Boots(t *testing.T) {
testBoots(t, "2enh", "<internal>/cpm65.po", nil, 5_000_000, "CP/M-65 for the Apple II", "\nA>", true) testBoots(t, "2enh", "<internal>/cpm65.po", nil, 5_000_000, "CP/M-65 for the Apple II", "\nA>", testTextMode80)
} }

View File

@ -51,7 +51,7 @@ func (k *keyboard) putKeyAction(keyEvent *fyne.KeyEvent, press bool) {
case fyne.KeyF1: case fyne.KeyF1:
k.s.a.SendCommand(izapple2.CommandReset) k.s.a.SendCommand(izapple2.CommandReset)
case fyne.KeyF12: case fyne.KeyF12:
screen.AddScenario(k.s.a, "../../screen/test_resources/") screen.AddScenario(k.s.a.GetVideoSource(), "../../screen/test_resources/")
} }
} }
@ -128,7 +128,7 @@ func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) {
k.s.a.SendCommand(izapple2.CommandToggleCPUTrace) k.s.a.SendCommand(izapple2.CommandToggleCPUTrace)
case fyne.KeyF12: case fyne.KeyF12:
//case fyne.KeyPrintScreen: //case fyne.KeyPrintScreen:
err := screen.SaveSnapshot(k.s.a, k.s.screenMode, "snapshot.png") err := screen.SaveSnapshot(k.s.a.GetVideoSource(), k.s.screenMode, "snapshot.png")
if err != nil { if err != nil {
fmt.Printf("Error saving snapshoot: %v.\n.", err) fmt.Printf("Error saving snapshoot: %v.\n.", err)
} else { } else {

View File

@ -95,11 +95,12 @@ func fyneRun(s *state) {
case <-ticker.C: case <-ticker.C:
if !s.a.IsPaused() { if !s.a.IsPaused() {
var img *image.RGBA var img *image.RGBA
vs := s.a.GetVideoSource()
if s.showPages { if s.showPages {
img = screen.SnapshotParts(s.a, s.screenMode) img = screen.SnapshotParts(vs, s.screenMode)
s.win.SetTitle(fmt.Sprintf("%v %v %vx%v", s.a.Name, screen.VideoModeName(s.a), img.Rect.Dx()/2, img.Rect.Dy()/2)) s.win.SetTitle(fmt.Sprintf("%v %v %vx%v", s.a.Name, screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2))
} else { } else {
img = screen.Snapshot(s.a, s.screenMode) img = screen.Snapshot(vs, s.screenMode)
} }
display.Image = img display.Image = img
canvas.Refresh(display) canvas.Refresh(display)

View File

@ -35,7 +35,7 @@ func buildToolbar(s *state) *widget.Toolbar {
})) }))
tb.Append(widget.NewToolbarAction( tb.Append(widget.NewToolbarAction(
theme.NewThemedResource(resourceCameraSvg), func() { theme.NewThemedResource(resourceCameraSvg), func() {
err := screen.SaveSnapshot(s.a, s.screenMode, "snapshot.png") err := screen.SaveSnapshot(s.a.GetVideoSource(), s.screenMode, "snapshot.png")
if err != nil { if err != nil {
s.app.SendNotification(fyne.NewNotification( s.app.SendNotification(fyne.NewNotification(
s.win.Title(), s.win.Title(),

View File

@ -107,16 +107,18 @@ func sdlRun(a *izapple2.Apple2) {
if !a.IsPaused() { if !a.IsPaused() {
var img *image.RGBA var img *image.RGBA
vs := a.GetVideoSource()
if kp.showHelp { if kp.showHelp {
img = screen.SnapshotMessageGenerator(a, helpMessage) img = screen.SnapshotMessageGenerator(vs, helpMessage)
} else if kp.showCharGen { } else if kp.showCharGen {
img = screen.SnapshotCharacterGenerator(a, kp.showAltText) cgPage, cgPages := a.GetCgPageInfo()
window.SetTitle(fmt.Sprintf("%v character map", a.Name)) img = screen.SnapshotCharacterGenerator(vs, kp.showAltText)
window.SetTitle(fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages))
} else if kp.showPages { } else if kp.showPages {
img = screen.SnapshotParts(a, kp.screenMode) img = screen.SnapshotParts(vs, kp.screenMode)
window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(a), img.Rect.Dx()/2, img.Rect.Dy()/2)) window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2))
} else { } else {
img = screen.Snapshot(a, kp.screenMode) img = screen.Snapshot(vs, kp.screenMode)
} }
if img != nil { if img != nil {
surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]), surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]),

View File

@ -129,9 +129,9 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
fallthrough fallthrough
case sdl.K_PRINTSCREEN: case sdl.K_PRINTSCREEN:
if ctrl { if ctrl {
screen.AddScenario(k.a, "../../screen/test_resources/") screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/")
} else { } else {
err := screen.SaveSnapshot(k.a, screen.ScreenModeNTSC, "snapshot.png") err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeNTSC, "snapshot.png")
if err != nil { if err != nil {
fmt.Printf("Error saving snapshoot: %v.\n.", err) fmt.Printf("Error saving snapshoot: %v.\n.", err)
} else { } else {

View File

@ -97,7 +97,7 @@ func main() {
// Old: // Old:
case "png": case "png":
err := screen.SaveSnapshot(a, screen.ScreenModeNTSC, "snapshot.png") err := screen.SaveSnapshot(a.GetVideoSource(), screen.ScreenModeNTSC, "snapshot.png")
if err != nil { if err != nil {
fmt.Printf("Error saving screen: %v.\n.", err) fmt.Printf("Error saving screen: %v.\n.", err)
} else { } else {
@ -105,7 +105,7 @@ func main() {
} }
case "pngm": case "pngm":
err := screen.SaveSnapshot(a, screen.ScreenModePlain, "snapshot.png") err := screen.SaveSnapshot(a.GetVideoSource(), screen.ScreenModePlain, "snapshot.png")
if err != nil { if err != nil {
fmt.Printf("Error saving screen: %v.\n.", err) fmt.Printf("Error saving screen: %v.\n.", err)
} else { } else {
@ -187,14 +187,14 @@ func SaveGif(a *izapple2.Apple2, filename string) error {
planned := time.Now() planned := time.Now()
for i := 0; i < frames; i++ { for i := 0; i < frames; i++ {
lapse := planned.Sub(time.Now()) lapse := time.Until(planned)
fmt.Printf("%v\n", lapse) fmt.Printf("%v\n", lapse)
if lapse > 0 { if lapse > 0 {
time.Sleep(lapse) time.Sleep(lapse)
} }
fmt.Printf("%v\n", time.Now()) fmt.Printf("%v\n", time.Now())
img := screen.SnapshotPaletted(a, screen.ScreenModeNTSC) img := screen.SnapshotPaletted(a.GetVideoSource(), screen.ScreenModeNTSC)
animation.Image = append(animation.Image, img) animation.Image = append(animation.Image, img)
animation.Delay = append(animation.Delay, delayHundredsS) animation.Delay = append(animation.Delay, delayHundredsS)

View File

@ -9,7 +9,7 @@ type memoryManager struct {
apple2 *Apple2 apple2 *Apple2
// Main RAM area: 0x0000 to 0xbfff // Main RAM area: 0x0000 to 0xbfff
physicalMainRAM *memoryRange // 0x0000 to 0xbfff, Up to 48 Kb physicalMainRAM memoryRangeHandler // 0x0000 to 0xbfff, Up to 48 Kb
// Slots area: 0xc000 to 0xcfff // Slots area: 0xc000 to 0xcfff
cardsROM [8]memoryHandler //0xcs00 to 0xcSff. 256 bytes for each card cardsROM [8]memoryHandler //0xcs00 to 0xcSff. 256 bytes for each card
@ -70,13 +70,15 @@ type memoryHandler interface {
poke(uint16, uint8) poke(uint16, uint8)
} }
type memoryRangeHandler interface {
memoryHandler
subRange(a, b uint16) []uint8
}
func newMemoryManager(a *Apple2) *memoryManager { func newMemoryManager(a *Apple2) *memoryManager {
var mmu memoryManager var mmu memoryManager
mmu.apple2 = a mmu.apple2 = a
mmu.physicalMainRAM = newMemoryRange(0, make([]uint8, 0xc000), "Main RAM")
mmu.slotC3ROMActive = true // For II+, this is the default behaviour mmu.slotC3ROMActive = true // For II+, this is the default behaviour
return &mmu return &mmu
} }
@ -147,7 +149,7 @@ func (mmu *memoryManager) getPhysicalMainRAM(ext bool) memoryHandler {
return mmu.physicalMainRAM return mmu.physicalMainRAM
} }
func (mmu *memoryManager) getVideoRAM(ext bool) *memoryRange { func (mmu *memoryManager) getVideoRAM(ext bool) memoryRangeHandler {
if ext && mmu.hasExtendedRAM() { if ext && mmu.hasExtendedRAM() {
// The video memory uses the first extended RAM block, even with RAMWorks // The video memory uses the first extended RAM block, even with RAMWorks
return mmu.physicalExtRAM[0] return mmu.physicalExtRAM[0]
@ -236,7 +238,7 @@ func (mmu *memoryManager) peekWord(address uint16) uint16 {
func (mmu *memoryManager) Peek(address uint16) uint8 { func (mmu *memoryManager) Peek(address uint16) uint8 {
mh := mmu.accessRead(address) mh := mmu.accessRead(address)
if mh == nil { if mh == nil {
return 0xf4 // Or some random number return uint8(address) // Or some random number
} }
value := mh.peek(address) value := mh.peek(address)
//if address >= 0xc400 && address < 0xc500 { //if address >= 0xc400 && address < 0xc500 {
@ -310,6 +312,15 @@ func (mmu *memoryManager) initLanguageRAM(groups uint8) {
} }
} }
func (mmu *memoryManager) initMainRAM() {
// Apple II+ main RAM
mmu.physicalMainRAM = newMemoryRange(0, make([]uint8, 0xc000), "Main RAM")
}
func (mmu *memoryManager) initCustomRAM(customRam memoryRangeHandler) {
mmu.physicalMainRAM = customRam
}
func (mmu *memoryManager) initExtendedRAM(groups int) { func (mmu *memoryManager) initExtendedRAM(groups int) {
// Apple IIe 80 col card with 64Kb style RAM or RAMWorks (up to 256 banks) // Apple IIe 80 col card with 64Kb style RAM or RAMWorks (up to 256 banks)
mmu.physicalExtRAM = make([]*memoryRange, groups) mmu.physicalExtRAM = make([]*memoryRange, groups)

View File

@ -66,7 +66,12 @@ func (m *memoryRangeROM) getPage() uint8 {
} }
func (m *memoryRangeROM) peek(address uint16) uint8 { func (m *memoryRangeROM) peek(address uint16) uint8 {
return m.data[address-m.base+m.pageOffset] pos := address - m.base + m.pageOffset
if pos >= uint16(len(m.data)) {
return uint8(address) // Non existent memory
}
return m.data[pos]
} }
func (m *memoryRangeROM) poke(address uint16, value uint8) { func (m *memoryRangeROM) poke(address uint16, value uint8) {
// Ignore // Ignore
@ -81,7 +86,7 @@ func identifyMemory(m memoryHandler) string {
rom, ok := m.(*memoryRangeROM) rom, ok := m.(*memoryRangeROM)
if ok { if ok {
return fmt.Sprintf("ROM 0x%04x %s", rom.base, ram.name) return fmt.Sprintf("ROM 0x%04x %s", rom.base, rom.name)
} }
return ("Unknown memory") return ("Unknown memory")

73
memoryRangeBasis108.go Normal file
View File

@ -0,0 +1,73 @@
package izapple2
/*
The Basis 108 clone has 128kb of RAM plus 2KB of static RAM at $0400 for 80 columns text.
*/
type memoryRangeBasis108 struct {
dataMain []uint8
dataAux []uint8
dataStatic []uint8
name string
staticRam bool
auxRam bool
}
func newMemoryRangeBasis108() *memoryRangeBasis108 {
var m memoryRangeBasis108
m.dataMain = make([]uint8, 48*1024)
m.dataAux = make([]uint8, 48*1024)
m.dataStatic = make([]uint8, 0xc000-0x0400)
// How is the static RAM initialized?
for i := 0; i < len(m.dataStatic); i++ {
m.dataStatic[i] = ' ' + 0x80
}
m.name = "Basis 108 RAM"
return &m
}
func (m *memoryRangeBasis108) peek(address uint16) uint8 {
if m.staticRam && address >= 0x0400 && address < 0x0c00 {
return m.dataStatic[address-0x0400]
}
if m.auxRam {
return m.dataAux[address]
}
return m.dataMain[address]
}
func (m *memoryRangeBasis108) poke(address uint16, value uint8) {
if m.staticRam && address >= 0x0400 && address < 0x0c00 {
m.dataStatic[address-0x0400] = value
} else if m.auxRam {
m.dataAux[address] = value
} else {
m.dataMain[address] = value
}
}
func (m *memoryRangeBasis108) subRange(a, b uint16) []uint8 {
if m.staticRam && a >= 0x0400 && b < 0x0c00 {
return m.dataStatic[a-0x0400 : b-0x0400]
}
if m.auxRam {
return m.dataAux[a:b]
}
return m.dataMain[a:b]
}
func (m *memoryRangeBasis108) getTextMemory(secondPage bool, ext bool) []uint8 {
addressStart := textPage1Address
if secondPage {
addressStart = textPage2Address
}
if ext {
return m.dataStatic[addressStart-0x0400 : addressStart-0x0400+textPageSize]
}
return m.dataMain[addressStart : addressStart+textPageSize]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -44,7 +44,7 @@ func snapshotLoRes(vs VideoSource, isSecondPage bool, light color.Color) *image.
} }
func snapshotMeRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { func snapshotMeRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA {
data := getText80FromMemory(vs, isSecondPage) data := getText80FromMemory(vs, isSecondPage, false)
return renderGr(data, true /*isMeres*/, light) return renderGr(data, true /*isMeres*/, light)
} }

View File

@ -46,13 +46,14 @@ func SnapshotPaletted(vs VideoSource, screenMode int) *image.Paletted {
// See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays // See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays
var greenPhosphorColor = color.RGBA{65, 255, 0, 255} var greenPhosphorColor = color.RGBA{65, 255, 0, 255}
func snapshotByMode(vs VideoSource, videoMode uint16, screenMode int) *image.RGBA { func snapshotByMode(vs VideoSource, videoMode uint32, screenMode int) *image.RGBA {
videoBase := videoMode & VideoBaseMask videoBase := videoMode & VideoBaseMask
mixMode := videoMode & VideoMixTextMask mixMode := videoMode & VideoMixTextMask
isSecondPage := (videoMode & VideoSecondPage) != 0 isSecondPage := (videoMode & VideoSecondPage) != 0
isAltText := (videoMode & VideoAltText) != 0 isAltText := (videoMode & VideoAltText) != 0
isRGBCard := (videoMode & VideoRGBCard) != 0 isRGBCard := (videoMode & VideoRGBCard) != 0
shiftSupported := (videoMode & VideoFourColors) == 0 shiftSupported := (videoMode & VideoFourColors) == 0
hasAltOrder := (videoMode & VideoText80AltOrder) != 0
var lightColor color.Color = color.White var lightColor color.Color = color.White
if screenMode == ScreenModeGreen { if screenMode == ScreenModeGreen {
@ -67,7 +68,7 @@ func snapshotByMode(vs VideoSource, videoMode uint16, screenMode int) *image.RGB
snap = snapshotText40(vs, isSecondPage, isAltText, lightColor) snap = snapshotText40(vs, isSecondPage, isAltText, lightColor)
applyNTSCFilter = false applyNTSCFilter = false
case VideoText80: case VideoText80:
snap = snapshotText80(vs, isSecondPage, isAltText, lightColor) snap = snapshotText80(vs, isSecondPage, isAltText, hasAltOrder, lightColor)
applyNTSCFilter = false applyNTSCFilter = false
case VideoText40RGB: case VideoText40RGB:
snap = snapshotText40RGB(vs, isSecondPage, isAltText) snap = snapshotText40RGB(vs, isSecondPage, isAltText)
@ -106,7 +107,7 @@ func snapshotByMode(vs VideoSource, videoMode uint16, screenMode int) *image.RGB
case VideoMixText40: case VideoMixText40:
bottom = snapshotText40(vs, isSecondPage, isAltText, lightColor) bottom = snapshotText40(vs, isSecondPage, isAltText, lightColor)
case VideoMixText80: case VideoMixText80:
bottom = snapshotText80(vs, isSecondPage, isAltText, lightColor) bottom = snapshotText80(vs, isSecondPage, isAltText, hasAltOrder, lightColor)
case VideoMixText40RGB: case VideoMixText40RGB:
bottom = snapshotText40RGB(vs, isSecondPage, isAltText) bottom = snapshotText40RGB(vs, isSecondPage, isAltText)
applyNTSCFilter = false applyNTSCFilter = false

View File

@ -12,7 +12,7 @@ import (
// TestScenario is the computer video state // TestScenario is the computer video state
type TestScenario struct { type TestScenario struct {
VideoMode uint16 `json:"mode"` VideoMode uint32 `json:"mode"`
VideoModeName string `json:"name"` VideoModeName string `json:"name"`
ScreenModes []int `json:"screens"` ScreenModes []int `json:"screens"`
TextPages [4][]uint8 `json:"text"` TextPages [4][]uint8 `json:"text"`
@ -89,7 +89,7 @@ func (ts *TestScenario) save(dir string) (string, error) {
} }
// GetCurrentVideoMode returns the active video mode // GetCurrentVideoMode returns the active video mode
func (ts *TestScenario) GetCurrentVideoMode() uint16 { func (ts *TestScenario) GetCurrentVideoMode() uint32 {
return ts.VideoMode return ts.VideoMode
} }

View File

@ -18,8 +18,8 @@ func snapshotText40(vs VideoSource, isSecondPage bool, isAltText bool, light col
return renderText(vs, text, isAltText, nil /*colorMap*/, light) return renderText(vs, text, isAltText, nil /*colorMap*/, light)
} }
func snapshotText80(vs VideoSource, isSecondPage bool, isAltText bool, light color.Color) *image.RGBA { func snapshotText80(vs VideoSource, isSecondPage bool, isAltText bool, hasAltOrder bool, light color.Color) *image.RGBA {
text := getText80FromMemory(vs, isSecondPage) text := getText80FromMemory(vs, isSecondPage, hasAltOrder)
return renderText(vs, text, isAltText, nil /*colorMap*/, light) return renderText(vs, text, isAltText, nil /*colorMap*/, light)
} }
@ -34,10 +34,16 @@ func snapshotText40RGBColors(vs VideoSource, isSecondPage bool) *image.RGBA {
return renderText(vs, nil /*text*/, false, colorMap, nil) return renderText(vs, nil /*text*/, false, colorMap, nil)
} }
func getText80FromMemory(vs VideoSource, isSecondPage bool) []uint8 { func getText80FromMemory(vs VideoSource, isSecondPage bool, hasAltOrder bool) []uint8 {
text40Columns := getTextFromMemory(vs, isSecondPage, false) text40Columns := getTextFromMemory(vs, isSecondPage, false)
text40ColumnsAlt := getTextFromMemory(vs, isSecondPage, true) text40ColumnsAlt := getTextFromMemory(vs, isSecondPage, true)
if hasAltOrder {
tmp := text40ColumnsAlt
text40ColumnsAlt = text40Columns
text40Columns = tmp
}
// Merge the two 40 cols to return 80 cols // Merge the two 40 cols to return 80 cols
text80Columns := make([]uint8, 2*len(text40Columns)) text80Columns := make([]uint8, 2*len(text40Columns))
for i := 0; i < len(text40Columns); i++ { for i := 0; i < len(text40Columns); i++ {

View File

@ -6,15 +6,11 @@ import (
) )
// RenderTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash // RenderTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash
func RenderTextModeAnsi(vs VideoSource, is80Columns bool, isSecondPage bool, isAltText bool, isApple2e bool) string { func RenderTextModeAnsi(vs VideoSource, is80Columns bool, isSecondPage bool, isAltText bool, supportsLowercase bool, hasAltOrder bool) string {
//func DumpTextModeAnsi(a *Apple2) string {
// is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
// isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
// isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar)
var text []uint8 var text []uint8
if is80Columns { if is80Columns {
text = getText80FromMemory(vs, isSecondPage) text = getText80FromMemory(vs, isSecondPage, hasAltOrder)
} else { } else {
text = getTextFromMemory(vs, isSecondPage, false) text = getTextFromMemory(vs, isSecondPage, false)
} }
@ -26,7 +22,7 @@ func RenderTextModeAnsi(vs VideoSource, is80Columns bool, isSecondPage bool, isA
line := "" line := ""
for c := 0; c < columns; c++ { for c := 0; c < columns; c++ {
char := text[l*columns+c] char := text[l*columns+c]
line += textMemoryByteToString(char, isAltText, isApple2e, true) line += textMemoryByteToString(char, isAltText, supportsLowercase, true)
} }
content += fmt.Sprintf("# %v #\n", line) content += fmt.Sprintf("# %v #\n", line)
} }
@ -49,7 +45,7 @@ $e0-$ff Low Nor Low Nor Low Nor Low Nor
---------------------------------------------------- ----------------------------------------------------
*/ */
func textMemoryByteToString(value uint8, isAltCharSet bool, isApple2e bool, ansi bool) string { func textMemoryByteToString(value uint8, isAltCharSet bool, supportsLowercase bool, ansi bool) string {
// Normal, inverse or flash // Normal, inverse or flash
topBits := value >> 6 topBits := value >> 6
isInverse := topBits == 0 isInverse := topBits == 0
@ -61,8 +57,8 @@ func textMemoryByteToString(value uint8, isAltCharSet bool, isApple2e bool, ansi
// Move blocks // Move blocks
value = value & 0x7f value = value & 0x7f
if !isApple2e { if !supportsLowercase {
// No uppercase // No lowercase
value = value & 0x3f value = value & 0x3f
} }
if isFlash || isInverse && !isAltCharSet { if isFlash || isInverse && !isAltCharSet {

View File

@ -6,11 +6,11 @@ import (
) )
// RenderTextModeString returns the text mode contents ignoring reverse and flash // RenderTextModeString returns the text mode contents ignoring reverse and flash
func RenderTextModeString(vs VideoSource, is80Columns bool, isSecondPage bool, isAltText bool, isApple2e bool) string { func RenderTextModeString(vs VideoSource, is80Columns bool, isSecondPage bool, isAltText bool, supportsLowercase bool, hasAltOrder bool) string {
var text []uint8 var text []uint8
if is80Columns { if is80Columns {
text = getText80FromMemory(vs, isSecondPage) text = getText80FromMemory(vs, isSecondPage, hasAltOrder)
} else { } else {
text = getTextFromMemory(vs, isSecondPage, false) text = getTextFromMemory(vs, isSecondPage, false)
} }
@ -21,7 +21,7 @@ func RenderTextModeString(vs VideoSource, is80Columns bool, isSecondPage bool, i
line := "" line := ""
for c := 0; c < columns; c++ { for c := 0; c < columns; c++ {
char := text[l*columns+c] char := text[l*columns+c]
line += textMemoryByteToString(char, isAltText, isApple2e, false) line += textMemoryByteToString(char, isAltText, supportsLowercase, false)
} }
line = strings.TrimRight(line, " ") line = strings.TrimRight(line, " ")
content += fmt.Sprintf("%v\n", line) content += fmt.Sprintf("%v\n", line)

View File

@ -7,42 +7,43 @@ import (
// Base Video Modes // Base Video Modes
const ( const (
VideoBaseMask uint16 = 0x1f VideoBaseMask uint32 = 0x1f
VideoText40 uint16 = 0x01 VideoText40 uint32 = 0x01
VideoGR uint16 = 0x02 VideoGR uint32 = 0x02
VideoHGR uint16 = 0x03 VideoHGR uint32 = 0x03
VideoText80 uint16 = 0x08 VideoText80 uint32 = 0x08
VideoDGR uint16 = 0x09 VideoDGR uint32 = 0x09
VideoDHGR uint16 = 0x0a VideoDHGR uint32 = 0x0a
VideoText40RGB uint16 = 0x10 VideoText40RGB uint32 = 0x10
VideoMono560 uint16 = 0x11 VideoMono560 uint32 = 0x11
VideoRGBMix uint16 = 0x12 VideoRGBMix uint32 = 0x12
VideoRGB160 uint16 = 0x13 VideoRGB160 uint32 = 0x13
VideoSHR uint16 = 0x14 VideoSHR uint32 = 0x14
VideoVidex uint16 = 0x15 VideoVidex uint32 = 0x15
) )
// Mix text video mdes modifiers // Mix text video mdes modifiers
const ( const (
VideoMixTextMask uint16 = 0x0f00 VideoMixTextMask uint32 = 0x0f00
VideoMixText40 uint16 = 0x0100 VideoMixText40 uint32 = 0x0100
VideoMixText80 uint16 = 0x0200 VideoMixText80 uint32 = 0x0200
VideoMixText40RGB uint16 = 0x0300 VideoMixText40RGB uint32 = 0x0300
) )
// Other video mode modifiers // Other video mode modifiers
const ( const (
VideoModifiersMask uint16 = 0xf000 VideoModifiersMask uint32 = 0xf000
VideoSecondPage uint16 = 0x1000 VideoSecondPage uint32 = 0x1000
VideoAltText uint16 = 0x2000 VideoAltText uint32 = 0x2000
VideoRGBCard uint16 = 0x4000 VideoRGBCard uint32 = 0x4000
VideoFourColors uint16 = 0x8000 VideoFourColors uint32 = 0x8000
VideoText80AltOrder uint32 = 0x10000
) )
// VideoSource provides the info to build the video output // VideoSource provides the info to build the video output
type VideoSource interface { type VideoSource interface {
// GetCurrentVideoMode returns the active video mode // GetCurrentVideoMode returns the active video mode
GetCurrentVideoMode() uint16 GetCurrentVideoMode() uint32
// GetTextMemory returns a slice to the text memory pages // GetTextMemory returns a slice to the text memory pages
GetTextMemory(secondPage bool, ext bool) []uint8 GetTextMemory(secondPage bool, ext bool) []uint8
// GetVideoMemory returns a slice to the video memory pages // GetVideoMemory returns a slice to the video memory pages

130
setup.go
View File

@ -9,23 +9,45 @@ import (
) )
func configure(configuration *configuration) (*Apple2, error) { func configure(configuration *configuration) (*Apple2, error) {
a := newApple2() var a Apple2
a.Name = configuration.get(confName) a.Name = configuration.get(confName)
a.mmu = newMemoryManager(&a)
a.video = newVideo(&a)
a.io = newIoC0Page(&a)
a.commandChannel = make(chan command, 100)
// Configure the board // Configure the board
board := configuration.get(confBoard) board := configuration.get(confBoard)
a.board = board a.board = board
a.isApple2e = board == "2e"
err := setupCharactedGenerator(&a, board, configuration.get(confCharRom))
if err != nil {
return nil, err
}
addApple2SoftSwitches(a.io) addApple2SoftSwitches(a.io)
if a.isApple2e { switch board {
a.hasLowerCase = true case "2plus":
a.mmu.initMainRAM()
case "2e":
a.isApple2e = true
a.mmu.initMainRAM()
a.mmu.initExtendedRAM(1) a.mmu.initExtendedRAM(1)
a.hasLowerCase = true
addApple2ESoftSwitches(a.io) addApple2ESoftSwitches(a.io)
} case "base64a":
if board == "base64a" { a.mmu.initMainRAM()
a.hasLowerCase = true a.hasLowerCase = true
addBase64aSoftSwitches(a.io) addBase64aSoftSwitches(a.io)
case "basis108":
memBasis108 := newMemoryRangeBasis108()
videoBasis108 := newVideoBasis108(&a, memBasis108)
a.mmu.initCustomRAM(memBasis108)
a.video = videoBasis108
a.hasLowerCase = true
addBasis108SoftSwitches(a.io, memBasis108, videoBasis108, a.cg)
default:
return nil, fmt.Errorf("board %s not supported it must be '2plus', '2e', 'base64a', 'basis108", board)
} }
cpu := configuration.get(confCpu) cpu := configuration.get(confCpu)
@ -36,12 +58,7 @@ func configure(configuration *configuration) (*Apple2, error) {
a.cpu = iz6502.NewCMOS65c02(a.mmu) a.cpu = iz6502.NewCMOS65c02(a.mmu)
} }
err := a.loadRom(configuration.get(confRom)) err = a.loadRom(configuration.get(confRom))
if err != nil {
return nil, err
}
err = setupCharactedGenerator(a, board, configuration.get(confCharRom))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,7 +75,7 @@ func configure(configuration *configuration) (*Apple2, error) {
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
cardConfig := configuration.get(fmt.Sprintf("s%v", i)) cardConfig := configuration.get(fmt.Sprintf("s%v", i))
if cardConfig != "" { if cardConfig != "" {
_, err := setupCard(a, i, cardConfig) _, err := setupCard(&a, i, cardConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -81,48 +98,37 @@ func configure(configuration *configuration) (*Apple2, error) {
// Add optional accesories including the aux slot // Add optional accesories including the aux slot
ramWorksSize := configuration.get(confRamworks) ramWorksSize := configuration.get(confRamworks)
if ramWorksSize != "" && ramWorksSize != "none" { if ramWorksSize != "" && ramWorksSize != "none" {
err = setupRAMWorksCard(a, ramWorksSize) err = setupRAMWorksCard(&a, ramWorksSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if configuration.getFlag(confRgb) { if configuration.getFlag(confRgb) {
setupRGBCard(a) setupRGBCard(&a)
} }
nsc := configuration.get(confNsc) nsc := configuration.get(confNsc)
if nsc != "none" && nsc != "" { if nsc != "none" && nsc != "" {
err = setupNoSlotClock(a, nsc) err = setupNoSlotClock(&a, nsc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if configuration.getFlag(confRomx) { if configuration.getFlag(confRomx) {
err := setupRomX(a) err := setupRomX(&a)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
err = setupTracers(a, configuration.get(confTrace)) err = setupTracers(&a, configuration.get(confTrace))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return a, nil return &a, nil
}
func newApple2() *Apple2 {
var a Apple2
a.Name = "Pending"
a.mmu = newMemoryManager(&a)
a.io = newIoC0Page(&a)
a.commandChannel = make(chan command, 100)
return &a
} }
func (a *Apple2) setClockSpeed(speed string) error { func (a *Apple2) setClockSpeed(speed string) error {
@ -152,10 +158,15 @@ func (a *Apple2) SetForceCaps(value bool) {
} }
func (a *Apple2) loadRom(filename string) error { func (a *Apple2) loadRom(filename string) error {
if a.board == "base64a" && filename == "<custom>" { if filename == "<custom>" {
// The ROM of the base64a has several file and pages switch a.board {
loadBase64aRom(a) case "base64a":
return nil return loadBase64aRom(a)
case "basis108":
return loadBasis108Rom(a)
default:
return fmt.Errorf("no custom ROM defined for board %s", a.board)
}
} }
data, _, err := LoadResource(filename) data, _, err := LoadResource(filename)
@ -170,6 +181,57 @@ func (a *Apple2) loadRom(filename string) error {
return nil return nil
} }
const pagedRomChipWindowSize = 0x800 // 2 KB
const pagedRomChipCount = 6 // There has to be six ROM chips
const pagedRomWindowSize = pagedRomChipWindowSize * pagedRomChipCount // To cover 0xd000 to 0xffff
func loadMultiPageRom(a *Apple2, filenames []string) error {
if len(filenames) != pagedRomChipCount {
return fmt.Errorf("expected %d ROM files, got %d", pagedRomChipCount, len(filenames))
}
// Load the 6 PROM dumps
proms := make([][]uint8, pagedRomChipCount)
banks := 1
for i, filename := range filenames {
var err error
proms[i], _, err = LoadResource(filename)
if err != nil {
return err
}
pages := len(proms[i]) / pagedRomChipWindowSize
if pages > banks {
banks = pages
}
}
// Init the array of banks
romBanksBytes := make([][]uint8, banks)
for bank := range romBanksBytes {
romBanksBytes[bank] = make([]uint8, 0, pagedRomWindowSize)
}
// Distribute the per chip banks on the full rom banks
for _, romData := range proms {
for bank := range romBanksBytes {
start := (bank * pagedRomChipWindowSize) % len(romData)
romBanksBytes[bank] = append(romBanksBytes[bank], romData[start:start+pagedRomChipWindowSize]...)
}
}
// Create paged ROM
romData := make([]uint8, 0, pagedRomWindowSize*banks)
for _, bank := range romBanksBytes {
romData = append(romData, bank...)
}
rom := newMemoryRangePagedROM(0xd000, romData, "Multipage main ROM", uint8(banks))
// Start with first bank active
rom.setPage(0)
a.mmu.physicalROM = rom
return nil
}
// CreateConfiguredApple is a device independent main. Video, keyboard and speaker won't be defined // CreateConfiguredApple is a device independent main. Video, keyboard and speaker won't be defined
func CreateConfiguredApple() (*Apple2, error) { func CreateConfiguredApple() (*Apple2, error) {
// Get configuration from defaults and the command line // Get configuration from defaults and the command line

View File

@ -18,28 +18,38 @@ const (
shResPageSize = uint16(0x8000) shResPageSize = uint16(0x8000)
) )
// GetCurrentVideoMode returns the active video mode type video struct {
func (a *Apple2) GetCurrentVideoMode() uint16 { a *Apple2
isTextMode := a.io.isSoftSwitchActive(ioFlagText) }
isHiResMode := a.io.isSoftSwitchActive(ioFlagHiRes)
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
isStore80Active := a.mmu.store80Active
isDoubleResMode := !isTextMode && is80Columns && !a.io.isSoftSwitchActive(ioFlagAnnunciator3)
isSuperHighResMode := a.io.isSoftSwitchActive(ioDataNewVideo)
isVidex := a.softVideoSwitch.isActive()
isRGBCard := a.io.isSoftSwitchActive(ioFlagRGBCardActive) var _ screen.VideoSource = (*video)(nil)
rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard)
rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard) func newVideo(a *Apple2) *video {
return &video{a}
}
// GetCurrentVideoMode returns the active video mode
func (v *video) GetCurrentVideoMode() uint32 {
isTextMode := v.a.io.isSoftSwitchActive(ioFlagText)
isHiResMode := v.a.io.isSoftSwitchActive(ioFlagHiRes)
is80Columns := v.a.io.isSoftSwitchActive(ioFlag80Col)
isStore80Active := v.a.mmu.store80Active
isDoubleResMode := !isTextMode && is80Columns && !v.a.io.isSoftSwitchActive(ioFlagAnnunciator3)
isSuperHighResMode := v.a.io.isSoftSwitchActive(ioDataNewVideo)
isVidex := v.a.softVideoSwitch.isActive()
isRGBCard := v.a.io.isSoftSwitchActive(ioFlagRGBCardActive)
rgbFlag1 := v.a.io.isSoftSwitchActive(ioFlag1RGBCard)
rgbFlag2 := v.a.io.isSoftSwitchActive(ioFlag2RGBCard)
isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2 isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2
isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2 isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2
isRGB160Mode := isDoubleResMode && rgbFlag1 && !rgbFlag2 isRGB160Mode := isDoubleResMode && rgbFlag1 && !rgbFlag2
isMixMode := a.io.isSoftSwitchActive(ioFlagMixed) isMixMode := v.a.io.isSoftSwitchActive(ioFlagMixed)
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active isSecondPage := v.a.io.isSoftSwitchActive(ioFlagSecondPage) && !v.a.mmu.store80Active
isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar) isAltText := v.a.isApple2e && v.a.io.isSoftSwitchActive(ioFlagAltChar)
var mode uint16 var mode uint32
if isSuperHighResMode { if isSuperHighResMode {
mode = screen.VideoSHR mode = screen.VideoSHR
isMixMode = false isMixMode = false
@ -92,7 +102,7 @@ func (a *Apple2) GetCurrentVideoMode() uint16 {
if isRGBCard { if isRGBCard {
mode |= screen.VideoRGBCard mode |= screen.VideoRGBCard
} }
if a.isFourColors { if v.a.isFourColors {
mode |= screen.VideoFourColors mode |= screen.VideoFourColors
} }
@ -100,8 +110,8 @@ func (a *Apple2) GetCurrentVideoMode() uint16 {
} }
// GetTextMemory returns a slice to the text memory pages // GetTextMemory returns a slice to the text memory pages
func (a *Apple2) GetTextMemory(secondPage bool, ext bool) []uint8 { func (v *video) GetTextMemory(secondPage bool, ext bool) []uint8 {
mem := a.mmu.getVideoRAM(ext) mem := v.a.mmu.getVideoRAM(ext)
addressStart := textPage1Address addressStart := textPage1Address
if secondPage { if secondPage {
addressStart = textPage2Address addressStart = textPage2Address
@ -110,8 +120,8 @@ func (a *Apple2) GetTextMemory(secondPage bool, ext bool) []uint8 {
} }
// GetVideoMemory returns a slice to the video memory pages // GetVideoMemory returns a slice to the video memory pages
func (a *Apple2) GetVideoMemory(secondPage bool, ext bool) []uint8 { func (v *video) GetVideoMemory(secondPage bool, ext bool) []uint8 {
mem := a.mmu.getVideoRAM(ext) mem := v.a.mmu.getVideoRAM(ext)
addressStart := hiResPage1Address addressStart := hiResPage1Address
if secondPage { if secondPage {
addressStart = hiResPage2Address addressStart = hiResPage2Address
@ -120,15 +130,15 @@ func (a *Apple2) GetVideoMemory(secondPage bool, ext bool) []uint8 {
} }
// GetSuperVideoMemory returns a slice to the SHR video memory // GetSuperVideoMemory returns a slice to the SHR video memory
func (a *Apple2) GetSuperVideoMemory() []uint8 { func (v *video) GetSuperVideoMemory() []uint8 {
mem := a.mmu.getVideoRAM(true) mem := v.a.mmu.getVideoRAM(true)
return mem.subRange(shResPageAddress, shResPageAddress+shResPageSize) return mem.subRange(shResPageAddress, shResPageAddress+shResPageSize)
} }
// GetCharacterPixel returns the pixel as output by the character generator // GetCharacterPixel returns the pixel as output by the character generator
func (a *Apple2) GetCharacterPixel(char uint8, rowInChar int, colInChar int, isAltText bool, isFlashedFrame bool) bool { func (v *video) GetCharacterPixel(char uint8, rowInChar int, colInChar int, isAltText bool, isFlashedFrame bool) bool {
var pixel bool var pixel bool
if a.isApple2e { if v.a.isApple2e {
vid6 := (char & 0x40) != 0 vid6 := (char & 0x40) != 0
vid7 := (char & 0x80) != 0 vid7 := (char & 0x80) != 0
char := char & 0x3f char := char & 0x3f
@ -138,9 +148,9 @@ func (a *Apple2) GetCharacterPixel(char uint8, rowInChar int, colInChar int, isA
if vid7 || (vid6 && isFlashedFrame && !isAltText) { if vid7 || (vid6 && isFlashedFrame && !isAltText) {
char += 0x80 char += 0x80
} }
pixel = !a.cg.getPixel(char, rowInChar, colInChar) pixel = !v.a.cg.getPixel(char, rowInChar, colInChar)
} else { } else {
pixel = a.cg.getPixel(char, rowInChar, colInChar) pixel = v.a.cg.getPixel(char, rowInChar, colInChar)
topBits := char >> 6 topBits := char >> 6
isInverse := topBits == 0 isInverse := topBits == 0
isFlash := topBits == 1 isFlash := topBits == 1
@ -151,13 +161,13 @@ func (a *Apple2) GetCharacterPixel(char uint8, rowInChar int, colInChar int, isA
} }
// GetCardImage returns an image provided by a card, like the videx card // GetCardImage returns an image provided by a card, like the videx card
func (a *Apple2) GetCardImage(light color.Color) *image.RGBA { func (v *video) GetCardImage(light color.Color) *image.RGBA {
return a.softVideoSwitch.BuildAlternateImage(light) return v.a.softVideoSwitch.BuildAlternateImage(light)
} }
// SupportsLowercase returns true if the video source supports lowercase // SupportsLowercase returns true if the video source supports lowercase
func (a *Apple2) SupportsLowercase() bool { func (v *video) SupportsLowercase() bool {
return a.hasLowerCase return v.a.hasLowerCase
} }
// DumpTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash // DumpTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash
@ -165,5 +175,6 @@ func DumpTextModeAnsi(a *Apple2) string {
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col) is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active
isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar) isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar)
return screen.RenderTextModeAnsi(a, is80Columns, isSecondPage, isAltText, a.isApple2e) supportsLowercase := a.hasLowerCase
return screen.RenderTextModeAnsi(a.video, is80Columns, isSecondPage, isAltText, supportsLowercase, false)
} }