- implemented SdlPlay
    - simplified and more efficient SDL interface
    - renamed PixelTV to SdlDebug
    - SdlDebug implies debugging is allowed so removed AllowDebugging
	request from gui interface
    - removed stability code from SdlDebug

o television
    - added stability detection to base television implementation
    - added top/bottom scanline figures to specification types. more
	intuitive to work with in some contexts
This commit is contained in:
steve 2019-11-08 20:49:21 +00:00
parent 0a4856d3b6
commit 2aadee3158
25 changed files with 1175 additions and 958 deletions

View file

@ -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)

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

365
gui/sdldebug/pixels.go Normal file
View file

@ -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
}
}

103
gui/sdldebug/requests.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

58
gui/sdlplay/guiloop.go Normal file
View file

@ -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:
}
}
}

60
gui/sdlplay/requests.go Normal file
View file

@ -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
}

287
gui/sdlplay/sdlplay.go Normal file
View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
)

View file

@ -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))
}

View file

@ -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
}