diff --git a/debugger/debugger.go b/debugger/debugger.go index d597e4ff..dcd76497 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -8,7 +8,7 @@ import ( "gopher2600/disassembly" "gopher2600/errors" "gopher2600/gui" - "gopher2600/gui/sdl" + "gopher2600/gui/sdldebug" "gopher2600/hardware" "gopher2600/hardware/cpu/definitions" "gopher2600/hardware/memory" @@ -139,11 +139,10 @@ func NewDebugger(tvType string) (*Debugger, error) { return nil, errors.New(errors.DebuggerError, err) } - dbg.gui, err = sdl.NewPixelTV(tvType, 2.0, btv) + dbg.gui, err = sdldebug.NewSdlDebug(tvType, 2.0, btv) if err != nil { return nil, errors.New(errors.DebuggerError, err) } - dbg.gui.SetFeature(gui.ReqSetAllowDebugging, true) // create a new VCS instance dbg.vcs, err = hardware.NewVCS(dbg.gui) diff --git a/gui/gui.go b/gui/gui.go index d0420535..4bf14110 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -41,7 +41,6 @@ const ( ReqSetVisibility FeatureReq = iota // bool, optional bool (update on show) default true ReqToggleVisibility // optional bool (update on show) default true ReqSetVisibilityStable // none - ReqSetAllowDebugging // bool ReqSetPause // bool ReqSetMasking // bool ReqToggleMasking // none diff --git a/gui/sdl/requests.go b/gui/sdl/requests.go deleted file mode 100644 index 4c1c67ae..00000000 --- a/gui/sdl/requests.go +++ /dev/null @@ -1,110 +0,0 @@ -package sdl - -import ( - "gopher2600/errors" - "gopher2600/gui" - - "github.com/veandco/go-sdl2/sdl" -) - -// SetFeature is used to set a television attribute -func (pxtv *PixelTV) SetFeature(request gui.FeatureReq, args ...interface{}) (returnedErr error) { - // lazy (but clear) handling of type assertion errors - defer func() { - if r := recover(); r != nil { - returnedErr = errors.New(errors.PanicError, "sdl.SetFeature()", r) - } - }() - - switch request { - case gui.ReqSetVisibilityStable: - err := pxtv.scr.stb.resolveSetVisibility() - if err != nil { - return err - } - - case gui.ReqSetVisibility: - if args[0].(bool) { - pxtv.scr.window.Show() - - // update screen - // -- default args[1] of true if not present - if len(args) < 2 || args[1].(bool) { - pxtv.scr.update() - } - } else { - pxtv.scr.window.Hide() - } - - case gui.ReqToggleVisibility: - if pxtv.scr.window.GetFlags()&sdl.WINDOW_HIDDEN == sdl.WINDOW_HIDDEN { - pxtv.scr.window.Show() - - // update screen - // -- default args[1] of true if not present - if len(args) < 2 || args[1].(bool) { - pxtv.scr.update() - } - } else { - pxtv.scr.window.Hide() - } - - case gui.ReqSetAllowDebugging: - pxtv.allowDebugging = (args[0].(bool)) - pxtv.scr.update() - - case gui.ReqSetPause: - pxtv.paused = args[0].(bool) - pxtv.scr.update() - - case gui.ReqSetMasking: - pxtv.scr.setMasking(args[0].(bool)) - pxtv.scr.update() - - case gui.ReqToggleMasking: - pxtv.scr.setMasking(!pxtv.scr.unmasked) - pxtv.scr.update() - - case gui.ReqSetAltColors: - pxtv.scr.useAltPixels = args[0].(bool) - pxtv.scr.update() - - case gui.ReqToggleAltColors: - pxtv.scr.useAltPixels = !pxtv.scr.useAltPixels - pxtv.scr.update() - - case gui.ReqSetOverlay: - pxtv.scr.overlayActive = args[0].(bool) - pxtv.scr.update() - - case gui.ReqToggleOverlay: - pxtv.scr.overlayActive = !pxtv.scr.overlayActive - pxtv.scr.update() - - case gui.ReqSetScale: - pxtv.scr.setScaling(args[0].(float32)) - pxtv.scr.update() - - case gui.ReqIncScale: - if pxtv.scr.pixelScaleY < 4.0 { - pxtv.scr.setScaling(pxtv.scr.pixelScaleY + 0.1) - pxtv.scr.update() - } - - case gui.ReqDecScale: - if pxtv.scr.pixelScaleY > 0.5 { - pxtv.scr.setScaling(pxtv.scr.pixelScaleY - 0.1) - pxtv.scr.update() - } - - default: - return errors.New(errors.UnknownGUIRequest, request) - } - - return nil -} - -// SetEventChannel implements the GUI interface -func (pxtv *PixelTV) SetEventChannel(eventChannel chan gui.Event) { - pxtv.eventChannel = eventChannel -} diff --git a/gui/sdl/screen.go b/gui/sdl/screen.go deleted file mode 100644 index bfe4f467..00000000 --- a/gui/sdl/screen.go +++ /dev/null @@ -1,408 +0,0 @@ -package sdl - -import ( - "gopher2600/errors" - "gopher2600/performance/limiter" - "gopher2600/television" - - "github.com/veandco/go-sdl2/sdl" -) - -// the number of bytes required for each screen pixel -// 4 == red + green + blue + alpha -const scrDepth int32 = 4 - -type screen struct { - pxtv *PixelTV - spec *television.Specification - - // regulates how often the screen is updated - fpsLimiter *limiter.FpsLimiter - - window *sdl.Window - renderer *sdl.Renderer - - // maxWidth and maxHeight are the maximum possible sizes for the current tv - // specification - maxWidth int32 - maxHeight int32 - maxMask *sdl.Rect - - // textures are used to present the pixels to the renderer - texture *sdl.Texture - textureFade *sdl.Texture - - // the width of each VCS colour clock (in SDL pixels) - pixelWidth int - - // by how much each pixel should be scaled - pixelScaleY float32 - pixelScaleX float32 - - // play variables differ depending on the ROM - playWidth int32 - playHeight int32 - playSrcMask *sdl.Rect - playDstMask *sdl.Rect - - // destRect and srcRect change depending on the value of unmasked - srcRect *sdl.Rect - destRect *sdl.Rect - - // stabiliser to make sure image remains solid - stb *screenStabiliser - - // whether we're using an unmasked screen - // -- changed by user request - unmasked bool - - // the remaining attributes change every update - - // last plot coordinates - lastX int32 - lastY int32 - - // pixels arrays are of maximum screen size - actual smaller play screens - // are masked appropriately - pixels []byte - pixelsFade []byte - - // altPixels mirrors the pixels array with alternative color palette - // -- useful for switching between regular and debug colors - // -- allocated but only used if pxtv.allowDebugging and useAltPixels is true - altPixels []byte - altPixelsFade []byte - useAltPixels bool - - // overlay for screen showing additional debugging information - // -- always allocated but only used when tv.allowDebugging and - // overlayActive are true - overlay *metapixelOverlay - overlayActive bool -} - -func newScreen(pxtv *PixelTV) (*screen, error) { - var err error - - scr := new(screen) - scr.pxtv = pxtv - - // SDL window - the correct size for the window will be determined below - scr.window, err = sdl.CreateWindow("Gopher2600", int32(sdl.WINDOWPOS_UNDEFINED), int32(sdl.WINDOWPOS_UNDEFINED), 0, 0, uint32(sdl.WINDOW_HIDDEN)) - if err != nil { - return nil, errors.New(errors.SDL, err) - } - - // SDL renderer - scr.renderer, err = sdl.CreateRenderer(scr.window, -1, uint32(sdl.RENDERER_ACCELERATED)|uint32(sdl.RENDERER_PRESENTVSYNC)) - if err != nil { - return nil, errors.New(errors.SDL, err) - } - - // set attributes that depend on the television specification - err = scr.initialiseScreen() - if err != nil { - return nil, errors.New(errors.SDL, err) - } - - // new stabiliser - scr.stb = newScreenStabiliser(scr) - - return scr, nil -} - -// initialise screen sets up SDL according to the current television -// specification. it is called on startup but also whenever a change in the TV -// spec is requested -func (scr *screen) initialiseScreen() error { - var err error - - scr.spec = scr.pxtv.GetSpec() - - scr.maxWidth = int32(television.ClocksPerScanline) - scr.maxHeight = int32(scr.spec.ScanlinesTotal) - scr.maxMask = &sdl.Rect{X: 0, Y: 0, W: scr.maxWidth, H: scr.maxHeight} - - scr.playWidth = int32(television.ClocksPerVisible) - scr.setPlayArea(int32(scr.spec.ScanlinesPerVisible), int32(scr.spec.ScanlinesPerVBlank+scr.spec.ScanlinesPerVSync)) - - // 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 = scr.renderer.CreateTexture(uint32(sdl.PIXELFORMAT_ABGR8888), int(sdl.TEXTUREACCESS_STREAMING), int32(scr.maxWidth), int32(scr.maxHeight)) - if err != nil { - return errors.New(errors.SDL, err) - } - scr.texture.SetBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND)) - - // 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.textureFade, err = scr.renderer.CreateTexture(uint32(sdl.PIXELFORMAT_ABGR8888), int(sdl.TEXTUREACCESS_STREAMING), int32(scr.maxWidth), int32(scr.maxHeight)) - if err != nil { - return errors.New(errors.SDL, err) - } - scr.textureFade.SetBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND)) - scr.textureFade.SetAlphaMod(50) - - // our acutal screen data - scr.pixels = make([]byte, scr.maxWidth*scr.maxHeight*scrDepth) - scr.pixelsFade = make([]byte, scr.maxWidth*scr.maxHeight*scrDepth) - scr.altPixels = make([]byte, scr.maxWidth*scr.maxHeight*scrDepth) - scr.altPixelsFade = make([]byte, scr.maxWidth*scr.maxHeight*scrDepth) - - // new overlay - scr.overlay, err = newMetapixelOverlay(scr) - if err != nil { - return errors.New(errors.SDL, err) - } - - // frame limiter - scr.fpsLimiter, err = limiter.NewFPSLimiter(int(scr.spec.FramesPerSecond)) - if err != nil { - return errors.New(errors.SDL, err) - } - - return nil -} - -// setPlayArea defines the limits of the "play area" -func (scr *screen) setPlayArea(scanlines int32, top int32) { - scr.playHeight = scanlines - scr.playDstMask = &sdl.Rect{X: 0, Y: 0, W: scr.playWidth, H: scr.playHeight} - scr.playSrcMask = &sdl.Rect{X: int32(television.ClocksPerHblank), Y: top, W: scr.playWidth, H: scr.playHeight} - scr.setMasking(scr.unmasked) -} - -// adjustPlayArea is used to move the play area up/down by the specified amount -func (scr *screen) adjustPlayArea(adjust int32) { - // !!TODO: make screen adjustment optional - scr.playSrcMask.Y += adjust -} - -// setScaling alters how big each pixel is on the physical screen. any change -// in the scale will cause the window size to change (via a call to -// the setMasking() function) -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.pixelScaleY = scale - scr.pixelScaleX = scale * scr.pxtv.GetSpec().AspectBias - - // make sure everything drawn through the renderer is correctly scaled - err := scr.renderer.SetScale(float32(scr.pixelWidth)*scr.pixelScaleX, scr.pixelScaleY) - if err != nil { - return err - } - - scr.setMasking(scr.unmasked) - - return nil -} - -// setMasking alters which scanlines are actually shown. i.e. when unmasked, we -// can see the vblank and hblank areas of the screen. this can cause the window size -// to change -func (scr *screen) setMasking(unmasked bool) { - var w, h int32 - - scr.unmasked = unmasked - - if scr.unmasked { - w = int32(float32(scr.maxWidth) * scr.pixelScaleX * float32(scr.pixelWidth)) - h = int32(float32(scr.maxHeight) * scr.pixelScaleY) - scr.destRect = scr.maxMask - scr.srcRect = scr.maxMask - } else { - w = int32(float32(scr.playWidth) * scr.pixelScaleX * float32(scr.pixelWidth)) - h = int32(float32(scr.playHeight) * scr.pixelScaleY) - scr.destRect = scr.playDstMask - scr.srcRect = scr.playSrcMask - } - - cw, ch := scr.window.GetSize() - if cw != w || ch != h { - // BUG: SetSize causes window to gain focus - scr.window.SetSize(w, h) - } -} - -func (scr *screen) setRegPixel(x, y int32, red, green, blue byte, vblank bool) error { - return scr.setPixel(&scr.pixels, x, y, red, green, blue, vblank) -} - -func (scr *screen) setAltPixel(x, y int32, red, green, blue byte, vblank bool) error { - return scr.setPixel(&scr.altPixels, x, y, red, green, blue, vblank) -} - -func (scr *screen) setPixel(pixels *[]byte, x, y int32, red, green, blue byte, vblank bool) error { - scr.lastX = x - scr.lastY = y - - if !vblank { - i := (y*scr.maxWidth + x) * scrDepth - if i < int32(len(scr.pixels))-scrDepth && i >= 0 { - (*pixels)[i] = red - (*pixels)[i+1] = green - (*pixels)[i+2] = blue - (*pixels)[i+3] = 255 - } - } - - return nil -} - -func (scr *screen) update() error { - // enforce a maximum frames-per-second - scr.fpsLimiter.Wait() - - var err error - - // clear image from rendered. using a non-video-black color if screen is - // unmasked - if scr.unmasked { - scr.renderer.SetDrawColor(5, 5, 5, 255) - } else { - scr.renderer.SetDrawColor(0, 0, 0, 255) - } - scr.renderer.SetDrawBlendMode(sdl.BlendMode(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 scr.pxtv.paused { - if scr.pxtv.allowDebugging && scr.useAltPixels { - err = scr.textureFade.Update(nil, scr.altPixelsFade, int(scr.maxWidth*scrDepth)) - } else { - err = scr.textureFade.Update(nil, scr.pixelsFade, int(scr.maxWidth*scrDepth)) - } - if err != nil { - return err - } - err = scr.renderer.Copy(scr.textureFade, scr.srcRect, scr.destRect) - if err != nil { - return err - } - } - - // show current frame's pixels - // - decide which set of pixels to use - // - if tv is paused this overwrites the faded image (drawn above) up to - // the pixel where the current frame has reached - if scr.pxtv.allowDebugging && scr.useAltPixels { - err = scr.texture.Update(nil, scr.altPixels, int(scr.maxWidth*scrDepth)) - } else { - err = scr.texture.Update(nil, scr.pixels, int(scr.maxWidth*scrDepth)) - } - if err != nil { - return err - } - - err = scr.renderer.Copy(scr.texture, scr.srcRect, scr.destRect) - if err != nil { - return err - } - - // show hblank overlay - if scr.unmasked { - scr.renderer.SetDrawColor(100, 100, 100, 20) - scr.renderer.SetDrawBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND)) - scr.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(television.ClocksPerHblank), H: int32(scr.spec.ScanlinesTotal)}) - } - - // show overlay - if scr.pxtv.allowDebugging && scr.overlayActive { - err = scr.overlay.update(scr.pxtv.paused) - if err != nil { - return err - } - } - - // add cursor if tv is paused - // - drawing last so that cursor isn't masked - if scr.pxtv.paused { - // cursor coordinates - x := int(scr.lastX) - y := int(scr.lastY) - - // cursor is one step ahead of pixel -- move to new scanline if - // necessary - if x >= television.ClocksPerScanline { - x = 0 - y++ - } - - // note whether cursor is "off-screen" (according to current masking) - offscreenCursorPos := false - - // adjust coordinates if screen is masked - if !scr.unmasked { - x -= int(scr.srcRect.X) - y -= int(scr.srcRect.Y) - - if x < 0 { - offscreenCursorPos = true - x = 0 - } - if y < 0 { - offscreenCursorPos = true - y = 0 - } - } - - // cursor color depends on whether cursor is off-screen or not - if offscreenCursorPos { - scr.renderer.SetDrawColor(100, 100, 255, 100) - } else { - scr.renderer.SetDrawColor(255, 255, 255, 100) - } - scr.renderer.SetDrawBlendMode(sdl.BlendMode(sdl.BLENDMODE_NONE)) - - // leave the current pixel visible at the top-left corner of the cursor - scr.renderer.DrawRect(&sdl.Rect{X: int32(x + 1), Y: int32(y), W: 1, H: 1}) - scr.renderer.DrawRect(&sdl.Rect{X: int32(x + 1), Y: int32(y + 1), W: 1, H: 1}) - scr.renderer.DrawRect(&sdl.Rect{X: int32(x), Y: int32(y + 1), W: 1, H: 1}) - } - - scr.renderer.Present() - - return nil -} - -func (scr *screen) newFrame() { - if scr.pxtv.allowDebugging { - // swap pixel array with pixelsFade array - // -- note that we don't do this with the texture instead because - // updating the the extra texture if we don't need to (faded pixels - // only show when the emulation is paused) is expensive - swp := scr.pixels - scr.pixels = scr.pixelsFade - scr.pixelsFade = swp - - // clear pixels in overlay - scr.overlay.newFrame() - - // swap pixel array with pixelsFade array - // -- see comment above - swp = scr.altPixels - scr.altPixels = scr.altPixelsFade - scr.altPixelsFade = swp - - // clear altpixels - for i := 0; i < len(scr.altPixels); i++ { - scr.altPixels[i] = 0 - } - } - - // clear regular pixels - for i := 0; i < len(scr.pixels); i++ { - scr.pixels[i] = 0 - } -} diff --git a/gui/sdl/stability.go b/gui/sdl/stability.go deleted file mode 100644 index 6785e696..00000000 --- a/gui/sdl/stability.go +++ /dev/null @@ -1,137 +0,0 @@ -package sdl - -import ( - "gopher2600/gui" - "gopher2600/television" -) - -// the purpose of the stability check is to prevent the window opening and then -// resizing after the initialisation sequence of the ROM. by giving the ROM -// time to settle down and produce frames with a consistent number of -// scanlines we prevent the window from flapping about too much. - -type screenStabiliser struct { - // the screen which is being stabilzed - scr *screen - - // how many count have been observed that look like they might be stable? - count int - - top int - bottom int - scanlines int - - // has a ReqSetVisibilityStable been received recently? we don't want to - // open the window until the screen is stable - queuedShowRequest bool -} - -func newScreenStabiliser(scr *screen) *screenStabiliser { - stb := new(screenStabiliser) - stb.scr = scr - return stb -} - -// number of consistent frames that needs to elapse before the screen is -// considered "stable". the value has been set arbitrarily, a more -// sophisticated approach may be worth investigating. for now, the lower the -// value the better. -const stabilityThreshold int = 2 - -// restart resets the stability count to zero thereby forcing the play area to -// be reconsidered -func (stb *screenStabiliser) restart() { - stb.count = 0 -} - -// stabiliseFrame checks to see if the screen dimensions have been stable for -// a count of "stabilityThreshold" -// -// currently: once it's been determined that the screen dimensions are stable -// then any changes are ignored -func (stb *screenStabiliser) stabiliseFrame() error { - // measures the consistency of the generated television frame and alters - // window sizing appropriately - - var err error - - top, err := stb.scr.pxtv.GetState(television.ReqVisibleTop) - if err != nil { - return err - } - - bottom, err := stb.scr.pxtv.GetState(television.ReqVisibleBottom) - if err != nil { - return err - } - - scanlines := bottom - top - - // update play height (which in turn updates masking and window size) - if stb.count < stabilityThreshold { - if stb.top != top || stb.bottom != bottom { - stb.top = top - stb.bottom = bottom - stb.scanlines = bottom - top - stb.count = 0 - } else { - stb.count++ - } - } else if stb.count == stabilityThreshold { - stb.count++ - - // calculate the play height from the top and bottom values with a - // minimum according to the tv specification - minScanlines := stb.scr.pxtv.GetSpec().ScanlinesPerVisible - if scanlines < minScanlines { - scanlines = minScanlines - } - - stb.scr.setPlayArea(int32(scanlines), int32(stb.top)) - - // show window if a show request has been queued up - if stb.queuedShowRequest { - err := stb.resolveSetVisibility() - if err != nil { - return err - } - } - } else { - // some ROMs turn VBLANK on/off at different times (no more than a - // scanline or two I would say) but the number of scanlines in the - // visiible area remains consistent. in these instances, because of how - // we've implemented play area masking in the SDL interface, we need to - // adjust the play area. - // - // ROMs affected: - // * Plaque Attack - // - // some other ROMs turn VBLANK on/off at different time but also allow - // the number of scanlines to change. in these instances, we do not - // make the play area adjustment. - // - // ROMs (not) affected: - // * 28c3intro - // - if scanlines == stb.scanlines && stb.top != top { - stb.scr.adjustPlayArea(int32(top - stb.top)) - stb.top = top - stb.bottom = bottom - } - } - - return nil -} - -func (stb *screenStabiliser) resolveSetVisibility() error { - if stb.count > stabilityThreshold { - err := stb.scr.pxtv.SetFeature(gui.ReqSetVisibility, true, true) - if err != nil { - return err - } - stb.queuedShowRequest = false - } else { - stb.queuedShowRequest = true - } - return nil -} diff --git a/gui/sdl/guiloop.go b/gui/sdldebug/guiloop.go similarity index 92% rename from gui/sdl/guiloop.go rename to gui/sdldebug/guiloop.go index 01abf5c5..a156e961 100644 --- a/gui/sdl/guiloop.go +++ b/gui/sdldebug/guiloop.go @@ -1,4 +1,4 @@ -package sdl +package sdldebug import ( "gopher2600/gui" @@ -8,7 +8,7 @@ import ( ) // guiLoop listens for SDL events and is run concurrently -func (pxtv *PixelTV) guiLoop() { +func (pxtv *SdlDebug) guiLoop() { for { sdlEvent := sdl.WaitEvent() switch sdlEvent := sdlEvent.(type) { @@ -116,15 +116,15 @@ func (pxtv *PixelTV) guiLoop() { } } -func (pxtv *PixelTV) convertMouseCoords(sdlEvent *sdl.MouseButtonEvent) (int, int) { +func (pxtv *SdlDebug) convertMouseCoords(sdlEvent *sdl.MouseButtonEvent) (int, int) { var hp, sl int - sx, sy := pxtv.scr.renderer.GetScale() + sx, sy := pxtv.pxl.renderer.GetScale() // convert X pixel value to horizpos equivalent // the opposite of pixelX() and also the scalining applied // by the SDL renderer - if pxtv.scr.unmasked { + if pxtv.pxl.unmasked { hp = int(float32(sdlEvent.X)/sx) - television.ClocksPerHblank } else { hp = int(float32(sdlEvent.X) / sx) @@ -133,10 +133,10 @@ func (pxtv *PixelTV) convertMouseCoords(sdlEvent *sdl.MouseButtonEvent) (int, in // convert Y pixel value to scanline equivalent // the opposite of pixelY() and also the scalining applied // by the SDL renderer - if pxtv.scr.unmasked { + if pxtv.pxl.unmasked { sl = int(float32(sdlEvent.Y) / sy) } else { - sl = int(float32(sdlEvent.Y)/sy) + pxtv.scr.stb.top + sl = int(float32(sdlEvent.Y)/sy) + int(pxtv.pxl.playTop) } return hp, sl diff --git a/gui/sdl/overlay.go b/gui/sdldebug/overlay.go similarity index 83% rename from gui/sdl/overlay.go rename to gui/sdldebug/overlay.go index 737b2913..2034113a 100644 --- a/gui/sdl/overlay.go +++ b/gui/sdldebug/overlay.go @@ -1,4 +1,4 @@ -package sdl +package sdldebug import ( "gopher2600/gui" @@ -7,7 +7,7 @@ import ( ) type metapixelOverlay struct { - scr *screen + scr *pixels texture *sdl.Texture textureFade *sdl.Texture @@ -18,13 +18,13 @@ type metapixelOverlay struct { labels [][]string } -func newMetapixelOverlay(scr *screen) (*metapixelOverlay, error) { +func newMetapixelOverlay(scr *pixels) (*metapixelOverlay, error) { ovl := new(metapixelOverlay) ovl.scr = scr // our acutal screen data - ovl.pixels = make([]byte, ovl.scr.maxWidth*ovl.scr.maxHeight*scrDepth) - ovl.pixelsFade = make([]byte, ovl.scr.maxWidth*ovl.scr.maxHeight*scrDepth) + ovl.pixels = make([]byte, ovl.scr.maxWidth*ovl.scr.maxHeight*pixelDepth) + ovl.pixelsFade = make([]byte, ovl.scr.maxWidth*ovl.scr.maxHeight*pixelDepth) // labels ovl.labels = make([][]string, ovl.scr.maxHeight) @@ -52,7 +52,7 @@ func newMetapixelOverlay(scr *screen) (*metapixelOverlay, error) { } func (ovl *metapixelOverlay) setPixel(sig gui.MetaPixel) error { - i := (ovl.scr.lastY*ovl.scr.maxWidth + ovl.scr.lastX) * scrDepth + i := (ovl.scr.lastY*ovl.scr.maxWidth + ovl.scr.lastX) * pixelDepth if i >= int32(len(ovl.pixels)) { return nil @@ -84,7 +84,7 @@ func (ovl *metapixelOverlay) newFrame() { func (ovl *metapixelOverlay) update(paused bool) error { if paused { - err := ovl.textureFade.Update(nil, ovl.pixelsFade, int(ovl.scr.maxWidth*scrDepth)) + err := ovl.textureFade.Update(nil, ovl.pixelsFade, int(ovl.scr.maxWidth*pixelDepth)) if err != nil { return err } @@ -95,7 +95,7 @@ func (ovl *metapixelOverlay) update(paused bool) error { } } - err := ovl.texture.Update(nil, ovl.pixels, int(ovl.scr.maxWidth*scrDepth)) + err := ovl.texture.Update(nil, ovl.pixels, int(ovl.scr.maxWidth*pixelDepth)) if err != nil { return err } @@ -109,11 +109,6 @@ func (ovl *metapixelOverlay) update(paused bool) error { } // SetMetaPixel recieves (and processes) additional emulator information from the emulator -func (pxtv *PixelTV) SetMetaPixel(sig gui.MetaPixel) error { - // don't do anything if debugging is not enabled - if !pxtv.allowDebugging { - return nil - } - - return pxtv.scr.overlay.setPixel(sig) +func (pxtv *SdlDebug) SetMetaPixel(sig gui.MetaPixel) error { + return pxtv.pxl.metaPixels.setPixel(sig) } diff --git a/gui/sdldebug/pixels.go b/gui/sdldebug/pixels.go new file mode 100644 index 00000000..b9575ba8 --- /dev/null +++ b/gui/sdldebug/pixels.go @@ -0,0 +1,365 @@ +package sdldebug + +import ( + "gopher2600/errors" + "gopher2600/television" + + "github.com/veandco/go-sdl2/sdl" +) + +const pixelDepth = 4 +const pixelWidth = 2.0 + +type pixels struct { + scr *SdlDebug + + renderer *sdl.Renderer + + // textures are used to present the pixels to the renderer + texture *sdl.Texture + textureFade *sdl.Texture + + // maxWidth and maxHeight are the maximum possible sizes for the current tv + // specification + maxWidth int32 + maxHeight int32 + maxMask *sdl.Rect + + // by how much each pixel should be scaled + pixelScaleY float32 + pixelScaleX float32 + + // play variables differ depending on the ROM + playWidth int32 + playHeight int32 + playSrcMask *sdl.Rect + playDstMask *sdl.Rect + playTop int32 + + // destRect and srcRect change depending on the value of unmasked + srcRect *sdl.Rect + destRect *sdl.Rect + + // whether we're using an unmasked screen + unmasked bool + + // the remaining attributes change every update + + // last plot coordinates. used for: + // - drawing cursor + // - adding metaPixels + lastX int32 + lastY int32 + + // pixels arrays are of maximum screen size - actual smaller play screens + // are masked appropriately + pixels []byte + pixelsFade []byte + + // altPixels mirrors the pixels array with alternative color palette + // - useful for switching between regular and debug colors + // - allocated but only used if scr.allowDebugging and useAltPixels is true + altPixels []byte + altPixelsFade []byte + useAltPixels bool + + // metaPixels for screen showing additional debugging information + // - always allocated but only used when tv.allowDebugging and + // overlayActive are true + metaPixels *metapixelOverlay + useMetaPixels bool +} + +func newScreen(scr *SdlDebug) (*pixels, error) { + var err error + + pxl := pixels{scr: scr} + + // SDL renderer + pxl.renderer, err = sdl.CreateRenderer(scr.window, -1, uint32(sdl.RENDERER_ACCELERATED)|uint32(sdl.RENDERER_PRESENTVSYNC)) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + return &pxl, nil +} + +func (pxl *pixels) reset() error { + pxl.newFrame() + pxl.lastX = 0 + pxl.lastY = 0 + return nil +} + +// initialise screen sets up SDL according to the current television +// specification. it is called on startup but also whenever a change in the TV +// spec is requested +func (pxl *pixels) resize(topScanline, numScanlines int) error { + var err error + + pxl.maxWidth = int32(television.ClocksPerScanline) + pxl.maxHeight = int32(pxl.scr.GetSpec().ScanlinesTotal) + pxl.maxMask = &sdl.Rect{X: 0, Y: 0, W: pxl.maxWidth, H: pxl.maxHeight} + + pxl.playTop = int32(topScanline) + pxl.playWidth = int32(television.ClocksPerVisible) + pxl.setPlayArea(int32(numScanlines), int32(topScanline)) + + // 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 + pxl.texture, err = pxl.renderer.CreateTexture(uint32(sdl.PIXELFORMAT_ABGR8888), int(sdl.TEXTUREACCESS_STREAMING), int32(pxl.maxWidth), int32(pxl.maxHeight)) + if err != nil { + return errors.New(errors.SDL, err) + } + pxl.texture.SetBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND)) + + // 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 + pxl.textureFade, err = pxl.renderer.CreateTexture(uint32(sdl.PIXELFORMAT_ABGR8888), int(sdl.TEXTUREACCESS_STREAMING), int32(pxl.maxWidth), int32(pxl.maxHeight)) + if err != nil { + return errors.New(errors.SDL, err) + } + pxl.textureFade.SetBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND)) + pxl.textureFade.SetAlphaMod(50) + + // our acutal screen data + pxl.pixels = make([]byte, pxl.maxWidth*pxl.maxHeight*pixelDepth) + pxl.pixelsFade = make([]byte, pxl.maxWidth*pxl.maxHeight*pixelDepth) + pxl.altPixels = make([]byte, pxl.maxWidth*pxl.maxHeight*pixelDepth) + pxl.altPixelsFade = make([]byte, pxl.maxWidth*pxl.maxHeight*pixelDepth) + + // new overlay + pxl.metaPixels, err = newMetapixelOverlay(pxl) + if err != nil { + return errors.New(errors.SDL, err) + } + + return nil +} + +// setPlayArea defines the limits of the "play area" +func (pxl *pixels) setPlayArea(scanlines int32, top int32) { + pxl.playHeight = scanlines + pxl.playDstMask = &sdl.Rect{X: 0, Y: 0, W: pxl.playWidth, H: pxl.playHeight} + pxl.playSrcMask = &sdl.Rect{X: int32(television.ClocksPerHblank), Y: top, W: pxl.playWidth, H: pxl.playHeight} + pxl.setMasking(pxl.unmasked) +} + +// setScaling alters how big each pixel is on the physical screen. any change +// in the scale will cause the window size to change (via a call to +// the setMasking() function) +func (pxl *pixels) setScaling(scale float32) error { + // pixel scale is the number of pixels each VCS "pixel" is to be occupy on + // the screen + pxl.pixelScaleY = scale + pxl.pixelScaleX = scale * pxl.scr.GetSpec().AspectBias + + // make sure everything drawn through the renderer is correctly scaled + err := pxl.renderer.SetScale(pixelWidth*pxl.pixelScaleX, pxl.pixelScaleY) + if err != nil { + return err + } + + pxl.setMasking(pxl.unmasked) + + return nil +} + +// setMasking alters which scanlines are actually shown. i.e. when unmasked, we +// can see the vblank and hblank areas of the screen. this can cause the window size +// to change +func (pxl *pixels) setMasking(unmasked bool) { + var w, h int32 + + pxl.unmasked = unmasked + + if pxl.unmasked { + w = int32(float32(pxl.maxWidth) * pxl.pixelScaleX * pixelWidth) + h = int32(float32(pxl.maxHeight) * pxl.pixelScaleY) + pxl.destRect = pxl.maxMask + pxl.srcRect = pxl.maxMask + } else { + w = int32(float32(pxl.playWidth) * pxl.pixelScaleX * pixelWidth) + h = int32(float32(pxl.playHeight) * pxl.pixelScaleY) + pxl.destRect = pxl.playDstMask + pxl.srcRect = pxl.playSrcMask + } + + // BUG: SetSize causes window to gain focus + cw, ch := pxl.scr.window.GetSize() + if cw != w || ch != h { + pxl.scr.window.SetSize(w, h) + } +} + +func (pxl *pixels) setRegPixel(x, y int32, red, green, blue byte, vblank bool) error { + return pxl.setPixel(&pxl.pixels, x, y, red, green, blue, vblank) +} + +func (pxl *pixels) setAltPixel(x, y int32, red, green, blue byte, vblank bool) error { + return pxl.setPixel(&pxl.altPixels, x, y, red, green, blue, vblank) +} + +func (pxl *pixels) setPixel(pixels *[]byte, x, y int32, red, green, blue byte, vblank bool) error { + pxl.lastX = x + pxl.lastY = y + + if !vblank { + i := (y*pxl.maxWidth + x) * pixelDepth + if i < int32(len(pxl.pixels))-pixelDepth && i >= 0 { + (*pixels)[i] = red + (*pixels)[i+1] = green + (*pixels)[i+2] = blue + (*pixels)[i+3] = 255 + } + } + + return nil +} + +func (pxl *pixels) update() error { + var err error + + // clear image from rendered. using a non-video-black color if screen is + // unmasked + if pxl.unmasked { + pxl.renderer.SetDrawColor(5, 5, 5, 255) + } else { + pxl.renderer.SetDrawColor(0, 0, 0, 255) + } + pxl.renderer.SetDrawBlendMode(sdl.BlendMode(sdl.BLENDMODE_NONE)) + err = pxl.renderer.Clear() + if err != nil { + return err + } + + // if tv is paused then show the previous frame's faded image + if pxl.scr.paused { + if pxl.useAltPixels { + err = pxl.textureFade.Update(nil, pxl.altPixelsFade, int(pxl.maxWidth*pixelDepth)) + } else { + err = pxl.textureFade.Update(nil, pxl.pixelsFade, int(pxl.maxWidth*pixelDepth)) + } + if err != nil { + return err + } + err = pxl.renderer.Copy(pxl.textureFade, pxl.srcRect, pxl.destRect) + if err != nil { + return err + } + } + + // show current frame's pixels + // - decide which set of pixels to use + // - if tv is paused this overwrites the faded image (drawn above) up to + // the pixel where the current frame has reached + if pxl.useAltPixels { + err = pxl.texture.Update(nil, pxl.altPixels, int(pxl.maxWidth*pixelDepth)) + } else { + err = pxl.texture.Update(nil, pxl.pixels, int(pxl.maxWidth*pixelDepth)) + } + if err != nil { + return err + } + + err = pxl.renderer.Copy(pxl.texture, pxl.srcRect, pxl.destRect) + if err != nil { + return err + } + + // show hblank overlay + if pxl.unmasked { + pxl.renderer.SetDrawColor(100, 100, 100, 20) + pxl.renderer.SetDrawBlendMode(sdl.BlendMode(sdl.BLENDMODE_BLEND)) + pxl.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(television.ClocksPerHblank), H: int32(pxl.scr.GetSpec().ScanlinesTotal)}) + } + + // show overlay + if pxl.useMetaPixels { + err = pxl.metaPixels.update(pxl.scr.paused) + if err != nil { + return err + } + } + + // add cursor if tv is paused + // - drawing last so that cursor isn't masked + if pxl.scr.paused { + // cursor coordinates + x := int(pxl.lastX) + y := int(pxl.lastY) + + // cursor is one step ahead of pixel -- move to new scanline if + // necessary + if x >= television.ClocksPerScanline { + x = 0 + y++ + } + + // note whether cursor is "off-screen" (according to current masking) + offscreenCursorPos := false + + // adjust coordinates if pxleen is masked + if !pxl.unmasked { + x -= int(pxl.srcRect.X) + y -= int(pxl.srcRect.Y) + + if x < 0 { + offscreenCursorPos = true + x = 0 + } + if y < 0 { + offscreenCursorPos = true + y = 0 + } + } + + // cursor color depends on whether cursor is off-screen or not + if offscreenCursorPos { + pxl.renderer.SetDrawColor(100, 100, 255, 100) + } else { + pxl.renderer.SetDrawColor(255, 255, 255, 100) + } + pxl.renderer.SetDrawBlendMode(sdl.BlendMode(sdl.BLENDMODE_NONE)) + + // leave the current pixel visible at the top-left corner of the cursor + pxl.renderer.DrawRect(&sdl.Rect{X: int32(x + 1), Y: int32(y), W: 1, H: 1}) + pxl.renderer.DrawRect(&sdl.Rect{X: int32(x + 1), Y: int32(y + 1), W: 1, H: 1}) + pxl.renderer.DrawRect(&sdl.Rect{X: int32(x), Y: int32(y + 1), W: 1, H: 1}) + } + + pxl.renderer.Present() + + return nil +} + +func (pxl *pixels) newFrame() { + // swap pixel array with pixelsFade array + // -- note that we don't do this with the texture instead because + // updating the the extra texture if we don't need to (faded pixels + // only show when the emulation is paused) is expensive + swp := pxl.pixels + pxl.pixels = pxl.pixelsFade + pxl.pixelsFade = swp + + // clear pixels in overlay + pxl.metaPixels.newFrame() + + // swap pixel array with pixelsFade array + // -- see comment above + swp = pxl.altPixels + pxl.altPixels = pxl.altPixelsFade + pxl.altPixelsFade = swp + + // clear altpixels + for i := 0; i < len(pxl.altPixels); i++ { + pxl.altPixels[i] = 0 + } + + // clear regular pixels + for i := 0; i < len(pxl.pixels); i++ { + pxl.pixels[i] = 0 + } +} diff --git a/gui/sdldebug/requests.go b/gui/sdldebug/requests.go new file mode 100644 index 00000000..5400f48c --- /dev/null +++ b/gui/sdldebug/requests.go @@ -0,0 +1,103 @@ +package sdldebug + +import ( + "gopher2600/errors" + "gopher2600/gui" + + "github.com/veandco/go-sdl2/sdl" +) + +// SetFeature is used to set a television attribute +func (pxtv *SdlDebug) SetFeature(request gui.FeatureReq, args ...interface{}) (returnedErr error) { + // lazy (but clear) handling of type assertion errors + defer func() { + if r := recover(); r != nil { + returnedErr = errors.New(errors.PanicError, "sdl.SetFeature()", r) + } + }() + + switch request { + case gui.ReqSetVisibilityStable: + fallthrough + + case gui.ReqSetVisibility: + if args[0].(bool) { + pxtv.window.Show() + + // update screen + // -- default args[1] of true if not present + if len(args) < 2 || args[1].(bool) { + pxtv.pxl.update() + } + } else { + pxtv.window.Hide() + } + + case gui.ReqToggleVisibility: + if pxtv.window.GetFlags()&sdl.WINDOW_HIDDEN == sdl.WINDOW_HIDDEN { + pxtv.window.Show() + + // update screen + // -- default args[1] of true if not present + if len(args) < 2 || args[1].(bool) { + pxtv.pxl.update() + } + } else { + pxtv.window.Hide() + } + + case gui.ReqSetPause: + pxtv.paused = args[0].(bool) + pxtv.pxl.update() + + case gui.ReqSetMasking: + pxtv.pxl.setMasking(args[0].(bool)) + pxtv.pxl.update() + + case gui.ReqToggleMasking: + pxtv.pxl.setMasking(!pxtv.pxl.unmasked) + pxtv.pxl.update() + + case gui.ReqSetAltColors: + pxtv.pxl.useAltPixels = args[0].(bool) + pxtv.pxl.update() + + case gui.ReqToggleAltColors: + pxtv.pxl.useAltPixels = !pxtv.pxl.useAltPixels + pxtv.pxl.update() + + case gui.ReqSetOverlay: + pxtv.pxl.useMetaPixels = args[0].(bool) + pxtv.pxl.update() + + case gui.ReqToggleOverlay: + pxtv.pxl.useMetaPixels = !pxtv.pxl.useMetaPixels + pxtv.pxl.update() + + case gui.ReqSetScale: + pxtv.pxl.setScaling(args[0].(float32)) + pxtv.pxl.update() + + case gui.ReqIncScale: + if pxtv.pxl.pixelScaleY < 4.0 { + pxtv.pxl.setScaling(pxtv.pxl.pixelScaleY + 0.1) + pxtv.pxl.update() + } + + case gui.ReqDecScale: + if pxtv.pxl.pixelScaleY > 0.5 { + pxtv.pxl.setScaling(pxtv.pxl.pixelScaleY - 0.1) + pxtv.pxl.update() + } + + default: + return errors.New(errors.UnknownGUIRequest, request) + } + + return nil +} + +// SetEventChannel implements the GUI interface +func (pxtv *SdlDebug) SetEventChannel(eventChannel chan gui.Event) { + pxtv.eventChannel = eventChannel +} diff --git a/gui/sdl/pixeltv.go b/gui/sdldebug/sdldebug.go similarity index 54% rename from gui/sdl/pixeltv.go rename to gui/sdldebug/sdldebug.go index b8eafd7a..07de46ad 100644 --- a/gui/sdl/pixeltv.go +++ b/gui/sdldebug/sdldebug.go @@ -1,4 +1,4 @@ -package sdl +package sdldebug import ( "gopher2600/errors" @@ -9,19 +9,14 @@ import ( "github.com/veandco/go-sdl2/sdl" ) -// PixelTV is a simple SDL implementation of the television.Renderer interface -// with an embedded television for convenience. It treats every SetPixel() call -// as gospel - no refraction or blurring of adjacent pixels. It is imagined -// that other SDL implementations will be more imaginitive with SetPixel() and -// produce a more convincing image. -type PixelTV struct { +// SdlDebug is a simple SDL implementation of the television.Renderer interface +type SdlDebug struct { television.Television - // much of the sdl magic happens in the screen object - scr *screen + window *sdl.Window - // audio - snd *sound + // much of the sdl magic happens in the screen object + pxl *pixels // connects SDL guiLoop with the parent process eventChannel chan gui.Event @@ -30,24 +25,20 @@ type PixelTV struct { // as much of the current frame is displayed as possible; the previous // frame will take up the remainder of the screen. paused bool - - // ther's a small bug significant performance boost if we disable certain - // code paths with this allowDebugging flag - allowDebugging bool } -// NewPixelTV creates a new instance of PixelTV. For convenience, the +// NewSdlDebug creates a new instance of PixelTV. For convenience, the // television argument can be nil, in which case an instance of // StellaTelevision will be created. -func NewPixelTV(tvType string, scale float32, tv television.Television) (gui.GUI, error) { +func NewSdlDebug(tvType string, scale float32, tv television.Television) (gui.GUI, error) { var err error // set up gui - pxtv := new(PixelTV) + scr := new(SdlDebug) // create or attach television implementation if tv == nil { - pxtv.Television, err = television.NewStellaTelevision(tvType) + scr.Television, err = television.NewStellaTelevision(tvType) if err != nil { return nil, errors.New(errors.SDL, err) } @@ -61,7 +52,7 @@ func NewPixelTV(tvType string, scale float32, tv television.Television) (gui.GUI if tvType != "AUTO" && tvType != tv.GetSpec().ID { return nil, errors.New(errors.SDL, "trying to piggyback a tv of a different spec") } - pxtv.Television = tv + scr.Television = tv } // set up sdl @@ -70,100 +61,91 @@ func NewPixelTV(tvType string, scale float32, tv television.Television) (gui.GUI return nil, errors.New(errors.SDL, err) } - // initialise the screens we'll be using - pxtv.scr, err = newScreen(pxtv) + // SDL window - the correct size for the window will be determined below + scr.window, err = sdl.CreateWindow("Gopher2600", int32(sdl.WINDOWPOS_UNDEFINED), int32(sdl.WINDOWPOS_UNDEFINED), 0, 0, uint32(sdl.WINDOW_HIDDEN)) if err != nil { return nil, errors.New(errors.SDL, err) } - // initialise the sound system - pxtv.snd, err = newSound(pxtv) + // initialise the screens we'll be using + scr.pxl, err = newScreen(scr) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // set attributes that depend on the television specification + err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesPerVisible) if err != nil { return nil, errors.New(errors.SDL, err) } // set window size and scaling - err = pxtv.scr.setScaling(scale) + err = scr.pxl.setScaling(scale) if err != nil { return nil, errors.New(errors.SDL, err) } // register ourselves as a television.Renderer - pxtv.AddPixelRenderer(pxtv) - - // register ourselves as a television.AudioMixer - pxtv.AddAudioMixer(pxtv) + scr.AddPixelRenderer(scr) // update tv (with a black image) - err = pxtv.scr.update() + err = scr.pxl.update() if err != nil { return nil, errors.New(errors.SDL, err) } // gui events are serviced by a separate go rountine - go pxtv.guiLoop() + go scr.guiLoop() // note that we've elected not to show the window on startup // window is instead opened on a ReqSetVisibility request - return pxtv, nil + return scr, nil } -// ChangeTVSpec implements television.Television interface -func (pxtv *PixelTV) ChangeTVSpec() error { - pxtv.scr.stb.restart() - return pxtv.scr.initialiseScreen() +// Resize implements television.Television interface +func (scr *SdlDebug) Resize(topScanline, numScanlines int) error { + return scr.pxl.resize(topScanline, numScanlines) } // NewFrame implements television.Renderer interface -func (pxtv *PixelTV) NewFrame(frameNum int) error { - err := pxtv.scr.stb.stabiliseFrame() +func (scr *SdlDebug) NewFrame(frameNum int) error { + err := scr.pxl.update() if err != nil { return err } - err = pxtv.scr.update() - if err != nil { - return err - } - - pxtv.scr.newFrame() + scr.pxl.newFrame() return nil } // NewScanline implements television.Renderer interface -func (pxtv *PixelTV) NewScanline(scanline int) error { +func (scr *SdlDebug) NewScanline(scanline int) error { return nil } // SetPixel implements television.Renderer interface -func (pxtv *PixelTV) SetPixel(x, y int, red, green, blue byte, vblank bool) error { - return pxtv.scr.setRegPixel(int32(x), int32(y), red, green, blue, vblank) +func (scr *SdlDebug) SetPixel(x, y int, red, green, blue byte, vblank bool) error { + return scr.pxl.setRegPixel(int32(x), int32(y), red, green, blue, vblank) } // SetAltPixel implements television.Renderer interface -func (pxtv *PixelTV) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { - if !pxtv.allowDebugging { - return nil - } - return pxtv.scr.setAltPixel(int32(x), int32(y), red, green, blue, vblank) +func (scr *SdlDebug) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { + return scr.pxl.setAltPixel(int32(x), int32(y), red, green, blue, vblank) } // Reset implements television.Renderer interface -func (pxtv *PixelTV) Reset() error { - err := pxtv.Television.Reset() +func (scr *SdlDebug) Reset() error { + err := scr.Television.Reset() if err != nil { return err } - pxtv.scr.newFrame() - pxtv.scr.lastX = 0 - pxtv.scr.lastY = 0 - return nil + return scr.pxl.reset() } // IsVisible implements gui.GUI interface -func (pxtv PixelTV) IsVisible() bool { - flgs := pxtv.scr.window.GetFlags() +func (scr SdlDebug) IsVisible() bool { + flgs := scr.window.GetFlags() return flgs&sdl.WINDOW_SHOWN == sdl.WINDOW_SHOWN } diff --git a/gui/sdl/audio.go b/gui/sdlplay/audio.go similarity index 75% rename from gui/sdl/audio.go rename to gui/sdlplay/audio.go index 3eac3d15..ed043286 100644 --- a/gui/sdl/audio.go +++ b/gui/sdlplay/audio.go @@ -1,4 +1,4 @@ -package sdl +package sdlplay import ( "gopher2600/hardware/tia/audio" @@ -20,7 +20,7 @@ type sound struct { samples [16][32]*mix.Chunk } -func newSound(pxtv *PixelTV) (*sound, error) { +func newSound(scr *SdlPlay) (*sound, error) { snd := &sound{} // prerequisite: SDL_INIT_AUDIO must be included in the call to sdl.Init() @@ -72,31 +72,31 @@ func newSound(pxtv *PixelTV) (*sound, error) { } // SetAudio implements the television.AudioMixer interface -func (pxtv *PixelTV) SetAudio(aud audio.Audio) error { - if aud.Volume0 != pxtv.snd.prevAud.Volume0 { +func (scr *SdlPlay) SetAudio(aud audio.Audio) error { + if aud.Volume0 != scr.snd.prevAud.Volume0 { mix.Volume(0, int(aud.Volume0*8)) } - if aud.Volume1 != pxtv.snd.prevAud.Volume1 { + if aud.Volume1 != scr.snd.prevAud.Volume1 { mix.Volume(1, int(aud.Volume1*8)) } - if aud.Control0 != pxtv.snd.prevAud.Control0 || aud.Freq0 != pxtv.snd.prevAud.Freq0 { + if aud.Control0 != scr.snd.prevAud.Control0 || aud.Freq0 != scr.snd.prevAud.Freq0 { if aud.Control0 == 0 { mix.HaltChannel(0) } else { - pxtv.snd.samples[aud.Control0][31-aud.Freq0].Play(0, -1) + scr.snd.samples[aud.Control0][31-aud.Freq0].Play(0, -1) } } - if aud.Control1 != pxtv.snd.prevAud.Control1 || aud.Freq1 != pxtv.snd.prevAud.Freq1 { + if aud.Control1 != scr.snd.prevAud.Control1 || aud.Freq1 != scr.snd.prevAud.Freq1 { if aud.Control1 == 0 { mix.HaltChannel(1) } else { - pxtv.snd.samples[aud.Control1][31-aud.Freq1].Play(1, -1) + scr.snd.samples[aud.Control1][31-aud.Freq1].Play(1, -1) } } - pxtv.snd.prevAud = aud + scr.snd.prevAud = aud return nil } diff --git a/gui/sdlplay/guiloop.go b/gui/sdlplay/guiloop.go new file mode 100644 index 00000000..d91ef033 --- /dev/null +++ b/gui/sdlplay/guiloop.go @@ -0,0 +1,58 @@ +package sdlplay + +import ( + "gopher2600/gui" + + "github.com/veandco/go-sdl2/sdl" +) + +// guiLoop listens for SDL events and is run concurrently +func (scr *SdlPlay) guiLoop() { + for { + sdlEvent := sdl.WaitEvent() + switch sdlEvent := sdlEvent.(type) { + + // close window + case *sdl.QuitEvent: + scr.SetFeature(gui.ReqSetVisibility, false) + scr.eventChannel <- gui.Event{ID: gui.EventWindowClose} + + case *sdl.KeyboardEvent: + mod := gui.KeyModNone + + if sdl.GetModState()&sdl.KMOD_LALT == sdl.KMOD_LALT || + sdl.GetModState()&sdl.KMOD_RALT == sdl.KMOD_RALT { + mod = gui.KeyModAlt + } else if sdl.GetModState()&sdl.KMOD_LSHIFT == sdl.KMOD_LSHIFT || + sdl.GetModState()&sdl.KMOD_RSHIFT == sdl.KMOD_RSHIFT { + mod = gui.KeyModShift + } else if sdl.GetModState()&sdl.KMOD_LCTRL == sdl.KMOD_LCTRL || + sdl.GetModState()&sdl.KMOD_RCTRL == sdl.KMOD_RCTRL { + mod = gui.KeyModCtrl + } + + switch sdlEvent.Type { + case sdl.KEYDOWN: + if sdlEvent.Repeat == 0 { + scr.eventChannel <- gui.Event{ + ID: gui.EventKeyboard, + Data: gui.EventDataKeyboard{ + Key: sdl.GetKeyName(sdlEvent.Keysym.Sym), + Mod: mod, + Down: true}} + } + case sdl.KEYUP: + if sdlEvent.Repeat == 0 { + scr.eventChannel <- gui.Event{ + ID: gui.EventKeyboard, + Data: gui.EventDataKeyboard{ + Key: sdl.GetKeyName(sdlEvent.Keysym.Sym), + Mod: mod, + Down: false}} + } + } + + default: + } + } +} diff --git a/gui/sdlplay/requests.go b/gui/sdlplay/requests.go new file mode 100644 index 00000000..eee1bfd6 --- /dev/null +++ b/gui/sdlplay/requests.go @@ -0,0 +1,60 @@ +package sdlplay + +import ( + "gopher2600/errors" + "gopher2600/gui" + + "github.com/veandco/go-sdl2/sdl" +) + +// SetFeature is used to set a television attribute +func (scr *SdlPlay) SetFeature(request gui.FeatureReq, args ...interface{}) (returnedErr error) { + // lazy (but clear) handling of type assertion errors + defer func() { + if r := recover(); r != nil { + returnedErr = errors.New(errors.PanicError, "sdl.SetFeature()", r) + } + }() + + switch request { + case gui.ReqSetVisibilityStable: + if scr.IsStable() { + scr.showWindow(args[0].(bool)) + } else { + scr.showOnNextStable = true + } + + case gui.ReqSetVisibility: + scr.showWindow(args[0].(bool)) + + case gui.ReqToggleVisibility: + if scr.window.GetFlags()&sdl.WINDOW_HIDDEN == sdl.WINDOW_HIDDEN { + scr.window.Show() + } else { + scr.window.Hide() + } + + case gui.ReqSetScale: + scr.setScaling(args[0].(float32)) + + case gui.ReqIncScale: + if scr.scaleY < 4.0 { + scr.setScaling(scr.scaleY + 0.1) + } + + case gui.ReqDecScale: + if scr.scaleY > 0.5 { + scr.setScaling(scr.scaleY - 0.1) + } + + default: + return errors.New(errors.UnknownGUIRequest, request) + } + + return nil +} + +// SetEventChannel implements the GUI interface +func (scr *SdlPlay) SetEventChannel(eventChannel chan gui.Event) { + scr.eventChannel = eventChannel +} diff --git a/gui/sdlplay/sdlplay.go b/gui/sdlplay/sdlplay.go new file mode 100644 index 00000000..e61de2b4 --- /dev/null +++ b/gui/sdlplay/sdlplay.go @@ -0,0 +1,287 @@ +package sdlplay + +import ( + "gopher2600/errors" + "gopher2600/gui" + "gopher2600/performance/limiter" + "gopher2600/television" + "strings" + + "github.com/veandco/go-sdl2/sdl" +) + +const pixelDepth = 4 +const pixelWidth = 2.0 + +// SdlPlay is a simple SDL implementation of the television.Renderer interface +type SdlPlay struct { + television.Television + + // connects SDL guiLoop with the parent process + eventChannel chan gui.Event + + // limit screen updates to a fixed fps + lmtr *limiter.FpsLimiter + + // all audio is handled by the sound type + snd *sound + + // sdl stuff + window *sdl.Window + renderer *sdl.Renderer + texture *sdl.Texture + + // horizPixels and scanlines represent the *actual* value for the current + // ROM. many ROMs go beyond the spec and push the number of scanlines into + // the overscan area. the horizPixels value never changes. it is included + // for completeness and clarity + // + // these values are not the same as the window size. window size is scaled + // appropriately + horizPixels int32 + scanlines int32 + topScanline int + + // pixels is the byte array that we copy to the texture before applying to + // the renderer. it is equal to horizPixels * scanlines * pixelDepth. + pixels []byte + + // the amount of scaling applied to each pixel. X is adjusted by an aspect + // bias, defined in the television specs + scaleX float32 + scaleY float32 + + showOnNextStable bool +} + +// NewSdlPlay creates a new instance of SdlPlay. For convenience, the +// television argument can be nil, in which case an instance of +// StellaTelevision will be created. +func NewSdlPlay(tvType string, scale float32, tv television.Television) (gui.GUI, error) { + // set up gui + scr := &SdlPlay{} + + var err error + + // create or attach television implementation + if tv == nil { + scr.Television, err = television.NewStellaTelevision(tvType) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + } else { + // check that the quoted tvType matches the specification of the + // supplied BasicTelevision instance. we don't really need this but + // becuase we're implying that tvType is required, even when an + // instance of BasicTelevision has been supplied, the caller may be + // expecting an error + tvType = strings.ToUpper(tvType) + if tvType != "AUTO" && tvType != tv.GetSpec().ID { + return nil, errors.New(errors.SDL, "trying to piggyback a tv of a different spec") + } + scr.Television = tv + } + + // set up sdl + err = sdl.Init(sdl.INIT_EVERYTHING) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // SDL window - window size is set in Resize() function + scr.window, err = sdl.CreateWindow("Gopher2600", + int32(sdl.WINDOWPOS_UNDEFINED), int32(sdl.WINDOWPOS_UNDEFINED), + 0, 0, + uint32(sdl.WINDOW_HIDDEN)) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // sdl renderer. we set the scaling amount in the setScaling function later + // once we know what the tv specification is + scr.renderer, err = sdl.CreateRenderer(scr.window, -1, uint32(sdl.RENDERER_ACCELERATED)) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // initialise the sound system + scr.snd, err = newSound(scr) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // register ourselves as a television.Renderer + scr.AddPixelRenderer(scr) + + // register ourselves as a television.AudioMixer + scr.AddAudioMixer(scr) + + // change tv spec after window creation (so we can set the window size) + err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesPerVisible) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // set scaling to default value + err = scr.setScaling(scale) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + scr.lmtr, err = limiter.NewFPSLimiter(scr.GetSpec().FramesPerSecond) + if err != nil { + return nil, errors.New(errors.SDL, err) + } + + // gui events are serviced by a separate go rountine + go scr.guiLoop() + + // note that we've elected not to show the window on startup + // window is instead opened on a ReqSetVisibility request + + return scr, nil +} + +// Resize implements television.Television interface +func (scr *SdlPlay) Resize(topScanline, numScanlines int) error { + var err error + + scr.horizPixels = television.ClocksPerVisible + scr.scanlines = int32(numScanlines) + scr.topScanline = topScanline + scr.pixels = make([]byte, scr.horizPixels*scr.scanlines*pixelDepth) + + // preset alpha channel - we never change the value of this channel + for i := pixelDepth - 1; i < len(scr.pixels); i += pixelDepth { + scr.pixels[i] = 255 + } + + // texture is applied to the renderer to show the image. we copy the pixels + // to it every NewFrame() + // + // texture is the same size as the pixel arry. scaling will be applied to + // in order to fit it in the window + scr.texture, err = scr.renderer.CreateTexture(uint32(sdl.PIXELFORMAT_ABGR8888), + int(sdl.TEXTUREACCESS_STREAMING), + scr.horizPixels, + scr.scanlines) + if err != nil { + return nil + } + + scr.setScaling(-1) + + return nil +} + +// use scale of -1 to reapply existing scale value +func (scr *SdlPlay) setScaling(scale float32) error { + if scale >= 0 { + scr.scaleY = scale + scr.scaleX = scale * scr.GetSpec().AspectBias + } + + w := int32(float32(scr.horizPixels) * scr.scaleX * pixelWidth) + h := int32(float32(scr.scanlines) * scr.scaleY) + scr.window.SetSize(w, h) + + // make sure everything drawn through the renderer is correctly scaled + err := scr.renderer.SetScale(float32(w/scr.horizPixels), float32(h/scr.scanlines)) + if err != nil { + return err + } + + return nil +} + +// NewFrame implements television.Renderer interface +func (scr *SdlPlay) NewFrame(frameNum int) error { + if scr.showOnNextStable { + scr.showWindow(true) + scr.showOnNextStable = false + } + + scr.lmtr.Wait() + + err := scr.texture.Update(nil, scr.pixels, int(scr.horizPixels*pixelDepth)) + if err != nil { + return err + } + + err = scr.renderer.Copy(scr.texture, nil, nil) + if err != nil { + return err + } + + scr.renderer.Present() + + return nil +} + +// NewScanline implements television.Renderer interface +func (scr *SdlPlay) NewScanline(scanline int) error { + return nil +} + +// SetPixel implements television.Renderer interface +func (scr *SdlPlay) SetPixel(x, y int, red, green, blue byte, vblank bool) error { + if vblank { + // we could return immediately but if vblank is on inside the visible + // area we need to the set pixel to black, in case the vblank was off + // in the previous frame (for efficiency, we're not clearing the pixel + // array at the end of the frame) + red = 0 + green = 0 + blue = 0 + } + + // adjust pixels so we're only dealing with the visible range + x -= television.ClocksPerHblank + y -= scr.topScanline + + if x < 0 || y < 0 { + return nil + } + + i := (y*int(scr.horizPixels) + x) * pixelDepth + if i <= len(scr.pixels)-pixelDepth { + scr.pixels[i] = red + scr.pixels[i+1] = green + scr.pixels[i+2] = blue + } + + return nil +} + +// SetAltPixel implements television.Renderer interface +func (scr *SdlPlay) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { + return nil +} + +// SetMetaPixel recieves (and processes) additional emulator information from the emulator +func (scr *SdlPlay) SetMetaPixel(sig gui.MetaPixel) error { + return nil +} + +// Reset implements television.Renderer interface +func (scr *SdlPlay) Reset() error { + err := scr.Television.Reset() + if err != nil { + return err + } + return nil +} + +// IsVisible implements gui.GUI interface +func (scr SdlPlay) IsVisible() bool { + flgs := scr.window.GetFlags() + return flgs&sdl.WINDOW_SHOWN == sdl.WINDOW_SHOWN +} + +func (scr SdlPlay) showWindow(show bool) { + if show { + scr.window.Show() + } else { + scr.window.Hide() + } +} diff --git a/performance/check.go b/performance/check.go index 3e866df4..d33104f0 100644 --- a/performance/check.go +++ b/performance/check.go @@ -4,7 +4,7 @@ import ( "fmt" "gopher2600/errors" "gopher2600/gui" - "gopher2600/gui/sdl" + "gopher2600/gui/sdlplay" "gopher2600/hardware" "gopher2600/hardware/memory" "gopher2600/setup" @@ -21,7 +21,7 @@ func Check(output io.Writer, profile bool, display bool, tvType string, scaling // create the "correct" type of TV depending on whether the display flag is // set or not if display { - ftv, err = sdl.NewPixelTV(tvType, scaling, nil) + ftv, err = sdlplay.NewSdlPlay(tvType, scaling, nil) if err != nil { return errors.New(errors.PerformanceError, err) } diff --git a/performance/fps.go b/performance/fps.go index 86135d35..62f67cb1 100644 --- a/performance/fps.go +++ b/performance/fps.go @@ -6,6 +6,6 @@ import "gopher2600/television" // frames-per-second and the accuracy of that value as a percentage. func CalcFPS(ftv television.Television, numFrames int, duration float64) (fps float64, accuracy float64) { fps = float64(numFrames) / duration - accuracy = 100 * float64(numFrames) / (duration * ftv.GetSpec().FramesPerSecond) + accuracy = 100 * float64(numFrames) / (duration * float64(ftv.GetSpec().FramesPerSecond)) return fps, accuracy } diff --git a/performance/limiter/limiter.go b/performance/limiter/limiter.go index 436d3bc1..7eebe6cd 100644 --- a/performance/limiter/limiter.go +++ b/performance/limiter/limiter.go @@ -5,6 +5,9 @@ import ( "time" ) +// this is a really rough attempt at frame rate limiting. probably only any +// good if base performance of the machine is well above the required rate. + // FpsLimiter will trigger every frames per second type FpsLimiter struct { framesPerSecond int diff --git a/playmode/play.go b/playmode/play.go index b0001ca3..783cfdc3 100644 --- a/playmode/play.go +++ b/playmode/play.go @@ -4,7 +4,7 @@ import ( "fmt" "gopher2600/errors" "gopher2600/gui" - "gopher2600/gui/sdl" + "gopher2600/gui/sdlplay" "gopher2600/hardware" "gopher2600/hardware/memory" "gopher2600/recorder" @@ -26,7 +26,7 @@ func Play(tvType string, scaling float32, stable bool, transcript string, newRec return errors.New(errors.PlayError, "specified cartridge is a playback file. use -recording flag") } - playtv, err := sdl.NewPixelTV(tvType, scaling, nil) + playtv, err := sdlplay.NewSdlPlay(tvType, scaling, nil) if err != nil { return errors.New(errors.PlayError, err) } diff --git a/television/renderers/digesttv.go b/television/renderers/digesttv.go index ccc488dc..a887f088 100644 --- a/television/renderers/digesttv.go +++ b/television/renderers/digesttv.go @@ -55,16 +55,13 @@ func NewDigestTV(tvType string, tv television.Television) (*DigestTV, error) { dtv.AddPixelRenderer(dtv) // set attributes that depend on the television specification - dtv.ChangeTVSpec() + dtv.Resize(-1, -1) return dtv, nil } -// ChangeTVSpec implements television.Television interface -func (dtv *DigestTV) ChangeTVSpec() error { - // memory for frameData has to be sufficient for the entirety of the - // screen plus the size of a fingerprint. we'll use the additional space to - // chain fingerprint hashes +// Resize implements television.Television interface +func (dtv *DigestTV) Resize(_, _ int) error { dtv.frameData = make([]byte, len(dtv.digest)+((television.ClocksPerScanline+1)*(dtv.GetSpec().ScanlinesTotal+1)*3)) return nil } diff --git a/television/renderers/imagetv.go b/television/renderers/imagetv.go index cb0d6d21..438a8f9a 100644 --- a/television/renderers/imagetv.go +++ b/television/renderers/imagetv.go @@ -18,8 +18,6 @@ import ( type ImageTV struct { television.Television - pixelWidth int - screenGeom image.Rectangle // currFrameData is the image we write to, until newFrame() is called again @@ -31,6 +29,8 @@ type ImageTV struct { lastFrameNum int } +const pixelWidth = 2 + // NewImageTV initialises a new instance of ImageTV. For convenience, the // television argument can be nil, in which case an instance of // StellaTelevision will be created. @@ -58,7 +58,7 @@ func NewImageTV(tvType string, tv television.Television) (*ImageTV, error) { } // set attributes that depend on the television specification - imtv.ChangeTVSpec() + imtv.Resize(imtv.GetSpec().ScanlineTop, imtv.GetSpec().ScanlineBottom) // start a new frame imtv.currFrameNum = -1 // we'll be adding 1 to this value immediately in newFrame() @@ -73,12 +73,11 @@ func NewImageTV(tvType string, tv television.Television) (*ImageTV, error) { return imtv, nil } -// ChangeTVSpec implements television.Television interface -func (imtv *ImageTV) ChangeTVSpec() error { - imtv.pixelWidth = 2 +// Resize implements television.Television interface +func (imtv *ImageTV) Resize(topScanline, numScanlines int) error { imtv.screenGeom = image.Rectangle{ Min: image.Point{X: 0, Y: 0}, - Max: image.Point{X: television.ClocksPerScanline * imtv.pixelWidth, Y: imtv.GetSpec().ScanlinesTotal}, + Max: image.Point{X: television.ClocksPerScanline * pixelWidth, Y: numScanlines}, } return nil } @@ -148,8 +147,8 @@ func (imtv *ImageTV) NewScanline(scanline int) error { // SetPixel implements television.Renderer interface func (imtv *ImageTV) SetPixel(x, y int, red, green, blue byte, vblank bool) error { col := color.NRGBA{R: red, G: green, B: blue, A: 255} - imtv.currFrameData.Set(x*imtv.pixelWidth, y, col) - imtv.currFrameData.Set(x*imtv.pixelWidth+1, y, col) + imtv.currFrameData.Set(x*pixelWidth, y, col) + imtv.currFrameData.Set(x*pixelWidth+1, y, col) return nil } diff --git a/television/specifications.go b/television/specifications.go index c376fe69..1fcda883 100644 --- a/television/specifications.go +++ b/television/specifications.go @@ -10,9 +10,12 @@ type Specification struct { ScanlinesPerOverscan int ScanlinesTotal int + ScanlineTop int + ScanlineBottom int + Colors colors - FramesPerSecond float64 + FramesPerSecond int SecondsPerFrame float64 // AspectBias transforms the scaling factor for the X axis. @@ -42,8 +45,10 @@ func init() { SpecNTSC.ScanlinesPerVisible = 192 SpecNTSC.ScanlinesPerOverscan = 30 SpecNTSC.ScanlinesTotal = 262 - SpecNTSC.FramesPerSecond = 60.0 - SpecNTSC.SecondsPerFrame = 1.0 / SpecNTSC.FramesPerSecond + SpecNTSC.ScanlineTop = SpecNTSC.ScanlinesPerVBlank + SpecNTSC.ScanlinesPerVSync + SpecNTSC.ScanlineBottom = SpecNTSC.ScanlinesTotal - SpecNTSC.ScanlinesPerOverscan + SpecNTSC.FramesPerSecond = 60 + SpecNTSC.SecondsPerFrame = 1.0 / float64(SpecNTSC.FramesPerSecond) SpecNTSC.Colors = colorsNTSC SpecPAL = new(Specification) @@ -53,13 +58,14 @@ func init() { SpecPAL.ScanlinesPerVisible = 228 SpecPAL.ScanlinesPerOverscan = 36 SpecPAL.ScanlinesTotal = 312 - SpecPAL.FramesPerSecond = 50.0 - SpecPAL.SecondsPerFrame = 1.0 / SpecPAL.FramesPerSecond + SpecPAL.ScanlineTop = SpecPAL.ScanlinesPerVBlank + SpecPAL.ScanlinesPerVSync + SpecPAL.ScanlineBottom = SpecPAL.ScanlinesTotal - SpecPAL.ScanlinesPerOverscan + SpecPAL.FramesPerSecond = 50 + SpecPAL.SecondsPerFrame = 1.0 / float64(SpecPAL.FramesPerSecond) SpecPAL.Colors = colorsPAL // AaspectBias transforms the scaling factor for the X axis. - // values taken from Stella emualtor. i've no idea from where these values - // were originated but they're useful for A/B testing + // values taken from Stella emualtor. useful for A/B testing SpecNTSC.AspectBias = 0.91 SpecPAL.AspectBias = 1.09 } diff --git a/television/stella.go b/television/stella.go index b386bdd7..8a8e7415 100644 --- a/television/stella.go +++ b/television/stella.go @@ -37,9 +37,6 @@ type StellaTelevision struct { frameNum int // - the current scanline number scanline int - // - the number of scanlines past the specification limit. used to - // trigger a change of tv specification - extraScanlines int // record of signal attributes from the last call to Signal() prevSignal SignalAttributes @@ -49,25 +46,38 @@ type StellaTelevision struct { vsyncCount int vsyncPos int - // the scanline at which the visible part of the screen begins and ends - // - we start off with ideal values and push the screen outwards as - // required - visibleTop int - visibleBottom int - - // thisVisibleTop/Bottom records visible part of the screen (as described - // above) during the current frame. we use these to update the real - // variables at the end of a frame - thisVisibleTop int - thisVisibleBottom int - // list of renderer implementations to consult renderers []PixelRenderer // list of audio mixers to consult mixers []AudioMixer + + // the following values are used for stability detection. we could possibly + // define a separate type for all of these. + + // top and bottom of screen as detected by vblank/color signal + top int + bottom int + + // new top and bottom values if stability threshold is met + speculativeTop int + speculativeBottom int + + // top and bottom as reckoned by the current frame - reset at the moment + // when a new frame is detected + thisTop int + thisBottom int + + // a frame has to be stable (speculative top and bottom unchanged) for a + // number of frames (stable threshold) before we accept that it is a true + // representation of frame dimensions + stability int } +// the number of frames that (speculative) top and bottom values must be steady +// before we accept the frame characteristics +const stabilityThreshold = 5 + // NewStellaTelevision creates a new instance of StellaTelevision for a // minimalist implementation of a televsion for the VCS emulation func NewStellaTelevision(tvType string) (*StellaTelevision, error) { @@ -100,24 +110,21 @@ func NewStellaTelevision(tvType string) (*StellaTelevision, error) { func (btv StellaTelevision) String() string { s := strings.Builder{} s.WriteString(fmt.Sprintf("FR=%d SL=%d", btv.frameNum, btv.scanline)) - if btv.extraScanlines > 0 { - s.WriteString(fmt.Sprintf(" [%d]", btv.extraScanlines)) - } s.WriteString(fmt.Sprintf(" HP=%d", btv.horizPos)) return s.String() } -// AddPixelRenderer adds a renderer implementation to the list +// AddPixelRenderer implements the Television interface func (btv *StellaTelevision) AddPixelRenderer(r PixelRenderer) { btv.renderers = append(btv.renderers, r) } -// AddAudioMixer adds a renderer implementation to the list +// AddAudioMixer implements the Television interface func (btv *StellaTelevision) AddAudioMixer(m AudioMixer) { btv.mixers = append(btv.mixers, m) } -// Reset all the values for the television +// Reset implements the Television interface func (btv *StellaTelevision) Reset() error { btv.horizPos = -ClocksPerHblank btv.frameNum = 0 @@ -125,33 +132,13 @@ func (btv *StellaTelevision) Reset() error { btv.vsyncCount = 0 btv.prevSignal = SignalAttributes{Pixel: VideoBlack} - // default top/bottom to the "ideal" values - btv.thisVisibleTop = btv.spec.ScanlinesTotal - btv.thisVisibleBottom = 0 + btv.top = btv.spec.ScanlineTop + btv.bottom = btv.spec.ScanlineBottom return nil } -func (btv *StellaTelevision) autoSpec() (bool, error) { - if !btv.auto { - return false, nil - } - - if btv.spec == SpecPAL { - return false, nil - } - - btv.spec = SpecPAL - for f := range btv.renderers { - err := btv.renderers[f].ChangeTVSpec() - if err != nil { - return false, err - } - } - return true, nil -} - -// Signal is principle method of communication between the VCS and televsion +// Signal implements the Television interface func (btv *StellaTelevision) Signal(sig SignalAttributes) error { // the following condition detects a new scanline by looking for the // non-textbook HSyncSimple signal @@ -191,12 +178,8 @@ func (btv *StellaTelevision) Signal(sig SignalAttributes) error { } } } else { - // if we're above the scanline limit for the specification then don't - // notify the renderers of a new scanline, instead repeat drawing to - // the last scanline and note the number of "extra" scanlines we've - // encountered + // repeat last scanline over and over btv.scanline = btv.spec.ScanlinesTotal - btv.extraScanlines++ } } else { @@ -221,59 +204,16 @@ func (btv *StellaTelevision) Signal(sig SignalAttributes) error { // if vsync has just be turned off then check that it has been held for // the requisite number of scanlines for a new frame to be started if btv.vsyncCount >= btv.spec.ScanlinesPerVSync { - btv.frameNum++ - btv.scanline = 0 - btv.extraScanlines = 0 - - // record visible top/bottom for this frame - btv.visibleTop = btv.thisVisibleTop - btv.visibleBottom = btv.thisVisibleBottom - - // call new frame for all renderers - for f := range btv.renderers { - err := btv.renderers[f].NewFrame(btv.frameNum) - if err != nil { - return err - } + err := btv.newFrame() + if err != nil { + return err } - - // default top/bottom to the "ideal" values - btv.thisVisibleTop = btv.spec.ScanlinesTotal - btv.thisVisibleBottom = 0 } + // reset vsync counter when vsync signal is dropped btv.vsyncCount = 0 } - // push screen limits outwards as required - if !sig.VBlank { - if btv.scanline > btv.thisVisibleBottom { - btv.thisVisibleBottom = btv.scanline - - // keep within limits - if btv.thisVisibleBottom > btv.spec.ScanlinesTotal { - btv.thisVisibleBottom = btv.spec.ScanlinesTotal - } - } - if btv.scanline < btv.thisVisibleTop { - btv.thisVisibleTop = btv.scanline - } - } - - // after the first frame, if there are "extra" scanlines then try changing - // the tv specification. - // - // we are currently defining "extra" as 10. one extra scanline is too few. - // for example, when using a value of one, the Fatal Run ROM experiences a - // false change from NTSC to PAL between the resume/new screen and the game - // "intro" screen. 10 is maybe too high but it's good for now. - if btv.frameNum > 1 && btv.extraScanlines > 10 { - _, err := btv.autoSpec() - if err != nil { - return err - } - } - // record the current signal settings so they can be used for reference btv.prevSignal = sig @@ -290,6 +230,16 @@ func (btv *StellaTelevision) Signal(sig SignalAttributes) error { } } + // push screen boundaries outward using vblank and color signal to help us + if !sig.VBlank && red != 0 && green != 0 && blue != 0 { + if btv.scanline < btv.thisTop { + btv.thisTop = btv.scanline + } + if btv.scanline > btv.thisBottom { + btv.thisBottom = btv.scanline + } + } + // decode color using the alternative color signal red, green, blue = getAltColor(sig.AltPixel) for f := range btv.renderers { @@ -312,7 +262,70 @@ func (btv *StellaTelevision) Signal(sig SignalAttributes) error { return nil } -// GetState returns the value for the named state. eg. the current frame number +func (btv *StellaTelevision) stabilise() (bool, error) { + if btv.frameNum <= 1 || (btv.thisTop == btv.top && btv.thisBottom == btv.bottom) { + return false, nil + } + + // if top and bottom has changed this frame update speculative values + if btv.thisTop != btv.speculativeTop || btv.thisBottom != btv.speculativeBottom { + btv.speculativeTop = btv.thisTop + btv.speculativeBottom = btv.thisBottom + return false, nil + } + + // increase stability value until we reach threshold + if !btv.IsStable() { + btv.stability++ + return false, nil + } + + // accept speculative values + btv.top = btv.speculativeTop + btv.bottom = btv.speculativeBottom + + if btv.spec == SpecNTSC && btv.bottom-btv.top >= SpecPAL.ScanlinesPerVisible { + btv.spec = SpecPAL + + // reset top/bottom to ideals of new spec. they may of course be + // pushed outward in subsequent frames + btv.top = btv.spec.ScanlineTop + btv.bottom = btv.spec.ScanlineBottom + } + + for f := range btv.renderers { + err := btv.renderers[f].Resize(btv.top, btv.bottom-btv.top+1) + if err != nil { + return false, err + } + } + return true, nil +} + +func (btv *StellaTelevision) newFrame() error { + _, err := btv.stabilise() + if err != nil { + return err + } + + // new frame + btv.frameNum++ + btv.scanline = 0 + btv.thisTop = btv.top + btv.thisBottom = btv.bottom + + // call new frame for all renderers + for f := range btv.renderers { + err = btv.renderers[f].NewFrame(btv.frameNum) + if err != nil { + return err + } + } + + return nil +} + +// GetState implements the Television interface func (btv *StellaTelevision) GetState(request StateReq) (int, error) { switch request { default: @@ -323,14 +336,15 @@ func (btv *StellaTelevision) GetState(request StateReq) (int, error) { return btv.scanline, nil case ReqHorizPos: return btv.horizPos, nil - case ReqVisibleTop: - return btv.visibleTop, nil - case ReqVisibleBottom: - return btv.visibleBottom, nil } } -// GetSpec returns the television specification +// GetSpec implements the Television interface func (btv StellaTelevision) GetSpec() *Specification { return btv.spec } + +// IsStable implements the Television interface +func (btv StellaTelevision) IsStable() bool { + return btv.stability >= stabilityThreshold +} diff --git a/television/television.go b/television/television.go index f892aba9..788b7eaa 100644 --- a/television/television.go +++ b/television/television.go @@ -20,15 +20,20 @@ type Television interface { // Returns the value of the requested state. eg. the current scanline. GetState(StateReq) (int, error) - // Returns the current specification the television is operating under + // Returns the television's current specification. Renderers should use + // GetSpec() rather than keeping a private pointer to the specification. GetSpec() *Specification + + // IsStable returns true if the television thinks the image being sent by + // the VCS is stable + IsStable() bool } // PixelRenderer implementations displays, or otherwise works with, visal // information from a television // // examples of renderers that display visual information: -// * SDL/PixelTV +// * SDLPlay // * ImageTV // // examples of renderers that do not display visual information but only work @@ -45,6 +50,22 @@ type Television interface { // ... // } type PixelRenderer interface { + // Resize is called when the television implementation detects that extra + // scanlines are required in the display. + // + // It may be called when television specification has changed. Renderers + // should use GetSpec() rather than keeping a private pointer to the + // specification. + // + // Renderers should use the values sent by the Resize() function, rather + // than the equivalent values in the specification. Unless of course, the + // renderer is intended to be strict about specification accuracy. + // + // Renderers should also make sure that any data structures that depend on + // the specification being used are still adequate. + Resize(topScanline, visibleScanlines int) error + + // NewFrame and NewScanline are called at the start of the frame/scanline NewFrame(frameNum int) error NewScanline(scanline int) error @@ -74,12 +95,6 @@ type PixelRenderer interface { // SetPixel(x, y int, red, green, blue byte, vblank bool) error SetAltPixel(x, y int, red, green, blue byte, vblank bool) error - - // ChangeTVSpec is called when the television implementation decides to - // change which TV specification is being used. Renderer implementations - // should make sure that any data structures that depend on the - // specification being used are still adequate. - ChangeTVSpec() error } // AudioMixer implementations work with sound; most probably playing it. @@ -132,6 +147,4 @@ const ( ReqFramenum StateReq = iota ReqScanline ReqHorizPos - ReqVisibleTop - ReqVisibleBottom ) diff --git a/test/gopher2600_test.go b/test/gopher2600_test.go index 5ab2fbac..a1725683 100644 --- a/test/gopher2600_test.go +++ b/test/gopher2600_test.go @@ -3,7 +3,7 @@ package main_test import ( "fmt" "gopher2600/gui" - "gopher2600/gui/sdl" + "gopher2600/gui/sdldebug" "gopher2600/hardware" "gopher2600/hardware/memory" "testing" @@ -12,7 +12,7 @@ import ( func BenchmarkSDL(b *testing.B) { var err error - tv, err := sdl.NewPixelTV("NTSC", 1.0, nil) + tv, err := sdldebug.NewSdlDebug("NTSC", 1.0, nil) if err != nil { panic(fmt.Errorf("error preparing television: %s", err)) } diff --git a/web2600/src/canvas.go b/web2600/src/canvas.go index 40151e03..3e738f48 100644 --- a/web2600/src/canvas.go +++ b/web2600/src/canvas.go @@ -6,12 +6,11 @@ package main import ( "encoding/base64" "gopher2600/television" - "strconv" "syscall/js" "time" ) -const screenDepth = 4 +const pixelDepth = 4 const pixelWidth = 2 const horizScale = 2 const vertScale = 2 @@ -22,11 +21,9 @@ type CanvasTV struct { worker js.Value television.Television - spec *television.Specification width int height int - - screenTop int + top int image []byte } @@ -35,24 +32,45 @@ type CanvasTV struct { func NewCanvasTV(worker js.Value) *CanvasTV { var err error - ctv := CanvasTV{worker: worker} + scr := CanvasTV{worker: worker} - ctv.Television, err = television.NewStellaTelevision("NTSC") + scr.Television, err = television.NewStellaTelevision("NTSC") if err != nil { return nil } - ctv.Television.AddPixelRenderer(&ctv) - ctv.ChangeTVSpec() + scr.Television.AddPixelRenderer(&scr) - return &ctv + // change tv spec after window creation (so we can set the window size) + err = scr.Resize(scr.GetSpec().ScanlineTop, scr.GetSpec().ScanlinesPerVisible) + if err != nil { + return nil + } + + return &scr +} + +func (scr *CanvasTV) Resize(topScanline, numScanlines int) error { + scr.top = topScanline + scr.height = numScanlines * vertScale + + // strictly, only the height will ever change on a specification change but + // it's convenient to set the width too + scr.width = television.ClocksPerVisible * pixelWidth * horizScale + + // recreate image buffer of correct length + scr.image = make([]byte, scr.width*scr.height*pixelDepth) + + // resize HTML canvas + scr.worker.Call("updateCanvasSize", scr.width, scr.height) + + return nil } // NewFrame implements telvision.PixelRenderer -func (ctv *CanvasTV) NewFrame(frameNum int) error { - ctv.worker.Call("updateDebug", "frameNum", frameNum) - encodedImage := base64.StdEncoding.EncodeToString(ctv.image) - ctv.worker.Call("updateCanvas", encodedImage) - ctv.screenTop = -1 +func (scr *CanvasTV) NewFrame(frameNum int) error { + scr.worker.Call("updateDebug", "frameNum", frameNum) + encodedImage := base64.StdEncoding.EncodeToString(scr.image) + scr.worker.Call("updateCanvas", encodedImage) // give way to messageHandler - there must be a more elegant way of doing this time.Sleep(1 * time.Millisecond) @@ -61,49 +79,41 @@ func (ctv *CanvasTV) NewFrame(frameNum int) error { } // NewScanline implements telvision.PixelRenderer -func (ctv *CanvasTV) NewScanline(scanline int) error { - ctv.worker.Call("updateDebug", "scanline", scanline) +func (scr *CanvasTV) NewScanline(scanline int) error { + scr.worker.Call("updateDebug", "scanline", scanline) return nil } // SetPixel implements telvision.PixelRenderer -func (ctv *CanvasTV) SetPixel(x, y int, red, green, blue byte, vblank bool) error { +func (scr *CanvasTV) SetPixel(x, y int, red, green, blue byte, vblank bool) error { + if vblank { + // we could return immediately but if vblank is on inside the visible + // area we need to the set pixel to black, in case the vblank was off + // in the previous frame (for efficiency, we're not clearing the pixel + // array at the end of the frame) + red = 0 + green = 0 + blue = 0 + } + // adjust pixels so we're only dealing with the visible range x -= television.ClocksPerHblank - if x < 0 { + y -= scr.top + + if x < 0 || y < 0 { return nil } - // we need to be careful how we treat VBLANK signals. some ROMs use VBLANK - // as a cheap way of showing a black pixel. so, at the start of every new - // frame we set the following to -1 and then to the current scanline at the - // moment VBLANK is turned of for the first time that frame. - if !vblank { - if ctv.screenTop == -1 { - ctv.screenTop = y - } - } else { - if ctv.screenTop == -1 { - return nil - } else { - red = 0 - green = 0 - blue = 0 - } - } - - y -= ctv.screenTop - - baseIdx := screenDepth * (y*vertScale*ctv.width + x*pixelWidth*horizScale) - if baseIdx < len(ctv.image)-screenDepth && baseIdx >= 0 { + baseIdx := pixelDepth * (y*vertScale*scr.width + x*pixelWidth*horizScale) + if baseIdx <= len(scr.image)-pixelDepth && baseIdx >= 0 { for h := 0; h < vertScale; h++ { - vertAdj := h * (ctv.width * pixelWidth * horizScale) + vertAdj := h * (scr.width * pixelWidth * horizScale) for w := 0; w < pixelWidth*horizScale; w++ { - horizAdj := baseIdx + (w * screenDepth) + vertAdj - ctv.image[horizAdj] = red - ctv.image[horizAdj+1] = green - ctv.image[horizAdj+2] = blue - ctv.image[horizAdj+3] = 255 + horizAdj := baseIdx + (w * pixelDepth) + vertAdj + scr.image[horizAdj] = red + scr.image[horizAdj+1] = green + scr.image[horizAdj+2] = blue + scr.image[horizAdj+3] = 255 } } } @@ -112,24 +122,6 @@ func (ctv *CanvasTV) SetPixel(x, y int, red, green, blue byte, vblank bool) erro } // SetAltPixel implements telvision.PixelRenderer -func (ctv *CanvasTV) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { - return nil -} - -// ChangeTVSpec implements telvision.PixelRenderer -func (ctv *CanvasTV) ChangeTVSpec() error { - ctv.spec = ctv.Television.GetSpec() - ctv.height = ctv.spec.ScanlinesPerVisible * vertScale - - // strictly, only the height will ever change on a specification change but - // it's convenient to set the width too - ctv.width = television.ClocksPerVisible * pixelWidth * horizScale - - // recreate image buffer of correct length - ctv.image = make([]byte, ctv.width*ctv.height*screenDepth) - - // resize HTML canvas - ctv.worker.Call("updateCanvasSize", strconv.Itoa(ctv.width), strconv.Itoa(ctv.height)) - +func (scr *CanvasTV) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { return nil }