diff --git a/debugger/colorterm/input.go b/debugger/colorterm/input.go index bef775f4..ef6eeed1 100644 --- a/debugger/colorterm/input.go +++ b/debugger/colorterm/input.go @@ -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: diff --git a/debugger/commands.go b/debugger/commands.go index 17778caf..929df33a 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -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 } diff --git a/debugger/debugger.go b/debugger/debugger.go index f699580d..453b2bec 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -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 } diff --git a/debugger/input/compiled.go b/debugger/input/compiled.go index 99cab0c1..acb2483c 100644 --- a/debugger/input/compiled.go +++ b/debugger/input/compiled.go @@ -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{} +} diff --git a/debugger/input/compiled_decompiler.go b/debugger/input/compiled_decompiler.go new file mode 100644 index 00000000..7422debf --- /dev/null +++ b/debugger/input/compiled_decompiler.go @@ -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", 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 "!!" +} diff --git a/debugger/input/template.go b/debugger/input/template.go index d9722b8a..6e2c5cac 100644 --- a/debugger/input/template.go +++ b/debugger/input/template.go @@ -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 +} diff --git a/gopher2600.go b/gopher2600.go index 7561c5b3..20b59881 100644 --- a/gopher2600.go +++ b/gopher2600.go @@ -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() diff --git a/hardware/tia/video/future.go b/hardware/tia/video/future.go index f67c1314..8e003bd5 100644 --- a/hardware/tia/video/future.go +++ b/hardware/tia/video/future.go @@ -30,7 +30,7 @@ func (dc *future) tick() bool { return true } - if dc.isScheduled() { + if dc.remainingCycles > 0 { dc.remainingCycles-- } diff --git a/hardware/tia/video/missile.go b/hardware/tia/video/missile.go index 1371e309..70c2a72f 100644 --- a/hardware/tia/video/missile.go +++ b/hardware/tia/video/missile.go @@ -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() diff --git a/hardware/tia/video/player.go b/hardware/tia/video/player.go index fa662453..04a9b0e1 100644 --- a/hardware/tia/video/player.go +++ b/hardware/tia/video/player.go @@ -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 diff --git a/television/dummy.go b/television/dummy.go new file mode 100644 index 00000000..5c8d43a2 --- /dev/null +++ b/television/dummy.go @@ -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) +} diff --git a/television/headless.go b/television/headless.go index da1d662a..ae898d7c 100644 --- a/television/headless.go +++ b/television/headless.go @@ -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) +} diff --git a/television/sdltv/callback.go b/television/sdltv/callback.go new file mode 100644 index 00000000..6a5a0a21 --- /dev/null +++ b/television/sdltv/callback.go @@ -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() + } +} diff --git a/television/sdltv/guiloop.go b/television/sdltv/guiloop.go index b1f0f73b..17fb8eef 100644 --- a/television/sdltv/guiloop.go +++ b/television/sdltv/guiloop.go @@ -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() } } diff --git a/television/sdltv/requests.go b/television/sdltv/requests.go new file mode 100644 index 00000000..1402f330 --- /dev/null +++ b/television/sdltv/requests.go @@ -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 +} diff --git a/television/sdltv/screen.go b/television/sdltv/screen.go index 4e64a38e..1163ea84 100644 --- a/television/sdltv/screen.go +++ b/television/sdltv/screen.go @@ -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 } } diff --git a/television/sdltv/sdltv.go b/television/sdltv/sdltv.go index ecdb4d7c..4077ee0f 100644 --- a/television/sdltv/sdltv.go +++ b/television/sdltv/sdltv.go @@ -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 diff --git a/television/sdltv/tvinterface.go b/television/sdltv/tvinterface.go deleted file mode 100644 index fcd9dbab..00000000 --- a/television/sdltv/tvinterface.go +++ /dev/null @@ -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) - } -} diff --git a/television/specifications.go b/television/specifications.go index 8cd286f8..fdace4dd 100644 --- a/television/specifications.go +++ b/television/specifications.go @@ -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 } diff --git a/television/television.go b/television/television.go index e7617b28..3af9032d 100644 --- a/television/television.go +++ b/television/television.go @@ -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 }