mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 03:58:02 -04:00
add visual comparison tool. command line option and gui
userinput for two emulations synced by RIOT ports. RIOT port driver/passenger synchronisation ensures user input is seen by the emulations at the same time (relative to the emulation's television) does not yet handle RNGs (randomise on startup or the RNG in the DPC or DPC+ formats yet). we need to add a context type first
This commit is contained in:
parent
56a0d6a4dd
commit
0e3ec6a5d5
378
comparison/comparison.go
Normal file
378
comparison/comparison.go
Normal file
|
@ -0,0 +1,378 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package comparison
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/emulation"
|
||||
"github.com/jetsetilly/gopher2600/hardware"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/signal"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
||||
)
|
||||
|
||||
// Comparison type runs a parallel emulation with the intention of comparing
|
||||
// the output with the driver emulation.
|
||||
type Comparison struct {
|
||||
VCS *hardware.VCS
|
||||
|
||||
frameInfo television.FrameInfo
|
||||
|
||||
img *image.RGBA
|
||||
cropImg *image.RGBA
|
||||
diffImg *image.RGBA
|
||||
cropDiffImg *image.RGBA
|
||||
|
||||
isEmulating atomic.Value
|
||||
emulationQuit chan bool
|
||||
emulationCompleted chan bool
|
||||
|
||||
Render chan *image.RGBA
|
||||
DiffRender chan *image.RGBA
|
||||
|
||||
// pixel renderer implementation for the "driver" emulation. ie. the
|
||||
// emulation we'll be comparing against
|
||||
driver driver
|
||||
}
|
||||
|
||||
// NewComparison is the preferred method of initialisation for the Comparison type.
|
||||
func NewComparison(driver *hardware.VCS) (*Comparison, error) {
|
||||
cmp := &Comparison{
|
||||
emulationQuit: make(chan bool, 1),
|
||||
emulationCompleted: make(chan bool, 1),
|
||||
Render: make(chan *image.RGBA, 1),
|
||||
DiffRender: make(chan *image.RGBA, 1),
|
||||
}
|
||||
|
||||
// emulation has completed, by definition, on startup
|
||||
cmp.emulationCompleted <- true
|
||||
|
||||
// set isEmulating atomic as a boolean
|
||||
cmp.isEmulating.Store(false)
|
||||
|
||||
// create a new television. this will be used during the initialisation of
|
||||
// the VCS and not referred to directly again
|
||||
tv, err := television.NewTelevision("AUTO")
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("comparison: %v", err)
|
||||
}
|
||||
tv.AddPixelRenderer(cmp)
|
||||
tv.SetFPSCap(true)
|
||||
|
||||
// create a new VCS instance
|
||||
cmp.VCS, err = hardware.NewVCS(tv)
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("comparison: %v", err)
|
||||
}
|
||||
|
||||
cmp.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
|
||||
cmp.diffImg = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
|
||||
|
||||
// start with a NTSC television as default
|
||||
cmp.Resize(television.NewFrameInfo(specification.SpecNTSC))
|
||||
cmp.Reset()
|
||||
|
||||
// create driver
|
||||
cmp.driver = newDriver()
|
||||
driver.TV.AddPixelRenderer(&cmp.driver)
|
||||
|
||||
// synchronise RIOT ports
|
||||
sync := make(chan ports.DrivenEvent, 32)
|
||||
err = cmp.VCS.RIOT.Ports.SynchroniseWithDriver(sync, tv)
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("comparison: %v", err)
|
||||
}
|
||||
err = driver.RIOT.Ports.SynchroniseWithPassenger(sync, driver.TV)
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("comparison: %v", err)
|
||||
}
|
||||
|
||||
return cmp, nil
|
||||
}
|
||||
|
||||
func (cmp *Comparison) String() string {
|
||||
cart := cmp.VCS.Mem.Cart
|
||||
s := strings.Builder{}
|
||||
s.WriteString(fmt.Sprintf("%s (%s cartridge)", cart.ShortName, cart.ID()))
|
||||
if cc := cart.GetContainer(); cc != nil {
|
||||
s.WriteString(fmt.Sprintf(" [in %s]", cc.ContainerID()))
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// IsEmulating returns true if the comparison emulator is working. Useful for
|
||||
// testing whether the cartridgeloader was an emulatable file.
|
||||
func (cmp *Comparison) IsEmulating() bool {
|
||||
return cmp.isEmulating.Load().(bool)
|
||||
}
|
||||
|
||||
// EndCreation ends a running emulation that is creating a stream of comparison
|
||||
// images. Safe to use even when no emulation is running.
|
||||
func (cmp *Comparison) EndCreation() {
|
||||
select {
|
||||
case cmp.emulationQuit <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFromLoader will cause images to be generated from a running emulation
|
||||
// initialised with the specified cartridge loader.
|
||||
func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error {
|
||||
if cmp.IsEmulating() {
|
||||
return curated.Errorf("comparison: emulation already running")
|
||||
}
|
||||
|
||||
// label cartridge loader as coming from the comparison emulation
|
||||
cartload.EmulationLabel = emulation.ComparisonLabel
|
||||
|
||||
// loading hook support required for supercharger
|
||||
cartload.VCSHook = func(cart mapper.CartMapper, event mapper.Event, args ...interface{}) error {
|
||||
if _, ok := cart.(*supercharger.Supercharger); ok {
|
||||
switch event {
|
||||
case mapper.EventSuperchargerFastloadEnded:
|
||||
// the supercharger ROM will eventually start execution from the PC
|
||||
// address given in the supercharger file
|
||||
|
||||
// CPU execution has been interrupted. update state of CPU
|
||||
cmp.VCS.CPU.Interrupted = true
|
||||
|
||||
// the interrupted CPU means it never got a chance to
|
||||
// finalise the result. we force that here by simply
|
||||
// setting the Final flag to true.
|
||||
cmp.VCS.CPU.LastResult.Final = true
|
||||
|
||||
// call function to complete tape loading procedure
|
||||
callback := args[0].(supercharger.FastLoaded)
|
||||
err := callback(cmp.VCS.CPU, cmp.VCS.Mem.RAM, cmp.VCS.RIOT.Timer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case mapper.EventSuperchargerSoundloadEnded:
|
||||
return cmp.VCS.TV.Reset(true)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
cmp.emulationCompleted <- true
|
||||
cmp.isEmulating.Store(false)
|
||||
}()
|
||||
|
||||
err := cmp.VCS.AttachCartridge(cartload)
|
||||
if err != nil {
|
||||
cmp.driver.quit <- err
|
||||
return
|
||||
}
|
||||
|
||||
// if we get to this point then we can be reasonably sure that the
|
||||
// cartridgeloader is emulatable
|
||||
cmp.isEmulating.Store(true)
|
||||
|
||||
// we need the main emulation to be exactly one frame ahead of the
|
||||
// comparison emulation. the best way of doing this is to not even
|
||||
// start the comparison emulation immediately
|
||||
|
||||
select {
|
||||
case <-cmp.driver.sync:
|
||||
case <-cmp.emulationQuit:
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case cmp.driver.ack <- true:
|
||||
case <-cmp.emulationQuit:
|
||||
return
|
||||
}
|
||||
|
||||
err = cmp.VCS.Run(func() (emulation.State, error) {
|
||||
select {
|
||||
case <-cmp.emulationQuit:
|
||||
return emulation.Ending, nil
|
||||
default:
|
||||
}
|
||||
|
||||
return emulation.Running, nil
|
||||
})
|
||||
if err != nil {
|
||||
cmp.driver.quit <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resize implements the television.PixelRenderer interface.
|
||||
func (cmp *Comparison) Resize(frameInfo television.FrameInfo) error {
|
||||
cmp.frameInfo = frameInfo
|
||||
crop := image.Rect(
|
||||
specification.ClksHBlank, frameInfo.VisibleTop,
|
||||
specification.ClksHBlank+specification.ClksVisible, frameInfo.VisibleBottom,
|
||||
)
|
||||
|
||||
cmp.cropImg = cmp.img.SubImage(crop).(*image.RGBA)
|
||||
cmp.cropDiffImg = cmp.diffImg.SubImage(crop).(*image.RGBA)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewFrame implements the television.PixelRenderer interface.
|
||||
func (cmp *Comparison) NewFrame(frameInfo television.FrameInfo) error {
|
||||
cmp.frameInfo = frameInfo
|
||||
|
||||
img := *cmp.cropImg
|
||||
img.Pix = make([]uint8, len(cmp.cropImg.Pix))
|
||||
copy(img.Pix, cmp.cropImg.Pix)
|
||||
|
||||
select {
|
||||
case cmp.Render <- &img:
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-cmp.driver.sync:
|
||||
case <-cmp.emulationQuit:
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case cmp.driver.ack <- true:
|
||||
case <-cmp.emulationQuit:
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmp.diff()
|
||||
}
|
||||
|
||||
// NewScanline implements the television.PixelRenderer interface.
|
||||
func (cmp *Comparison) NewScanline(scanline int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmp *Comparison) diff() error {
|
||||
if !cmp.frameInfo.Stable || !cmp.driver.frameInfo.Stable {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmp.frameInfo.FrameNum > cmp.driver.frameInfo.FrameNum {
|
||||
return curated.Errorf("comparison: comparison emulation is running AHEAD of the driver emulation")
|
||||
} else if cmp.frameInfo.FrameNum < cmp.driver.frameInfo.FrameNum-1 {
|
||||
return curated.Errorf("comparison: comparison emulation is running BEHIND of the driver emulation")
|
||||
}
|
||||
|
||||
var drvImg *image.RGBA
|
||||
if cmp.driver.swapIdx {
|
||||
drvImg = cmp.driver.cropImg[0]
|
||||
} else {
|
||||
drvImg = cmp.driver.cropImg[1]
|
||||
}
|
||||
|
||||
if len(cmp.cropImg.Pix) != len(drvImg.Pix) {
|
||||
return curated.Errorf("comparison: frames are different sizes")
|
||||
}
|
||||
|
||||
if cmp.frameInfo.TotalScanlines != cmp.driver.frameInfo.TotalScanlines || cmp.frameInfo.VisibleTop != cmp.driver.frameInfo.VisibleTop || cmp.frameInfo.VisibleBottom != cmp.driver.frameInfo.VisibleBottom {
|
||||
return curated.Errorf("comparison: unaligned frames")
|
||||
}
|
||||
|
||||
for i := 0; i < len(cmp.cropImg.Pix); i += 4 {
|
||||
a := cmp.cropImg.Pix[i : i+3 : i+3]
|
||||
b := drvImg.Pix[i : i+3 : i+3]
|
||||
c := cmp.cropDiffImg.Pix[i : i+4 : i+4]
|
||||
if a[0] != b[0] || a[1] != b[1] || a[2] != b[2] {
|
||||
c[0] = 0xff
|
||||
c[1] = 0xff
|
||||
c[2] = 0xff
|
||||
} else {
|
||||
c[0] = 0x00
|
||||
c[1] = 0x00
|
||||
c[2] = 0x00
|
||||
}
|
||||
c[3] = 0xff
|
||||
}
|
||||
|
||||
cmp.DiffRender <- cmp.cropDiffImg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPixels implements the television.PixelRenderer interface.
|
||||
func (cmp *Comparison) SetPixels(sig []signal.SignalAttributes, last int) error {
|
||||
var col color.RGBA
|
||||
var offset int
|
||||
|
||||
for i := range sig {
|
||||
// handle VBLANK by setting pixels to black
|
||||
if sig[i]&signal.VBlank == signal.VBlank {
|
||||
col = color.RGBA{R: 0, G: 0, B: 0}
|
||||
} else {
|
||||
px := signal.ColorSignal((sig[i] & signal.Color) >> signal.ColorShift)
|
||||
col = cmp.frameInfo.Spec.GetColor(px)
|
||||
}
|
||||
|
||||
// small cap improves performance, see https://golang.org/issue/27857
|
||||
s := cmp.img.Pix[offset : offset+3 : offset+3]
|
||||
s[0] = col.R
|
||||
s[1] = col.G
|
||||
s[2] = col.B
|
||||
|
||||
offset += 4
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset implements the television.PixelRenderer interface.
|
||||
func (cmp *Comparison) Reset() {
|
||||
// clear pixels. setting the alpha channel so we don't have to later (the
|
||||
// alpha channel never changes)
|
||||
for y := 0; y < cmp.img.Bounds().Size().Y; y++ {
|
||||
for x := 0; x < cmp.img.Bounds().Size().X; x++ {
|
||||
cmp.img.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
|
||||
}
|
||||
}
|
||||
|
||||
for y := 0; y < cmp.diffImg.Bounds().Size().Y; y++ {
|
||||
for x := 0; x < cmp.diffImg.Bounds().Size().X; x++ {
|
||||
cmp.diffImg.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
|
||||
}
|
||||
}
|
||||
|
||||
img := *cmp.cropImg
|
||||
img.Pix = make([]uint8, len(cmp.cropImg.Pix))
|
||||
copy(img.Pix, cmp.cropImg.Pix)
|
||||
|
||||
select {
|
||||
case cmp.Render <- &img:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// EndRendering implements the television.PixelRenderer interface.
|
||||
func (cmp *Comparison) EndRendering() error {
|
||||
return nil
|
||||
}
|
35
comparison/doc.go
Normal file
35
comparison/doc.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package comparison facilitates the running of a comparison emulator
|
||||
// alongside the main emulation.
|
||||
//
|
||||
// The package synchronises the two emulations and the main emulation will
|
||||
// always be exactly one frame ahead of the comparison emulation. Either
|
||||
// emulation will be stalled for the duration that the other emulation
|
||||
// completes the next frame.
|
||||
//
|
||||
// User input is synchronised by setting the main emulation's RIOT Ports as a
|
||||
// driver and the comparison emulation's as a passenger (see RIOT package).
|
||||
//
|
||||
// Note that the main emulation will be stalled and will not be able to service
|
||||
// any of the normal communication channels for the duration that the
|
||||
// comparison emulation is running.
|
||||
//
|
||||
// The comparison emulation will produce two streams of pixels. The first is
|
||||
// the frame-by-frame video output of the emulation; and the second stream
|
||||
// shows the differences (as white pixels) between corresponding frames from
|
||||
// the two emulations. Each video stream has a one frame buffer.
|
||||
package comparison
|
144
comparison/driver.go
Normal file
144
comparison/driver.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package comparison
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/signal"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
||||
)
|
||||
|
||||
type driver struct {
|
||||
frameInfo television.FrameInfo
|
||||
|
||||
img [2]*image.RGBA
|
||||
cropImg [2]*image.RGBA
|
||||
swapIdx bool
|
||||
|
||||
sync chan bool
|
||||
ack chan bool
|
||||
|
||||
quit chan error
|
||||
}
|
||||
|
||||
func newDriver() driver {
|
||||
drv := driver{
|
||||
sync: make(chan bool),
|
||||
ack: make(chan bool),
|
||||
quit: make(chan error),
|
||||
}
|
||||
|
||||
drv.img[0] = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
|
||||
drv.img[1] = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
|
||||
|
||||
// start with a NTSC television as default
|
||||
drv.Resize(television.NewFrameInfo(specification.SpecNTSC))
|
||||
drv.Reset()
|
||||
|
||||
return drv
|
||||
}
|
||||
|
||||
// Resize implements the television.PixelRenderer interface.
|
||||
func (drv *driver) Resize(frameInfo television.FrameInfo) error {
|
||||
drv.frameInfo = frameInfo
|
||||
crop := image.Rect(
|
||||
specification.ClksHBlank, frameInfo.VisibleTop,
|
||||
specification.ClksHBlank+specification.ClksVisible, frameInfo.VisibleBottom,
|
||||
)
|
||||
|
||||
drv.cropImg[0] = drv.img[0].SubImage(crop).(*image.RGBA)
|
||||
drv.cropImg[1] = drv.img[1].SubImage(crop).(*image.RGBA)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewFrame implements the television.PixelRenderer interface.
|
||||
func (drv *driver) NewFrame(frameInfo television.FrameInfo) error {
|
||||
drv.frameInfo = frameInfo
|
||||
drv.swapIdx = !drv.swapIdx
|
||||
|
||||
select {
|
||||
case drv.sync <- true:
|
||||
case err := <-drv.quit:
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-drv.ack:
|
||||
case err := <-drv.quit:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewScanline implements the television.PixelRenderer interface.
|
||||
func (drv *driver) NewScanline(scanline int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPixels implements the television.PixelRenderer interface.
|
||||
func (drv *driver) SetPixels(sig []signal.SignalAttributes, last int) error {
|
||||
var col color.RGBA
|
||||
var offset int
|
||||
|
||||
var pix []uint8
|
||||
if drv.swapIdx {
|
||||
pix = drv.img[0].Pix
|
||||
} else {
|
||||
pix = drv.img[1].Pix
|
||||
}
|
||||
|
||||
for i := range sig {
|
||||
// handle VBLANK by setting pixels to black
|
||||
if sig[i]&signal.VBlank == signal.VBlank {
|
||||
col = color.RGBA{R: 0, G: 0, B: 0}
|
||||
} else {
|
||||
px := signal.ColorSignal((sig[i] & signal.Color) >> signal.ColorShift)
|
||||
col = drv.frameInfo.Spec.GetColor(px)
|
||||
}
|
||||
|
||||
// small cap improves performance, see https://golang.org/issue/27857
|
||||
s := pix[offset : offset+3 : offset+3]
|
||||
s[0] = col.R
|
||||
s[1] = col.G
|
||||
s[2] = col.B
|
||||
|
||||
offset += 4
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset implements the television.PixelRenderer interface.
|
||||
func (m *driver) Reset() {
|
||||
// clear pixels. setting the alpha channel so we don't have to later (the
|
||||
// alpha channel never changes)
|
||||
for i := range m.img {
|
||||
for y := 0; y < m.img[i].Bounds().Size().Y; y++ {
|
||||
for x := 0; x < m.img[i].Bounds().Size().X; x++ {
|
||||
m.img[i].SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EndRendering implements the television.PixelRenderer interface.
|
||||
func (drv *driver) EndRendering() error {
|
||||
return nil
|
||||
}
|
|
@ -23,6 +23,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
||||
"github.com/jetsetilly/gopher2600/comparison"
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/debugger/dbgmem"
|
||||
"github.com/jetsetilly/gopher2600/debugger/script"
|
||||
|
@ -82,6 +83,9 @@ type Debugger struct {
|
|||
// sure Close() is called on end
|
||||
loader *cartridgeloader.Loader
|
||||
|
||||
// comparison emulator
|
||||
comparison *comparison.Comparison
|
||||
|
||||
// GUI, terminal and controllers
|
||||
gui gui.GUI
|
||||
term terminal.Terminal
|
||||
|
@ -563,9 +567,14 @@ func (dbg *Debugger) end() {
|
|||
}
|
||||
|
||||
// Starts the main emulation sequence.
|
||||
func (dbg *Debugger) Start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader) error {
|
||||
func (dbg *Debugger) Start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader, comparisonROM string) error {
|
||||
err := dbg.addComparisonEmulation(comparisonROM)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
defer dbg.end()
|
||||
err := dbg.start(mode, initScript, cartload)
|
||||
err = dbg.start(mode, initScript, cartload)
|
||||
if err != nil {
|
||||
if curated.Has(err, terminal.UserQuit) {
|
||||
return nil
|
||||
|
@ -861,6 +870,29 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) addComparisonEmulation(comparisonROM string) error {
|
||||
if comparisonROM == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
dbg.comparison, err = comparison.NewComparison(dbg.vcs)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
dbg.gui.SetFeature(gui.ReqComparison, dbg.comparison.Render, dbg.comparison.DiffRender)
|
||||
|
||||
cartload, err := cartridgeloader.NewLoader(comparisonROM, "AUTO")
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
dbg.comparison.CreateFromLoader(cartload)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) hotload() (e error) {
|
||||
// tell GUI that we're in the initialistion phase
|
||||
dbg.setState(emulation.Initialising)
|
||||
|
|
|
@ -156,7 +156,7 @@ func TestDebugger_withNonExistantInitScript(t *testing.T) {
|
|||
|
||||
go trm.testSequence()
|
||||
|
||||
err = dbg.Start(emulation.ModeDebugger, "non_existent_script", cartridgeloader.Loader{})
|
||||
err = dbg.Start(emulation.ModeDebugger, "non_existent_script", cartridgeloader.Loader{}, "")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
@ -179,7 +179,7 @@ func TestDebugger(t *testing.T) {
|
|||
|
||||
go trm.testSequence()
|
||||
|
||||
err = dbg.Start(emulation.ModeDebugger, "", cartridgeloader.Loader{})
|
||||
err = dbg.Start(emulation.ModeDebugger, "", cartridgeloader.Loader{}, "")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
|
|
@ -112,4 +112,7 @@ const (
|
|||
|
||||
// Label that can be used in the EmulationLabel field of the cartridgeloader.Loader type. to indicate
|
||||
// that the cartridge has been loaded into the thumbnailer emulation
|
||||
const ThumbnailerLabel = "thumbnailer"
|
||||
const (
|
||||
ThumbnailerLabel = "thumbnailer"
|
||||
ComparisonLabel = "comparison"
|
||||
)
|
||||
|
|
|
@ -423,6 +423,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
useSavekey := md.AddBool("savekey", false, "use savekey in player 1 port")
|
||||
profile := md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
|
||||
log := md.AddBool("log", false, "echo debugging log to stdout")
|
||||
comparison := md.AddString("comparison", "", "ROM to run in parallel for comparison")
|
||||
|
||||
stats := &[]bool{false}[0]
|
||||
if statsview.Available() {
|
||||
|
@ -529,7 +530,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
|
||||
// set up a launch function
|
||||
dbgLaunch := func() error {
|
||||
err := dbg.Start(emulationMode, *initScript, cartload)
|
||||
err := dbg.Start(emulationMode, *initScript, cartload, *comparison)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -76,7 +76,10 @@ const (
|
|||
// open ROM selector and return selection over channel. channel is unused
|
||||
// if emulation is a debugging emulation, in which case the 'chan string'
|
||||
// can be nil
|
||||
ReqROMSelector FeatureReq = "ReqROMSelector" // chan string
|
||||
ReqROMSelector FeatureReq = "ReqROMSelector" // nil
|
||||
|
||||
// request for a comparison window to be opened
|
||||
ReqComparison FeatureReq = "ReqComparison" // chan *image.RGBA, chan *image.RGBA
|
||||
)
|
||||
|
||||
// Sentinal error returned if GUI does no support requested feature.
|
||||
|
|
|
@ -279,6 +279,10 @@ func (rnd *glsl) render() {
|
|||
shader = rnd.shaders[colorShaderID]
|
||||
case rnd.img.wm.timeline.thmbTexture:
|
||||
shader = rnd.shaders[colorShaderID]
|
||||
case rnd.img.wm.windows[winComparisonID].(*winComparison).cmpTexture:
|
||||
shader = rnd.shaders[colorShaderID]
|
||||
case rnd.img.wm.windows[winComparisonID].(*winComparison).diffTexture:
|
||||
shader = rnd.shaders[colorShaderID]
|
||||
default:
|
||||
shader = rnd.shaders[guiShaderID]
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ var windowDefs = [...]windowDef{
|
|||
{create: newWinOscilloscope, menu: menuEntry{group: menuTools}, open: true},
|
||||
{create: newWinTracker, menu: menuEntry{group: menuTools}, open: false},
|
||||
{create: newWinTimeline, menu: menuEntry{group: menuTools}, open: true},
|
||||
{create: newWinComparison, menu: menuEntry{group: menuTools}, open: false},
|
||||
|
||||
// windows that appear in cartridge specific menu
|
||||
{create: newWinDPCregisters, menu: menuEntry{group: menuCart, restrictBus: menuRestrictRegister, restrictMapper: []string{"DPC"}}},
|
||||
|
@ -137,6 +138,7 @@ var playmodeWindows = [...]string{
|
|||
winPrefsID,
|
||||
winSelectROMID,
|
||||
winTrackerID,
|
||||
winComparisonID,
|
||||
}
|
||||
|
||||
func newManager(img *SdlImgui) (*manager, error) {
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
package sdlimgui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/emulation"
|
||||
"github.com/jetsetilly/gopher2600/gui"
|
||||
|
@ -126,6 +128,14 @@ func (img *SdlImgui) serviceSetFeature(request featureRequest) {
|
|||
img.wm.windows[winSelectROMID].setOpen(true)
|
||||
}
|
||||
|
||||
case gui.ReqComparison:
|
||||
err = argLen(request.args, 2)
|
||||
if err == nil {
|
||||
img.wm.windows[winComparisonID].(*winComparison).render = request.args[0].(chan *image.RGBA)
|
||||
img.wm.windows[winComparisonID].(*winComparison).diffRender = request.args[1].(chan *image.RGBA)
|
||||
img.wm.windows[winComparisonID].(*winComparison).setOpen(true)
|
||||
}
|
||||
|
||||
default:
|
||||
err = curated.Errorf(gui.UnsupportedGuiFeature, request.request)
|
||||
}
|
||||
|
|
136
gui/sdlimgui/win_comparison.go
Normal file
136
gui/sdlimgui/win_comparison.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package sdlimgui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/go-gl/gl/v3.2-core/gl"
|
||||
"github.com/inkyblackness/imgui-go/v4"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
||||
)
|
||||
|
||||
const winComparisonID = "Comparison"
|
||||
|
||||
type winComparison struct {
|
||||
img *SdlImgui
|
||||
open bool
|
||||
|
||||
cmpTexture uint32
|
||||
diffTexture uint32
|
||||
|
||||
// render channels are given to use by the main emulation through a GUI request
|
||||
render chan *image.RGBA
|
||||
diffRender chan *image.RGBA
|
||||
}
|
||||
|
||||
func newWinComparison(img *SdlImgui) (window, error) {
|
||||
win := &winComparison{
|
||||
img: img,
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &win.cmpTexture)
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.cmpTexture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
|
||||
gl.GenTextures(1, &win.diffTexture)
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.diffTexture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
|
||||
return win, nil
|
||||
}
|
||||
|
||||
func (win *winComparison) init() {
|
||||
}
|
||||
|
||||
func (win winComparison) id() string {
|
||||
return winComparisonID
|
||||
}
|
||||
|
||||
func (win *winComparison) isOpen() bool {
|
||||
return win.open
|
||||
}
|
||||
|
||||
func (win *winComparison) setOpen(open bool) {
|
||||
if win.render == nil {
|
||||
return
|
||||
}
|
||||
|
||||
win.open = open
|
||||
|
||||
if win.open {
|
||||
// clear texture
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.cmpTexture)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0,
|
||||
gl.RGBA, 1, 1, 0,
|
||||
gl.RGBA, gl.UNSIGNED_BYTE,
|
||||
gl.Ptr([]uint8{0}))
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.diffTexture)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0,
|
||||
gl.RGBA, 1, 1, 0,
|
||||
gl.RGBA, gl.UNSIGNED_BYTE,
|
||||
gl.Ptr([]uint8{0}))
|
||||
}
|
||||
}
|
||||
|
||||
func (win *winComparison) draw() {
|
||||
// receive new thumbnail data and copy to texture
|
||||
select {
|
||||
case img := <-win.render:
|
||||
if img != nil {
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(img.Stride)/4)
|
||||
defer gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.cmpTexture)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0,
|
||||
gl.RGBA, int32(img.Bounds().Size().X), int32(img.Bounds().Size().Y), 0,
|
||||
gl.RGBA, gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(img.Pix))
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// receive new thumbnail data and copy to texture
|
||||
select {
|
||||
case img := <-win.diffRender:
|
||||
if img != nil {
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(img.Stride)/4)
|
||||
defer gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.diffTexture)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0,
|
||||
gl.RGBA, int32(img.Bounds().Size().X), int32(img.Bounds().Size().Y), 0,
|
||||
gl.RGBA, gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(img.Pix))
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
if !win.open {
|
||||
return
|
||||
}
|
||||
|
||||
imgui.SetNextWindowPosV(imgui.Vec2{75, 75}, imgui.ConditionFirstUseEver, imgui.Vec2{0, 0})
|
||||
|
||||
if imgui.BeginV(win.id(), &win.open, imgui.WindowFlagsAlwaysAutoResize) {
|
||||
imgui.Image(imgui.TextureID(win.cmpTexture), imgui.Vec2{specification.ClksVisible * 3, specification.AbsoluteMaxScanlines})
|
||||
imgui.Image(imgui.TextureID(win.diffTexture), imgui.Vec2{specification.ClksVisible * 3, specification.AbsoluteMaxScanlines})
|
||||
imgui.End()
|
||||
}
|
||||
}
|
|
@ -59,8 +59,8 @@ type ARMPreferences struct {
|
|||
// NOTE: this may be superceded in the future to allow for more flexibility
|
||||
Model prefs.String
|
||||
|
||||
// whether the ARM coprocessor (as found in Harmony cartridges) execute
|
||||
// instantly or if the cycle accurate steppint is attempted
|
||||
// whether the ARM coprocessor (as found in Harmony cartridges) executes
|
||||
// instantly
|
||||
Immediate prefs.Bool
|
||||
|
||||
// a value of MAMDriver says to use the driver supplied MAM value. any
|
||||
|
|
|
@ -20,5 +20,13 @@
|
|||
// Input from "real" devices is handled by HandleEvent() which passes the event
|
||||
// to peripherals in the specified PortID.
|
||||
//
|
||||
// Peripherals write back to the VCS through the PeripheralBus.
|
||||
// Emulations can share user input through the DrivenEvent mechanism. The driver
|
||||
// emulation should call SynchroniseWithPassenger() and the passenger emulation
|
||||
// should call SynchroniseWithDriver().
|
||||
//
|
||||
// With the DrivenEvent mechanism, the driver sends events to the passenger.
|
||||
// Both emulations will receive the same user input at the same time, relative
|
||||
// to television coordinates, so it is important that the driver is running
|
||||
// ahead of the passenger at all time. See comparison package for model
|
||||
// implementation.
|
||||
package ports
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
|
||||
package ports
|
||||
|
||||
import "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
)
|
||||
|
||||
// Event represents the actions that can be performed at one of the VCS ports,
|
||||
// either the panel or one of the two player ports.
|
||||
|
@ -120,3 +123,12 @@ type EventPlayback interface {
|
|||
type EventRecorder interface {
|
||||
RecordEvent(plugging.PortID, Event, EventData) error
|
||||
}
|
||||
|
||||
// DrivenEvent is the data passed from a "driver" emulation to a "passenger"
|
||||
// emulation.
|
||||
type DrivenEvent struct {
|
||||
time coords.TelevisionCoords
|
||||
id plugging.PortID
|
||||
ev Event
|
||||
d EventData
|
||||
}
|
||||
|
|
136
hardware/riot/ports/handle.go
Normal file
136
hardware/riot/ports/handle.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
// This file is part of Gopher2600.
|
||||
//
|
||||
// Gopher2600 is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Gopher2600 is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ports
|
||||
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
)
|
||||
|
||||
// HandleDrivenEvents checks for input driven from a another emulation.
|
||||
func (p *Ports) HandleDrivenEvents() error {
|
||||
if p.checkForDriven {
|
||||
f := p.drivenInputData
|
||||
done := false
|
||||
for !done {
|
||||
c := p.tv.GetCoords()
|
||||
if coords.Equal(c, f.time) {
|
||||
_, err := p.HandleEvent(f.id, f.ev, f.d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if coords.GreaterThan(c, f.time) {
|
||||
return curated.Errorf("ports: driven input seen too late. emulations not synced correctly.")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case p.drivenInputData = <-p.fromDriver:
|
||||
if p.checkForDriven {
|
||||
curated.Errorf("ports: driven input received before previous input was processed")
|
||||
}
|
||||
default:
|
||||
done = true
|
||||
p.checkForDriven = false
|
||||
}
|
||||
|
||||
f = p.drivenInputData
|
||||
}
|
||||
}
|
||||
|
||||
if p.fromDriver != nil {
|
||||
select {
|
||||
case p.drivenInputData = <-p.fromDriver:
|
||||
if p.checkForDriven {
|
||||
curated.Errorf("ports: driven input received before previous input was processed")
|
||||
}
|
||||
p.checkForDriven = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandlePlaybackEvents requests playback events from all attached and eligible peripherals.
|
||||
func (p *Ports) HandlePlaybackEvents() error {
|
||||
if p.playback == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loop with GetPlayback() until we encounter a NoPortID or NoEvent
|
||||
// condition. there might be more than one entry for a particular
|
||||
// frame/scanline/horizpas state so we need to make sure we've processed
|
||||
// them all.
|
||||
//
|
||||
// this happens in particular with recordings that were made of ROMs with
|
||||
// panel setup configurations (see setup package) - where the switches are
|
||||
// set when the TV state is at fr=0 sl=0 cl=0
|
||||
morePlayback := true
|
||||
for morePlayback {
|
||||
id, ev, v, err := p.playback.GetPlayback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
morePlayback = id != plugging.PortUnplugged && ev != NoEvent
|
||||
if morePlayback {
|
||||
_, err := p.HandleEvent(id, ev, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleEvent implements userinput.HandleInput interface.
|
||||
func (p *Ports) HandleEvent(id plugging.PortID, ev Event, d EventData) (bool, error) {
|
||||
var handled bool
|
||||
var err error
|
||||
|
||||
switch id {
|
||||
case plugging.PortPanel:
|
||||
handled, err = p.Panel.HandleEvent(ev, d)
|
||||
case plugging.PortLeftPlayer:
|
||||
handled, err = p.LeftPlayer.HandleEvent(ev, d)
|
||||
case plugging.PortRightPlayer:
|
||||
handled, err = p.RightPlayer.HandleEvent(ev, d)
|
||||
}
|
||||
|
||||
if handled && p.toPassenger != nil {
|
||||
select {
|
||||
case p.toPassenger <- DrivenEvent{time: p.tv.GetCoords(), id: id, ev: ev, d: d}:
|
||||
default:
|
||||
return handled, curated.Errorf("ports: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// if error was because of an unhandled event then return without error
|
||||
if err != nil {
|
||||
return handled, curated.Errorf("ports: %v", err)
|
||||
}
|
||||
|
||||
// record event with the EventRecorder
|
||||
for _, r := range p.recorder {
|
||||
return handled, r.RecordEvent(id, ev, d)
|
||||
}
|
||||
|
||||
return handled, nil
|
||||
}
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/hardware/memory/addresses"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/bus"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/signal"
|
||||
)
|
||||
|
||||
// Input implements the input/output part of the RIOT (the IO in RIOT).
|
||||
|
@ -78,6 +79,16 @@ type Ports struct {
|
|||
//
|
||||
// we use it to mux the Player0 and Player 1 nibbles into the single register
|
||||
swchaMux uint8
|
||||
|
||||
// the following fields all relate to driven input, for either the driver
|
||||
// or for the passenger (the driven)
|
||||
fromDriver chan DrivenEvent
|
||||
toPassenger chan DrivenEvent
|
||||
checkForDriven bool
|
||||
drivenInputData DrivenEvent
|
||||
|
||||
// the time of driven events are measured by television coordinates
|
||||
tv signal.TelevisionCoords
|
||||
}
|
||||
|
||||
// NewPorts is the preferred method of initialisation of the Ports type.
|
||||
|
@ -123,7 +134,7 @@ func (p *Ports) Plumb(riotMem bus.ChipBus, tiaMem bus.ChipBus) {
|
|||
func (p *Ports) Plug(port plugging.PortID, c NewPeripheral) error {
|
||||
periph := c(port, p)
|
||||
|
||||
// notify monitor of pluggin
|
||||
// notify monitor of plug event
|
||||
if p.monitor != nil {
|
||||
p.monitor.Plugged(port, periph.ID())
|
||||
}
|
||||
|
@ -245,14 +256,41 @@ func (p *Ports) Step() {
|
|||
p.Panel.Step()
|
||||
}
|
||||
|
||||
// AttachPlayback attaches an EventPlayback implementation to all ports that
|
||||
// implement RecordablePort.
|
||||
func (p *Ports) AttachPlayback(b EventPlayback) {
|
||||
p.playback = b
|
||||
// SynchroniseWithDriver implies that the emulation will receive driven events
|
||||
// from another emulation.
|
||||
func (p *Ports) SynchroniseWithDriver(driver chan DrivenEvent, tv signal.TelevisionCoords) error {
|
||||
if p.toPassenger != nil {
|
||||
return curated.Errorf("ports: cannot sync with driver: emulation already defined as a driver of input")
|
||||
}
|
||||
if p.playback != nil {
|
||||
return curated.Errorf("ports: cannot sync with driver: emulation is already receiving input from a playback")
|
||||
}
|
||||
p.tv = tv
|
||||
p.fromDriver = driver
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachEventRecorder attaches an EventRecorder implementation to all ports
|
||||
// that implement RecordablePort.
|
||||
// SynchroniseWithPassenger connects the emulation to a second emulation (the
|
||||
// passenger) to which user input events will be "driven".
|
||||
func (p *Ports) SynchroniseWithPassenger(passenger chan DrivenEvent, tv signal.TelevisionCoords) error {
|
||||
if p.fromDriver != nil {
|
||||
return curated.Errorf("ports: cannot sync with passenger: emulation already defined as being driven")
|
||||
}
|
||||
p.tv = tv
|
||||
p.toPassenger = passenger
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachPlayback attaches an EventPlayback implementation.
|
||||
func (p *Ports) AttachPlayback(b EventPlayback) error {
|
||||
if p.fromDriver != nil {
|
||||
return curated.Errorf("ports: cannot attach playback: emulation already defined as being driven")
|
||||
}
|
||||
p.playback = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachEventRecorder attaches an EventRecorder implementation.
|
||||
func (p *Ports) AttachEventRecorder(r EventRecorder) {
|
||||
p.recorder = append(p.recorder, r)
|
||||
}
|
||||
|
@ -279,66 +317,6 @@ func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetPlayback requests playback events from all attached and eligible peripherals.
|
||||
func (p *Ports) GetPlayback() error {
|
||||
if p.playback == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loop with GetPlayback() until we encounter a NoPortID or NoEvent
|
||||
// condition. there might be more than one entry for a particular
|
||||
// frame/scanline/horizpas state so we need to make sure we've processed
|
||||
// them all.
|
||||
//
|
||||
// this happens in particular with recordings that were made of ROMs with
|
||||
// panel setup configurations (see setup package) - where the switches are
|
||||
// set when the TV state is at fr=0 sl=0 cl=0
|
||||
morePlayback := true
|
||||
for morePlayback {
|
||||
id, ev, v, err := p.playback.GetPlayback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
morePlayback = id != plugging.PortUnplugged && ev != NoEvent
|
||||
if morePlayback {
|
||||
_, err := p.HandleEvent(id, ev, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleEvent implements userinput.HandleInput interface.
|
||||
func (p *Ports) HandleEvent(id plugging.PortID, ev Event, d EventData) (bool, error) {
|
||||
var handled bool
|
||||
var err error
|
||||
|
||||
switch id {
|
||||
case plugging.PortPanel:
|
||||
handled, err = p.Panel.HandleEvent(ev, d)
|
||||
case plugging.PortLeftPlayer:
|
||||
handled, err = p.LeftPlayer.HandleEvent(ev, d)
|
||||
case plugging.PortRightPlayer:
|
||||
handled, err = p.RightPlayer.HandleEvent(ev, d)
|
||||
}
|
||||
|
||||
// if error was because of an unhandled event then return without error
|
||||
if err != nil {
|
||||
return handled, curated.Errorf("ports: %v", err)
|
||||
}
|
||||
|
||||
// record event with the EventRecorder
|
||||
for _, r := range p.recorder {
|
||||
return handled, r.RecordEvent(id, ev, d)
|
||||
}
|
||||
|
||||
return handled, nil
|
||||
}
|
||||
|
||||
// PeripheralID implements userinput.HandleInput interface.
|
||||
func (p *Ports) PeripheralID(id plugging.PortID) plugging.PeripheralID {
|
||||
switch id {
|
||||
|
|
|
@ -50,13 +50,23 @@ func (vcs *VCS) Run(continueCheck func() (emulation.State, error)) error {
|
|||
// see the equivalient videoCycle() in the VCS.Step() function for an
|
||||
// explanation for what's going on here:
|
||||
videoCycle := func() error {
|
||||
if err := vcs.RIOT.Ports.GetPlayback(); err != nil {
|
||||
if err := vcs.RIOT.Ports.HandleDrivenEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vcs.TIA.Step(false)
|
||||
vcs.TIA.Step(false)
|
||||
vcs.TIA.Step(true)
|
||||
if err := vcs.RIOT.Ports.HandlePlaybackEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vcs.TIA.Step(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vcs.TIA.Step(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vcs.TIA.Step(true); err != nil {
|
||||
return err
|
||||
}
|
||||
vcs.RIOT.Step()
|
||||
vcs.Mem.Cart.Step(vcs.Clock)
|
||||
|
||||
|
|
|
@ -57,25 +57,30 @@ func (vcs *VCS) Step(videoCycleCallback func() error) error {
|
|||
// I don't believe any visual or audible artefacts of the VCS (undocumented
|
||||
// or not) rely on the details of the CPU-TIA relationship.
|
||||
videoCycle := func() error {
|
||||
// probe for playback events
|
||||
err := vcs.RIOT.Ports.GetPlayback()
|
||||
if err != nil {
|
||||
if err := vcs.RIOT.Ports.HandleDrivenEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vcs.TIA.Step(false)
|
||||
err = videoCycleCallback()
|
||||
if err != nil {
|
||||
if err := vcs.RIOT.Ports.HandlePlaybackEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
vcs.TIA.Step(false)
|
||||
err = videoCycleCallback()
|
||||
if err != nil {
|
||||
|
||||
if err := vcs.TIA.Step(false); err != nil {
|
||||
return err
|
||||
}
|
||||
vcs.TIA.Step(true)
|
||||
err = videoCycleCallback()
|
||||
if err != nil {
|
||||
if err := videoCycleCallback(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vcs.TIA.Step(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := videoCycleCallback(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vcs.TIA.Step(true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := videoCycleCallback(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,12 @@
|
|||
// Package coords represents and can work with television coorindates
|
||||
//
|
||||
// Coordinates represent the state of the emulation from the point of the
|
||||
// television and are used throughout the emulation for rewinding,
|
||||
// recording/playback and general information.
|
||||
// television. A good way to think about them is as a measurement of time. They
|
||||
// define *when* something happened (this pixel was drawn, this user input was
|
||||
// received, etc.) relative to the start of the emulation.
|
||||
//
|
||||
// They are used throughout the emulation for rewinding, recording/playback and
|
||||
// many other sub-systems.
|
||||
package coords
|
||||
|
||||
import "fmt"
|
||||
|
|
|
@ -85,7 +85,8 @@ type TelevisionTIA interface {
|
|||
GetCoords() coords.TelevisionCoords
|
||||
}
|
||||
|
||||
// TelevisionSprite exposes only the functions required by the video sprites.
|
||||
type TelevisionSprite interface {
|
||||
// TelevisionCoords allows probing of the current "coordinates" of the
|
||||
// television. Useful for measuring time.
|
||||
type TelevisionCoords interface {
|
||||
GetCoords() coords.TelevisionCoords
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/cpu"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/bus"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/signal"
|
||||
|
@ -354,7 +355,7 @@ func (tia *TIA) resolveDelayedEvents() {
|
|||
}
|
||||
|
||||
// Step moves the state of the tia forward one video cycle.
|
||||
func (tia *TIA) Step(readMemory bool) {
|
||||
func (tia *TIA) Step(readMemory bool) error {
|
||||
// update debugging information
|
||||
tia.videoCycles++
|
||||
|
||||
|
@ -604,6 +605,8 @@ func (tia *TIA) Step(readMemory bool) {
|
|||
// send signal to television
|
||||
if err := tia.tv.Signal(tia.sig); err != nil {
|
||||
// TODO: handle error
|
||||
return
|
||||
return curated.Errorf("TIA: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ type Video struct {
|
|||
|
||||
// tia is a convenient packaging of TIA state that is required by the playfield/sprites.
|
||||
type tia struct {
|
||||
tv signal.TelevisionSprite
|
||||
tv signal.TelevisionCoords
|
||||
rev *revision.TIARevision
|
||||
pclk *phaseclock.PhaseClock
|
||||
hsync *polycounter.Polycounter
|
||||
|
@ -121,7 +121,7 @@ type tia struct {
|
|||
// The references to the TIA's HBLANK state and whether HMOVE is latched, are
|
||||
// required to tune the delays experienced by the various sprite events (eg.
|
||||
// reset position).
|
||||
func NewVideo(mem bus.ChipBus, tv signal.TelevisionSprite, rev *revision.TIARevision,
|
||||
func NewVideo(mem bus.ChipBus, tv signal.TelevisionCoords, rev *revision.TIARevision,
|
||||
pclk *phaseclock.PhaseClock, hsync *polycounter.Polycounter,
|
||||
hblank *bool, hmove *hmove.Hmove) *Video {
|
||||
tia := tia{
|
||||
|
@ -159,7 +159,7 @@ func (vd *Video) Snapshot() *Video {
|
|||
}
|
||||
|
||||
// Plumb ChipBus into TIA/Video components. Update pointers that refer to parent TIA.
|
||||
func (vd *Video) Plumb(mem bus.ChipBus, tv signal.TelevisionSprite, rev *revision.TIARevision,
|
||||
func (vd *Video) Plumb(mem bus.ChipBus, tv signal.TelevisionCoords, rev *revision.TIARevision,
|
||||
pclk *phaseclock.PhaseClock, hsync *polycounter.Polycounter,
|
||||
hblank *bool, hmove *hmove.Hmove) {
|
||||
vd.Collisions.Plumb(mem)
|
||||
|
|
|
@ -208,7 +208,10 @@ func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
|
|||
}
|
||||
|
||||
// attach playback to all vcs ports
|
||||
vcs.RIOT.Ports.AttachPlayback(plb)
|
||||
err = vcs.RIOT.Ports.AttachPlayback(plb)
|
||||
if err != nil {
|
||||
return curated.Errorf("playback: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -435,6 +435,10 @@ func (c *Controllers) ClearInputHandlers() {
|
|||
|
||||
// Add HandleInput implementation to list of input handlers. Each input handler
|
||||
// will receive the ports.Event and ports.EventData.
|
||||
//
|
||||
// In many instances when running two parallel emulators that require the same
|
||||
// user input it is better to add the "driver" and "passenger" emulations to
|
||||
// RIOT.Ports
|
||||
func (c *Controllers) AddInputHandler(h HandleInput) {
|
||||
c.inputHandlers = append(c.inputHandlers, h)
|
||||
}
|
||||
|
|
|
@ -13,14 +13,15 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package userinput handles input from real hardware that the the user of the
|
||||
// emulator is using to control the emualted console.
|
||||
// Package userinput translates real hardware input from the user's computer,
|
||||
// to the emulated console. It abstracts real user interface to the emulated
|
||||
// interface.
|
||||
//
|
||||
// It can be thought of as a translation layer between the GUI implementation
|
||||
// and the hardware riot.ports package. As such, this package attempts to hide
|
||||
// details of the GUI implementation while protecting the ports package from
|
||||
// complication.
|
||||
// The Controllers type processes userinput Events (input from a gamepad for
|
||||
// example) with the HandleUserInput function. That input is translated into
|
||||
// an event understood by the emulation (paddle left for example).
|
||||
//
|
||||
// The GUI implementation in use during development was SDL and so there will
|
||||
// be a bias towards that system.
|
||||
// Many emulations can be attached to a single Controllers type with the
|
||||
// AddInputHandler(). However, the DrivenEvent mechanism in the riot.ports
|
||||
// package is a better way to do this in many instances.
|
||||
package userinput
|
||||
|
|
Loading…
Reference in a new issue