Separate package for screen rendering

This commit is contained in:
Ivan Izaguirre 2020-10-16 20:41:34 +02:00 committed by Iván Izaguirre
parent 9d4df5b643
commit 083c19d2c2
19 changed files with 525 additions and 450 deletions

View File

@ -6,6 +6,7 @@ import (
"fyne.io/fyne" "fyne.io/fyne"
"fyne.io/fyne/driver/desktop" "fyne.io/fyne/driver/desktop"
"github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2"
"github.com/ivanizag/izapple2/screen"
) )
type keyboard struct { type keyboard struct {
@ -103,10 +104,10 @@ func (k *keyboard) putKey(keyEvent *fyne.KeyEvent) {
k.s.a.SendCommand(izapple2.CommandToggleSpeed) k.s.a.SendCommand(izapple2.CommandToggleSpeed)
} }
case fyne.KeyF6: case fyne.KeyF6:
if k.s.screenMode != izapple2.ScreenModeGreen { if k.s.screenMode != screen.ScreenModeGreen {
k.s.screenMode = izapple2.ScreenModeGreen k.s.screenMode = screen.ScreenModeGreen
} else { } else {
k.s.screenMode = izapple2.ScreenModeNTSC k.s.screenMode = screen.ScreenModeNTSC
} }
case fyne.KeyF7: case fyne.KeyF7:
k.s.showPages = !k.s.showPages k.s.showPages = !k.s.showPages
@ -118,7 +119,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 := izapple2.SaveSnapshot(k.s.a, k.s.screenMode, "snapshot.png") err := screen.SaveSnapshot(k.s.a, 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

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2"
"github.com/ivanizag/izapple2/screen"
"github.com/pkg/profile" "github.com/pkg/profile"
@ -42,7 +43,7 @@ func main() {
} }
func fyneRun(s *state) { func fyneRun(s *state) {
s.screenMode = izapple2.ScreenModeNTSC s.screenMode = screen.ScreenModeNTSC
s.app = app.New() s.app = app.New()
s.app.SetIcon(resourceApple2Png) s.app.SetIcon(resourceApple2Png)
@ -52,13 +53,13 @@ func fyneRun(s *state) {
s.devices = newPanelDevices(s) s.devices = newPanelDevices(s)
s.devices.w.Hide() s.devices.w.Hide()
toolbar := buildToolbar(s) toolbar := buildToolbar(s)
screen := canvas.NewImageFromImage(nil) display := canvas.NewImageFromImage(nil)
screen.ScaleMode = canvas.ImageScalePixels // Looks worst but loads less. display.ScaleMode = canvas.ImageScalePixels // Looks worst but loads less.
screen.SetMinSize(fyne.NewSize(280*2, 192*2)) display.SetMinSize(fyne.NewSize(280*2, 192*2))
container := fyne.NewContainerWithLayout( container := fyne.NewContainerWithLayout(
layout.NewBorderLayout(nil, toolbar, nil, s.devices.w), layout.NewBorderLayout(nil, toolbar, nil, s.devices.w),
screen, toolbar, s.devices.w, display, toolbar, s.devices.w,
) )
s.win.SetContent(container) s.win.SetContent(container)
s.win.SetPadded(false) s.win.SetPadded(false)
@ -81,13 +82,13 @@ func fyneRun(s *state) {
if !s.a.IsPaused() { if !s.a.IsPaused() {
var img *image.RGBA var img *image.RGBA
if s.showPages { if s.showPages {
img = s.a.SnapshotParts(s.screenMode) img = screen.SnapshotParts(s.a, 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)) 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 { } else {
img = s.a.Snapshot(s.screenMode) img = screen.Snapshot(s.a, s.screenMode)
} }
screen.Image = img display.Image = img
canvas.Refresh(screen) canvas.Refresh(display)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2"
"github.com/ivanizag/izapple2/screen"
"fyne.io/fyne" "fyne.io/fyne"
"fyne.io/fyne/theme" "fyne.io/fyne/theme"
@ -34,7 +35,7 @@ func buildToolbar(s *state) *widget.Toolbar {
})) }))
tb.Append(widget.NewToolbarAction( tb.Append(widget.NewToolbarAction(
theme.NewThemedResource(resourceCameraSvg, nil), func() { 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 { if err != nil {
s.app.SendNotification(fyne.NewNotification( s.app.SendNotification(fyne.NewNotification(
s.win.Title(), s.win.Title(),

View File

@ -4,7 +4,7 @@ import (
"fyne.io/fyne" "fyne.io/fyne"
"fyne.io/fyne/theme" "fyne.io/fyne/theme"
"fyne.io/fyne/widget" "fyne.io/fyne/widget"
"github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2/screen"
) )
type toolbarScreen struct { type toolbarScreen struct {
@ -25,7 +25,7 @@ func newToolbarScreen(s *state) *toolbarScreen {
tbs.ntsc = widget.NewButtonWithIcon("", tbs.ntsc = widget.NewButtonWithIcon("",
theme.NewThemedResource(resourceTelevisionClassicSvg, nil), theme.NewThemedResource(resourceTelevisionClassicSvg, nil),
func() { func() {
tbs.setScreenMode(izapple2.ScreenModeNTSC) tbs.setScreenMode(screen.ScreenModeNTSC)
}) })
tbs.ntscDisabled = widget.NewIcon( tbs.ntscDisabled = widget.NewIcon(
theme.NewDisabledResource(resourceTelevisionClassicSvg)) theme.NewDisabledResource(resourceTelevisionClassicSvg))
@ -33,7 +33,7 @@ func newToolbarScreen(s *state) *toolbarScreen {
tbs.plain = widget.NewButtonWithIcon("", tbs.plain = widget.NewButtonWithIcon("",
theme.NewThemedResource(resourceTelevisionSvg, nil), theme.NewThemedResource(resourceTelevisionSvg, nil),
func() { func() {
tbs.setScreenMode(izapple2.ScreenModePlain) tbs.setScreenMode(screen.ScreenModePlain)
}) })
tbs.plainDisabled = widget.NewIcon( tbs.plainDisabled = widget.NewIcon(
theme.NewDisabledResource(resourceTelevisionSvg)) theme.NewDisabledResource(resourceTelevisionSvg))
@ -41,7 +41,7 @@ func newToolbarScreen(s *state) *toolbarScreen {
tbs.green = widget.NewButtonWithIcon("", tbs.green = widget.NewButtonWithIcon("",
theme.NewThemedResource(resourceMonitorSvg, nil), theme.NewThemedResource(resourceMonitorSvg, nil),
func() { func() {
tbs.setScreenMode(izapple2.ScreenModeGreen) tbs.setScreenMode(screen.ScreenModeGreen)
}) })
tbs.greenDisabled = widget.NewIcon( tbs.greenDisabled = widget.NewIcon(
theme.NewDisabledResource(resourceMonitorSvg)) theme.NewDisabledResource(resourceMonitorSvg))
@ -61,13 +61,13 @@ func newToolbarScreen(s *state) *toolbarScreen {
func (tbs *toolbarScreen) setScreenMode(screenMode int) { func (tbs *toolbarScreen) setScreenMode(screenMode int) {
switch tbs.s.screenMode { switch tbs.s.screenMode {
case izapple2.ScreenModeNTSC: case screen.ScreenModeNTSC:
tbs.ntsc.Show() tbs.ntsc.Show()
tbs.ntscDisabled.Hide() tbs.ntscDisabled.Hide()
case izapple2.ScreenModePlain: case screen.ScreenModePlain:
tbs.plain.Show() tbs.plain.Show()
tbs.plainDisabled.Hide() tbs.plainDisabled.Hide()
case izapple2.ScreenModeGreen: case screen.ScreenModeGreen:
tbs.green.Show() tbs.green.Show()
tbs.greenDisabled.Hide() tbs.greenDisabled.Hide()
} }
@ -75,13 +75,13 @@ func (tbs *toolbarScreen) setScreenMode(screenMode int) {
tbs.s.screenMode = screenMode tbs.s.screenMode = screenMode
switch screenMode { switch screenMode {
case izapple2.ScreenModeNTSC: case screen.ScreenModeNTSC:
tbs.ntsc.Hide() tbs.ntsc.Hide()
tbs.ntscDisabled.Show() tbs.ntscDisabled.Show()
case izapple2.ScreenModePlain: case screen.ScreenModePlain:
tbs.plain.Hide() tbs.plain.Hide()
tbs.plainDisabled.Show() tbs.plainDisabled.Show()
case izapple2.ScreenModeGreen: case screen.ScreenModeGreen:
tbs.green.Hide() tbs.green.Hide()
tbs.greenDisabled.Show() tbs.greenDisabled.Show()
} }

View File

@ -6,6 +6,7 @@ import (
"unsafe" "unsafe"
"github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2"
"github.com/ivanizag/izapple2/screen"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
@ -85,10 +86,10 @@ func sdlRun(a *izapple2.Apple2) {
if !a.IsPaused() { if !a.IsPaused() {
var img *image.RGBA var img *image.RGBA
if kp.showPages { if kp.showPages {
img = a.SnapshotParts(izapple2.ScreenModeNTSC) img = screen.SnapshotParts(a, screen.ScreenModeNTSC)
window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, a.VideoModeName(), img.Rect.Dx()/2, img.Rect.Dy()/2)) window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(a), img.Rect.Dx()/2, img.Rect.Dy()/2))
} else { } else {
img = a.Snapshot(izapple2.ScreenModeNTSC) img = screen.Snapshot(a, screen.ScreenModeNTSC)
} }
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

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2"
"github.com/ivanizag/izapple2/screen"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
) )
@ -104,7 +105,7 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
k.a.SendCommand(izapple2.CommandToggleCPUTrace) k.a.SendCommand(izapple2.CommandToggleCPUTrace)
case sdl.K_F12: case sdl.K_F12:
case sdl.K_PRINTSCREEN: 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 { if err != nil {
fmt.Printf("Error saving snapshoot: %v.\n.", err) fmt.Printf("Error saving snapshoot: %v.\n.", err)
} else { } else {

260
screen.go
View File

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

168
screen/screen.go Normal file
View File

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

View File

@ -1,20 +1,20 @@
package izapple2 package screen
import ( import (
"image" "image"
) )
// SnapshotParts the currently visible screen // SnapshotParts the currently visible screen
func (a *Apple2) SnapshotParts(screenMode int) *image.RGBA { func SnapshotParts(vs VideoSource, screenMode int) *image.RGBA {
videoMode := getCurrentVideoMode(a) videoMode := vs.GetCurrentVideoMode()
isSecondPage := (videoMode & videoSecondPage) != 0 isSecondPage := (videoMode & VideoSecondPage) != 0
videoBase := videoMode & videoBaseMask videoBase := videoMode & VideoBaseMask
mixMode := videoMode & videoMixTextMask mixMode := videoMode & VideoMixTextMask
modifiers := videoMode & videoModifiersMask modifiers := videoMode & VideoModifiersMask
snapScreen := snapshotByMode(a, videoMode, screenMode) snapScreen := snapshotByMode(vs, videoMode, screenMode)
snapPage1 := snapshotByMode(a, videoMode&^videoSecondPage, screenMode) snapPage1 := snapshotByMode(vs, videoMode&^VideoSecondPage, screenMode)
snapPage2 := snapshotByMode(a, videoMode|videoSecondPage, screenMode) snapPage2 := snapshotByMode(vs, videoMode|VideoSecondPage, screenMode)
var snapAux *image.RGBA var snapAux *image.RGBA
/* /*
@ -23,16 +23,16 @@ func (a *Apple2) SnapshotParts(screenMode int) *image.RGBA {
snapAux = filterMask(mask) snapAux = filterMask(mask)
}*/ }*/
if videoBase == videoText40RGB { if videoBase == VideoText40RGB {
snapAux = snapshotText40RGBModeColors(a, isSecondPage) snapAux = snapshotText40RGBColors(vs, isSecondPage)
} else { } else {
switch mixMode { switch mixMode {
case videoMixText80: case VideoMixText80:
snapAux = snapshotByMode(a, videoText80|modifiers, screenMode) snapAux = snapshotByMode(vs, VideoText80|modifiers, screenMode)
case videoMixText40RGB: case VideoMixText40RGB:
snapAux = snapshotByMode(a, videoText40RGB|modifiers, screenMode) snapAux = snapshotByMode(vs, VideoText40RGB|modifiers, screenMode)
default: 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 // VideoModeName returns the name of the current video mode
func (a *Apple2) VideoModeName() string { func VideoModeName(vs VideoSource) string {
videoMode := getCurrentVideoMode(a) videoMode := vs.GetCurrentVideoMode()
videoBase := videoMode & videoBaseMask videoBase := videoMode & VideoBaseMask
mixMode := videoMode & videoMixTextMask mixMode := videoMode & VideoMixTextMask
var name string var name string
switch videoBase { switch videoBase {
case videoText40: case VideoText40:
name = "TEXT40COL" name = "TEXT40COL"
case videoText80: case VideoText80:
name = "TEXT80COL" name = "TEXT80COL"
case videoText40RGB: case VideoText40RGB:
name = "TEXT40COLRGB" name = "TEXT40COLRGB"
case videoGR: case VideoGR:
name = "GR" name = "GR"
case videoDGR: case VideoDGR:
name = "DGR" name = "DGR"
case videoHGR: case VideoHGR:
name = "HGR" name = "HGR"
case videoDHGR: case VideoDHGR:
name = "DHGR" name = "DHGR"
case videoMono560: case VideoMono560:
name = "Mono560" name = "Mono560"
case videoRGBMix: case VideoRGBMix:
name = "RGMMIX" name = "RGMMIX"
case videoRGB160: case VideoRGB160:
name = "RGB160" name = "RGB160"
case videoSHR: case VideoSHR:
name = "SHR" name = "SHR"
default: default:
name = "Unknown video mode" name = "Unknown video mode"
} }
if (videoMode & videoSecondPage) != 0 { if (videoMode & VideoSecondPage) != 0 {
name += "-PAGE2" name += "-PAGE2"
} }
switch mixMode { switch mixMode {
case videoMixText40: case VideoMixText40:
name += "-MIX40" name += "-MIX40"
case videoMixText80: case VideoMixText80:
name += "-MIX80" name += "-MIX80"
case videoMixText40RGB: case VideoMixText40RGB:
name += "-MIX40RGB" name += "-MIX40RGB"
} }

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"image" "image"
@ -10,7 +10,14 @@ const (
rgb160Width = 4 * 160 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" // As described in "Inside the Apple IIe"
size := image.Rect(0, 0, doubleHiResWidth, hiResHeight) size := image.Rect(0, 0, doubleHiResWidth, hiResHeight)
img := image.NewRGBA(size) img := image.NewRGBA(size)
@ -23,9 +30,10 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool,
} }
for y := 0; y < hiResHeight; y++ { for y := 0; y < hiResHeight; y++ {
offset := getHiResLineOffset(y)
lineParts := [][]uint8{ lineParts := [][]uint8{
getHiResLine(a, y, isSecondPage, true /*auxmem*/), dataAux[offset : offset+hiResLineBytes],
getHiResLine(a, y, isSecondPage, false /*auxmem*/), dataMain[offset : offset+hiResLineBytes],
} }
x := 0 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 ¿?
@ -64,14 +72,21 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, getNTSCMask bool,
return img, ntscMask 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) size := image.Rect(0, 0, rgb160Width, hiResHeight)
img := image.NewRGBA(size) img := image.NewRGBA(size)
for y := 0; y < hiResHeight; y++ { for y := 0; y < hiResHeight; y++ {
offset := getHiResLineOffset(y)
lineParts := [][]uint8{ lineParts := [][]uint8{
getHiResLine(a, y, isSecondPage, true /*auxmem*/), dataAux[offset : offset+hiResLineBytes],
getHiResLine(a, y, isSecondPage, false /*auxmem*/), dataMain[offset : offset+hiResLineBytes],
} }
x := 0 x := 0
for iByte := 0; iByte < hiResLineBytes; iByte++ { for iByte := 0; iByte < hiResLineBytes; iByte++ {

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"image" "image"
@ -6,16 +6,18 @@ import (
) )
const ( const (
hiResWidth = 280 hiResWidth = 280
hiResLineBytes = hiResWidth / 7 hiResLineBytes = hiResWidth / 7
hiResHeight = 192 hiResHeight = 192
hiResHeightMixed = 160 hiResHeightMixed = 160
hiResPage1Address = uint16(0x2000)
hiResPage2Address = uint16(0x4000)
) )
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 // See "Understanding the Apple II", page 5-14
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf // http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf
section := line >> 6 // Top, middle and bottom section := line >> 6 // Top, middle and bottom
@ -24,23 +26,14 @@ func getHiResLineOffset(line int) uint16 {
return uint16(section*40 + outerEighth*0x80 + innerEighth*0x400) return uint16(section*40 + outerEighth*0x80 + innerEighth*0x400)
} }
func getHiResLine(a *Apple2, line int, isSecondPage bool, auxMem bool) []uint8 { func renderHiRes(data []uint8, light color.Color) *image.RGBA {
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 {
// As described in "Undertanding the Apple II", with half pixel shifts // As described in "Undertanding the Apple II", with half pixel shifts
size := image.Rect(0, 0, 2*hiResWidth, hiResHeight) size := image.Rect(0, 0, 2*hiResWidth, hiResHeight)
img := image.NewRGBA(size) img := image.NewRGBA(size)
for y := 0; y < hiResHeight; y++ { for y := 0; y < hiResHeight; y++ {
bytes := getHiResLine(a, y, isSecondPage, false /*auxMem*/) offset := getHiResLineOffset(y)
bytes := data[offset : offset+hiResLineBytes]
x := 0 x := 0
var previousColour color.Color = color.Black var previousColour color.Color = color.Black
for _, b := range bytes { for _, b := range bytes {

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"testing" "testing"

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"image" "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 { func snapshotLoRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA {
data := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) data := getTextFromMemory(vs, isSecondPage, false)
return renderGrMode(data, false /*isMeres*/, light) return renderGr(data, false /*isMeres*/, light)
} }
func snapshotMeResModeMono(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { func snapshotMeRes(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA {
data := getText80FromMemory(a, isSecondPage) data := getText80FromMemory(vs, isSecondPage)
return renderGrMode(data, true /*isMeres*/, light) 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 grLines := textLines * 2
columns := len(data) / textLines columns := len(data) / textLines
pixelWidth := loResPixelWidth pixelWidth := loResPixelWidth

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"image" "image"
@ -11,29 +11,28 @@ const (
shrHeight = 200 shrHeight = 200
palettesCount = 256 palettesCount = 256
shrPixelDataAddress = uint16(0x2000) shrScanLineControlOffset = uint16(0x7d00)
shrScanLineControlAddress = uint16(0x9d00) shrColorPalettesOffset = uint16(0x7e00)
shrColorPalettesAddress = uint16(0x9e00)
shrColorPalettesAddressEnd = uint16(0xa000)
) )
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 // See "Apple IIGS Hardware Reference", chapter 4, page 91
// http://www.applelogic.org/files/GSHARDWAREREF.pdf // http://www.applelogic.org/files/GSHARDWAREREF.pdf
size := image.Rect(0, 0, shrWidth, shrHeight) size := image.Rect(0, 0, shrWidth, shrHeight)
img := image.NewRGBA(size) img := image.NewRGBA(size)
videoRAM := a.mmu.getVideoRAM(true)
// Load the palettes // Load the palettes
paletteMem := videoRAM.subRange(shrColorPalettesAddress, shrColorPalettesAddressEnd)
colors := make([]color.Color, palettesCount) colors := make([]color.Color, palettesCount)
iMem := 0 iMem := uint16(0)
for i := 0; i < palettesCount; i++ { for i := 0; i < palettesCount; i++ {
b0 := paletteMem[iMem] b0 := data[iMem+shrColorPalettesOffset]
iMem++ iMem++
b1 := paletteMem[iMem] b1 := data[iMem+shrColorPalettesOffset]
iMem++ iMem++
red := (b1 & 0x0f) << 4 red := (b1 & 0x0f) << 4
@ -48,14 +47,13 @@ func snapshotSuperHiResMode(a *Apple2) *image.RGBA {
// Build the lines // Build the lines
for y := 0; y < shrHeight; y++ { for y := 0; y < shrHeight; y++ {
controlByte := videoRAM.peek(shrScanLineControlAddress + uint16(y)) controlByte := data[uint16(y)+shrScanLineControlOffset]
is640Wide := (controlByte & 0x80) != 0 is640Wide := (controlByte & 0x80) != 0
isColorFill := (controlByte & 0x20) != 0 isColorFill := (controlByte & 0x20) != 0
paletteIndex := (controlByte & 0x0f) << 4 paletteIndex := (controlByte & 0x0f) << 4
lineAddress := shrPixelDataAddress + uint16(shrWidthBytes*y) lineAddress := uint16(shrWidthBytes * y)
lineBytes := videoRAM.subRange(lineAddress, uint16(lineAddress+shrWidthBytes)) lineBytes := data[lineAddress : lineAddress+shrWidthBytes]
if is640Wide { if is640Wide {
// Line is 640 pixels, two bits per pixel // Line is 640 pixels, two bits per pixel
x := 0 x := 0

View File

@ -1,11 +1,10 @@
package izapple2 package screen
import ( import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"strings" "strings"
"time"
) )
const ( const (
@ -13,44 +12,32 @@ const (
charHeight = 8 charHeight = 8
text40Columns = 40 text40Columns = 40
textLines = 24 textLines = 24
textPage1Address = uint16(0x0400)
textPage2Address = uint16(0x0800)
textPageSize = uint16(0x0400)
) )
func snapshotText40Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { func snapshotText40(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA {
text := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) text := getTextFromMemory(vs, isSecondPage, false)
return renderTextMode(a, text, nil /*colorMap*/, light) return renderText(vs, text, nil /*colorMap*/, light)
} }
func snapshotText80Mode(a *Apple2, isSecondPage bool, light color.Color) *image.RGBA { func snapshotText80(vs VideoSource, isSecondPage bool, light color.Color) *image.RGBA {
text := getText80FromMemory(a, isSecondPage) text := getText80FromMemory(vs, isSecondPage)
return renderTextMode(a, text, nil /*colorMap*/, light) return renderText(vs, text, nil /*colorMap*/, light)
} }
func snapshotText40RGBMode(a *Apple2, isSecondPage bool) *image.RGBA { func snapshotText40RGB(vs VideoSource, isSecondPage bool) *image.RGBA {
text := getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) text := getTextFromMemory(vs, isSecondPage, false)
colorMap := getTextFromMemory(a.mmu.getVideoRAM(true), isSecondPage) colorMap := getTextFromMemory(vs, isSecondPage, true)
return renderTextMode(a, text, colorMap, nil) return renderText(vs, text, colorMap, nil)
} }
func snapshotText40RGBModeColors(a *Apple2, isSecondPage bool) *image.RGBA { func snapshotText40RGBColors(vs VideoSource, isSecondPage bool) *image.RGBA {
colorMap := getTextFromMemory(a.mmu.getVideoRAM(true), isSecondPage) colorMap := getTextFromMemory(vs, isSecondPage, true)
return renderTextMode(a, nil /*text*/, colorMap, nil) return renderText(vs, nil /*text*/, colorMap, nil)
} }
func getTextCharOffset(col int, line int) uint16 { func getText80FromMemory(vs VideoSource, isSecondPage bool) []uint8 {
// See "Understanding the Apple II", page 5-10 text40Columns := getTextFromMemory(vs, isSecondPage, false)
// http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf text40ColumnsAlt := getTextFromMemory(vs, isSecondPage, true)
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)
// 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))
@ -62,13 +49,8 @@ func getText80FromMemory(a *Apple2, isSecondPage bool) []uint8 {
return text80Columns return text80Columns
} }
func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 { func getTextFromMemory(vs VideoSource, isSecondPage bool, isExt bool) []uint8 {
addressStart := textPage1Address data := vs.GetTextMemory(isSecondPage, isExt)
if isSecondPage {
addressStart = textPage2Address
}
addressEnd := addressStart + textPageSize
data := mem.subRange(addressStart, addressEnd)
text := make([]uint8, textLines*text40Columns) text := make([]uint8, textLines*text40Columns)
for l := 0; l < textLines; l++ { for l := 0; l < textLines; l++ {
@ -80,6 +62,14 @@ func getTextFromMemory(mem *memoryRange, isSecondPage bool) []uint8 {
return text 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 { func getRGBTextColor(pixel bool, colorKey uint8) color.Color {
if pixel { if pixel {
colorKey >>= 4 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 { func renderText(vs VideoSource, 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)
columns := len(text) / textLines columns := len(text) / textLines
if text == nil { if text == nil {
columns = text40Columns 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 char = 79 + 128 // Debug screen filed with O
} }
var pixel bool pixel := vs.GetCharacterPixel(char, rowInChar, colInChar)
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))
}
var colour color.Color var colour color.Color
if colorMap != nil { if colorMap != nil {
@ -160,18 +127,18 @@ func renderTextMode(a *Apple2, text []uint8, colorMap []uint8, light color.Color
return img return img
} }
// DumpTextModeAnsi returns the text mode contents using ANSI escape codes // RenderTextModeAnsi returns the text mode contents using ANSI escape codes for reverse and flash
// for reverse and flash func RenderTextModeAnsi(vs VideoSource, is80Columns bool, isSecondPage bool, isAltText bool) string {
func DumpTextModeAnsi(a *Apple2) string { //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)
var text []uint8 var text []uint8
if is80Columns { if is80Columns {
text = getText80FromMemory(a, isSecondPage) text = getText80FromMemory(vs, isSecondPage)
} else { } else {
text = getTextFromMemory(a.mmu.getVideoRAM(false), isSecondPage) text = getTextFromMemory(vs, isSecondPage, false)
} }
columns := len(text) / textLines columns := len(text) / textLines

View File

@ -1,4 +1,4 @@
package izapple2 package screen
import ( import (
"testing" "testing"

41
screen/videoSource.go Normal file
View File

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

148
videoSourceImpl.go Normal file
View File

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