diff --git a/README.md b/README.md index 004fc98..06ead2d 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ An Apple //e emulator written in Go using [ebiten](https://github.com/hajimehosh * MOS 6502 CPU * Keyboard * 40 column text mode -* Low resolution color graphics -* High resolution monochrome graphics +* Low resolution monochrome and color graphics +* High resolution monochrome and color graphics * Upper memory bank switching: $d000 page and ROM/RAM * Main memory page1/page2 switching in text, lores and hires * Disk image reading & writing @@ -34,7 +34,7 @@ Download `apple2e.rom` from ## Keyboard shortcuts * ctrl-alt-R reset -* ctrl-alt-M mute +* ctrl-alt-M toggle monochrome/color display * ctrl-alt-C caps lock * ctrl-alt-F show FPS diff --git a/apple2.go b/apple2.go index 920b396..fc406f1 100644 --- a/apple2.go +++ b/apple2.go @@ -26,8 +26,9 @@ var ( disableDosDelay *bool // Disable DOS delay functions 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 + resetKeysDown bool // Keep track of ctrl-alt-R key down state + fpsKeysDown bool // Keep track of ctrl-alt-F key down state + monochromeKeysDown bool // Keep track of ctrl-alt-M key down state ) // checkSpecialKeys checks @@ -53,14 +54,27 @@ func checkSpecialKeys() { } else { fpsKeysDown = false } + + // Check for ctrl-alt-M and toggle FPS display + if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyM) { + monochromeKeysDown = true + } else if ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyAlt) && !ebiten.IsKeyPressed(ebiten.KeyM) && monochromeKeysDown { + monochromeKeysDown = false + video.Monochrome = !video.Monochrome + } else { + monochromeKeysDown = false + } } // update is the main ebiten loop func update(screen *ebiten.Image) error { - keyboard.Poll() // Convert ebiten's keyboard state to an interal value checkSpecialKeys() // Poll the keyboard and check for R and F keys + if !(fpsKeysDown || monochromeKeysDown) { + keyboard.Poll() // Convert ebiten's keyboard state to an interal value + } + system.FrameCycles = 0 // Reset cycles processed this frame system.LastAudioCycles = 0 // Reset processed audio cycles exitAtBreak := true // Die if a BRK instruction is seen @@ -122,7 +136,7 @@ func main() { // Start the ebiten main loop ebiten.SetRunnableInBackground(true) - ebiten.Run(update, 280*video.ScreenSizeFactor, 192*video.ScreenSizeFactor, 2, "Apple //e") + ebiten.Run(update, 560, 384, 1, "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/video/video.go b/video/video.go index 4f87c80..30a43b1 100644 --- a/video/video.go +++ b/video/video.go @@ -12,9 +12,6 @@ import ( ) const ( - // ScreenSizeFactor is the fFactor by which the whole screen is resized - ScreenSizeFactor = 1 - textVideoMemory = 0x400 // Base location of page 1 text video memory flashFrames = 11 // Number of frames when FLASH mode is toggled ) @@ -23,19 +20,27 @@ const ( type drawTextLoresByte func(*ebiten.Image, int, int, uint8) error var ( - 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 + flashCounter int // Counter used for flashing characters on the text screen + flashOn bool // Are we currently flashing? + monochromeLoresSquares [16]*ebiten.Image // Monochrome blocks for lores rendering + colorLoresSquares [16]*ebiten.Image // Colored blocks for lores rendering + colors [16]color.NRGBA // 4-bit Colors // ShowFPS determines if the FPS is shown in the corner of the video - ShowFPS bool + ShowFPS bool + Monochrome bool ) // 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) + monochromeLoresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest) + if err != nil { + panic(err) + } + + colorLoresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest) if err != nil { panic(err) } @@ -45,27 +50,36 @@ func initLoresSquares() { // 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}) - loresSquares[0x02].Fill(color.NRGBA{0, 0, 153, alpha}) - loresSquares[0x03].Fill(color.NRGBA{221, 34, 221, alpha}) - loresSquares[0x04].Fill(color.NRGBA{0, 119, 34, alpha}) - loresSquares[0x05].Fill(color.NRGBA{85, 85, 85, alpha}) - loresSquares[0x06].Fill(color.NRGBA{34, 34, 255, alpha}) - loresSquares[0x07].Fill(color.NRGBA{102, 170, 255, alpha}) - loresSquares[0x08].Fill(color.NRGBA{136, 85, 0, alpha}) - loresSquares[0x09].Fill(color.NRGBA{255, 102, 0, alpha}) - loresSquares[0x0A].Fill(color.NRGBA{170, 170, 170, alpha}) - loresSquares[0x0B].Fill(color.NRGBA{255, 153, 136, alpha}) - loresSquares[0x0C].Fill(color.NRGBA{17, 221, 0, alpha}) - 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}) + + colors[0x00] = color.NRGBA{0, 0, 0, alpha} + colors[0x01] = color.NRGBA{221, 0, 51, alpha} + colors[0x02] = color.NRGBA{0, 0, 153, alpha} + colors[0x03] = color.NRGBA{221, 34, 221, alpha} + colors[0x04] = color.NRGBA{0, 119, 34, alpha} + colors[0x05] = color.NRGBA{85, 85, 85, alpha} + colors[0x06] = color.NRGBA{34, 34, 255, alpha} + colors[0x07] = color.NRGBA{102, 170, 255, alpha} + colors[0x08] = color.NRGBA{136, 85, 0, alpha} + colors[0x09] = color.NRGBA{255, 102, 0, alpha} + colors[0x0A] = color.NRGBA{170, 170, 170, alpha} + colors[0x0B] = color.NRGBA{255, 153, 136, alpha} + colors[0x0C] = color.NRGBA{17, 221, 0, alpha} + colors[0x0D] = color.NRGBA{255, 255, 0, alpha} + colors[0x0E] = color.NRGBA{68, 255, 153, alpha} + colors[0x0F] = color.NRGBA{255, 255, 255, alpha} + + for i := 0; i < 0x10; i++ { + colorLoresSquares[i].Fill(colors[i]) + avgIntensity := float64(int(colors[i].R)+int(colors[i].G)+int(colors[i].B)) / 3 + avgColor := color.NRGBA{byte(avgIntensity * 0.2), byte(avgIntensity * 0.75), byte(avgIntensity * 0.2), alpha} + monochromeLoresSquares[i].Fill(avgColor) + } } // Init the video data structures used for rendering func Init() { ShowFPS = false + Monochrome = true initTextCharMap() initLoresSquares() @@ -95,8 +109,8 @@ func drawText(screen *ebiten.Image, x int, y int, value uint8) error { } op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor) - op.GeoM.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y)) + op.GeoM.Scale(2, 2) + op.GeoM.Translate(2*7*float64(x), 2*8*float64(y)) r := image.Rect(0, 0, 7, 8) op.SourceRect = &r @@ -107,8 +121,10 @@ func drawText(screen *ebiten.Image, x int, y int, value uint8) error { op.ColorM.Translate(1, 1, 1, 0) } - // Make it look greenish - op.ColorM.Scale(0.20, 0.75, 0.20, 1) + if Monochrome { + // Make it look greenish + op.ColorM.Scale(0.20, 0.75, 0.20, 1) + } return screen.DrawImage(charMap[value], op) } @@ -121,9 +137,17 @@ func drawLores(screen *ebiten.Image, x int, y int, value uint8) error { // Render top & bottom squares for i := 0; i < 2; i++ { op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor) - op.GeoM.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y)+float64(i)*4) - if err := screen.DrawImage(loresSquares[values[i]], op); err != nil { + op.GeoM.Scale(2, 2) + op.GeoM.Translate(2*7*float64(x), 2*8*float64(y)+2*float64(i)*4) + + var loresSquare *ebiten.Image + if Monochrome { + loresSquare = monochromeLoresSquares[values[i]] + } else { + loresSquare = colorLoresSquares[values[i]] + } + + if err := screen.DrawImage(loresSquare, op); err != nil { return err } } @@ -187,11 +211,8 @@ func drawTextOrLoresScreen(screen *ebiten.Image) error { // 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") - } - - pixels := make([]byte, 280*192*4) + pixels := make([]byte, 560*384*4) + halfPixels := make([]byte, 14) // Loop over all hires lines for y := 0; y < 192; y++ { @@ -207,21 +228,65 @@ func drawHiresScreen(screen *ebiten.Image) error { yOffset += 0x2000 } - // For each byte, flip the 7 bits and write it to the pixels array + // Initialize 4-bit 4-color and 14 half-pixels + var color uint8 // Current 4-bit color + colorPos := uint8(0) // Current half-pixel in the 4-bit color + for i := 0; i < 14; i++ { + halfPixels[i] = 0 + } + + // For each byte, expand the 7 bits to the 14 half-pixels array + // If the high bit is set, shift one half pixel over. + // Don't shift half-bits in monochrome mode for x := 0; x < 40; x++ { offset := yOffset + x value := mmu.ReadPageTable[offset>>8][offset&0xff] - value &= 0x7f + phaseShifted := value >> 7 + + var hp uint8 + if Monochrome { + hp = 0 + } else { + hp = phaseShifted + } + + halfPixels[0] = halfPixels[13] // Rotate the last phase shifted pixel in + + // Double up the pixels into half pixels starting at offset hp for bit := 0; bit < 7; bit++ { - b := float64(value & 1) + halfPixels[hp] = value & 1 + hp = hp + 1 + if hp < 14 { + halfPixels[hp] = value & 1 + hp = hp + 1 + } value = value >> 1 - p := (y*280 + x*7 + bit) * 4 + } - pixels[p+0] = byte(0xff * float64(0.20) * b) - pixels[p+1] = byte(0xff * float64(0.75) * b) - pixels[p+2] = byte(0xff * float64(0.20) * b) - pixels[p+3] = 0xff + for hp = 0; hp < 14; hp++ { + // Update the color bit in colorPos with the half pixel value + color &= ((1 << colorPos) ^ 0xf) + color |= halfPixels[hp] << colorPos + colorPos = (colorPos + 1) & 3 + + // Draw two lines at a time + for rowDouble := 0; rowDouble < 2; rowDouble++ { + p := ((y*2+rowDouble)*560 + x*2*7 + int(hp)) * 4 + + if Monochrome { + b := float64(halfPixels[hp]) + pixels[p+0] = byte(0xff * float64(0.20) * b) + pixels[p+1] = byte(0xff * float64(0.75) * b) + pixels[p+2] = byte(0xff * float64(0.20) * b) + pixels[p+3] = 0xff + } else { + pixels[p+0] = colors[color].R + pixels[p+1] = colors[color].G + pixels[p+2] = colors[color].B + pixels[p+3] = 0xff + } + } } } }