mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 20:18:20 -04:00
thumbnail preview on timeline hover
introduced PlumbFromDifferentEmulation interface. the ARM emulation doesn't like being moved between emulation instances so the ARM is recreated when plumbing a state that originated in a different emulation. not all mappers need to implement this interface.
This commit is contained in:
parent
98b7b13c0b
commit
4f4bee0b36
|
@ -451,8 +451,8 @@ frame is indicated by the orange circle.
|
|||
<img src=".screenshots/timeline_window.png" width="500" alt="timeline window"/>
|
||||
|
||||
Hovering over the timeline will show details of the frame (number of scanlines,
|
||||
percentage of WSYNC usage, etc.). Clicking on the timeline will instantly take
|
||||
the emulation to that state.
|
||||
percentage of WSYNC usage, etc.) and thumbnail preview. Clicking on the
|
||||
timeline will instantly take the emulation to that state.
|
||||
|
||||
The `TV Screen` window is fully interactive and clicking or dragging on any
|
||||
portion of the screen will take the emulation to that scanline/clock of the
|
||||
|
|
|
@ -277,6 +277,8 @@ func (rnd *glsl) render() {
|
|||
shader = rnd.shaders[playscrShaderID]
|
||||
case rnd.img.wm.selectROM.thmbTexture:
|
||||
shader = rnd.shaders[colorShaderID]
|
||||
case rnd.img.wm.timeline.thmbTexture:
|
||||
shader = rnd.shaders[colorShaderID]
|
||||
default:
|
||||
shader = rnd.shaders[guiShaderID]
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ type manager struct {
|
|||
// the following fields are provided for convenience.
|
||||
dbgScr *winDbgScr
|
||||
selectROM *winSelectROM
|
||||
timeline *winTimeline
|
||||
|
||||
// the position of the screen on the current display. the SDL function
|
||||
// Window.GetPosition() is unsuitable for use in conjunction with imgui
|
||||
|
@ -173,6 +174,7 @@ func newManager(img *SdlImgui) (*manager, error) {
|
|||
// get references to specific windows that need to be referenced elsewhere in the system
|
||||
wm.dbgScr = wm.windows[winDbgScrID].(*winDbgScr)
|
||||
wm.selectROM = wm.windows[winSelectROMID].(*winSelectROM)
|
||||
wm.timeline = wm.windows[winTimelineID].(*winTimeline)
|
||||
|
||||
return wm, nil
|
||||
}
|
||||
|
|
|
@ -335,5 +335,5 @@ func (win *winSelectROM) setSelectedFile(filename string) {
|
|||
}
|
||||
cartload.EmulationLabel = thumbnailer.EmulationLabel
|
||||
|
||||
win.thmbReceive = win.thmb.Create(cartload, thumbnailer.UndefinedNumFrames)
|
||||
win.thmbReceive = win.thmb.CreateFromLoader(cartload, thumbnailer.UndefinedNumFrames)
|
||||
}
|
||||
|
|
|
@ -17,9 +17,13 @@ package sdlimgui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/go-gl/gl/v3.2-core/gl"
|
||||
"github.com/inkyblackness/imgui-go/v4"
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
||||
"github.com/jetsetilly/gopher2600/thumbnailer"
|
||||
)
|
||||
|
||||
const winTimelineID = "Timeline"
|
||||
|
@ -30,12 +34,32 @@ type winTimeline struct {
|
|||
|
||||
// whether the rewind "slider" is active
|
||||
rewindingActive bool
|
||||
|
||||
thmb *thumbnailer.Thumbnailer
|
||||
thmbReceive chan *image.RGBA
|
||||
thmbTexture uint32
|
||||
|
||||
thumbnailFrame int
|
||||
}
|
||||
|
||||
func newWinTimeline(img *SdlImgui) (window, error) {
|
||||
win := &winTimeline{
|
||||
img: img,
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// create a new thumbnailer instance
|
||||
win.thmb, err = thumbnailer.NewThumbnailer()
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &win.thmbTexture)
|
||||
gl.BindTexture(gl.TEXTURE_2D, win.thmbTexture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
|
||||
return win, nil
|
||||
}
|
||||
|
||||
|
@ -55,6 +79,21 @@ func (win *winTimeline) setOpen(open bool) {
|
|||
}
|
||||
|
||||
func (win *winTimeline) draw() {
|
||||
select {
|
||||
case img := <-win.thmbReceive:
|
||||
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.thmbTexture)
|
||||
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
|
||||
}
|
||||
|
@ -266,57 +305,90 @@ func (win *winTimeline) drawTimeline() {
|
|||
hovered := imgui.IsItemHoveredV(imgui.HoveredFlagsAllowWhenOverlapped)
|
||||
hoverX := imgui.MousePos().X - pos.X
|
||||
|
||||
rewindStartFrame := win.img.lz.Rewind.Timeline.AvailableStart
|
||||
rewindEndFrame := win.img.lz.Rewind.Timeline.AvailableEnd
|
||||
rewindHoverFrame := int(hoverX/traceWidth) + rewindOffset
|
||||
|
||||
// rewind "slider" is attached to scanline trace
|
||||
if imgui.IsMouseDown(0) && (hovered || win.rewindingActive) {
|
||||
s := win.img.lz.Rewind.Timeline.AvailableStart
|
||||
e := win.img.lz.Rewind.Timeline.AvailableEnd
|
||||
|
||||
fr := int(hoverX/traceWidth) + rewindOffset
|
||||
win.rewindingActive = true
|
||||
|
||||
// making sure we only call PushRewind() when we need to. also,
|
||||
// allowing mouse to travel beyond the rewind boundaries (and without
|
||||
// calling PushRewind() too often)
|
||||
if fr >= e {
|
||||
if win.img.lz.TV.Coords.Frame < e {
|
||||
win.img.dbg.RewindToFrame(fr, true)
|
||||
if rewindHoverFrame >= rewindEndFrame {
|
||||
if win.img.lz.TV.Coords.Frame < rewindEndFrame {
|
||||
win.img.dbg.RewindToFrame(rewindHoverFrame, true)
|
||||
}
|
||||
} else if fr <= s {
|
||||
if win.img.lz.TV.Coords.Frame > s {
|
||||
win.img.dbg.RewindToFrame(fr, false)
|
||||
} else if rewindHoverFrame <= rewindStartFrame {
|
||||
if win.img.lz.TV.Coords.Frame > rewindStartFrame {
|
||||
win.img.dbg.RewindToFrame(rewindHoverFrame, false)
|
||||
}
|
||||
} else if fr != win.img.lz.TV.Coords.Frame {
|
||||
win.img.dbg.RewindToFrame(fr, fr == e)
|
||||
} else if rewindHoverFrame != win.img.lz.TV.Coords.Frame {
|
||||
win.img.dbg.RewindToFrame(rewindHoverFrame, rewindHoverFrame == rewindEndFrame)
|
||||
}
|
||||
} else {
|
||||
win.rewindingActive = false
|
||||
|
||||
if hovered && len(win.img.lz.Rewind.Timeline.FrameNum) > 0 {
|
||||
fr := int(hoverX/traceWidth) + traceOffset
|
||||
s := win.img.lz.Rewind.Timeline.FrameNum[0]
|
||||
e := win.img.lz.Rewind.Timeline.FrameNum[len(win.img.lz.Rewind.Timeline.FrameNum)-1]
|
||||
traceHoverIdx := int(hoverX/traceWidth) + traceOffset
|
||||
traceStartFrame := win.img.lz.Rewind.Timeline.FrameNum[0]
|
||||
traceEndFrame := win.img.lz.Rewind.Timeline.FrameNum[len(win.img.lz.Rewind.Timeline.FrameNum)-1]
|
||||
traceHoverFrame := traceHoverIdx + traceStartFrame
|
||||
|
||||
if traceHoverFrame >= traceStartFrame && traceHoverFrame <= traceEndFrame {
|
||||
thumbnail := rewindHoverFrame >= rewindStartFrame && rewindHoverFrame <= rewindEndFrame
|
||||
|
||||
if fr >= s && fr <= e {
|
||||
imgui.BeginTooltip()
|
||||
imgui.Text(fmt.Sprintf("Frame: %d", fr))
|
||||
if fr >= s && fr <= e {
|
||||
|
||||
flgs := imgui.TableFlagsNone
|
||||
if thumbnail {
|
||||
flgs = imgui.TableFlagsPadOuterX
|
||||
}
|
||||
if imgui.BeginTableV("timelineTooltip", 2, flgs, imgui.Vec2{}, 10.0) {
|
||||
imgui.TableNextRow()
|
||||
imgui.TableNextColumn()
|
||||
|
||||
imgui.Text(fmt.Sprintf("Frame: %d", traceHoverFrame))
|
||||
|
||||
// adjust text color slightly - the colors we use for the
|
||||
// plots are too dark
|
||||
textColAdj := imgui.Vec4{0.2, 0.2, 0.2, 0.0}
|
||||
|
||||
imgui.Text("Scanlines:")
|
||||
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.TimelineScanlines.Plus(textColAdj))
|
||||
imgui.Text(fmt.Sprintf("Scanlines: %d", win.img.lz.Rewind.Timeline.TotalScanlines[fr]))
|
||||
imgui.SameLine()
|
||||
imgui.Text(fmt.Sprintf("%d", win.img.lz.Rewind.Timeline.TotalScanlines[traceHoverIdx]))
|
||||
imgui.PopStyleColor()
|
||||
|
||||
imgui.Text("WSYNC:")
|
||||
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.TimelineWSYNC.Plus(textColAdj))
|
||||
imgui.Text(fmt.Sprintf("WSYNC %%: %.01f%%", win.img.lz.Rewind.Timeline.Ratios[fr].WSYNC*100))
|
||||
imgui.SameLine()
|
||||
imgui.Text(fmt.Sprintf("%.01f%%", win.img.lz.Rewind.Timeline.Ratios[traceHoverIdx].WSYNC*100))
|
||||
imgui.PopStyleColor()
|
||||
|
||||
if win.img.lz.CoProc.HasCoProcBus {
|
||||
imgui.Text(win.img.lz.CoProc.ID)
|
||||
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.TimelineCoProc.Plus(textColAdj))
|
||||
imgui.Text(fmt.Sprintf("%s %%: %.01f%%", win.img.lz.CoProc.ID, win.img.lz.Rewind.Timeline.Ratios[fr].CoProc*100))
|
||||
imgui.SameLine()
|
||||
imgui.Text(fmt.Sprintf("%.01f%%", win.img.lz.Rewind.Timeline.Ratios[traceHoverIdx].CoProc*100))
|
||||
imgui.PopStyleColor()
|
||||
}
|
||||
|
||||
// show rewind thumbnail if hover is in range of rewind
|
||||
if thumbnail {
|
||||
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
|
||||
}
|
||||
|
||||
imgui.Image(imgui.TextureID(win.thmbTexture), imgui.Vec2{specification.ClksVisible * 3, specification.AbsoluteMaxScanlines}.Times(0.3))
|
||||
}
|
||||
|
||||
imgui.EndTable()
|
||||
}
|
||||
imgui.EndTooltip()
|
||||
}
|
||||
|
|
|
@ -67,7 +67,13 @@ func (cart *Cartridge) Snapshot() *Cartridge {
|
|||
return &n
|
||||
}
|
||||
|
||||
func (cart *Cartridge) Plumb() {
|
||||
func (cart *Cartridge) Plumb(fromDifferentEmulation bool) {
|
||||
if fromDifferentEmulation {
|
||||
if m, ok := cart.mapper.(mapper.PlumbFromDifferentEmulation); ok {
|
||||
m.PlumbFromDifferentEmulation()
|
||||
return
|
||||
}
|
||||
}
|
||||
cart.mapper.Plumb()
|
||||
}
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ func NewCDF(prefs *preferences.Preferences, version string, data []byte) (mapper
|
|||
//
|
||||
// if bank0 has any ARM code then it will start at offset 0x08. first eight
|
||||
// bytes are the ARM header
|
||||
cart.arm = arm7tdmi.NewARM(cart.version.mmap, &prefs.ARM, cart.state.static, cart)
|
||||
cart.arm = arm7tdmi.NewARM(cart.version.mmap, &cart.prefs.ARM, cart.state.static, cart)
|
||||
|
||||
return cart, nil
|
||||
}
|
||||
|
@ -144,6 +144,11 @@ func (cart *cdf) Plumb() {
|
|||
cart.arm.Plumb(cart.state.static, cart)
|
||||
}
|
||||
|
||||
// Plumb implements the mapper.CartMapper interface.
|
||||
func (cart *cdf) PlumbFromDifferentEmulation() {
|
||||
cart.arm = arm7tdmi.NewARM(cart.version.mmap, &cart.prefs.ARM, cart.state.static, cart)
|
||||
}
|
||||
|
||||
// Reset implements the mapper.CartMapper interface.
|
||||
func (cart *cdf) Reset(_ *rand.Rand) {
|
||||
bank := len(cart.banks) - 1
|
||||
|
|
|
@ -138,6 +138,11 @@ func (cart *dpcPlus) Plumb() {
|
|||
cart.arm.Plumb(cart.state.static, cart)
|
||||
}
|
||||
|
||||
// Plumb implements the mapper.CartMapper interface.
|
||||
func (cart *dpcPlus) PlumbFromDifferentEmulation() {
|
||||
cart.arm = arm7tdmi.NewARM(cart.version.mmap, &cart.prefs.ARM, cart.state.static, cart)
|
||||
}
|
||||
|
||||
// Reset implements the mapper.CartMapper interface.
|
||||
func (cart *dpcPlus) Reset(randSrc *rand.Rand) {
|
||||
cart.state.initialise(randSrc, len(cart.banks)-1)
|
||||
|
|
|
@ -68,6 +68,14 @@ type CartMapper interface {
|
|||
CopyBanks() []BankContent
|
||||
}
|
||||
|
||||
// PlumbFromDifferentEmulation is for mappers that are sensitive to being
|
||||
// transferred from one emulation to another. When it is known that a state is
|
||||
// being copied from one instance of hardware.VCS to another then this
|
||||
// interface should be used if available.
|
||||
type PlumbFromDifferentEmulation interface {
|
||||
PlumbFromDifferentEmulation()
|
||||
}
|
||||
|
||||
// OptionalSuperchip are implemented by cartMappers that have an optional
|
||||
// superchip. This shouldn't be used to decide if a cartridge has additional
|
||||
// RAM or not. Use the CartRAMbus interface for that.
|
||||
|
|
|
@ -81,8 +81,8 @@ func (mem *Memory) Snapshot() *Memory {
|
|||
}
|
||||
|
||||
// Plumb makes sure everythin is ship-shape after a rewind event.
|
||||
func (mem *Memory) Plumb() {
|
||||
mem.Cart.Plumb()
|
||||
func (mem *Memory) Plumb(fromDifferentEmulation bool) {
|
||||
mem.Cart.Plumb(fromDifferentEmulation)
|
||||
}
|
||||
|
||||
// Reset contents of memory.
|
||||
|
|
|
@ -376,7 +376,7 @@ func (r *Rewind) append(s *State) {
|
|||
}
|
||||
|
||||
// Plumb state into VCS.
|
||||
func Plumb(vcs *hardware.VCS, state *State) {
|
||||
func Plumb(vcs *hardware.VCS, state *State, fromDifferentEmulation bool) {
|
||||
// tv plumbing works a bit different to other areas because we're only
|
||||
// recording the state of the TV not the entire TV itself.
|
||||
vcs.TV.PlumbState(vcs, state.TV.Snapshot())
|
||||
|
@ -390,7 +390,7 @@ func Plumb(vcs *hardware.VCS, state *State) {
|
|||
vcs.TIA = state.TIA.Snapshot()
|
||||
|
||||
vcs.CPU.Plumb(vcs.Mem)
|
||||
vcs.Mem.Plumb()
|
||||
vcs.Mem.Plumb(fromDifferentEmulation)
|
||||
vcs.RIOT.Plumb(vcs.Mem.RIOT, vcs.Mem.TIA)
|
||||
vcs.TIA.Plumb(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
|
||||
|
||||
|
@ -404,7 +404,7 @@ func Plumb(vcs *hardware.VCS, state *State) {
|
|||
//
|
||||
// note that this will not change the splice point. use setSplicePoint() for that
|
||||
func (r *Rewind) runFromStateToCoords(fromState *State, toCoords coords.TelevisionCoords) error {
|
||||
Plumb(r.vcs, fromState)
|
||||
Plumb(r.vcs, fromState, false)
|
||||
|
||||
// if this is a reset entry then TV must be reset
|
||||
if fromState.level == levelReset {
|
||||
|
@ -611,3 +611,10 @@ func (r *Rewind) NewFrame(frameInfo television.FrameInfo) error {
|
|||
r.newFrame = true
|
||||
return nil
|
||||
}
|
||||
|
||||
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.fromIdx]
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ func (r *Rewind) SearchMemoryWrite(tgt *State, addr uint16, value uint8, valueMa
|
|||
|
||||
// find a recent state from the rewind history and plumb it our searchVCS
|
||||
idx := r.findFrameIndex(endCoords.Frame).fromIdx
|
||||
Plumb(searchVCS, r.entries[idx])
|
||||
Plumb(searchVCS, r.entries[idx], false)
|
||||
|
||||
// loop until we reach (or just surpass) the target State
|
||||
done := false
|
||||
|
@ -127,7 +127,7 @@ func (r *Rewind) SearchRegisterWrite(tgt *State, reg rune, value uint8, valueMas
|
|||
|
||||
// find a recent state and plumb it into searchVCS
|
||||
idx := r.findFrameIndex(endCoords.Frame).fromIdx
|
||||
Plumb(searchVCS, r.entries[idx])
|
||||
Plumb(searchVCS, r.entries[idx], false)
|
||||
|
||||
// onLoad() is called whenever a CPU register is loaded with a new value
|
||||
match := false
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/hardware/television/signal"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
||||
"github.com/jetsetilly/gopher2600/logger"
|
||||
"github.com/jetsetilly/gopher2600/rewind"
|
||||
)
|
||||
|
||||
// Label that can be used in the EmulationLabel field of the cartridgeloader.Loader type. to indicate
|
||||
|
@ -102,17 +103,7 @@ func (thmb *Thumbnailer) EndCreation() {
|
|||
}
|
||||
}
|
||||
|
||||
// UndefinedNumFrames indicates the that the thumbnailing emulation should run
|
||||
// until it is explicitely stopped with the EndCreation() function (or
|
||||
// implicitely with a second call to Create())
|
||||
const UndefinedNumFrames = -1
|
||||
|
||||
// Create 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) Create(loader cartridgeloader.Loader, numFrames int) chan *image.RGBA {
|
||||
func (thmb *Thumbnailer) wait() {
|
||||
// drain existing emulationQuit channel
|
||||
select {
|
||||
case <-thmb.emulationQuit:
|
||||
|
@ -127,6 +118,20 @@ func (thmb *Thumbnailer) Create(loader cartridgeloader.Loader, numFrames int) ch
|
|||
thmb.emulationQuit <- true
|
||||
<-thmb.emulationCompleted
|
||||
}
|
||||
}
|
||||
|
||||
// UndefinedNumFrames indicates the that the thumbnailing emulation should run
|
||||
// until it is explicitely stopped with the EndCreation() function (or
|
||||
// implicitely with a second call to Create())
|
||||
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 {
|
||||
thmb.wait()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
|
@ -139,6 +144,8 @@ func (thmb *Thumbnailer) Create(loader cartridgeloader.Loader, numFrames int) ch
|
|||
return
|
||||
}
|
||||
|
||||
tgtFrame := thmb.vcs.TV.GetCoords().Frame + numFrames
|
||||
|
||||
err = thmb.vcs.Run(func() (emulation.State, error) {
|
||||
select {
|
||||
case <-thmb.emulationQuit:
|
||||
|
@ -146,7 +153,45 @@ func (thmb *Thumbnailer) Create(loader cartridgeloader.Loader, numFrames int) ch
|
|||
default:
|
||||
}
|
||||
|
||||
if numFrames != UndefinedNumFrames && thmb.vcs.TV.GetCoords().Frame >= numFrames {
|
||||
if numFrames != UndefinedNumFrames && thmb.vcs.TV.GetCoords().Frame >= tgtFrame {
|
||||
return emulation.Ending, nil
|
||||
}
|
||||
return emulation.Running, nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Logf("thumbnailer", err.Error())
|
||||
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.
|
||||
//
|
||||
// It returns the channel over which new frames will be sent.
|
||||
func (thmb *Thumbnailer) CreateFromState(state *rewind.State, numFrames int) chan *image.RGBA {
|
||||
thmb.wait()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
thmb.emulationCompleted <- true
|
||||
}()
|
||||
|
||||
rewind.Plumb(thmb.vcs, state, true)
|
||||
|
||||
tgtFrame := thmb.vcs.TV.GetCoords().Frame + numFrames
|
||||
|
||||
err := thmb.vcs.Run(func() (emulation.State, error) {
|
||||
select {
|
||||
case <-thmb.emulationQuit:
|
||||
return emulation.Ending, nil
|
||||
default:
|
||||
}
|
||||
|
||||
if numFrames != UndefinedNumFrames && thmb.vcs.TV.GetCoords().Frame >= tgtFrame {
|
||||
return emulation.Ending, nil
|
||||
}
|
||||
return emulation.Running, nil
|
||||
|
|
Loading…
Reference in a new issue