mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
added PAL60 specification. even though it isn't a real specification it is more convenient and provides better user feedback moved FrameInfo into frameinfo package, now called Current as in frameinfo.Current clarified storage of requested specification by the television: how the television is probed for the specification has changed, in particular the current spec is retreived via the GetFrameInfo() function. in fact, this was how most other packages did it but there also existed a GetSpecID() which was uncessary GetReqSpecID() and GetCreationSpecID() removed, replaced with GetResetSpecID() and IsAutoSpec() simplified SetSpec(). removed the force argument removed reset option for vcs.AttachCartridge()
634 lines
20 KiB
Go
634 lines
20 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 sdlimgui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"os"
|
|
|
|
"github.com/inkyblackness/imgui-go/v4"
|
|
"github.com/jetsetilly/gopher2600/gui/fonts"
|
|
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
"github.com/jetsetilly/gopher2600/resources/unique"
|
|
"github.com/jetsetilly/gopher2600/thumbnailer"
|
|
"golang.org/x/image/draw"
|
|
)
|
|
|
|
const winTimelineID = "Timeline"
|
|
|
|
type winTimeline struct {
|
|
debuggerWin
|
|
|
|
img *SdlImgui
|
|
|
|
// thumbnailer will be using emulation states created in the main emulation
|
|
// goroutine so we must thumbnail those states in the same goroutine.
|
|
thmb *thumbnailer.Image
|
|
thmbTexture texture
|
|
thmbImage *image.RGBA
|
|
|
|
// whether the thumbnail is being shown on the left of the timeline rather
|
|
// than the right
|
|
thumbLeft bool
|
|
|
|
// which frame was the last thumbnail generated for. used to prevent another
|
|
// thumbnail being generated for the same frame
|
|
thmbFrame int
|
|
|
|
// throttle the number of thumbnails that are being generated at once. if
|
|
// channel is full then the thumbnail is being generated. the channel is
|
|
// drained once the thumbnail creation has completed
|
|
thmbRunning chan bool
|
|
|
|
// mouse hover information
|
|
isHovered bool
|
|
hoverX float32
|
|
hoverIdx int
|
|
hoverFrame int
|
|
|
|
// mouse is being used to scrub the timeline area. see isScrubbingValid()
|
|
// for a function that is a more likely to provide a useful value
|
|
scrubbing bool
|
|
|
|
// the following two fields are used to help understand what to do when the
|
|
// timeline window is resized
|
|
//
|
|
// the trace offset from the previous frame
|
|
//
|
|
// pushing flag is true if the current frame indicator is near the right
|
|
// most limit of the trace area. it's used to
|
|
pushing bool
|
|
prevTraceOffset int
|
|
|
|
// height of toolbar
|
|
toolbarHeight float32
|
|
}
|
|
|
|
func newWinTimeline(img *SdlImgui) (window, error) {
|
|
win := &winTimeline{
|
|
img: img,
|
|
thmbRunning: make(chan bool, 1),
|
|
}
|
|
|
|
var err error
|
|
|
|
win.thmb, err = thumbnailer.NewImage(win.img.dbg.VCS().Env.Prefs, win.img.dbg.VCS().TV.GetFrameInfo().Spec.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
win.thmbTexture = img.rnd.addTexture(shaderColor, true, true, nil)
|
|
win.thmbImage = image.NewRGBA(image.Rect(0, 0, 0, 0))
|
|
|
|
return win, nil
|
|
}
|
|
|
|
func (win *winTimeline) isScrubbingValid() bool {
|
|
return win.scrubbing && win.hoverIdx >= 0 && win.hoverIdx < len(win.img.cache.Rewind.Timeline.FrameNum)
|
|
}
|
|
|
|
func (win *winTimeline) init() {
|
|
}
|
|
|
|
func (win *winTimeline) id() string {
|
|
return winTimelineID
|
|
}
|
|
|
|
const timelinePopupID = "timelinePopupID"
|
|
|
|
func (win *winTimeline) debuggerDraw() bool {
|
|
// receive new thumbnail data and copy to texture
|
|
select {
|
|
case newImage := <-win.thmb.Render:
|
|
if newImage != nil {
|
|
sz := newImage.Bounds().Size()
|
|
if sz != win.thmbImage.Bounds().Size() {
|
|
win.thmbImage = image.NewRGBA(image.Rect(0, 0, sz.X, sz.Y))
|
|
win.thmbTexture.markForCreation()
|
|
}
|
|
|
|
// copy new image so that it is centred in the thumbnail image
|
|
draw.Copy(win.thmbImage, image.Point{X: 0, Y: 0},
|
|
newImage, newImage.Bounds(), draw.Over, nil)
|
|
|
|
// render image
|
|
win.thmbTexture.render(win.thmbImage)
|
|
}
|
|
default:
|
|
}
|
|
|
|
if !win.debuggerOpen {
|
|
return false
|
|
}
|
|
|
|
const winHeightRatio = 0.05
|
|
const scanlineRatio = specification.AbsoluteMaxScanlines * winHeightRatio
|
|
|
|
imgui.SetNextWindowPosV(imgui.Vec2{X: 39, Y: 722}, imgui.ConditionFirstUseEver, imgui.Vec2{X: 0, Y: 0})
|
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 875, Y: 220}, imgui.ConditionFirstUseEver)
|
|
w, _ := win.img.plt.windowSize()
|
|
imgui.SetNextWindowSizeConstraints(imgui.Vec2{X: 750, Y: 200}, imgui.Vec2{X: w * 0.95, Y: 300})
|
|
|
|
if imgui.BeginV(win.debuggerID(win.id()), &win.debuggerOpen, imgui.WindowFlagsNone) {
|
|
// trace area
|
|
win.drawTrace()
|
|
|
|
// toolbar
|
|
win.toolbarHeight = imguiMeasureHeight(func() {
|
|
imguiSeparator()
|
|
win.drawToolbar()
|
|
})
|
|
|
|
// popup menu
|
|
if imgui.BeginPopup(timelinePopupID) {
|
|
if imgui.Selectable(fmt.Sprintf("%c Save Timeline to CSV", fonts.Disk)) {
|
|
win.saveToCSV()
|
|
}
|
|
if win.isHovered {
|
|
imgui.Spacing()
|
|
imgui.Separator()
|
|
imgui.Spacing()
|
|
|
|
if imgui.Selectable(fmt.Sprintf("Set Comparison to Frame %d", win.hoverFrame)) {
|
|
win.img.dbg.PushFunction(func() {
|
|
win.img.term.pushCommand(fmt.Sprintf("COMPARISON %d", win.hoverFrame))
|
|
})
|
|
}
|
|
|
|
var label string
|
|
var command string
|
|
if win.img.cache.Rewind.Comparison.Locked {
|
|
label = "Unlock Comparison Frame"
|
|
command = "COMPARISON UNLOCK"
|
|
} else {
|
|
label = fmt.Sprintf("Lock Comparison Frame (at frame %d)",
|
|
win.img.cache.Rewind.Comparison.State.TV.GetCoords().Frame)
|
|
command = "COMPARISON LOCK"
|
|
}
|
|
if imgui.Selectable(label) {
|
|
win.img.term.pushCommand(command)
|
|
}
|
|
}
|
|
imgui.EndPopup()
|
|
}
|
|
}
|
|
|
|
win.debuggerGeom.update()
|
|
imgui.End()
|
|
|
|
if (win.isHovered || win.isScrubbingValid()) && win.img.prefs.showTimelineThumbnail.Get().(bool) {
|
|
if win.thmbFrame != win.hoverFrame {
|
|
select {
|
|
case win.thmbRunning <- true:
|
|
win.thmbFrame = win.hoverFrame
|
|
hoverFrame := win.hoverFrame
|
|
win.img.dbg.PushFunction(func() {
|
|
// thumbnailer must be run in the same goroutine as the main emulation
|
|
win.thmb.Create(win.img.dbg.Rewind.GetState(hoverFrame))
|
|
<-win.thmbRunning
|
|
})
|
|
default:
|
|
// if a thumbnail is currently being generated then we need to
|
|
// carry on with the GUI thread without delay
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (win *winTimeline) drawToolbar() {
|
|
timeline := win.img.cache.Rewind.Timeline
|
|
if timeline.AvailableStart == timeline.AvailableEnd && timeline.AvailableStart == 0 {
|
|
imgui.Text("No rewind history")
|
|
} else {
|
|
imgui.Text(fmt.Sprintf("Rewind between %d and %d", timeline.AvailableStart, timeline.AvailableEnd))
|
|
imgui.SameLineV(0, 15)
|
|
imguiColorLabelSimple(fmt.Sprintf("Comparing frame %d", win.img.cache.Rewind.Comparison.State.TV.GetCoords().Frame), win.img.cols.TimelineComparison)
|
|
|
|
if win.isHovered || win.isScrubbingValid() {
|
|
imgui.SameLineV(0, 15)
|
|
imgui.Text(fmt.Sprintf("Frame %d", win.img.cache.Rewind.Timeline.FrameNum[win.hoverIdx]))
|
|
|
|
imgui.SameLineV(0, 15)
|
|
imguiColorLabelSimple(fmt.Sprintf("%d Scanlines", win.img.cache.Rewind.Timeline.FrameInfo[win.hoverIdx].TotalScanlines), win.img.cols.TimelineScanlines)
|
|
|
|
imgui.SameLineV(0, 15)
|
|
if timeline.FrameInfo[win.hoverIdx].FromVSYNC {
|
|
imguiColorLabelSimple(fmt.Sprintf("%d+%d VSYNC", win.img.cache.Rewind.Timeline.FrameInfo[win.hoverIdx].VSYNCscanline, win.img.cache.Rewind.Timeline.FrameInfo[win.hoverIdx].VSYNCcount), win.img.cols.TimelineVSYNC)
|
|
} else {
|
|
imguiColorLabelSimple("No VSYNC", win.img.cols.TimelineVSYNC)
|
|
}
|
|
|
|
imgui.SameLineV(0, 15)
|
|
imguiColorLabelSimple(fmt.Sprintf("%.02f%% WSYNC", win.img.cache.Rewind.Timeline.Ratios[win.hoverIdx].WSYNC*100), win.img.cols.TimelineWSYNC)
|
|
|
|
if win.img.cache.VCS.Mem.Cart.GetCoProcBus() != nil {
|
|
imgui.SameLineV(0, 15)
|
|
imguiColorLabelSimple(fmt.Sprintf("%.02f%% Coproc", win.img.cache.Rewind.Timeline.Ratios[win.hoverIdx].CoProc*100), win.img.cols.TimelineCoProc)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (win *winTimeline) drawTrace() {
|
|
timeline := win.img.cache.Rewind.Timeline
|
|
dl := imgui.WindowDrawList()
|
|
|
|
// size of trace area elements. the size of the graph depends on the size of
|
|
// the timeline window
|
|
const (
|
|
gap = 5
|
|
inputTrace = 2
|
|
rewindRangeTrace = 3
|
|
plotWidth = 4
|
|
plotHeight = 2
|
|
cursorWidth = 5
|
|
)
|
|
|
|
// the amount to allow for the icons when centering etc.
|
|
iconRadius := win.img.fonts.guiSize / 2
|
|
|
|
// the width that can be seen in the window at any one time. reduce by the
|
|
// iconRadius*2 value to allow for the TV icon (current frame icon) when it
|
|
// reaches the extreme left/right of the window
|
|
availableWidth := imgui.ContentRegionAvail().X - iconRadius*2
|
|
|
|
// the width of the timeline window in frames (ie. number of frames visible)
|
|
availableWidthInFrames := int(availableWidth / plotWidth)
|
|
|
|
// size of entire timeline trace area
|
|
traceSize := imgui.Vec2{X: availableWidth, Y: imgui.ContentRegionAvail().Y - win.toolbarHeight}
|
|
|
|
// height of the graph portion of the trace area
|
|
graphHeight := traceSize.Y - (gap*2 + inputTrace + gap + rewindRangeTrace + gap + iconRadius*2 + gap)
|
|
|
|
// check if end of timeline overflows the available width and adjust offset
|
|
// so that the trace is right-justified (for want of a better description)
|
|
//
|
|
// we also don't want to decrease the traceoffset from a previous high value
|
|
var traceOffset int
|
|
if len(timeline.FrameNum)*plotWidth >= int(availableWidth) {
|
|
traceOffset = len(timeline.FrameNum) - availableWidthInFrames
|
|
}
|
|
if win.pushing || traceOffset > win.prevTraceOffset {
|
|
win.prevTraceOffset = traceOffset
|
|
} else {
|
|
traceOffset = win.prevTraceOffset
|
|
}
|
|
|
|
// similar to traceOffset, rewindOffset adjusts the placement of the rewind
|
|
// range and frame indicators
|
|
rewindOffset := traceOffset
|
|
if len(timeline.FrameNum) > 0 {
|
|
rewindOffset += timeline.FrameNum[0]
|
|
}
|
|
|
|
// list of scanline jitter points to indicate. these will be found during
|
|
// the plot of the scanline trace and then used to draw the jitter
|
|
// indicators in a second loop
|
|
var scanlineJitter []int
|
|
scanlineJitter = append(scanlineJitter, 0)
|
|
|
|
// scanline/coproc/WSYNC trace
|
|
imgui.BeginChildV("##timelinetrace", traceSize, false, imgui.WindowFlagsNoMove)
|
|
|
|
// the position of the trace widget. move right slightly to create a margin
|
|
// of width iconRadius
|
|
rootPos := imgui.CursorScreenPos()
|
|
rootPos.X += iconRadius
|
|
imgui.SetCursorScreenPos(rootPos)
|
|
|
|
// the Y position of each trace area
|
|
yPos := rootPos.Y + gap
|
|
|
|
// rewind start/end X positions
|
|
rewindStartX := rootPos.X + float32((timeline.AvailableStart-rewindOffset)*plotWidth)
|
|
if rewindStartX < rootPos.X {
|
|
rewindStartX = rootPos.X
|
|
}
|
|
rewindEndX := rootPos.X + float32((timeline.AvailableEnd-rewindOffset)*plotWidth)
|
|
|
|
// draw frame guides
|
|
const guideFrameCount = 20
|
|
imgui.PushFont(win.img.fonts.diagram)
|
|
|
|
var guideStart int
|
|
if len(timeline.FrameNum) > 0 {
|
|
guideStart = timeline.FrameNum[traceOffset]
|
|
}
|
|
|
|
guideX := rootPos.X
|
|
for fn := guideStart; fn < guideStart+availableWidthInFrames; fn++ {
|
|
if fn%guideFrameCount == 0 {
|
|
// draw vertical frame guides
|
|
top := imgui.Vec2{X: guideX, Y: rootPos.Y}
|
|
bot := imgui.Vec2{X: guideX, Y: rootPos.Y + traceSize.Y}
|
|
dl.AddRectFilled(top, bot, win.img.cols.timelineGuides)
|
|
|
|
// label frame guides with frame numbers
|
|
bot.X += 5
|
|
bot.Y -= win.img.fonts.diagramSize / 2
|
|
dl.AddText(bot, win.img.cols.timelineGuidesLabel, fmt.Sprintf("%d", fn))
|
|
}
|
|
guideX += plotWidth
|
|
}
|
|
imgui.PopFont()
|
|
|
|
// show hover/scrubbing cursor
|
|
if win.isHovered || win.isScrubbingValid() {
|
|
dl.AddRectFilled(imgui.Vec2{X: win.hoverX - cursorWidth/2, Y: rootPos.Y},
|
|
imgui.Vec2{X: win.hoverX + cursorWidth/2, Y: rootPos.Y + traceSize.Y},
|
|
win.img.cols.timelineHoverCursor)
|
|
|
|
if win.img.prefs.showTimelineThumbnail.Get().(bool) {
|
|
// size of thumbnail is based on height of trace area
|
|
sz := imgui.Vec2{X: specification.WidthTV, Y: specification.HeightTV}
|
|
sz = sz.Times(traceSize.Y / sz.Y)
|
|
|
|
// show thumbnail on either the left or right of the timeline window
|
|
var pos imgui.Vec2
|
|
if win.thumbLeft {
|
|
pos = imgui.Vec2{X: rootPos.X + iconRadius*2, Y: rootPos.Y}
|
|
if win.hoverX <= pos.X+sz.X {
|
|
win.thumbLeft = false
|
|
pos.X = rootPos.X + availableWidth - sz.X - iconRadius*2
|
|
}
|
|
} else {
|
|
pos = imgui.Vec2{X: rootPos.X + availableWidth - sz.X - iconRadius*2, Y: rootPos.Y}
|
|
if win.hoverX >= pos.X {
|
|
win.thumbLeft = true
|
|
pos.X = rootPos.X + iconRadius*2
|
|
}
|
|
}
|
|
imgui.SetCursorScreenPos(pos)
|
|
|
|
imgui.ImageV(imgui.TextureID(win.thmbTexture.getID()), sz,
|
|
imgui.Vec2{}, imgui.Vec2{X: 1, Y: 1},
|
|
win.img.cols.TimelineThumbnailTint, imgui.Vec4{})
|
|
|
|
imgui.SetCursorScreenPos(rootPos)
|
|
}
|
|
}
|
|
|
|
// draw main trace plot
|
|
plotX := rootPos.X
|
|
for i := range timeline.FrameNum[traceOffset:] {
|
|
// adjust index by starting point
|
|
i += traceOffset
|
|
|
|
// SCANLINE TRACE
|
|
plotY := yPos + graphHeight
|
|
|
|
// scale TotalScanlines value so that it covers the entire height of traceSize
|
|
plotY -= float32(timeline.FrameInfo[i].TotalScanlines) * graphHeight / specification.AbsoluteMaxScanlines
|
|
|
|
// add jitter to trace to indicate changes in value through exaggeration
|
|
if i > 0 {
|
|
if timeline.FrameInfo[i].TotalScanlines != timeline.FrameInfo[i-1].TotalScanlines {
|
|
if timeline.FrameInfo[i].TotalScanlines < timeline.FrameInfo[i-1].TotalScanlines {
|
|
plotY++
|
|
} else if timeline.FrameInfo[i].TotalScanlines > timeline.FrameInfo[i-1].TotalScanlines {
|
|
plotY--
|
|
}
|
|
|
|
// add to jitter history if it hasn't been updated for a while
|
|
prev := scanlineJitter[len(scanlineJitter)-1]
|
|
j := i - traceOffset
|
|
if j-prev > 3 {
|
|
scanlineJitter = append(scanlineJitter, j)
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw scanline trace
|
|
dl.AddRectFilled(imgui.Vec2{X: plotX, Y: plotY},
|
|
imgui.Vec2{X: plotX + plotWidth, Y: plotY + plotHeight},
|
|
win.img.cols.timelineScanlines)
|
|
|
|
// VSYNC TRACE
|
|
if timeline.FrameInfo[i].FromVSYNC {
|
|
vsyncCount := min(timeline.FrameInfo[i].VSYNCcount, 6)
|
|
|
|
plotY = yPos + (graphHeight * 0.75)
|
|
plotY -= float32(timeline.FrameInfo[i].VSYNCscanline) * (graphHeight * 0.75) / specification.AbsoluteMaxScanlines
|
|
|
|
// add jitter to trace to indicate changes in value through exaggeration
|
|
if i > 0 {
|
|
diff := timeline.FrameInfo[i].VSYNCscanline - timeline.FrameInfo[i-1].VSYNCscanline
|
|
if diff > 0 && diff < vsyncCount {
|
|
plotY--
|
|
} else if diff < 0 && diff > -vsyncCount {
|
|
plotY++
|
|
}
|
|
}
|
|
|
|
dl.AddRectFilled(imgui.Vec2{X: plotX, Y: plotY},
|
|
imgui.Vec2{X: plotX + plotWidth, Y: plotY + float32(vsyncCount)},
|
|
win.img.cols.timelineVSYNC)
|
|
}
|
|
|
|
// WSYNC TRACE
|
|
|
|
// plot WSYNC from the bottom
|
|
plotY = yPos + graphHeight
|
|
plotY -= float32(timeline.Counts[i].WSYNC) * graphHeight / specification.AbsoluteMaxClks
|
|
|
|
// add jitter to trace to indicate changes in value through exaggeration
|
|
if i > 0 {
|
|
if timeline.Counts[i].WSYNC < timeline.Counts[i-1].WSYNC {
|
|
plotY++
|
|
} else if timeline.Counts[i].WSYNC > timeline.Counts[i-1].WSYNC {
|
|
plotY--
|
|
}
|
|
}
|
|
|
|
dl.AddRectFilled(imgui.Vec2{X: plotX, Y: plotY},
|
|
imgui.Vec2{X: plotX + plotWidth, Y: plotY + plotHeight},
|
|
win.img.cols.timelineWSYNC)
|
|
|
|
// COPROCESSOR TRACE
|
|
|
|
// plot coprocessor from the top
|
|
if win.img.cache.VCS.Mem.Cart.GetCoProcBus() != nil {
|
|
plotY = yPos
|
|
plotY += float32(timeline.Counts[i].CoProc) * graphHeight / specification.AbsoluteMaxClks
|
|
|
|
// add jitter to trace to indicate changes in value through exaggeration
|
|
if i > 0 {
|
|
if timeline.Counts[i].CoProc < timeline.Counts[i-1].CoProc {
|
|
plotY++
|
|
} else if timeline.Counts[i].CoProc > timeline.Counts[i-1].CoProc {
|
|
plotY--
|
|
}
|
|
}
|
|
|
|
dl.AddRectFilled(imgui.Vec2{X: plotX, Y: plotY},
|
|
imgui.Vec2{X: plotX + plotWidth, Y: plotY + plotHeight},
|
|
win.img.cols.timelineCoProc)
|
|
}
|
|
|
|
plotX += plotWidth
|
|
}
|
|
yPos += graphHeight + gap
|
|
|
|
// input trace
|
|
// TODO: right player and panel input
|
|
plotX = rootPos.X
|
|
for i := range timeline.FrameNum[traceOffset:] {
|
|
i += traceOffset
|
|
if timeline.LeftPlayerInput[i] {
|
|
dl.AddRectFilled(imgui.Vec2{X: plotX, Y: yPos},
|
|
imgui.Vec2{X: plotX + plotWidth, Y: yPos + inputTrace},
|
|
win.img.cols.timelineLeftPlayer)
|
|
}
|
|
plotX += plotWidth
|
|
}
|
|
yPos += inputTrace + gap
|
|
|
|
// rewind range indicator
|
|
dl.AddRectFilled(imgui.Vec2{X: rewindStartX, Y: yPos},
|
|
imgui.Vec2{X: rewindEndX, Y: yPos + rewindRangeTrace},
|
|
win.img.cols.timelineRewindRange)
|
|
yPos += rewindRangeTrace + gap
|
|
|
|
// jitter indicators
|
|
for _, i := range scanlineJitter[1:] {
|
|
pos := imgui.Vec2{X: rootPos.X + float32(i*plotWidth), Y: yPos}
|
|
dl.AddText(pos, win.img.cols.timelineScanlines, string(fonts.TimelineJitter))
|
|
}
|
|
|
|
// comparison frame indicator
|
|
if win.img.cache.Rewind.Comparison.State != nil && len(timeline.FrameNum) > 0 {
|
|
fr := win.img.cache.Rewind.Comparison.State.TV.GetCoords().Frame - rewindOffset
|
|
|
|
if fr < 0 {
|
|
// indicate that the comparison frame is not visible
|
|
pos := imgui.Vec2{X: rootPos.X - iconRadius, Y: yPos}
|
|
dl.AddText(pos, win.img.cols.timelineComparison, string(fonts.TimelineOffScreen))
|
|
} else {
|
|
pos := imgui.Vec2{X: rootPos.X + float32(fr*plotWidth) - iconRadius, Y: yPos}
|
|
if win.img.cache.Rewind.Comparison.Locked {
|
|
dl.AddText(pos, win.img.cols.timelineComparison, string(fonts.TimelineComparisonLock))
|
|
} else {
|
|
dl.AddText(pos, win.img.cols.timelineComparison, string(fonts.TimelineComparison))
|
|
}
|
|
}
|
|
}
|
|
|
|
// current frame indicator
|
|
currentFrame := win.img.cache.TV.GetCoords().Frame - rewindOffset
|
|
if currentFrame < 0 {
|
|
// indicate that the current frame indicator is not visible
|
|
pos := imgui.Vec2{X: rootPos.X - iconRadius, Y: yPos}
|
|
dl.AddText(pos, win.img.cols.timelineRewindRange, string(fonts.TimelineOffScreen))
|
|
} else {
|
|
dl.AddText(imgui.Vec2{X: rootPos.X + float32(currentFrame*plotWidth) - iconRadius, Y: yPos},
|
|
win.img.cols.timelineRewindRange, string(fonts.TV))
|
|
}
|
|
win.pushing = float32(currentFrame*plotWidth) >= availableWidth*0.99
|
|
|
|
// end of trace area
|
|
imgui.EndChild()
|
|
|
|
// no mouse handling for timeline window if popup window is open
|
|
if imgui.IsPopupOpen(timelinePopupID) {
|
|
return
|
|
}
|
|
|
|
// detect mouse is in scrubbing areas
|
|
win.scrubbing = (win.scrubbing && imgui.IsMouseDown(0)) ||
|
|
(imgui.IsItemHovered() && imgui.IsMouseClicked(0))
|
|
|
|
// check for mouse hover over rewindable area
|
|
mouse := imgui.MousePos()
|
|
win.isHovered = (imgui.IsItemHovered() &&
|
|
mouse.X >= rewindStartX && mouse.X <= rewindEndX &&
|
|
mouse.Y >= rootPos.Y && mouse.Y <= rootPos.Y+traceSize.Y)
|
|
win.hoverX = mouse.X
|
|
|
|
// rewind support
|
|
rewindX := win.hoverX - rootPos.X
|
|
rewindStartFrame := win.img.cache.Rewind.Timeline.AvailableStart
|
|
rewindEndFrame := win.img.cache.Rewind.Timeline.AvailableEnd
|
|
|
|
// index and frame number for hover position
|
|
win.hoverIdx = int(rewindX/plotWidth) + traceOffset
|
|
win.hoverFrame = int(rewindX/plotWidth) + rewindOffset
|
|
|
|
// mouse handling
|
|
if imgui.IsMouseDown(1) && imgui.IsItemHovered() {
|
|
imgui.OpenPopup(timelinePopupID)
|
|
|
|
} else if win.isScrubbingValid() {
|
|
coords := win.img.cache.TV.GetCoords()
|
|
|
|
// 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 win.hoverFrame >= rewindEndFrame {
|
|
if coords.Frame < rewindEndFrame {
|
|
win.img.dbg.RewindToFrame(win.hoverFrame, true)
|
|
}
|
|
} else if win.hoverFrame <= rewindStartFrame {
|
|
if coords.Frame > rewindStartFrame {
|
|
win.img.dbg.RewindToFrame(win.hoverFrame, false)
|
|
}
|
|
} else if win.hoverFrame != coords.Frame {
|
|
win.img.dbg.RewindToFrame(win.hoverFrame, win.hoverFrame == rewindEndFrame)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (win *winTimeline) saveToCSV() {
|
|
// open unique file
|
|
fn := unique.Filename("timeline", win.img.cache.VCS.Mem.Cart.ShortName)
|
|
fn = fmt.Sprintf("%s.csv", fn)
|
|
f, err := os.Create(fn)
|
|
if err != nil {
|
|
logger.Logf(logger.Allow, "sdlimgui", "could not save timeline CSV: %v", err)
|
|
return
|
|
}
|
|
defer func() {
|
|
err := f.Close()
|
|
if err != nil {
|
|
logger.Logf(logger.Allow, "sdlimgui", "error saving timeline CSV: %v", err)
|
|
}
|
|
}()
|
|
|
|
f.WriteString("FrameNum,")
|
|
f.WriteString("Scanlines,")
|
|
f.WriteString("CoProc,")
|
|
f.WriteString("WSYNC,")
|
|
f.WriteString("Left Player,")
|
|
f.WriteString("Right Player,")
|
|
f.WriteString("Panel")
|
|
f.WriteString("\n")
|
|
|
|
timeline := win.img.cache.Rewind.Timeline
|
|
for i, n := range timeline.FrameNum {
|
|
f.WriteString(fmt.Sprintf("%d,", n))
|
|
f.WriteString(fmt.Sprintf("%d,", timeline.FrameInfo[i].TotalScanlines))
|
|
f.WriteString(fmt.Sprintf("%d,", timeline.Counts[i].CoProc))
|
|
f.WriteString(fmt.Sprintf("%d,", timeline.Counts[i].WSYNC))
|
|
f.WriteString(fmt.Sprintf("%v,", timeline.LeftPlayerInput[i]))
|
|
f.WriteString(fmt.Sprintf("%v,", timeline.RightPlayerInput[i]))
|
|
f.WriteString(fmt.Sprintf("%v,", timeline.PanelInput[i]))
|
|
f.WriteString("\n")
|
|
}
|
|
}
|