diff --git a/izapple2fyne/fyneKeyboard.go b/izapple2fyne/fyneKeyboard.go index e44fb52..a627e43 100644 --- a/izapple2fyne/fyneKeyboard.go +++ b/izapple2fyne/fyneKeyboard.go @@ -6,6 +6,7 @@ import ( "fyne.io/fyne" "fyne.io/fyne/driver/desktop" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" ) type keyboard struct { @@ -103,10 +104,10 @@ func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) { k.s.a.SendCommand(izapple2.CommandToggleSpeed) } case fyne.KeyF6: - if k.s.screenMode != izapple2.ScreenModeGreen { - k.s.screenMode = izapple2.ScreenModeGreen + if k.s.screenMode != screen.ScreenModeGreen { + k.s.screenMode = screen.ScreenModeGreen } else { - k.s.screenMode = izapple2.ScreenModeNTSC + k.s.screenMode = screen.ScreenModeNTSC } case fyne.KeyF7: k.s.showPages = !k.s.showPages @@ -118,7 +119,7 @@ func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) { k.s.a.SendCommand(izapple2.CommandToggleCPUTrace) case fyne.KeyF12: //case fyne.KeyPrintScreen: - err := izapple2.SaveSnapshot(k.s.a, k.s.screenMode, "snapshot.png") + err := screen.SaveSnapshot(k.s.a, k.s.screenMode, "snapshot.png") if err != nil { fmt.Printf("Error saving snapshoot: %v.\n.", err) } else { diff --git a/izapple2fyne/main.go b/izapple2fyne/main.go index 9dd279b..51370f5 100644 --- a/izapple2fyne/main.go +++ b/izapple2fyne/main.go @@ -6,6 +6,7 @@ import ( "time" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" "github.com/pkg/profile" @@ -42,7 +43,7 @@ func main() { } func fyneRun(s *state) { - s.screenMode = izapple2.ScreenModeNTSC + s.screenMode = screen.ScreenModeNTSC s.app = app.New() s.app.SetIcon(resourceApple2Png) @@ -52,13 +53,13 @@ func fyneRun(s *state) { s.devices = newPanelDevices(s) s.devices.w.Hide() toolbar := buildToolbar(s) - screen := canvas.NewImageFromImage(nil) - screen.ScaleMode = canvas.ImageScalePixels // Looks worst but loads less. - screen.SetMinSize(fyne.NewSize(280*2, 192*2)) + display := canvas.NewImageFromImage(nil) + display.ScaleMode = canvas.ImageScalePixels // Looks worst but loads less. + display.SetMinSize(fyne.NewSize(280*2, 192*2)) container := fyne.NewContainerWithLayout( layout.NewBorderLayout(nil, toolbar, nil, s.devices.w), - screen, toolbar, s.devices.w, + display, toolbar, s.devices.w, ) s.win.SetContent(container) s.win.SetPadded(false) @@ -81,13 +82,13 @@ func fyneRun(s *state) { if !s.a.IsPaused() { var img *image.RGBA if s.showPages { - img = s.a.SnapshotParts(s.screenMode) - s.win.SetTitle(fmt.Sprintf("%v %v %vx%v", s.a.Name, s.a.VideoModeName(), img.Rect.Dx()/2, img.Rect.Dy()/2)) + img = screen.SnapshotParts(s.a, 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)) } else { - img = s.a.Snapshot(s.screenMode) + img = screen.Snapshot(s.a, s.screenMode) } - screen.Image = img - canvas.Refresh(screen) + display.Image = img + canvas.Refresh(display) } } } diff --git a/izapple2fyne/toolbar.go b/izapple2fyne/toolbar.go index 578b530..6566db1 100644 --- a/izapple2fyne/toolbar.go +++ b/izapple2fyne/toolbar.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" "fyne.io/fyne" "fyne.io/fyne/theme" @@ -34,7 +35,7 @@ func buildToolbar(s *state) *widget.Toolbar { })) tb.Append(widget.NewToolbarAction( theme.NewThemedResource(resourceCameraSvg, nil), func() { - err := izapple2.SaveSnapshot(s.a, s.screenMode, "snapshot.png") + err := screen.SaveSnapshot(s.a, s.screenMode, "snapshot.png") if err != nil { s.app.SendNotification(fyne.NewNotification( s.win.Title(), diff --git a/izapple2fyne/toolbarScreen.go b/izapple2fyne/toolbarScreen.go index 973f376..60f0798 100644 --- a/izapple2fyne/toolbarScreen.go +++ b/izapple2fyne/toolbarScreen.go @@ -4,7 +4,7 @@ import ( "fyne.io/fyne" "fyne.io/fyne/theme" "fyne.io/fyne/widget" - "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" ) type toolbarScreen struct { @@ -25,7 +25,7 @@ func newToolbarScreen(s *state) *toolbarScreen { tbs.ntsc = widget.NewButtonWithIcon("", theme.NewThemedResource(resourceTelevisionClassicSvg, nil), func() { - tbs.setScreenMode(izapple2.ScreenModeNTSC) + tbs.setScreenMode(screen.ScreenModeNTSC) }) tbs.ntscDisabled = widget.NewIcon( theme.NewDisabledResource(resourceTelevisionClassicSvg)) @@ -33,7 +33,7 @@ func newToolbarScreen(s *state) *toolbarScreen { tbs.plain = widget.NewButtonWithIcon("", theme.NewThemedResource(resourceTelevisionSvg, nil), func() { - tbs.setScreenMode(izapple2.ScreenModePlain) + tbs.setScreenMode(screen.ScreenModePlain) }) tbs.plainDisabled = widget.NewIcon( theme.NewDisabledResource(resourceTelevisionSvg)) @@ -41,7 +41,7 @@ func newToolbarScreen(s *state) *toolbarScreen { tbs.green = widget.NewButtonWithIcon("", theme.NewThemedResource(resourceMonitorSvg, nil), func() { - tbs.setScreenMode(izapple2.ScreenModeGreen) + tbs.setScreenMode(screen.ScreenModeGreen) }) tbs.greenDisabled = widget.NewIcon( theme.NewDisabledResource(resourceMonitorSvg)) @@ -61,13 +61,13 @@ func newToolbarScreen(s *state) *toolbarScreen { func (tbs *toolbarScreen) setScreenMode(screenMode int) { switch tbs.s.screenMode { - case izapple2.ScreenModeNTSC: + case screen.ScreenModeNTSC: tbs.ntsc.Show() tbs.ntscDisabled.Hide() - case izapple2.ScreenModePlain: + case screen.ScreenModePlain: tbs.plain.Show() tbs.plainDisabled.Hide() - case izapple2.ScreenModeGreen: + case screen.ScreenModeGreen: tbs.green.Show() tbs.greenDisabled.Hide() } @@ -75,13 +75,13 @@ func (tbs *toolbarScreen) setScreenMode(screenMode int) { tbs.s.screenMode = screenMode switch screenMode { - case izapple2.ScreenModeNTSC: + case screen.ScreenModeNTSC: tbs.ntsc.Hide() tbs.ntscDisabled.Show() - case izapple2.ScreenModePlain: + case screen.ScreenModePlain: tbs.plain.Hide() tbs.plainDisabled.Show() - case izapple2.ScreenModeGreen: + case screen.ScreenModeGreen: tbs.green.Hide() tbs.greenDisabled.Show() } diff --git a/izapple2sdl/main.go b/izapple2sdl/main.go index 85d0123..f86a316 100644 --- a/izapple2sdl/main.go +++ b/izapple2sdl/main.go @@ -6,6 +6,7 @@ import ( "unsafe" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" "github.com/pkg/profile" "github.com/veandco/go-sdl2/sdl" @@ -85,10 +86,10 @@ func sdlRun(a *izapple2.Apple2) { if !a.IsPaused() { var img *image.RGBA if kp.showPages { - img = a.SnapshotParts(izapple2.ScreenModeNTSC) - window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, a.VideoModeName(), img.Rect.Dx()/2, img.Rect.Dy()/2)) + img = screen.SnapshotParts(a, screen.ScreenModeNTSC) + window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(a), img.Rect.Dx()/2, img.Rect.Dy()/2)) } else { - img = a.Snapshot(izapple2.ScreenModeNTSC) + img = screen.Snapshot(a, screen.ScreenModeNTSC) } if img != nil { surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]), diff --git a/izapple2sdl/sdlKeyboard.go b/izapple2sdl/sdlKeyboard.go index b2116ca..4b9160a 100644 --- a/izapple2sdl/sdlKeyboard.go +++ b/izapple2sdl/sdlKeyboard.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" "github.com/veandco/go-sdl2/sdl" ) @@ -104,7 +105,7 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) { k.a.SendCommand(izapple2.CommandToggleCPUTrace) case sdl.K_F12: case sdl.K_PRINTSCREEN: - err := izapple2.SaveSnapshot(k.a, izapple2.ScreenModeNTSC, "snapshot.png") + err := screen.SaveSnapshot(k.a, screen.ScreenModeNTSC, "snapshot.png") if err != nil { fmt.Printf("Error saving snapshoot: %v.\n.", err) } else { diff --git a/screen.go b/screen.go deleted file mode 100644 index 7d6acc6..0000000 --- a/screen.go +++ /dev/null @@ -1,260 +0,0 @@ -package izapple2 - -import ( - "image" - "image/color" - "image/png" - "os" -) - -/* -References: - - "Understanding the Apple II", http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf - - "Apple II Reference Manual" - - "More Colors for your Apple", https://archive.org/details/byte-magazine-1979-06/page/n61 -*/ - -const ( - // Base Video Mode - videoBaseMask uint16 = 0x1f - videoText40 uint16 = 0x01 - videoGR uint16 = 0x02 - videoHGR uint16 = 0x03 - videoText80 uint16 = 0x08 - videoDGR uint16 = 0x09 - videoDHGR uint16 = 0x0a - videoText40RGB uint16 = 0x10 - videoMono560 uint16 = 0x11 - videoRGBMix uint16 = 0x12 - videoRGB160 uint16 = 0x13 - videoSHR uint16 = 0x14 - - // Mix text modifiers - videoMixTextMask uint16 = 0x0f00 - videoMixText40 uint16 = 0x0100 - videoMixText80 uint16 = 0x0200 - videoMixText40RGB uint16 = 0x0300 - - // Other modifiers - videoModifiersMask uint16 = 0xf000 - videoSecondPage uint16 = 0x1000 -) - -const ( - // ScreenModeGreen to render as a green phosphor monitor - ScreenModeGreen = iota - // ScreenModePlain to render in color with filled areas - ScreenModePlain - //ScreenModeNTSC shows spaces between pixels - ScreenModeNTSC -) - -func getCurrentVideoMode(a *Apple2) uint16 { - 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) - - isRGBCard := a.io.isSoftSwitchActive(ioFlagRGBCardActive) - rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard) - rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard) - isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2 - isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2 - isRGB160Mode := isDoubleResMode && rgbFlag1 && !rgbFlag2 - - isMixMode := a.io.isSoftSwitchActive(ioFlagMixed) - - var mode uint16 - if isSuperHighResMode { - mode = videoSHR - isMixMode = false - } else if isTextMode { - if is80Columns { - mode = videoText80 - } else if isRGBCard && isStore80Active { - mode = videoText40RGB - } else { - mode = videoText40 - } - isMixMode = false - } else if isHiResMode { - if !isDoubleResMode { - mode = videoHGR - } else if isMono560 { - mode = videoMono560 - } else if isRGBMixMode { - mode = videoRGBMix - } else if isRGB160Mode { - mode = videoRGB160 - } else { - mode = videoDHGR - } - } else if isDoubleResMode { - mode = videoDGR - } else { - mode = videoGR - } - - // Modifiers - if isMixMode { - if is80Columns { - mode |= videoMixText80 - } else /* if isStore80Active { - mode |= videoMixText40RGB - } else */{ - mode |= videoMixText40 - } - } - isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active - if isSecondPage { - mode |= videoSecondPage - } - - return mode -} - -func snapshotByMode(a *Apple2, videoMode uint16, screenMode int) *image.RGBA { - videoBase := videoMode & videoBaseMask - mixMode := videoMode & videoMixTextMask - isSecondPage := (videoMode & videoSecondPage) != 0 - - var lightColor color.Color = color.White - if screenMode == ScreenModeGreen { - // Color for typical Apple ][ period green P1 phosphor monitors - // See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays - lightColor = color.RGBA{65, 255, 0, 255} - } - - applyNTSCFilter := screenMode != ScreenModeGreen - var snap *image.RGBA - var ntscMask *image.Alpha - switch videoBase { - case videoText40: - snap = snapshotText40Mode(a, isSecondPage, lightColor) - applyNTSCFilter = false - case videoText80: - snap = snapshotText80Mode(a, isSecondPage, lightColor) - applyNTSCFilter = false - case videoText40RGB: - snap = snapshotText40RGBMode(a, isSecondPage) - applyNTSCFilter = false - case videoGR: - snap = snapshotLoResModeMono(a, isSecondPage, lightColor) - case videoDGR: - snap = snapshotMeResModeMono(a, 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 videoRGB160: - snap = snapshotDoubleHiRes160ModeMono(a, isSecondPage, lightColor) - case videoSHR: - snap = snapshotSuperHiResMode(a) - applyNTSCFilter = false - } - - if applyNTSCFilter { - snap = filterNTSCColor(snap, ntscMask, screenMode) - } - - if mixMode != 0 { - var bottom *image.RGBA - applyNTSCFilter := screenMode != ScreenModeGreen - switch mixMode { - case videoMixText40: - bottom = snapshotText40Mode(a, isSecondPage, lightColor) - case videoMixText80: - bottom = snapshotText80Mode(a, isSecondPage, lightColor) - case videoMixText40RGB: - bottom = snapshotText40RGBMode(a, isSecondPage) - applyNTSCFilter = false - } - if applyNTSCFilter { - bottom = filterNTSCColor(bottom, ntscMask, screenMode) - } - snap = mixSnapshots(snap, bottom) - } - - return snap -} - -// Snapshot the currently visible screen -func (a *Apple2) Snapshot(screenMode int) *image.RGBA { - videoMode := getCurrentVideoMode(a) - snap := snapshotByMode(a, videoMode, screenMode) - - if screenMode != ScreenModePlain && snap.Bounds().Dy() == hiResHeight { - // Apply the filter to regular CRT snapshots with 192 lines. Not to SHR - snap = linesSeparatedFilter(snap) - } - - return snap -} - -func mixSnapshots(top, bottom *image.RGBA) *image.RGBA { - bottomWidth := bottom.Bounds().Dx() - - // Copy bottom's bottom on top's bottom - for y := hiResHeightMixed; y < hiResHeight; y++ { - for x := 0; x < bottomWidth; x++ { - c := bottom.At(x, y) - top.Set(x, y, c) - } - } - - return top -} - -// SaveSnapshot saves a snapshot of the screen to a png file -func SaveSnapshot(a *Apple2, screenMode int, filename string) error { - img := a.Snapshot(screenMode) - img = squarishPixelsFilter(img) - - f, err := os.Create(filename) - if err != nil { - return err - } - defer f.Close() - - png.Encode(f, img) - return nil -} - -func squarishPixelsFilter(in *image.RGBA) *image.RGBA { - b := in.Bounds() - factor := 1200 / b.Dx() - size := image.Rect(0, 0, factor*b.Dx(), b.Dy()) - out := image.NewRGBA(size) - for x := b.Min.X; x < b.Max.X; x++ { - for y := b.Min.Y; y < b.Max.Y; y++ { - c := in.At(x, y) - for i := 0; i < factor; i++ { - out.Set(factor*x+i, y, c) - } - } - } - return out -} - -func linesSeparatedFilter(in *image.RGBA) *image.RGBA { - b := in.Bounds() - size := image.Rect(0, 0, b.Dx(), 4*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(x, 4*y, c) - out.Set(x, 4*y+1, c) - out.Set(x, 4*y+2, c) - out.Set(x, 4*y+3, color.Black) - } - } - return out -} diff --git a/screen/screen.go b/screen/screen.go new file mode 100644 index 0000000..25041fb --- /dev/null +++ b/screen/screen.go @@ -0,0 +1,168 @@ +package screen + +import ( + "image" + "image/color" + "image/png" + "os" +) + +/* +References: + - "Understanding the Apple II", http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf + - "Apple II Reference Manual" + - "More Colors for your Apple", https://archive.org/details/byte-magazine-1979-06/page/n61 +*/ + +const ( + // ScreenModeGreen to render as a green phosphor monitor + ScreenModeGreen = iota + // ScreenModePlain to render in color with filled areas + ScreenModePlain + //ScreenModeNTSC shows spaces between pixels + ScreenModeNTSC +) + +// Snapshot the currently visible screen +func Snapshot(vs VideoSource, screenMode int) *image.RGBA { + videoMode := vs.GetCurrentVideoMode() + snap := snapshotByMode(vs, videoMode, screenMode) + + if screenMode != ScreenModePlain && snap.Bounds().Dy() == hiResHeight { + // Apply the filter to regular CRT snapshots with 192 lines. Not to SHR + snap = linesSeparatedFilter(snap) + } + + return snap +} + +func snapshotByMode(vs VideoSource, videoMode uint16, screenMode int) *image.RGBA { + videoBase := videoMode & VideoBaseMask + mixMode := videoMode & VideoMixTextMask + isSecondPage := (videoMode & VideoSecondPage) != 0 + + var lightColor color.Color = color.White + if screenMode == ScreenModeGreen { + // Color for typical Apple ][ period green P1 phosphor monitors + // See: https://superuser.com/questions/361297/what-colour-is-the-dark-green-on-old-fashioned-green-screen-computer-displays + lightColor = color.RGBA{65, 255, 0, 255} + } + + applyNTSCFilter := screenMode != ScreenModeGreen + var snap *image.RGBA + var ntscMask *image.Alpha + switch videoBase { + case VideoText40: + snap = snapshotText40(vs, isSecondPage, lightColor) + applyNTSCFilter = false + case VideoText80: + snap = snapshotText80(vs, isSecondPage, lightColor) + applyNTSCFilter = false + case VideoText40RGB: + snap = snapshotText40RGB(vs, isSecondPage) + applyNTSCFilter = false + case VideoGR: + snap = snapshotLoRes(vs, isSecondPage, lightColor) + case VideoDGR: + snap = snapshotMeRes(vs, isSecondPage, lightColor) + case VideoHGR: + snap = snapshotHiRes(vs, isSecondPage, lightColor) + case VideoDHGR: + snap, _ = snapshotDoubleHiRes(vs, isSecondPage, false /*isRGBMixMode*/, lightColor) + case VideoMono560: + snap, _ = snapshotDoubleHiRes(vs, isSecondPage, false /*isRGBMixMode*/, lightColor) + applyNTSCFilter = false + case VideoRGBMix: + snap, ntscMask = snapshotDoubleHiRes(vs, isSecondPage, true /*isRGBMixMode*/, lightColor) + case VideoRGB160: + snap = snapshotDoubleHiRes160(vs, isSecondPage, lightColor) + case VideoSHR: + snap = snapshotSuperHiRes(vs) + applyNTSCFilter = false + } + + if applyNTSCFilter { + snap = filterNTSCColor(snap, ntscMask, screenMode) + } + + if mixMode != 0 { + var bottom *image.RGBA + applyNTSCFilter := screenMode != ScreenModeGreen + switch mixMode { + case VideoMixText40: + bottom = snapshotText40(vs, isSecondPage, lightColor) + case VideoMixText80: + bottom = snapshotText80(vs, isSecondPage, lightColor) + case VideoMixText40RGB: + bottom = snapshotText40RGB(vs, isSecondPage) + applyNTSCFilter = false + } + if applyNTSCFilter { + bottom = filterNTSCColor(bottom, ntscMask, screenMode) + } + snap = mixSnapshots(snap, bottom) + } + + return snap +} + +func mixSnapshots(top, bottom *image.RGBA) *image.RGBA { + bottomWidth := bottom.Bounds().Dx() + + // Copy bottom's bottom on top's bottom + for y := hiResHeightMixed; y < hiResHeight; y++ { + for x := 0; x < bottomWidth; x++ { + c := bottom.At(x, y) + top.Set(x, y, c) + } + } + + return top +} + +// SaveSnapshot saves a snapshot of the screen to a png file +func SaveSnapshot(vs VideoSource, screenMode int, filename string) error { + img := Snapshot(vs, screenMode) + img = squarishPixelsFilter(img) + + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + png.Encode(f, img) + return nil +} + +func squarishPixelsFilter(in *image.RGBA) *image.RGBA { + b := in.Bounds() + factor := 1200 / b.Dx() + size := image.Rect(0, 0, factor*b.Dx(), b.Dy()) + out := image.NewRGBA(size) + for x := b.Min.X; x < b.Max.X; x++ { + for y := b.Min.Y; y < b.Max.Y; y++ { + c := in.At(x, y) + for i := 0; i < factor; i++ { + out.Set(factor*x+i, y, c) + } + } + } + return out +} + +func linesSeparatedFilter(in *image.RGBA) *image.RGBA { + b := in.Bounds() + size := image.Rect(0, 0, b.Dx(), 4*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(x, 4*y, c) + out.Set(x, 4*y+1, c) + out.Set(x, 4*y+2, c) + out.Set(x, 4*y+3, color.Black) + } + } + return out +} diff --git a/screenDebugParts.go b/screen/screenDebugParts.go similarity index 57% rename from screenDebugParts.go rename to screen/screenDebugParts.go index c7bc632..09cd1e9 100644 --- a/screenDebugParts.go +++ b/screen/screenDebugParts.go @@ -1,20 +1,20 @@ -package izapple2 +package screen import ( "image" ) // SnapshotParts the currently visible screen -func (a *Apple2) SnapshotParts(screenMode int) *image.RGBA { - videoMode := getCurrentVideoMode(a) - isSecondPage := (videoMode & videoSecondPage) != 0 - videoBase := videoMode & videoBaseMask - mixMode := videoMode & videoMixTextMask - modifiers := videoMode & videoModifiersMask +func SnapshotParts(vs VideoSource, screenMode int) *image.RGBA { + videoMode := vs.GetCurrentVideoMode() + isSecondPage := (videoMode & VideoSecondPage) != 0 + videoBase := videoMode & VideoBaseMask + mixMode := videoMode & VideoMixTextMask + modifiers := videoMode & VideoModifiersMask - snapScreen := snapshotByMode(a, videoMode, screenMode) - snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage, screenMode) - snapPage2 := snapshotByMode(a, videoMode|videoSecondPage, screenMode) + snapScreen := snapshotByMode(vs, videoMode, screenMode) + snapPage1 := snapshotByMode(vs, videoMode&^VideoSecondPage, screenMode) + snapPage2 := snapshotByMode(vs, videoMode|VideoSecondPage, screenMode) var snapAux *image.RGBA /* @@ -23,16 +23,16 @@ func (a *Apple2) SnapshotParts(screenMode int) *image.RGBA { snapAux = filterMask(mask) }*/ - if videoBase == videoText40RGB { - snapAux = snapshotText40RGBModeColors(a, isSecondPage) + if videoBase == VideoText40RGB { + snapAux = snapshotText40RGBColors(vs, isSecondPage) } else { switch mixMode { - case videoMixText80: - snapAux = snapshotByMode(a, videoText80|modifiers, screenMode) - case videoMixText40RGB: - snapAux = snapshotByMode(a, videoText40RGB|modifiers, screenMode) + case VideoMixText80: + snapAux = snapshotByMode(vs, VideoText80|modifiers, screenMode) + case VideoMixText40RGB: + snapAux = snapshotByMode(vs, VideoText40RGB|modifiers, screenMode) default: - snapAux = snapshotByMode(a, videoText40|modifiers, screenMode) + snapAux = snapshotByMode(vs, VideoText40|modifiers, screenMode) } } @@ -40,50 +40,50 @@ func (a *Apple2) SnapshotParts(screenMode int) *image.RGBA { } // VideoModeName returns the name of the current video mode -func (a *Apple2) VideoModeName() string { - videoMode := getCurrentVideoMode(a) - videoBase := videoMode & videoBaseMask - mixMode := videoMode & videoMixTextMask +func VideoModeName(vs VideoSource) string { + videoMode := vs.GetCurrentVideoMode() + videoBase := videoMode & VideoBaseMask + mixMode := videoMode & VideoMixTextMask var name string switch videoBase { - case videoText40: + case VideoText40: name = "TEXT40COL" - case videoText80: + case VideoText80: name = "TEXT80COL" - case videoText40RGB: + case VideoText40RGB: name = "TEXT40COLRGB" - case videoGR: + case VideoGR: name = "GR" - case videoDGR: + case VideoDGR: name = "DGR" - case videoHGR: + case VideoHGR: name = "HGR" - case videoDHGR: + case VideoDHGR: name = "DHGR" - case videoMono560: + case VideoMono560: name = "Mono560" - case videoRGBMix: + case VideoRGBMix: name = "RGMMIX" - case videoRGB160: + case VideoRGB160: name = "RGB160" - case videoSHR: + case VideoSHR: name = "SHR" default: name = "Unknown video mode" } - if (videoMode & videoSecondPage) != 0 { + if (videoMode & VideoSecondPage) != 0 { name += "-PAGE2" } switch mixMode { - case videoMixText40: + case VideoMixText40: name += "-MIX40" - case videoMixText80: + case VideoMixText80: name += "-MIX80" - case videoMixText40RGB: + case VideoMixText40RGB: name += "-MIX40RGB" } diff --git a/screenDoubleHiRes.go b/screen/screenDoubleHiRes.go similarity index 64% rename from screenDoubleHiRes.go rename to screen/screenDoubleHiRes.go index df0c8d1..486d203 100644 --- a/screenDoubleHiRes.go +++ b/screen/screenDoubleHiRes.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "image" @@ -10,7 +10,14 @@ const ( rgb160Width = 4 * 160 ) -func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) { +func snapshotDoubleHiRes(vs VideoSource, isSecondPage bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) { + dataMain := vs.GetVideoMemory(isSecondPage, false) + dataAux := vs.GetVideoMemory(isSecondPage, true) + return renderDoubleHiRes(dataMain, dataAux, getNTSCMask, light) +} + +func renderDoubleHiRes(dataMain []uint8, dataAux []uint8, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) { + // As described in "Inside the Apple IIe" size := image.Rect(0, 0, doubleHiResWidth, hiResHeight) img := image.NewRGBA(size) @@ -23,9 +30,10 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool, } for y := 0; y < hiResHeight; y++ { + offset := getHiResLineOffset(y) lineParts := [][]uint8{ - getHiResLine(a, y, isSecondPage, true /*auxmem*/), - getHiResLine(a, y, isSecondPage, false /*auxmem*/), + dataAux[offset : offset+hiResLineBytes], + dataMain[offset : offset+hiResLineBytes], } x := 0 // For the NTSC filter to work we have to insert an initial black pixel and skip the last one ¿? @@ -64,14 +72,21 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool, return img, ntscMask } -func snapshotDoubleHiRes160ModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { +func snapshotDoubleHiRes160(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { + dataMain := vs.GetVideoMemory(isSecondPage, false) + dataAux := vs.GetVideoMemory(isSecondPage, true) + return renderDoubleHiRes160(dataMain, dataAux, light) +} + +func renderDoubleHiRes160(dataMain []uint8, dataAux []uint8, light color.Color) *image.RGBA { size := image.Rect(0, 0, rgb160Width, hiResHeight) img := image.NewRGBA(size) for y := 0; y < hiResHeight; y++ { + offset := getHiResLineOffset(y) lineParts := [][]uint8{ - getHiResLine(a, y, isSecondPage, true /*auxmem*/), - getHiResLine(a, y, isSecondPage, false /*auxmem*/), + dataAux[offset : offset+hiResLineBytes], + dataMain[offset : offset+hiResLineBytes], } x := 0 for iByte := 0; iByte < hiResLineBytes; iByte++ { diff --git a/screenHiRes.go b/screen/screenHiRes.go similarity index 61% rename from screenHiRes.go rename to screen/screenHiRes.go index 76bd336..988026c 100644 --- a/screenHiRes.go +++ b/screen/screenHiRes.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "image" @@ -6,16 +6,18 @@ import ( ) const ( - hiResWidth = 280 - hiResLineBytes = hiResWidth / 7 - hiResHeight = 192 - hiResHeightMixed = 160 - hiResPage1Address = uint16(0x2000) - hiResPage2Address = uint16(0x4000) + hiResWidth = 280 + hiResLineBytes = hiResWidth / 7 + hiResHeight = 192 + hiResHeightMixed = 160 ) -func getHiResLineOffset(line int) uint16 { +func snapshotHiRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { + data := vs.GetVideoMemory(isSecondPage, false) + return renderHiRes(data, light) +} +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 @@ -24,23 +26,14 @@ func getHiResLineOffset(line int) uint16 { return uint16(section*40 + outerEighth*0x80 + innerEighth*0x400) } -func getHiResLine(a *Apple2, line int, isSecondPage bool, auxMem bool) []uint8 { - address := hiResPage1Address - if isSecondPage { - address = hiResPage2Address - } - - address += getHiResLineOffset(line) - return a.mmu.getVideoRAM(auxMem).subRange(address, address+hiResLineBytes) -} - -func snapshotHiResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { +func renderHiRes(data []uint8, light color.Color) *image.RGBA { // As described in "Undertanding the Apple II", with half pixel shifts size := image.Rect(0, 0, 2*hiResWidth, hiResHeight) img := image.NewRGBA(size) for y := 0; y < hiResHeight; y++ { - bytes := getHiResLine(a, y, isSecondPage, false /*auxMem*/) + offset := getHiResLineOffset(y) + bytes := data[offset : offset+hiResLineBytes] x := 0 var previousColour color.Color = color.Black for _, b := range bytes { diff --git a/screenHiRes_test.go b/screen/screenHiRes_test.go similarity index 95% rename from screenHiRes_test.go rename to screen/screenHiRes_test.go index d6c440c..5d938bd 100644 --- a/screenHiRes_test.go +++ b/screen/screenHiRes_test.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "testing" diff --git a/screenLoRes.go b/screen/screenLoRes.go similarity index 79% rename from screenLoRes.go rename to screen/screenLoRes.go index 2229026..1035437 100644 --- a/screenLoRes.go +++ b/screen/screenLoRes.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "image" @@ -38,17 +38,17 @@ func getColorPatterns(light color.Color) [16][16]color.Color { } -func snapshotLoResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { - data := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) - return renderGrMode(data, false /*isMeres*/, light) +func snapshotLoRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { + data := getTextFromMemory(vs, isSecondPage, false) + return renderGr(data, false /*isMeres*/, light) } -func snapshotMeResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { - data := getText80FromMemory(a, isSecondPage) - return renderGrMode(data, true /*isMeres*/, light) +func snapshotMeRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { + data := getText80FromMemory(vs, isSecondPage) + return renderGr(data, true /*isMeres*/, light) } -func renderGrMode(data []uint8, isDoubleResMode bool, light color.Color) *image.RGBA { +func renderGr(data []uint8, isDoubleResMode bool, light color.Color) *image.RGBA { grLines := textLines * 2 columns := len(data) / textLines pixelWidth := loResPixelWidth diff --git a/screenNtscFilter.go b/screen/screenNtscFilter.go similarity index 99% rename from screenNtscFilter.go rename to screen/screenNtscFilter.go index 56c9512..438b989 100644 --- a/screenNtscFilter.go +++ b/screen/screenNtscFilter.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "fmt" diff --git a/screenSuperHiRes.go b/screen/screenSuperHiRes.go similarity index 73% rename from screenSuperHiRes.go rename to screen/screenSuperHiRes.go index 4109a4d..e02916f 100644 --- a/screenSuperHiRes.go +++ b/screen/screenSuperHiRes.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "image" @@ -11,29 +11,28 @@ const ( shrHeight = 200 palettesCount = 256 - shrPixelDataAddress = uint16(0x2000) - shrScanLineControlAddress = uint16(0x9d00) - shrColorPalettesAddress = uint16(0x9e00) - shrColorPalettesAddressEnd = uint16(0xa000) + shrScanLineControlOffset = uint16(0x7d00) + shrColorPalettesOffset = uint16(0x7e00) ) -func snapshotSuperHiResMode(a *Apple2) *image.RGBA { +func snapshotSuperHiRes(vs VideoSource) *image.RGBA { + data := vs.GetSuperVideoMemory() + return renderSuperHiRes(data) +} + +func renderSuperHiRes(data []uint8) *image.RGBA { // See "Apple IIGS Hardware Reference", chapter 4, page 91 // http://www.applelogic.org/files/GSHARDWAREREF.pdf - size := image.Rect(0, 0, shrWidth, shrHeight) img := image.NewRGBA(size) - videoRAM := a.mmu.getVideoRAM(true) - // Load the palettes - paletteMem := videoRAM.subRange(shrColorPalettesAddress, shrColorPalettesAddressEnd) colors := make([]color.Color, palettesCount) - iMem := 0 + iMem := uint16(0) for i := 0; i < palettesCount; i++ { - b0 := paletteMem[iMem] + b0 := data[iMem+shrColorPalettesOffset] iMem++ - b1 := paletteMem[iMem] + b1 := data[iMem+shrColorPalettesOffset] iMem++ red := (b1 & 0x0f) << 4 @@ -48,14 +47,13 @@ func snapshotSuperHiResMode(a *Apple2) *image.RGBA { // Build the lines for y := 0; y < shrHeight; y++ { - controlByte := videoRAM.peek(shrScanLineControlAddress + uint16(y)) + controlByte := data[uint16(y)+shrScanLineControlOffset] is640Wide := (controlByte & 0x80) != 0 isColorFill := (controlByte & 0x20) != 0 paletteIndex := (controlByte & 0x0f) << 4 - lineAddress := shrPixelDataAddress + uint16(shrWidthBytes*y) - lineBytes := videoRAM.subRange(lineAddress, uint16(lineAddress+shrWidthBytes)) - + lineAddress := uint16(shrWidthBytes * y) + lineBytes := data[lineAddress : lineAddress+shrWidthBytes] if is640Wide { // Line is 640 pixels, two bits per pixel x := 0 diff --git a/screenText.go b/screen/screenText.go similarity index 55% rename from screenText.go rename to screen/screenText.go index 833e2cd..f792c37 100644 --- a/screenText.go +++ b/screen/screenText.go @@ -1,11 +1,10 @@ -package izapple2 +package screen import ( "fmt" "image" "image/color" "strings" - "time" ) const ( @@ -13,44 +12,32 @@ const ( charHeight = 8 text40Columns = 40 textLines = 24 - - textPage1Address = uint16(0x0400) - textPage2Address = uint16(0x0800) - textPageSize = uint16(0x0400) ) -func snapshotText40Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { - text := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) - return renderTextMode(a, text, nil /*colorMap*/, light) +func snapshotText40(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { + text := getTextFromMemory(vs, isSecondPage, false) + return renderText(vs, text, nil /*colorMap*/, light) } -func snapshotText80Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { - text := getText80FromMemory(a, isSecondPage) - return renderTextMode(a, text, nil /*colorMap*/, light) +func snapshotText80(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA { + text := getText80FromMemory(vs, isSecondPage) + return renderText(vs, text, nil /*colorMap*/, light) } -func snapshotText40RGBMode(a *Apple2, isSecondPage bool) *image.RGBA { - text := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) - colorMap := getTextFromMemory(a.mmu.getVideoRAM(true), isSecondPage) - return renderTextMode(a, text, colorMap, nil) +func snapshotText40RGB(vs VideoSource, isSecondPage bool) *image.RGBA { + text := getTextFromMemory(vs, isSecondPage, false) + colorMap := getTextFromMemory(vs, isSecondPage, true) + return renderText(vs, text, colorMap, nil) } -func snapshotText40RGBModeColors(a *Apple2, isSecondPage bool) *image.RGBA { - colorMap := getTextFromMemory(a.mmu.getVideoRAM(true), isSecondPage) - return renderTextMode(a, nil /*text*/, colorMap, nil) +func snapshotText40RGBColors(vs VideoSource, isSecondPage bool) *image.RGBA { + colorMap := getTextFromMemory(vs, isSecondPage, true) + return renderText(vs, nil /*text*/, colorMap, nil) } -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 - eighth := line % 8 - return uint16(section*40 + eighth*0x80 + col) -} - -func getText80FromMemory(a *Apple2, isSecondPage bool) []uint8 { - text40Columns := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) - text40ColumnsAlt := getTextFromMemory(a.mmu.getVideoRAM(true), isSecondPage) +func getText80FromMemory(vs VideoSource, isSecondPage bool) []uint8 { + text40Columns := getTextFromMemory(vs, isSecondPage, false) + text40ColumnsAlt := getTextFromMemory(vs, isSecondPage, true) // Merge the two 40 cols to return 80 cols text80Columns := make([]uint8, 2*len(text40Columns)) @@ -62,13 +49,8 @@ func getText80FromMemory(a *Apple2, isSecondPage bool) []uint8 { return text80Columns } -func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 { - addressStart := textPage1Address - if isSecondPage { - addressStart = textPage2Address - } - addressEnd := addressStart + textPageSize - data := mem.subRange(addressStart, addressEnd) +func getTextFromMemory(vs VideoSource, isSecondPage bool, isExt bool) []uint8 { + data := vs.GetTextMemory(isSecondPage, isExt) text := make([]uint8, textLines*text40Columns) for l := 0; l < textLines; l++ { @@ -80,6 +62,14 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 { return text } +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 + eighth := line % 8 + return uint16(section*40 + eighth*0x80 + col) +} + func getRGBTextColor(pixel bool, colorKey uint8) color.Color { if pixel { colorKey >>= 4 @@ -89,11 +79,7 @@ func getRGBTextColor(pixel bool, colorKey uint8) color.Color { } -func renderTextMode(a *Apple2, text []uint8, colorMap []uint8, light color.Color) *image.RGBA { - // Flash mode is 2Hz (host time) - isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2) - isAltText := a.io.isSoftSwitchActive(ioFlagAltChar) - +func renderText(vs VideoSource, text []uint8, colorMap []uint8, light color.Color) *image.RGBA { columns := len(text) / textLines if text == nil { columns = text40Columns @@ -118,26 +104,7 @@ func renderTextMode(a *Apple2, text []uint8, colorMap []uint8, light color.Color char = 79 + 128 // Debug screen filed with O } - var pixel bool - if a.isApple2e { - vid6 := (char & 0x40) != 0 - vid7 := (char & 0x80) != 0 - char := char & 0x3f - if vid6 && (vid7 || isAltText) { - char += 0x40 - } - if vid7 || (vid6 && isFlashedFrame && !isAltText) { - char += 0x80 - } - pixel = !a.cg.getPixel(char, rowInChar, colInChar) - } else { - pixel = a.cg.getPixel(char, rowInChar, colInChar) - topBits := char >> 6 - isInverse := topBits == 0 - isFlash := topBits == 1 - - pixel = pixel != (isInverse || (isFlash && isFlashedFrame)) - } + pixel := vs.GetCharacterPixel(char, rowInChar, colInChar) var colour color.Color if colorMap != nil { @@ -160,18 +127,18 @@ func renderTextMode(a *Apple2, text []uint8, colorMap []uint8, light color.Color return img } -// DumpTextModeAnsi returns the text mode contents using ANSI escape codes -// for reverse and flash -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) +// RenderTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash +func RenderTextModeAnsi(vs VideoSource, is80Columns bool, isSecondPage bool, isAltText 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 if is80Columns { - text = getText80FromMemory(a, isSecondPage) + text = getText80FromMemory(vs, isSecondPage) } else { - text = getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) + text = getTextFromMemory(vs, isSecondPage, false) } columns := len(text) / textLines diff --git a/screenText_test.go b/screen/screenText_test.go similarity index 98% rename from screenText_test.go rename to screen/screenText_test.go index b5a6e43..7138942 100644 --- a/screenText_test.go +++ b/screen/screenText_test.go @@ -1,4 +1,4 @@ -package izapple2 +package screen import ( "testing" diff --git a/screen/videoSource.go b/screen/videoSource.go new file mode 100644 index 0000000..6ca4448 --- /dev/null +++ b/screen/videoSource.go @@ -0,0 +1,41 @@ +package screen + +const ( + // Base Video Mode + VideoBaseMask uint16 = 0x1f + VideoText40 uint16 = 0x01 + VideoGR uint16 = 0x02 + VideoHGR uint16 = 0x03 + VideoText80 uint16 = 0x08 + VideoDGR uint16 = 0x09 + VideoDHGR uint16 = 0x0a + VideoText40RGB uint16 = 0x10 + VideoMono560 uint16 = 0x11 + VideoRGBMix uint16 = 0x12 + VideoRGB160 uint16 = 0x13 + VideoSHR uint16 = 0x14 + + // Mix text modifiers + VideoMixTextMask uint16 = 0x0f00 + VideoMixText40 uint16 = 0x0100 + VideoMixText80 uint16 = 0x0200 + VideoMixText40RGB uint16 = 0x0300 + + // Other modifiers + VideoModifiersMask uint16 = 0xf000 + VideoSecondPage uint16 = 0x1000 +) + +// VideoSource provides the info to build the video output +type VideoSource interface { + // GetCurrentVideoMode returns the active video mode + GetCurrentVideoMode() uint16 + // GetTextMemory returns a slice to the text memory pages + GetTextMemory(secondPage bool, ext bool) []uint8 + // GetVideoMemory returns a slice to the video memory pages + GetVideoMemory(secondPage bool, ext bool) []uint8 + // GetCharactePixel returns the pixel as output by the character generator + GetCharacterPixel(char uint8, rowInChar int, colInChar int) bool + // GetSuperVideoMemory returns a slice to the SHR video memory + GetSuperVideoMemory() []uint8 +} diff --git a/videoSourceImpl.go b/videoSourceImpl.go new file mode 100644 index 0000000..3d06c54 --- /dev/null +++ b/videoSourceImpl.go @@ -0,0 +1,148 @@ +package izapple2 + +import ( + "time" + + "github.com/ivanizag/izapple2/screen" +) + +const ( + textPage1Address = uint16(0x0400) + textPage2Address = uint16(0x0800) + textPageSize = uint16(0x0400) + hiResPage1Address = uint16(0x2000) + hiResPage2Address = uint16(0x4000) + hiResPageSize = uint16(0x2000) + shResPageAddress = uint16(0x2000) + shResPageSize = uint16(0x8000) +) + +// GetCurrentVideoMode returns the active video mode +func (a *Apple2) GetCurrentVideoMode() uint16 { + 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) + + isRGBCard := a.io.isSoftSwitchActive(ioFlagRGBCardActive) + rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard) + rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard) + isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2 + isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2 + isRGB160Mode := isDoubleResMode && rgbFlag1 && !rgbFlag2 + + isMixMode := a.io.isSoftSwitchActive(ioFlagMixed) + + var mode uint16 + if isSuperHighResMode { + mode = screen.VideoSHR + isMixMode = false + } else if isTextMode { + if is80Columns { + mode = screen.VideoText80 + } else if isRGBCard && isStore80Active { + mode = screen.VideoText40RGB + } else { + mode = screen.VideoText40 + } + isMixMode = false + } else if isHiResMode { + if !isDoubleResMode { + mode = screen.VideoHGR + } else if isMono560 { + mode = screen.VideoMono560 + } else if isRGBMixMode { + mode = screen.VideoRGBMix + } else if isRGB160Mode { + mode = screen.VideoRGB160 + } else { + mode = screen.VideoDHGR + } + } else if isDoubleResMode { + mode = screen.VideoDGR + } else { + mode = screen.VideoGR + } + + // Modifiers + if isMixMode { + if is80Columns { + mode |= screen.VideoMixText80 + } else /* if isStore80Active { + mode |= screen.VideoMixText40RGB + } else */{ + mode |= screen.VideoMixText40 + } + } + isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active + if isSecondPage { + mode |= screen.VideoSecondPage + } + + return mode +} + +// GetTextMemory returns a slice to the text memory pages +func (a *Apple2) GetTextMemory(secondPage bool, ext bool) []uint8 { + mem := a.mmu.getVideoRAM(ext) + addressStart := textPage1Address + if secondPage { + addressStart = textPage2Address + } + return mem.subRange(addressStart, addressStart+textPageSize) +} + +// GetVideoMemory returns a slice to the video memory pages +func (a *Apple2) GetVideoMemory(secondPage bool, ext bool) []uint8 { + mem := a.mmu.getVideoRAM(ext) + addressStart := hiResPage1Address + if secondPage { + addressStart = hiResPage2Address + } + return mem.subRange(addressStart, addressStart+hiResPageSize) +} + +// GetSuperVideoMemory returns a slice to the SHR video memory +func (a *Apple2) GetSuperVideoMemory() []uint8 { + mem := a.mmu.getVideoRAM(true) + return mem.subRange(shResPageAddress, shResPageAddress+shResPageSize) +} + +// GetCharacterPixel returns the pixel as output by the character generator +func (a *Apple2) GetCharacterPixel(char uint8, rowInChar int, colInChar int) bool { + // Flash mode is 2Hz (host time) + isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2) + isAltText := a.io.isSoftSwitchActive(ioFlagAltChar) + + var pixel bool + if a.isApple2e { + vid6 := (char & 0x40) != 0 + vid7 := (char & 0x80) != 0 + char := char & 0x3f + if vid6 && (vid7 || isAltText) { + char += 0x40 + } + if vid7 || (vid6 && isFlashedFrame && !isAltText) { + char += 0x80 + } + pixel = !a.cg.getPixel(char, rowInChar, colInChar) + } else { + pixel = a.cg.getPixel(char, rowInChar, colInChar) + topBits := char >> 6 + isInverse := topBits == 0 + isFlash := topBits == 1 + + pixel = pixel != (isInverse || (isFlash && isFlashedFrame)) + } + return pixel +} + +// DumpTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash +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) + return screen.RenderTextModeAnsi(a, is80Columns, isSecondPage, isAltText) +}