izapple2/screen.go

239 lines
5.8 KiB
Go

package apple2
import (
"fmt"
"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 (
videoText40 uint8 = 0x01
videoGR uint8 = 0x02
videoHGR uint8 = 0x03
videoText80 uint8 = 0x08
videoDGR uint8 = 0x09
videoDHGR uint8 = 0x0a
videoRGBText40 uint8 = 0x10
videoMono560 uint8 = 0x11
videoRGBMix uint8 = 0x12
videoSHR uint8 = 0x13
// 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)
is80Columns := a.io.isSoftSwitchActive(ioFlag80Col)
isDoubleResMode := !isTextMode && is80Columns && !a.io.isSoftSwitchActive(ioFlagAnnunciator3)
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 {
if a.mmu.store80Active {
mode = videoRGBText40
} 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 a.isColor {
lightColor = color.White
} else {
// 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 := a.isColor
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 videoRGBText40:
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 videoSHR:
snap = snapshotSuperHiResMode(a)
applyNTSCFilter = false
}
if isMixMode {
var bottom *image.RGBA
if (videoMode & videoMixText40) != 0 {
bottom = snapshotText40Mode(a, isSecondPage, lightColor)
} else {
bottom = snapshotText80Mode(a, isSecondPage, lightColor)
}
snap = mixSnapshots(snap, bottom)
}
if applyNTSCFilter {
snap = filterNTSCColor(snap, ntscMask)
}
return snap
}
// Snapshot the currently visible screen
func (a *Apple2) Snapshot() *image.RGBA {
videoMode := getCurrentVideoMode(a)
snap := snapshotByMode(a, videoMode)
if 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 {
topWidth := top.Bounds().Dx()
bottomWidth := bottom.Bounds().Dx()
factor := topWidth / bottomWidth
// 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++ {
top.Set(x*factor+f, y, c)
}
}
}
return top
}
// SaveSnapshot saves a snapshot of the screen to a png file
func SaveSnapshot(a *Apple2, filename string) error {
img := a.Snapshot()
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()
fmt.Println(factor)
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
}