Added colored hires mode and monochrome keyboard toggle
This commit is contained in:
parent
9323078bc0
commit
85417562c1
|
@ -7,8 +7,8 @@ An Apple //e emulator written in Go using [ebiten](https://github.com/hajimehosh
|
||||||
* MOS 6502 CPU
|
* MOS 6502 CPU
|
||||||
* Keyboard
|
* Keyboard
|
||||||
* 40 column text mode
|
* 40 column text mode
|
||||||
* Low resolution color graphics
|
* Low resolution monochrome and color graphics
|
||||||
* High resolution monochrome graphics
|
* High resolution monochrome and color graphics
|
||||||
* Upper memory bank switching: $d000 page and ROM/RAM
|
* Upper memory bank switching: $d000 page and ROM/RAM
|
||||||
* Main memory page1/page2 switching in text, lores and hires
|
* Main memory page1/page2 switching in text, lores and hires
|
||||||
* Disk image reading & writing
|
* Disk image reading & writing
|
||||||
|
@ -34,7 +34,7 @@ Download `apple2e.rom` from
|
||||||
## Keyboard shortcuts
|
## Keyboard shortcuts
|
||||||
|
|
||||||
* ctrl-alt-R reset
|
* ctrl-alt-R reset
|
||||||
* ctrl-alt-M mute
|
* ctrl-alt-M toggle monochrome/color display
|
||||||
* ctrl-alt-C caps lock
|
* ctrl-alt-C caps lock
|
||||||
* ctrl-alt-F show FPS
|
* ctrl-alt-F show FPS
|
||||||
|
|
||||||
|
|
22
apple2.go
22
apple2.go
|
@ -26,8 +26,9 @@ var (
|
||||||
disableDosDelay *bool // Disable DOS delay functions
|
disableDosDelay *bool // Disable DOS delay functions
|
||||||
breakAddress *uint16 // Break address from the command line
|
breakAddress *uint16 // Break address from the command line
|
||||||
|
|
||||||
resetKeysDown bool // Keep track of ctrl-alt-R 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
|
fpsKeysDown bool // Keep track of ctrl-alt-F key down state
|
||||||
|
monochromeKeysDown bool // Keep track of ctrl-alt-M key down state
|
||||||
)
|
)
|
||||||
|
|
||||||
// checkSpecialKeys checks
|
// checkSpecialKeys checks
|
||||||
|
@ -53,14 +54,27 @@ func checkSpecialKeys() {
|
||||||
} else {
|
} else {
|
||||||
fpsKeysDown = false
|
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
|
// update is the main ebiten loop
|
||||||
func update(screen *ebiten.Image) error {
|
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
|
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.FrameCycles = 0 // Reset cycles processed this frame
|
||||||
system.LastAudioCycles = 0 // Reset processed audio cycles
|
system.LastAudioCycles = 0 // Reset processed audio cycles
|
||||||
exitAtBreak := true // Die if a BRK instruction is seen
|
exitAtBreak := true // Die if a BRK instruction is seen
|
||||||
|
@ -122,7 +136,7 @@ func main() {
|
||||||
|
|
||||||
// Start the ebiten main loop
|
// Start the ebiten main loop
|
||||||
ebiten.SetRunnableInBackground(true)
|
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.
|
// The main loop has ended, flush any data to the disk image if any writes have been done.
|
||||||
disk.FlushImage()
|
disk.FlushImage()
|
||||||
|
|
153
video/video.go
153
video/video.go
|
@ -12,9 +12,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ScreenSizeFactor is the fFactor by which the whole screen is resized
|
|
||||||
ScreenSizeFactor = 1
|
|
||||||
|
|
||||||
textVideoMemory = 0x400 // Base location of page 1 text video memory
|
textVideoMemory = 0x400 // Base location of page 1 text video memory
|
||||||
flashFrames = 11 // Number of frames when FLASH mode is toggled
|
flashFrames = 11 // Number of frames when FLASH mode is toggled
|
||||||
)
|
)
|
||||||
|
@ -23,19 +20,27 @@ const (
|
||||||
type drawTextLoresByte func(*ebiten.Image, int, int, uint8) error
|
type drawTextLoresByte func(*ebiten.Image, int, int, uint8) error
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flashCounter int // Counter used for flashing characters on the text screen
|
flashCounter int // Counter used for flashing characters on the text screen
|
||||||
flashOn bool // Are we currently flashing?
|
flashOn bool // Are we currently flashing?
|
||||||
loresSquares [16]*ebiten.Image // Colored blocks for lores rendering
|
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 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
|
// initLoresSquares creates 16 colored squares for the lores renderer
|
||||||
func initLoresSquares() {
|
func initLoresSquares() {
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < 16; i++ {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -45,27 +50,36 @@ func initLoresSquares() {
|
||||||
// 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[0x01].Fill(color.NRGBA{221, 0, 51, alpha})
|
colors[0x00] = color.NRGBA{0, 0, 0, alpha}
|
||||||
loresSquares[0x02].Fill(color.NRGBA{0, 0, 153, alpha})
|
colors[0x01] = color.NRGBA{221, 0, 51, alpha}
|
||||||
loresSquares[0x03].Fill(color.NRGBA{221, 34, 221, alpha})
|
colors[0x02] = color.NRGBA{0, 0, 153, alpha}
|
||||||
loresSquares[0x04].Fill(color.NRGBA{0, 119, 34, alpha})
|
colors[0x03] = color.NRGBA{221, 34, 221, alpha}
|
||||||
loresSquares[0x05].Fill(color.NRGBA{85, 85, 85, alpha})
|
colors[0x04] = color.NRGBA{0, 119, 34, alpha}
|
||||||
loresSquares[0x06].Fill(color.NRGBA{34, 34, 255, alpha})
|
colors[0x05] = color.NRGBA{85, 85, 85, alpha}
|
||||||
loresSquares[0x07].Fill(color.NRGBA{102, 170, 255, alpha})
|
colors[0x06] = color.NRGBA{34, 34, 255, alpha}
|
||||||
loresSquares[0x08].Fill(color.NRGBA{136, 85, 0, alpha})
|
colors[0x07] = color.NRGBA{102, 170, 255, alpha}
|
||||||
loresSquares[0x09].Fill(color.NRGBA{255, 102, 0, alpha})
|
colors[0x08] = color.NRGBA{136, 85, 0, alpha}
|
||||||
loresSquares[0x0A].Fill(color.NRGBA{170, 170, 170, alpha})
|
colors[0x09] = color.NRGBA{255, 102, 0, alpha}
|
||||||
loresSquares[0x0B].Fill(color.NRGBA{255, 153, 136, alpha})
|
colors[0x0A] = color.NRGBA{170, 170, 170, alpha}
|
||||||
loresSquares[0x0C].Fill(color.NRGBA{17, 221, 0, alpha})
|
colors[0x0B] = color.NRGBA{255, 153, 136, alpha}
|
||||||
loresSquares[0x0D].Fill(color.NRGBA{255, 255, 0, alpha})
|
colors[0x0C] = color.NRGBA{17, 221, 0, alpha}
|
||||||
loresSquares[0x0E].Fill(color.NRGBA{68, 255, 153, alpha})
|
colors[0x0D] = color.NRGBA{255, 255, 0, alpha}
|
||||||
loresSquares[0x0F].Fill(color.NRGBA{255, 255, 255, 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
|
// Init the video data structures used for rendering
|
||||||
func Init() {
|
func Init() {
|
||||||
ShowFPS = false
|
ShowFPS = false
|
||||||
|
Monochrome = true
|
||||||
|
|
||||||
initTextCharMap()
|
initTextCharMap()
|
||||||
initLoresSquares()
|
initLoresSquares()
|
||||||
|
@ -95,8 +109,8 @@ func drawText(screen *ebiten.Image, x int, y int, value uint8) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.GeoM.Scale(ScreenSizeFactor, ScreenSizeFactor)
|
op.GeoM.Scale(2, 2)
|
||||||
op.GeoM.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y))
|
op.GeoM.Translate(2*7*float64(x), 2*8*float64(y))
|
||||||
|
|
||||||
r := image.Rect(0, 0, 7, 8)
|
r := image.Rect(0, 0, 7, 8)
|
||||||
op.SourceRect = &r
|
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)
|
op.ColorM.Translate(1, 1, 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make it look greenish
|
if Monochrome {
|
||||||
op.ColorM.Scale(0.20, 0.75, 0.20, 1)
|
// Make it look greenish
|
||||||
|
op.ColorM.Scale(0.20, 0.75, 0.20, 1)
|
||||||
|
}
|
||||||
|
|
||||||
return screen.DrawImage(charMap[value], op)
|
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
|
// 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(2, 2)
|
||||||
op.GeoM.Translate(ScreenSizeFactor*7*float64(x), ScreenSizeFactor*8*float64(y)+float64(i)*4)
|
op.GeoM.Translate(2*7*float64(x), 2*8*float64(y)+2*float64(i)*4)
|
||||||
if err := screen.DrawImage(loresSquares[values[i]], op); err != nil {
|
|
||||||
|
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
|
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.
|
// 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 {
|
pixels := make([]byte, 560*384*4)
|
||||||
panic("Hires mode for ScreenSizeFactor != 1 not implemented")
|
halfPixels := make([]byte, 14)
|
||||||
}
|
|
||||||
|
|
||||||
pixels := make([]byte, 280*192*4)
|
|
||||||
|
|
||||||
// Loop over all hires lines
|
// Loop over all hires lines
|
||||||
for y := 0; y < 192; y++ {
|
for y := 0; y < 192; y++ {
|
||||||
|
@ -207,21 +228,65 @@ func drawHiresScreen(screen *ebiten.Image) error {
|
||||||
yOffset += 0x2000
|
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++ {
|
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]
|
||||||
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++ {
|
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
|
value = value >> 1
|
||||||
p := (y*280 + x*7 + bit) * 4
|
}
|
||||||
|
|
||||||
pixels[p+0] = byte(0xff * float64(0.20) * b)
|
for hp = 0; hp < 14; hp++ {
|
||||||
pixels[p+1] = byte(0xff * float64(0.75) * b)
|
// Update the color bit in colorPos with the half pixel value
|
||||||
pixels[p+2] = byte(0xff * float64(0.20) * b)
|
color &= ((1 << colorPos) ^ 0xf)
|
||||||
pixels[p+3] = 0xff
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue