Gopher2600/comparison/comparison.go
JetSetIlly 28ae543e36 cartridgeloader commentary and documenation
removed all references to hotloading. will reimplement in the future

made sure then cartridgeloader.Loader instances are closed when no
longer needed. added commentary where required to explain

information pane in the ROM selector window is not disabled when
animation emulation is not running. the load button is still visible
based on the animation emulation
2024-04-16 22:15:39 +01:00

373 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 comparisonEnv = 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(tv, cmp, driverVCS.Env.Prefs)
if err != nil {
return nil, fmt.Errorf("comparison: %w", err)
}
cmp.VCS.Env.Label = comparisonEnv
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
}