From ce69239c1e44713d21757d9931cea351ce450193 Mon Sep 17 00:00:00 2001 From: Will Angenent Date: Sun, 27 May 2018 22:58:19 +0100 Subject: [PATCH] Added comments to video.go --- README.md | 1 + apple2.go | 89 +++++++++++++++++++++++++------------------- audio/ebiten.go | 1 + cpu/cpu.go | 6 ++- keyboard/keyboard.go | 1 + system/system.go | 1 + video/video.go | 55 +++++++++++++++++++++------ 7 files changed, 103 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 8f327ee..693c178 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Download `apple2e.rom` from * ctrl-alt-R reset * ctrl-alt-M mute * ctrl-alt-C caps lock +* ctrl-alt-F show FPS ## Running the tests ### Setup diff --git a/apple2.go b/apple2.go index 866411c..5f5ce7f 100644 --- a/apple2.go +++ b/apple2.go @@ -1,10 +1,15 @@ package main +// Main emulator executable. This integrates all the components of the emulator +// and contains the ebiten main loop. + import ( "flag" "fmt" "os" + "github.com/hajimehoshi/ebiten" + "github.com/freewilll/apple2/audio" "github.com/freewilll/apple2/cpu" "github.com/freewilll/apple2/disk" @@ -13,53 +18,62 @@ import ( "github.com/freewilll/apple2/system" "github.com/freewilll/apple2/utils" "github.com/freewilll/apple2/video" - "github.com/hajimehoshi/ebiten" ) -var showInstructions *bool -var disableFirmwareWait *bool -var resetKeysDown bool -var fpsKeysDown bool -var breakAddress *uint16 +var ( + showInstructions *bool // Display all instructions as they are executed + disableFirmwareWait *bool // Disable the WAIT function at $fca8 + breakAddress *uint16 // Break address from the command line + + resetKeysDown bool // Keep track of ctrl-alt-R key down state + fpsKeysDown bool // Keep track of ctrl-alt-F key down state +) // checkSpecialKeys checks // - ctrl-alt-R has been pressed. Releasing the R does a warm reset // - ctrl-alt-F has been pressed, toggling FPS display 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) { resetKeysDown = true } else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyR) && resetKeysDown { resetKeysDown = false cpu.Reset() - } else { resetKeysDown = false } + // Check for ctrl-alt-F and toggle FPS display if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyF) { fpsKeysDown = true } else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyF) && fpsKeysDown { fpsKeysDown = false video.ShowFPS = !video.ShowFPS - fmt.Println("Toggled") - } else { fpsKeysDown = false } } +// update is the main ebiten loop func update(screen *ebiten.Image) error { - keyboard.Poll() - checkSpecialKeys() - system.FrameCycles = 0 - system.LastAudioCycles = 0 - exitAtBreak := true + keyboard.Poll() // Convert ebiten's keyboard state to an interal value + checkSpecialKeys() // Poll the keyboard and check for R and F keys + + 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) - 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) } @@ -79,36 +93,35 @@ func main() { clickWhenDriveHeadMoves := flag.Bool("drive-head-click", false, "Click speaker when drive head moves") flag.Parse() - diskImages := flag.Args() - var diskImage string - if len(diskImages) > 0 { - diskImage = diskImages[0] - } - 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() - mmu.InitRAM() - mmu.InitApple2eROM() - mmu.InitIO() - - if diskImage != "" { - disk.ReadDiskImage(diskImage) + // If there is a disk image on the command line, load it + diskImages := flag.Args() + if len(diskImages) > 0 { + disk.ReadDiskImage(diskImages[0]) } - cpu.Init() - keyboard.Init() - video.Init() - audio.InitEbiten() + cpu.Init() // Init the CPU registers, interrupts and disable testing code + keyboard.Init() // Init the keyboard state and ebiten translation tables + video.Init() // Init the video data structures used for rendering + audio.InitEbiten() // Initialize the audio sets up the ebiten output stream + audio.Mute = *mute 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") + // The main loop has ended, flush any data to the disk image if any writes have been done. disk.FlushImage() } diff --git a/audio/ebiten.go b/audio/ebiten.go index ac40ca2..f99105c 100644 --- a/audio/ebiten.go +++ b/audio/ebiten.go @@ -60,6 +60,7 @@ func (s *stream) Close() error { return nil } +// InitEbiten initializes the audio sets up the ebiten output stream func InitEbiten() { firstAudio = true Mute = false diff --git a/cpu/cpu.go b/cpu/cpu.go index 9d0b3ab..fba83c1 100644 --- a/cpu/cpu.go +++ b/cpu/cpu.go @@ -28,6 +28,7 @@ var State struct { P uint8 } +// Init the CPU registers, interrupts and disable testing code func Init() { system.RunningTests = 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() { - // 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)) } +// Reset sets the CPU and memory states so that a next call to cpu.Run() calls the firmware reset code func Reset() { mmu.InitROM() mmu.InitRAM() diff --git a/keyboard/keyboard.go b/keyboard/keyboard.go index 4445074..861b2c9 100644 --- a/keyboard/keyboard.go +++ b/keyboard/keyboard.go @@ -13,6 +13,7 @@ var strobe uint8 var previousKeysPressed map[uint8]bool var capsLock bool +// Init the keyboard state and ebiten translation tables func Init() { keyBoardData = 0 strobe = 0 diff --git a/system/system.go b/system/system.go index bc87f24..2b05235 100644 --- a/system/system.go +++ b/system/system.go @@ -29,6 +29,7 @@ var DriveState struct { Q7 bool } +// Init initializes the system-wide state func Init() { Cycles = 0 AudioChannel = make(chan int16, AudioSampleRate*4) // 1 second diff --git a/video/video.go b/video/video.go index 10d7956..b4c4710 100644 --- a/video/video.go +++ b/video/video.go @@ -18,23 +18,28 @@ const ( ) var ( - charMap *ebiten.Image - flashCounter int - flashOn bool - loresSquares [16]*ebiten.Image - ShowFPS bool + charMap *ebiten.Image // Character map for text screen + flashCounter int // Counter used for flashing characters on the text screen + flashOn bool // Are we currently flashing? + loresSquares [16]*ebiten.Image // Colored blocks for lores rendering + ShowFPS bool // Show FPS in corner? ) -func Init() { - var err error +// initTextCharMap initializes the text character map +func initTextCharMap() { // The character map pr-latin1.png was downloaded from // http://www.kreativekorp.com/software/fonts/apple2.shtml + var err error charMap, _, err = ebitenutil.NewImageFromFile("video/pr-latin1.png", ebiten.FilterNearest) if err != nil { panic(err) } +} +// initLoresSquares creates 16 colored squares for the lores renderer +func initLoresSquares() { + var err error for i := 0; i < 16; i++ { loresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest) if err != nil { @@ -45,7 +50,6 @@ func Init() { // From // https://mrob.com/pub/xgithub.com/freewilll/apple2/colors.html // https://archive.org/details/IIgs_2523063_Master_Color_Values - alpha := uint8(0xff) loresSquares[0x00].Fill(color.NRGBA{0, 0, 0, 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[0x0E].Fill(color.NRGBA{68, 255, 153, 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 { + // Determine if the character is inverted inverted := false if (value & 0xc0) == 0 { + // Inverted inverted = true } else if (value & 0x80) == 0 { + // Flashing value = value & 0x3f inverted = flashOn } + // Convert the value to a index for the charMap if !inverted { 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.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y)) + // Grab the image from the font image fontRow := value % 16 fontCol := value / 16 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) op.SourceRect = &r + // The charMap is already inverted. Invert it back if we ourselves aren't inverted. if !inverted { op.ColorM.Scale(-1, -1, -1, 1) op.ColorM.Translate(1, 1, 1, 0) } + // Make it look greenish op.ColorM.Scale(0.20, 0.75, 0.20, 1) 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 { + // Convert the 8 bit value to two 4 bit values var values [2]uint8 = [2]uint8{value & 0xf, value >> 4} + // Render top & bottom squares for i := 0; i < 2; i++ { op := &ebiten.DrawImageOptions{} op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor) @@ -121,10 +142,12 @@ func drawLores(screen *ebiten.Image, x int, y int, value uint8) error { return nil } +// drawTextBlock draws a number of lines of text from start to end func drawTextBlock(screen *ebiten.Image, start int, end int) error { for y := start; y < end; y++ { base := 128*(y%8) + 40*(y/8) + // Flip to the 2nd page if so toggled if mmu.Page2 { base += 0x400 } @@ -132,7 +155,6 @@ func drawTextBlock(screen *ebiten.Image, start int, end int) error { for x := 0; x < 40; x++ { offset := textVideoMemory + base + x value := mmu.ReadPageTable[offset>>8][offset&0xff] - if err := drawText(screen, x, y, value); err != nil { return err } @@ -142,10 +164,12 @@ func drawTextBlock(screen *ebiten.Image, start int, end int) error { 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 { for y := start; y < end; y++ { base := 128*(y%8) + 40*(y/8) + // Flip to the 2nd page if so toggled if mmu.Page2 { base += 0x400 } @@ -162,6 +186,7 @@ func drawLoresBlock(screen *ebiten.Image, start int, end int) error { return nil } +// drawTextOrLoresScreen draws a text and/or lores screen depending on the VideoState func drawTextOrLoresScreen(screen *ebiten.Image) error { topHalfIsLowRes := !mmu.VideoState.TextMode bottomHalfIsLowRes := !mmu.VideoState.TextMode && !mmu.VideoState.Mixed @@ -181,6 +206,7 @@ func drawTextOrLoresScreen(screen *ebiten.Image) error { 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 { if ScreenSizeFactor != 1 { panic("Hires mode for ScreenSizeFactor != 1 not implemented") @@ -188,6 +214,7 @@ func drawHiresScreen(screen *ebiten.Image) error { pixels := make([]byte, 280*192*4) + // Loop over all hires lines for y := 0; y < 192; y++ { if mmu.VideoState.Mixed && y >= 160 { continue @@ -196,10 +223,12 @@ func drawHiresScreen(screen *ebiten.Image) error { // Woz is a genius yOffset := 0x2000 - (0x3d8)*(y>>6) + 0x80*(y>>3) + 0x400*(y&0x7) + // Flip to the 2nd page if so toggled if mmu.Page2 { yOffset += 0x2000 } + // For each byte, flip the 7 bits and write it to the pixels array for x := 0; x < 40; x++ { offset := yOffset + x 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) + // Draw text bit at the bottom if mmu.VideoState.Mixed { drawTextBlock(screen, 20, 24) } + return nil } +// DrawScreen draws a text, lores, hires or combination screen func DrawScreen(screen *ebiten.Image) error { flashCounter-- if flashCounter < 0 {