mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 20:18:20 -04:00
remove Television interface / renamed Reference to Television
the interface was becoming too large and was a remnant of a very early development phase. the gui interface type has replaced that concept.
This commit is contained in:
parent
6ad6d31a6c
commit
63da4073d9
|
@ -56,7 +56,7 @@ type Debugger struct {
|
|||
lastResult *disassembly.Entry
|
||||
|
||||
// gui, tv and terminal
|
||||
tv television.Television
|
||||
tv *television.Television
|
||||
scr gui.GUI
|
||||
term terminal.Terminal
|
||||
|
||||
|
@ -138,7 +138,7 @@ type Debugger struct {
|
|||
|
||||
// NewDebugger creates and initialises everything required for a new debugging
|
||||
// session. Use the Start() method to actually begin the session.
|
||||
func NewDebugger(tv television.Television, scr gui.GUI, term terminal.Terminal, useSavekey bool) (*Debugger, error) {
|
||||
func NewDebugger(tv *television.Television, scr gui.GUI, term terminal.Terminal, useSavekey bool) (*Debugger, error) {
|
||||
var err error
|
||||
|
||||
dbg := &Debugger{
|
||||
|
|
|
@ -27,81 +27,6 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
)
|
||||
|
||||
type mockTV struct{}
|
||||
|
||||
func (t *mockTV) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *mockTV) Snapshot() television.TelevisionState {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *mockTV) Plumb(_ television.TelevisionState) {
|
||||
}
|
||||
|
||||
func (t *mockTV) AddPixelRenderer(_ television.PixelRenderer) {
|
||||
}
|
||||
|
||||
func (t *mockTV) AddPixelRefresher(_ television.PixelRefresher) {
|
||||
}
|
||||
|
||||
func (t *mockTV) AddFrameTrigger(_ television.FrameTrigger) {
|
||||
}
|
||||
|
||||
func (t *mockTV) AddAudioMixer(_ television.AudioMixer) {
|
||||
}
|
||||
|
||||
func (t *mockTV) Reset() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *mockTV) End() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *mockTV) Signal(_ television.SignalAttributes) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *mockTV) IsStable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *mockTV) GetLastSignal() television.SignalAttributes {
|
||||
return television.SignalAttributes{}
|
||||
}
|
||||
|
||||
func (t *mockTV) GetState(_ television.StateReq) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (t *mockTV) SetSpec(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *mockTV) GetReqSpecID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *mockTV) GetSpec() television.Spec {
|
||||
return television.SpecNTSC
|
||||
}
|
||||
|
||||
func (t *mockTV) SetFPSCap(set bool) {
|
||||
}
|
||||
|
||||
func (t *mockTV) SetFPS(fps float32) {
|
||||
}
|
||||
|
||||
func (t *mockTV) GetReqFPS() float32 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (t *mockTV) GetActualFPS() float32 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
type mockGUI struct{}
|
||||
|
||||
func (g *mockGUI) ReqFeature(request gui.FeatureReq, args ...interface{}) error {
|
||||
|
@ -212,7 +137,12 @@ func (trm *mockTerm) testSequence() {
|
|||
func TestDebugger_withNonExistantInitScript(t *testing.T) {
|
||||
trm := newMockTerm(t)
|
||||
|
||||
dbg, err := debugger.NewDebugger(&mockTV{}, &mockGUI{}, trm, false)
|
||||
tv, err := television.NewTelevision("NTSC")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
dbg, err := debugger.NewDebugger(tv, &mockGUI{}, trm, false)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
@ -227,8 +157,12 @@ func TestDebugger_withNonExistantInitScript(t *testing.T) {
|
|||
|
||||
func TestDebugger(t *testing.T) {
|
||||
trm := newMockTerm(t)
|
||||
tv, err := television.NewTelevision("NTSC")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
dbg, err := debugger.NewDebugger(&mockTV{}, &mockGUI{}, trm, false)
|
||||
dbg, err := debugger.NewDebugger(tv, &mockGUI{}, trm, false)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ const (
|
|||
)
|
||||
|
||||
type limiter struct {
|
||||
tv television.Television
|
||||
tv *television.Television
|
||||
lmtr *time.Ticker
|
||||
throt throttleLevel
|
||||
reqFrames float32
|
||||
|
@ -70,7 +70,7 @@ type limiter struct {
|
|||
checkEvents func() error
|
||||
}
|
||||
|
||||
func newLimiter(tv television.Television, checkEvents func() error) *limiter {
|
||||
func newLimiter(tv *television.Television, checkEvents func() error) *limiter {
|
||||
tv.SetFPSCap(false)
|
||||
|
||||
lmtr := &limiter{
|
||||
|
|
|
@ -30,7 +30,7 @@ import (
|
|||
// Note that the use of SHA-1 is fine for this application because this is not
|
||||
// a cryptographic task.
|
||||
type Video struct {
|
||||
television.Television
|
||||
*television.Television
|
||||
spec television.Spec
|
||||
digest [sha1.Size]byte
|
||||
pixels []byte
|
||||
|
@ -42,7 +42,7 @@ const pixelDepth = 3
|
|||
// NewVideo initialises a new instance of DigestTV. For convenience, the
|
||||
// television argument can be nil, in which case an instance of
|
||||
// StellaTelevision will be created.
|
||||
func NewVideo(tv television.Television) (*Video, error) {
|
||||
func NewVideo(tv *television.Television) (*Video, error) {
|
||||
// set up digest tv
|
||||
dig := &Video{Television: tv}
|
||||
|
||||
|
|
|
@ -290,7 +290,7 @@ func play(md *modalflag.Modes, sync *mainSync) error {
|
|||
case 1:
|
||||
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
||||
|
||||
tv, err := television.NewReference(*spec)
|
||||
tv, err := television.NewTelevision(*spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -389,7 +389,7 @@ func debug(md *modalflag.Modes, sync *mainSync) error {
|
|||
return err
|
||||
}
|
||||
|
||||
tv, err := television.NewReference(*spec)
|
||||
tv, err := television.NewTelevision(*spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -569,7 +569,7 @@ func perform(md *modalflag.Modes, sync *mainSync) error {
|
|||
case 1:
|
||||
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
||||
|
||||
tv, err := television.NewReference(*spec)
|
||||
tv, err := television.NewTelevision(*spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
|
||||
// SdlDebug is a simple SDL implementation of the television.PixelRenderer interfac.
|
||||
type SdlDebug struct {
|
||||
television.Television
|
||||
*television.Television
|
||||
|
||||
// functions that need to be performed in the main thread should be queued
|
||||
// for service
|
||||
|
@ -98,7 +98,7 @@ const windowTitle = "Gopher2600"
|
|||
const windowTitleCaptured = "Gopher2600 [captured]"
|
||||
|
||||
// NewSdlDebug is the preferred method of initialisation for SdlDebug.
|
||||
func NewSdlDebug(tv television.Television, scale float32) (*SdlDebug, error) {
|
||||
func NewSdlDebug(tv *television.Television, scale float32) (*SdlDebug, error) {
|
||||
scr := &SdlDebug{
|
||||
Television: tv,
|
||||
service: make(chan func(), 1),
|
||||
|
|
|
@ -47,7 +47,7 @@ type SdlImgui struct {
|
|||
|
||||
// references to the emulation
|
||||
lz *lazyvalues.LazyValues
|
||||
tv television.Television
|
||||
tv *television.Television
|
||||
vcs *hardware.VCS
|
||||
|
||||
// terminal interface to the debugger
|
||||
|
@ -97,7 +97,7 @@ type SdlImgui struct {
|
|||
// NewSdlImgui is the preferred method of initialisation for type SdlImgui
|
||||
//
|
||||
// MUST ONLY be called from the #mainthread.
|
||||
func NewSdlImgui(tv television.Television, playmode bool) (*SdlImgui, error) {
|
||||
func NewSdlImgui(tv *television.Television, playmode bool) (*SdlImgui, error) {
|
||||
img := &SdlImgui{
|
||||
context: imgui.CreateContext(nil),
|
||||
io: imgui.CurrentIO(),
|
||||
|
|
181
hardware/television/protocol.go
Normal file
181
hardware/television/protocol.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package television
|
||||
|
||||
import "strings"
|
||||
|
||||
// TelevisionTIA exposes only the functions required by the TIA.
|
||||
type TelevisionTIA interface {
|
||||
Signal(SignalAttributes) error
|
||||
GetState(StateReq) (int, error)
|
||||
}
|
||||
|
||||
// TelevisionSprite exposes only the functions required by the video sprites.
|
||||
type TelevisionSprite interface {
|
||||
GetState(StateReq) (int, error)
|
||||
}
|
||||
|
||||
// TelevisionState is a deliberately opaque type returned by Snapshot() and
|
||||
// used by RestoreSnapshot() in the Television interface. The state itself can
|
||||
// consist of anything necessary to the Television implementation.
|
||||
type TelevisionState interface{}
|
||||
|
||||
// PixelRenderer implementations displays, or otherwise works with, visual
|
||||
// information from a television. For example digest.Video.
|
||||
//
|
||||
// PixelRenderer implementations often find it convenient to maintain a reference to
|
||||
// the parent Television implementation and maybe even embed the Television
|
||||
// interface. ie.
|
||||
//
|
||||
// type ExampleTV struct {
|
||||
// television.Television
|
||||
// ...
|
||||
// }
|
||||
type PixelRenderer interface {
|
||||
// Resize is called when the television implementation detects that extra
|
||||
// scanlines are required in the display.
|
||||
//
|
||||
// It may be called when television specification has changed. As a point
|
||||
// of convenience a reference to the currently selected specification is
|
||||
// provided. However, renderers should call GetSpec() rather than keeping a
|
||||
// private pointer to the specification, if knowledge of the spec is
|
||||
// required after the Resize() event.
|
||||
//
|
||||
// Renderers should use the values sent by the Resize() function, rather
|
||||
// than the equivalent values in the specification. Unless of course, the
|
||||
// renderer is intended to be strict about specification accuracy.
|
||||
//
|
||||
// Renderers should make sure that any data structures that depend on the
|
||||
// specification being used are still adequate.
|
||||
Resize(spec Spec, topScanline, visibleScanlines int) error
|
||||
|
||||
// NewFrame and NewScanline are called at the start of the frame/scanline
|
||||
NewFrame(frameNum int, isStable bool) error
|
||||
NewScanline(scanline int) error
|
||||
|
||||
// setPixel() is called every cycle regardless of the state of VBLANK and
|
||||
// HBLANK.
|
||||
//
|
||||
// things to consider:
|
||||
//
|
||||
// o the x argument is measured from zero so renderers should decide how to
|
||||
// handle pixels of during the HBLANK (x < ClocksPerHBLANK)
|
||||
//
|
||||
// o the y argument is also measured from zero but because VBLANK can be
|
||||
// turned on at any time there's no easy test. the VBLANK flag is sent to
|
||||
// help renderers decide what to do.
|
||||
//
|
||||
// o for renderers that are producing an accurate visual image, the pixel
|
||||
// should always be set to video black if VBLANK is on.
|
||||
//
|
||||
// some renderers however, may find it useful to set the pixel to the RGB
|
||||
// value regardless of VBLANK. for example, DigestTV does this.
|
||||
//
|
||||
// a vey important note is that some ROMs use VBLANK to control pixel
|
||||
// color within the visible display area. ROMs affected:
|
||||
//
|
||||
// * Custer's Revenge
|
||||
// * Ladybug
|
||||
// * ET (turns VBLANK off late on scanline 40)
|
||||
//
|
||||
// Set refreshing flag to true when called between Refresh(true) and
|
||||
// Refresh(false)
|
||||
SetPixel(x, y int, red, green, blue byte, vblank bool) error
|
||||
|
||||
// some renderers may need to conclude and/or dispose of resources gently.
|
||||
// for simplicity, the PixelRenderer should be considered unusable after
|
||||
// EndRendering() has been called
|
||||
EndRendering() error
|
||||
}
|
||||
|
||||
// PixelRefresher implementations are prepared to accept pixels outside of the
|
||||
// normal PixelRenderer sequence.
|
||||
type PixelRefresher interface {
|
||||
// Mark the start and end of a refresh event from the television. These
|
||||
// events occur when the television wants to dump many pixels at once.
|
||||
// Use SetPixel() with a refreshing flag of true between calls to
|
||||
// Refresh(true) and Refresh(false)
|
||||
Refresh(refreshing bool)
|
||||
|
||||
// RefreshPixel should only be called between two call of Refresh() as
|
||||
// described above
|
||||
RefreshPixel(x, y int, red, green, blue byte, vblank bool, stale bool) error
|
||||
}
|
||||
|
||||
// FrameTrigger implementations listen for NewFrame events. FrameTrigger is a
|
||||
// subset of PixelRenderer.
|
||||
type FrameTrigger interface {
|
||||
NewFrame(frameNum int, isStable bool) error
|
||||
}
|
||||
|
||||
// AudioMixer implementations work with sound; most probably playing it. An
|
||||
// example of an AudioMixer that does not play sound but otherwise works with
|
||||
// it is the digest.Audio type.
|
||||
type AudioMixer interface {
|
||||
SetAudio(audioData uint8) error
|
||||
|
||||
// some mixers may need to conclude and/or dispose of resources gently.
|
||||
// for simplicity, the AudioMixer should be considered unusable after
|
||||
// EndMixing() has been called
|
||||
EndMixing() error
|
||||
}
|
||||
|
||||
// ColorSignal represents the signal that is sent from the VCS to the.
|
||||
type ColorSignal int
|
||||
|
||||
// VideoBlack is the PixelSignal value that indicates no VCS pixel is to be shown.
|
||||
const VideoBlack ColorSignal = -1
|
||||
|
||||
// SignalAttributes represents the data sent to the television.
|
||||
type SignalAttributes struct {
|
||||
VSync bool
|
||||
VBlank bool
|
||||
CBurst bool
|
||||
HSync bool
|
||||
Pixel ColorSignal
|
||||
AudioData uint8
|
||||
|
||||
// which equates to 30Khz
|
||||
AudioUpdate bool
|
||||
}
|
||||
|
||||
func (a SignalAttributes) String() string {
|
||||
s := strings.Builder{}
|
||||
if a.VSync {
|
||||
s.WriteString("VSYNC ")
|
||||
}
|
||||
if a.VBlank {
|
||||
s.WriteString("VBLANK ")
|
||||
}
|
||||
if a.CBurst {
|
||||
s.WriteString("CBURST ")
|
||||
}
|
||||
if a.HSync {
|
||||
s.WriteString("HSYNC ")
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// StateReq is used to identify which television attribute is being asked
|
||||
// with the GetState() function.
|
||||
type StateReq int
|
||||
|
||||
// List of valid state requests.
|
||||
const (
|
||||
ReqFramenum StateReq = iota
|
||||
ReqScanline
|
||||
ReqHorizPos
|
||||
)
|
|
@ -1,511 +0,0 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ROMs used to test PAL switching and resizing:
|
||||
// * Pitfall
|
||||
// * Hero
|
||||
// * Chiphead
|
||||
// * Bang!
|
||||
// * Ladybug
|
||||
// * Hack Em Hangly Pacman
|
||||
// * Andrew Davies' Chess
|
||||
// * Communist Mutants From Space
|
||||
// * Mega Bitmap Demo
|
||||
|
||||
package television
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
)
|
||||
|
||||
// the number of additional lines over the NTSC spec that is allowed before the
|
||||
// TV flips to the PAL specification.
|
||||
const excessScanlinesNTSC = 40
|
||||
|
||||
// the number of synced frames where we can expect things to be in flux.
|
||||
const leadingFrames = 5
|
||||
|
||||
// the number of synced frames required before the tv frame is considered to "stable".
|
||||
const stabilityThreshold = 20
|
||||
|
||||
type state struct {
|
||||
// television specification (NTSC or PAL)
|
||||
spec Spec
|
||||
|
||||
// auto flag indicates that the tv type/specification should switch if it
|
||||
// appears to be outside of the current spec.
|
||||
//
|
||||
// in practice this means that if auto is true then we start with the NTSC
|
||||
// spec and move to PAL if the number of scanlines exceeds the NTSC maximum
|
||||
auto bool
|
||||
|
||||
// state of the television
|
||||
// - the current horizontal position. the position where the next pixel will be
|
||||
// drawn. also used to check we're receiving the correct signals at the
|
||||
// correct time.
|
||||
horizPos int
|
||||
// - the current frame
|
||||
frameNum int
|
||||
// - the current scanline number
|
||||
scanline int
|
||||
// - the current synced frame number. a synced frame is one which was
|
||||
// generated from a valid VSYNC/VBLANK sequence. we use this to detect:
|
||||
// * whether the image is "stable"
|
||||
// * whether specification changes should still occur
|
||||
syncedFrameNum int
|
||||
|
||||
// is current frame as a result of a VSYNC flyback or not (a "natural"
|
||||
// flyback). we use this in the context of newFrame() so we should probably
|
||||
// think of this as the previous frame.
|
||||
syncedFrame bool
|
||||
|
||||
// record of signal attributes from the last call to Signal()
|
||||
lastSignal SignalAttributes
|
||||
|
||||
// vsyncCount records the number of consecutive colorClocks the vsync signal
|
||||
// has been sustained. we use this to help correctly implement vsync.
|
||||
vsyncCount int
|
||||
|
||||
// top and bottom of screen as detected by vblank/color signal
|
||||
top int
|
||||
bottom int
|
||||
|
||||
// list of signals sent to pixel renderers since the beginning of the
|
||||
// current frame
|
||||
signalHistory []signalHistoryEntry
|
||||
|
||||
// the index to write the next signal
|
||||
signalHistoryIdx int
|
||||
}
|
||||
|
||||
type signalHistoryEntry struct {
|
||||
x int
|
||||
y int
|
||||
sig SignalAttributes
|
||||
}
|
||||
|
||||
// reference is a reference implementation of the Television interface. In all
|
||||
// honesty, it's most likely the only implementation required.
|
||||
type reference struct {
|
||||
// spec on creation ID is the string that was to ID the television
|
||||
// type/spec on creation. because the actual spec can change, the ID field
|
||||
// of the Spec type can not be used for things like regression
|
||||
// test recreation etc.
|
||||
reqSpecID string
|
||||
|
||||
// frame resizer
|
||||
resizer resizer
|
||||
|
||||
// framerate limiter
|
||||
lmtr limiter
|
||||
|
||||
// list of renderer implementations to consult
|
||||
renderers []PixelRenderer
|
||||
|
||||
// list of refresher implementations to consult
|
||||
refreshers []PixelRefresher
|
||||
|
||||
// list of frametrigger implementations to consult
|
||||
frameTriggers []FrameTrigger
|
||||
|
||||
// list of audio mixers to consult
|
||||
mixers []AudioMixer
|
||||
|
||||
state *state
|
||||
}
|
||||
|
||||
// NewReference creates a new instance of the reference television type,
|
||||
// satisfying the Television interface.
|
||||
func NewReference(spec string) (Television, error) {
|
||||
tv := &reference{
|
||||
resizer: &simpleResizer{},
|
||||
reqSpecID: strings.ToUpper(spec),
|
||||
state: &state{},
|
||||
}
|
||||
|
||||
// set specification
|
||||
err := tv.SetSpec(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initialise frame rate limiter
|
||||
tv.lmtr.init()
|
||||
tv.SetFPS(-1)
|
||||
|
||||
// empty list of renderers
|
||||
tv.renderers = make([]PixelRenderer, 0)
|
||||
|
||||
return tv, nil
|
||||
}
|
||||
|
||||
func (tv reference) String() string {
|
||||
s := strings.Builder{}
|
||||
s.WriteString(fmt.Sprintf("FR=%04d SL=%03d HP=%03d", tv.state.frameNum, tv.state.scanline, tv.state.horizPos-HorizClksHBlank))
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// Snapshot implements the Television interface.
|
||||
func (tv *reference) Snapshot() TelevisionState {
|
||||
n := *tv.state
|
||||
n.signalHistory = make([]signalHistoryEntry, len(tv.state.signalHistory))
|
||||
copy(n.signalHistory, tv.state.signalHistory)
|
||||
return &n
|
||||
}
|
||||
|
||||
// Plumb implements the Television interface.
|
||||
func (tv *reference) Plumb(s TelevisionState) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tv.state = s.(*state)
|
||||
|
||||
for _, r := range tv.refreshers {
|
||||
r.Refresh(true)
|
||||
}
|
||||
|
||||
for i, e := range tv.state.signalHistory {
|
||||
col := tv.state.spec.getColor(e.sig.Pixel)
|
||||
for _, r := range tv.refreshers {
|
||||
r.RefreshPixel(e.x, e.y, col.R, col.G, col.B, e.sig.VBlank, i >= tv.state.signalHistoryIdx)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range tv.refreshers {
|
||||
r.Refresh(false)
|
||||
}
|
||||
}
|
||||
|
||||
// AddPixelRenderer implements the Television interface.
|
||||
func (tv *reference) AddPixelRenderer(r PixelRenderer) {
|
||||
tv.renderers = append(tv.renderers, r)
|
||||
tv.frameTriggers = append(tv.frameTriggers, r)
|
||||
}
|
||||
|
||||
// AddPixelRefresher implements the Television interface.
|
||||
func (tv *reference) AddPixelRefresher(r PixelRefresher) {
|
||||
tv.refreshers = append(tv.refreshers, r)
|
||||
}
|
||||
|
||||
// AddFrameTrigger implements the Television interface.
|
||||
func (tv *reference) AddFrameTrigger(f FrameTrigger) {
|
||||
tv.frameTriggers = append(tv.frameTriggers, f)
|
||||
}
|
||||
|
||||
// AddAudioMixer implements the Television interface.
|
||||
func (tv *reference) AddAudioMixer(m AudioMixer) {
|
||||
tv.mixers = append(tv.mixers, m)
|
||||
}
|
||||
|
||||
// Reset implements the Television interface.
|
||||
func (tv *reference) Reset() error {
|
||||
// we definitely do not call this on television initialisation because the
|
||||
// rest of the system may not be yet be in a suitable state
|
||||
|
||||
err := tv.SetSpec(tv.reqSpecID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tv.state.horizPos = 0
|
||||
tv.state.frameNum = 0
|
||||
tv.state.scanline = 0
|
||||
tv.state.syncedFrameNum = 0
|
||||
tv.state.vsyncCount = 0
|
||||
tv.state.lastSignal = SignalAttributes{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// End implements the Television interface.
|
||||
func (tv reference) End() error {
|
||||
var err error
|
||||
|
||||
// call new frame for all renderers
|
||||
for f := range tv.renderers {
|
||||
err = tv.renderers[f].EndRendering()
|
||||
}
|
||||
|
||||
// flush audio for all mixers
|
||||
for f := range tv.mixers {
|
||||
err = tv.mixers[f].EndMixing()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Signal implements the Television interface.
|
||||
func (tv *reference) Signal(sig SignalAttributes) error {
|
||||
// mix audio before we do anything else
|
||||
if sig.AudioUpdate {
|
||||
for f := range tv.mixers {
|
||||
err := tv.mixers[f].SetAudio(sig.AudioData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// examine signal for resizing possibility
|
||||
tv.resizer.examine(tv, sig)
|
||||
|
||||
// a Signal() is by definition a new color clock. increase the horizontal count
|
||||
tv.state.horizPos++
|
||||
|
||||
// once we reach the scanline's back-porch we'll reset the horizPos counter
|
||||
// and wait for the HSYNC signal. we do this so that the front-porch and
|
||||
// back-porch are 'together' at the beginning of the scanline. this isn't
|
||||
// strictly technically correct but it's convenient to think about
|
||||
// scanlines in this way (rather than having a split front and back porch)
|
||||
if tv.state.horizPos >= HorizClksScanline {
|
||||
tv.state.horizPos = 0
|
||||
|
||||
// bump scanline counter
|
||||
tv.state.scanline++
|
||||
|
||||
// reached end of screen without synchronisation. fly-back naturally.
|
||||
if tv.state.scanline > tv.state.spec.ScanlinesTotal {
|
||||
err := tv.newFrame(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// if we're not at end of screen then indicate new scanline
|
||||
err := tv.newScanline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// checkRate evey scanline. see checkRate() commentary for why this is
|
||||
tv.lmtr.checkRate()
|
||||
}
|
||||
|
||||
// check vsync signal at the time of the flyback
|
||||
//
|
||||
// !!TODO: replace VSYNC signal with extended HSYNC signal
|
||||
if sig.VSync && !tv.state.lastSignal.VSync {
|
||||
tv.state.vsyncCount = 0
|
||||
} else if !sig.VSync && tv.state.lastSignal.VSync {
|
||||
if tv.state.vsyncCount > 0 {
|
||||
err := tv.newFrame(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we've "faked" the flyback signal above when horizPos reached
|
||||
// horizClksScanline. we need to handle the real flyback signal however, by
|
||||
// making sure we're at the correct horizPos value. if horizPos doesn't
|
||||
// equal 16 at the front of the HSYNC or 36 at then back of the HSYNC, then
|
||||
// it indicates that the RSYNC register was used last scanline.
|
||||
if sig.HSync && !tv.state.lastSignal.HSync {
|
||||
tv.state.horizPos = 16
|
||||
|
||||
// count vsync lines at start of hsync
|
||||
if sig.VSync || tv.state.lastSignal.VSync {
|
||||
tv.state.vsyncCount++
|
||||
}
|
||||
}
|
||||
if !sig.HSync && tv.state.lastSignal.HSync {
|
||||
tv.state.horizPos = 36
|
||||
}
|
||||
|
||||
// doing nothing with CBURST signal
|
||||
|
||||
// decode color using the regular color signal
|
||||
col := tv.state.spec.getColor(sig.Pixel)
|
||||
for f := range tv.renderers {
|
||||
err := tv.renderers[f].SetPixel(tv.state.horizPos, tv.state.scanline,
|
||||
col.R, col.G, col.B, sig.VBlank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// record the current signal settings so they can be used for reference
|
||||
tv.state.lastSignal = sig
|
||||
|
||||
e := signalHistoryEntry{
|
||||
x: tv.state.horizPos,
|
||||
y: tv.state.scanline,
|
||||
sig: sig,
|
||||
}
|
||||
|
||||
if tv.state.signalHistoryIdx >= len(tv.state.signalHistory) {
|
||||
tv.state.signalHistory = append(tv.state.signalHistory, e)
|
||||
} else {
|
||||
tv.state.signalHistory[tv.state.signalHistoryIdx] = e
|
||||
}
|
||||
tv.state.signalHistoryIdx++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tv *reference) newScanline() error {
|
||||
// notify renderers of new scanline
|
||||
for f := range tv.renderers {
|
||||
err := tv.renderers[f].NewScanline(tv.state.scanline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tv *reference) newFrame(synced bool) error {
|
||||
// a synced frame is one which was generated from a valid VSYNC/VBLANK sequence
|
||||
if tv.state.syncedFrame {
|
||||
tv.state.syncedFrameNum++
|
||||
}
|
||||
|
||||
// specification change
|
||||
if tv.state.syncedFrameNum > leadingFrames && tv.state.syncedFrameNum < stabilityThreshold {
|
||||
if tv.state.auto && !tv.state.syncedFrame && tv.state.scanline > excessScanlinesNTSC {
|
||||
// flip from NTSC to PAL
|
||||
if tv.state.spec.ID == SpecNTSC.ID {
|
||||
_ = tv.SetSpec("PAL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// commit any resizing that maybe pending
|
||||
err := tv.resizer.commit(tv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare for next frame
|
||||
tv.state.frameNum++
|
||||
tv.state.scanline = 0
|
||||
tv.resizer.prepare(tv)
|
||||
tv.state.syncedFrame = synced
|
||||
|
||||
// process all FrameTriggers
|
||||
for f := range tv.frameTriggers {
|
||||
err = tv.frameTriggers[f].NewFrame(tv.state.frameNum, tv.IsStable())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// reset signal history for next frame
|
||||
tv.state.signalHistoryIdx = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsStable implements the Television interface.
|
||||
func (tv reference) IsStable() bool {
|
||||
return tv.state.syncedFrameNum >= stabilityThreshold
|
||||
}
|
||||
|
||||
// GetLastSignal implements the Television interface.
|
||||
func (tv *reference) GetLastSignal() SignalAttributes {
|
||||
return tv.state.lastSignal
|
||||
}
|
||||
|
||||
// GetState implements the Television interface.
|
||||
func (tv *reference) GetState(request StateReq) (int, error) {
|
||||
switch request {
|
||||
case ReqFramenum:
|
||||
return tv.state.frameNum, nil
|
||||
case ReqScanline:
|
||||
return tv.state.scanline, nil
|
||||
case ReqHorizPos:
|
||||
return tv.state.horizPos - HorizClksHBlank, nil
|
||||
default:
|
||||
return 0, curated.Errorf("television: unhandled tv state request (%v)", request)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSpec implements the Television interface.
|
||||
func (tv *reference) SetSpec(spec string) error {
|
||||
switch strings.ToUpper(spec) {
|
||||
case "NTSC":
|
||||
tv.state.spec = SpecNTSC
|
||||
tv.state.auto = false
|
||||
case "PAL":
|
||||
tv.state.spec = SpecPAL
|
||||
tv.state.auto = false
|
||||
case "AUTO":
|
||||
tv.state.spec = SpecNTSC
|
||||
tv.state.auto = true
|
||||
default:
|
||||
return curated.Errorf("television: unsupported spec (%s)", spec)
|
||||
}
|
||||
|
||||
tv.state.top = tv.state.spec.ScanlineTop
|
||||
tv.state.bottom = tv.state.spec.ScanlineBottom
|
||||
tv.resizer.prepare(tv)
|
||||
|
||||
for f := range tv.renderers {
|
||||
err := tv.renderers[f].Resize(tv.state.spec, tv.state.top, tv.state.bottom-tv.state.top)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// allocate enough memory for a TV screen that stays within the limits of
|
||||
// the specification
|
||||
tv.state.signalHistory = make([]signalHistoryEntry, HorizClksScanline*(tv.state.bottom-tv.state.top))
|
||||
tv.state.signalHistoryIdx = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetReqSpecID implements the Television interface.
|
||||
func (tv *reference) GetReqSpecID() string {
|
||||
return tv.reqSpecID
|
||||
}
|
||||
|
||||
// GetSpec implements the Television interface.
|
||||
func (tv reference) GetSpec() Spec {
|
||||
return tv.state.spec
|
||||
}
|
||||
|
||||
// SetFPSCap implements the Television interface. Reasons for turning the cap
|
||||
// off include performance measurement. The debugger also turns the cap off and
|
||||
// replaces it with its own. The FPS limiter in this television implementation
|
||||
// works at the frame level which is not fine grained enough for effective
|
||||
// limiting of rates less than 1fps.
|
||||
func (tv *reference) SetFPSCap(limit bool) {
|
||||
tv.lmtr.limit = limit
|
||||
}
|
||||
|
||||
// SetFPS implements the Television interface. A negative value resets the FPS
|
||||
// to the specification's ideal value.
|
||||
func (tv *reference) SetFPS(fps float32) {
|
||||
if fps == -1 {
|
||||
fps = tv.state.spec.FramesPerSecond
|
||||
}
|
||||
tv.lmtr.setRate(fps, tv.state.spec.ScanlinesTotal)
|
||||
}
|
||||
|
||||
// GetReqFPS implements the Television interface.
|
||||
func (tv *reference) GetReqFPS() float32 {
|
||||
return tv.lmtr.requested
|
||||
}
|
||||
|
||||
// GetActualFPS implements the Television interface. Note that FPS measurement
|
||||
// still works even when frame capping is disabled.
|
||||
func (tv *reference) GetActualFPS() float32 {
|
||||
return tv.lmtr.actual
|
||||
}
|
|
@ -33,13 +33,13 @@ type resizer interface {
|
|||
id() FrameResizeID
|
||||
|
||||
// examine signal for resizing possibility. called on every Signal()
|
||||
examine(tv *reference, sig SignalAttributes)
|
||||
examine(tv *Television, sig SignalAttributes)
|
||||
|
||||
// commit resizing possiblity. called on every newFrame()
|
||||
commit(tv *reference) error
|
||||
commit(tv *Television) error
|
||||
|
||||
// preapare for next frame
|
||||
prepare(tv *reference)
|
||||
prepare(tv *Television)
|
||||
}
|
||||
|
||||
// simpleResizer is the simplest functional and non-trivial implementation of
|
||||
|
@ -52,7 +52,7 @@ func (sr simpleResizer) id() FrameResizeID {
|
|||
return FrameResizerSimple
|
||||
}
|
||||
|
||||
func (sr *simpleResizer) examine(tv *reference, sig SignalAttributes) {
|
||||
func (sr *simpleResizer) examine(tv *Television, sig SignalAttributes) {
|
||||
// if vblank is off at any point of then extend the bottom of the screen.
|
||||
// we'll commit the resize procedure in the newFrame() function
|
||||
//
|
||||
|
@ -74,7 +74,7 @@ func (sr *simpleResizer) examine(tv *reference, sig SignalAttributes) {
|
|||
}
|
||||
}
|
||||
|
||||
func (sr *simpleResizer) commit(tv *reference) error {
|
||||
func (sr *simpleResizer) commit(tv *Television) error {
|
||||
// always perform resize operation
|
||||
if tv.state.syncedFrameNum <= leadingFrames || sr.bottom == tv.state.bottom {
|
||||
return nil
|
||||
|
@ -104,6 +104,6 @@ func (sr *simpleResizer) commit(tv *reference) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (sr *simpleResizer) prepare(tv *reference) {
|
||||
func (sr *simpleResizer) prepare(tv *Television) {
|
||||
sr.bottom = tv.state.bottom
|
||||
}
|
||||
|
|
|
@ -13,252 +13,513 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ROMs used to test PAL switching and resizing:
|
||||
// * Pitfall
|
||||
// * Hero
|
||||
// * Chiphead
|
||||
// * Bang!
|
||||
// * Ladybug
|
||||
// * Hack Em Hangly Pacman
|
||||
// * Andrew Davies' Chess
|
||||
// * Communist Mutants From Space
|
||||
// * Mega Bitmap Demo
|
||||
|
||||
package television
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
// Television defines the operations that can be performed on the conceptual
|
||||
// television. Note that the television implementation itself does not present
|
||||
// any information, either visually or sonically. Instead, PixelRenderers and
|
||||
// AudioMixers are added to perform those tasks.
|
||||
type Television interface {
|
||||
String() string
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
)
|
||||
|
||||
// Make a copy of the television state
|
||||
Snapshot() TelevisionState
|
||||
// the number of additional lines over the NTSC spec that is allowed before the
|
||||
// TV flips to the PAL specification.
|
||||
const excessScanlinesNTSC = 40
|
||||
|
||||
// Copy back television state
|
||||
Plumb(TelevisionState)
|
||||
// the number of synced frames where we can expect things to be in flux.
|
||||
const leadingFrames = 5
|
||||
|
||||
// AddPixelRenderer registers an implementation of PixelRenderer. Multiple
|
||||
// implemntations can be added
|
||||
AddPixelRenderer(PixelRenderer)
|
||||
// the number of synced frames required before the tv frame is considered to "stable".
|
||||
const stabilityThreshold = 20
|
||||
|
||||
// AddPixelRefresh registers an implementation of PixelRefresher. Multiple
|
||||
// implemntations can be added
|
||||
AddPixelRefresher(PixelRefresher)
|
||||
type state struct {
|
||||
// television specification (NTSC or PAL)
|
||||
spec Spec
|
||||
|
||||
// AddFrameTrigger registers an implementation of FrameTrigger. Multiple
|
||||
// implemntations can be added
|
||||
AddFrameTrigger(FrameTrigger)
|
||||
|
||||
// AddAudioMixer registers an implementation of AudioMixer. Multiple
|
||||
// implemntations can be added
|
||||
AddAudioMixer(AudioMixer)
|
||||
|
||||
// Reset the television to an initial state
|
||||
Reset() error
|
||||
|
||||
// some televisions may need to conclude and/or dispose of resources
|
||||
// gently. implementations of End() should call EndRendering() and
|
||||
// EndMixing() on each PixelRenderer and AudioMixer that has been added.
|
||||
// auto flag indicates that the tv type/specification should switch if it
|
||||
// appears to be outside of the current spec.
|
||||
//
|
||||
// for simplicity, the Television should be considered unusable
|
||||
// after EndRendering() has been called
|
||||
End() error
|
||||
// in practice this means that if auto is true then we start with the NTSC
|
||||
// spec and move to PAL if the number of scanlines exceeds the NTSC maximum
|
||||
auto bool
|
||||
|
||||
Signal(SignalAttributes) error
|
||||
// state of the television
|
||||
// - the current horizontal position. the position where the next pixel will be
|
||||
// drawn. also used to check we're receiving the correct signals at the
|
||||
// correct time.
|
||||
horizPos int
|
||||
// - the current frame
|
||||
frameNum int
|
||||
// - the current scanline number
|
||||
scanline int
|
||||
// - the current synced frame number. a synced frame is one which was
|
||||
// generated from a valid VSYNC/VBLANK sequence. we use this to detect:
|
||||
// * whether the image is "stable"
|
||||
// * whether specification changes should still occur
|
||||
syncedFrameNum int
|
||||
|
||||
// \/\/ reflection/information \/\/
|
||||
// is current frame as a result of a VSYNC flyback or not (a "natural"
|
||||
// flyback). we use this in the context of newFrame() so we should probably
|
||||
// think of this as the previous frame.
|
||||
syncedFrame bool
|
||||
|
||||
// IsStable returns true if the television thinks the image being sent by
|
||||
// the VCS is stable
|
||||
IsStable() bool
|
||||
// record of signal attributes from the last call to Signal()
|
||||
lastSignal SignalAttributes
|
||||
|
||||
// Returns a copy of SignalAttributes for reference
|
||||
GetLastSignal() SignalAttributes
|
||||
// vsyncCount records the number of consecutive colorClocks the vsync signal
|
||||
// has been sustained. we use this to help correctly implement vsync.
|
||||
vsyncCount int
|
||||
|
||||
// Returns state information
|
||||
GetState(StateReq) (int, error)
|
||||
// top and bottom of screen as detected by vblank/color signal
|
||||
top int
|
||||
bottom int
|
||||
|
||||
// \/\/ specfication \/\/
|
||||
//
|
||||
// Set the television's specification
|
||||
SetSpec(spec string) error
|
||||
// list of signals sent to pixel renderers since the beginning of the
|
||||
// current frame
|
||||
signalHistory []signalHistoryEntry
|
||||
|
||||
// GetReqSpecID returns the specification that was requested on creation
|
||||
GetReqSpecID() string
|
||||
|
||||
// Returns the television's current specification. Renderers should use
|
||||
// GetSpec() rather than keeping a private pointer to the specification.
|
||||
GetSpec() Spec
|
||||
|
||||
// \/\/ FPS \/\/
|
||||
|
||||
// Set whether the emulation should wait for FPS limiter
|
||||
SetFPSCap(set bool)
|
||||
|
||||
// Request the number frames per second. This overrides the frame rate of
|
||||
// the specification. A negative value restores the spec's frame rate.
|
||||
SetFPS(fps float32)
|
||||
|
||||
// The requested number of frames per second. Compare with GetActualFPS()
|
||||
// to check for accuracy
|
||||
GetReqFPS() float32
|
||||
|
||||
// The current number of frames per second
|
||||
GetActualFPS() float32
|
||||
// the index to write the next signal
|
||||
signalHistoryIdx int
|
||||
}
|
||||
|
||||
// TelevisionTIA exposes only the functions required by the TIA.
|
||||
type TelevisionTIA interface {
|
||||
Signal(SignalAttributes) error
|
||||
GetState(StateReq) (int, error)
|
||||
type signalHistoryEntry struct {
|
||||
x int
|
||||
y int
|
||||
sig SignalAttributes
|
||||
}
|
||||
|
||||
// TelevisionSprite exposes only the functions required by the video sprites.
|
||||
type TelevisionSprite interface {
|
||||
GetState(StateReq) (int, error)
|
||||
// Television is a Television implementation of the Television interface. In all
|
||||
// honesty, it's most likely the only implementation required.
|
||||
type Television struct {
|
||||
// spec on creation ID is the string that was to ID the television
|
||||
// type/spec on creation. because the actual spec can change, the ID field
|
||||
// of the Spec type can not be used for things like regression
|
||||
// test recreation etc.
|
||||
reqSpecID string
|
||||
|
||||
// frame resizer
|
||||
resizer resizer
|
||||
|
||||
// framerate limiter
|
||||
lmtr limiter
|
||||
|
||||
// list of renderer implementations to consult
|
||||
renderers []PixelRenderer
|
||||
|
||||
// list of refresher implementations to consult
|
||||
refreshers []PixelRefresher
|
||||
|
||||
// list of frametrigger implementations to consult
|
||||
frameTriggers []FrameTrigger
|
||||
|
||||
// list of audio mixers to consult
|
||||
mixers []AudioMixer
|
||||
|
||||
state *state
|
||||
}
|
||||
|
||||
// TelevisionState is a deliberately opaque type returned by Snapshot() and
|
||||
// used by RestoreSnapshot() in the Television interface. The state itself can
|
||||
// consist of anything necessary to the Television implementation.
|
||||
type TelevisionState interface{}
|
||||
// NewReference creates a new instance of the reference television type,
|
||||
// satisfying the Television interface.
|
||||
func NewTelevision(spec string) (*Television, error) {
|
||||
tv := &Television{
|
||||
resizer: &simpleResizer{},
|
||||
reqSpecID: strings.ToUpper(spec),
|
||||
state: &state{},
|
||||
}
|
||||
|
||||
// PixelRenderer implementations displays, or otherwise works with, visual
|
||||
// information from a television. For example digest.Video.
|
||||
//
|
||||
// PixelRenderer implementations often find it convenient to maintain a reference to
|
||||
// the parent Television implementation and maybe even embed the Television
|
||||
// interface. ie.
|
||||
//
|
||||
// type ExampleTV struct {
|
||||
// television.Television
|
||||
// ...
|
||||
// }
|
||||
type PixelRenderer interface {
|
||||
// Resize is called when the television implementation detects that extra
|
||||
// scanlines are required in the display.
|
||||
//
|
||||
// It may be called when television specification has changed. As a point
|
||||
// of convenience a reference to the currently selected specification is
|
||||
// provided. However, renderers should call GetSpec() rather than keeping a
|
||||
// private pointer to the specification, if knowledge of the spec is
|
||||
// required after the Resize() event.
|
||||
//
|
||||
// Renderers should use the values sent by the Resize() function, rather
|
||||
// than the equivalent values in the specification. Unless of course, the
|
||||
// renderer is intended to be strict about specification accuracy.
|
||||
//
|
||||
// Renderers should make sure that any data structures that depend on the
|
||||
// specification being used are still adequate.
|
||||
Resize(spec Spec, topScanline, visibleScanlines int) error
|
||||
// set specification
|
||||
err := tv.SetSpec(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NewFrame and NewScanline are called at the start of the frame/scanline
|
||||
NewFrame(frameNum int, isStable bool) error
|
||||
NewScanline(scanline int) error
|
||||
// initialise frame rate limiter
|
||||
tv.lmtr.init()
|
||||
tv.SetFPS(-1)
|
||||
|
||||
// setPixel() is called every cycle regardless of the state of VBLANK and
|
||||
// HBLANK.
|
||||
//
|
||||
// things to consider:
|
||||
//
|
||||
// o the x argument is measured from zero so renderers should decide how to
|
||||
// handle pixels of during the HBLANK (x < ClocksPerHBLANK)
|
||||
//
|
||||
// o the y argument is also measured from zero but because VBLANK can be
|
||||
// turned on at any time there's no easy test. the VBLANK flag is sent to
|
||||
// help renderers decide what to do.
|
||||
//
|
||||
// o for renderers that are producing an accurate visual image, the pixel
|
||||
// should always be set to video black if VBLANK is on.
|
||||
//
|
||||
// some renderers however, may find it useful to set the pixel to the RGB
|
||||
// value regardless of VBLANK. for example, DigestTV does this.
|
||||
//
|
||||
// a vey important note is that some ROMs use VBLANK to control pixel
|
||||
// color within the visible display area. ROMs affected:
|
||||
//
|
||||
// * Custer's Revenge
|
||||
// * Ladybug
|
||||
// * ET (turns VBLANK off late on scanline 40)
|
||||
//
|
||||
// Set refreshing flag to true when called between Refresh(true) and
|
||||
// Refresh(false)
|
||||
SetPixel(x, y int, red, green, blue byte, vblank bool) error
|
||||
// empty list of renderers
|
||||
tv.renderers = make([]PixelRenderer, 0)
|
||||
|
||||
// some renderers may need to conclude and/or dispose of resources gently.
|
||||
// for simplicity, the PixelRenderer should be considered unusable after
|
||||
// EndRendering() has been called
|
||||
EndRendering() error
|
||||
return tv, nil
|
||||
}
|
||||
|
||||
// PixelRefresher implementations are prepared to accept pixels outside of the
|
||||
// normal PixelRenderer sequence.
|
||||
type PixelRefresher interface {
|
||||
// Mark the start and end of a refresh event from the television. These
|
||||
// events occur when the television wants to dump many pixels at once.
|
||||
// Use SetPixel() with a refreshing flag of true between calls to
|
||||
// Refresh(true) and Refresh(false)
|
||||
Refresh(refreshing bool)
|
||||
|
||||
// RefreshPixel should only be called between two call of Refresh() as
|
||||
// described above
|
||||
RefreshPixel(x, y int, red, green, blue byte, vblank bool, stale bool) error
|
||||
}
|
||||
|
||||
// FrameTrigger implementations listen for NewFrame events. FrameTrigger is a
|
||||
// subset of PixelRenderer.
|
||||
type FrameTrigger interface {
|
||||
NewFrame(frameNum int, isStable bool) error
|
||||
}
|
||||
|
||||
// AudioMixer implementations work with sound; most probably playing it. An
|
||||
// example of an AudioMixer that does not play sound but otherwise works with
|
||||
// it is the digest.Audio type.
|
||||
type AudioMixer interface {
|
||||
SetAudio(audioData uint8) error
|
||||
|
||||
// some mixers may need to conclude and/or dispose of resources gently.
|
||||
// for simplicity, the AudioMixer should be considered unusable after
|
||||
// EndMixing() has been called
|
||||
EndMixing() error
|
||||
}
|
||||
|
||||
// ColorSignal represents the signal that is sent from the VCS to the.
|
||||
type ColorSignal int
|
||||
|
||||
// VideoBlack is the PixelSignal value that indicates no VCS pixel is to be shown.
|
||||
const VideoBlack ColorSignal = -1
|
||||
|
||||
// SignalAttributes represents the data sent to the television.
|
||||
type SignalAttributes struct {
|
||||
VSync bool
|
||||
VBlank bool
|
||||
CBurst bool
|
||||
HSync bool
|
||||
Pixel ColorSignal
|
||||
AudioData uint8
|
||||
|
||||
// which equates to 30Khz
|
||||
AudioUpdate bool
|
||||
}
|
||||
|
||||
func (a SignalAttributes) String() string {
|
||||
func (tv Television) String() string {
|
||||
s := strings.Builder{}
|
||||
if a.VSync {
|
||||
s.WriteString("VSYNC ")
|
||||
}
|
||||
if a.VBlank {
|
||||
s.WriteString("VBLANK ")
|
||||
}
|
||||
if a.CBurst {
|
||||
s.WriteString("CBURST ")
|
||||
}
|
||||
if a.HSync {
|
||||
s.WriteString("HSYNC ")
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("FR=%04d SL=%03d HP=%03d", tv.state.frameNum, tv.state.scanline, tv.state.horizPos-HorizClksHBlank))
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// StateReq is used to identify which television attribute is being asked
|
||||
// with the GetState() function.
|
||||
type StateReq int
|
||||
// Snapshot makes a copy of the television state.
|
||||
func (tv *Television) Snapshot() TelevisionState {
|
||||
n := *tv.state
|
||||
n.signalHistory = make([]signalHistoryEntry, len(tv.state.signalHistory))
|
||||
copy(n.signalHistory, tv.state.signalHistory)
|
||||
return &n
|
||||
}
|
||||
|
||||
// List of valid state requests.
|
||||
const (
|
||||
ReqFramenum StateReq = iota
|
||||
ReqScanline
|
||||
ReqHorizPos
|
||||
)
|
||||
// Plumb in an existing television state.
|
||||
func (tv *Television) Plumb(s TelevisionState) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tv.state = s.(*state)
|
||||
|
||||
for _, r := range tv.refreshers {
|
||||
r.Refresh(true)
|
||||
}
|
||||
|
||||
for i, e := range tv.state.signalHistory {
|
||||
col := tv.state.spec.getColor(e.sig.Pixel)
|
||||
for _, r := range tv.refreshers {
|
||||
r.RefreshPixel(e.x, e.y, col.R, col.G, col.B, e.sig.VBlank, i >= tv.state.signalHistoryIdx)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range tv.refreshers {
|
||||
r.Refresh(false)
|
||||
}
|
||||
}
|
||||
|
||||
// AddPixelRenderer registers an implementation of PixelRenderer. Multiple
|
||||
// implemntations can be added.
|
||||
func (tv *Television) AddPixelRenderer(r PixelRenderer) {
|
||||
tv.renderers = append(tv.renderers, r)
|
||||
tv.frameTriggers = append(tv.frameTriggers, r)
|
||||
}
|
||||
|
||||
// AddPixelRefresh registers an implementation of PixelRefresher. Multiple
|
||||
// implemntations can be added.
|
||||
func (tv *Television) AddPixelRefresher(r PixelRefresher) {
|
||||
tv.refreshers = append(tv.refreshers, r)
|
||||
}
|
||||
|
||||
// AddFrameTrigger registers an implementation of FrameTrigger. Multiple
|
||||
// implemntations can be added.
|
||||
func (tv *Television) AddFrameTrigger(f FrameTrigger) {
|
||||
tv.frameTriggers = append(tv.frameTriggers, f)
|
||||
}
|
||||
|
||||
// AddAudioMixer registers an implementation of AudioMixer. Multiple
|
||||
// implemntations can be added.
|
||||
func (tv *Television) AddAudioMixer(m AudioMixer) {
|
||||
tv.mixers = append(tv.mixers, m)
|
||||
}
|
||||
|
||||
// Reset the television to an initial state.
|
||||
func (tv *Television) Reset() error {
|
||||
// we definitely do not call this on television initialisation because the
|
||||
// rest of the system may not be yet be in a suitable state
|
||||
|
||||
err := tv.SetSpec(tv.reqSpecID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tv.state.horizPos = 0
|
||||
tv.state.frameNum = 0
|
||||
tv.state.scanline = 0
|
||||
tv.state.syncedFrameNum = 0
|
||||
tv.state.vsyncCount = 0
|
||||
tv.state.lastSignal = SignalAttributes{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// some televisions may need to conclude and/or dispose of resources
|
||||
// gently. implementations of End() should call EndRendering() and
|
||||
// EndMixing() on each PixelRenderer and AudioMixer that has been added.
|
||||
//
|
||||
// for simplicity, the Television should be considered unusable
|
||||
// after EndRendering() has been called.
|
||||
func (tv Television) End() error {
|
||||
var err error
|
||||
|
||||
// call new frame for all renderers
|
||||
for f := range tv.renderers {
|
||||
err = tv.renderers[f].EndRendering()
|
||||
}
|
||||
|
||||
// flush audio for all mixers
|
||||
for f := range tv.mixers {
|
||||
err = tv.mixers[f].EndMixing()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Signal updates the current state of the television.
|
||||
func (tv *Television) Signal(sig SignalAttributes) error {
|
||||
// mix audio before we do anything else
|
||||
if sig.AudioUpdate {
|
||||
for f := range tv.mixers {
|
||||
err := tv.mixers[f].SetAudio(sig.AudioData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// examine signal for resizing possibility
|
||||
tv.resizer.examine(tv, sig)
|
||||
|
||||
// a Signal() is by definition a new color clock. increase the horizontal count
|
||||
tv.state.horizPos++
|
||||
|
||||
// once we reach the scanline's back-porch we'll reset the horizPos counter
|
||||
// and wait for the HSYNC signal. we do this so that the front-porch and
|
||||
// back-porch are 'together' at the beginning of the scanline. this isn't
|
||||
// strictly technically correct but it's convenient to think about
|
||||
// scanlines in this way (rather than having a split front and back porch)
|
||||
if tv.state.horizPos >= HorizClksScanline {
|
||||
tv.state.horizPos = 0
|
||||
|
||||
// bump scanline counter
|
||||
tv.state.scanline++
|
||||
|
||||
// reached end of screen without synchronisation. fly-back naturally.
|
||||
if tv.state.scanline > tv.state.spec.ScanlinesTotal {
|
||||
err := tv.newFrame(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// if we're not at end of screen then indicate new scanline
|
||||
err := tv.newScanline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// checkRate evey scanline. see checkRate() commentary for why this is
|
||||
tv.lmtr.checkRate()
|
||||
}
|
||||
|
||||
// check vsync signal at the time of the flyback
|
||||
//
|
||||
// !!TODO: replace VSYNC signal with extended HSYNC signal
|
||||
if sig.VSync && !tv.state.lastSignal.VSync {
|
||||
tv.state.vsyncCount = 0
|
||||
} else if !sig.VSync && tv.state.lastSignal.VSync {
|
||||
if tv.state.vsyncCount > 0 {
|
||||
err := tv.newFrame(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we've "faked" the flyback signal above when horizPos reached
|
||||
// horizClksScanline. we need to handle the real flyback signal however, by
|
||||
// making sure we're at the correct horizPos value. if horizPos doesn't
|
||||
// equal 16 at the front of the HSYNC or 36 at then back of the HSYNC, then
|
||||
// it indicates that the RSYNC register was used last scanline.
|
||||
if sig.HSync && !tv.state.lastSignal.HSync {
|
||||
tv.state.horizPos = 16
|
||||
|
||||
// count vsync lines at start of hsync
|
||||
if sig.VSync || tv.state.lastSignal.VSync {
|
||||
tv.state.vsyncCount++
|
||||
}
|
||||
}
|
||||
if !sig.HSync && tv.state.lastSignal.HSync {
|
||||
tv.state.horizPos = 36
|
||||
}
|
||||
|
||||
// doing nothing with CBURST signal
|
||||
|
||||
// decode color using the regular color signal
|
||||
col := tv.state.spec.getColor(sig.Pixel)
|
||||
for f := range tv.renderers {
|
||||
err := tv.renderers[f].SetPixel(tv.state.horizPos, tv.state.scanline,
|
||||
col.R, col.G, col.B, sig.VBlank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// record the current signal settings so they can be used for reference
|
||||
tv.state.lastSignal = sig
|
||||
|
||||
e := signalHistoryEntry{
|
||||
x: tv.state.horizPos,
|
||||
y: tv.state.scanline,
|
||||
sig: sig,
|
||||
}
|
||||
|
||||
if tv.state.signalHistoryIdx >= len(tv.state.signalHistory) {
|
||||
tv.state.signalHistory = append(tv.state.signalHistory, e)
|
||||
} else {
|
||||
tv.state.signalHistory[tv.state.signalHistoryIdx] = e
|
||||
}
|
||||
tv.state.signalHistoryIdx++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tv *Television) newScanline() error {
|
||||
// notify renderers of new scanline
|
||||
for f := range tv.renderers {
|
||||
err := tv.renderers[f].NewScanline(tv.state.scanline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tv *Television) newFrame(synced bool) error {
|
||||
// a synced frame is one which was generated from a valid VSYNC/VBLANK sequence
|
||||
if tv.state.syncedFrame {
|
||||
tv.state.syncedFrameNum++
|
||||
}
|
||||
|
||||
// specification change
|
||||
if tv.state.syncedFrameNum > leadingFrames && tv.state.syncedFrameNum < stabilityThreshold {
|
||||
if tv.state.auto && !tv.state.syncedFrame && tv.state.scanline > excessScanlinesNTSC {
|
||||
// flip from NTSC to PAL
|
||||
if tv.state.spec.ID == SpecNTSC.ID {
|
||||
_ = tv.SetSpec("PAL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// commit any resizing that maybe pending
|
||||
err := tv.resizer.commit(tv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare for next frame
|
||||
tv.state.frameNum++
|
||||
tv.state.scanline = 0
|
||||
tv.resizer.prepare(tv)
|
||||
tv.state.syncedFrame = synced
|
||||
|
||||
// process all FrameTriggers
|
||||
for f := range tv.frameTriggers {
|
||||
err = tv.frameTriggers[f].NewFrame(tv.state.frameNum, tv.IsStable())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// reset signal history for next frame
|
||||
tv.state.signalHistoryIdx = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsStable returns true if the television thinks the image being sent by
|
||||
// the VCS is stable.
|
||||
func (tv Television) IsStable() bool {
|
||||
return tv.state.syncedFrameNum >= stabilityThreshold
|
||||
}
|
||||
|
||||
// Returns a copy of SignalAttributes for reference.
|
||||
func (tv *Television) GetLastSignal() SignalAttributes {
|
||||
return tv.state.lastSignal
|
||||
}
|
||||
|
||||
// Returns state information.
|
||||
func (tv *Television) GetState(request StateReq) (int, error) {
|
||||
switch request {
|
||||
case ReqFramenum:
|
||||
return tv.state.frameNum, nil
|
||||
case ReqScanline:
|
||||
return tv.state.scanline, nil
|
||||
case ReqHorizPos:
|
||||
return tv.state.horizPos - HorizClksHBlank, nil
|
||||
default:
|
||||
return 0, curated.Errorf("television: unhandled tv state request (%v)", request)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the television's specification.
|
||||
func (tv *Television) SetSpec(spec string) error {
|
||||
switch strings.ToUpper(spec) {
|
||||
case "NTSC":
|
||||
tv.state.spec = SpecNTSC
|
||||
tv.state.auto = false
|
||||
case "PAL":
|
||||
tv.state.spec = SpecPAL
|
||||
tv.state.auto = false
|
||||
case "AUTO":
|
||||
tv.state.spec = SpecNTSC
|
||||
tv.state.auto = true
|
||||
default:
|
||||
return curated.Errorf("television: unsupported spec (%s)", spec)
|
||||
}
|
||||
|
||||
tv.state.top = tv.state.spec.ScanlineTop
|
||||
tv.state.bottom = tv.state.spec.ScanlineBottom
|
||||
tv.resizer.prepare(tv)
|
||||
|
||||
for f := range tv.renderers {
|
||||
err := tv.renderers[f].Resize(tv.state.spec, tv.state.top, tv.state.bottom-tv.state.top)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// allocate enough memory for a TV screen that stays within the limits of
|
||||
// the specification
|
||||
tv.state.signalHistory = make([]signalHistoryEntry, HorizClksScanline*(tv.state.bottom-tv.state.top))
|
||||
tv.state.signalHistoryIdx = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetReqSpecID returns the specification that was requested on creation.
|
||||
func (tv *Television) GetReqSpecID() string {
|
||||
return tv.reqSpecID
|
||||
}
|
||||
|
||||
// Returns the television's current specification. Renderers should use
|
||||
// GetSpec() rather than keeping a private pointer to the specification.
|
||||
func (tv Television) GetSpec() Spec {
|
||||
return tv.state.spec
|
||||
}
|
||||
|
||||
// SetFPSCap whether the emulation should wait for FPS limiter.
|
||||
//
|
||||
// Reasons for turning the cap
|
||||
// off include performance measurement. The debugger also turns the cap off and
|
||||
// replaces it with its own. The FPS limiter in this television implementation
|
||||
// works at the frame level which is not fine grained enough for effective
|
||||
// limiting of rates less than 1fps.
|
||||
func (tv *Television) SetFPSCap(limit bool) {
|
||||
tv.lmtr.limit = limit
|
||||
}
|
||||
|
||||
// Request the number frames per second. This overrides the frame rate of
|
||||
// the specification. A negative value restores the spec's frame rate.
|
||||
func (tv *Television) SetFPS(fps float32) {
|
||||
if fps == -1 {
|
||||
fps = tv.state.spec.FramesPerSecond
|
||||
}
|
||||
tv.lmtr.setRate(fps, tv.state.spec.ScanlinesTotal)
|
||||
}
|
||||
|
||||
// The requested number of frames per second. Compare with GetActualFPS()
|
||||
// to check for accuracy.
|
||||
func (tv *Television) GetReqFPS() float32 {
|
||||
return tv.lmtr.requested
|
||||
}
|
||||
|
||||
// The current number of frames per second. Note that FPS measurement still
|
||||
// works even when frame capping is disabled.
|
||||
func (tv *Television) GetActualFPS() float32 {
|
||||
return tv.lmtr.actual
|
||||
}
|
||||
|
|
|
@ -22,25 +22,22 @@ import (
|
|||
)
|
||||
|
||||
func TestNewTelevision(t *testing.T) {
|
||||
var tv television.Television
|
||||
var err error
|
||||
|
||||
tv, err = television.NewReference("PAL")
|
||||
tv, err := television.NewTelevision("PAL")
|
||||
if tv == nil || err != nil {
|
||||
t.Errorf("PAL spec creation failed")
|
||||
}
|
||||
|
||||
tv, err = television.NewReference("NTSC")
|
||||
tv, err = television.NewTelevision("NTSC")
|
||||
if tv == nil || err != nil {
|
||||
t.Errorf("NTSC spec creation failed")
|
||||
}
|
||||
|
||||
tv, err = television.NewReference("AUTO")
|
||||
tv, err = television.NewTelevision("AUTO")
|
||||
if tv == nil || err != nil {
|
||||
t.Errorf("AUTO spec creation failed")
|
||||
}
|
||||
|
||||
tv, err = television.NewReference("FOO")
|
||||
tv, err = television.NewTelevision("FOO")
|
||||
if tv != nil || err == nil {
|
||||
t.Errorf("'FOO' spec creation unexpectedly succeeded")
|
||||
}
|
|
@ -33,7 +33,7 @@ import (
|
|||
// VCS struct is the main container for the emulated components of the VCS.
|
||||
type VCS struct {
|
||||
Prefs *preferences.Preferences
|
||||
TV television.Television
|
||||
TV *television.Television
|
||||
|
||||
// references to the different components of the VCS. do not take copies of
|
||||
// these pointer values because the rewind feature will change them.
|
||||
|
@ -47,7 +47,7 @@ type VCS struct {
|
|||
|
||||
// NewVCS creates a new VCS and everything associated with the hardware. It is
|
||||
// used for all aspects of emulation: debugging sessions, and regular play.
|
||||
func NewVCS(tv television.Television) (*VCS, error) {
|
||||
func NewVCS(tv *television.Television) (*VCS, error) {
|
||||
// set up preferences
|
||||
prefs, err := preferences.NewPreferences()
|
||||
if err != nil {
|
||||
|
|
|
@ -19,7 +19,7 @@ import "github.com/jetsetilly/gopher2600/hardware/television"
|
|||
|
||||
// CalcFPS takes the the number of frames and duration (in seconds) and returns
|
||||
// the frames-per-second and the accuracy of that value as a percentage.
|
||||
func CalcFPS(tv television.Television, numFrames int, duration float64) (fps float64, accuracy float64) {
|
||||
func CalcFPS(tv *television.Television, numFrames int, duration float64) (fps float64, accuracy float64) {
|
||||
fps = float64(numFrames) / duration
|
||||
spec := tv.GetSpec()
|
||||
accuracy = 100 * float64(numFrames) / (duration * float64(spec.FramesPerSecond))
|
||||
|
|
|
@ -30,7 +30,7 @@ import (
|
|||
)
|
||||
|
||||
// Check is a very rough and ready calculation of the emulator's performance.
|
||||
func Check(output io.Writer, profile bool, tv television.Television, runTime string, cartload cartridgeloader.Loader) error {
|
||||
func Check(output io.Writer, profile bool, tv *television.Television, runTime string, cartload cartridgeloader.Loader) error {
|
||||
var err error
|
||||
|
||||
// create vcs using the tv created above
|
||||
|
|
|
@ -50,7 +50,7 @@ type playmode struct {
|
|||
// contents of the file specified in Filename field of the Loader instance will
|
||||
// be checked. If it is a playback file then the playback codepath will be
|
||||
// used.
|
||||
func Play(tv television.Television, scr gui.GUI, newRecording bool, cartload cartridgeloader.Loader, patchFile string, hiscoreServer bool, useSavekey bool) error {
|
||||
func Play(tv *television.Television, scr gui.GUI, newRecording bool, cartload cartridgeloader.Loader, patchFile string, hiscoreServer bool, useSavekey bool) error {
|
||||
var recording string
|
||||
|
||||
// if supplied cartridge name is actually a playback file then set
|
||||
|
|
|
@ -127,7 +127,7 @@ func (reg *LogRegression) regress(newRegression bool, output io.Writer, msg stri
|
|||
output.Write([]byte(msg))
|
||||
|
||||
// create headless television. we'll use this to initialise the digester
|
||||
tv, err := television.NewReference(reg.TVtype)
|
||||
tv, err := television.NewTelevision(reg.TVtype)
|
||||
if err != nil {
|
||||
return false, "", curated.Errorf("log: %v", err)
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg
|
|||
return false, "", curated.Errorf("playback: %v", err)
|
||||
}
|
||||
|
||||
tv, err := television.NewReference(plb.TVSpec)
|
||||
tv, err := television.NewTelevision(plb.TVSpec)
|
||||
if err != nil {
|
||||
return false, "", curated.Errorf("playback: %v", err)
|
||||
}
|
||||
|
|
|
@ -180,7 +180,7 @@ func (reg *VideoRegression) regress(newRegression bool, output io.Writer, msg st
|
|||
output.Write([]byte(msg))
|
||||
|
||||
// create headless television. we'll use this to initialise the digester
|
||||
tv, err := television.NewReference(reg.TVtype)
|
||||
tv, err := television.NewTelevision(reg.TVtype)
|
||||
if err != nil {
|
||||
return false, "", curated.Errorf("video: %v", err)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue