improved thumbnailing

This commit is contained in:
JetSetIlly 2024-01-28 15:15:37 +00:00
parent 5c7e122d3e
commit 745e977ef2
7 changed files with 102 additions and 60 deletions

View file

@ -468,11 +468,7 @@ func (scr *screen) NewFrame(frameInfo television.FrameInfo) error {
// prevent ugly screen updates while the resizer is finding the correct
// size. in the case of PAL60 ROMs in particular, the screen can resize
// dramatically
scr.crit.resize = scr.crit.frameInfo.Stable &&
(scr.crit.resize ||
scr.crit.frameInfo.Spec.ID != frameInfo.Spec.ID ||
scr.crit.frameInfo.VisibleTop != frameInfo.VisibleTop ||
scr.crit.frameInfo.VisibleBottom != frameInfo.VisibleBottom)
scr.crit.resize = scr.crit.frameInfo.Stable && (scr.crit.resize || scr.crit.frameInfo.IsDifferent(frameInfo))
// record frame info
scr.crit.frameInfo = frameInfo

View file

@ -17,10 +17,13 @@ package sdlimgui
import (
"fmt"
"image"
"os"
"path/filepath"
"strings"
"golang.org/x/image/draw"
"github.com/inkyblackness/imgui-go/v4"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
@ -52,6 +55,9 @@ type winSelectROM struct {
thmb *thumbnailer.Anim
thmbTexture texture
thmbImage *image.RGBA
thmbDimensions image.Point
}
func newSelectROM(img *SdlImgui) (window, error) {
@ -76,6 +82,8 @@ func newSelectROM(img *SdlImgui) (window, error) {
}
win.thmbTexture = img.rnd.addTexture(textureColor, true, true)
win.thmbImage = image.NewRGBA(image.Rect(0, 0, specification.ClksVisible, specification.AbsoluteMaxScanlines))
win.thmbDimensions = win.thmbImage.Bounds().Size()
return win, nil
}
@ -136,7 +144,7 @@ func (win *winSelectROM) playmodeDraw() bool {
}
posFlgs := imgui.ConditionAppearing
winFlgs := imgui.WindowFlagsAlwaysAutoResize | imgui.WindowFlagsNoSavedSettings
winFlgs := imgui.WindowFlagsNoSavedSettings | imgui.WindowFlagsAlwaysAutoResize
imgui.SetNextWindowPosV(imgui.Vec2{75, 75}, posFlgs, imgui.Vec2{0, 0})
@ -186,10 +194,25 @@ func (win *winSelectROM) debuggerDraw() bool {
func (win *winSelectROM) render() {
// receive new thumbnail data and copy to texture
select {
case image := <-win.thmb.Render:
if image != nil {
win.thmbTexture.markForCreation()
win.thmbTexture.render(image)
case newImage := <-win.thmb.Render:
if newImage != nil {
// clear image
for i := 0; i < len(win.thmbImage.Pix); i += 4 {
s := win.thmbImage.Pix[i : i+4 : i+4]
s[0] = 10
s[1] = 10
s[2] = 10
s[3] = 255
}
// copy new image so that it is centred in the thumbnail image
sz := newImage.Bounds().Size()
y := ((win.thmbDimensions.Y - sz.Y) / 2)
draw.Copy(win.thmbImage, image.Point{X: 0, Y: y},
newImage, newImage.Bounds(), draw.Over, nil)
// render image
win.thmbTexture.render(win.thmbImage)
}
default:
}
@ -214,6 +237,9 @@ func (win *winSelectROM) draw() {
imgui.Text(win.currPath)
if imgui.BeginTable("romSelector", 2) {
imgui.TableSetupColumnV("filelist", imgui.TableColumnFlagsWidthStretch, -1, 0)
imgui.TableSetupColumnV("thumbnail", imgui.TableColumnFlagsWidthStretch, -1, 1)
imgui.TableNextRow()
imgui.TableNextColumn()
@ -303,23 +329,25 @@ func (win *winSelectROM) draw() {
imgui.EndChild()
imgui.TableNextColumn()
imgui.Image(imgui.TextureID(win.thmbTexture.getID()), imgui.Vec2{specification.ClksVisible * 3, specification.AbsoluteMaxScanlines})
imguiSeparator()
if win.thmb.IsEmulating() {
imgui.Text(win.thmb.String())
} else {
imgui.Text("")
}
imgui.Image(imgui.TextureID(win.thmbTexture.getID()),
imgui.Vec2{float32(win.thmbDimensions.X) * 2, float32(win.thmbDimensions.Y)})
imgui.EndTable()
}
// control buttons. start controlHeight measurement
win.controlHeight = imguiMeasureHeight(func() {
if win.thmb.IsEmulating() {
imgui.Text(win.thmb.String())
} else {
imgui.Text("")
}
imguiSeparator()
imgui.Checkbox("Show all files", &win.showAllFiles)
imgui.SameLine()
imgui.Checkbox("Show hidden entries", &win.showHidden)
imgui.Checkbox("Show hidden files", &win.showHidden)
imgui.Spacing()

View file

@ -17,6 +17,7 @@ package sdlimgui
import (
"fmt"
"image"
"os"
"github.com/inkyblackness/imgui-go/v4"
@ -25,6 +26,7 @@ import (
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/resources/unique"
"github.com/jetsetilly/gopher2600/thumbnailer"
"golang.org/x/image/draw"
)
const winTimelineID = "Timeline"
@ -38,7 +40,10 @@ type winTimeline struct {
// goroutine so we must thumbnail those states in the same goroutine.
thmb *thumbnailer.Image
thmbTexture texture
thmbValid bool
// the backing image for the texture
thmbImage *image.RGBA
thmbDimensions image.Point
// whether the thumbnail is being shown on the left of the timeline rather
// than the right
@ -91,6 +96,8 @@ func newWinTimeline(img *SdlImgui) (window, error) {
}
win.thmbTexture = img.rnd.addTexture(textureColor, true, true)
win.thmbImage = image.NewRGBA(image.Rect(0, 0, specification.ClksVisible, specification.AbsoluteMaxScanlines))
win.thmbDimensions = win.thmbImage.Bounds().Size()
return win, nil
}
@ -111,11 +118,23 @@ const timelinePopupID = "timelinePopupID"
func (win *winTimeline) debuggerDraw() bool {
// receive new thumbnail data and copy to texture
select {
case image := <-win.thmb.Render:
if image != nil {
win.thmbTexture.markForCreation()
win.thmbTexture.render(image)
win.thmbValid = true
case newImage := <-win.thmb.Render:
if newImage != nil {
// clear image
for i := 0; i < len(win.thmbImage.Pix); i += 4 {
s := win.thmbImage.Pix[i : i+4 : i+4]
s[0] = 10
s[1] = 10
s[2] = 10
s[3] = 255
}
// copy new image so that it is centred in the thumbnail image
sz := newImage.Bounds().Size()
y := ((win.thmbDimensions.Y - sz.Y) / 2)
draw.Copy(win.thmbImage, image.Point{X: 0, Y: y},
newImage, newImage.Bounds(), draw.Over, nil)
win.thmbTexture.render(win.thmbImage)
}
default:
}
@ -188,7 +207,6 @@ func (win *winTimeline) debuggerDraw() bool {
// thumbnailer must be run in the same goroutine as the main emulation
win.thmb.Create(win.img.dbg.Rewind.GetState(hoverFrame))
<-win.thmbRunning
win.thmbValid = false
})
default:
// if a thumbnail is currently being generated then we need to
@ -334,8 +352,9 @@ func (win *winTimeline) drawTrace() {
imgui.Vec2{X: win.hoverX + cursorWidth/2, Y: rootPos.Y + traceSize.Y},
win.img.cols.timelineHoverCursor)
if win.thmbValid && win.img.prefs.showTimelineThumbnail.Get().(bool) {
sz := imgui.Vec2{X: specification.ClksVisible * 3, Y: specification.AbsoluteMaxScanlines}.Times(traceSize.Y / specification.AbsoluteMaxScanlines)
if win.img.prefs.showTimelineThumbnail.Get().(bool) {
sz := imgui.Vec2{float32(win.thmbDimensions.X) * 2, float32(win.thmbDimensions.Y)}
sz = sz.Times(traceSize.Y / specification.AbsoluteMaxScanlines)
// show thumbnail on either the left or right of the timeline window
var pos imgui.Vec2

View file

@ -118,6 +118,14 @@ func (info FrameInfo) Crop() image.Rectangle {
)
}
// IsDifferent returns true if any of the pertinent display information is
// different between the two copies of FrameInfo
func (info FrameInfo) IsDifferent(cmp FrameInfo) bool {
return info.Spec.ID != cmp.Spec.ID ||
info.VisibleTop != cmp.VisibleTop ||
info.VisibleBottom != cmp.VisibleBottom
}
func (info *FrameInfo) reset() {
info.VisibleTop = info.Spec.AtariSafeVisibleTop
info.VisibleBottom = info.Spec.AtariSafeVisibleBottom

View file

@ -29,11 +29,12 @@ import (
// the number of synced frames where we can expect things to be in flux.
const leadingFrames = 1
// the number of synced frames required before the tv is considered to be
// "stable". once the tv is stable then specification switching cannot happen.
// the number of synced frames required before the TV is considered to be
// stable. once the tv is stable then specification switching cannot happen.
//
// resizing can still happen however. so this value is important if we don't
// want to see the screen jump on ROM startup
// resizing also works a little differently before the TV is stable. the size of
// the screen can shrink as well as grow. once the stability threshold is
// reached, the screen can only grow
const stabilityThreshold = 6
// State encapsulates the television values that can change from moment to

View file

@ -83,11 +83,7 @@ func NewAnim(prefs *preferences.Preferences) (*Anim, error) {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
thmb.vcs.Env.Label = thumbnailerEnv
thmb.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
// start with a NTSC television as default
thmb.Resize(television.NewFrameInfo(specification.SpecNTSC))
thmb.Reset()
return thmb, nil
@ -231,21 +227,16 @@ func (thmb *Anim) CartYield(yield coprocessor.CoProcYieldType) coprocessor.Yield
return coprocessor.YieldHookEnd
}
// Resize implements the television.PixelRenderer interface
func (thmb *Anim) Resize(frameInfo television.FrameInfo) error {
func (thmb *Anim) resize(frameInfo television.FrameInfo, force bool) {
if thmb.frameInfo.IsDifferent(frameInfo) && (force || frameInfo.Stable) {
thmb.cropImg = thmb.img.SubImage(frameInfo.Crop()).(*image.RGBA)
}
thmb.frameInfo = frameInfo
crop := thmb.frameInfo.Crop()
thmb.cropImg = thmb.img.SubImage(crop).(*image.RGBA)
return nil
}
// NewFrame implements the television.PixelRenderer interface
func (thmb *Anim) NewFrame(frameInfo television.FrameInfo) error {
if !frameInfo.Stable {
return nil
}
thmb.frameInfo = frameInfo
thmb.resize(frameInfo, false)
img := *thmb.cropImg
img.Pix = make([]uint8, len(thmb.cropImg.Pix))
@ -286,11 +277,15 @@ func (thmb *Anim) SetPixels(sig []signal.SignalAttributes, last int) error {
offset += 4
}
return nil
}
// Reset implements the television.PixelRenderer interface
func (thmb *Anim) Reset() {
// start with a NTSC television as default
thmb.resize(television.NewFrameInfo(specification.SpecNTSC), true)
// 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++ {

View file

@ -71,7 +71,7 @@ func NewImage(prefs *preferences.Preferences) (*Image, error) {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
tv.AddPixelRenderer(thmb)
tv.SetFPSCap(true)
tv.SetFPSCap(false)
// create a new VCS emulation
thmb.vcs, err = hardware.NewVCS(tv, prefs)
@ -79,11 +79,7 @@ func NewImage(prefs *preferences.Preferences) (*Image, error) {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
thmb.vcs.Env.Label = thumbnailerEnv
thmb.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
// start with a NTSC television as default
thmb.Resize(television.NewFrameInfo(specification.SpecNTSC))
thmb.Reset()
return thmb, nil
@ -177,21 +173,17 @@ func (thmb *Image) CartYield(yield coprocessor.CoProcYieldType) coprocessor.Yiel
return coprocessor.YieldHookEnd
}
// Resize implements the television.PixelRenderer interface
func (thmb *Image) Resize(frameInfo television.FrameInfo) error {
func (thmb *Image) resize(frameInfo television.FrameInfo, force bool) error {
if thmb.frameInfo.IsDifferent(frameInfo) && (force || frameInfo.Stable) {
thmb.cropImg = thmb.img.SubImage(frameInfo.Crop()).(*image.RGBA)
}
thmb.frameInfo = frameInfo
crop := thmb.frameInfo.Crop()
thmb.cropImg = thmb.img.SubImage(crop).(*image.RGBA)
return nil
}
// NewFrame implements the television.PixelRenderer interface
func (thmb *Image) NewFrame(frameInfo television.FrameInfo) error {
if !frameInfo.Stable {
return nil
}
thmb.frameInfo = frameInfo
thmb.resize(frameInfo, false)
img := *thmb.cropImg
img.Pix = make([]uint8, len(thmb.cropImg.Pix))
@ -237,6 +229,9 @@ func (thmb *Image) SetPixels(sig []signal.SignalAttributes, last int) error {
// Reset implements the television.PixelRenderer interface
func (thmb *Image) Reset() {
// start with a NTSC television as default
thmb.resize(television.NewFrameInfo(specification.SpecNTSC), true)
// 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++ {