o television

- reworked television module(s)
	- SetVisibility()/SetPause() replaced with RequestSetAttr()

o debugger
	- "DISPLAY SCALE n" now works
	- tightening up of command template parsing - more error
	conditions/messages
This commit is contained in:
steve 2018-08-22 20:35:00 +01:00
parent 74bf70437b
commit ba8e03ab7d
20 changed files with 741 additions and 641 deletions

View file

@ -107,7 +107,7 @@ func (ct *ColorTerminal) UserRead(input []byte, prompt string) (int, error) {
ct.commandHistory = append(ct.commandHistory, command{input: nh})
}
ct.Print("\n")
ct.Print("\r\n")
return n + 1, nil
case easyterm.KeyEsc:

View file

@ -93,7 +93,7 @@ var Help = map[string]string{
var commandTemplate = input.CommandTemplate{
KeywordInsert: "%F",
KeywordSymbol: "%V [|ALL]",
KeywordSymbol: "%S [|ALL]",
KeywordBreak: "%*",
KeywordTrap: "%*",
KeywordList: "[BREAKS|TRAPS]",
@ -122,7 +122,7 @@ var commandTemplate = input.CommandTemplate{
KeywordMissile: "",
KeywordBall: "",
KeywordPlayfield: "",
KeywordDisplay: "[|OFF|OVERSCAN]",
KeywordDisplay: "[|OFF|DEBUG|SCALE] %*",
KeywordMouse: "[|X|Y]",
KeywordScript: "%F",
KeywordDisassemble: "",
@ -560,20 +560,39 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) {
case KeywordDisplay:
visibility := true
showOverscan := false
debug := false
action, present := tokens.Get()
if present {
action = strings.ToUpper(action)
switch action {
case "OFF":
visibility = false
case "OVERSCAN":
showOverscan = true
case "DEBUG":
debug = true
case "SCALE":
scl, present := tokens.Get()
if !present {
return false, fmt.Errorf("value required for %s %s", command, action)
}
scale, err := strconv.ParseFloat(scl, 32)
if err != nil {
return false, fmt.Errorf("%s %s value not valid (%s)", command, action, scl)
}
err = dbg.vcs.TV.RequestSetAttr(television.ReqSetScale, float32(scale))
return false, err
default:
return false, fmt.Errorf("unknown display action (%s)", action)
}
}
err := dbg.vcs.TV.SetVisibility(visibility, showOverscan)
err := dbg.vcs.TV.RequestSetAttr(television.ReqSetVisibility, visibility)
if err != nil {
return false, err
}
err = dbg.vcs.TV.RequestSetAttr(television.ReqSetDebug, debug)
if err != nil {
return false, err
}

View file

@ -96,7 +96,7 @@ func NewDebugger() (*Debugger, error) {
dbg.ui = new(ui.PlainTerminal)
// prepare hardware
tv, err := sdltv.NewSDLTV("NTSC", sdltv.IdealScale)
tv, err := sdltv.NewSDLTV("NTSC", 2.0)
if err != nil {
return nil, fmt.Errorf("error preparing television: %s", err)
}
@ -124,7 +124,7 @@ func NewDebugger() (*Debugger, error) {
dbg.syncChannel = make(chan func(), 2)
// register tv callbacks
err = tv.RegisterCallback(television.ReqOnMouseButtonRight, dbg.syncChannel, func() {
err = tv.RequestCallbackRegistration(television.ReqOnMouseButtonRight, dbg.syncChannel, func() {
// this callback function may be running inside a different goroutine
// so care must be taken not to cause a deadlock
hp, _ := dbg.vcs.TV.RequestTVInfo(television.ReqLastMouseX)
@ -314,7 +314,7 @@ func (dbg *Debugger) inputLoop(mainLoop bool) error {
if dbg.inputloopHalt {
// pause tv when emulation has halted
err = dbg.vcs.TV.SetPause(true)
err = dbg.vcs.TV.RequestSetAttr(television.ReqSetPause, true)
if err != nil {
return err
}
@ -367,7 +367,7 @@ func (dbg *Debugger) inputLoop(mainLoop bool) error {
// make sure tv is unpaused if emulation is about to resume
if dbg.inputloopNext {
err = dbg.vcs.TV.SetPause(false)
err = dbg.vcs.TV.RequestSetAttr(television.ReqSetPause, false)
if err != nil {
return err
}

View file

@ -3,25 +3,6 @@ package input
// Commands is the root of the argument "tree"
type Commands map[string]commandArgList
// argType defines the expected argument type
type argType int
// the possible values for argType
const (
argKeyword argType = iota
argFile
argValue
argString
argIndeterminate
)
// commandArg specifies the type and properties of an individual argument
type commandArg struct {
typ argType
required bool
values interface{}
}
// commandArgList is the list of commandArgList for each command
type commandArgList []commandArg
@ -32,7 +13,7 @@ func (a commandArgList) maximumLen() int {
return 0
}
if a[len(a)-1].typ == argIndeterminate {
// return the maximum value allowed for an integer
// to indicate indeterminancy, return the maximum value allowed for an integer
return int(^uint(0) >> 1)
}
return len(a)
@ -50,3 +31,23 @@ func (a commandArgList) requiredLen() (m int) {
}
return
}
// argType defines the expected argument type
type argType int
// the possible values for argType
const (
argKeyword argType = iota
argFile
argValue
argString
argIndeterminate
argNode
)
// commandArg specifies the type and properties of an individual argument
type commandArg struct {
typ argType
required bool
values interface{}
}

View file

@ -0,0 +1,61 @@
package input
import (
"fmt"
"strings"
)
// string functions for the three types:
//
// o Commands
// o commandArgList
// o commandArg
//
// calling String() on Commands should reproduce the template from which the
// commands were compiled
func (cmd Commands) String() string {
s := strings.Builder{}
for k, v := range cmd {
s.WriteString(k)
s.WriteString(fmt.Sprintf("%s", v))
s.WriteString("\n")
}
return s.String()
}
func (a commandArgList) String() string {
s := strings.Builder{}
for i := range a {
s.WriteString(fmt.Sprintf(" %s", a[i]))
}
return s.String()
}
func (c commandArg) String() string {
switch c.typ {
case argKeyword:
s := "["
switch values := c.values.(type) {
case []string:
for i := range values {
s = fmt.Sprintf("%s%s|", s, values[i])
}
s = strings.TrimSuffix(s, "|")
case *Commands:
s = fmt.Sprintf("%s<commands>", s)
default:
s = fmt.Sprintf("%s%T", s, values)
}
return fmt.Sprintf("%s]", s)
case argFile:
return "%F"
case argValue:
return "%V"
case argString:
return "%S"
case argIndeterminate:
return "%*"
}
return "!!"
}

View file

@ -9,61 +9,16 @@ import (
type CommandTemplate map[string]string
// CompileCommandTemplate creates a new instance of Commands from an instance
// of CommandTemplate. if no help is command is required, use the empty-string
// to for the helpKeyword argument
// of CommandTemplate. if no help command is required, call the function with
// helpKeyword == ""
func CompileCommandTemplate(template CommandTemplate, helpKeyword string) (Commands, error) {
var err error
commands := Commands{}
for k, v := range template {
commands[k] = commandArgList{}
placeholder := false
for i := 0; i < len(v); i++ {
switch v[i] {
case '%':
placeholder = true
case '[':
// find end of option list
j := strings.Index(v[i:], "]")
if j == -1 {
return commands, fmt.Errorf("unclosed option list (%s)", k)
}
options := strings.Split(v[i+1:j], "|")
if len(options) == 1 {
// note: Split() returns a slice of the input string, if
// the seperator ("|") cannot be found. the length of an
// empty option list is therefore 1.
return commands, fmt.Errorf("empty option list (%s)", k)
}
// decide whether the option is required
req := true
for m := 0; m < len(options); m++ {
if options[m] == "" {
req = false
break
}
}
// add a new argument for current keyword with the options
// we've found
commands[k] = append(commands[k], commandArg{typ: argKeyword, required: req, values: options})
default:
if placeholder {
switch v[i] {
case 'F':
commands[k] = append(commands[k], commandArg{typ: argFile, required: true})
case 'S':
commands[k] = append(commands[k], commandArg{typ: argString, required: true})
case 'V':
commands[k] = append(commands[k], commandArg{typ: argValue, required: true})
case '*':
commands[k] = append(commands[k], commandArg{typ: argIndeterminate, required: false})
}
}
}
commands[k], err = compileTemplateFragment(v)
if err != nil {
return nil, fmt.Errorf("error compiling %s: %s", k, err)
}
}
@ -73,3 +28,81 @@ func CompileCommandTemplate(template CommandTemplate, helpKeyword string) (Comma
return commands, nil
}
func compileTemplateFragment(fragment string) (commandArgList, error) {
argl := commandArgList{}
placeholder := false
// loop over template string
for i := 0; i < len(fragment); i++ {
switch fragment[i] {
case '%':
placeholder = true
case '[':
// find end of option list
j := strings.LastIndex(fragment[i:], "]") + i
if j == -1 {
return nil, fmt.Errorf("unterminated option list")
}
// check for empty list
if i+1 == j {
return nil, fmt.Errorf("empty option list")
}
// split options list into individual options
options := strings.Split(fragment[i+1:j], "|")
if len(options) == 1 {
options = make([]string, 1)
options[0] = fragment[i+1 : j]
}
// decide whether the option is a required option - if there is an
// empty option then the option isn't required
req := true
for o := 0; o < len(options); o++ {
if options[o] == "" {
if req == false {
return nil, fmt.Errorf("option list can contain only one empty option")
}
req = false
}
optionParts := strings.Split(options[o], " ")
if len(optionParts) > 1 {
return nil, fmt.Errorf("option list can only contain single keywords (%s)", options[o])
}
}
argl = append(argl, commandArg{typ: argKeyword, required: req, values: options})
i = j
case ' ':
// skip spaces
default:
if placeholder {
switch fragment[i] {
case 'F':
argl = append(argl, commandArg{typ: argFile, required: true})
case 'S':
argl = append(argl, commandArg{typ: argString, required: true})
case 'V':
argl = append(argl, commandArg{typ: argValue, required: true})
case '*':
argl = append(argl, commandArg{typ: argIndeterminate, required: false})
default:
return nil, fmt.Errorf("unknown placeholder directive (%c)", fragment[i])
}
placeholder = false
i++
} else {
return nil, fmt.Errorf("unparsable fragment (%s)", fragment, fragment[i])
}
}
}
return argl, nil
}

View file

@ -112,12 +112,16 @@ func fps(cartridgeFile string, justTheVCS bool) error {
return fmt.Errorf("error preparing television: %s", err)
}
} else {
tv, err = sdltv.NewSDLTV("NTSC", sdltv.IdealScale)
tv, err = sdltv.NewSDLTV("NTSC", 3.0)
if err != nil {
return fmt.Errorf("error preparing television: %s", err)
}
err = tv.RequestSetAttr(television.ReqSetVisibility, true)
if err != nil {
return fmt.Errorf("error preparing television: %s", err)
}
}
tv.SetVisibility(true, false)
vcs, err := hardware.NewVCS(tv)
if err != nil {
@ -158,11 +162,15 @@ func fps(cartridgeFile string, justTheVCS bool) error {
}
func run(cartridgeFile string) error {
tv, err := sdltv.NewSDLTV("NTSC", sdltv.IdealScale)
tv, err := sdltv.NewSDLTV("NTSC", 3.0)
if err != nil {
return fmt.Errorf("error preparing television: %s", err)
}
err = tv.RequestSetAttr(television.ReqSetVisibility, true)
if err != nil {
return fmt.Errorf("error preparing television: %s", err)
}
tv.SetVisibility(true, false)
vcs, err := hardware.NewVCS(tv)
if err != nil {
@ -178,7 +186,7 @@ func run(cartridgeFile string) error {
var runningLock sync.Mutex
running := true
err = tv.RegisterCallback(television.ReqOnWindowClose, nil, func() {
err = tv.RequestCallbackRegistration(television.ReqOnWindowClose, nil, func() {
runningLock.Lock()
running = false
runningLock.Unlock()

View file

@ -30,7 +30,7 @@ func (dc *future) tick() bool {
return true
}
if dc.isScheduled() {
if dc.remainingCycles > 0 {
dc.remainingCycles--
}

View file

@ -70,7 +70,7 @@ func (ms *missileSprite) tick() {
} else {
// tick draw signal only if a position reset is within three cycles of
// occuring. in effect, this prevents draw signal ticking during the
// first two cycles of a reset request , unless the reset is scheduled
// first two cycles of a reset request, unless the reset is scheduled
// during a HBLANK
if ms.futureReset.remainingCycles < 4 {
ms.tickGraphicsScan()

View file

@ -78,7 +78,12 @@ func (ps *playerSprite) tick() {
ps.deferDrawStart = true
} else {
ps.startDrawing()
ps.graphicsScanFilter = 3
if ps.size == 0x05 {
ps.graphicsScanFilter = 1
} else if ps.size == 0x07 {
ps.graphicsScanFilter = 3
}
}
} else {
// if player.position.tick() has not caused the position counter to

45
television/dummy.go Normal file
View file

@ -0,0 +1,45 @@
package television
import "gopher2600/errors"
// DummyTV is the null implementation of the television interface. useful
// for tools that don't need a television or related information at all.
type DummyTV struct{ Television }
// MachineInfoTerse (with DummyTV reciever) is the null implementation
func (DummyTV) MachineInfoTerse() string {
return ""
}
// MachineInfo (with DummyTV reciever) is the null implementation
func (DummyTV) MachineInfo() string {
return ""
}
// map String to MachineInfo
func (tv DummyTV) String() string {
return tv.MachineInfo()
}
// Signal (with DummyTV reciever) is the null implementation
func (DummyTV) Signal(SignalAttributes) {}
// RequestTVState (with dummyTV reciever) is the null implementation
func (DummyTV) RequestTVState(request TVStateReq) (*TVState, error) {
return nil, errors.NewGopherError(errors.UnknownTVRequest, request)
}
// RequestTVInfo (with dummyTV reciever) is the null implementation
func (DummyTV) RequestTVInfo(request TVInfoReq) (string, error) {
return "", errors.NewGopherError(errors.UnknownTVRequest, request)
}
// RequestCallbackRegistration (with dummyTV reciever) is the null implementation
func (DummyTV) RequestCallbackRegistration(request CallbackReq, channel chan func(), callback func()) error {
return errors.NewGopherError(errors.UnknownTVRequest, request)
}
// RequestSetAttr (with dummyTV reciever) is the null implementation
func (DummyTV) RequestSetAttr(request SetAttrReq, args ...interface{}) error {
return errors.NewGopherError(errors.UnknownTVRequest, request)
}

View file

@ -12,24 +12,29 @@ import (
// InitHeadlessTV() method is useful in this regard.
type HeadlessTV struct {
// spec is the specification of the tv type (NTSC or PAL)
Spec *specification
Spec *Specification
// 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 *TVState
HorizPos *TVState
// the current frame and scanline number
frameNum *TVState
scanline *TVState
FrameNum *TVState
Scanline *TVState
// 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
// has been sustained. we use this to help correctly implement vsync.
vsyncCount int
// records of signal information from the last call to Signal()
prevHSync bool
prevCBurst bool
// the scanline at which vblank is turned off and on
// - top mask ranges from 0 to VBlankOff-1
// - bottom mask ranges from VBlankOn to Spec.ScanlinesTotal
VBlankOff int
VBlankOn int
// if the signals we've received do not match what we expect then outOfSpec
// will be false for the duration of the rest of the frame. this is useful
@ -37,13 +42,9 @@ type HeadlessTV struct {
// misbehave.
outOfSpec bool
// phospher indicates whether the phosphor gun is active
Phosphor bool
// callbacks
NewFrame func() error
NewScanline func() error
forceUpdate func() error
// callback hooks from Signal()
SignalNewFrameHook func() error
SignalNewScanlineHook func() error
}
// NewHeadlessTV creates a new instance of HeadlessTV for a minimalist
@ -65,22 +66,21 @@ func NewHeadlessTV(tvType string) (*HeadlessTV, error) {
func InitHeadlessTV(tv *HeadlessTV, tvType string) error {
switch strings.ToUpper(tvType) {
case "NTSC":
tv.Spec = specNTSC
tv.Spec = SpecNTSC
case "PAL":
tv.Spec = specPAL
tv.Spec = SpecPAL
default:
return fmt.Errorf("unsupport tv type (%s)", tvType)
}
// empty callbacks
tv.NewFrame = func() error { return nil }
tv.NewScanline = func() error { return nil }
tv.forceUpdate = func() error { return nil }
tv.SignalNewFrameHook = func() error { return nil }
tv.SignalNewScanlineHook = func() error { return nil }
// initialise TVState
tv.horizPos = &TVState{label: "Horiz Pos", shortLabel: "HP", value: -tv.Spec.ClocksPerHblank, valueFormat: "%d"}
tv.frameNum = &TVState{label: "Frame", shortLabel: "FR", value: 0, valueFormat: "%d"}
tv.scanline = &TVState{label: "Scanline", shortLabel: "SL", value: 0, valueFormat: "%d"}
tv.HorizPos = &TVState{label: "Horiz Pos", shortLabel: "HP", value: -tv.Spec.ClocksPerHblank, valueFormat: "%d"}
tv.FrameNum = &TVState{label: "Frame", shortLabel: "FR", value: 0, valueFormat: "%d"}
tv.Scanline = &TVState{label: "Scanline", shortLabel: "SL", value: 0, valueFormat: "%d"}
return nil
}
@ -91,7 +91,7 @@ func (tv HeadlessTV) MachineInfoTerse() string {
if tv.outOfSpec {
specExclaim = " !!"
}
return fmt.Sprintf("%s %s %s%s", tv.frameNum.MachineInfoTerse(), tv.scanline.MachineInfoTerse(), tv.horizPos.MachineInfoTerse(), specExclaim)
return fmt.Sprintf("%s %s %s%s", tv.FrameNum.MachineInfoTerse(), tv.Scanline.MachineInfoTerse(), tv.HorizPos.MachineInfoTerse(), specExclaim)
}
// MachineInfo returns the television information in verbose format
@ -100,7 +100,7 @@ func (tv HeadlessTV) MachineInfo() string {
if tv.outOfSpec {
outOfSpec = "!!"
}
return fmt.Sprintf("%v\n%v\n%v%s\nPixel: %d", tv.frameNum, tv.scanline, tv.horizPos, outOfSpec, tv.PixelX(false))
return fmt.Sprintf("%v\n%v\n%v%s", tv.FrameNum, tv.Scanline, tv.HorizPos, outOfSpec)
}
// map String to MachineInfo
@ -108,57 +108,27 @@ func (tv HeadlessTV) String() string {
return tv.MachineInfo()
}
// PixelX returns an adjusted horizPos value
// -- adjustOrigin argument specifies whether or not pixel origin should be the
// visible portion of the screen
// -- note that if adjust origin is true, the function may return a negative
// number
func (tv HeadlessTV) PixelX(adjustOrigin bool) int {
if adjustOrigin {
return tv.horizPos.value
}
return tv.horizPos.value + tv.Spec.ClocksPerHblank
}
// PixelY returns an adjusted scanline value
// -- adjustOrigin argument specifies whether or not pixel origin should be the
// visible portion of the screen
// -- note that if adjust origin is true, the function may return a negative
// number
func (tv HeadlessTV) PixelY(adjustOrigin bool) int {
if adjustOrigin {
return tv.scanline.value - tv.Spec.ScanlinesPerVBlank
}
return tv.scanline.value
}
// ForceUpdate forces the tv image to be updated -- calls the forceUpdate
// callback from outside the television context (eg. from the debugger)
func (tv HeadlessTV) ForceUpdate() error {
return tv.forceUpdate()
}
// Signal is principle method of communication between the VCS and televsion
func (tv *HeadlessTV) Signal(attr SignalAttributes) {
// check that hsync signal is within the specification
if attr.HSync && !tv.prevHSync {
if tv.horizPos.value < -52 || tv.horizPos.value > -49 {
if attr.HSync && !tv.prevSignal.HSync {
if tv.HorizPos.value < -52 || tv.HorizPos.value > -49 {
tv.outOfSpec = true
}
} else if !attr.HSync && tv.prevHSync {
if tv.horizPos.value < -36 || tv.horizPos.value > -33 {
} else if !attr.HSync && tv.prevSignal.HSync {
if tv.HorizPos.value < -36 || tv.HorizPos.value > -33 {
tv.outOfSpec = true
}
}
// check that color burst signal is within the specification
if attr.CBurst && !tv.prevCBurst {
if tv.horizPos.value < -28 || tv.horizPos.value > -17 {
if attr.CBurst && !tv.prevSignal.CBurst {
if tv.HorizPos.value < -28 || tv.HorizPos.value > -17 {
tv.outOfSpec = true
}
} else if !attr.CBurst && tv.prevCBurst {
if tv.horizPos.value < -19 || tv.horizPos.value > -16 {
} else if !attr.CBurst && tv.prevSignal.CBurst {
if tv.HorizPos.value < -19 || tv.HorizPos.value > -16 {
tv.outOfSpec = true
}
}
@ -169,53 +139,48 @@ func (tv *HeadlessTV) Signal(attr SignalAttributes) {
} else {
if tv.vsyncCount >= tv.Spec.VsyncClocks {
tv.outOfSpec = false
tv.frameNum.value++
tv.scanline.value = 0
_ = tv.NewFrame()
tv.FrameNum.value++
tv.Scanline.value = 0
_ = tv.SignalNewFrameHook()
}
tv.vsyncCount = 0
}
// start a new scanline if a frontporch signal has been received
if attr.FrontPorch {
tv.horizPos.value = -tv.Spec.ClocksPerHblank
tv.scanline.value++
tv.NewScanline()
tv.HorizPos.value = -tv.Spec.ClocksPerHblank
tv.Scanline.value++
tv.SignalNewScanlineHook()
if tv.scanline.value > tv.Spec.ScanlinesTotal {
if tv.Scanline.value > tv.Spec.ScanlinesTotal {
// we've not yet received a correct vsync signal but we really should
// have. continue but mark the frame as being out of spec
tv.outOfSpec = true
}
} else {
tv.horizPos.value++
if tv.horizPos.value > tv.Spec.ClocksPerVisible {
tv.HorizPos.value++
if tv.HorizPos.value > tv.Spec.ClocksPerVisible {
// we've not yet received a front porch signal yet but we really should
// have. continue but mark the frame as being out of spec
tv.outOfSpec = true
}
}
// set phosphor state
tv.Phosphor = tv.horizPos.value >= 0 && !attr.VBlank
// note the scanline when vblank is turned on/off
if !attr.VBlank && tv.prevSignal.VBlank {
tv.VBlankOff = tv.Scanline.value
}
if attr.VBlank && !tv.prevSignal.VBlank {
tv.VBlankOn = tv.Scanline.value
}
// record the current signal settings so they can be used for reference
tv.prevHSync = attr.HSync
tv.prevSignal = attr
// everthing else we could possibly do requires a screen of some sort
// (eg. color decoding)
}
// SetVisibility does nothing for the HeadlessTV
func (tv *HeadlessTV) SetVisibility(visible, showOverscan bool) error {
return nil
}
// SetPause does nothing for the HeadlessTV
func (tv *HeadlessTV) SetPause(pause bool) error {
return nil
}
// RequestTVState returns the TVState object for the named state. television
// implementations in other packages will difficulty extending this function
// because TVStateReq does not expose its members.
@ -224,11 +189,11 @@ func (tv *HeadlessTV) RequestTVState(request TVStateReq) (*TVState, error) {
default:
return nil, errors.NewGopherError(errors.UnknownTVRequest, request)
case ReqFramenum:
return tv.frameNum, nil
return tv.FrameNum, nil
case ReqScanline:
return tv.scanline, nil
return tv.Scanline, nil
case ReqHorizPos:
return tv.horizPos, nil
return tv.HorizPos, nil
}
}
@ -242,8 +207,13 @@ func (tv *HeadlessTV) RequestTVInfo(request TVInfoReq) (string, error) {
}
}
// RegisterCallback is used to hook custom functionality into the televsion
func (tv *HeadlessTV) RegisterCallback(request CallbackReq, callback func()) error {
// RequestCallbackRegistration is used to hook custom functionality into the televsion
func (tv *HeadlessTV) RequestCallbackRegistration(request CallbackReq, channel chan func(), callback func()) error {
// the HeadlessTV implementation does nothing currently
return errors.NewGopherError(errors.UnknownTVRequest, request)
}
// RequestSetAttr is used to set a television attibute
func (tv *HeadlessTV) RequestSetAttr(request SetAttrReq, args ...interface{}) error {
return errors.NewGopherError(errors.UnknownTVRequest, request)
}

View file

@ -0,0 +1,20 @@
package sdltv
// callback is used to wrap functions supplied to RequestCallbackRegistration()
type callback struct {
channel chan func()
function func()
}
func (cb *callback) dispatch() {
if cb.function == nil {
return
}
if cb.channel != nil {
cb.channel <- cb.function
} else {
cb.function()
}
}

View file

@ -1,40 +1,33 @@
package sdltv
import (
"gopher2600/television"
"github.com/veandco/go-sdl2/sdl"
)
// guiLoop listens for SDL events and is run concurrently. critical sections
// protected by tv.guiLoopLock
func (tv *SDLTV) guiLoop() {
for true {
for {
ev := sdl.WaitEvent()
switch ev := ev.(type) {
// close window
case *sdl.QuitEvent:
// SetVisibility is outside of the critical section
tv.SetVisibility(false, false)
// *CRITICAL SECTION*
// (R) tv.onWindowClose
tv.guiLoopLock.Lock()
tv.onWindowClose.dispatch()
tv.guiLoopLock.Unlock()
tv.RequestSetAttr(television.ReqSetVisibility, false)
case *sdl.KeyboardEvent:
if ev.Type == sdl.KEYDOWN {
switch ev.Keysym.Sym {
case sdl.K_BACKQUOTE:
var showOverscan bool
// *CRITICAL SECTION*
// (R) tv.scr, tv.dbgScr
tv.guiLoopLock.Lock()
showOverscan = tv.scr != tv.dbgScr
tv.scr.toggleMasking()
tv.guiLoopLock.Unlock()
tv.SetVisibility(true, showOverscan)
// TODO: this doesn't work properly because we're in a
// different goroutine than the one in which we intialised
// the SDL library.
}
}
@ -46,18 +39,13 @@ func (tv *SDLTV) guiLoop() {
tv.onMouseButtonLeft.dispatch()
case sdl.BUTTON_RIGHT:
sx, sy := tv.renderer.GetScale()
sx, sy := tv.scr.renderer.GetScale()
// *CRITICAL SECTION*
// (W) mouseX, mouseY
// (R) tv.scr, tv.dbgScr
// (R) tv.onMouseButtonRight
tv.guiLoopLock.Lock()
// convert X pixel value to horizpos equivalent
// the opposite of pixelX() and also the scalining applied
// by the SDL renderer
if tv.scr == tv.dbgScr {
if tv.scr.unmasked {
tv.mouseX = int(float32(ev.X)/sx) - tv.Spec.ClocksPerHblank
} else {
tv.mouseX = int(float32(ev.X) / sx)
@ -66,15 +54,14 @@ func (tv *SDLTV) guiLoop() {
// convert Y pixel value to scanline equivalent
// the opposite of pixelY() and also the scalining applied
// by the SDL renderer
if tv.scr == tv.dbgScr {
if tv.scr.unmasked {
tv.mouseY = int(float32(ev.Y) / sy)
} else {
tv.mouseY = int(float32(ev.Y)/sy) + tv.Spec.ScanlinesPerVBlank
tv.mouseY = int(float32(ev.Y)/sy) + tv.Spec.ScanlinesPerVBlank + tv.Spec.ScanlinesPerVSync
}
tv.guiLoopLock.Unlock()
tv.onMouseButtonRight.dispatch()
tv.guiLoopLock.Unlock()
}
}

View file

@ -0,0 +1,110 @@
// television interface implementation - SDLTV has an embedded HeadlessTV so
// much of the interface is implementated there.
package sdltv
import (
"fmt"
"gopher2600/errors"
"gopher2600/television"
)
// RequestCallbackRegistration implements Television interface
func (tv *SDLTV) RequestCallbackRegistration(request television.CallbackReq, channel chan func(), callback func()) error {
// call embedded implementation and filter out UnknownCallbackRequests
err := tv.HeadlessTV.RequestCallbackRegistration(request, channel, callback)
switch err := err.(type) {
case errors.GopherError:
if err.Errno != errors.UnknownTVRequest {
return err
}
default:
return err
}
switch request {
case television.ReqOnWindowClose:
tv.onWindowClose.channel = channel
tv.onWindowClose.function = callback
case television.ReqOnMouseButtonLeft:
tv.onMouseButtonLeft.channel = channel
tv.onMouseButtonLeft.function = callback
case television.ReqOnMouseButtonRight:
tv.onMouseButtonRight.channel = channel
tv.onMouseButtonRight.function = callback
default:
return errors.NewGopherError(errors.UnknownTVRequest, request)
}
return nil
}
// RequestTVInfo returns the TVState object for the named state
func (tv *SDLTV) RequestTVInfo(request television.TVInfoReq) (string, error) {
state, err := tv.HeadlessTV.RequestTVInfo(request)
switch err := err.(type) {
case errors.GopherError:
if err.Errno != errors.UnknownTVRequest {
return state, err
}
default:
return state, err
}
switch request {
case television.ReqLastMouse:
return fmt.Sprintf("mouse: hp=%d, sl=%d", tv.mouseX, tv.mouseY), nil
case television.ReqLastMouseX:
return fmt.Sprintf("%d", tv.mouseX), nil
case television.ReqLastMouseY:
return fmt.Sprintf("%d", tv.mouseY), nil
default:
return "", errors.NewGopherError(errors.UnknownTVRequest, request)
}
}
// RequestSetAttr is used to set a television attibute
func (tv *SDLTV) RequestSetAttr(request television.SetAttrReq, args ...interface{}) error {
err := tv.HeadlessTV.RequestSetAttr(request)
switch err := err.(type) {
case errors.GopherError:
if err.Errno != errors.UnknownTVRequest {
return err
}
default:
return err
}
switch request {
case television.ReqSetVisibility:
if args[0].(bool) {
tv.scr.window.Show()
tv.update()
} else {
tv.scr.window.Hide()
}
case television.ReqSetPause:
tv.guiLoopLock.Lock()
tv.paused = args[0].(bool)
tv.guiLoopLock.Unlock()
if args[0].(bool) {
tv.update()
}
case television.ReqSetDebug:
tv.guiLoopLock.Lock()
tv.scr.setMasking(args[0].(bool))
tv.guiLoopLock.Unlock()
case television.ReqSetScale:
tv.guiLoopLock.Lock()
tv.scr.setScaling(args[0].(float32))
tv.guiLoopLock.Unlock()
default:
return errors.NewGopherError(errors.UnknownTVRequest, request)
}
return nil
}

View file

@ -1,33 +1,83 @@
package sdltv
import "github.com/veandco/go-sdl2/sdl"
import (
"gopher2600/television"
"github.com/veandco/go-sdl2/sdl"
)
type screen struct {
width int32
height int32
pixelDepth int32
tv *television.HeadlessTV
window *sdl.Window
renderer *sdl.Renderer
playWidth int32
playHeight int32
maxWidth int32
maxHeight int32
depth int32
pitch int
// the width of each VCS colour clock (in SDL pixels)
pixelWidth int
// by how much each pixel should be scaled
pixelScale float32
noMask *sdl.Rect
maskRectDst *sdl.Rect
maskRectSrc *sdl.Rect
texture *sdl.Texture
fadeTexture *sdl.Texture
pixelsA []byte
pixelsB []byte
pixels []byte
pixelsFade []byte
pixelSwapA []byte
pixelSwapB []byte
// whether we're using the max screen
// - destRect and srcRect change depending on the value of unmasked
unmasked bool
destRect *sdl.Rect
srcRect *sdl.Rect
}
func newScreen(width, height int32, renderer *sdl.Renderer) (*screen, error) {
func newScreen(tv *television.HeadlessTV) (*screen, error) {
var err error
scr := new(screen)
scr.width = width
scr.height = height
scr.pixelDepth = 4
scr.tv = tv
// SDL window - the correct size for the window will be determined below
scr.window, err = sdl.CreateWindow("Gopher2600", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 0, 0, sdl.WINDOW_HIDDEN|sdl.WINDOW_OPENGL)
if err != nil {
return nil, err
}
// SDL renderer
scr.renderer, err = sdl.CreateRenderer(scr.window, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC)
if err != nil {
return nil, err
}
scr.playWidth = int32(tv.Spec.ClocksPerVisible)
scr.playHeight = int32(tv.Spec.ScanlinesPerVisible)
scr.maxWidth = int32(tv.Spec.ClocksPerScanline)
scr.maxHeight = int32(tv.Spec.ScanlinesTotal)
scr.depth = 4
scr.pitch = int(scr.maxWidth * scr.depth)
// pixelWidth is the number of tv pixels per color clock. we don't need to
// worry about this again once we've created the window and set the scaling
// for the renderer
scr.pixelWidth = 2
// screen texture is used to draw the pixels onto the sdl window (by the
// renderer). it is used evey frame, regardless of whether the tv is paused
// or unpaused
scr.texture, err = renderer.CreateTexture(sdl.PIXELFORMAT_ABGR8888, sdl.TEXTUREACCESS_STREAMING, scr.width, scr.height)
scr.texture, err = scr.renderer.CreateTexture(sdl.PIXELFORMAT_ABGR8888, sdl.TEXTUREACCESS_STREAMING, scr.maxWidth, scr.maxHeight)
if err != nil {
return nil, err
}
@ -36,7 +86,7 @@ func newScreen(width, height int32, renderer *sdl.Renderer) (*screen, error) {
// fade texture is only used when the tv is paused. it is used to display
// the previous frame as a guide, in case the current frame is not completely
// rendered
scr.fadeTexture, err = renderer.CreateTexture(sdl.PIXELFORMAT_ABGR8888, sdl.TEXTUREACCESS_STREAMING, scr.width, scr.height)
scr.fadeTexture, err = scr.renderer.CreateTexture(sdl.PIXELFORMAT_ABGR8888, sdl.TEXTUREACCESS_STREAMING, scr.maxWidth, scr.maxHeight)
if err != nil {
return nil, err
}
@ -44,30 +94,135 @@ func newScreen(width, height int32, renderer *sdl.Renderer) (*screen, error) {
scr.fadeTexture.SetAlphaMod(50)
// our acutal screen data
scr.pixelSwapA = make([]byte, scr.width*scr.height*scr.pixelDepth)
scr.pixelSwapB = make([]byte, scr.width*scr.height*scr.pixelDepth)
scr.pixels = scr.pixelSwapA
scr.pixelsFade = scr.pixelSwapB
scr.pixelsA = make([]byte, scr.maxWidth*scr.maxHeight*scr.depth)
scr.pixelsB = make([]byte, scr.maxWidth*scr.maxHeight*scr.depth)
scr.pixels = scr.pixelsA
scr.pixelsFade = scr.pixelsB
scr.noMask = &sdl.Rect{X: 0, Y: 0, W: scr.maxWidth, H: scr.maxHeight}
scr.maskRectDst = &sdl.Rect{X: 0, Y: 0, W: scr.playWidth, H: scr.playHeight}
scr.maskRectSrc = &sdl.Rect{X: int32(tv.Spec.ClocksPerHblank), Y: int32(tv.Spec.ScanlinesPerVBlank + tv.Spec.ScanlinesPerVSync), W: scr.playWidth, H: scr.playHeight}
return scr, nil
}
func (scr *screen) swapBuffer() {
// swap which pixel buffer we're using
func (scr *screen) setScaling(scale float32) error {
// pixel scale is the number of pixels each VCS "pixel" is to be occupy on
// the screen
scr.pixelScale = scale
// make sure everything drawn through the renderer is correctly scaled
err := scr.renderer.SetScale(float32(scr.pixelWidth)*scr.pixelScale, scr.pixelScale)
if err != nil {
return err
}
scr.setMasking(scr.unmasked)
return nil
}
func (scr *screen) setMasking(unmasked bool) {
var w, h int32
scr.unmasked = unmasked
if scr.unmasked {
w = int32(float32(scr.maxWidth) * scr.pixelScale * float32(scr.pixelWidth))
h = int32(float32(scr.maxHeight) * scr.pixelScale)
scr.destRect = scr.noMask
scr.srcRect = scr.noMask
} else {
w = int32(float32(scr.playWidth) * scr.pixelScale * float32(scr.pixelWidth))
h = int32(float32(scr.playHeight) * scr.pixelScale)
scr.destRect = scr.maskRectDst
scr.srcRect = scr.maskRectSrc
}
scr.window.SetSize(w, h)
}
func (scr *screen) toggleMasking() {
scr.setMasking(!scr.unmasked)
}
func (scr *screen) setPixel(x, y int32, red, green, blue byte) {
i := (y*scr.maxWidth + x) * scr.depth
if i < int32(len(scr.pixels))-scr.depth && i >= 0 {
scr.pixels[i] = red
scr.pixels[i+1] = green
scr.pixels[i+2] = blue
scr.pixels[i+3] = 255
}
}
func (scr *screen) update(paused bool) error {
var err error
// clear image from rendered
scr.renderer.SetDrawColor(5, 5, 5, 255)
scr.renderer.SetDrawBlendMode(sdl.BLENDMODE_NONE)
err = scr.renderer.Clear()
if err != nil {
return err
}
// if tv is paused then show the previous frame's faded image
if paused {
err = scr.fadeTexture.Update(nil, scr.pixelsFade, scr.pitch)
if err != nil {
return err
}
err = scr.renderer.Copy(scr.fadeTexture, scr.srcRect, scr.destRect)
if err != nil {
return err
}
}
// show current frame's pixels
err = scr.texture.Update(nil, scr.pixels, scr.pitch)
if err != nil {
return err
}
err = scr.renderer.Copy(scr.texture, scr.srcRect, scr.destRect)
if err != nil {
return err
}
// draw masks
if scr.unmasked {
scr.renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
scr.renderer.SetDrawColor(10, 10, 10, 100)
// hblank mask
scr.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(scr.tv.Spec.ClocksPerHblank), H: scr.srcRect.H})
} else {
scr.renderer.SetDrawBlendMode(sdl.BLENDMODE_NONE)
scr.renderer.SetDrawColor(0, 0, 0, 255)
}
// top vblank mask
h := int32(scr.tv.VBlankOff) - scr.srcRect.Y
scr.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: scr.srcRect.W, H: h})
// bottom vblank mask
y := int32(scr.tv.VBlankOn) - scr.srcRect.Y
h = int32(scr.tv.Spec.ScanlinesTotal - scr.tv.VBlankOn)
scr.renderer.FillRect(&sdl.Rect{X: 0, Y: y, W: scr.srcRect.W, H: h})
return nil
}
func (scr *screen) swapPixels() {
// swap which pixel buffer we're using in time for next roung of pixel
// plotting
swp := scr.pixels
scr.pixels = scr.pixelsFade
scr.pixelsFade = swp
scr.clearBuffer()
}
func (scr *screen) clearBuffer() {
for y := int32(0); y < scr.height; y++ {
for x := int32(0); x < scr.width; x++ {
i := (y*scr.width + x) * scr.pixelDepth
scr.pixels[i] = 0
scr.pixels[i+1] = 0
scr.pixels[i+2] = 0
scr.pixels[i+3] = 0
}
// clear pixels
for i := 0; i < len(scr.pixels); i++ {
scr.pixels[i] = 0
}
}

View file

@ -8,48 +8,13 @@ import (
"github.com/veandco/go-sdl2/sdl"
)
// IdealScale is the suggested scaling for the screen
const IdealScale = 2.0
type callback struct {
channel chan func()
function func()
}
func (cb *callback) dispatch() {
if cb.function == nil {
return
}
if cb.channel != nil {
cb.channel <- cb.function
} else {
cb.function()
}
}
// SDLTV is the SDL implementation of a simple television
type SDLTV struct {
television.HeadlessTV
window *sdl.Window
renderer *sdl.Renderer
// we can flip between two screen types. a regular play screen, which is
// masked as per a real television. and a debug screen, which has no
// masking
playScr *screen
dbgScr *screen
// scr points to the screen (playScr or dbScr) currently in use
// much of the sdl magic happens in the screen object
scr *screen
// the width of each VCS colour clock (in SDL pixels)
pixelWidth int
// by how much each pixel should be scaled
pixelScale float32
// the time the last frame was rendered - used to limit frame rate
lastFrameRender time.Time
@ -66,10 +31,7 @@ type SDLTV struct {
mouseX int // expressed as horizontal position
mouseY int // expressed as scanlines
// guiLoopLock is used to protect anything that happens inside guiLoop()
// care must be taken to activate the lock when those assets are accessed
// outside of the guiLoop(), or for strong commentary to be present when it
// is not required.
// critical section protection
guiLoopLock sync.Mutex
}
@ -85,74 +47,30 @@ func NewSDLTV(tvType string, scale float32) (*SDLTV, error) {
}
// set up sdl
err = sdl.Init(uint32(0))
err = sdl.Init(sdl.INIT_EVERYTHING)
if err != nil {
return nil, err
}
// pixelWidth is the number of tv pixels per color clock. we don't need to
// worry about this again once we've created the window and set the scaling
// for the renderer
tv.pixelWidth = 2
// initialise the screens we'll be using
tv.scr, err = newScreen(&tv.HeadlessTV)
// pixel scale is the number of pixels each VCS "pixel" is to be occupy on
// the screen
tv.pixelScale = scale
// SDL initialisation
// SDL window - the correct size for the window will be determined below
tv.window, err = sdl.CreateWindow("Gopher2600", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 0, 0, sdl.WINDOW_HIDDEN|sdl.WINDOW_OPENGL)
// set window size and scaling
err = tv.scr.setScaling(scale)
if err != nil {
return nil, err
}
// SDL renderer
tv.renderer, err = sdl.CreateRenderer(tv.window, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC)
if err != nil {
return nil, err
}
// make sure everything drawn through the renderer is correctly scaled
err = tv.renderer.SetScale(float32(tv.pixelWidth)*tv.pixelScale, tv.pixelScale)
if err != nil {
return nil, err
}
// new screens
playWidth := int32(tv.HeadlessTV.Spec.ClocksPerVisible)
playHeight := int32(tv.HeadlessTV.Spec.ScanlinesPerVisible)
tv.playScr, err = newScreen(playWidth, playHeight, tv.renderer)
if err != nil {
return nil, err
}
debugWidth := int32(tv.HeadlessTV.Spec.ClocksPerScanline)
debugHeight := int32(tv.HeadlessTV.Spec.ScanlinesTotal)
tv.dbgScr, err = newScreen(debugWidth, debugHeight, tv.renderer)
if err != nil {
return nil, err
}
tv.scr = tv.playScr
tv.setWindowSize(tv.scr.width, tv.scr.height)
// register callbacks from HeadlessTV to SDLTV
tv.NewFrame = func() error {
defer tv.scr.swapBuffer()
return tv.update()
}
tv.SignalNewFrameHook = tv.newFrame
// update tv with a black image
tv.scr.clearBuffer()
// update tv (with a black image)
err = tv.update()
if err != nil {
return nil, err
}
// begin "gui loop"
// begin gui loop
go tv.guiLoop()
// note that we've elected not to show the window on startup
@ -160,98 +78,47 @@ func NewSDLTV(tvType string, scale float32) (*SDLTV, error) {
return tv, nil
}
// set window size scales the width and height correctly so that the VCS image
// is correct
func (tv *SDLTV) setWindowSize(width, height int32) {
// *CRITICAL SECTION* *NOT REQUIRED*
// called from NewSDLTV and then guiLoop() but never in parallel
// Signal is the principle method of communication between the VCS and
// televsion. note that most of the work is done in the embedded HeadlessTV
// instance
func (tv *SDLTV) Signal(attr television.SignalAttributes) {
tv.HeadlessTV.Signal(attr)
winWidth := int32(float32(width) * tv.pixelScale * float32(tv.pixelWidth))
winHeight := int32(float32(height) * tv.pixelScale)
tv.window.SetSize(winWidth, winHeight)
tv.guiLoopLock.Lock()
// decode color
r, g, b := byte(0), byte(0), byte(0)
if attr.Pixel <= 256 {
col := tv.Spec.Colors[attr.Pixel]
r, g, b = byte((col&0xff0000)>>16), byte((col&0xff00)>>8), byte(col&0xff)
}
x := int32(tv.HorizPos.Value().(int)) + int32(tv.Spec.ClocksPerHblank)
y := int32(tv.Scanline.Value().(int))
tv.scr.setPixel(x, y, r, g, b)
tv.guiLoopLock.Unlock()
}
func (tv *SDLTV) setPixel(x, y int32, red, green, blue byte, pixels []byte) {
i := (y*tv.scr.width + x) * tv.scr.pixelDepth
if i < int32(len(pixels))-tv.scr.pixelDepth && i >= 0 {
pixels[i] = red
pixels[i+1] = green
pixels[i+2] = blue
pixels[i+3] = 255
}
func (tv *SDLTV) newFrame() error {
defer tv.scr.swapPixels()
return tv.update()
}
// update the gui so that it reflects changes to buffered data in the tv struct
func (tv *SDLTV) update() error {
// *CRITICAL SECTION*
// (R) tv.scr
tv.guiLoopLock.Lock()
defer tv.guiLoopLock.Unlock()
var err error
// clear image from rendered
if tv.scr == tv.dbgScr {
tv.renderer.SetDrawColor(5, 5, 5, 255)
} else {
tv.renderer.SetDrawColor(0, 0, 0, 255)
}
tv.renderer.SetDrawBlendMode(sdl.BLENDMODE_NONE)
err = tv.renderer.Clear()
// abbrogate mot of the updating to the screem
err := tv.scr.update(tv.paused)
if err != nil {
return err
}
// if tv is paused then show the previous frame's faded image
if tv.paused {
err := tv.scr.fadeTexture.Update(nil, tv.scr.pixelsFade, int(tv.scr.width*tv.scr.pixelDepth))
if err != nil {
return err
}
err = tv.renderer.Copy(tv.scr.fadeTexture, nil, nil)
if err != nil {
return err
}
}
// show current frame's pixels
err = tv.scr.texture.Update(nil, tv.scr.pixels, int(tv.scr.width*tv.scr.pixelDepth))
if err != nil {
return err
}
err = tv.renderer.Copy(tv.scr.texture, nil, nil)
if err != nil {
return err
}
if tv.scr == tv.dbgScr {
// add screen boundary overlay
tv.renderer.SetDrawColor(100, 100, 100, 25)
tv.renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
tv.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(tv.Spec.ClocksPerHblank), H: int32(tv.Spec.ScanlinesTotal)})
tv.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(tv.Spec.ClocksPerScanline), H: int32(tv.Spec.ScanlinesPerVBlank)})
tv.renderer.FillRect(&sdl.Rect{X: 0, Y: int32(tv.Spec.ScanlinesTotal - tv.Spec.ScanlinesPerOverscan), W: int32(tv.Spec.ClocksPerScanline), H: int32(tv.Spec.ScanlinesPerOverscan)})
// add cursor overlay only if tv is paused
if tv.paused {
tv.renderer.SetDrawColor(255, 255, 255, 100)
tv.renderer.SetDrawBlendMode(sdl.BLENDMODE_NONE)
cursorX := tv.PixelX(false)
cursorY := tv.PixelY(false)
if cursorX >= tv.Spec.ClocksPerScanline+tv.Spec.ClocksPerHblank {
cursorX = 0
cursorY++
}
tv.renderer.DrawRect(&sdl.Rect{X: int32(cursorX), Y: int32(cursorY), W: 2, H: 2})
}
}
// finalise updating of screen
// for windowed SDL, attempt to synchronise to 60fps (VSYNC hint only seems
// to work if window is in full screen mode)
// FPS limiting - for windowed SDL, attempt to synchronise to 60fps (VSYNC
// hint only seems to work if window is in full screen mode)
time.Sleep(16666*time.Microsecond - time.Since(tv.lastFrameRender))
tv.renderer.Present()
tv.scr.renderer.Present()
tv.lastFrameRender = time.Now()
return nil

View file

@ -1,147 +0,0 @@
// television interface implementation - SDLTV has an embedded HeadlessTV so
// much of the interface is implementated there.
package sdltv
import (
"fmt"
"gopher2600/errors"
"gopher2600/television"
)
// Signal is the principle method of communication between the VCS and
// televsion. note that most of the work is done in the embedded HeadlessTV
// instance
func (tv *SDLTV) Signal(attr television.SignalAttributes) {
tv.HeadlessTV.Signal(attr)
// *CRITICAL SECTION*
// (R) tv.scr, tv.dbgScr
tv.guiLoopLock.Lock()
defer tv.guiLoopLock.Unlock()
guiDbgScr := tv.scr == tv.dbgScr
if tv.Phosphor || guiDbgScr {
// decode color
r, g, b := byte(0), byte(0), byte(0)
if attr.Pixel <= 256 {
col := tv.Spec.Colors[attr.Pixel]
r, g, b = byte((col&0xff0000)>>16), byte((col&0xff00)>>8), byte(col&0xff)
}
tv.setPixel(int32(tv.PixelX(!guiDbgScr)), int32(tv.PixelY(!guiDbgScr)), r, g, b, tv.scr.pixels)
}
}
// SetVisibility toggles the visiblity of the SDLTV window
func (tv *SDLTV) SetVisibility(visible, showOverscan bool) error {
// *CRITICAL SECTION*
// (W) tv.scr
// (R) tv.playScr, tv.dbgScr
tv.guiLoopLock.Lock()
if showOverscan {
tv.scr = tv.dbgScr
} else {
tv.scr = tv.playScr
}
tv.setWindowSize(tv.scr.width, tv.scr.height)
tv.guiLoopLock.Unlock()
// *NON-CRITICAL SECTION* SDL handles its own concurrency conflicts
if visible {
tv.window.Show()
} else {
tv.window.Hide()
}
return nil
}
// SetPause toggles whether the tv is currently being updated. we can use this
// when we pause the emulation to make sure we aren't left with a blank screen
func (tv *SDLTV) SetPause(pause bool) error {
if pause {
tv.paused = true
tv.update()
} else {
tv.paused = false
}
return nil
}
// RegisterCallback implements Television interface
func (tv *SDLTV) RegisterCallback(request television.CallbackReq, channel chan func(), callback func()) error {
// call embedded implementation and filter out UnknownCallbackRequests
err := tv.HeadlessTV.RegisterCallback(request, callback)
switch err := err.(type) {
case errors.GopherError:
if err.Errno != errors.UnknownTVRequest {
return err
}
default:
return err
}
switch request {
case television.ReqOnWindowClose:
// * CRITICAL SEECTION*
// (W) tv.onWindowClose
tv.guiLoopLock.Lock()
tv.onWindowClose.channel = channel
tv.onWindowClose.function = callback
tv.guiLoopLock.Unlock()
case television.ReqOnMouseButtonLeft:
// * CRITICAL SEECTION*
// (W) tv.onMouseButtonLeft
tv.guiLoopLock.Lock()
tv.onMouseButtonLeft.channel = channel
tv.onMouseButtonLeft.function = callback
tv.guiLoopLock.Unlock()
case television.ReqOnMouseButtonRight:
// * CRITICAL SEECTION*
// (W) tv.onMouseButtonRight
tv.guiLoopLock.Lock()
tv.onMouseButtonRight.channel = channel
tv.onMouseButtonRight.function = callback
tv.guiLoopLock.Unlock()
default:
return errors.NewGopherError(errors.UnknownTVRequest, request)
}
return nil
}
// RequestTVInfo returns the TVState object for the named state
func (tv *SDLTV) RequestTVInfo(request television.TVInfoReq) (string, error) {
state, err := tv.HeadlessTV.RequestTVInfo(request)
switch err := err.(type) {
case errors.GopherError:
if err.Errno != errors.UnknownTVRequest {
return state, err
}
default:
return state, err
}
switch request {
case television.ReqLastMouse:
// * CRITICAL SEECTION*
// (R) tv.mouseX, tv.mouseY
tv.guiLoopLock.Lock()
defer tv.guiLoopLock.Unlock()
return fmt.Sprintf("mouse: hp=%d, sl=%d", tv.mouseX, tv.mouseY), nil
case television.ReqLastMouseX:
// * CRITICAL SEECTION*
// (R) tv.mouseX
tv.guiLoopLock.Lock()
defer tv.guiLoopLock.Unlock()
return fmt.Sprintf("%d", tv.mouseX), nil
case television.ReqLastMouseY:
// * CRITICAL SEECTION*
// (R) tv.mouseY
tv.guiLoopLock.Lock()
defer tv.guiLoopLock.Unlock()
return fmt.Sprintf("%d", tv.mouseY), nil
default:
return "", errors.NewGopherError(errors.UnknownTVRequest, request)
}
}

View file

@ -1,50 +1,56 @@
package television
type specification struct {
// Specification is used to define the two television specifications
type Specification struct {
ID string
ClocksPerHblank int
ClocksPerVisible int
ClocksPerScanline int
VsyncClocks int
ScanlinesPerVSync int
ScanlinesPerVBlank int
ScanlinesPerVisible int
ScanlinesPerOverscan int
ScanlinesTotal int
VsyncClocks int
Colors []color
}
var specNTSC *specification
var specPAL *specification
// SpecNTSC is the specification for NTSC television typee
var SpecNTSC *Specification
// SpecPAL is the specification for PAL television typee
var SpecPAL *Specification
func init() {
specNTSC = new(specification)
specNTSC.ID = "NTSC"
specNTSC.ClocksPerHblank = 68
specNTSC.ClocksPerVisible = 160
specNTSC.ClocksPerScanline = 228
specNTSC.VsyncClocks = 3 * specNTSC.ClocksPerScanline
specNTSC.ScanlinesPerVBlank = 37
specNTSC.ScanlinesPerVisible = 228
specNTSC.ScanlinesPerOverscan = 30
specNTSC.ScanlinesTotal = 298
specNTSC.Colors = ntscColors
SpecNTSC = new(Specification)
SpecNTSC.ID = "NTSC"
SpecNTSC.ClocksPerHblank = 68
SpecNTSC.ClocksPerVisible = 160
SpecNTSC.ClocksPerScanline = 228
SpecNTSC.ScanlinesPerVSync = 3
SpecNTSC.ScanlinesPerVBlank = 37
SpecNTSC.ScanlinesPerVisible = 192
SpecNTSC.ScanlinesPerOverscan = 30
SpecNTSC.ScanlinesTotal = 262
SpecNTSC.Colors = ntscColors
SpecNTSC.VsyncClocks = SpecNTSC.ScanlinesPerVSync * SpecNTSC.ClocksPerScanline
specPAL = new(specification)
specPAL.ID = "PAL"
specPAL.ClocksPerHblank = 68
specPAL.ClocksPerVisible = 160
specPAL.ClocksPerScanline = 228
specPAL.VsyncClocks = 3 * specPAL.ClocksPerScanline
specPAL.ScanlinesPerVBlank = 45
specPAL.ScanlinesPerVisible = 228
specPAL.ScanlinesPerOverscan = 36
specPAL.ScanlinesTotal = 312
SpecPAL = new(Specification)
SpecPAL.ID = "PAL"
SpecPAL.ClocksPerHblank = 68
SpecPAL.ClocksPerVisible = 160
SpecPAL.ClocksPerScanline = 228
SpecPAL.ScanlinesPerVBlank = 45
SpecPAL.ScanlinesPerVisible = 228
SpecPAL.ScanlinesPerOverscan = 36
SpecPAL.ScanlinesTotal = 312
SpecPAL.VsyncClocks = SpecPAL.ScanlinesPerVSync * SpecPAL.ClocksPerScanline
// use NTSC colors for PAL specification for now
// TODO: implement PAL colors
specPAL.Colors = ntscColors
SpecPAL.Colors = ntscColors
}

View file

@ -1,6 +1,19 @@
package television
import "gopher2600/errors"
// TVStateReq is used to identify which television attribute is being asked
// for with the GetTVState() function
type TVStateReq string
// TVInfoReq is used to identiry what information is being requested with the
// GetTVInfo() function
type TVInfoReq string
// CallbackReq is used to identify which callback to register
type CallbackReq string
// SetAttrReq is used to request the setting of a television attribute
// eg. setting debugging overscan
type SetAttrReq string
// list of valid requests for television implementations. it is not
// required that every implementation does something useful for every request.
@ -19,6 +32,11 @@ const (
ReqOnWindowClose CallbackReq = "ONWINDOWCLOSE"
ReqOnMouseButtonLeft CallbackReq = "ONMOUSEBUTTONLEFT"
ReqOnMouseButtonRight CallbackReq = "ONMOUSEBUTTONRIGHT"
ReqSetVisibility SetAttrReq = "SETVISIBILITY" // bool
ReqSetPause SetAttrReq = "SETPAUSE" // bool
ReqSetDebug SetAttrReq = "SETDEBUG" // bool
ReqSetScale SetAttrReq = "SETSCALE" // float
)
// SignalAttributes represents the data sent to the television
@ -27,73 +45,15 @@ type SignalAttributes struct {
Pixel PixelSignal
}
// TVStateReq is used to identify which television attribute is being asked
// for with the GetTVState() function
type TVStateReq string
// TVInfoReq is used to identiry what information is being requested with the
// GetTVInfo() function
type TVInfoReq string
// CallbackReq is used to identify which callback to register
type CallbackReq string
// Television defines the operations that can be performed on the television
type Television interface {
MachineInfoTerse() string
MachineInfo() string
Signal(SignalAttributes)
SetVisibility(visible, showOverscan bool) error
SetPause(pause bool) error
RequestTVState(TVStateReq) (*TVState, error)
RequestTVInfo(TVInfoReq) (string, error)
RegisterCallback(CallbackReq, chan func(), func()) error
}
// DummyTV is the null implementation of the television interface. useful
// for tools that don't need a television or related information at all.
type DummyTV struct{ Television }
// MachineInfoTerse (with DummyTV reciever) is the null implementation
func (DummyTV) MachineInfoTerse() string {
return ""
}
// MachineInfo (with DummyTV reciever) is the null implementation
func (DummyTV) MachineInfo() string {
return ""
}
// map String to MachineInfo
func (tv DummyTV) String() string {
return tv.MachineInfo()
}
// Signal (with DummyTV reciever) is the null implementation
func (DummyTV) Signal(SignalAttributes) {}
// SetVisibility (with dummyTV reciever) is the null implementation
func (DummyTV) SetVisibility(visible, showOverscan bool) error {
return nil
}
// SetPause (with dummyTV reciever) is the null implementation
func (DummyTV) SetPause(pause bool) error {
return nil
}
// RequestTVState (with dummyTV reciever) is the null implementation
func (DummyTV) RequestTVState(request TVStateReq) (*TVState, error) {
return nil, errors.NewGopherError(errors.UnknownTVRequest, request)
}
// RequestTVInfo (with dummyTV reciever) is the null implementation
func (DummyTV) RequestTVInfo(request TVInfoReq) (string, error) {
return "", errors.NewGopherError(errors.UnknownTVRequest, request)
}
// RegisterCallback (with dummyTV reciever) is the null implementation
func (DummyTV) RegisterCallback(request CallbackReq, channel chan func(), callback func()) error {
return errors.NewGopherError(errors.UnknownTVRequest, request)
RequestCallbackRegistration(CallbackReq, chan func(), func()) error
RequestSetAttr(request SetAttrReq, args ...interface{}) error
}