o performance

- added performance package
    - moved fps() function from gopher.go to new package
    - added percentage of full-speed indicator to fps

o gopher2600
    - changed FPS mode to PERFORMANCE

o polycounter
    - squeezed a little more time out of polycounter by changing the
    receivers of some frequently called functions to pointer receivers.

o television / colors
    - changed how colors are transalted from signals
    - the color specifications are pre-processed in an init() function
This commit is contained in:
steve 2019-05-04 09:54:08 +01:00
parent 6272b90892
commit 1073c53724
13 changed files with 260 additions and 426 deletions

View file

@ -8,21 +8,14 @@ import (
"gopher2600/debugger/console"
"gopher2600/disassembly"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/gui/sdl"
"gopher2600/hardware"
"gopher2600/performance"
"gopher2600/playmode"
"gopher2600/recorder"
"gopher2600/regression"
"gopher2600/television"
"io"
"os"
"path"
"runtime"
"runtime/pprof"
"strings"
"sync/atomic"
"time"
)
const defaultInitScript = ".gopher2600/debuggerInit"
@ -35,12 +28,12 @@ func main() {
var modeFlags *flag.FlagSet
var modeFlagsParse func()
progModes := []string{"RUN", "PLAY", "DEBUG", "DISASM", "FPS", "REGRESS"}
progModes := []string{"RUN", "PLAY", "DEBUG", "DISASM", "PERFORMANCE", "REGRESS"}
defaultMode := "RUN"
progFlags := flag.NewFlagSet(progName, flag.ContinueOnError)
// prevent Parse() from outputting it's own error messages
// prevent Parse() (part of the flag package) from outputting it's own error messages
progFlags.SetOutput(&nopWriter{})
err := progFlags.Parse(os.Args[1:])
@ -205,7 +198,7 @@ func main() {
os.Exit(2)
}
case "FPS":
case "PERFORMANCE":
display := modeFlags.Bool("display", false, "display TV output: boolean")
tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
scaling := modeFlags.Float64("scale", 3.0, "television scaling")
@ -218,7 +211,7 @@ func main() {
fmt.Println("* 2600 cartridge required")
os.Exit(2)
case 1:
err := fps(*profile, modeFlags.Arg(0), *display, *tvType, float32(*scaling), *runTime)
err := performance.Check(os.Stdout, *profile, modeFlags.Arg(0), *display, *tvType, float32(*scaling), *runTime)
if err != nil {
fmt.Printf("* %s\n", err)
os.Exit(2)
@ -336,117 +329,6 @@ func main() {
}
}
func fps(profile bool, cartridgeFile string, display bool, tvType string, scaling float32, runTime string) error {
var fpstv television.Television
var err error
if display {
fpstv, err = sdl.NewGUI(tvType, scaling, nil)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
err = fpstv.(gui.GUI).SetFeature(gui.ReqSetVisibility, true)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
} else {
fpstv, err = television.NewBasicTelevision("NTSC")
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
}
vcs, err := hardware.NewVCS(fpstv)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
err = vcs.AttachCartridge(cartridgeFile)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// write cpu profile
if profile {
f, err := os.Create("cpu.profile")
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
defer pprof.StopCPUProfile()
}
// get starting frame number
fn, err := fpstv.GetState(television.ReqFramenum)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
startFrame := fn
// run for specified period of time
// -- parse supplied duration
duration, err := time.ParseDuration(runTime)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// -- setup trigger that expires when duration has elapsed
var timerRunning atomic.Value
timerRunning.Store(1)
go func() {
// force a two second leadtime to allow framerate to settle down
time.AfterFunc(2*time.Second, func() {
fn, _ = fpstv.GetState(television.ReqFramenum)
startFrame = fn
time.AfterFunc(duration, func() {
timerRunning.Store(-1)
})
})
}()
// -- run until specified time elapses (running is changed to -1)
err = vcs.Run(func() (bool, error) {
return timerRunning.Load().(int) > 0, nil
})
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// get ending frame number
fn, err = vcs.TV.GetState(television.ReqFramenum)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
endFrame := fn
// calculate and display frames-per-second
frameCount := endFrame - startFrame
fps := float64(frameCount) / duration.Seconds()
fmt.Printf("%.2f fps (%d frames in %.2f seconds)\n", fps, frameCount, duration.Seconds())
// write memory profile
if profile {
f, err := os.Create("mem.profile")
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
runtime.GC()
err = pprof.WriteHeapProfile(f)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
f.Close()
}
return nil
}
// special purpose io.Reader / io.Writer
type nopWriter struct{}

View file

@ -31,7 +31,7 @@ type screen struct {
// altPixels mirrors the pixels array with alternative color palette
// -- useful for switching between regular and debug colors
// -- allocated but only used if tv.allowDebugging and useAltPixels is true
// -- allocated but only used if gtv.allowDebugging and useAltPixels is true
altPixels []byte
altPixelsFade []byte
useAltPixels bool
@ -208,7 +208,8 @@ func (scr *screen) setPixel(pixels *[]byte, x, y int32, red, green, blue byte, v
scr.lastX = x
scr.lastY = y
// do not plot pixel if VBLANK is on. some ROMs use VBLANK to output black
// do not plot pixel if VBLANK is on. some ROMs use VBLANK to output black,
// rather than having to play around with the current color of the sprites
//
// ROMs affected:
// * Custer's Revenge

View file

@ -44,11 +44,13 @@ type CPU struct {
// silently ignore addressing errors unless StrictAddressing is true
StrictAddressing bool
// NoFlowControl sets whehter the cpu responds accurately to instructions that
// affect the flow of the program (branches, JPS, subroutines and interrupts).
// we use this in the disassembly package to make sure we reach every part of
// the program. note that the alteration flow as a result of bank switching is
// still possible.
// NoFlowControl sets whehter the cpu responds accurately to instructions
// that affect the flow of the program (branches, JPS, subroutines and
// interrupts). we use this in the disassembly package to make sure we
// reach every part of the program.
//
// note that the alteration of flow as a result of bank switching is still
// possible even if NoFlowControl is true
NoFlowControl bool
}
@ -202,9 +204,15 @@ func (mc *CPU) read8Bit(address uint16) (uint8, error) {
}
}
return val, mc.endCycle()
err = mc.endCycle()
if err != nil {
return 0, err
}
return val, nil
}
// note that read16Bit calls endCycle as appropriate
func (mc *CPU) read16Bit(address uint16) (uint16, error) {
lo, err := mc.mem.Read(address)
if err != nil {

View file

@ -110,36 +110,36 @@ func (pk *Polycounter) Tick() bool {
}
// Match check whether polycounter is at the given count, any phase
func (pk Polycounter) Match(count int) bool {
func (pk *Polycounter) Match(count int) bool {
return pk.Count == count
}
// MatchPhase checks whether polycounter is at the given count and given phase
func (pk Polycounter) MatchPhase(count, phase int) bool {
func (pk *Polycounter) MatchPhase(count, phase int) bool {
return pk.Count == count && pk.Phase == phase
}
// MatchEnd checks whether polycounter is at the *end* (ie. last phase) of the given count
func (pk Polycounter) MatchEnd(count int) bool {
func (pk *Polycounter) MatchEnd(count int) bool {
return pk.Count == count && pk.Phase == MaxPhase
}
// MatchBeginning checks whether polycounter is at the *beginning* (ie. first phase) of the given count
func (pk Polycounter) MatchBeginning(count int) bool {
func (pk *Polycounter) MatchBeginning(count int) bool {
return pk.Count == count && pk.Phase == 0
}
// Pixel returns the color clock when expressed as a pixel
func (pk Polycounter) Pixel() int {
func (pk *Polycounter) Pixel() int {
return (pk.Count * 4) + pk.Phase - 68
}
// CycleOnNextTick checks to see if polycounter is about to cycle
func (pk Polycounter) CycleOnNextTick() bool {
func (pk *Polycounter) CycleOnNextTick() bool {
return pk.Count == pk.ResetPoint && pk.Phase == MaxPhase
}
// CycledOnLastTick checks to see if polycounter has just cycled
func (pk Polycounter) CycledOnLastTick() bool {
func (pk *Polycounter) CycledOnLastTick() bool {
return pk.Count == 0 && pk.Phase == 0
}

115
performance/fps.go Normal file
View file

@ -0,0 +1,115 @@
package performance
import (
"fmt"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/gui/sdl"
"gopher2600/hardware"
"gopher2600/television"
"io"
"time"
)
// CalcFPS takes the the number of frames and duration and returns the
// frames-per-second and the accuracy of that value as a percentage.
func CalcFPS(ftv television.Television, numFrames int, duration float64) (fps float64, accuracy float64) {
fps = float64(numFrames) / duration
accuracy = 100 * float64(numFrames) / (duration * ftv.GetSpec().FramesPerSecond)
return fps, accuracy
}
// Check is a very rough and ready calculation of the emulator's performance
func Check(output io.Writer, profile bool, cartridgeFile string, display bool, tvType string, scaling float32, runTime string) error {
var ftv television.Television
var err error
// create the "correct" type of TV depending on whether the display flag is
// set or not
if display {
ftv, err = sdl.NewGUI(tvType, scaling, nil)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
err = ftv.(gui.GUI).SetFeature(gui.ReqSetVisibility, true)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
} else {
ftv, err = television.NewBasicTelevision(tvType)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
}
// create vcs using the tv created above
vcs, err := hardware.NewVCS(ftv)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// attach cartridge to te vcs
err = vcs.AttachCartridge(cartridgeFile)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// parse supplied duration
duration, err := time.ParseDuration(runTime)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// get starting frame number (should be 0)
startFrame, err := ftv.GetState(television.ReqFramenum)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// run for specified period of time
err = cpuProfile(profile, "cpu.profile", func() error {
// setup trigger that expires when duration has elapsed
timesUp := make(chan bool)
// force a two second leadtime to allow framerate to settle down and
// then restart timer for the specified duration
go func() {
time.AfterFunc(2*time.Second, func() {
startFrame, _ = ftv.GetState(television.ReqFramenum)
time.AfterFunc(duration, func() {
timesUp <- true
})
})
}()
// run until specified time elapses
err = vcs.Run(func() (bool, error) {
select {
case v := <-timesUp:
return !v, nil
default:
return true, nil
}
})
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
return nil
})
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
// get ending frame number
endFrame, err := vcs.TV.GetState(television.ReqFramenum)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
numFrames := endFrame - startFrame
fps, accuracy := CalcFPS(ftv, numFrames, duration.Seconds())
output.Write([]byte(fmt.Sprintf("%.2f fps (%d frames in %.2f seconds) %.1f%%\n", fps, numFrames, duration.Seconds(), accuracy)))
return memProfile(profile, "mem.profile")
}

42
performance/profiling.go Normal file
View file

@ -0,0 +1,42 @@
package performance
import (
"gopher2600/errors"
"os"
"runtime"
"runtime/pprof"
)
func cpuProfile(profile bool, outFile string, run func() error) error {
if profile {
// write cpu profile
f, err := os.Create(outFile)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
defer pprof.StopCPUProfile()
}
return run()
}
func memProfile(profile bool, outFile string) error {
if profile {
f, err := os.Create(outFile)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
runtime.GC()
err = pprof.WriteHeapProfile(f)
if err != nil {
return errors.NewFormattedError(errors.FPSError, err)
}
f.Close()
}
return nil
}

View file

@ -95,26 +95,26 @@ func NewPlayback(transcript string) (*Playback, error) {
n, err := strconv.Atoi(toks[fieldEvent])
if err != nil {
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:2], fieldSep)))
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldEvent+1], fieldSep)))
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
}
event.event = peripherals.Event(n)
event.frame, err = strconv.Atoi(toks[fieldFrame])
if err != nil {
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:3], fieldSep)))
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldFrame+1], fieldSep)))
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
}
event.scanline, err = strconv.Atoi(toks[fieldScanline])
if err != nil {
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:4], fieldSep)))
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldScanline+1], fieldSep)))
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
}
event.horizpos, err = strconv.Atoi(toks[fieldHorizPos])
if err != nil {
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:5], fieldSep)))
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldHorizPos+1], fieldSep)))
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
}

View file

@ -17,7 +17,10 @@ const (
numFrameFields
)
// FrameRegression is the simplest regression type
// FrameRegression is the simplest regression type. it works by running the
// emulation for N frames and the screen digest recorded at that point.
// regression tests pass if the screen digest after N frames matches the stored
// value.
type FrameRegression struct {
key int
CartFile string
@ -102,4 +105,5 @@ func (reg *FrameRegression) regress(newRegression bool) (bool, error) {
}
func (reg FrameRegression) cleanUp() {
// no cleanup required for frame regression type
}

View file

@ -19,8 +19,10 @@ const (
numPlaybackFields
)
// PlaybackRegression represents a regression type that processes vcs playback
// recording
// PlaybackRegression represents a regression type that processes a VCS
// recording. playback regressions can take a while to run because by their
// nature they extend over many frames - many more than is typical with the
// FrameRegression type.
type PlaybackRegression struct {
key int
Script string
@ -163,5 +165,6 @@ func (reg *PlaybackRegression) regress(newRegression bool) (bool, error) {
}
func (reg PlaybackRegression) cleanUp() {
// ignore errors from remove process
_ = os.Remove(reg.Script)
}

View file

@ -14,9 +14,6 @@ type Handler interface {
// the database
getID() string
// String implements the Stringer interface
String() string
// setKey sets the key value for the regression
setKey(int)
@ -34,6 +31,9 @@ type Handler interface {
// action perfomed when regression entry is removed from database. for
// example, removing additional files from disk
cleanUp()
// String implements the Stringer interface
String() string
}
// RegressList displays all entries in the database

View file

@ -227,7 +227,7 @@ func (btv *BasicTelevision) Signal(sig SignalAttributes) error {
y := int32(btv.scanline)
// decode color using the regular color signal
red, green, blue := btv.spec.TranslateColorSignal(sig.Pixel)
red, green, blue := getColor(btv.spec, sig.Pixel)
for f := range btv.renderers {
err := btv.renderers[f].SetPixel(x, y, red, green, blue, sig.VBlank)
if err != nil {
@ -236,7 +236,7 @@ func (btv *BasicTelevision) Signal(sig SignalAttributes) error {
}
// decode color using the alternative color signal
red, green, blue = btv.spec.TranslateColorSignal(sig.AltPixel)
red, green, blue = getColor(btv.spec, sig.AltPixel)
for f := range btv.renderers {
err := btv.renderers[f].SetAltPixel(x, y, red, green, blue, sig.VBlank)
if err != nil {

View file

@ -5,270 +5,53 @@ package television
type ColorSignal uint16
// VideoBlack is the PixelSignal value that indicates no VCS pixel is to be
// shown
// shown - the need for the video black signal explains why ColorSignal is
// defined to be uint16 and not uint8, as you might expect
const VideoBlack ColorSignal = 0xffff
// color is the color that is show on screen. different television
// specifications interpret colors differently
type color uint32
// components of the translated color signal
const (
red = iota
green
blue
colorDepth
)
// TODO: implement PAL colors
// a color is made of a number of color components
type color [colorDepth]byte
// ntscColors is the list of NTSC colors for each value of possible pixel
var ntscColors = []color{
0x000000,
0x000000,
0x404040,
0x404040,
0x6c6c6c,
0x6c6c6c,
0x909090,
0x909090,
0xb0b0b0,
0xb0b0b0,
0xc8c8c8,
0xc8c8c8,
0xdcdcdc,
0xdcdcdc,
0xececec,
0xececec,
0x444400,
0x444400,
0x646410,
0x646410,
0x848424,
0x848424,
0xa0a034,
0xa0a034,
0xb8b840,
0xb8b840,
0xd0d050,
0xd0d050,
0xe8e85c,
0xe8e85c,
0xfcfc68,
0xfcfc68,
0x702800,
0x702800,
0x844414,
0x844414,
0x985c28,
0x985c28,
0xac783c,
0xac783c,
0xbc8c4c,
0xbc8c4c,
0xcca05c,
0xcca05c,
0xdcb468,
0xdcb468,
0xecc878,
0xecc878,
0x841800,
0x841800,
0x983418,
0x983418,
0xac5030,
0xac5030,
0xc06848,
0xc06848,
0xd0805c,
0xd0805c,
0xe09470,
0xe09470,
0xeca880,
0xeca880,
0xfcbc94,
0xfcbc94,
0x880000,
0x880000,
0x9c2020,
0x9c2020,
0xb03c3c,
0xb03c3c,
0xc05858,
0xc05858,
0xd07070,
0xd07070,
0xe08888,
0xe08888,
0xeca0a0,
0xeca0a0,
0xfcb4b4,
0xfcb4b4,
0x78005c,
0x78005c,
0x8c2074,
0x8c2074,
0xa03c88,
0xa03c88,
0xb0589c,
0xb0589c,
0xc070b0,
0xc070b0,
0xd084c0,
0xd084c0,
0xdc9cd0,
0xdc9cd0,
0xecb0e0,
0xecb0e0,
0x480078,
0x480078,
0x602090,
0x602090,
0x783ca4,
0x783ca4,
0x8c58b8,
0x8c58b8,
0xa070cc,
0xa070cc,
0xb484dc,
0xb484dc,
0xc49cec,
0xc49cec,
0xd4b0fc,
0xd4b0fc,
0x140084,
0x140084,
0x302098,
0x302098,
0x4c3cac,
0x4c3cac,
0x6858c0,
0x6858c0,
0x7c70d0,
0x7c70d0,
0x9488e0,
0x9488e0,
0xa8a0ec,
0xa8a0ec,
0xbcb4fc,
0xbcb4fc,
0x000088,
0x000088,
0x1c209c,
0x1c209c,
0x3840b0,
0x3840b0,
0x505cc0,
0x505cc0,
0x6874d0,
0x6874d0,
0x7c8ce0,
0x7c8ce0,
0x90a4ec,
0x90a4ec,
0xa4b8fc,
0xa4b8fc,
0x00187c,
0x00187c,
0x1c3890,
0x1c3890,
0x3854a8,
0x3854a8,
0x5070bc,
0x5070bc,
0x6888cc,
0x6888cc,
0x7c9cdc,
0x7c9cdc,
0x90b4ec,
0x90b4ec,
0xa4c8fc,
0xa4c8fc,
0x002c5c,
0x002c5c,
0x1c4c78,
0x1c4c78,
0x386890,
0x386890,
0x5084ac,
0x5084ac,
0x689cc0,
0x689cc0,
0x7cb4d4,
0x7cb4d4,
0x90cce8,
0x90cce8,
0xa4e0fc,
0xa4e0fc,
0x003c2c,
0x003c2c,
0x1c5c48,
0x1c5c48,
0x387c64,
0x387c64,
0x509c80,
0x509c80,
0x68b494,
0x68b494,
0x7cd0ac,
0x7cd0ac,
0x90e4c0,
0x90e4c0,
0xa4fcd4,
0xa4fcd4,
0x003c00,
0x003c00,
0x205c20,
0x205c20,
0x407c40,
0x407c40,
0x5c9c5c,
0x5c9c5c,
0x74b474,
0x74b474,
0x8cd08c,
0x8cd08c,
0xa4e4a4,
0xa4e4a4,
0xb8fcb8,
0xb8fcb8,
0x143800,
0x143800,
0x345c1c,
0x345c1c,
0x507c38,
0x507c38,
0x6c9850,
0x6c9850,
0x84b468,
0x84b468,
0x9ccc7c,
0x9ccc7c,
0xb4e490,
0xb4e490,
0xc8fca4,
0xc8fca4,
0x2c3000,
0x2c3000,
0x4c501c,
0x4c501c,
0x687034,
0x687034,
0x848c4c,
0x848c4c,
0x9ca864,
0x9ca864,
0xb4c078,
0xb4c078,
0xccd488,
0xccd488,
0xe0ec9c,
0xe0ec9c,
0x442800,
0x442800,
0x644818,
0x644818,
0x846830,
0x846830,
0xa08444,
0xa08444,
0xb89c58,
0xb89c58,
0xd0b46c,
0xd0b46c,
0xe8cc7c,
0xe8cc7c,
0xfce08c,
0xfce08c}
// colors is the entire palette
type colors []color
// the entire palette is made up of many colors
var colorsNTSC = colors{}
var colorsPAL = colors{}
// the VideoBlack signal results in the following color
var videoBlack = color{0, 0, 0}
// the raw color values are the component values expressed as a single 32 bit
// number. we'll use these raw values in the init() function below to create
// the real palette
var colorsNTSCRaw = []uint32{
0x000000, 0x000000, 0x404040, 0x404040, 0x6c6c6c, 0x6c6c6c, 0x909090, 0x909090, 0xb0b0b0, 0xb0b0b0, 0xc8c8c8, 0xc8c8c8, 0xdcdcdc, 0xdcdcdc, 0xececec, 0xececec, 0x444400, 0x444400, 0x646410, 0x646410, 0x848424, 0x848424, 0xa0a034, 0xa0a034, 0xb8b840, 0xb8b840, 0xd0d050, 0xd0d050, 0xe8e85c, 0xe8e85c, 0xfcfc68, 0xfcfc68, 0x702800, 0x702800, 0x844414, 0x844414, 0x985c28, 0x985c28, 0xac783c, 0xac783c, 0xbc8c4c, 0xbc8c4c, 0xcca05c, 0xcca05c, 0xdcb468, 0xdcb468, 0xecc878, 0xecc878, 0x841800, 0x841800, 0x983418, 0x983418, 0xac5030, 0xac5030, 0xc06848, 0xc06848, 0xd0805c, 0xd0805c, 0xe09470, 0xe09470, 0xeca880, 0xeca880, 0xfcbc94, 0xfcbc94, 0x880000, 0x880000, 0x9c2020, 0x9c2020, 0xb03c3c, 0xb03c3c, 0xc05858, 0xc05858, 0xd07070, 0xd07070, 0xe08888, 0xe08888, 0xeca0a0, 0xeca0a0, 0xfcb4b4, 0xfcb4b4, 0x78005c, 0x78005c, 0x8c2074, 0x8c2074, 0xa03c88, 0xa03c88, 0xb0589c, 0xb0589c, 0xc070b0, 0xc070b0, 0xd084c0, 0xd084c0, 0xdc9cd0, 0xdc9cd0, 0xecb0e0, 0xecb0e0, 0x480078, 0x480078, 0x602090, 0x602090, 0x783ca4, 0x783ca4, 0x8c58b8, 0x8c58b8, 0xa070cc, 0xa070cc, 0xb484dc, 0xb484dc, 0xc49cec, 0xc49cec, 0xd4b0fc, 0xd4b0fc, 0x140084, 0x140084, 0x302098, 0x302098, 0x4c3cac, 0x4c3cac, 0x6858c0, 0x6858c0, 0x7c70d0, 0x7c70d0, 0x9488e0, 0x9488e0, 0xa8a0ec, 0xa8a0ec, 0xbcb4fc, 0xbcb4fc, 0x000088, 0x000088, 0x1c209c, 0x1c209c, 0x3840b0, 0x3840b0, 0x505cc0, 0x505cc0, 0x6874d0, 0x6874d0, 0x7c8ce0, 0x7c8ce0, 0x90a4ec, 0x90a4ec, 0xa4b8fc, 0xa4b8fc, 0x00187c, 0x00187c, 0x1c3890, 0x1c3890, 0x3854a8, 0x3854a8, 0x5070bc, 0x5070bc, 0x6888cc, 0x6888cc, 0x7c9cdc, 0x7c9cdc, 0x90b4ec, 0x90b4ec, 0xa4c8fc, 0xa4c8fc, 0x002c5c, 0x002c5c, 0x1c4c78, 0x1c4c78, 0x386890, 0x386890, 0x5084ac, 0x5084ac, 0x689cc0, 0x689cc0, 0x7cb4d4, 0x7cb4d4, 0x90cce8, 0x90cce8, 0xa4e0fc, 0xa4e0fc, 0x003c2c, 0x003c2c, 0x1c5c48, 0x1c5c48, 0x387c64, 0x387c64, 0x509c80, 0x509c80, 0x68b494, 0x68b494, 0x7cd0ac, 0x7cd0ac, 0x90e4c0, 0x90e4c0, 0xa4fcd4, 0xa4fcd4, 0x003c00, 0x003c00, 0x205c20, 0x205c20, 0x407c40, 0x407c40, 0x5c9c5c, 0x5c9c5c, 0x74b474, 0x74b474, 0x8cd08c, 0x8cd08c, 0xa4e4a4, 0xa4e4a4, 0xb8fcb8, 0xb8fcb8, 0x143800, 0x143800, 0x345c1c, 0x345c1c, 0x507c38, 0x507c38, 0x6c9850, 0x6c9850, 0x84b468, 0x84b468, 0x9ccc7c, 0x9ccc7c, 0xb4e490, 0xb4e490, 0xc8fca4, 0xc8fca4, 0x2c3000, 0x2c3000, 0x4c501c, 0x4c501c, 0x687034, 0x687034, 0x848c4c, 0x848c4c, 0x9ca864, 0x9ca864, 0xb4c078, 0xb4c078, 0xccd488, 0xccd488, 0xe0ec9c, 0xe0ec9c, 0x442800, 0x442800, 0x644818, 0x644818, 0x846830, 0x846830, 0xa08444, 0xa08444, 0xb89c58, 0xb89c58, 0xd0b46c, 0xd0b46c, 0xe8cc7c, 0xe8cc7c, 0xfce08c, 0xfce08c}
func init() {
for i := range colorsNTSCRaw {
col := colorsNTSCRaw[i]
red, green, blue := byte((col&0xff0000)>>16), byte((col&0xff00)>>8), byte(col&0xff)
colorsNTSC = append(colorsNTSC, color{red, green, blue})
// TODO: implement PAL colors correctly. just use NTSC palette for now
colorsPAL = append(colorsPAL, color{red, green, blue})
}
}
// getColor translates a color signal to the individual color components
func getColor(spec *Specification, sig ColorSignal) (byte, byte, byte) {
if sig == VideoBlack {
return videoBlack[red], videoBlack[green], videoBlack[blue]
}
col := spec.Colors[sig]
return col[red], col[green], col[blue]
}

View file

@ -16,22 +16,14 @@ type Specification struct {
VsyncClocks int
Colors []color
Colors colors
IdealTop int
IdealBottom int
IdealScanlines int
}
// TranslateColorSignal decodaes color signal to an RGB value
func (spec Specification) TranslateColorSignal(sig ColorSignal) (byte, byte, byte) {
red, green, blue := byte(0), byte(0), byte(0)
if sig != VideoBlack {
col := spec.Colors[sig]
red, green, blue = byte((col&0xff0000)>>16), byte((col&0xff00)>>8), byte(col&0xff)
}
return red, green, blue
FramesPerSecond float64
SecondsPerFrame float64
}
// SpecNTSC is the specification for NTSC television typee
@ -51,11 +43,13 @@ func init() {
SpecNTSC.ScanlinesPerVisible = 192
SpecNTSC.ScanlinesPerOverscan = 30
SpecNTSC.ScanlinesTotal = 262
SpecNTSC.Colors = ntscColors
SpecNTSC.Colors = colorsNTSC
SpecNTSC.VsyncClocks = SpecNTSC.ScanlinesPerVSync * SpecNTSC.ClocksPerScanline
SpecNTSC.IdealTop = SpecNTSC.ScanlinesPerVSync + SpecNTSC.ScanlinesPerVBlank
SpecNTSC.IdealBottom = SpecNTSC.ScanlinesTotal - SpecNTSC.ScanlinesPerOverscan
SpecNTSC.IdealScanlines = SpecNTSC.IdealBottom - SpecNTSC.IdealTop
SpecNTSC.FramesPerSecond = 60.0
SpecNTSC.SecondsPerFrame = 1.0 / SpecNTSC.FramesPerSecond
SpecPAL = new(Specification)
SpecPAL.ID = "PAL"
@ -70,8 +64,10 @@ func init() {
SpecPAL.IdealTop = SpecPAL.ScanlinesPerVSync + SpecPAL.ScanlinesPerVBlank
SpecPAL.IdealBottom = SpecPAL.ScanlinesTotal - SpecPAL.ScanlinesPerOverscan
SpecPAL.IdealScanlines = SpecPAL.IdealBottom - SpecPAL.IdealTop
SpecPAL.FramesPerSecond = 50.0
SpecPAL.SecondsPerFrame = 1.0 / SpecPAL.FramesPerSecond
// use NTSC colors for PAL specification for now
// TODO: implement PAL colors
SpecPAL.Colors = ntscColors
SpecPAL.Colors = colorsNTSC
}