izapple2/screen.go

261 lines
6.6 KiB
Go

package izapple2
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 (
// 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
)
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)
mode := uint16(0)
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) *image.RGBA {
videoBase := videoMode & videoBaseMask
mixMode := videoMode & videoMixTextMask
isSecondPage := (videoMode & videoSecondPage) != 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 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)
}
if mixMode != 0 {
var bottom *image.RGBA
applyNTSCFilter := a.isColor
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)
}
snap = mixSnapshots(snap, bottom)
}
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
}