mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 13:48:02 -04:00
96ad0797b4
this helps force the environment.Label to be set to something meaningful
372 lines
10 KiB
Go
372 lines
10 KiB
Go
// 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/debugger/govern"
|
|
"github.com/jetsetilly/gopher2600/environment"
|
|
"github.com/jetsetilly/gopher2600/hardware"
|
|
"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"
|
|
"github.com/jetsetilly/gopher2600/notifications"
|
|
)
|
|
|
|
// 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
|
|
|
|
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
|
|
}
|
|
|
|
const comparisonLabel = environment.Label("comparison")
|
|
|
|
// NewComparison is the preferred method of initialisation for the Comparison type.
|
|
func NewComparison(driverVCS *hardware.VCS) (*Comparison, error) {
|
|
cmp := &Comparison{
|
|
emulationQuit: make(chan bool, 1),
|
|
Render: make(chan *image.RGBA, 1),
|
|
DiffRender: make(chan *image.RGBA, 1),
|
|
}
|
|
|
|
// 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, fmt.Errorf("comparison: %w", err)
|
|
}
|
|
tv.AddPixelRenderer(cmp)
|
|
tv.SetFPSCap(true)
|
|
|
|
// create a new VCS emulation
|
|
cmp.VCS, err = hardware.NewVCS(comparisonLabel, tv, cmp, driverVCS.Env.Prefs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("comparison: %w", 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(driverVCS.TV)
|
|
driverVCS.TV.AddPixelRenderer(&cmp.driver)
|
|
|
|
// synchronise RIOT ports
|
|
sync := make(chan ports.TimedInputEvent, 32)
|
|
err = cmp.VCS.Input.AttachPassenger(sync)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("comparison: %w", err)
|
|
}
|
|
err = driverVCS.Input.AttachDriver(sync)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("comparison: %w", 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)
|
|
}
|
|
|
|
// Quit ends the running comparison emulation.
|
|
func (cmp *Comparison) Quit() {
|
|
// very important that we remove the pixel renderer from the main
|
|
// emulation's television that we added as part of the newDriver()
|
|
// function. if we don't then the renderer will continue firing and get
|
|
// jammed waiting on a channel that has been abandone
|
|
cmp.driver.tv.RemovePixelRenderer(&cmp.driver)
|
|
|
|
// send quit signal to the comparison emulation
|
|
cmp.emulationQuit <- true
|
|
}
|
|
|
|
// Notify implements the notifications.Notify interface
|
|
func (cmp *Comparison) Notify(notice notifications.Notice) error {
|
|
switch notice {
|
|
case notifications.NotifySuperchargerFastload:
|
|
// 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
|
|
fastload := cmp.VCS.Mem.Cart.GetSuperchargerFastLoad()
|
|
err := fastload.Fastload(cmp.VCS.CPU, cmp.VCS.Mem.RAM, cmp.VCS.RIOT.Timer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case notifications.NotifySuperchargerSoundloadEnded:
|
|
return cmp.VCS.TV.Reset(true)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 fmt.Errorf("comparison: emulation already running")
|
|
}
|
|
|
|
go func() {
|
|
defer func() {
|
|
cmp.driver.quit <- nil
|
|
cmp.isEmulating.Store(false)
|
|
cartload.Close()
|
|
}()
|
|
|
|
// not using setup system to attach cartridge. maybe we should?
|
|
err := cmp.VCS.AttachCartridge(cartload, true)
|
|
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() (govern.State, error) {
|
|
select {
|
|
case <-cmp.emulationQuit:
|
|
return govern.Ending, nil
|
|
default:
|
|
}
|
|
|
|
return govern.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 fmt.Errorf("comparison: comparison emulation is running AHEAD of the driver emulation")
|
|
} else if cmp.frameInfo.FrameNum < cmp.driver.frameInfo.FrameNum-1 {
|
|
return fmt.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 fmt.Errorf("comparison: frames are different sizes")
|
|
}
|
|
|
|
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
|
|
}
|