mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
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:
parent
74bf70437b
commit
ba8e03ab7d
20 changed files with 741 additions and 641 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
|
|
61
debugger/input/compiled_decompiler.go
Normal file
61
debugger/input/compiled_decompiler.go
Normal 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 "!!"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -30,7 +30,7 @@ func (dc *future) tick() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
if dc.isScheduled() {
|
||||
if dc.remainingCycles > 0 {
|
||||
dc.remainingCycles--
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
45
television/dummy.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
20
television/sdltv/callback.go
Normal file
20
television/sdltv/callback.go
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
110
television/sdltv/requests.go
Normal file
110
television/sdltv/requests.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue