timeline window now runs the rewind.GetState()/thumbnailer in the main emulation goroutine

the thumbnailer does the actual emulation in a new goroutine so there's
no lag here but it's necessary to PushRawEvent() so that
rewind.GetState() doesn't race
This commit is contained in:
JetSetIlly 2021-11-18 16:41:25 +00:00
parent eecd5aa8c1
commit 249e3c4b60
9 changed files with 113 additions and 73 deletions

View file

@ -3,13 +3,13 @@ compileFlags = '-c 3 -B -wb=false'
# profilingRom = roms/Homebrew/hs_2600.bin
# profilingRom = roms/Homebrew/CDF/galaga_dmo_v2_NTSC.bin
# profilingRom = roms/Homebrew/DPC+ARM/ZaxxonHDDemo_150927_NTSC.bin
# profilingRom = roms/Rsboxing.bin
profilingRom = roms/Rsboxing.bin
# profilingRom = "test_roms/plusrom/sokoboo Plus.bin"
# profilingRom = "roms/starpath/02 - Communist Mutants From Space (Ntsc).mp3"
# profilingRom = "roms/The Official Frogger.bin"
# profilingRom = roms/Homebrew/CDF/gorfarc_20201231_demo1_NTSC.bin
# profilingRom = roms/Pitfall.bin
profilingRom =
# profilingRom =
.PHONY: all clean tidy generate check_lint lint check_glsl glsl_validate check_pandoc readme_spell test race race_debug profile profile_cpu profile_cpu_again profile_mem_debug profile_trace build_assertions build check_upx release release_statsview cross_windows cross_windows_statsview binaries check_gotip build_with_gotip

View file

@ -40,7 +40,7 @@ func newLazyRewind(val *LazyValues) *LazyRewind {
func (lz *LazyRewind) push() {
lz.timeline.Store(lz.val.dbg.Rewind.GetTimeline())
lz.comparison.Store(lz.val.dbg.Rewind.GetComparison())
lz.comparison.Store(lz.val.dbg.Rewind.GetComparisonState())
}
func (lz *LazyRewind) update() {

View file

@ -17,7 +17,6 @@ package sdlimgui
import (
"fmt"
"image"
"os"
"path/filepath"
"strings"
@ -52,7 +51,6 @@ type winSelectROM struct {
controlHeight float32
thmb *thumbnailer.Thumbnailer
thmbReceive chan *image.RGBA
thmbTexture uint32
}
@ -67,7 +65,6 @@ func newFileSelector(img *SdlImgui) (window, error) {
var err error
// create a new thumbnailer instance
win.thmb, err = thumbnailer.NewThumbnailer()
if err != nil {
return nil, curated.Errorf("debugger: %v", err)
@ -127,8 +124,9 @@ func (win *winSelectROM) setOpen(open bool) {
}
func (win *winSelectROM) draw() {
// receive new thumbnail data and copy to texture
select {
case img := <-win.thmbReceive:
case img := <-win.thmb.Render:
if img != nil {
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(img.Stride)/4)
defer gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
@ -335,5 +333,5 @@ func (win *winSelectROM) setSelectedFile(filename string) {
}
cartload.EmulationLabel = thumbnailer.EmulationLabel
win.thmbReceive = win.thmb.CreateFromLoader(cartload, thumbnailer.UndefinedNumFrames)
win.thmb.CreateFromLoader(cartload, thumbnailer.UndefinedNumFrames)
}

View file

@ -17,7 +17,6 @@ package sdlimgui
import (
"fmt"
"image"
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/inkyblackness/imgui-go/v4"
@ -35,11 +34,11 @@ type winTimeline struct {
// whether the rewind "slider" is active
rewindingActive bool
// thumbnailer will be using emulation states created in the main emulation
// goroutine so we must thumbnail those states in the same goroutine.
thmb *thumbnailer.Thumbnailer
thmbReceive chan *image.RGBA
thmbTexture uint32
thumbnailFrame int
thmbFrame int
}
func newWinTimeline(img *SdlImgui) (window, error) {
@ -49,7 +48,6 @@ func newWinTimeline(img *SdlImgui) (window, error) {
var err error
// create a new thumbnailer instance
win.thmb, err = thumbnailer.NewThumbnailer()
if err != nil {
return nil, curated.Errorf("debugger: %v", err)
@ -79,8 +77,9 @@ func (win *winTimeline) setOpen(open bool) {
}
func (win *winTimeline) draw() {
// receive new thumbnail data and copy to texture
select {
case img := <-win.thmbReceive:
case img := <-win.thmb.Render:
if img != nil {
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(img.Stride)/4)
defer gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
@ -171,7 +170,7 @@ func (win *winTimeline) drawTimeline() {
// scanline trace
traceSize = imgui.Vec2{X: availableWidth, Y: 50}
imgui.BeginChildV("##timelinescanlinetrace", traceSize, false, imgui.WindowFlagsNoMove)
imgui.BeginChildV("##timelinetrace", traceSize, false, imgui.WindowFlagsNoMove)
pos = imgui.CursorScreenPos()
x := pos.X
@ -244,7 +243,7 @@ func (win *winTimeline) drawTimeline() {
// input trace
// TODO: right player and panel input
traceSize = imgui.Vec2{X: availableWidth, Y: inputHeight}
imgui.BeginChildV("##timelineinputtrace", traceSize, false, imgui.WindowFlagsNoMove)
imgui.BeginChildV("##timelinetrace_input", traceSize, false, imgui.WindowFlagsNoMove)
pos = imgui.CursorScreenPos()
x = pos.X
y := pos.Y
@ -263,7 +262,7 @@ func (win *winTimeline) drawTimeline() {
// rewind range indicator
traceSize = imgui.Vec2{X: availableWidth, Y: rangeHeight}
imgui.BeginChildV("##timelineindicators", traceSize, false, imgui.WindowFlagsNoMove)
imgui.BeginChildV("##timelinetrace_indicators", traceSize, false, imgui.WindowFlagsNoMove)
pos = imgui.CursorScreenPos()
dl.AddRectFilled(imgui.Vec2{X: pos.X + float32((timeline.AvailableStart-rewindOffset)*traceWidth), Y: pos.Y},
@ -274,7 +273,7 @@ func (win *winTimeline) drawTimeline() {
// frame indicators
traceSize = imgui.Vec2{X: availableWidth, Y: frameIndicatorRadius}
imgui.BeginChildV("##timelinecurrent", traceSize, false, imgui.WindowFlagsNoMove)
imgui.BeginChildV("##timelinetrace_current", traceSize, false, imgui.WindowFlagsNoMove)
pos = imgui.CursorScreenPos()
// comparison frame indicator
@ -309,7 +308,8 @@ func (win *winTimeline) drawTimeline() {
rewindEndFrame := win.img.lz.Rewind.Timeline.AvailableEnd
rewindHoverFrame := int(hoverX/traceWidth) + rewindOffset
// rewind "slider" is attached to scanline trace
// hover and clicking works on the group
if imgui.IsMouseDown(0) && (hovered || win.rewindingActive) {
win.rewindingActive = true
@ -380,9 +380,15 @@ func (win *winTimeline) drawTimeline() {
imgui.TableNextColumn()
// selecting the correct thumbnail requires different indexing than the timline
if win.thumbnailFrame != rewindHoverFrame {
win.thmbReceive = win.thmb.CreateFromState(win.img.dbg.Rewind.GetState(rewindHoverFrame), 1)
win.thumbnailFrame = rewindHoverFrame
if win.thmbFrame != rewindHoverFrame {
win.thmbFrame = rewindHoverFrame
// Rewind.GetState must be run in the emulation thread. CreateFromState doesn't need to be but
// because the thumbnailer runs in its own goroutine there's no real time penalty on the main
// emulation even when it is running
win.img.dbg.PushRawEvent(func() {
win.thmb.CreateFromState(win.img.dbg.Rewind.GetState(rewindHoverFrame), 1)
})
}
imgui.Image(imgui.TextureID(win.thmbTexture), imgui.Vec2{specification.ClksVisible * 3, specification.AbsoluteMaxScanlines}.Times(0.3))

View file

@ -214,3 +214,13 @@ func (vcs *VCS) SetClockSpeed(tvSpec string) error {
}
return curated.Errorf("vcs: cannot set clock speed for unknown tv specification (%s)", tvSpec)
}
// DetatchEmulationExtras removes all possible monitors, recorders, etc. from
// the emulation. Currently this mean: the TIA audio tracker, the RIOT event
// recorders and playback, and RIOT plug monitor.
func (vcs *VCS) DetatchEmulationExtras() {
vcs.TIA.Audio.SetTracker(nil)
vcs.RIOT.Ports.AttachEventRecorder(nil)
vcs.RIOT.Ports.AttachPlayback(nil)
vcs.RIOT.Ports.AttachPlugMonitor(nil)
}

View file

@ -41,15 +41,6 @@
// next snapshot is taken (meaning that there is only ever one execution state
// in the history at any one time and that it will be at the end).
//
// Do not call RecordFrameState() or RecordExecutionState() between CPU
// instruction boundaries - the program will intentionally panic if this is
// attempted.
//
// Not being able to snapshot state in between CPU instructions isn't as
// limiting as you may think. The Goto() function can be used to specify frame
// coordinates to the video cycle level and will take care of finding the
// nearest snapshot and "catching up" from there.
//
// Snapshots are stored in frame order from the splice point. The splice point
// will be wherever the snapshot history has been rewound to. For example, in a
// history of length 100 frames: the emulation has rewound back to frame 50.

View file

@ -286,13 +286,6 @@ func (r *Rewind) snapshot(level snapshotLevel) *State {
return snapshot(r.vcs, level)
}
// GetCurrentState returns a temporary snapshot of the current state.
//
// It is not added to the rewind history.
func (r *Rewind) GetCurrentState() *State {
return r.snapshot(levelTemporary)
}
// RecordState should be called after every CPU instruction. A new state will
// be recorded if the current rewind policy agrees.
func (r *Rewind) RecordState() {
@ -571,11 +564,6 @@ func (r *Rewind) SetComparison() {
r.comparison = r.GetCurrentState()
}
// GetComparison gets a reference to current comparison point.
func (r *Rewind) GetComparison() *State {
return r.comparison
}
// NewFrame is in an implementation of television.FrameTrigger.
func (r *Rewind) NewFrame(frameInfo television.FrameInfo) error {
r.addTimelineEntry(frameInfo)
@ -583,9 +571,30 @@ func (r *Rewind) NewFrame(frameInfo television.FrameInfo) error {
return nil
}
// GetState returns a copy for the nearest state for the indicated frame.
func (r *Rewind) GetState(frame int) *State {
// get nearest index of entry from which we can being to (re)generate the
// current frame
res := r.findFrameIndex(frame)
return r.entries[res.nearestIdx]
s := r.entries[res.nearestIdx]
// return copy of state
return &State{
TV: s.TV.Snapshot(),
CPU: s.CPU.Snapshot(),
Mem: s.Mem.Snapshot(),
RIOT: s.RIOT.Snapshot(),
TIA: s.TIA.Snapshot(),
}
}
// GetCurrentState returns a temporary snapshot of the current state.
func (r *Rewind) GetCurrentState() *State {
return r.snapshot(levelTemporary)
}
// GetComparisonState gets a reference to current comparison point. This is not
// a copy of the state but the actual state.
func (r *Rewind) GetComparisonState() *State {
return r.comparison
}

20
thumbnailer/doc.go Normal file
View file

@ -0,0 +1,20 @@
// 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 thumbnailer can be used to create a single thumbnail or a series of
// thumbnail images with the CreateFromLoader() or CreateFromState() functions.
//
// Thumbnailers can be created to run synchronously or asynchronously.
package thumbnailer

View file

@ -13,8 +13,6 @@
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package thumbnailer can be used to create a single thumbnail or a series of
// thumbnail images with the Create() function.
package thumbnailer
import (
@ -49,7 +47,7 @@ type Thumbnailer struct {
emulationQuit chan bool
emulationCompleted chan bool
renderChannel chan *image.RGBA
Render chan *image.RGBA
}
// NewThumbnailer is the preferred method of initialisation for the Thumbnailer type.
@ -57,7 +55,7 @@ func NewThumbnailer() (*Thumbnailer, error) {
thmb := &Thumbnailer{
emulationQuit: make(chan bool, 1),
emulationCompleted: make(chan bool, 1),
renderChannel: make(chan *image.RGBA, 1),
Render: make(chan *image.RGBA, 1),
}
// emulation has completed, by definition, on startup
@ -80,16 +78,9 @@ func NewThumbnailer() (*Thumbnailer, error) {
thmb.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
// clear pixels. setting the alpha channel so we don't have to later (the
// alpha channel never changes)
for y := 0; y < thmb.img.Bounds().Size().Y; y++ {
for x := 0; x < thmb.img.Bounds().Size().X; x++ {
thmb.img.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
}
}
// start with a NTSC television as default
thmb.Resize(television.NewFrameInfo(specification.SpecNTSC))
thmb.Reset()
return thmb, nil
}
@ -128,9 +119,7 @@ const UndefinedNumFrames = -1
// CreateFromLoader will cause images to be returned from a running emulation initialised
// with the specified cartridge loader. The emulation will run for a number of
// frames before ending.
//
// It returns the channel over which new frames will be sent.
func (thmb *Thumbnailer) CreateFromLoader(loader cartridgeloader.Loader, numFrames int) chan *image.RGBA {
func (thmb *Thumbnailer) CreateFromLoader(loader cartridgeloader.Loader, numFrames int) {
thmb.wait()
go func() {
@ -163,16 +152,14 @@ func (thmb *Thumbnailer) CreateFromLoader(loader cartridgeloader.Loader, numFram
return
}
}()
return thmb.renderChannel
}
// CreateFromState will cause images to be returned from a running emulation initialised
// with the specified cartridge loader. The emulation will run for a number of
// frames before ending.
// CreateFromState will cause images to be returned from a running emulation
// initialised with the specified cartridge loader. The emulation will run for
// a number of frames before ending.
//
// It returns the channel over which new frames will be sent.
func (thmb *Thumbnailer) CreateFromState(state *rewind.State, numFrames int) chan *image.RGBA {
// See comment for rewind.GetState() about how to use handle rewind.State.
func (thmb *Thumbnailer) CreateFromState(state *rewind.State, numFrames int) {
thmb.wait()
go func() {
@ -181,10 +168,11 @@ func (thmb *Thumbnailer) CreateFromState(state *rewind.State, numFrames int) cha
}()
rewind.Plumb(thmb.vcs, state, true)
thmb.vcs.TIA.Audio.SetTracker(nil)
thmb.vcs.RIOT.Ports.AttachEventRecorder(nil)
thmb.vcs.RIOT.Ports.AttachPlayback(nil)
thmb.vcs.RIOT.Ports.AttachPlugMonitor(nil)
// the state we've just plumbed into the thumbnailing emulation is from
// a different emulation which potentially has some links to that
// emulator still remaining
thmb.vcs.DetatchEmulationExtras()
tgtFrame := thmb.vcs.TV.GetCoords().Frame + numFrames
@ -205,8 +193,6 @@ func (thmb *Thumbnailer) CreateFromState(state *rewind.State, numFrames int) cha
return
}
}()
return thmb.renderChannel
}
// Resize implements the television.PixelRenderer interface.
@ -224,6 +210,10 @@ func (thmb *Thumbnailer) Resize(frameInfo television.FrameInfo) error {
// NewFrame implements the television.PixelRenderer interface.
func (thmb *Thumbnailer) NewFrame(frameInfo television.FrameInfo) error {
if !frameInfo.Stable {
return nil
}
thmb.frameInfo = frameInfo
img := *thmb.cropImg
@ -231,7 +221,7 @@ func (thmb *Thumbnailer) NewFrame(frameInfo television.FrameInfo) error {
copy(img.Pix, thmb.cropImg.Pix)
select {
case thmb.renderChannel <- &img:
case thmb.Render <- &img:
default:
}
@ -270,6 +260,22 @@ func (thmb *Thumbnailer) SetPixels(sig []signal.SignalAttributes, last int) erro
// Reset implements the television.PixelRenderer interface.
func (thmb *Thumbnailer) Reset() {
// clear pixels. setting the alpha channel so we don't have to later (the
// alpha channel never changes)
for y := 0; y < thmb.img.Bounds().Size().Y; y++ {
for x := 0; x < thmb.img.Bounds().Size().X; x++ {
thmb.img.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
}
}
img := *thmb.cropImg
img.Pix = make([]uint8, len(thmb.cropImg.Pix))
copy(img.Pix, thmb.cropImg.Pix)
select {
case thmb.Render <- &img:
default:
}
}
// EndRendering implements the television.PixelRenderer interface.