mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
o sdl
- 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:
parent
0a4856d3b6
commit
2aadee3158
25 changed files with 1175 additions and 958 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
365
gui/sdldebug/pixels.go
Normal 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
103
gui/sdldebug/requests.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
58
gui/sdlplay/guiloop.go
Normal 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
60
gui/sdlplay/requests.go
Normal 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
287
gui/sdlplay/sdlplay.go
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue