Added comments to video.go

This commit is contained in:
Will Angenent 2018-05-27 22:58:19 +01:00
parent 650669ecff
commit ce69239c1e
7 changed files with 103 additions and 51 deletions

View File

@ -38,6 +38,7 @@ Download `apple2e.rom` from
* ctrl-alt-R reset * ctrl-alt-R reset
* ctrl-alt-M mute * ctrl-alt-M mute
* ctrl-alt-C caps lock * ctrl-alt-C caps lock
* ctrl-alt-F show FPS
## Running the tests ## Running the tests
### Setup ### Setup

View File

@ -1,10 +1,15 @@
package main package main
// Main emulator executable. This integrates all the components of the emulator
// and contains the ebiten main loop.
import ( import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"github.com/hajimehoshi/ebiten"
"github.com/freewilll/apple2/audio" "github.com/freewilll/apple2/audio"
"github.com/freewilll/apple2/cpu" "github.com/freewilll/apple2/cpu"
"github.com/freewilll/apple2/disk" "github.com/freewilll/apple2/disk"
@ -13,53 +18,62 @@ import (
"github.com/freewilll/apple2/system" "github.com/freewilll/apple2/system"
"github.com/freewilll/apple2/utils" "github.com/freewilll/apple2/utils"
"github.com/freewilll/apple2/video" "github.com/freewilll/apple2/video"
"github.com/hajimehoshi/ebiten"
) )
var showInstructions *bool var (
var disableFirmwareWait *bool showInstructions *bool // Display all instructions as they are executed
var resetKeysDown bool disableFirmwareWait *bool // Disable the WAIT function at $fca8
var fpsKeysDown bool breakAddress *uint16 // Break address from the command line
var breakAddress *uint16
resetKeysDown bool // Keep track of ctrl-alt-R key down state
fpsKeysDown bool // Keep track of ctrl-alt-F key down state
)
// checkSpecialKeys checks // checkSpecialKeys checks
// - ctrl-alt-R has been pressed. Releasing the R does a warm reset // - ctrl-alt-R has been pressed. Releasing the R does a warm reset
// - ctrl-alt-F has been pressed, toggling FPS display // - ctrl-alt-F has been pressed, toggling FPS display
func checkSpecialKeys() { func checkSpecialKeys() {
// Check for ctrl-alt-R, and if released, do a warm CPU reset
if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyR) { if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyR) {
resetKeysDown = true resetKeysDown = true
} else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyR) && resetKeysDown { } else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyR) && resetKeysDown {
resetKeysDown = false resetKeysDown = false
cpu.Reset() cpu.Reset()
} else { } else {
resetKeysDown = false resetKeysDown = false
} }
// Check for ctrl-alt-F and toggle FPS display
if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyF) { if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyF) {
fpsKeysDown = true fpsKeysDown = true
} else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyF) && fpsKeysDown { } else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyF) && fpsKeysDown {
fpsKeysDown = false fpsKeysDown = false
video.ShowFPS = !video.ShowFPS video.ShowFPS = !video.ShowFPS
fmt.Println("Toggled")
} else { } else {
fpsKeysDown = false fpsKeysDown = false
} }
} }
// update is the main ebiten loop
func update(screen *ebiten.Image) error { func update(screen *ebiten.Image) error {
keyboard.Poll()
checkSpecialKeys()
system.FrameCycles = 0 keyboard.Poll() // Convert ebiten's keyboard state to an interal value
system.LastAudioCycles = 0 checkSpecialKeys() // Poll the keyboard and check for R and F keys
exitAtBreak := true
system.FrameCycles = 0 // Reset cycles processed this frame
system.LastAudioCycles = 0 // Reset processed audio cycles
exitAtBreak := true // Die if a BRK instruction is seen
// Run for 1/60 of a second, the duration of an ebiten frame
cpu.Run(*showInstructions, breakAddress, exitAtBreak, *disableFirmwareWait, system.CpuFrequency/60) cpu.Run(*showInstructions, breakAddress, exitAtBreak, *disableFirmwareWait, system.CpuFrequency/60)
audio.ForwardToFrameCycle()
system.Cycles += system.FrameCycles
system.FrameCycles = 0
// Process any audio speaker clicks from this frame
audio.ForwardToFrameCycle()
// Updated the cycle accounting
system.Cycles += system.FrameCycles
// Finally render the screen
return video.DrawScreen(screen) return video.DrawScreen(screen)
} }
@ -79,36 +93,35 @@ func main() {
clickWhenDriveHeadMoves := flag.Bool("drive-head-click", false, "Click speaker when drive head moves") clickWhenDriveHeadMoves := flag.Bool("drive-head-click", false, "Click speaker when drive head moves")
flag.Parse() flag.Parse()
diskImages := flag.Args()
var diskImage string
if len(diskImages) > 0 {
diskImage = diskImages[0]
}
breakAddress = utils.DecodeCmdLineAddress(breakAddressString) breakAddress = utils.DecodeCmdLineAddress(breakAddressString)
ebiten.SetRunnableInBackground(true) cpu.InitInstructionDecoder() // Init the instruction decoder data structures
mmu.InitRAM() // Set all switches to bootup values and initialize the page tables
mmu.InitApple2eROM() // Load the ROM and init page tables
mmu.InitIO() // Init slots, video and disk image statuses
cpu.InitInstructionDecoder() // If there is a disk image on the command line, load it
mmu.InitRAM() diskImages := flag.Args()
mmu.InitApple2eROM() if len(diskImages) > 0 {
mmu.InitIO() disk.ReadDiskImage(diskImages[0])
if diskImage != "" {
disk.ReadDiskImage(diskImage)
} }
cpu.Init() cpu.Init() // Init the CPU registers, interrupts and disable testing code
keyboard.Init() keyboard.Init() // Init the keyboard state and ebiten translation tables
video.Init() video.Init() // Init the video data structures used for rendering
audio.InitEbiten() audio.InitEbiten() // Initialize the audio sets up the ebiten output stream
audio.Mute = *mute audio.Mute = *mute
audio.ClickWhenDriveHeadMoves = *clickWhenDriveHeadMoves audio.ClickWhenDriveHeadMoves = *clickWhenDriveHeadMoves
system.Init()
cpu.SetColdStartReset()
cpu.Reset()
system.Init() // Initialize the system-wide state
cpu.SetColdStartReset() // Prepare memory to ensure a cold reset
cpu.Reset() // Set the CPU and memory states so that a next call to cpu.Run() calls the firmware reset code
// Start the ebiten main loop
ebiten.SetRunnableInBackground(true)
ebiten.Run(update, 280*video.ScreenSizeFactor, 192*video.ScreenSizeFactor, 2, "Apple //e") ebiten.Run(update, 280*video.ScreenSizeFactor, 192*video.ScreenSizeFactor, 2, "Apple //e")
// The main loop has ended, flush any data to the disk image if any writes have been done.
disk.FlushImage() disk.FlushImage()
} }

View File

@ -60,6 +60,7 @@ func (s *stream) Close() error {
return nil return nil
} }
// InitEbiten initializes the audio sets up the ebiten output stream
func InitEbiten() { func InitEbiten() {
firstAudio = true firstAudio = true
Mute = false Mute = false

View File

@ -28,6 +28,7 @@ var State struct {
P uint8 P uint8
} }
// Init the CPU registers, interrupts and disable testing code
func Init() { func Init() {
system.RunningTests = false system.RunningTests = false
system.RunningFunctionalTests = false system.RunningFunctionalTests = false
@ -858,12 +859,13 @@ func Run(showInstructions bool, breakAddress *uint16, exitAtBreak bool, disableF
} }
} }
// SetColdStartReset nukes the checkum byte for the reset vector. When this is called, the apple boot firmware will
// conclude that the reset vector is invalid and do a cold start.
func SetColdStartReset() { func SetColdStartReset() {
// Nuke the checkum byte for the reset vector. When this is called, the apple boot firmware will
// conclude that the reset vector is invalid and do a cold start.
mmu.WriteMemory(uint16(0x3f4), uint8(0)) mmu.WriteMemory(uint16(0x3f4), uint8(0))
} }
// Reset sets the CPU and memory states so that a next call to cpu.Run() calls the firmware reset code
func Reset() { func Reset() {
mmu.InitROM() mmu.InitROM()
mmu.InitRAM() mmu.InitRAM()

View File

@ -13,6 +13,7 @@ var strobe uint8
var previousKeysPressed map[uint8]bool var previousKeysPressed map[uint8]bool
var capsLock bool var capsLock bool
// Init the keyboard state and ebiten translation tables
func Init() { func Init() {
keyBoardData = 0 keyBoardData = 0
strobe = 0 strobe = 0

View File

@ -29,6 +29,7 @@ var DriveState struct {
Q7 bool Q7 bool
} }
// Init initializes the system-wide state
func Init() { func Init() {
Cycles = 0 Cycles = 0
AudioChannel = make(chan int16, AudioSampleRate*4) // 1 second AudioChannel = make(chan int16, AudioSampleRate*4) // 1 second

View File

@ -18,23 +18,28 @@ const (
) )
var ( var (
charMap *ebiten.Image charMap *ebiten.Image // Character map for text screen
flashCounter int flashCounter int // Counter used for flashing characters on the text screen
flashOn bool flashOn bool // Are we currently flashing?
loresSquares [16]*ebiten.Image loresSquares [16]*ebiten.Image // Colored blocks for lores rendering
ShowFPS bool ShowFPS bool // Show FPS in corner?
) )
func Init() { // initTextCharMap initializes the text character map
var err error func initTextCharMap() {
// The character map pr-latin1.png was downloaded from // The character map pr-latin1.png was downloaded from
// http://www.kreativekorp.com/software/fonts/apple2.shtml // http://www.kreativekorp.com/software/fonts/apple2.shtml
var err error
charMap, _, err = ebitenutil.NewImageFromFile("video/pr-latin1.png", ebiten.FilterNearest) charMap, _, err = ebitenutil.NewImageFromFile("video/pr-latin1.png", ebiten.FilterNearest)
if err != nil { if err != nil {
panic(err) panic(err)
} }
}
// initLoresSquares creates 16 colored squares for the lores renderer
func initLoresSquares() {
var err error
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
loresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest) loresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest)
if err != nil { if err != nil {
@ -45,7 +50,6 @@ func Init() {
// From // From
// https://mrob.com/pub/xgithub.com/freewilll/apple2/colors.html // https://mrob.com/pub/xgithub.com/freewilll/apple2/colors.html
// https://archive.org/details/IIgs_2523063_Master_Color_Values // https://archive.org/details/IIgs_2523063_Master_Color_Values
alpha := uint8(0xff) alpha := uint8(0xff)
loresSquares[0x00].Fill(color.NRGBA{0, 0, 0, alpha}) loresSquares[0x00].Fill(color.NRGBA{0, 0, 0, alpha})
loresSquares[0x01].Fill(color.NRGBA{221, 0, 51, alpha}) loresSquares[0x01].Fill(color.NRGBA{221, 0, 51, alpha})
@ -63,20 +67,31 @@ func Init() {
loresSquares[0x0D].Fill(color.NRGBA{255, 255, 0, alpha}) loresSquares[0x0D].Fill(color.NRGBA{255, 255, 0, alpha})
loresSquares[0x0E].Fill(color.NRGBA{68, 255, 153, alpha}) loresSquares[0x0E].Fill(color.NRGBA{68, 255, 153, alpha})
loresSquares[0x0F].Fill(color.NRGBA{255, 255, 255, alpha}) loresSquares[0x0F].Fill(color.NRGBA{255, 255, 255, alpha})
ShowFPS = false
} }
// Init the video data structures used for rendering
func Init() {
ShowFPS = false
initTextCharMap()
initLoresSquares()
}
// drawText draws a single text character at x, y. The characters are either normal, inverted or flashing
func drawText(screen *ebiten.Image, x int, y int, value uint8) error { func drawText(screen *ebiten.Image, x int, y int, value uint8) error {
// Determine if the character is inverted
inverted := false inverted := false
if (value & 0xc0) == 0 { if (value & 0xc0) == 0 {
// Inverted
inverted = true inverted = true
} else if (value & 0x80) == 0 { } else if (value & 0x80) == 0 {
// Flashing
value = value & 0x3f value = value & 0x3f
inverted = flashOn inverted = flashOn
} }
// Convert the value to a index for the charMap
if !inverted { if !inverted {
value = value & 0x7f value = value & 0x7f
} }
@ -89,6 +104,7 @@ func drawText(screen *ebiten.Image, x int, y int, value uint8) error {
op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor) op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor)
op.GeoM.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y)) op.GeoM.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y))
// Grab the image from the font image
fontRow := value % 16 fontRow := value % 16
fontCol := value / 16 fontCol := value / 16
var fontX = (int)(15 + fontCol*12) var fontX = (int)(15 + fontCol*12)
@ -96,19 +112,24 @@ func drawText(screen *ebiten.Image, x int, y int, value uint8) error {
r := image.Rect(fontX, fontY, fontX+7, fontY+8) r := image.Rect(fontX, fontY, fontX+7, fontY+8)
op.SourceRect = &r op.SourceRect = &r
// The charMap is already inverted. Invert it back if we ourselves aren't inverted.
if !inverted { if !inverted {
op.ColorM.Scale(-1, -1, -1, 1) op.ColorM.Scale(-1, -1, -1, 1)
op.ColorM.Translate(1, 1, 1, 0) op.ColorM.Translate(1, 1, 1, 0)
} }
// Make it look greenish
op.ColorM.Scale(0.20, 0.75, 0.20, 1) op.ColorM.Scale(0.20, 0.75, 0.20, 1)
return screen.DrawImage(charMap, op) return screen.DrawImage(charMap, op)
} }
// drawLores draws two colored lores squares at the equivalent text location x,y.
func drawLores(screen *ebiten.Image, x int, y int, value uint8) error { func drawLores(screen *ebiten.Image, x int, y int, value uint8) error {
// Convert the 8 bit value to two 4 bit values
var values [2]uint8 = [2]uint8{value & 0xf, value >> 4} var values [2]uint8 = [2]uint8{value & 0xf, value >> 4}
// Render top & bottom squares
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor) op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor)
@ -121,10 +142,12 @@ func drawLores(screen *ebiten.Image, x int, y int, value uint8) error {
return nil return nil
} }
// drawTextBlock draws a number of lines of text from start to end
func drawTextBlock(screen *ebiten.Image, start int, end int) error { func drawTextBlock(screen *ebiten.Image, start int, end int) error {
for y := start; y < end; y++ { for y := start; y < end; y++ {
base := 128*(y%8) + 40*(y/8) base := 128*(y%8) + 40*(y/8)
// Flip to the 2nd page if so toggled
if mmu.Page2 { if mmu.Page2 {
base += 0x400 base += 0x400
} }
@ -132,7 +155,6 @@ func drawTextBlock(screen *ebiten.Image, start int, end int) error {
for x := 0; x < 40; x++ { for x := 0; x < 40; x++ {
offset := textVideoMemory + base + x offset := textVideoMemory + base + x
value := mmu.ReadPageTable[offset>>8][offset&0xff] value := mmu.ReadPageTable[offset>>8][offset&0xff]
if err := drawText(screen, x, y, value); err != nil { if err := drawText(screen, x, y, value); err != nil {
return err return err
} }
@ -142,10 +164,12 @@ func drawTextBlock(screen *ebiten.Image, start int, end int) error {
return nil return nil
} }
// drawTextBlock draws a number of lores lines from the equivalent text start to end line
func drawLoresBlock(screen *ebiten.Image, start int, end int) error { func drawLoresBlock(screen *ebiten.Image, start int, end int) error {
for y := start; y < end; y++ { for y := start; y < end; y++ {
base := 128*(y%8) + 40*(y/8) base := 128*(y%8) + 40*(y/8)
// Flip to the 2nd page if so toggled
if mmu.Page2 { if mmu.Page2 {
base += 0x400 base += 0x400
} }
@ -162,6 +186,7 @@ func drawLoresBlock(screen *ebiten.Image, start int, end int) error {
return nil return nil
} }
// drawTextOrLoresScreen draws a text and/or lores screen depending on the VideoState
func drawTextOrLoresScreen(screen *ebiten.Image) error { func drawTextOrLoresScreen(screen *ebiten.Image) error {
topHalfIsLowRes := !mmu.VideoState.TextMode topHalfIsLowRes := !mmu.VideoState.TextMode
bottomHalfIsLowRes := !mmu.VideoState.TextMode && !mmu.VideoState.Mixed bottomHalfIsLowRes := !mmu.VideoState.TextMode && !mmu.VideoState.Mixed
@ -181,6 +206,7 @@ func drawTextOrLoresScreen(screen *ebiten.Image) error {
return nil return nil
} }
// drawHiresScreen draws an entire hires screen. If it's in mixed mode, the lower end is drawn in text.
func drawHiresScreen(screen *ebiten.Image) error { func drawHiresScreen(screen *ebiten.Image) error {
if ScreenSizeFactor != 1 { if ScreenSizeFactor != 1 {
panic("Hires mode for ScreenSizeFactor != 1 not implemented") panic("Hires mode for ScreenSizeFactor != 1 not implemented")
@ -188,6 +214,7 @@ func drawHiresScreen(screen *ebiten.Image) error {
pixels := make([]byte, 280*192*4) pixels := make([]byte, 280*192*4)
// Loop over all hires lines
for y := 0; y < 192; y++ { for y := 0; y < 192; y++ {
if mmu.VideoState.Mixed && y >= 160 { if mmu.VideoState.Mixed && y >= 160 {
continue continue
@ -196,10 +223,12 @@ func drawHiresScreen(screen *ebiten.Image) error {
// Woz is a genius // Woz is a genius
yOffset := 0x2000 - (0x3d8)*(y>>6) + 0x80*(y>>3) + 0x400*(y&0x7) yOffset := 0x2000 - (0x3d8)*(y>>6) + 0x80*(y>>3) + 0x400*(y&0x7)
// Flip to the 2nd page if so toggled
if mmu.Page2 { if mmu.Page2 {
yOffset += 0x2000 yOffset += 0x2000
} }
// For each byte, flip the 7 bits and write it to the pixels array
for x := 0; x < 40; x++ { for x := 0; x < 40; x++ {
offset := yOffset + x offset := yOffset + x
value := mmu.ReadPageTable[offset>>8][offset&0xff] value := mmu.ReadPageTable[offset>>8][offset&0xff]
@ -218,14 +247,18 @@ func drawHiresScreen(screen *ebiten.Image) error {
} }
} }
// The hires pixels are read, flush them to the screen
screen.ReplacePixels(pixels) screen.ReplacePixels(pixels)
// Draw text bit at the bottom
if mmu.VideoState.Mixed { if mmu.VideoState.Mixed {
drawTextBlock(screen, 20, 24) drawTextBlock(screen, 20, 24)
} }
return nil return nil
} }
// DrawScreen draws a text, lores, hires or combination screen
func DrawScreen(screen *ebiten.Image) error { func DrawScreen(screen *ebiten.Image) error {
flashCounter-- flashCounter--
if flashCounter < 0 { if flashCounter < 0 {