diff --git a/README.md b/README.md index 6f81851..99ae277 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Portable emulator of an Apple II+ or //e. Written in Go. - NTSC Color TV (extracting the phase from the mono signal) - RGB for Super High Resolution and RGB card - ANSI Console, avoiding the SDL2 dependency + - Debug mode: shows four panels with actual screen, page1, page2 and extra info dependant of the video mode - Other features: - Sound - Joystick support. Up to two joysticks or four paddles @@ -131,10 +132,10 @@ Line: - 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 -- Ctrl-F6: Show a split screen with the views for NTSC color TV, monochrome monitor, HGR page 1 and HGR page 2. +- Ctrl-F6: Show the video mode and a split screen with the views for NTSC color TV, page 1, page 2 and extra info. - F7: Save current state to disk (incomplete) - F8: Restore state from disk (incomplete) -- F10: Cycle character generator codepages. Only if the character generator ROM has more than one 2Kb page. +- F10: Cycle character generator code pages. Only if the character generator ROM has more than one 2Kb page. - F11: Toggle on and off the trace to console of the CPU execution - F12: Save a screen snapshot to a file `snapshot.png` - Pause: Pause the emulation diff --git a/apple2sdl/main.go b/apple2sdl/main.go index f3a7acc..37db463 100644 --- a/apple2sdl/main.go +++ b/apple2sdl/main.go @@ -80,9 +80,10 @@ func SDLRun(a *apple2.Apple2) { if !a.IsPaused() { var img *image.RGBA if kp.showPages { - img = apple2.SnapshotHGRModes(a) + img = a.SnapshotParts() + window.SetTitle(a.Name + " " + a.VideoModeName()) } else { - img = apple2.Snapshot(a) + img = a.Snapshot() } if img != nil { surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]), diff --git a/screen.go b/screen.go index 17b2efd..0e52982 100644 --- a/screen.go +++ b/screen.go @@ -15,28 +15,89 @@ References: - "More Colors for your Apple", https://archive.org/details/byte-magazine-1979-06/page/n61 */ -// Snapshot the currently visible screen -func Snapshot(a *Apple2) *image.RGBA { - return activeSnapshot(a, false) -} +const ( + videoText40 uint8 = 0x01 + videoGR uint8 = 0x02 + videoHGR uint8 = 0x03 -func activeSnapshot(a *Apple2, raw bool) *image.RGBA { - isColor := a.isColor + videoText80 uint8 = 0x08 + videoDGR uint8 = 0x09 + videoDHGR uint8 = 0x0a + + videoMono560 uint8 = 0x10 + videoRGBMix uint8 = 0x11 + videoSHR uint8 = 0x12 + + // Modifiers + videoBaseMask uint8 = 0x1f + videoSecondPage uint8 = 0x20 + videoMixText40 uint8 = 0x40 + videoMixText80 uint8 = 0x80 +) + +func getCurrentVideoMode(a *Apple2) uint8 { 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 isSuperHighResMode := a.io.isSoftSwitchActive(ioDataNewVideo) rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard) rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard) isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2 isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2 + isMixMode := a.io.isSoftSwitchActive(ioFlagMixed) + + mode := uint8(0) + if isSuperHighResMode { + mode = videoSHR + isMixMode = false + } else if isTextMode { + if is80Columns { + mode = videoText80 + } else { + mode = videoText40 + } + isMixMode = false + } else if isHiResMode { + if !isDoubleResMode { + mode = videoHGR + } else if isMono560 { + mode = videoMono560 + } else if isRGBMixMode { + mode = videoRGBMix + } else { + mode = videoDHGR + } + } else if isDoubleResMode { + mode = videoDGR + } else { + mode = videoGR + } + + // Modifiers + if isMixMode { + if is80Columns { + mode |= videoMixText80 + } else { + mode |= videoMixText40 + } + } + isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active + if isSecondPage { + mode |= videoSecondPage + } + + return mode +} + +func snapshotByMode(a *Apple2, videoMode uint8) *image.RGBA { + videoBase := videoMode & videoBaseMask + isSecondPage := (videoMode & videoSecondPage) != 0 + isMixMode := (videoMode & (videoMixText40 | videoMixText80)) != 0 var lightColor color.Color - if isColor { + if a.isColor { lightColor = color.White } else { // Color for typical Apple ][ period green P1 phosphor monitors @@ -45,101 +106,80 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA { } + applyNTSCFilter := a.isColor var snap *image.RGBA var ntscMask *image.Alpha - if isSuperHighResMode { // Has to be first and disables the rest + switch videoBase { + case videoText40: + snap = snapshotTextMode(a, false /*is80Columns*/, isSecondPage, lightColor) + applyNTSCFilter = false + case videoText80: + snap = snapshotTextMode(a, true /*is80Columns*/, isSecondPage, lightColor) + applyNTSCFilter = false + case videoGR: + snap = snapshotLoResModeMono(a, false /*isDoubleResMode*/, isSecondPage, lightColor) + case videoDGR: + snap = snapshotLoResModeMono(a, true /*isDoubleResMode*/, isSecondPage, lightColor) + case videoHGR: + snap = snapshotHiResModeMono(a, isSecondPage, lightColor) + case videoDHGR: + snap, _ = snapshotDoubleHiResModeMono(a, isSecondPage, false /*isRGBMixMode*/, lightColor) + case videoMono560: + snap, _ = snapshotDoubleHiResModeMono(a, isSecondPage, false /*isRGBMixMode*/, lightColor) + applyNTSCFilter = false + case videoRGBMix: + snap, ntscMask = snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/, lightColor) + case videoSHR: snap = snapshotSuperHiResMode(a) - } else if isTextMode { - snap = snapshotTextMode(a, is80Columns, isSecondPage, false /*isMixMode*/, lightColor) - } else { - if isHiResMode { - if isDoubleResMode { - snap, ntscMask = snapshotDoubleHiResModeMono(a, isSecondPage, isMixMode, isRGBMixMode, lightColor) - } else { - snap = snapshotHiResModeMono(a, isSecondPage, isMixMode, lightColor) - } - } else { - snap = snapshotLoResModeMono(a, isDoubleResMode, isSecondPage, isMixMode, lightColor) - } - - if isMixMode { - snapText := snapshotTextMode(a, is80Columns, false /*isSecondPage*/, true /*isMixMode*/, lightColor) - snap = mixSnapshots(snap, snapText) - } - if isColor && !(raw || isMono560) { - snap = filterNTSCColor(snap, ntscMask) - } + applyNTSCFilter = false } - if !raw && !isSuperHighResMode { - snap = linesSeparatedFilter(snap) + if isMixMode { + isMix80 := (videoMode & videoMixText80) != 0 + bottom := snapshotTextMode(a, isMix80, isSecondPage, lightColor) + snap = mixSnapshots(snap, bottom) + } + + if applyNTSCFilter { + snap = filterNTSCColor(snap, ntscMask) } return snap } -// SnapshotHGRModes to get all modes mixed -func SnapshotHGRModes(a *Apple2) *image.RGBA { - bwSnap := activeSnapshot(a, true) - if bwSnap.Bounds().Dx() == hiResWidth { - bwSnap = doubleWidthFilter(bwSnap) - } - colorSnap := filterNTSCColor(bwSnap, nil) - page1Snap := filterNTSCColor(snapshotHiResModeMono(a, false /*2nd page*/, false /*mix*/, color.White), nil) // HGR 1 - page2Snap := filterNTSCColor(snapshotHiResModeMono(a, true /*2nd page*/, false /*mix*/, color.White), nil) // HGR 2 +// Snapshot the currently visible screen +func (a *Apple2) Snapshot() *image.RGBA { + videoMode := getCurrentVideoMode(a) + snap := snapshotByMode(a, videoMode) - size := image.Rect(0, 0, hiResWidth*4, hiResHeight*2) - out := image.NewRGBA(size) - - for y := 0; y < hiResHeight; y++ { - for x := 0; x < hiResWidth*2; x++ { - out.Set(x, y, colorSnap.At(x, y)) - out.Set(x+hiResWidth*2, y, bwSnap.At(x, y)) - out.Set(x, y+hiResHeight, page1Snap.At(x, y)) - out.Set(x+hiResWidth*2, y+hiResHeight, page2Snap.At(x, y)) - } + if snap.Bounds().Dy() == hiResHeight { + // Apply the filter to regular CRT snapshots with 192 lines. Not to SHR + snap = linesSeparatedFilter(snap) } - return out + return snap } func mixSnapshots(top, bottom *image.RGBA) *image.RGBA { - topBounds := top.Bounds() - topWidth := topBounds.Dx() - topHeight := topBounds.Dy() - - bottomBounds := bottom.Bounds() - bottomWidth := bottomBounds.Dx() - bottomHeight := bottomBounds.Dy() - + topWidth := top.Bounds().Dx() + bottomWidth := bottom.Bounds().Dx() factor := topWidth / bottomWidth - size := image.Rect(0, 0, topWidth, topHeight+bottomHeight) - out := image.NewRGBA(size) - - // Copy top - for y := topBounds.Min.Y; y < topBounds.Max.Y; y++ { - for x := topBounds.Min.X; x < topBounds.Max.X; x++ { - c := top.At(x, y) - out.Set(x, y, c) - } - } - - // Copy bottom, applying the factor - for y := bottomBounds.Min.Y; y < bottomBounds.Max.Y; y++ { - for x := bottomBounds.Min.X; x < bottomBounds.Max.X; x++ { + // Copy bottom's bottom on top's bottom, applying the factor + for y := hiResHeightMixed; y < hiResHeight; y++ { + for x := 0; x < topWidth; x++ { c := bottom.At(x, y) for f := 0; f < factor; f++ { - out.Set(x*factor+f, topHeight+y, c) + top.Set(x*factor+f, y, c) } } } - return out + return top } // SaveSnapshot saves a snapshot of the screen to a png file func SaveSnapshot(a *Apple2, filename string) error { - img := Snapshot(a) + img := a.Snapshot() img = squarishPixelsFilter(img) f, err := os.Create(filename) @@ -184,17 +224,3 @@ func linesSeparatedFilter(in *image.RGBA) *image.RGBA { } return out } - -func doubleWidthFilter(in *image.RGBA) *image.RGBA { - b := in.Bounds() - size := image.Rect(0, 0, 2*b.Dx(), b.Dy()) - out := image.NewRGBA(size) - for y := b.Min.Y; y < b.Max.Y; y++ { - for x := b.Min.X; x < b.Max.X; x++ { - c := in.At(x, y) - out.Set(2*x, y, c) - out.Set(2*x+1, y, c) - } - } - return out -} diff --git a/screenDebugParts.go b/screenDebugParts.go new file mode 100644 index 0000000..10dfde2 --- /dev/null +++ b/screenDebugParts.go @@ -0,0 +1,120 @@ +package apple2 + +import ( + "image" +) + +// SnapshotParts the currently visible screen +func (a *Apple2) SnapshotParts() *image.RGBA { + videoMode := getCurrentVideoMode(a) + snapScreen := snapshotByMode(a, videoMode) + snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage) + snapPage2 := snapshotByMode(a, videoMode|videoSecondPage) + var snapAux *image.RGBA + + /*videoBase := videoMode & videoBaseMask + if videoBase == videoRGBMix { + isSecondPage := (videoMode & videoSecondPage) != 0 + _, mask := snapshotDoubleHiResModeMono(a, isSecondPage, true /*isRGBMixMode*/ /*, color.White) + snapAux = filterMask(mask) + }*/ + + if snapAux == nil && (videoMode&videoMixText80) != 0 { + snapAux = snapshotByMode(a, videoText80) + } + + if snapAux == nil { + snapAux = snapshotByMode(a, videoText40) + } + + return mixFourSnapshots([]*image.RGBA{snapScreen, snapAux, snapPage1, snapPage2}) +} + +// VideoModeName returns the name of the current video mode +func (a *Apple2) VideoModeName() string { + videoMode := getCurrentVideoMode(a) + videoBase := videoMode & videoBaseMask + + var name string + applyNTSCFilter := a.isColor + + switch videoBase { + case videoText40: + name = "TEXT40COL" + applyNTSCFilter = false + case videoText80: + name = "TEXT80COL" + applyNTSCFilter = false + case videoGR: + name = "GR" + case videoDGR: + name = "DGR" + case videoHGR: + name = "HGR" + case videoDHGR: + name = "DHGR" + case videoMono560: + name = "Mono560" + applyNTSCFilter = false + case videoRGBMix: + name = "RGMMIX" + case videoSHR: + name = "SHR" + applyNTSCFilter = false + default: + name = "Unknown video mode" + } + + if (videoMode & videoSecondPage) != 0 { + name += "-PAGE2" + } + + if (videoMode & videoMixText40) != 0 { + name += "-MIX40" + } + + if (videoMode & videoMixText80) != 0 { + name += "-MIX80" + } + + if applyNTSCFilter { + name += "-NTSC" + } + return name +} + +func mixFourSnapshots(snaps []*image.RGBA) *image.RGBA { + size := image.Rect(0, 0, hiResWidth*4, hiResHeight*2) + out := image.NewRGBA(size) + + for i := 0; i < 4; i++ { + if snaps[i].Bounds().Dx() < hiResWidth*2 { + snaps[i] = doubleWidthFilter(snaps[i]) + } + } + + for y := 0; y < hiResHeight; y++ { + for x := 0; x < hiResWidth*2; x++ { + out.Set(x, y, snaps[0].At(x, y)) + out.Set(x+hiResWidth*2, y, snaps[1].At(x, y)) + out.Set(x, y+hiResHeight, snaps[2].At(x, y)) + out.Set(x+hiResWidth*2, y+hiResHeight, snaps[3].At(x, y)) + } + } + + return out +} + +func doubleWidthFilter(in *image.RGBA) *image.RGBA { + b := in.Bounds() + size := image.Rect(0, 0, 2*b.Dx(), b.Dy()) + out := image.NewRGBA(size) + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + c := in.At(x, y) + out.Set(2*x, y, c) + out.Set(2*x+1, y, c) + } + } + return out +} diff --git a/screenDoubleHiRes.go b/screenDoubleHiRes.go index 0f5b756..8321c18 100644 --- a/screenDoubleHiRes.go +++ b/screenDoubleHiRes.go @@ -9,29 +9,25 @@ const ( doubleHiResWidth = 2 * hiResWidth ) -func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) { +func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) { // As described in "Inside the Apple IIe" - height := hiResHeight - if mixedMode { - height = hiResHeightMixed - } - size := image.Rect(0, 0, doubleHiResWidth, height) + size := image.Rect(0, 0, doubleHiResWidth, hiResHeight) + img := image.NewRGBA(size) - // To support RGB-mode14 we will have a mask to mark where we should not have the NTSC filter applied + // To support RGB-mode 14 we will have a mask to mark where we should not have the NTSC filter applied // See: https://apple2online.com/web_documents/Video-7%20Manual%20KB.pdf var ntscMask *image.Alpha if getNTSCMask { ntscMask = image.NewAlpha(size) } - img := image.NewRGBA(size) - for y := 0; y < height; y++ { + for y := 0; y < hiResHeight; y++ { lineParts := [][]uint8{ - getHiResLine(a, y, isSecondPage, true), - getHiResLine(a, y, isSecondPage, false), + getHiResLine(a, y, isSecondPage, true /*auxmem*/), + getHiResLine(a, y, isSecondPage, false /*auxmem*/), } x := 0 - // For the NTSC filter to work we have to insert an initial black pixel and skip the last one + // For the NTSC filter to work we have to insert an initial black pixel and skip the last one ¿? img.Set(x, y, color.Black) if getNTSCMask { ntscMask.Set(x, y, color.Opaque) @@ -40,10 +36,12 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, g for iByte := 0; iByte < hiResLineBytes; iByte++ { for iPart := 0; iPart < 2; iPart++ { b := lineParts[iPart][iByte] + mask := color.Transparent // Apply the NTSC filter if getNTSCMask && b&0x80 == 0 { mask = color.Opaque // Do not apply the NTSC filter } + for j := uint(0); j < 7; j++ { // Set color bit := (b >> j) & 1 diff --git a/screenHiRes.go b/screenHiRes.go index 249b802..9351b4d 100644 --- a/screenHiRes.go +++ b/screenHiRes.go @@ -19,9 +19,9 @@ func getHiResLineOffset(line int) uint16 { // See "Understanding the Apple II", page 5-14 // http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf section := line >> 6 // Top, middle and bottom - outerEigth := (line >> 3) & 0x07 - innerEigth := line & 0x07 - return uint16(section*40 + outerEigth*0x80 + innerEigth*0x400) + outerEighth := (line >> 3) & 0x07 + innerEighth := line & 0x07 + return uint16(section*40 + outerEighth*0x80 + innerEighth*0x400) } func getHiResLine(a *Apple2, line int, isSecondPage bool, auxMem bool) []uint8 { @@ -34,18 +34,12 @@ func getHiResLine(a *Apple2, line int, isSecondPage bool, auxMem bool) []uint8 { return a.mmu.getPhysicalMainRAM(auxMem).subRange(address, address+hiResLineBytes) } -func snapshotHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, light color.Color) *image.RGBA { +func snapshotHiResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { // As described in "Undertanding the Apple II", with half pixel shifts - - height := hiResHeight - if mixedMode { - height = hiResHeightMixed - } - - size := image.Rect(0, 0, 2*hiResWidth, height) + size := image.Rect(0, 0, 2*hiResWidth, hiResHeight) img := image.NewRGBA(size) - for y := 0; y < height; y++ { + for y := 0; y < hiResHeight; y++ { bytes := getHiResLine(a, y, isSecondPage, false /*auxMem*/) x := 0 var previousColour color.Color = color.Black diff --git a/screenLoRes.go b/screenLoRes.go index 073f898..21c51dc 100644 --- a/screenLoRes.go +++ b/screenLoRes.go @@ -38,11 +38,8 @@ func getColorPatterns(light color.Color) [16][16]color.Color { } -func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA { - text, columns, lines := getActiveText(a, isDoubleResMode, isSecondPage, false) - if isMixMode { - lines -= textLinesMix - } +func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, light color.Color) *image.RGBA { + text, columns, lines := getActiveText(a, isDoubleResMode, isSecondPage) grLines := lines * 2 pixelWidth := loResPixelWidth if isDoubleResMode { @@ -61,7 +58,7 @@ func snapshotLoResModeMono(a *Apple2, isDoubleResMode bool, isSecondPage bool, i grPixel = char & 0xf } // We place pixelWidth mono pixels per graphic pixel. - // The groups of 4 mono pixels need to be alligned with an offset to get plain surfaces + // The groups of 4 mono pixels need to be aligned with an offset to get plain surfaces offset := (c * pixelWidth) % 4 if isDoubleResMode && ((c % 2) == 0) { diff --git a/screenText.go b/screenText.go index 95b7383..ee49786 100644 --- a/screenText.go +++ b/screenText.go @@ -9,11 +9,11 @@ import ( ) const ( - charWidth = 7 - charHeight = 8 - textColumns = 40 - textLines = 24 - textLinesMix = 4 + charWidth = 7 + charHeight = 8 + textColumns = 40 + textLines = 24 + textPage1Address = uint16(0x0400) textPage2Address = uint16(0x0800) textPageSize = uint16(0x0400) @@ -24,44 +24,33 @@ func getTextCharOffset(col int, line int) uint16 { // See "Understanding the Apple II", page 5-10 // http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf section := line / 8 // Top, middle and bottom - eigth := line % 8 - return uint16(section*40 + eigth*0x80 + col) + eighth := line % 8 + return uint16(section*40 + eighth*0x80 + col) } -func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA { - text, columns, lines := getActiveText(a, is80Columns, isSecondPage, isMixMode) +func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, light color.Color) *image.RGBA { + text, columns, lines := getActiveText(a, is80Columns, isSecondPage) return renderTextMode(a, text, columns, lines, light) } -func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool) ([]uint8, int, int) { - - lines := textLines - if isMixMode { - lines = textLinesMix - } - +func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool) ([]uint8, int, int) { if !is80Columns { - text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode) - return text40Columns, textColumns, lines + text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage) + return text40Columns, textColumns, textLines } - text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode) - text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage, isMixMode) + text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage) + text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage) // 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 + return text80Columns, textColumns * 2, textLines } -func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []uint8 { - lineStart := 0 - if isMixMode { - lineStart = textLines - textLinesMix - } - +func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 { addressStart := textPage1Address if isSecondPage { addressStart = textPage2Address @@ -69,11 +58,10 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []ui addressEnd := addressStart + textPageSize data := mem.subRange(addressStart, addressEnd) - lines := textLines - lineStart - text := make([]uint8, lines*textColumns) - for l := 0; l < lines; l++ { + text := make([]uint8, textLines*textColumns) + for l := 0; l < textLines; l++ { for c := 0; c < textColumns; c++ { - char := data[getTextCharOffset(c, l+lineStart)] + char := data[getTextCharOffset(c, l)] text[textColumns*l+c] = char } } @@ -81,7 +69,7 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []ui } func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color.Color) *image.RGBA { - // Flash mode is 2Hz + // Flash mode is 2Hz (host time) isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2) width := columns * charWidth @@ -137,7 +125,7 @@ func DumpTextModeAnsi(a *Apple2) string { is80Columns := a.io.isSoftSwitchActive(ioFlag80Col) isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active - text, columns, lines := getActiveText(a, is80Columns, isSecondPage, false /*isMixedMode*/) + text, columns, lines := getActiveText(a, is80Columns, isSecondPage) content := "\n" content += fmt.Sprintln(strings.Repeat("#", columns+4))