2018-05-09 16:34:10 +00:00
|
|
|
package video
|
2018-04-29 19:41:11 +00:00
|
|
|
|
|
|
|
import (
|
2018-05-14 09:33:49 +00:00
|
|
|
"fmt"
|
2018-04-29 19:41:11 +00:00
|
|
|
"image"
|
2018-05-12 17:55:36 +00:00
|
|
|
"image/color"
|
2018-04-29 19:41:11 +00:00
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
2018-05-09 16:34:10 +00:00
|
|
|
"github.com/hajimehoshi/ebiten/ebitenutil"
|
2018-05-27 10:05:00 +00:00
|
|
|
|
2019-11-02 13:33:05 +00:00
|
|
|
"github.com/freewilll/apple2-go/mmu"
|
2018-04-29 19:41:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2018-05-28 16:31:52 +00:00
|
|
|
textVideoMemory = 0x400 // Base location of page 1 text video memory
|
|
|
|
flashFrames = 11 // Number of frames when FLASH mode is toggled
|
2018-04-29 19:41:11 +00:00
|
|
|
)
|
|
|
|
|
2018-05-27 22:07:40 +00:00
|
|
|
// drawTextLoresByte is a function definition used for mixed text/lores rendering
|
|
|
|
type drawTextLoresByte func(*ebiten.Image, int, int, uint8) error
|
|
|
|
|
2018-04-29 19:41:11 +00:00
|
|
|
var (
|
2019-11-25 19:38:58 +00:00
|
|
|
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
|
2018-05-28 16:31:52 +00:00
|
|
|
|
|
|
|
// ShowFPS determines if the FPS is shown in the corner of the video
|
2019-11-25 19:38:58 +00:00
|
|
|
ShowFPS bool
|
|
|
|
Monochrome bool
|
2018-04-29 19:41:11 +00:00
|
|
|
)
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// initLoresSquares creates 16 colored squares for the lores renderer
|
|
|
|
func initLoresSquares() {
|
|
|
|
var err error
|
2018-05-12 17:55:36 +00:00
|
|
|
for i := 0; i < 16; i++ {
|
2019-11-25 19:38:58 +00:00
|
|
|
monochromeLoresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
colorLoresSquares[i], err = ebiten.NewImage(7, 4, ebiten.FilterNearest)
|
2018-05-12 17:55:36 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// From
|
2018-05-27 10:48:03 +00:00
|
|
|
// https://mrob.com/pub/xgithub.com/freewilll/apple2/colors.html
|
2018-05-12 17:55:36 +00:00
|
|
|
// https://archive.org/details/IIgs_2523063_Master_Color_Values
|
|
|
|
alpha := uint8(0xff)
|
2019-11-25 19:38:58 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2018-05-27 21:58:19 +00:00
|
|
|
}
|
2018-05-14 09:33:49 +00:00
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Init the video data structures used for rendering
|
|
|
|
func Init() {
|
2018-05-14 09:33:49 +00:00
|
|
|
ShowFPS = false
|
2019-11-25 19:38:58 +00:00
|
|
|
Monochrome = true
|
2018-05-27 21:58:19 +00:00
|
|
|
|
|
|
|
initTextCharMap()
|
|
|
|
initLoresSquares()
|
2018-05-12 17:55:36 +00:00
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// drawText draws a single text character at x, y. The characters are either normal, inverted or flashing
|
2018-05-15 20:54:33 +00:00
|
|
|
func drawText(screen *ebiten.Image, x int, y int, value uint8) error {
|
2018-05-27 21:58:19 +00:00
|
|
|
// Determine if the character is inverted
|
2018-05-12 17:55:36 +00:00
|
|
|
inverted := false
|
|
|
|
|
|
|
|
if (value & 0xc0) == 0 {
|
2018-05-27 21:58:19 +00:00
|
|
|
// Inverted
|
2018-05-12 17:55:36 +00:00
|
|
|
inverted = true
|
|
|
|
} else if (value & 0x80) == 0 {
|
2018-05-27 21:58:19 +00:00
|
|
|
// Flashing
|
2018-05-12 17:55:36 +00:00
|
|
|
value = value & 0x3f
|
|
|
|
inverted = flashOn
|
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Convert the value to a index for the charMap
|
2018-05-12 17:55:36 +00:00
|
|
|
if !inverted {
|
|
|
|
value = value & 0x7f
|
|
|
|
}
|
|
|
|
|
|
|
|
if value < 0x20 {
|
|
|
|
value += 0x40
|
|
|
|
}
|
|
|
|
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
2019-11-25 19:38:58 +00:00
|
|
|
op.GeoM.Scale(2, 2)
|
|
|
|
op.GeoM.Translate(2*7*float64(x), 2*8*float64(y))
|
2018-05-12 17:55:36 +00:00
|
|
|
|
2018-05-29 18:51:19 +00:00
|
|
|
r := image.Rect(0, 0, 7, 8)
|
2018-05-12 17:55:36 +00:00
|
|
|
op.SourceRect = &r
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// The charMap is already inverted. Invert it back if we ourselves aren't inverted.
|
2018-05-29 18:51:19 +00:00
|
|
|
if inverted {
|
2018-05-12 17:55:36 +00:00
|
|
|
op.ColorM.Scale(-1, -1, -1, 1)
|
|
|
|
op.ColorM.Translate(1, 1, 1, 0)
|
|
|
|
}
|
|
|
|
|
2019-11-25 19:38:58 +00:00
|
|
|
if Monochrome {
|
|
|
|
// Make it look greenish
|
|
|
|
op.ColorM.Scale(0.20, 0.75, 0.20, 1)
|
|
|
|
}
|
2018-05-12 17:55:36 +00:00
|
|
|
|
2018-05-29 18:51:19 +00:00
|
|
|
return screen.DrawImage(charMap[value], op)
|
2018-05-09 16:34:10 +00:00
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// drawLores draws two colored lores squares at the equivalent text location x,y.
|
2018-05-12 17:55:36 +00:00
|
|
|
func drawLores(screen *ebiten.Image, x int, y int, value uint8) error {
|
2018-05-27 21:58:19 +00:00
|
|
|
// Convert the 8 bit value to two 4 bit values
|
2018-05-28 16:31:52 +00:00
|
|
|
var values = [2]uint8{value & 0xf, value >> 4}
|
2018-05-12 17:55:36 +00:00
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Render top & bottom squares
|
2018-05-12 17:55:36 +00:00
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
2019-11-25 19:38:58 +00:00
|
|
|
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 {
|
2018-05-12 17:55:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-27 22:07:40 +00:00
|
|
|
// drawTextLoresBlock draws a number of lines of text or lores from start to end
|
|
|
|
func drawTextLoresBlock(screen *ebiten.Image, start int, end int, drawer drawTextLoresByte) error {
|
2018-05-15 20:54:33 +00:00
|
|
|
for y := start; y < end; y++ {
|
|
|
|
base := 128*(y%8) + 40*(y/8)
|
2018-05-26 21:27:29 +00:00
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Flip to the 2nd page if so toggled
|
2018-05-26 21:27:29 +00:00
|
|
|
if mmu.Page2 {
|
|
|
|
base += 0x400
|
|
|
|
}
|
|
|
|
|
2018-05-15 20:54:33 +00:00
|
|
|
for x := 0; x < 40; x++ {
|
|
|
|
offset := textVideoMemory + base + x
|
2018-05-20 10:02:08 +00:00
|
|
|
value := mmu.ReadPageTable[offset>>8][offset&0xff]
|
2018-05-27 22:07:40 +00:00
|
|
|
if err := drawer(screen, x, y, value); err != nil {
|
2018-05-15 20:54:33 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-27 22:07:40 +00:00
|
|
|
// drawTextBlock draws a number of lines of text from start to end
|
|
|
|
func drawTextBlock(screen *ebiten.Image, start int, end int) error {
|
|
|
|
drawTextLoresBlock(screen, start, end, drawText)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// drawTextBlock draws a number of lores lines from the equivalent text start to end line
|
2018-05-15 20:54:33 +00:00
|
|
|
func drawLoresBlock(screen *ebiten.Image, start int, end int) error {
|
2018-05-27 22:07:40 +00:00
|
|
|
drawTextLoresBlock(screen, start, end, drawLores)
|
2018-05-15 20:54:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// drawTextOrLoresScreen draws a text and/or lores screen depending on the VideoState
|
2018-05-15 20:54:33 +00:00
|
|
|
func drawTextOrLoresScreen(screen *ebiten.Image) error {
|
2018-05-12 17:55:36 +00:00
|
|
|
topHalfIsLowRes := !mmu.VideoState.TextMode
|
|
|
|
bottomHalfIsLowRes := !mmu.VideoState.TextMode && !mmu.VideoState.Mixed
|
|
|
|
|
2018-05-15 20:54:33 +00:00
|
|
|
if !topHalfIsLowRes {
|
|
|
|
drawTextBlock(screen, 0, 20)
|
|
|
|
} else {
|
|
|
|
drawLoresBlock(screen, 0, 20)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bottomHalfIsLowRes {
|
|
|
|
drawTextBlock(screen, 20, 24)
|
|
|
|
} else {
|
|
|
|
drawLoresBlock(screen, 20, 24)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// drawHiresScreen draws an entire hires screen. If it's in mixed mode, the lower end is drawn in text.
|
2018-05-15 20:54:33 +00:00
|
|
|
func drawHiresScreen(screen *ebiten.Image) error {
|
2019-11-25 19:38:58 +00:00
|
|
|
pixels := make([]byte, 560*384*4)
|
|
|
|
halfPixels := make([]byte, 14)
|
2018-05-15 20:54:33 +00:00
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Loop over all hires lines
|
2018-05-15 20:54:33 +00:00
|
|
|
for y := 0; y < 192; y++ {
|
|
|
|
if mmu.VideoState.Mixed && y >= 160 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Woz is a genius
|
|
|
|
yOffset := 0x2000 - (0x3d8)*(y>>6) + 0x80*(y>>3) + 0x400*(y&0x7)
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Flip to the 2nd page if so toggled
|
2018-05-26 21:27:29 +00:00
|
|
|
if mmu.Page2 {
|
|
|
|
yOffset += 0x2000
|
|
|
|
}
|
|
|
|
|
2019-11-25 19:38:58 +00:00
|
|
|
// 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
|
2018-05-15 20:54:33 +00:00
|
|
|
for x := 0; x < 40; x++ {
|
|
|
|
offset := yOffset + x
|
2018-05-20 10:02:08 +00:00
|
|
|
value := mmu.ReadPageTable[offset>>8][offset&0xff]
|
2018-05-15 20:54:33 +00:00
|
|
|
|
2019-11-25 19:38:58 +00:00
|
|
|
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
|
2018-05-15 20:54:33 +00:00
|
|
|
for bit := 0; bit < 7; bit++ {
|
2019-11-25 19:38:58 +00:00
|
|
|
halfPixels[hp] = value & 1
|
|
|
|
hp = hp + 1
|
|
|
|
if hp < 14 {
|
|
|
|
halfPixels[hp] = value & 1
|
|
|
|
hp = hp + 1
|
|
|
|
}
|
2018-05-15 20:54:33 +00:00
|
|
|
value = value >> 1
|
2019-11-25 19:38:58 +00:00
|
|
|
}
|
2018-05-15 20:54:33 +00:00
|
|
|
|
2019-11-25 19:38:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2018-05-15 20:54:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// The hires pixels are read, flush them to the screen
|
2018-05-15 20:54:33 +00:00
|
|
|
screen.ReplacePixels(pixels)
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// Draw text bit at the bottom
|
2018-05-15 20:54:33 +00:00
|
|
|
if mmu.VideoState.Mixed {
|
|
|
|
drawTextBlock(screen, 20, 24)
|
|
|
|
}
|
2018-05-27 21:58:19 +00:00
|
|
|
|
2018-05-15 20:54:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-27 21:58:19 +00:00
|
|
|
// DrawScreen draws a text, lores, hires or combination screen
|
2018-05-15 20:54:33 +00:00
|
|
|
func DrawScreen(screen *ebiten.Image) error {
|
2018-04-29 19:41:11 +00:00
|
|
|
flashCounter--
|
|
|
|
if flashCounter < 0 {
|
|
|
|
flashCounter = flashFrames
|
|
|
|
flashOn = !flashOn
|
|
|
|
}
|
|
|
|
|
|
|
|
if ebiten.IsRunningSlowly() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-15 20:54:33 +00:00
|
|
|
if !mmu.VideoState.HiresMode {
|
|
|
|
drawTextOrLoresScreen(screen)
|
|
|
|
} else {
|
|
|
|
drawHiresScreen(screen)
|
2018-04-29 19:41:11 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 09:33:49 +00:00
|
|
|
if ShowFPS {
|
|
|
|
msg := fmt.Sprintf(`FPS: %0.2f`, ebiten.CurrentFPS())
|
|
|
|
ebitenutil.DebugPrint(screen, msg)
|
|
|
|
}
|
|
|
|
|
2018-04-29 19:41:11 +00:00
|
|
|
return nil
|
|
|
|
}
|