mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
o recorder
- digest tv support added to recorder/playback module
This commit is contained in:
parent
99d04c747b
commit
218c3d7823
10 changed files with 291 additions and 157 deletions
|
@ -64,4 +64,5 @@ const (
|
|||
// Recorder
|
||||
RecordingError
|
||||
PlaybackError
|
||||
PlaybackHashError
|
||||
)
|
||||
|
|
|
@ -61,6 +61,7 @@ var messages = map[Errno]string{
|
|||
UnknownPeripheralEvent: "this peripheral (%s) does not understand that event (%v)",
|
||||
|
||||
// Recorder
|
||||
RecordingError: "error when recording input (%s)",
|
||||
PlaybackError: "error when playing back recorded input (%s)",
|
||||
RecordingError: "error when recording input (%s)",
|
||||
PlaybackError: "error when playing back recorded input (%s)",
|
||||
PlaybackHashError: "hash error when playing back recording input (%s)",
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ func main() {
|
|||
fallthrough
|
||||
|
||||
case "PLAY":
|
||||
tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
scaling := modeFlags.Float64("scale", 3.0, "television scaling")
|
||||
stable := modeFlags.Bool("stable", true, "wait for stable frame before opening display")
|
||||
record := modeFlags.Bool("record", false, "record user input to a file")
|
||||
|
@ -124,12 +124,15 @@ func main() {
|
|||
|
||||
switch len(modeFlags.Args()) {
|
||||
case 0:
|
||||
fmt.Println("* 2600 cartridge required")
|
||||
os.Exit(2)
|
||||
if *recording == "" {
|
||||
fmt.Println("* 2600 cartridge required")
|
||||
os.Exit(2)
|
||||
}
|
||||
fallthrough
|
||||
case 1:
|
||||
err := playmode.Play(modeFlags.Arg(0), *tvMode, float32(*scaling), *stable, *recording, *record)
|
||||
err := playmode.Play(modeFlags.Arg(0), *tvType, float32(*scaling), *stable, *recording, *record)
|
||||
if err != nil {
|
||||
fmt.Printf("* error running emulator: %s\n", err)
|
||||
fmt.Println(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
default:
|
||||
|
@ -204,7 +207,7 @@ func main() {
|
|||
|
||||
case "FPS":
|
||||
display := modeFlags.Bool("display", false, "display TV output: boolean")
|
||||
tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
scaling := modeFlags.Float64("scale", 3.0, "television scaling")
|
||||
runTime := modeFlags.String("time", "5s", "run duration (note: there is a 2s overhead)")
|
||||
profile := modeFlags.Bool("profile", false, "perform cpu and memory profiling")
|
||||
|
@ -215,7 +218,7 @@ func main() {
|
|||
fmt.Println("* 2600 cartridge required")
|
||||
os.Exit(2)
|
||||
case 1:
|
||||
err := fps(*profile, modeFlags.Arg(0), *display, *tvMode, float32(*scaling), *runTime)
|
||||
err := fps(*profile, modeFlags.Arg(0), *display, *tvType, float32(*scaling), *runTime)
|
||||
if err != nil {
|
||||
fmt.Printf("* error starting fps profiler: %s\n", err)
|
||||
os.Exit(2)
|
||||
|
@ -275,7 +278,7 @@ func main() {
|
|||
}
|
||||
|
||||
case "ADD":
|
||||
tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
numFrames := modeFlags.Int("frames", 10, "number of frames to run")
|
||||
modeFlagsParse()
|
||||
|
||||
|
@ -284,7 +287,7 @@ func main() {
|
|||
fmt.Println("* 2600 cartridge required")
|
||||
os.Exit(2)
|
||||
case 1:
|
||||
err := regression.RegressAddCartridge(modeFlags.Arg(0), *tvMode, *numFrames)
|
||||
err := regression.RegressAddCartridge(modeFlags.Arg(0), *tvType, *numFrames)
|
||||
if err != nil {
|
||||
fmt.Printf("* error adding regression test: %s\n", err)
|
||||
os.Exit(2)
|
||||
|
@ -295,7 +298,7 @@ func main() {
|
|||
os.Exit(2)
|
||||
}
|
||||
case "UPDATE":
|
||||
tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL")
|
||||
numFrames := modeFlags.Int("frames", 10, "number of frames to run")
|
||||
modeFlagsParse()
|
||||
|
||||
|
@ -304,7 +307,7 @@ func main() {
|
|||
fmt.Println("* 2600 cartridge required")
|
||||
os.Exit(2)
|
||||
case 1:
|
||||
err := regression.RegressUpdateCartridge(modeFlags.Arg(0), *tvMode, *numFrames)
|
||||
err := regression.RegressUpdateCartridge(modeFlags.Arg(0), *tvType, *numFrames)
|
||||
if err != nil {
|
||||
fmt.Printf("* error updating regression test: %s\n", err)
|
||||
os.Exit(2)
|
||||
|
@ -318,12 +321,12 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func fps(profile bool, cartridgeFile string, display bool, tvMode string, scaling float32, runTime string) error {
|
||||
func fps(profile bool, cartridgeFile string, display bool, tvType string, scaling float32, runTime string) error {
|
||||
var fpstv television.Television
|
||||
var err error
|
||||
|
||||
if display {
|
||||
fpstv, err = sdl.NewGUI(tvMode, scaling, nil)
|
||||
fpstv, err = sdl.NewGUI(tvType, scaling, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error preparing television: %s", err)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"gopher2600/errors"
|
||||
"io"
|
||||
|
@ -20,7 +21,14 @@ type Cartridge struct {
|
|||
Area
|
||||
AreaInfo
|
||||
|
||||
name string
|
||||
// full path to the cartridge as stored on disk
|
||||
Filename string
|
||||
|
||||
// hash of binary loaded from disk. any subsequent pokes to cartridge
|
||||
// memory will not be reflected in the value
|
||||
Hash string
|
||||
|
||||
// cartridge bank-switching method
|
||||
method string
|
||||
|
||||
NumBanks int
|
||||
|
@ -40,6 +48,7 @@ type Cartridge struct {
|
|||
}
|
||||
|
||||
const ejectedName = "ejected"
|
||||
const ejectedHash = "nohash"
|
||||
const ejectedMethod = "none"
|
||||
|
||||
// NewCart is the preferred method of initialisation for the cartridges
|
||||
|
@ -56,12 +65,12 @@ func NewCart() *Cartridge {
|
|||
|
||||
// MachineInfoTerse returns the cartridge information in terse format
|
||||
func (cart Cartridge) MachineInfoTerse() string {
|
||||
return fmt.Sprintf("%s [%s] bank=%d", cart.name, cart.method, cart.Bank)
|
||||
return fmt.Sprintf("%s [%s] bank=%d", cart.Filename, cart.method, cart.Bank)
|
||||
}
|
||||
|
||||
// MachineInfo returns the cartridge information in verbose format
|
||||
func (cart Cartridge) MachineInfo() string {
|
||||
return fmt.Sprintf("name: %s\nmethod: %s\nbank:%d", cart.name, cart.method, cart.Bank)
|
||||
return fmt.Sprintf("name: %s\nmethod: %s\nbank:%d", cart.Filename, cart.method, cart.Bank)
|
||||
}
|
||||
|
||||
// Label is an implementation of Area.Label
|
||||
|
@ -146,6 +155,14 @@ func (cart *Cartridge) Attach(filename string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// generate hash
|
||||
key := sha1.New()
|
||||
if _, err := io.Copy(key, cf); err != nil {
|
||||
return err
|
||||
}
|
||||
cart.Hash = fmt.Sprintf("%x", key.Sum(nil))
|
||||
|
||||
// we always start in bank 0
|
||||
cart.Bank = 0
|
||||
|
||||
// set default read hooks
|
||||
|
@ -354,7 +371,7 @@ func (cart *Cartridge) Attach(filename string) error {
|
|||
}
|
||||
|
||||
// note name of cartridge
|
||||
cart.name = filename
|
||||
cart.Filename = filename
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -418,8 +435,9 @@ func (cart *Cartridge) BankSwitch(bank int) error {
|
|||
// Eject removes memory from cartridge space and unlike the real hardware,
|
||||
// attaches a bank of empty memory - for convenience of the debugger
|
||||
func (cart *Cartridge) Eject() {
|
||||
cart.name = ejectedName
|
||||
cart.Filename = ejectedName
|
||||
cart.method = ejectedMethod
|
||||
cart.Hash = ejectedHash
|
||||
cart.NumBanks = 1
|
||||
cart.Bank = 0
|
||||
cart.readBanks(nil, cart.NumBanks)
|
||||
|
|
|
@ -9,13 +9,12 @@ import (
|
|||
"gopher2600/recorder"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Play sets the emulation running - without any debugging features
|
||||
func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording string, newRecording bool) error {
|
||||
playtv, err := sdl.NewGUI(tvMode, scaling, nil)
|
||||
func Play(cartridgeFile, tvType string, scaling float32, stable bool, recording string, newRecording bool) error {
|
||||
playtv, err := sdl.NewGUI(tvType, scaling, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error preparing television: %s", err)
|
||||
}
|
||||
|
@ -31,11 +30,6 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording
|
|||
}
|
||||
vcs.Ports.Player0.Attach(stk)
|
||||
|
||||
err = vcs.AttachCartridge(cartridgeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create default recording file name if no name has been supplied
|
||||
if newRecording && recording == "" {
|
||||
shortCartName := path.Base(cartridgeFile)
|
||||
|
@ -45,35 +39,67 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording
|
|||
recording = fmt.Sprintf("recording_%s_%s", shortCartName, timestamp)
|
||||
}
|
||||
|
||||
var rec *recorder.Recorder
|
||||
var plb *recorder.Playback
|
||||
|
||||
// note that we attach the cartridge in three different branches below - we
|
||||
// need to do this at different times depending on whether a new recording
|
||||
// or playback is taking place; or if it's just a regular playback
|
||||
|
||||
if recording != "" {
|
||||
if newRecording {
|
||||
recording, err := recorder.NewRecorder(recording, vcs)
|
||||
err = vcs.AttachCartridge(cartridgeFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error preparing VCS: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
rec, err = recorder.NewRecorder(recording, vcs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error preparing recording: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
recording.End()
|
||||
rec.End()
|
||||
}()
|
||||
|
||||
vcs.Ports.Player0.AttachTranscriber(recording)
|
||||
vcs.Ports.Player1.AttachTranscriber(recording)
|
||||
vcs.Panel.AttachTranscriber(recording)
|
||||
vcs.Ports.Player0.AttachTranscriber(rec)
|
||||
vcs.Ports.Player1.AttachTranscriber(rec)
|
||||
vcs.Panel.AttachTranscriber(rec)
|
||||
} else {
|
||||
recording, err := recorder.NewPlayback(recording, vcs)
|
||||
plb, err = recorder.NewPlayback(recording, vcs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error preparing VCS: %s", err)
|
||||
return fmt.Errorf("error playing back recording: %s", err)
|
||||
}
|
||||
|
||||
vcs.Ports.Player0.Attach(recording)
|
||||
vcs.Ports.Player1.Attach(recording)
|
||||
vcs.Panel.Attach(recording)
|
||||
vcs.Ports.Player0.Attach(plb)
|
||||
vcs.Ports.Player1.Attach(plb)
|
||||
vcs.Panel.Attach(plb)
|
||||
|
||||
if cartridgeFile != "" && cartridgeFile != plb.CartName {
|
||||
return fmt.Errorf("error playing back recording: cartridge name doesn't match the name in the recording")
|
||||
}
|
||||
|
||||
// if no cartridge filename has been provided then use the one in
|
||||
// the playback file
|
||||
cartridgeFile = plb.CartName
|
||||
|
||||
err = vcs.AttachCartridge(cartridgeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = vcs.AttachCartridge(cartridgeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// run while value of running variable is positive
|
||||
var running atomic.Value
|
||||
running.Store(0)
|
||||
// now that we've attached the cartridge check the hash against the
|
||||
// playback has (if it exists)
|
||||
if plb != nil && plb.CartHash != vcs.Mem.Cart.Hash {
|
||||
return fmt.Errorf("error playing back recording: cartridge hash doesn't match")
|
||||
}
|
||||
|
||||
// connect debugger to gui
|
||||
guiChannel := make(chan gui.Event, 2)
|
||||
|
@ -90,6 +116,7 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording
|
|||
|
||||
}
|
||||
|
||||
// run and handle gui events
|
||||
return vcs.Run(func() (bool, error) {
|
||||
select {
|
||||
case ev := <-guiChannel:
|
||||
|
|
74
recorder/fileformat.go
Normal file
74
recorder/fileformat.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package recorder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
fieldID int = iota
|
||||
fieldEvent
|
||||
fieldFrame
|
||||
fieldScanline
|
||||
fieldHorizPos
|
||||
fieldHash
|
||||
numFields
|
||||
)
|
||||
|
||||
const fieldSep = ", "
|
||||
|
||||
// playback file header format
|
||||
// ---------------------------
|
||||
//
|
||||
// # <cartridge name>
|
||||
// # <cartridge hash>
|
||||
// # <tv type>
|
||||
|
||||
const (
|
||||
lineCartName int = iota
|
||||
lineCartHash
|
||||
lineTVtype
|
||||
numHeaderLines
|
||||
)
|
||||
|
||||
func (rec *Recorder) writeHeader() error {
|
||||
lines := make([]string, numHeaderLines)
|
||||
|
||||
// add header information
|
||||
lines[lineCartName] = rec.vcs.Mem.Cart.Filename
|
||||
lines[lineCartHash] = rec.vcs.Mem.Cart.Hash
|
||||
lines[lineTVtype] = fmt.Sprintf("%v\n", rec.vcs.TV.GetSpec().ID)
|
||||
|
||||
line := strings.Join(lines, "\n")
|
||||
|
||||
n, err := io.WriteString(rec.output, line)
|
||||
|
||||
if err != nil {
|
||||
rec.output.Close()
|
||||
return errors.NewFormattedError(errors.RecordingError, err)
|
||||
}
|
||||
|
||||
if n != len(line) {
|
||||
rec.output.Close()
|
||||
return errors.NewFormattedError(errors.RecordingError, "output truncated")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (plb *Playback) readHeader(lines []string) error {
|
||||
// read header
|
||||
plb.CartName = lines[lineCartName]
|
||||
plb.CartHash = lines[lineCartHash]
|
||||
plb.TVtype = lines[lineTVtype]
|
||||
|
||||
// validate header
|
||||
tvspec := plb.vcs.TV.GetSpec()
|
||||
if tvspec.ID != lines[lineTVtype] {
|
||||
return errors.NewFormattedError(errors.PlaybackError, "current TV type does not match that in the recording")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"gopher2600/hardware"
|
||||
"gopher2600/hardware/peripherals"
|
||||
"gopher2600/television"
|
||||
"gopher2600/television/renderers"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -17,6 +18,10 @@ type event struct {
|
|||
frame int
|
||||
scanline int
|
||||
horizpos int
|
||||
hash string
|
||||
|
||||
// the line in the recording file the playback event appears
|
||||
line int
|
||||
}
|
||||
|
||||
type playbackSequence struct {
|
||||
|
@ -27,12 +32,19 @@ type playbackSequence struct {
|
|||
// Playback is an implementation of the controller interface. it reads from an
|
||||
// existing recording file and responds to GetInput() requests
|
||||
type Playback struct {
|
||||
CartName string
|
||||
CartHash string
|
||||
TVtype string
|
||||
|
||||
vcs *hardware.VCS
|
||||
digest *renderers.DigestTV
|
||||
sequences map[string]*playbackSequence
|
||||
}
|
||||
|
||||
// NewPlayback is hte preferred method of implementation for the Playback type
|
||||
func NewPlayback(transcript string, vcs *hardware.VCS) (*Playback, error) {
|
||||
var err error
|
||||
|
||||
// check we're working with correct information
|
||||
if vcs == nil || vcs.TV == nil {
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, "no playback hardware available")
|
||||
|
@ -41,73 +53,83 @@ func NewPlayback(transcript string, vcs *hardware.VCS) (*Playback, error) {
|
|||
plb := &Playback{vcs: vcs}
|
||||
plb.sequences = make(map[string]*playbackSequence)
|
||||
|
||||
// open file
|
||||
// create digesttv, piggybacking on the tv already being used by vcs
|
||||
plb.digest, err = renderers.NewDigestTV(vcs.TV.GetSpec().ID, vcs.TV)
|
||||
if err != nil {
|
||||
return nil, errors.NewFormattedError(errors.RecordingError, err)
|
||||
}
|
||||
|
||||
// open file; read the entirity of the contents; close file
|
||||
tf, err := os.Open(transcript)
|
||||
if err != nil {
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, err)
|
||||
}
|
||||
|
||||
buffer, err := ioutil.ReadAll(tf)
|
||||
if err != nil {
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, err)
|
||||
}
|
||||
err = tf.Close()
|
||||
if err != nil {
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, err)
|
||||
}
|
||||
|
||||
_ = tf.Close()
|
||||
|
||||
// convert buffer to an array of lines
|
||||
// convert file contents to an array of lines
|
||||
lines := strings.Split(string(buffer), "\n")
|
||||
|
||||
// read header
|
||||
tvspec := plb.vcs.TV.GetSpec()
|
||||
if tvspec.ID != lines[0] {
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, "current TV type does not match that in the recording")
|
||||
// read header and perform validation checks
|
||||
err = plb.readHeader(lines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// loop through transcript and divide events according to the first field
|
||||
// (the ID)
|
||||
for i := 1; i < len(lines)-1; i++ {
|
||||
for i := numHeaderLines; i < len(lines)-1; i++ {
|
||||
toks := strings.Split(lines[i], fieldSep)
|
||||
|
||||
// ignore lines that don't have enough fields
|
||||
if len(toks) != numFields {
|
||||
continue
|
||||
msg := fmt.Sprintf("expected %d fields at line %d", numFields, i+1)
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
|
||||
}
|
||||
|
||||
// add a new playbackSequence for the id if it doesn't exist
|
||||
id := toks[0]
|
||||
id := toks[fieldID]
|
||||
if _, ok := plb.sequences[id]; !ok {
|
||||
plb.sequences[id] = &playbackSequence{}
|
||||
}
|
||||
|
||||
// create a new event and convert tokens accordingly
|
||||
// any errors in the transcript causes failure
|
||||
event := event{}
|
||||
event := event{line: i + 1}
|
||||
|
||||
n, err := strconv.Atoi(toks[1])
|
||||
n, err := strconv.Atoi(toks[fieldEvent])
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:2], fieldSep)))
|
||||
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:2], fieldSep)))
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
|
||||
}
|
||||
event.event = peripherals.Event(n)
|
||||
|
||||
event.frame, err = strconv.Atoi(toks[2])
|
||||
event.frame, err = strconv.Atoi(toks[fieldFrame])
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:3], fieldSep)))
|
||||
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:3], fieldSep)))
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
|
||||
}
|
||||
|
||||
event.scanline, err = strconv.Atoi(toks[3])
|
||||
event.scanline, err = strconv.Atoi(toks[fieldScanline])
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:4], fieldSep)))
|
||||
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:4], fieldSep)))
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
|
||||
}
|
||||
|
||||
event.horizpos, err = strconv.Atoi(toks[4])
|
||||
event.horizpos, err = strconv.Atoi(toks[fieldHorizPos])
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:5], fieldSep)))
|
||||
msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:5], fieldSep)))
|
||||
return nil, errors.NewFormattedError(errors.PlaybackError, msg)
|
||||
}
|
||||
|
||||
event.hash = toks[fieldHash]
|
||||
|
||||
// add new event to list of events in the correct playback sequence
|
||||
seq := plb.sequences[id]
|
||||
seq.events = append(seq.events, event)
|
||||
|
@ -146,6 +168,11 @@ func (plb *Playback) GetInput(id string) (peripherals.Event, error) {
|
|||
// compare current state with the state in the transcript
|
||||
nextEvent := seq.events[seq.eventCt]
|
||||
if frame == nextEvent.frame && scanline == nextEvent.scanline && horizpos == nextEvent.horizpos {
|
||||
if nextEvent.hash != plb.digest.String() {
|
||||
msg := fmt.Sprintf("line %d", nextEvent.line)
|
||||
return peripherals.NoEvent, errors.NewFormattedError(errors.PlaybackHashError, msg)
|
||||
}
|
||||
|
||||
seq.eventCt++
|
||||
return nextEvent.event, nil
|
||||
}
|
||||
|
|
|
@ -6,32 +6,39 @@ import (
|
|||
"gopher2600/hardware"
|
||||
"gopher2600/hardware/peripherals"
|
||||
"gopher2600/television"
|
||||
"gopher2600/television/renderers"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
const fieldSep = ", "
|
||||
const numFields = 5
|
||||
|
||||
// Recorder records controller events to disk, intended for future playback
|
||||
type Recorder struct {
|
||||
vcs *hardware.VCS
|
||||
digest *renderers.DigestTV
|
||||
output *os.File
|
||||
}
|
||||
|
||||
// NewRecorder is the preferred method of implementation for the FileRecorder type
|
||||
func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
|
||||
var err error
|
||||
|
||||
// check we're working with correct information
|
||||
if vcs == nil || vcs.TV == nil {
|
||||
return nil, errors.NewFormattedError(errors.RecordingError, "hardware is not suitable for recording")
|
||||
}
|
||||
|
||||
scr := &Recorder{vcs: vcs}
|
||||
rec := &Recorder{vcs: vcs}
|
||||
|
||||
// create digesttv, piggybacking on the tv already being used by vcs
|
||||
rec.digest, err = renderers.NewDigestTV(vcs.TV.GetSpec().ID, vcs.TV)
|
||||
if err != nil {
|
||||
return nil, errors.NewFormattedError(errors.RecordingError, err)
|
||||
}
|
||||
|
||||
// open file
|
||||
_, err := os.Stat(transcript)
|
||||
_, err = os.Stat(transcript)
|
||||
if os.IsNotExist(err) {
|
||||
scr.output, err = os.Create(transcript)
|
||||
rec.output, err = os.Create(transcript)
|
||||
if err != nil {
|
||||
return nil, errors.NewFormattedError(errors.RecordingError, "can't create file")
|
||||
}
|
||||
|
@ -39,26 +46,17 @@ func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
|
|||
return nil, errors.NewFormattedError(errors.RecordingError, "file already exists")
|
||||
}
|
||||
|
||||
// add header information
|
||||
tvspec := scr.vcs.TV.GetSpec()
|
||||
line := fmt.Sprintf("%v\n", tvspec.ID)
|
||||
|
||||
n, err := io.WriteString(scr.output, line)
|
||||
err = rec.writeHeader()
|
||||
if err != nil {
|
||||
scr.output.Close()
|
||||
return nil, errors.NewFormattedError(errors.RecordingError, err)
|
||||
}
|
||||
if n != len(line) {
|
||||
scr.output.Close()
|
||||
return nil, errors.NewFormattedError(errors.RecordingError, "output truncated")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scr, nil
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// End closes the output file.
|
||||
func (scr *Recorder) End() error {
|
||||
err := scr.output.Close()
|
||||
func (rec *Recorder) End() error {
|
||||
err := rec.output.Close()
|
||||
if err != nil {
|
||||
return errors.NewFormattedError(errors.RecordingError, err)
|
||||
}
|
||||
|
@ -67,38 +65,49 @@ func (scr *Recorder) End() error {
|
|||
}
|
||||
|
||||
// Transcribe implements the Transcriber interface
|
||||
func (scr *Recorder) Transcribe(id string, event peripherals.Event) error {
|
||||
func (rec *Recorder) Transcribe(id string, event peripherals.Event) error {
|
||||
// don't do anything if event is the NoEvent
|
||||
if event == peripherals.NoEvent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanity checks
|
||||
if scr.output == nil {
|
||||
if rec.output == nil {
|
||||
return errors.NewFormattedError(errors.RecordingError, "recording file is not open")
|
||||
}
|
||||
|
||||
if scr.vcs == nil || scr.vcs.TV == nil {
|
||||
if rec.vcs == nil || rec.vcs.TV == nil {
|
||||
return errors.NewFormattedError(errors.RecordingError, "hardware is not suitable for recording")
|
||||
}
|
||||
|
||||
// create line and write to file
|
||||
frame, err := scr.vcs.TV.GetState(television.ReqFramenum)
|
||||
frame, err := rec.vcs.TV.GetState(television.ReqFramenum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanline, err := scr.vcs.TV.GetState(television.ReqScanline)
|
||||
scanline, err := rec.vcs.TV.GetState(television.ReqScanline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
horizpos, err := scr.vcs.TV.GetState(television.ReqHorizPos)
|
||||
horizpos, err := rec.vcs.TV.GetState(television.ReqHorizPos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%v%s%v%s%v%s%v%s%v\n", id, fieldSep, event, fieldSep, frame, fieldSep, scanline, fieldSep, horizpos)
|
||||
line := fmt.Sprintf("%v%s%v%s%v%s%v%s%v%s%v\n", id,
|
||||
fieldSep,
|
||||
event,
|
||||
fieldSep,
|
||||
frame,
|
||||
fieldSep,
|
||||
scanline,
|
||||
fieldSep,
|
||||
horizpos,
|
||||
fieldSep,
|
||||
rec.digest.String(),
|
||||
)
|
||||
|
||||
n, err := io.WriteString(scr.output, line)
|
||||
n, err := io.WriteString(rec.output, line)
|
||||
if err != nil {
|
||||
return errors.NewFormattedError(errors.RecordingError, err)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package regression
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"gopher2600/errors"
|
||||
|
@ -12,29 +11,15 @@ import (
|
|||
|
||||
const regressionDBFile = ".gopher2600/regressionDB"
|
||||
|
||||
func getCartridgeHash(cartridgeFile string) (string, error) {
|
||||
f, err := os.Open(cartridgeFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
key := sha1.New()
|
||||
if _, err := io.Copy(key, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", key.Sum(nil)), nil
|
||||
}
|
||||
|
||||
type regressionEntry struct {
|
||||
cartridgeHash string
|
||||
cartridgePath string
|
||||
tvMode string
|
||||
numOFrames int
|
||||
screenDigest string
|
||||
}
|
||||
|
||||
const numFields = 4
|
||||
|
||||
func (entry regressionEntry) String() string {
|
||||
return fmt.Sprintf("%s [%s] frames=%d", entry.cartridgePath, entry.tvMode, entry.numOFrames)
|
||||
}
|
||||
|
@ -62,36 +47,37 @@ func startSession() (*regressionDB, error) {
|
|||
return db, nil
|
||||
}
|
||||
|
||||
func (db *regressionDB) endSession() error {
|
||||
func (db *regressionDB) endSession(commitChanges bool) error {
|
||||
// write entries to regression database
|
||||
csvw := csv.NewWriter(db.dbfile)
|
||||
if commitChanges {
|
||||
csvw := csv.NewWriter(db.dbfile)
|
||||
|
||||
err := db.dbfile.Truncate(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db.dbfile.Seek(0, os.SEEK_SET)
|
||||
|
||||
for _, entry := range db.entries {
|
||||
rec := make([]string, 5)
|
||||
rec[0] = entry.cartridgeHash
|
||||
rec[1] = entry.cartridgePath
|
||||
rec[2] = entry.tvMode
|
||||
rec[3] = strconv.Itoa(entry.numOFrames)
|
||||
rec[4] = entry.screenDigest
|
||||
|
||||
err := csvw.Write(rec)
|
||||
err := db.dbfile.Truncate(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// make sure everything's been written
|
||||
csvw.Flush()
|
||||
err = csvw.Error()
|
||||
if err != nil {
|
||||
return err
|
||||
db.dbfile.Seek(0, os.SEEK_SET)
|
||||
|
||||
for _, entry := range db.entries {
|
||||
rec := make([]string, numFields)
|
||||
rec[0] = entry.cartridgePath
|
||||
rec[1] = entry.tvMode
|
||||
rec[2] = strconv.Itoa(entry.numOFrames)
|
||||
rec[3] = entry.screenDigest
|
||||
|
||||
err := csvw.Write(rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// make sure everything's been written
|
||||
csvw.Flush()
|
||||
err = csvw.Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// end session by closing file
|
||||
|
@ -114,7 +100,7 @@ func (db *regressionDB) readEntries() error {
|
|||
csvr.Comment = rune('#')
|
||||
csvr.TrimLeadingSpace = true
|
||||
csvr.ReuseRecord = true
|
||||
csvr.FieldsPerRecord = 5
|
||||
csvr.FieldsPerRecord = numFields
|
||||
|
||||
db.dbfile.Seek(0, os.SEEK_SET)
|
||||
|
||||
|
@ -128,20 +114,19 @@ func (db *regressionDB) readEntries() error {
|
|||
return err
|
||||
}
|
||||
|
||||
numOfFrames, err := strconv.Atoi(rec[3])
|
||||
numOfFrames, err := strconv.Atoi(rec[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add entry to database
|
||||
entry := regressionEntry{
|
||||
cartridgeHash: rec[0],
|
||||
cartridgePath: rec[1],
|
||||
tvMode: rec[2],
|
||||
cartridgePath: rec[0],
|
||||
tvMode: rec[1],
|
||||
numOFrames: numOfFrames,
|
||||
screenDigest: rec[4]}
|
||||
screenDigest: rec[3]}
|
||||
|
||||
db.entries[entry.cartridgeHash] = entry
|
||||
db.entries[entry.cartridgePath] = entry
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -153,7 +138,7 @@ func addCartridge(cartridgeFile string, tvMode string, numOfFrames int, allowUpd
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.endSession()
|
||||
defer db.endSession(true)
|
||||
|
||||
// run cartdrige and get digest
|
||||
digest, err := run(cartridgeFile, tvMode, numOfFrames)
|
||||
|
@ -161,20 +146,14 @@ func addCartridge(cartridgeFile string, tvMode string, numOfFrames int, allowUpd
|
|||
return err
|
||||
}
|
||||
|
||||
// add new entry to database
|
||||
key, err := getCartridgeHash(cartridgeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry := regressionEntry{
|
||||
cartridgeHash: key,
|
||||
cartridgePath: cartridgeFile,
|
||||
tvMode: tvMode,
|
||||
numOFrames: numOfFrames,
|
||||
screenDigest: digest}
|
||||
|
||||
if allowUpdate == false {
|
||||
if existEntry, ok := db.entries[entry.cartridgeHash]; ok {
|
||||
if existEntry, ok := db.entries[entry.cartridgePath]; ok {
|
||||
if existEntry.cartridgePath == entry.cartridgePath {
|
||||
return errors.NewFormattedError(errors.RegressionEntryExists, entry)
|
||||
}
|
||||
|
@ -183,7 +162,7 @@ func addCartridge(cartridgeFile string, tvMode string, numOfFrames int, allowUpd
|
|||
}
|
||||
}
|
||||
|
||||
db.entries[entry.cartridgeHash] = entry
|
||||
db.entries[entry.cartridgePath] = entry
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,18 +14,13 @@ func RegressDeleteCartridge(cartridgeFile string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.endSession()
|
||||
defer db.endSession(true)
|
||||
|
||||
key, err := getCartridgeHash(cartridgeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := db.entries[key]; ok == false {
|
||||
if _, ok := db.entries[cartridgeFile]; ok == false {
|
||||
return errors.NewFormattedError(errors.RegressionEntryDoesNotExist, cartridgeFile)
|
||||
}
|
||||
|
||||
delete(db.entries, key)
|
||||
delete(db.entries, cartridgeFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -46,7 +41,7 @@ func RegressRunTests(output io.Writer, failOnError bool) (int, int, error) {
|
|||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
defer db.endSession()
|
||||
defer db.endSession(false)
|
||||
|
||||
numSucceed := 0
|
||||
numFail := 0
|
||||
|
|
Loading…
Add table
Reference in a new issue