mirror of
https://github.com/ivanizag/izapple2.git
synced 2025-03-25 10:31:06 +00:00
Separate package for screen rendering
This commit is contained in:
parent
9d4df5b643
commit
083c19d2c2
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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]),
|
||||
|
@ -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 {
|
||||
|
260
screen.go
260
screen.go
@ -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
168
screen/screen.go
Normal 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
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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++ {
|
@ -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 {
|
@ -1,4 +1,4 @@
|
||||
package izapple2
|
||||
package screen
|
||||
|
||||
import (
|
||||
"testing"
|
@ -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
|
@ -1,4 +1,4 @@
|
||||
package izapple2
|
||||
package screen
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -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
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
package izapple2
|
||||
package screen
|
||||
|
||||
import (
|
||||
"testing"
|
41
screen/videoSource.go
Normal file
41
screen/videoSource.go
Normal 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
148
videoSourceImpl.go
Normal 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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user