o debugger / playmode

- removed references to SdlPlay and SdlDebug
    - constructors for debugger and playmode now expect instances of GUI
    and Television, rather than creating them
    - this should help future porting efforts

o peripherals
    - renamed Events to Actions
    - to avoid confusion with Events in the GUI package

o television
    - renamed StellaTelevision to television; a better name because
    it serves as a reference implementation and is the only television
    implementation currently needed.
    - originally, PixelRenderers were implemented as Television that
    embedded StellaTelevision; it made sense to use a more unique name
    - note that we're still keeping and using the Television interface
    - reworked specifications file
This commit is contained in:
steve 2019-11-13 11:09:57 +00:00
parent cc8c14f0ae
commit 8ad481e132
40 changed files with 917 additions and 937 deletions

View file

@ -618,13 +618,13 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
option, _ := tokens.Get()
switch strings.ToUpper(option) {
case "OFF":
err := dbg.gui.SetFeature(gui.ReqSetOverlay, false)
err := dbg.scr.SetFeature(gui.ReqSetOverlay, false)
if err != nil {
dbg.print(console.StyleError, err.Error())
}
dbg.relfectMonitor.Activate(false)
case "ON":
err := dbg.gui.SetFeature(gui.ReqSetOverlay, true)
err := dbg.scr.SetFeature(gui.ReqSetOverlay, true)
if err != nil {
dbg.print(console.StyleError, err.Error())
}
@ -647,14 +647,14 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
if err != nil {
return doNothing, err
}
err = dbg.gui.Reset()
err = dbg.tv.Reset()
if err != nil {
return doNothing, err
}
dbg.print(console.StyleFeedback, "machine reset")
case cmdRun:
if !dbg.gui.IsVisible() && dbg.commandOnStep == "" {
if !dbg.scr.IsVisible() && dbg.commandOnStep == "" {
dbg.print(console.StyleEmulatorInfo, "running with no display or terminal output")
}
dbg.runUntilHalt = true
@ -918,12 +918,12 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
option = strings.ToUpper(option)
switch option {
case "SPEC":
dbg.print(console.StyleInstrument, dbg.gui.GetSpec().ID)
dbg.print(console.StyleInstrument, dbg.tv.GetSpec().ID)
default:
// already caught by command line ValidateTokens()
}
} else {
dbg.printInstrument(dbg.gui)
dbg.printInstrument(dbg.tv)
}
case cmdPanel:
@ -1022,12 +1022,12 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
action = strings.ToUpper(action)
switch action {
case "ON":
err = dbg.gui.SetFeature(gui.ReqSetVisibility, true)
err = dbg.scr.SetFeature(gui.ReqSetVisibility, true)
if err != nil {
return doNothing, err
}
case "OFF":
err = dbg.gui.SetFeature(gui.ReqSetVisibility, false)
err = dbg.scr.SetFeature(gui.ReqSetVisibility, false)
if err != nil {
return doNothing, err
}
@ -1036,17 +1036,17 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
action = strings.ToUpper(action)
switch action {
case "OFF":
err = dbg.gui.SetFeature(gui.ReqSetMasking, false)
err = dbg.scr.SetFeature(gui.ReqSetMasking, false)
if err != nil {
return doNothing, err
}
case "ON":
err = dbg.gui.SetFeature(gui.ReqSetMasking, true)
err = dbg.scr.SetFeature(gui.ReqSetMasking, true)
if err != nil {
return doNothing, err
}
default:
err = dbg.gui.SetFeature(gui.ReqToggleMasking)
err = dbg.scr.SetFeature(gui.ReqToggleMasking)
if err != nil {
return doNothing, err
}
@ -1062,24 +1062,24 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
return doNothing, errors.New(errors.CommandError, fmt.Sprintf("%s %s value not valid (%s)", command, action, scl))
}
err = dbg.gui.SetFeature(gui.ReqSetScale, float32(scale))
err = dbg.scr.SetFeature(gui.ReqSetScale, float32(scale))
return doNothing, err
case "ALT":
action, _ := tokens.Get()
action = strings.ToUpper(action)
switch action {
case "OFF":
err = dbg.gui.SetFeature(gui.ReqSetAltColors, false)
err = dbg.scr.SetFeature(gui.ReqSetAltColors, false)
if err != nil {
return doNothing, err
}
case "ON":
err = dbg.gui.SetFeature(gui.ReqSetAltColors, true)
err = dbg.scr.SetFeature(gui.ReqSetAltColors, true)
if err != nil {
return doNothing, err
}
default:
err = dbg.gui.SetFeature(gui.ReqToggleAltColors)
err = dbg.scr.SetFeature(gui.ReqToggleAltColors)
if err != nil {
return doNothing, err
}
@ -1093,23 +1093,23 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
action = strings.ToUpper(action)
switch action {
case "OFF":
err = dbg.gui.SetFeature(gui.ReqSetOverlay, false)
err = dbg.scr.SetFeature(gui.ReqSetOverlay, false)
if err != nil {
return doNothing, err
}
case "ON":
err = dbg.gui.SetFeature(gui.ReqSetOverlay, true)
err = dbg.scr.SetFeature(gui.ReqSetOverlay, true)
if err != nil {
return doNothing, err
}
default:
err = dbg.gui.SetFeature(gui.ReqToggleOverlay)
err = dbg.scr.SetFeature(gui.ReqToggleOverlay)
if err != nil {
return doNothing, err
}
}
default:
err = dbg.gui.SetFeature(gui.ReqToggleVisibility)
err = dbg.scr.SetFeature(gui.ReqToggleVisibility)
if err != nil {
return doNothing, err
}
@ -1121,7 +1121,7 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
stick, _ := tokens.Get()
action, _ := tokens.Get()
var event peripherals.Event
var event peripherals.Action
switch strings.ToUpper(action) {
case "UP":
event = peripherals.Up

View file

@ -9,7 +9,6 @@ import (
"gopher2600/disassembly"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/gui/sdldebug"
"gopher2600/hardware"
"gopher2600/hardware/cpu/definitions"
"gopher2600/screendigest"
@ -31,8 +30,9 @@ type Debugger struct {
disasm *disassembly.Disassembly
// gui/tv
tv television.Television
scr gui.GUI
digest *screendigest.SHA1
gui gui.GUI
// whether the debugger is to continue with the debugging loop
// set to false only when debugger is to finish
@ -122,29 +122,18 @@ type Debugger struct {
// NewDebugger creates and initialises everything required for a new debugging
// session. Use the Start() method to actually begin the session.
func NewDebugger(tvType string) (*Debugger, error) {
func NewDebugger(tv television.Television, scr gui.GUI) (*Debugger, error) {
var err error
dbg := new(Debugger)
dbg := &Debugger{tv: tv, scr: scr}
// prepare gui/tv
btv, err := television.NewStellaTelevision(tvType)
if err != nil {
return nil, errors.New(errors.DebuggerError, err)
}
dbg.digest, err = screendigest.NewSHA1(tvType, btv)
if err != nil {
return nil, errors.New(errors.DebuggerError, err)
}
dbg.gui, err = sdldebug.NewSdlDebug(tvType, 2.0, btv)
dbg.digest, err = screendigest.NewSHA1(dbg.tv)
if err != nil {
return nil, errors.New(errors.DebuggerError, err)
}
// create a new VCS instance
dbg.vcs, err = hardware.NewVCS(dbg.gui)
dbg.vcs, err = hardware.NewVCS(dbg.tv)
if err != nil {
return nil, errors.New(errors.DebuggerError, err)
}
@ -157,7 +146,7 @@ func NewDebugger(tvType string) (*Debugger, error) {
dbg.dbgmem = &memoryDebug{mem: dbg.vcs.Mem, symtable: &dbg.disasm.Symtable}
// set up reflection monitor
dbg.relfectMonitor = reflection.NewMonitor(dbg.vcs, dbg.gui)
dbg.relfectMonitor = reflection.NewMonitor(dbg.vcs, dbg.scr)
dbg.relfectMonitor.Activate(true)
// set up breakpoints/traps
@ -183,7 +172,7 @@ func NewDebugger(tvType string) (*Debugger, error) {
signal.Notify(dbg.intChan, os.Interrupt)
// connect debugger to gui
dbg.gui.SetEventChannel(dbg.guiChan)
dbg.scr.SetEventChannel(dbg.guiChan)
return dbg, nil
}
@ -386,7 +375,7 @@ func (dbg *Debugger) inputLoop(inputter console.UserInput, videoCycle bool) erro
// enter halt state
if dbg.inputloopHalt {
// pause tv when emulation has halted
err = dbg.gui.SetFeature(gui.ReqSetPause, true)
err = dbg.scr.SetFeature(gui.ReqSetPause, true)
if err != nil {
return err
}
@ -477,7 +466,7 @@ func (dbg *Debugger) inputLoop(inputter console.UserInput, videoCycle bool) erro
// make sure tv is unpaused if emulation is about to resume
if dbg.inputloopNext {
err = dbg.gui.SetFeature(gui.ReqSetPause, false)
err = dbg.scr.SetFeature(gui.ReqSetPause, false)
if err != nil {
return err
}

View file

@ -17,7 +17,7 @@ func (dbg *Debugger) guiEventHandler(event gui.Event) error {
data := event.Data.(gui.EventDataKeyboard)
// check playmode key presses first
err = playmode.KeyboardEventHandler(data, dbg.gui, dbg.vcs)
err = playmode.KeyboardEventHandler(data, dbg.scr, dbg.vcs)
if err != nil {
break // switch event.ID
}
@ -26,11 +26,11 @@ func (dbg *Debugger) guiEventHandler(event gui.Event) error {
switch data.Key {
case "`":
// back-tick: toggle masking
err = dbg.gui.SetFeature(gui.ReqToggleMasking)
err = dbg.scr.SetFeature(gui.ReqToggleMasking)
case "1":
// toggle debugging colours
err = dbg.gui.SetFeature(gui.ReqToggleAltColors)
err = dbg.scr.SetFeature(gui.ReqToggleAltColors)
case "2":
// toggle overlay
@ -39,16 +39,16 @@ func (dbg *Debugger) guiEventHandler(event gui.Event) error {
// return errors.New(errors.ReflectionNotRunning)
// }
err = dbg.gui.SetFeature(gui.ReqToggleOverlay)
err = dbg.scr.SetFeature(gui.ReqToggleOverlay)
case "=":
fallthrough // equal sign is the same as plus, for convenience
case "+":
// increase scaling
err = dbg.gui.SetFeature(gui.ReqIncScale)
err = dbg.scr.SetFeature(gui.ReqIncScale)
case "-":
// decrease window scanling
err = dbg.gui.SetFeature(gui.ReqDecScale)
err = dbg.scr.SetFeature(gui.ReqDecScale)
}
}

View file

@ -102,7 +102,7 @@ const (
// tv
UnknownTVRequest
StellaTelevision
Television
// screen digest
ScreenDigest

View file

@ -87,12 +87,12 @@ var messages = map[Errno]string{
PeriphHardwareUnavailable: "peripheral error: controller hardware unavailable (%s)",
UnknownPeriphEvent: "peripheral error: %s: unsupported event (%v)",
// tv
UnknownTVRequest: "tv error: unsupported request (%v)",
StellaTelevision: "tv error: StellaTV: %s",
// television
UnknownTVRequest: "television error: unsupported request (%v)",
Television: "television error: %s",
// screen digest
ScreenDigest: "tv error: DigestTV: %s",
ScreenDigest: "television error: screendigest: %s",
// gui
UnknownGUIRequest: "gui error: unsupported request (%v)",

View file

@ -8,12 +8,16 @@ import (
"gopher2600/debugger/colorterm"
"gopher2600/debugger/console"
"gopher2600/disassembly"
"gopher2600/gui"
"gopher2600/gui/sdldebug"
"gopher2600/gui/sdlplay"
"gopher2600/magicflags"
"gopher2600/paths"
"gopher2600/performance"
"gopher2600/playmode"
"gopher2600/recorder"
"gopher2600/regression"
"gopher2600/television"
"io"
"math/rand"
"os"
@ -96,7 +100,19 @@ func play(mf *magicflags.MagicFlags) bool {
Format: *cartFormat,
}
err := playmode.Play(*tvType, float32(*scaling), *stable, *record, cartload)
tv, err := television.NewTelevision(*tvType)
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling))
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
err = playmode.Play(tv, scr, *stable, *record, cartload)
if err != nil {
fmt.Printf("* %s\n", err)
return false
@ -123,7 +139,19 @@ func debug(mf *magicflags.MagicFlags) bool {
return false
}
dbg, err := debugger.NewDebugger(*tvType)
tv, err := television.NewTelevision(*tvType)
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
scr, err := sdldebug.NewSdlDebug(tv, 2.0)
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
dbg, err := debugger.NewDebugger(tv, scr)
if err != nil {
fmt.Printf("* %s\n", err)
return false
@ -242,7 +270,28 @@ func perform(mf *magicflags.MagicFlags) bool {
Filename: mf.SubModeFlags.Arg(0),
Format: *cartFormat,
}
err := performance.Check(os.Stdout, *profile, *display, *tvType, float32(*scaling), *runTime, cartload)
tv, err := television.NewTelevision(*tvType)
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
if *display {
scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling))
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
err = scr.(gui.GUI).SetFeature(gui.ReqSetVisibility, true)
if err != nil {
fmt.Printf("* %s\n", err)
return false
}
}
err = performance.Check(os.Stdout, *profile, tv, *runTime, cartload)
if err != nil {
fmt.Printf("* %s\n", err)
return false

53
gui/events.go Normal file
View file

@ -0,0 +1,53 @@
package gui
// Events are the things that happen in the gui, as a result of user interaction,
// and sent over a registered event channel.
//
// Do not confuse this with the peripheral Action type.
// EventID idintifies the type of event taking place
type EventID int
// list of valid events
const (
EventWindowClose EventID = iota
EventKeyboard
EventMouseLeft
EventMouseRight
)
// KeyMod identifies
type KeyMod int
// list of valud key modifiers
const (
KeyModNone KeyMod = iota
KeyModShift
KeyModCtrl
KeyModAlt
)
// EventData represents the data that is associated with an event
type EventData interface{}
// Event is the structure that is passed over the event channel
type Event struct {
ID EventID
Data EventData
}
// EventDataKeyboard is the data that accompanies EvenKeyboard events
type EventDataKeyboard struct {
Key string
Down bool
Mod KeyMod
}
// EventDataMouse is the data that accompanies EventMouse events
type EventDataMouse struct {
Down bool
X int
Y int
HorizPos int
Scanline int
}

View file

@ -1,9 +1,5 @@
package gui
import (
"gopher2600/television"
)
// GUI defines the operations that can be performed on visual user interfaces.
//
// Currently, GUI implementations expect also to be an instance of
@ -12,9 +8,7 @@ import (
// Renderer and AudioMixer interfaces from the television packages but this is
// not mandated by the GUI interface.
type GUI interface {
television.Television
// All GUIs should implement a MetaPixelRenderer even if only as a stub
// All GUIs should implement a MetaPixelRenderer even if only a stub
MetaPixelRenderer
// returns true if GUI is currently visible. false if not
@ -29,73 +23,3 @@ type GUI interface {
// purpose.
SetEventChannel(chan (Event))
}
// FeatureReq is used to request the setting of a gui attribute
// eg. toggling the overlay
type FeatureReq int
// list of valid feature requests. argument must be of the type specified or
// else the interface{} type conversion will fail and the application will
// probably crash
const (
ReqSetVisibility FeatureReq = iota // bool, optional bool (update on show) default true
ReqToggleVisibility // optional bool (update on show) default true
ReqSetVisibilityStable // none
ReqSetPause // bool
ReqSetMasking // bool
ReqToggleMasking // none
ReqSetAltColors // bool
ReqToggleAltColors // none
ReqSetOverlay // bool
ReqToggleOverlay // none
ReqSetScale // float
ReqIncScale // none
ReqDecScale // none
)
// EventID idintifies the type of event taking place
type EventID int
// list of valid events
const (
EventWindowClose EventID = iota
EventKeyboard
EventMouseLeft
EventMouseRight
)
// KeyMod identifies
type KeyMod int
// list of valud key modifiers
const (
KeyModNone KeyMod = iota
KeyModShift
KeyModCtrl
KeyModAlt
)
// EventData represents the data that is associated with an event
type EventData interface{}
// Event is the structure that is passed over the event channel
type Event struct {
ID EventID
Data EventData
}
// EventDataKeyboard is the data that accompanies EvenKeyboard events
type EventDataKeyboard struct {
Key string
Down bool
Mod KeyMod
}
// EventDataMouse is the data that accompanies EventMouse events
type EventDataMouse struct {
Down bool
X int
Y int
HorizPos int
Scanline int
}

1
gui/headless.go Normal file
View file

@ -0,0 +1 @@
package gui

24
gui/requests.go Normal file
View file

@ -0,0 +1,24 @@
package gui
// FeatureReq is used to request the setting of a gui attribute
// eg. toggling the overlay
type FeatureReq int
// list of valid feature requests. argument must be of the type specified or
// else the interface{} type conversion will fail and the application will
// probably crash
const (
ReqSetVisibility FeatureReq = iota // bool, optional bool (update on show) default true
ReqToggleVisibility // optional bool (update on show) default true
ReqSetVisibilityStable // none
ReqSetPause // bool
ReqSetMasking // bool
ReqToggleMasking // none
ReqSetAltColors // bool
ReqToggleAltColors // none
ReqSetOverlay // bool
ReqToggleOverlay // none
ReqSetScale // float
ReqIncScale // none
ReqDecScale // none
)

View file

@ -8,15 +8,15 @@ import (
)
// guiLoop listens for SDL events and is run concurrently
func (pxtv *SdlDebug) guiLoop() {
func (scr *SdlDebug) guiLoop() {
for {
sdlEvent := sdl.WaitEvent()
switch sdlEvent := sdlEvent.(type) {
// close window
case *sdl.QuitEvent:
pxtv.SetFeature(gui.ReqSetVisibility, false)
pxtv.eventChannel <- gui.Event{ID: gui.EventWindowClose}
scr.SetFeature(gui.ReqSetVisibility, false)
scr.eventChannel <- gui.Event{ID: gui.EventWindowClose}
case *sdl.KeyboardEvent:
mod := gui.KeyModNone
@ -35,7 +35,7 @@ func (pxtv *SdlDebug) guiLoop() {
switch sdlEvent.Type {
case sdl.KEYDOWN:
if sdlEvent.Repeat == 0 {
pxtv.eventChannel <- gui.Event{
scr.eventChannel <- gui.Event{
ID: gui.EventKeyboard,
Data: gui.EventDataKeyboard{
Key: sdl.GetKeyName(sdlEvent.Keysym.Sym),
@ -44,7 +44,7 @@ func (pxtv *SdlDebug) guiLoop() {
}
case sdl.KEYUP:
if sdlEvent.Repeat == 0 {
pxtv.eventChannel <- gui.Event{
scr.eventChannel <- gui.Event{
ID: gui.EventKeyboard,
Data: gui.EventDataKeyboard{
Key: sdl.GetKeyName(sdlEvent.Keysym.Sym),
@ -54,13 +54,13 @@ func (pxtv *SdlDebug) guiLoop() {
}
case *sdl.MouseButtonEvent:
hp, sl := pxtv.convertMouseCoords(sdlEvent)
hp, sl := scr.convertMouseCoords(sdlEvent)
switch sdlEvent.Type {
case sdl.MOUSEBUTTONDOWN:
switch sdlEvent.Button {
case sdl.BUTTON_LEFT:
pxtv.eventChannel <- gui.Event{
scr.eventChannel <- gui.Event{
ID: gui.EventMouseLeft,
Data: gui.EventDataMouse{
Down: true,
@ -70,7 +70,7 @@ func (pxtv *SdlDebug) guiLoop() {
Scanline: sl}}
case sdl.BUTTON_RIGHT:
pxtv.eventChannel <- gui.Event{
scr.eventChannel <- gui.Event{
ID: gui.EventMouseRight,
Data: gui.EventDataMouse{
Down: true,
@ -84,7 +84,7 @@ func (pxtv *SdlDebug) guiLoop() {
switch sdlEvent.Button {
case sdl.BUTTON_LEFT:
pxtv.eventChannel <- gui.Event{
scr.eventChannel <- gui.Event{
ID: gui.EventMouseLeft,
Data: gui.EventDataMouse{
Down: false,
@ -94,7 +94,7 @@ func (pxtv *SdlDebug) guiLoop() {
Scanline: sl}}
case sdl.BUTTON_RIGHT:
pxtv.eventChannel <- gui.Event{
scr.eventChannel <- gui.Event{
ID: gui.EventMouseRight,
Data: gui.EventDataMouse{
Down: false,
@ -116,16 +116,16 @@ func (pxtv *SdlDebug) guiLoop() {
}
}
func (pxtv *SdlDebug) convertMouseCoords(sdlEvent *sdl.MouseButtonEvent) (int, int) {
func (scr *SdlDebug) convertMouseCoords(sdlEvent *sdl.MouseButtonEvent) (int, int) {
var hp, sl int
sx, sy := pxtv.pxl.renderer.GetScale()
sx, sy := scr.pxl.renderer.GetScale()
// convert X pixel value to horizpos equivalent
// the opposite of pixelX() and also the scalining applied
// by the SDL renderer
if pxtv.pxl.unmasked {
hp = int(float32(sdlEvent.X)/sx) - television.ClocksPerHblank
if scr.pxl.unmasked {
hp = int(float32(sdlEvent.X)/sx) - television.HorizClksHBlank
} else {
hp = int(float32(sdlEvent.X) / sx)
}
@ -133,10 +133,10 @@ func (pxtv *SdlDebug) convertMouseCoords(sdlEvent *sdl.MouseButtonEvent) (int, i
// convert Y pixel value to scanline equivalent
// the opposite of pixelY() and also the scalining applied
// by the SDL renderer
if pxtv.pxl.unmasked {
if scr.pxl.unmasked {
sl = int(float32(sdlEvent.Y) / sy)
} else {
sl = int(float32(sdlEvent.Y)/sy) + int(pxtv.pxl.playTop)
sl = int(float32(sdlEvent.Y)/sy) + int(scr.pxl.playTop)
}
return hp, sl

View file

@ -109,6 +109,6 @@ func (ovl *metapixelOverlay) update(paused bool) error {
}
// SetMetaPixel recieves (and processes) additional emulator information from the emulator
func (pxtv *SdlDebug) SetMetaPixel(sig gui.MetaPixel) error {
return pxtv.pxl.metaPixels.setPixel(sig)
func (scr *SdlDebug) SetMetaPixel(sig gui.MetaPixel) error {
return scr.pxl.metaPixels.setPixel(sig)
}

View file

@ -97,12 +97,12 @@ func (pxl *pixels) reset() error {
func (pxl *pixels) resize(topScanline, numScanlines int) error {
var err error
pxl.maxWidth = int32(television.ClocksPerScanline)
pxl.maxWidth = int32(television.HorizClksScanline)
pxl.maxHeight = int32(pxl.scr.GetSpec().ScanlinesTotal)
pxl.maxMask = &sdl.Rect{X: 0, Y: 0, W: pxl.maxWidth, H: pxl.maxHeight}
pxl.playTop = int32(topScanline)
pxl.playWidth = int32(television.ClocksPerVisible)
pxl.playWidth = int32(television.HorizClksVisible)
pxl.setPlayArea(int32(numScanlines), int32(topScanline))
// screen texture is used to draw the pixels onto the sdl window (by the
@ -143,7 +143,7 @@ func (pxl *pixels) resize(topScanline, numScanlines int) error {
func (pxl *pixels) setPlayArea(scanlines int32, top int32) {
pxl.playHeight = scanlines
pxl.playDstMask = &sdl.Rect{X: 0, Y: 0, W: pxl.playWidth, H: pxl.playHeight}
pxl.playSrcMask = &sdl.Rect{X: int32(television.ClocksPerHblank), Y: top, W: pxl.playWidth, H: pxl.playHeight}
pxl.playSrcMask = &sdl.Rect{X: int32(television.HorizClksHBlank), Y: top, W: pxl.playWidth, H: pxl.playHeight}
pxl.setMasking(pxl.unmasked)
}
@ -273,7 +273,7 @@ func (pxl *pixels) update() error {
if pxl.unmasked {
pxl.renderer.SetDrawColor(100, 100, 100, 20)
pxl.renderer.SetDrawBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND))
pxl.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(television.ClocksPerHblank), H: int32(pxl.scr.GetSpec().ScanlinesTotal)})
pxl.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(television.HorizClksHBlank), H: int32(pxl.scr.GetSpec().ScanlinesTotal)})
}
// show overlay
@ -293,7 +293,7 @@ func (pxl *pixels) update() error {
// cursor is one step ahead of pixel -- move to new scanline if
// necessary
if x >= television.ClocksPerScanline {
if x >= television.HorizClksScanline {
x = 0
y++
}

View file

@ -8,7 +8,7 @@ import (
)
// SetFeature is used to set a television attribute
func (pxtv *SdlDebug) SetFeature(request gui.FeatureReq, args ...interface{}) (returnedErr error) {
func (scr *SdlDebug) SetFeature(request gui.FeatureReq, args ...interface{}) (returnedErr error) {
// lazy (but clear) handling of type assertion errors
defer func() {
if r := recover(); r != nil {
@ -22,72 +22,72 @@ func (pxtv *SdlDebug) SetFeature(request gui.FeatureReq, args ...interface{}) (r
case gui.ReqSetVisibility:
if args[0].(bool) {
pxtv.window.Show()
scr.window.Show()
// update screen
// -- default args[1] of true if not present
if len(args) < 2 || args[1].(bool) {
pxtv.pxl.update()
scr.pxl.update()
}
} else {
pxtv.window.Hide()
scr.window.Hide()
}
case gui.ReqToggleVisibility:
if pxtv.window.GetFlags()&sdl.WINDOW_HIDDEN == sdl.WINDOW_HIDDEN {
pxtv.window.Show()
if scr.window.GetFlags()&sdl.WINDOW_HIDDEN == sdl.WINDOW_HIDDEN {
scr.window.Show()
// update screen
// -- default args[1] of true if not present
if len(args) < 2 || args[1].(bool) {
pxtv.pxl.update()
scr.pxl.update()
}
} else {
pxtv.window.Hide()
scr.window.Hide()
}
case gui.ReqSetPause:
pxtv.paused = args[0].(bool)
pxtv.pxl.update()
scr.paused = args[0].(bool)
scr.pxl.update()
case gui.ReqSetMasking:
pxtv.pxl.setMasking(args[0].(bool))
pxtv.pxl.update()
scr.pxl.setMasking(args[0].(bool))
scr.pxl.update()
case gui.ReqToggleMasking:
pxtv.pxl.setMasking(!pxtv.pxl.unmasked)
pxtv.pxl.update()
scr.pxl.setMasking(!scr.pxl.unmasked)
scr.pxl.update()
case gui.ReqSetAltColors:
pxtv.pxl.useAltPixels = args[0].(bool)
pxtv.pxl.update()
scr.pxl.useAltPixels = args[0].(bool)
scr.pxl.update()
case gui.ReqToggleAltColors:
pxtv.pxl.useAltPixels = !pxtv.pxl.useAltPixels
pxtv.pxl.update()
scr.pxl.useAltPixels = !scr.pxl.useAltPixels
scr.pxl.update()
case gui.ReqSetOverlay:
pxtv.pxl.useMetaPixels = args[0].(bool)
pxtv.pxl.update()
scr.pxl.useMetaPixels = args[0].(bool)
scr.pxl.update()
case gui.ReqToggleOverlay:
pxtv.pxl.useMetaPixels = !pxtv.pxl.useMetaPixels
pxtv.pxl.update()
scr.pxl.useMetaPixels = !scr.pxl.useMetaPixels
scr.pxl.update()
case gui.ReqSetScale:
pxtv.pxl.setScaling(args[0].(float32))
pxtv.pxl.update()
scr.pxl.setScaling(args[0].(float32))
scr.pxl.update()
case gui.ReqIncScale:
if pxtv.pxl.pixelScaleY < 4.0 {
pxtv.pxl.setScaling(pxtv.pxl.pixelScaleY + 0.1)
pxtv.pxl.update()
if scr.pxl.pixelScaleY < 4.0 {
scr.pxl.setScaling(scr.pxl.pixelScaleY + 0.1)
scr.pxl.update()
}
case gui.ReqDecScale:
if pxtv.pxl.pixelScaleY > 0.5 {
pxtv.pxl.setScaling(pxtv.pxl.pixelScaleY - 0.1)
pxtv.pxl.update()
if scr.pxl.pixelScaleY > 0.5 {
scr.pxl.setScaling(scr.pxl.pixelScaleY - 0.1)
scr.pxl.update()
}
default:
@ -98,6 +98,6 @@ func (pxtv *SdlDebug) SetFeature(request gui.FeatureReq, args ...interface{}) (r
}
// SetEventChannel implements the GUI interface
func (pxtv *SdlDebug) SetEventChannel(eventChannel chan gui.Event) {
pxtv.eventChannel = eventChannel
func (scr *SdlDebug) SetEventChannel(eventChannel chan gui.Event) {
scr.eventChannel = eventChannel
}

View file

@ -4,7 +4,6 @@ import (
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/television"
"strings"
"github.com/veandco/go-sdl2/sdl"
)
@ -27,33 +26,12 @@ type SdlDebug struct {
paused bool
}
// NewSdlDebug creates a new instance of PixelTV. For convenience, the
// television argument can be nil, in which case an instance of
// StellaTelevision will be created.
func NewSdlDebug(tvType string, scale float32, tv television.Television) (gui.GUI, error) {
// NewSdlDebug is the preferred method for creating a new instance of SdlDebug
func NewSdlDebug(tv television.Television, scale float32) (gui.GUI, error) {
var err error
// set up gui
scr := new(SdlDebug)
// create or attach television implementation
if tv == nil {
scr.Television, err = television.NewStellaTelevision(tvType)
if err != nil {
return nil, errors.New(errors.SDL, err)
}
} else {
// check that the quoted tvType matches the specification of the
// supplied BasicTelevision instance. we don't really need this but
// becuase we're implying that tvType is required, even when an
// instance of BasicTelevision has been supplied, the caller may be
// expecting an error
tvType = strings.ToUpper(tvType)
if tvType != "AUTO" && tvType != tv.GetSpec().ID {
return nil, errors.New(errors.SDL, "trying to piggyback a tv of a different spec")
}
scr.Television = tv
}
scr := &SdlDebug{Television: tv}
// set up sdl
err = sdl.Init(sdl.INIT_EVERYTHING)
@ -74,7 +52,7 @@ func NewSdlDebug(tvType string, scale float32, tv television.Television) (gui.GU
}
// set attributes that depend on the television specification
err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesPerVisible)
err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesVisible)
if err != nil {
return nil, errors.New(errors.SDL, err)
}

View file

@ -5,7 +5,6 @@ import (
"gopher2600/gui"
"gopher2600/performance/limiter"
"gopher2600/television"
"strings"
"github.com/veandco/go-sdl2/sdl"
)
@ -54,34 +53,13 @@ type SdlPlay struct {
showOnNextStable bool
}
// NewSdlPlay creates a new instance of SdlPlay. For convenience, the
// television argument can be nil, in which case an instance of
// StellaTelevision will be created.
func NewSdlPlay(tvType string, scale float32, tv television.Television) (gui.GUI, error) {
// NewSdlPlay is the preferred method of initialisation for SdlPlay
func NewSdlPlay(tv television.Television, scale float32) (gui.GUI, error) {
// set up gui
scr := &SdlPlay{}
scr := &SdlPlay{Television: tv}
var err error
// create or attach television implementation
if tv == nil {
scr.Television, err = television.NewStellaTelevision(tvType)
if err != nil {
return nil, errors.New(errors.SDL, err)
}
} else {
// check that the quoted tvType matches the specification of the
// supplied BasicTelevision instance. we don't really need this but
// becuase we're implying that tvType is required, even when an
// instance of BasicTelevision has been supplied, the caller may be
// expecting an error
tvType = strings.ToUpper(tvType)
if tvType != "AUTO" && tvType != tv.GetSpec().ID {
return nil, errors.New(errors.SDL, "trying to piggyback a tv of a different spec")
}
scr.Television = tv
}
// set up sdl
err = sdl.Init(sdl.INIT_EVERYTHING)
if err != nil {
@ -117,7 +95,7 @@ func NewSdlPlay(tvType string, scale float32, tv television.Television) (gui.GUI
scr.AddAudioMixer(scr)
// change tv spec after window creation (so we can set the window size)
err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesPerVisible)
err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesVisible)
if err != nil {
return nil, errors.New(errors.SDL, err)
}
@ -146,7 +124,7 @@ func NewSdlPlay(tvType string, scale float32, tv television.Television) (gui.GUI
func (scr *SdlPlay) Resize(topScanline, numScanlines int) error {
var err error
scr.horizPixels = television.ClocksPerVisible
scr.horizPixels = television.HorizClksVisible
scr.scanlines = int32(numScanlines)
scr.topScanline = topScanline
scr.pixels = make([]byte, scr.horizPixels*scr.scanlines*pixelDepth)
@ -236,7 +214,7 @@ func (scr *SdlPlay) SetPixel(x, y int, red, green, blue byte, vblank bool) error
}
// adjust pixels so we're only dealing with the visible range
x -= television.ClocksPerHblank
x -= television.HorizClksHBlank
y -= scr.topScanline
if x < 0 || y < 0 {

View file

@ -1,18 +1,18 @@
package peripherals
// Event represents the possible actions that can be performed with a
// controller
type Event int
// Action represents the possible actions that can be performed by the user
// when interacting with the console
type Action int
// list of defined events
// list of defined actions
//
// *** do not monkey with the ordering of these constants unless you know what
// you're doing. existing playback scripts will probably break ***
const (
NoEvent Event = iota
NoAction Action = iota
// the controller has been unplugged
Unplugged
Unplug
// joystick
Fire
@ -43,5 +43,5 @@ const (
// !!TODO: paddle and keyboard controllers
PanelPowerOff Event = 255
PanelPowerOff Action = 255
)

View file

@ -6,5 +6,5 @@ package peripherals
// Peripherals can also be controlled more directly by calling the Handle
// function of that peripheral.
type Controller interface {
GetInput(id PeriphID) (Event, error)
GetInput(id PeriphID) (Action, error)
}

View file

@ -100,11 +100,11 @@ func (pan *Panel) commit() {
}
// Handle interprets an event into the correct sequence of memory addressing
func (pan *Panel) Handle(event Event) error {
func (pan *Panel) Handle(event Action) error {
switch event {
// do nothing at all if event is a NoEvent
case NoEvent:
case NoAction:
return nil
case PanelSelectPress:

View file

@ -15,7 +15,7 @@ const (
type peripheral struct {
id PeriphID
handle func(Event) error
handle func(Action) error
controller Controller
prevController Controller

View file

@ -95,11 +95,11 @@ func newPlayer1(riot memory.PeriphBus, tia memory.PeriphBus, panel *Panel) *play
}
// Handle interprets an event into the correct sequence of memory addressing
func (pl *player) Handle(event Event) error {
func (pl *player) Handle(event Action) error {
switch event {
// do nothing at all if event is a NoEvent
case NoEvent:
case NoAction:
return nil
case Left:
@ -144,7 +144,7 @@ func (pl *player) Handle(event Event) error {
case PanelResetRelease:
return pl.panel.Handle(PanelResetRelease)
case Unplugged:
case Unplug:
return errors.New(errors.PeriphUnplugged, pl.id)
// return now if there is no event to process

View file

@ -12,5 +12,5 @@ package peripherals
// be used as the source for controller input (by implementing the Controller
// interface).
type Transcriber interface {
Transcribe(id PeriphID, event Event) error
Transcribe(id PeriphID, event Action) error
}

View file

@ -132,10 +132,10 @@ func (bs *ballSprite) rsync(adjustment int) {
bs.resetPixel -= adjustment
bs.hmovedPixel -= adjustment
if bs.resetPixel < 0 {
bs.resetPixel += television.ClocksPerVisible
bs.resetPixel += television.HorizClksVisible
}
if bs.hmovedPixel < 0 {
bs.hmovedPixel += television.ClocksPerVisible
bs.hmovedPixel += television.HorizClksVisible
}
}
@ -162,7 +162,7 @@ func (bs *ballSprite) tick(visible, isHmove bool, hmoveCt uint8) {
// adjust for screen boundary
if bs.hmovedPixel < 0 {
bs.hmovedPixel += television.ClocksPerVisible
bs.hmovedPixel += television.HorizClksVisible
}
}
@ -199,8 +199,8 @@ func (bs *ballSprite) prepareForHMOVE() {
bs.hmovedPixel += 8
// adjust for screen boundary
if bs.hmovedPixel > television.ClocksPerVisible {
bs.hmovedPixel -= television.ClocksPerVisible
if bs.hmovedPixel > television.HorizClksVisible {
bs.hmovedPixel -= television.HorizClksVisible
}
}
}
@ -258,8 +258,8 @@ func (bs *ballSprite) _futureResetPosition() {
bs.resetPixel++
// adjust resetPixel for screen boundaries
if bs.resetPixel > television.ClocksPerVisible {
bs.resetPixel -= television.ClocksPerVisible
if bs.resetPixel > television.HorizClksVisible {
bs.resetPixel -= television.HorizClksVisible
}
// by definition the current pixel is the same as the reset pixel at

View file

@ -157,10 +157,10 @@ func (ms *missileSprite) rsync(adjustment int) {
ms.resetPixel -= adjustment
ms.hmovedPixel -= adjustment
if ms.resetPixel < 0 {
ms.resetPixel += television.ClocksPerVisible
ms.resetPixel += television.HorizClksVisible
}
if ms.hmovedPixel < 0 {
ms.hmovedPixel += television.ClocksPerVisible
ms.hmovedPixel += television.HorizClksVisible
}
}
@ -204,7 +204,7 @@ func (ms *missileSprite) tick(visible, isHmove bool, hmoveCt uint8) {
// adjust for screen boundary
if ms.hmovedPixel < 0 {
ms.hmovedPixel += television.ClocksPerVisible
ms.hmovedPixel += television.HorizClksVisible
}
}
@ -274,8 +274,8 @@ func (ms *missileSprite) prepareForHMOVE() {
ms.hmovedPixel += 8
// adjust for screen boundary
if ms.hmovedPixel > television.ClocksPerVisible {
ms.hmovedPixel -= television.ClocksPerVisible
if ms.hmovedPixel > television.HorizClksVisible {
ms.hmovedPixel -= television.HorizClksVisible
}
}
}
@ -331,8 +331,8 @@ func (ms *missileSprite) _futureResetPosition() {
ms.resetPixel++
// adjust resetPixel for screen boundaries
if ms.resetPixel > television.ClocksPerVisible {
ms.resetPixel -= television.ClocksPerVisible
if ms.resetPixel > television.HorizClksVisible {
ms.resetPixel -= television.HorizClksVisible
}
// by definition the current pixel is the same as the reset pixel at

View file

@ -242,10 +242,10 @@ func (ps *playerSprite) rsync(adjustment int) {
ps.resetPixel -= adjustment
ps.hmovedPixel -= adjustment
if ps.resetPixel < 0 {
ps.resetPixel += television.ClocksPerVisible
ps.resetPixel += television.HorizClksVisible
}
if ps.hmovedPixel < 0 {
ps.hmovedPixel += television.ClocksPerVisible
ps.hmovedPixel += television.HorizClksVisible
}
}
@ -269,7 +269,7 @@ func (ps *playerSprite) tick(visible, isHmove bool, hmoveCt uint8) {
// adjust for screen boundary
if ps.hmovedPixel < 0 {
ps.hmovedPixel += television.ClocksPerVisible
ps.hmovedPixel += television.HorizClksVisible
}
}
@ -381,8 +381,8 @@ func (ps *playerSprite) prepareForHMOVE() {
ps.hmovedPixel += 8
// adjust for screen boundary
if ps.hmovedPixel > television.ClocksPerVisible {
ps.hmovedPixel -= television.ClocksPerVisible
if ps.hmovedPixel > television.HorizClksVisible {
ps.hmovedPixel -= television.HorizClksVisible
}
}
}
@ -459,8 +459,8 @@ func (ps *playerSprite) _futureResetPosition() {
}
// adjust resetPixel for screen boundaries
if ps.resetPixel > television.ClocksPerVisible {
ps.resetPixel -= television.ClocksPerVisible
if ps.resetPixel > television.HorizClksVisible {
ps.resetPixel -= television.HorizClksVisible
}
// by definition the current pixel is the same as the reset pixel at
@ -641,11 +641,11 @@ func (ps *playerSprite) _futureSetNusiz(v interface{}) {
}
// adjust reset pixel for screen boundaries
if ps.resetPixel > television.ClocksPerVisible {
ps.resetPixel -= television.ClocksPerVisible
if ps.resetPixel > television.HorizClksVisible {
ps.resetPixel -= television.HorizClksVisible
}
if ps.hmovedPixel > television.ClocksPerVisible {
ps.hmovedPixel -= television.ClocksPerVisible
if ps.hmovedPixel > television.HorizClksVisible {
ps.hmovedPixel -= television.HorizClksVisible
}
}

View file

@ -4,8 +4,6 @@ import (
"fmt"
"gopher2600/cartridgeloader"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/gui/sdlplay"
"gopher2600/hardware"
"gopher2600/setup"
"gopher2600/television"
@ -14,31 +12,11 @@ import (
)
// Check is a very rough and ready calculation of the emulator's performance
func Check(output io.Writer, profile bool, display bool, tvType string, scaling float32, runTime string, cartload cartridgeloader.Loader) error {
var ftv television.Television
func Check(output io.Writer, profile bool, tv television.Television, runTime string, cartload cartridgeloader.Loader) error {
var err error
// create the "correct" type of TV depending on whether the display flag is
// set or not
if display {
ftv, err = sdlplay.NewSdlPlay(tvType, scaling, nil)
if err != nil {
return errors.New(errors.PerformanceError, err)
}
err = ftv.(gui.GUI).SetFeature(gui.ReqSetVisibility, true)
if err != nil {
return errors.New(errors.PerformanceError, err)
}
} else {
ftv, err = television.NewStellaTelevision(tvType)
if err != nil {
return errors.New(errors.PerformanceError, err)
}
}
// create vcs using the tv created above
vcs, err := hardware.NewVCS(ftv)
vcs, err := hardware.NewVCS(tv)
if err != nil {
return errors.New(errors.PerformanceError, err)
}
@ -56,7 +34,7 @@ func Check(output io.Writer, profile bool, display bool, tvType string, scaling
}
// get starting frame number (should be 0)
startFrame, err := ftv.GetState(television.ReqFramenum)
startFrame, err := tv.GetState(television.ReqFramenum)
if err != nil {
return errors.New(errors.PerformanceError, err)
}
@ -70,7 +48,7 @@ func Check(output io.Writer, profile bool, display bool, tvType string, scaling
// then restart timer for the specified duration
go func() {
time.AfterFunc(2*time.Second, func() {
startFrame, _ = ftv.GetState(television.ReqFramenum)
startFrame, _ = tv.GetState(television.ReqFramenum)
time.AfterFunc(duration, func() {
timesUp <- true
})
@ -109,7 +87,7 @@ func Check(output io.Writer, profile bool, display bool, tvType string, scaling
}
numFrames := endFrame - startFrame
fps, accuracy := CalcFPS(ftv, numFrames, duration.Seconds())
fps, accuracy := CalcFPS(tv, numFrames, duration.Seconds())
output.Write([]byte(fmt.Sprintf("%.2f fps (%d frames in %.2f seconds) %.1f%%\n", fps, numFrames, duration.Seconds(), accuracy)))
if profile {

View file

@ -4,8 +4,8 @@ import "gopher2600/television"
// 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) {
func CalcFPS(tv television.Television, numFrames int, duration float64) (fps float64, accuracy float64) {
fps = float64(numFrames) / duration
accuracy = 100 * float64(numFrames) / (duration * float64(ftv.GetSpec().FramesPerSecond))
accuracy = 100 * float64(numFrames) / (duration * float64(tv.GetSpec().FramesPerSecond))
return fps, accuracy
}

View file

@ -5,23 +5,17 @@ import (
"gopher2600/cartridgeloader"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/gui/sdlplay"
"gopher2600/hardware"
"gopher2600/recorder"
"gopher2600/setup"
"gopher2600/television"
"os"
"os/signal"
"time"
)
func uniqueFilename(cartload cartridgeloader.Loader) string {
n := time.Now()
timestamp := fmt.Sprintf("%04d%02d%02d_%02d%02d%02d", n.Year(), n.Month(), n.Day(), n.Hour(), n.Minute(), n.Second())
return fmt.Sprintf("recording_%s_%s", cartload.ShortName(), timestamp)
}
// Play sets the emulation running - without any debugging features
func Play(tvType string, scaling float32, stable bool, newRecording bool, cartload cartridgeloader.Loader) error {
func Play(tv television.Television, scr gui.GUI, stable bool, newRecording bool, cartload cartridgeloader.Loader) error {
var transcript string
// if supplied cartridge name is actually a playback file then set
@ -37,12 +31,7 @@ func Play(tvType string, scaling float32, stable bool, newRecording bool, cartlo
cartload = cartridgeloader.Loader{}
}
playtv, err := sdlplay.NewSdlPlay(tvType, scaling, nil)
if err != nil {
return errors.New(errors.PlayError, err)
}
vcs, err := hardware.NewVCS(playtv)
vcs, err := hardware.NewVCS(tv)
if err != nil {
return errors.New(errors.PlayError, err)
}
@ -53,24 +42,30 @@ func Play(tvType string, scaling float32, stable bool, newRecording bool, cartlo
if newRecording {
// new recording requested
transcript = uniqueFilename(cartload)
// create a unique filename
n := time.Now()
transcript = fmt.Sprintf("recording_%s_%s",
cartload.ShortName(), fmt.Sprintf("%04d%02d%02d_%02d%02d%02d",
n.Year(), n.Month(), n.Day(), n.Hour(), n.Minute(), n.Second()))
// prepare new recording
rec, err := recorder.NewRecorder(transcript, vcs)
if err != nil {
return errors.New(errors.PlayError, err)
}
// making sure we end the recording gracefully when we leave the function
defer func() {
rec.End()
}()
// attach recorder to vcs peripherals, including the panel
vcs.Ports.Player0.AttachTranscriber(rec)
vcs.Ports.Player1.AttachTranscriber(rec)
vcs.Panel.AttachTranscriber(rec)
// attaching cartridge after recorder and transcribers have been
// attach cartridge after recorder and transcribers have been
// setup because we want to catch any setup events in the recording
err = setup.AttachCartridge(vcs, cartload)
if err != nil {
return errors.New(errors.PlayError, err)
@ -114,14 +109,14 @@ func Play(tvType string, scaling float32, stable bool, newRecording bool, cartlo
// connect gui
guiChannel := make(chan gui.Event, 2)
playtv.SetEventChannel(guiChannel)
scr.SetEventChannel(guiChannel)
// request television visibility
request := gui.ReqSetVisibilityStable
if !stable {
request = gui.ReqSetVisibility
}
err = playtv.SetFeature(request, true)
err = scr.SetFeature(request, true)
if err != nil {
return errors.New(errors.PlayError, err)
}
@ -141,7 +136,7 @@ func Play(tvType string, scaling float32, stable bool, newRecording bool, cartlo
case gui.EventWindowClose:
return false, nil
case gui.EventKeyboard:
err = KeyboardEventHandler(ev.Data.(gui.EventDataKeyboard), playtv, vcs)
err = KeyboardEventHandler(ev.Data.(gui.EventDataKeyboard), scr, vcs)
return err == nil, err
}
default:

View file

@ -15,7 +15,7 @@ import (
)
type event struct {
event peripherals.Event
event peripherals.Action
frame int
scanline int
horizpos int
@ -130,7 +130,7 @@ func NewPlayback(transcript string) (*Playback, error) {
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldEvent+1], fieldSep)))
return nil, errors.New(errors.PlaybackError, msg)
}
event.event = peripherals.Event(n)
event.event = peripherals.Action(n)
event.frame, err = strconv.Atoi(toks[fieldFrame])
if err != nil {
@ -180,16 +180,10 @@ func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
var err error
// create digesttv, piggybacking on the tv already being used by vcs;
// unless that tv is already a digesttv
switch tv := plb.vcs.TV.(type) {
case *screendigest.SHA1:
plb.digest = tv
default:
plb.digest, err = screendigest.NewSHA1(plb.vcs.TV.GetSpec().ID, plb.vcs.TV)
if err != nil {
return errors.New(errors.RecordingError, err)
}
// create digesttv using TV attached to VCS
plb.digest, err = screendigest.NewSHA1(plb.vcs.TV)
if err != nil {
return errors.New(errors.RecordingError, err)
}
// attach playback to controllers
@ -201,34 +195,34 @@ func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
}
// GetInput implements peripherals.Controller interface
func (plb *Playback) GetInput(id peripherals.PeriphID) (peripherals.Event, error) {
func (plb *Playback) GetInput(id peripherals.PeriphID) (peripherals.Action, error) {
// there's no events for this id at all
seq := plb.sequences[id]
// we've reached the end of the list of events for this id
if seq.eventCt >= len(seq.events) {
return peripherals.NoEvent, nil
return peripherals.NoAction, nil
}
// get current state of the television
frame, err := plb.vcs.TV.GetState(television.ReqFramenum)
if err != nil {
return peripherals.NoEvent, errors.New(errors.PlaybackError, err)
return peripherals.NoAction, errors.New(errors.PlaybackError, err)
}
scanline, err := plb.vcs.TV.GetState(television.ReqScanline)
if err != nil {
return peripherals.NoEvent, errors.New(errors.PlaybackError, err)
return peripherals.NoAction, errors.New(errors.PlaybackError, err)
}
horizpos, err := plb.vcs.TV.GetState(television.ReqHorizPos)
if err != nil {
return peripherals.NoEvent, errors.New(errors.PlaybackError, err)
return peripherals.NoAction, errors.New(errors.PlaybackError, err)
}
// compare current state with the recording
nextEvent := seq.events[seq.eventCt]
if frame == nextEvent.frame && scanline == nextEvent.scanline && horizpos == nextEvent.horizpos {
if nextEvent.hash != plb.digest.String() {
return peripherals.NoEvent, errors.New(errors.PlaybackHashError, fmt.Sprintf("line %d", nextEvent.line))
return peripherals.NoAction, errors.New(errors.PlaybackHashError, fmt.Sprintf("line %d", nextEvent.line))
}
seq.eventCt++
@ -236,5 +230,5 @@ func (plb *Playback) GetInput(id peripherals.PeriphID) (peripherals.Event, error
}
// next event does not match
return peripherals.NoEvent, nil
return peripherals.NoAction, nil
}

View file

@ -32,7 +32,7 @@ func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
rec := &Recorder{vcs: vcs}
// create digesttv, piggybacking on the tv already being used by vcs
rec.digest, err = screendigest.NewSHA1(vcs.TV.GetSpec().ID, vcs.TV)
rec.digest, err = screendigest.NewSHA1(vcs.TV)
if err != nil {
return nil, errors.New(errors.RecordingError, err)
}
@ -77,7 +77,7 @@ func (rec *Recorder) End() error {
}
// Transcribe implements the Transcriber interface
func (rec *Recorder) Transcribe(id peripherals.PeriphID, event peripherals.Event) error {
func (rec *Recorder) Transcribe(id peripherals.PeriphID, event peripherals.Action) error {
var err error
// write header if it's not been written already
@ -90,7 +90,7 @@ func (rec *Recorder) Transcribe(id peripherals.PeriphID, event peripherals.Event
}
// don't do anything if event is the NoEvent
if event == peripherals.NoEvent {
if event == peripherals.NoAction {
return nil
}

View file

@ -126,17 +126,17 @@ func (reg FrameRegression) CleanUp() error {
func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg string) (bool, string, error) {
output.Write([]byte(msg))
stv, err := television.NewStellaTelevision(reg.TVtype)
tv, err := television.NewTelevision(reg.TVtype)
if err != nil {
return false, "", errors.New(errors.RegressionFrameError, err)
}
dtv, err := screendigest.NewSHA1(reg.TVtype, stv)
dig, err := screendigest.NewSHA1(tv)
if err != nil {
return false, "", errors.New(errors.RegressionFrameError, err)
}
vcs, err := hardware.NewVCS(dtv)
vcs, err := hardware.NewVCS(dig)
if err != nil {
return false, "", errors.New(errors.RegressionFrameError, err)
}
@ -159,7 +159,7 @@ func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg st
// add the starting state of the tv
if reg.State {
state = append(state, stv.String())
state = append(state, tv.String())
}
// run emulation
@ -170,7 +170,7 @@ func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg st
// store tv state at every step
if reg.State {
state = append(state, stv.String())
state = append(state, tv.String())
}
return true, nil
@ -181,7 +181,7 @@ func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg st
}
if newRegression {
reg.screenDigest = dtv.String()
reg.screenDigest = dig.String()
if reg.State {
// create a unique filename
@ -256,7 +256,7 @@ func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg st
}
if dtv.String() != reg.screenDigest {
if dig.String() != reg.screenDigest {
return false, "screen digest mismatch", nil
}

View file

@ -92,7 +92,12 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg
return false, "", errors.New(errors.RegressionPlaybackError, err)
}
tv, err := screendigest.NewSHA1(plb.TVtype, nil)
tv, err := television.NewTelevision(plb.TVtype)
if err != nil {
return false, "", errors.New(errors.RegressionFrameError, err)
}
_, err = screendigest.NewSHA1(tv)
if err != nil {
return false, "", errors.New(errors.RegressionPlaybackError, err)
}

View file

@ -5,7 +5,6 @@ import (
"fmt"
"gopher2600/errors"
"gopher2600/television"
"strings"
)
// SHA1 is an implementation of the television.Renderer interface with an
@ -26,30 +25,9 @@ const pixelDepth = 3
// NewSHA1 initialises a new instance of DigestTV. For convenience, the
// television argument can be nil, in which case an instance of
// StellaTelevision will be created.
func NewSHA1(tvType string, tv television.Television) (*SHA1, error) {
var err error
func NewSHA1(tv television.Television) (*SHA1, error) {
// set up digest tv
dig := new(SHA1)
// create or attach television implementation
if tv == nil {
dig.Television, err = television.NewStellaTelevision(tvType)
if err != nil {
return nil, err
}
} else {
// check that the quoted tvType matches the specification of the
// supplied BasicTelevision instance. we don't really need this but
// becuase we're implying that tvType is required, even when an
// instance of BasicTelevision has been supplied, the caller may be
// expecting an error
tvType = strings.ToUpper(tvType)
if tvType != "AUTO" && tvType != tv.GetSpec().ID {
return nil, errors.New(errors.ScreenDigest, "trying to piggyback a tv of a different spec")
}
dig.Television = tv
}
dig := &SHA1{Television: tv}
// register ourselves as a television.Renderer
dig.AddPixelRenderer(dig)
@ -78,7 +56,7 @@ func (dig *SHA1) Resize(_, _ int) error {
l := len(dig.digest)
// alloscate enough pixels for entire frame
l += ((television.ClocksPerScanline + 1) * (dig.GetSpec().ScanlinesTotal + 1) * pixelDepth)
l += ((television.HorizClksScanline + 1) * (dig.GetSpec().ScanlinesTotal + 1) * pixelDepth)
dig.pixels = make([]byte, l)
return nil
@ -106,7 +84,7 @@ func (dig *SHA1) NewScanline(scanline int) error {
func (dig *SHA1) SetPixel(x, y int, red, green, blue byte, vblank bool) error {
// preserve the first few bytes for a chained fingerprint
i := len(dig.digest)
i += television.ClocksPerScanline * y * pixelDepth
i += television.HorizClksScanline * y * pixelDepth
i += x * pixelDepth
if i <= len(dig.pixels)-pixelDepth {

155
television/protocol.go Normal file
View file

@ -0,0 +1,155 @@
package television
import (
"gopher2600/hardware/tia/audio"
)
// Television defines the operations that can be performed on the conceptual
// television. Note that the television implementation itself does not present
// any information, either visually or sonically. Instead, PixelRenderers and
// AudioMixers are added to perform those tasks.
type Television interface {
String() string
// Reset the television to an initial state
Reset() error
// AddPixelRenderer registers an (additional) implementation of PixelRenderer
AddPixelRenderer(PixelRenderer)
// AddAudioMixer registers an (additional) implementation of AudioMixer
AddAudioMixer(AudioMixer)
Signal(SignalAttributes) error
// Returns the value of the requested state. eg. the current scanline.
GetState(StateReq) (int, error)
// Returns the television's current specification. Renderers should use
// GetSpec() rather than keeping a private pointer to the specification.
GetSpec() *Specification
// IsStable returns true if the television thinks the image being sent by
// the VCS is stable
IsStable() bool
}
// PixelRenderer implementations displays, or otherwise works with, visal
// information from a television
//
// examples of renderers that display visual information:
// * SDLPlay
// * ImageTV
//
// examples of renderers that do not display visual information but only work
// with it:
// * DigestTV
//
// PixelRenderer implementations find it convenient to maintain a reference to
// the parent Television implementation and maybe even embed the Television
// interface. ie.
//
// type ExampleTV struct {
// television.Television
//
// ...
// }
type PixelRenderer interface {
// Resize is called when the television implementation detects that extra
// scanlines are required in the display.
//
// It may be called when television specification has changed. Renderers
// should use GetSpec() rather than keeping a private pointer to the
// specification.
//
// Renderers should use the values sent by the Resize() function, rather
// than the equivalent values in the specification. Unless of course, the
// renderer is intended to be strict about specification accuracy.
//
// Renderers should also make sure that any data structures that depend on
// the specification being used are still adequate.
Resize(topScanline, visibleScanlines int) error
// NewFrame and NewScanline are called at the start of the frame/scanline
NewFrame(frameNum int) error
NewScanline(scanline int) error
// setPixel() and setAltPixel() are called every cycle regardless of the
// state of VBLANK and HBLANK.
//
// things to consider:
//
// o the x argument is measured from zero so renderers should decide how to
// handle pixels of during the HBLANK (x < ClocksPerHBLANK)
//
// o the y argument is also measure from zero but because VBLANK can be
// turned on at any time there's no easy test. the VBLANK flag is sent to
// help renderers decide what to do.
//
// o for renderers that are producing an accurate visual image, the pixel
// should always be set to video black if VBLANK is on.
//
// some renderers however, may find it useful to set the pixel to the RGB
// value regardless of VBLANK. for example, DigestTV does this.
//
// a vey important note is that some ROMs use VBLANK to control pixel
// color within the visible display area. ROMs affected:
//
// * Custer's Revenge
// * Ladybug
//
SetPixel(x, y int, red, green, blue byte, vblank bool) error
SetAltPixel(x, y int, red, green, blue byte, vblank bool) error
}
// AudioMixer implementations work with sound; most probably playing it.
type AudioMixer interface {
SetAudio(audio audio.Audio) error
}
// SignalAttributes represents the data sent to the television
type SignalAttributes struct {
VSync bool
VBlank bool
CBurst bool
HSync bool
Pixel ColorSignal
// AltPixel allows the emulator to set an alternative color for each pixel
// - used to signal the debug color in addition to the regular color
// - arguable that this be sent as some sort of meta-signal
AltPixel ColorSignal
// the HSyncSimple attribute is not part of the real TV spec. The signal
// for a real flyback is the HSync signal (held for 8 color clocks).
// however, this results in a confusing way of counting pixels - confusing
// at least to people who are used to the Stella method of counting.
//
// if we were to use HSync to detect a new scanline then we have to treat
// the front porch and back porch separately. the convenient HSyncSimple
// attribute effectively pushes the front and back porches together meaning
// we can count from -68 to 159 - the same as Stella. this is helpful when
// A/B testing.
//
// the TIA emulation sends both HSync and HSyncSimple signals. television
// implementations can use either, it doesn't really make any difference
// except to debugging information. the "basic" television implementation
// uses HSyncSimple instead of HSync
HSyncSimple bool
// audio signal is just the content of the VCS audio registers. for now,
// sounds is generated/mixed by the television or gui implementation
Audio audio.Audio
UpdateAudio bool
}
// StateReq is used to identify which television attribute is being asked
// with the GetState() function
type StateReq int
// list of valid state requests
const (
ReqFramenum StateReq = iota
ReqScanline
ReqHorizPos
)

View file

@ -2,67 +2,99 @@ package television
// Specification is used to define the two television specifications
type Specification struct {
ID string
ID string
Colors colors
ScanlinesPerVSync int
ScanlinesPerVBlank int
ScanlinesPerVisible int
ScanlinesPerOverscan int
ScanlinesTotal int
// the number of scanlines the 2600 Programmer's guide recommends for the
// top/bottom parts of the screen:
//
// "A typical frame will consists of 3 vertical sync (VSYNC) lines*, 37 vertical
// blank (VBLANK) lines, 192 TV picture lines, and 30 overscan lines. Ataris
// research has shown that this pattern will work on all types of TV sets."
//
// the above figures are in reference to the NTSC protocol
scanlinesVSync int
scanlinesVBlank int
ScanlinesVisible int
scanlinesOverscan int
// the total number of scanlines for the entire frame is the sum of the
// four individual portions
ScanlinesTotal int
// the scanline at which the VBLANK should be turned off (Top) and
// turned back on again (Bottom). the period between the top and bottom
// scanline is the visible portion of the screen.
//
// in practice, the VCS can turn VBLANK on and off at any time; what the
// two values below represent what "Atari's research" has shown to be safe.
// by definition this means that:
//
// Top = VSync + Vblank
//
// Bottom = Top + Visible
//
// or
//
// Bottom = Total - Overscan
ScanlineTop int
ScanlineBottom int
Colors colors
// the number of frames per second required by the specification
FramesPerSecond int
SecondsPerFrame float64
// AspectBias transforms the scaling factor for the X axis.
// AspectBias transforms the scaling factor for the X axis. in other words,
// for width of every pixel is height of every pixel multiplied by the
// aspect bias
AspectBias float32
}
// ClocksPerHblank is the same for all tv specifications
const ClocksPerHblank = 68
// "Each scan lines starts with 68 clock counts of horizontal blank (not seen on
// the TV screen) followed by 160 clock counts to fully scan one line of TV
// picture. When the electron beam reaches the end of a scan line, it returns
// to the left side of the screen, waits for the 68 horizontal blank clock
// counts, and proceeds to draw the next line below."
//
// Horizontal clock counts are the same for both TV specificationst
const (
HorizClksHBlank = 68
HorizClksVisible = 160
HorizClksScanline = 228
)
// ClocksPerVisible is the same for all tv specifications
const ClocksPerVisible = 160
// ClocksPerScanline is the same for all tv specifications
const ClocksPerScanline = 228
// SpecNTSC is the specification for NTSC television typee
// SpecNTSC is the specification for NTSC television types
var SpecNTSC *Specification
// SpecPAL is the specification for PAL television typee
// SpecPAL is the specification for PAL television types
var SpecPAL *Specification
func init() {
SpecNTSC = new(Specification)
SpecNTSC.ID = "NTSC"
SpecNTSC.ScanlinesPerVSync = 3
SpecNTSC.ScanlinesPerVBlank = 37
SpecNTSC.ScanlinesPerVisible = 192
SpecNTSC.ScanlinesPerOverscan = 30
SpecNTSC.Colors = colorsNTSC
SpecNTSC.scanlinesVSync = 3
SpecNTSC.scanlinesVBlank = 37
SpecNTSC.ScanlinesVisible = 192
SpecNTSC.scanlinesOverscan = 30
SpecNTSC.ScanlinesTotal = 262
SpecNTSC.ScanlineTop = SpecNTSC.ScanlinesPerVBlank + SpecNTSC.ScanlinesPerVSync
SpecNTSC.ScanlineBottom = SpecNTSC.ScanlinesTotal - SpecNTSC.ScanlinesPerOverscan
SpecNTSC.ScanlineTop = SpecNTSC.scanlinesVBlank + SpecNTSC.scanlinesVSync
SpecNTSC.ScanlineBottom = SpecNTSC.ScanlinesTotal - SpecNTSC.scanlinesOverscan
SpecNTSC.FramesPerSecond = 60
SpecNTSC.SecondsPerFrame = 1.0 / float64(SpecNTSC.FramesPerSecond)
SpecNTSC.Colors = colorsNTSC
SpecPAL = new(Specification)
SpecPAL.ID = "PAL"
SpecPAL.ScanlinesPerVSync = 3
SpecPAL.ScanlinesPerVBlank = 45
SpecPAL.ScanlinesPerVisible = 228
SpecPAL.ScanlinesPerOverscan = 36
SpecPAL.Colors = colorsPAL
SpecPAL.scanlinesVSync = 3
SpecPAL.scanlinesVBlank = 45
SpecPAL.ScanlinesVisible = 228
SpecPAL.scanlinesOverscan = 36
SpecPAL.ScanlinesTotal = 312
SpecPAL.ScanlineTop = SpecPAL.ScanlinesPerVBlank + SpecPAL.ScanlinesPerVSync
SpecPAL.ScanlineBottom = SpecPAL.ScanlinesTotal - SpecPAL.ScanlinesPerOverscan
SpecPAL.ScanlineTop = SpecPAL.scanlinesVBlank + SpecPAL.scanlinesVSync
SpecPAL.ScanlineBottom = SpecPAL.ScanlinesTotal - SpecPAL.scanlinesOverscan
SpecPAL.FramesPerSecond = 50
SpecPAL.SecondsPerFrame = 1.0 / float64(SpecPAL.FramesPerSecond)
SpecPAL.Colors = colorsPAL
// AaspectBias transforms the scaling factor for the X axis.
// values taken from Stella emualtor. useful for A/B testing

View file

@ -1,350 +0,0 @@
package television
import (
"fmt"
"gopher2600/errors"
"strings"
)
// StellaTelevision is the minimalist implementation of the Television
// interface. It is so called because the reporting of the TV state, via
// GetState(), is meant to mirror exactly the state as reported by the stella
// emulator. The intention is to make it easier to perform A/B testing.
//
// To make the state reporting as intuitive as possible, StellaTelevision makes
// use of the HSyncSimple sigal attribute (see SignalAttributes type in the
// television package for details). Consequently, calls to NewScanline() for
// any attached renderers, are made when the HSyncSimple signal is recieved.
// This will have an effect on how the renderer displays off screen information
// (if it chooses to that is).
type StellaTelevision struct {
// television specification (NTSC or PAL)
spec *Specification
// auto flag indicates that the tv type/specification should switch if it
// appears to be outside of the current spec.
//
// in practice this means that if auto is true then we start with the NTSC
// spec and move to PAL if the number of scanlines exceeds the NTSC maximum
auto bool
// state of the television
// - the current horizontal position. the position where the next pixel will be
// drawn. also used to check we're receiving the correct signals at the
// correct time.
horizPos int
// - the current frame
frameNum int
// - the current scanline number
scanline int
// record of signal attributes from the last call to Signal()
prevSignal SignalAttributes
// vsyncCount records the number of consecutive colorClocks the vsync signal
// has been sustained. we use this to help correctly implement vsync.
vsyncCount int
vsyncPos int
// list of renderer implementations to consult
renderers []PixelRenderer
// list of audio mixers to consult
mixers []AudioMixer
// the following values are used for stability detection. we could possibly
// define a separate type for all of these.
// top and bottom of screen as detected by vblank/color signal
top int
bottom int
// new top and bottom values if stability threshold is met
speculativeTop int
speculativeBottom int
// top and bottom as reckoned by the current frame - reset at the moment
// when a new frame is detected
thisTop int
thisBottom int
// a frame has to be stable (speculative top and bottom unchanged) for a
// number of frames (stable threshold) before we accept that it is a true
// representation of frame dimensions
stability int
}
// the number of frames that (speculative) top and bottom values must be steady
// before we accept the frame characteristics
const stabilityThreshold = 5
// NewStellaTelevision creates a new instance of StellaTelevision for a
// minimalist implementation of a televsion for the VCS emulation
func NewStellaTelevision(tvType string) (*StellaTelevision, error) {
btv := new(StellaTelevision)
switch strings.ToUpper(tvType) {
case "NTSC":
btv.spec = SpecNTSC
case "PAL":
btv.spec = SpecPAL
case "AUTO":
btv.spec = SpecNTSC
btv.auto = true
default:
return nil, errors.New(errors.StellaTelevision, fmt.Sprintf("unsupported tv type (%s)", tvType))
}
// empty list of renderers
btv.renderers = make([]PixelRenderer, 0)
// initialise TVState
err := btv.Reset()
if err != nil {
return nil, err
}
return btv, nil
}
func (btv StellaTelevision) String() string {
s := strings.Builder{}
s.WriteString(fmt.Sprintf("FR=%d SL=%d", btv.frameNum, btv.scanline))
s.WriteString(fmt.Sprintf(" HP=%d", btv.horizPos))
return s.String()
}
// AddPixelRenderer implements the Television interface
func (btv *StellaTelevision) AddPixelRenderer(r PixelRenderer) {
btv.renderers = append(btv.renderers, r)
}
// AddAudioMixer implements the Television interface
func (btv *StellaTelevision) AddAudioMixer(m AudioMixer) {
btv.mixers = append(btv.mixers, m)
}
// Reset implements the Television interface
func (btv *StellaTelevision) Reset() error {
btv.horizPos = -ClocksPerHblank
btv.frameNum = 0
btv.scanline = 0
btv.vsyncCount = 0
btv.prevSignal = SignalAttributes{Pixel: VideoBlack}
btv.top = btv.spec.ScanlineTop
btv.bottom = btv.spec.ScanlineBottom
return nil
}
// Signal implements the Television interface
func (btv *StellaTelevision) Signal(sig SignalAttributes) error {
// the following condition detects a new scanline by looking for the
// non-textbook HSyncSimple signal
//
// see SignalAttributes type definition for notes about the HSyncSimple
// attribute
if sig.HSyncSimple && !btv.prevSignal.HSyncSimple {
btv.horizPos = -ClocksPerHblank
btv.scanline++
if btv.scanline <= btv.spec.ScanlinesTotal {
// when observing Stella we can see that on the first frame (frame
// number zero) a new frame is triggered when the scanline reaches
// 51. it does this with every ROM and regardless of what signals
// have been sent.
//
// I'm not sure why it does this but we emulate the behaviour here
// in order to facilitate A/B testing.
if btv.frameNum == 0 && btv.scanline > 50 {
btv.scanline = 0
btv.frameNum++
// notify renderers of new frame
for f := range btv.renderers {
err := btv.renderers[f].NewFrame(btv.frameNum)
if err != nil {
return err
}
}
} else {
// notify renderers of new scanline
for f := range btv.renderers {
err := btv.renderers[f].NewScanline(btv.scanline)
if err != nil {
return err
}
}
}
} else {
// repeat last scanline over and over
btv.scanline = btv.spec.ScanlinesTotal
}
} else {
btv.horizPos++
if btv.horizPos > ClocksPerScanline {
return errors.New(errors.StellaTelevision, "no flyback signal")
}
}
// simple vsync implementation. when compared to the HSync detection above,
// the following is correct (front porch at the end of the display and back
// porch at the beginning). it is also in keeping with how Stella counts
// scanlines, meaning A/B testing is relatively straightforward.
if sig.VSync {
// if this a new vsync sequence note the horizontal position
if !btv.prevSignal.VSync {
btv.vsyncPos = btv.horizPos
}
// bump the vsync count whenever vsync is set
btv.vsyncCount++
} else if btv.prevSignal.VSync {
// if vsync has just be turned off then check that it has been held for
// the requisite number of scanlines for a new frame to be started
if btv.vsyncCount >= btv.spec.ScanlinesPerVSync {
err := btv.newFrame()
if err != nil {
return err
}
}
// reset vsync counter when vsync signal is dropped
btv.vsyncCount = 0
}
// record the current signal settings so they can be used for reference
btv.prevSignal = sig
// current coordinates
x := btv.horizPos + ClocksPerHblank
y := btv.scanline
// decode color using the regular color signal
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 {
return err
}
}
// push screen boundaries outward using vblank and color signal to help us
if !sig.VBlank && red != 0 && green != 0 && blue != 0 {
if btv.scanline < btv.thisTop {
btv.thisTop = btv.scanline
}
if btv.scanline > btv.thisBottom {
btv.thisBottom = btv.scanline
}
}
// decode color using the alternative color signal
red, green, blue = getAltColor(sig.AltPixel)
for f := range btv.renderers {
err := btv.renderers[f].SetAltPixel(x, y, red, green, blue, sig.VBlank)
if err != nil {
return err
}
}
// mix audio on UpdateAudio signal
if sig.UpdateAudio {
for f := range btv.mixers {
err := btv.mixers[f].SetAudio(sig.Audio)
if err != nil {
return err
}
}
}
return nil
}
func (btv *StellaTelevision) stabilise() (bool, error) {
if btv.frameNum <= 1 || (btv.thisTop == btv.top && btv.thisBottom == btv.bottom) {
return false, nil
}
// if top and bottom has changed this frame update speculative values
if btv.thisTop != btv.speculativeTop || btv.thisBottom != btv.speculativeBottom {
btv.speculativeTop = btv.thisTop
btv.speculativeBottom = btv.thisBottom
return false, nil
}
// increase stability value until we reach threshold
if !btv.IsStable() {
btv.stability++
return false, nil
}
// accept speculative values
btv.top = btv.speculativeTop
btv.bottom = btv.speculativeBottom
if btv.spec == SpecNTSC && btv.bottom-btv.top >= SpecPAL.ScanlinesPerVisible {
btv.spec = SpecPAL
// reset top/bottom to ideals of new spec. they may of course be
// pushed outward in subsequent frames
btv.top = btv.spec.ScanlineTop
btv.bottom = btv.spec.ScanlineBottom
}
for f := range btv.renderers {
err := btv.renderers[f].Resize(btv.top, btv.bottom-btv.top+1)
if err != nil {
return false, err
}
}
return true, nil
}
func (btv *StellaTelevision) newFrame() error {
_, err := btv.stabilise()
if err != nil {
return err
}
// new frame
btv.frameNum++
btv.scanline = 0
btv.thisTop = btv.top
btv.thisBottom = btv.bottom
// call new frame for all renderers
for f := range btv.renderers {
err = btv.renderers[f].NewFrame(btv.frameNum)
if err != nil {
return err
}
}
return nil
}
// GetState implements the Television interface
func (btv *StellaTelevision) GetState(request StateReq) (int, error) {
switch request {
default:
return 0, errors.New(errors.UnknownTVRequest, request)
case ReqFramenum:
return btv.frameNum, nil
case ReqScanline:
return btv.scanline, nil
case ReqHorizPos:
return btv.horizPos, nil
}
}
// GetSpec implements the Television interface
func (btv StellaTelevision) GetSpec() *Specification {
return btv.spec
}
// IsStable implements the Television interface
func (btv StellaTelevision) IsStable() bool {
return btv.stability >= stabilityThreshold
}

View file

@ -1,150 +1,341 @@
package television
import "gopher2600/hardware/tia/audio"
// Television defines the operations that can be performed on the conceptual
// television. Implementations should not actually present the information,
// either visually or sonically. Instead, Renderers and Mixers can be added to
// perform those tasks.
//
// Note that for convenience, many television implementations "double-up" as
// Renderer interfaces. In these instances, the television will call
// AddRenderer() with itself as an argument.
type Television interface {
String() string
AddPixelRenderer(PixelRenderer)
AddAudioMixer(AudioMixer)
Reset() error
Signal(SignalAttributes) error
// Returns the value of the requested state. eg. the current scanline.
GetState(StateReq) (int, error)
// Returns the television's current specification. Renderers should use
// GetSpec() rather than keeping a private pointer to the specification.
GetSpec() *Specification
// IsStable returns true if the television thinks the image being sent by
// the VCS is stable
IsStable() bool
}
// PixelRenderer implementations displays, or otherwise works with, visal
// information from a television
//
// examples of renderers that display visual information:
// * SDLPlay
// * ImageTV
//
// examples of renderers that do not display visual information but only work
// with it:
// * DigestTV
//
// PixelRenderer implementations find it convenient to maintain a reference to
// the parent Television implementation and maybe even embed the Television
// interface. ie.
//
// type ExampleTV struct {
// television.Television
//
// ...
// }
type PixelRenderer interface {
// Resize is called when the television implementation detects that extra
// scanlines are required in the display.
//
// It may be called when television specification has changed. Renderers
// should use GetSpec() rather than keeping a private pointer to the
// specification.
//
// Renderers should use the values sent by the Resize() function, rather
// than the equivalent values in the specification. Unless of course, the
// renderer is intended to be strict about specification accuracy.
//
// Renderers should also make sure that any data structures that depend on
// the specification being used are still adequate.
Resize(topScanline, visibleScanlines int) error
// NewFrame and NewScanline are called at the start of the frame/scanline
NewFrame(frameNum int) error
NewScanline(scanline int) error
// setPixel() and setAltPixel() are called every cycle regardless of the
// state of VBLANK and HBLANK.
//
// things to consider:
//
// o the x argument is measured from zero so renderers should decide how to
// handle pixels of during the HBLANK (x < ClocksPerHBLANK)
//
// o the y argument is also measure from zero but because VBLANK can be
// turned on at any time there's no easy test. the VBLANK flag is sent to
// help renderers decide what to do.
//
// o for renderers that are producing an accurate visual image, the pixel
// should always be set to video black if VBLANK is on.
//
// some renderers however, may find it useful to set the pixel to the RGB
// value regardless of VBLANK. for example, DigestTV does this.
//
// a vey important note is that some ROMs use VBLANK to control pixel
// color within the visible display area. ROMs affected:
//
// * Custer's Revenge
// * Ladybug
//
SetPixel(x, y int, red, green, blue byte, vblank bool) error
SetAltPixel(x, y int, red, green, blue byte, vblank bool) error
}
// AudioMixer implementations work with sound; most probably playing it.
type AudioMixer interface {
SetAudio(audio audio.Audio) error
}
// SignalAttributes represents the data sent to the television
type SignalAttributes struct {
VSync bool
VBlank bool
CBurst bool
HSync bool
Pixel ColorSignal
// AltPixel allows the emulator to set an alternative color for each pixel
// - used to signal the debug color in addition to the regular color
// - arguable that this be sent as some sort of meta-signal
AltPixel ColorSignal
// the HSyncSimple attribute is not part of the real TV spec. The signal
// for a real flyback is the HSync signal (held for 8 color clocks).
// however, this results in a confusing way of counting pixels - confusing
// at least to people who are used to the Stella method of counting.
//
// if we were to use HSync to detect a new scanline then we have to treat
// the front porch and back porch separately. the convenient HSyncSimple
// attribute effectively pushes the front and back porches together meaning
// we can count from -68 to 159 - the same as Stella. this is helpful when
// A/B testing.
//
// the TIA emulation sends both HSync and HSyncSimple signals. television
// implementations can use either, it doesn't really make any difference
// except to debugging information. the "basic" television implementation
// uses HSyncSimple instead of HSync
HSyncSimple bool
// audio signal is just the content of the VCS audio registers. for now,
// sounds is generated/mixed by the television or gui implementation
Audio audio.Audio
UpdateAudio bool
}
// StateReq is used to identify which television attribute is being asked
// with the GetState() function
type StateReq int
// list of valid state requests
const (
ReqFramenum StateReq = iota
ReqScanline
ReqHorizPos
import (
"fmt"
"gopher2600/errors"
"strings"
)
// television is a reference implementation of the Television interface. In all
// honesty, it's most likely the only implementation required.
type television struct {
// television specification (NTSC or PAL)
spec *Specification
// auto flag indicates that the tv type/specification should switch if it
// appears to be outside of the current spec.
//
// in practice this means that if auto is true then we start with the NTSC
// spec and move to PAL if the number of scanlines exceeds the NTSC maximum
auto bool
// state of the television
// - the current horizontal position. the position where the next pixel will be
// drawn. also used to check we're receiving the correct signals at the
// correct time.
horizPos int
// - the current frame
frameNum int
// - the current scanline number
scanline int
// record of signal attributes from the last call to Signal()
prevSignal SignalAttributes
// vsyncCount records the number of consecutive colorClocks the vsync signal
// has been sustained. we use this to help correctly implement vsync.
vsyncCount int
vsyncPos int
// list of renderer implementations to consult
renderers []PixelRenderer
// list of audio mixers to consult
mixers []AudioMixer
// the following values are used for stability detection. we could possibly
// define a separate type for all of these.
// top and bottom of screen as detected by vblank/color signal
top int
bottom int
// new top and bottom values if stability threshold is met
speculativeTop int
speculativeBottom int
// top and bottom as reckoned by the current frame - reset at the moment
// when a new frame is detected
thisTop int
thisBottom int
// a frame has to be stable (speculative top and bottom unchanged) for a
// number of frames (stable threshold) before we accept that it is a true
// representation of frame dimensions
stability int
}
// the number of frames that (speculative) top and bottom values must be steady
// before we accept the frame characteristics
const stabilityThreshold = 5
// NewTelevision creates a new instance of StellaTelevision for a
// minimalist implementation of a televsion for the VCS emulation
func NewTelevision(tvType string) (Television, error) {
tv := new(television)
switch strings.ToUpper(tvType) {
case "NTSC":
tv.spec = SpecNTSC
case "PAL":
tv.spec = SpecPAL
case "AUTO":
tv.spec = SpecNTSC
tv.auto = true
default:
return nil, errors.New(errors.Television, fmt.Sprintf("unsupported tv type (%s)", tvType))
}
// empty list of renderers
tv.renderers = make([]PixelRenderer, 0)
// initialise TVState
err := tv.Reset()
if err != nil {
return nil, err
}
return tv, nil
}
func (tv television) String() string {
s := strings.Builder{}
s.WriteString(fmt.Sprintf("FR=%d SL=%d", tv.frameNum, tv.scanline))
s.WriteString(fmt.Sprintf(" HP=%d", tv.horizPos))
return s.String()
}
// AddPixelRenderer implements the Television interface
func (tv *television) AddPixelRenderer(r PixelRenderer) {
tv.renderers = append(tv.renderers, r)
}
// AddAudioMixer implements the Television interface
func (tv *television) AddAudioMixer(m AudioMixer) {
tv.mixers = append(tv.mixers, m)
}
// Reset implements the Television interface
func (tv *television) Reset() error {
tv.horizPos = -HorizClksHBlank
tv.frameNum = 0
tv.scanline = 0
tv.vsyncCount = 0
tv.prevSignal = SignalAttributes{Pixel: VideoBlack}
tv.top = tv.spec.ScanlineTop
tv.bottom = tv.spec.ScanlineBottom
return nil
}
// Signal implements the Television interface
func (tv *television) Signal(sig SignalAttributes) error {
// the following condition detects a new scanline by looking for the
// non-textbook HSyncSimple signal
//
// see SignalAttributes type definition for notes about the HSyncSimple
// attribute
if sig.HSyncSimple && !tv.prevSignal.HSyncSimple {
tv.horizPos = -HorizClksHBlank
tv.scanline++
if tv.scanline <= tv.spec.ScanlinesTotal {
// when observing Stella we can see that on the first frame (frame
// number zero) a new frame is triggered when the scanline reaches
// 51. it does this with every ROM and regardless of what signals
// have been sent.
//
// I'm not sure why it does this but we emulate the behaviour here
// in order to facilitate A/B testing.
if tv.frameNum == 0 && tv.scanline > 50 {
tv.scanline = 0
tv.frameNum++
// notify renderers of new frame
for f := range tv.renderers {
err := tv.renderers[f].NewFrame(tv.frameNum)
if err != nil {
return err
}
}
} else {
// notify renderers of new scanline
for f := range tv.renderers {
err := tv.renderers[f].NewScanline(tv.scanline)
if err != nil {
return err
}
}
}
} else {
// repeat last scanline over and over
tv.scanline = tv.spec.ScanlinesTotal
}
} else {
tv.horizPos++
if tv.horizPos > HorizClksScanline {
return errors.New(errors.Television, "no flyback signal")
}
}
// simple vsync implementation. when compared to the HSync detection above,
// the following is correct (front porch at the end of the display and back
// porch at the beginning). it is also in keeping with how Stella counts
// scanlines, meaning A/B testing is relatively straightforward.
if sig.VSync {
// if this a new vsync sequence note the horizontal position
if !tv.prevSignal.VSync {
tv.vsyncPos = tv.horizPos
}
// bump the vsync count whenever vsync is set
tv.vsyncCount++
} else if tv.prevSignal.VSync {
// if vsync has just be turned off then check that it has been held for
// the requisite number of scanlines for a new frame to be started
if tv.vsyncCount >= tv.spec.scanlinesVSync {
err := tv.newFrame()
if err != nil {
return err
}
}
// reset vsync counter when vsync signal is dropped
tv.vsyncCount = 0
}
// current coordinates
x := tv.horizPos + HorizClksHBlank
y := tv.scanline
// decode color using the alternative color signal
red, green, blue := getAltColor(sig.AltPixel)
for f := range tv.renderers {
err := tv.renderers[f].SetAltPixel(x, y, red, green, blue, sig.VBlank)
if err != nil {
return err
}
}
// decode color using the regular color signal
red, green, blue = getColor(tv.spec, sig.Pixel)
for f := range tv.renderers {
err := tv.renderers[f].SetPixel(x, y, red, green, blue, sig.VBlank)
if err != nil {
return err
}
}
// push screen boundaries outward using vblank and color signal to help us
if !sig.VBlank && red != 0 && green != 0 && blue != 0 {
if tv.scanline < tv.thisTop {
tv.thisTop = tv.scanline
}
if tv.scanline > tv.thisBottom {
tv.thisBottom = tv.scanline
}
}
// mix audio on UpdateAudio signal
if sig.UpdateAudio {
for f := range tv.mixers {
err := tv.mixers[f].SetAudio(sig.Audio)
if err != nil {
return err
}
}
}
// record the current signal settings so they can be used for reference
tv.prevSignal = sig
return nil
}
func (tv *television) stabilise() (bool, error) {
if tv.frameNum <= 1 || (tv.thisTop == tv.top && tv.thisBottom == tv.bottom) {
return false, nil
}
// if top and bottom has changed this frame update speculative values
if tv.thisTop != tv.speculativeTop || tv.thisBottom != tv.speculativeBottom {
tv.speculativeTop = tv.thisTop
tv.speculativeBottom = tv.thisBottom
return false, nil
}
// increase stability value until we reach threshold
if !tv.IsStable() {
tv.stability++
return false, nil
}
// accept speculative values
tv.top = tv.speculativeTop
tv.bottom = tv.speculativeBottom
if tv.spec == SpecNTSC && tv.bottom-tv.top >= SpecPAL.ScanlinesVisible {
tv.spec = SpecPAL
// reset top/bottom to ideals of new spec. they may of course be
// pushed outward in subsequent frames
tv.top = tv.spec.ScanlineTop
tv.bottom = tv.spec.ScanlineBottom
}
for f := range tv.renderers {
err := tv.renderers[f].Resize(tv.top, tv.bottom-tv.top+1)
if err != nil {
return false, err
}
}
return true, nil
}
func (tv *television) newFrame() error {
_, err := tv.stabilise()
if err != nil {
return err
}
// new frame
tv.frameNum++
tv.scanline = 0
tv.thisTop = tv.top
tv.thisBottom = tv.bottom
// call new frame for all renderers
for f := range tv.renderers {
err = tv.renderers[f].NewFrame(tv.frameNum)
if err != nil {
return err
}
}
return nil
}
// GetState implements the Television interface
func (tv *television) GetState(request StateReq) (int, error) {
switch request {
default:
return 0, errors.New(errors.UnknownTVRequest, request)
case ReqFramenum:
return tv.frameNum, nil
case ReqScanline:
return tv.scanline, nil
case ReqHorizPos:
return tv.horizPos, nil
}
}
// GetSpec implements the Television interface
func (tv television) GetSpec() *Specification {
return tv.spec
}
// IsStable implements the Television interface
func (tv television) IsStable() bool {
return tv.stability >= stabilityThreshold
}

View file

@ -6,20 +6,26 @@ import (
"gopher2600/gui"
"gopher2600/gui/sdldebug"
"gopher2600/hardware"
"gopher2600/television"
"testing"
)
func BenchmarkSDL(b *testing.B) {
var err error
tv, err := sdldebug.NewSdlDebug("NTSC", 1.0, nil)
tv, err := television.NewTelevision("AUTO")
if err != nil {
panic(fmt.Errorf("error preparing television: %s", err))
}
err = tv.SetFeature(gui.ReqSetVisibility, true)
scr, err := sdldebug.NewSdlDebug(tv, 1.0)
if err != nil {
panic(fmt.Errorf("error preparing television: %s", err))
panic(fmt.Errorf("error preparing screen: %s", err))
}
err = scr.SetFeature(gui.ReqSetVisibility, true)
if err != nil {
panic(fmt.Errorf("error preparing screen: %s", err))
}
vcs, err := hardware.NewVCS(tv)

View file

@ -15,8 +15,8 @@ const pixelWidth = 2
const horizScale = 2
const vertScale = 2
// CanvasTV implements television.PixelRenderer
type CanvasTV struct {
// Canvas implements television.PixelRenderer
type Canvas struct {
// the worker in which our WASM application is running
worker js.Value
@ -28,34 +28,34 @@ type CanvasTV struct {
image []byte
}
// NewCanvasTV is the preferred method of initialisation for the CanvasTV type
func NewCanvasTV(worker js.Value) *CanvasTV {
// NewCanvas is the preferred method of initialisation for the Canvas type
func NewCanvas(worker js.Value) *Canvas {
var err error
scr := CanvasTV{worker: worker}
scr := &Canvas{worker: worker}
scr.Television, err = television.NewStellaTelevision("NTSC")
scr.Television, err = television.NewTelevision("NTSC")
if err != nil {
return nil
}
scr.Television.AddPixelRenderer(&scr)
scr.Television.AddPixelRenderer(scr)
// change tv spec after window creation (so we can set the window size)
err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesPerVisible)
err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesVisible)
if err != nil {
return nil
}
return &scr
return scr
}
func (scr *CanvasTV) Resize(topScanline, numScanlines int) error {
func (scr *Canvas) Resize(topScanline, numScanlines int) error {
scr.top = topScanline
scr.height = numScanlines * vertScale
// strictly, only the height will ever change on a specification change but
// it's convenient to set the width too
scr.width = television.ClocksPerVisible * pixelWidth * horizScale
scr.width = television.HorizClksVisible * pixelWidth * horizScale
// recreate image buffer of correct length
scr.image = make([]byte, scr.width*scr.height*pixelDepth)
@ -67,7 +67,7 @@ func (scr *CanvasTV) Resize(topScanline, numScanlines int) error {
}
// NewFrame implements telvision.PixelRenderer
func (scr *CanvasTV) NewFrame(frameNum int) error {
func (scr *Canvas) NewFrame(frameNum int) error {
scr.worker.Call("updateDebug", "frameNum", frameNum)
encodedImage := base64.StdEncoding.EncodeToString(scr.image)
scr.worker.Call("updateCanvas", encodedImage)
@ -79,13 +79,13 @@ func (scr *CanvasTV) NewFrame(frameNum int) error {
}
// NewScanline implements telvision.PixelRenderer
func (scr *CanvasTV) NewScanline(scanline int) error {
func (scr *Canvas) NewScanline(scanline int) error {
scr.worker.Call("updateDebug", "scanline", scanline)
return nil
}
// SetPixel implements telvision.PixelRenderer
func (scr *CanvasTV) SetPixel(x, y int, red, green, blue byte, vblank bool) error {
func (scr *Canvas) SetPixel(x, y int, red, green, blue byte, vblank bool) error {
if vblank {
// we could return immediately but if vblank is on inside the visible
// area we need to the set pixel to black, in case the vblank was off
@ -97,7 +97,7 @@ func (scr *CanvasTV) SetPixel(x, y int, red, green, blue byte, vblank bool) erro
}
// adjust pixels so we're only dealing with the visible range
x -= television.ClocksPerHblank
x -= television.HorizClksHBlank
y -= scr.top
if x < 0 || y < 0 {
@ -122,6 +122,6 @@ func (scr *CanvasTV) SetPixel(x, y int, red, green, blue byte, vblank bool) erro
}
// SetAltPixel implements telvision.PixelRenderer
func (scr *CanvasTV) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error {
func (scr *Canvas) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error {
return nil
}

View file

@ -12,10 +12,10 @@ import (
func main() {
worker := js.Global().Get("self")
ctv := NewCanvasTV(worker)
scr := NewCanvas(worker)
// create new vcs
vcs, err := hardware.NewVCS(ctv)
vcs, err := hardware.NewVCS(scr)
if err != nil {
panic(err)
}