Gopher2600/thumbnailer/image.go
JetSetIlly b54439e574 correct processing of signal array in pixel renderers
all SetPixel() function now set VBLANK pixels to 'VideoBlack' via a call
to GetColor(), rather than setting the RGBA values directly. except the
video digest, which remains a special case

this complete work from the previous commit 68263991f
2024-11-23 14:56:37 +00:00

258 lines
6.9 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 thumbnailer
import (
"fmt"
"image"
"image/color"
"strings"
"sync/atomic"
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/preferences"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/signal"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/rewind"
)
// Image type handles the emulation necessary for thumbnail image
// generation.
type Image struct {
vcs *hardware.VCS
frameInfo television.FrameInfo
img *image.RGBA
cropImg *image.RGBA
isEmulating atomic.Value
emulationQuit chan bool
emulationCompleted chan bool
Render chan *image.RGBA
}
var imageLabel = environment.Label("image")
// NewImage is the preferred method of initialisation for the Image type
func NewImage(prefs *preferences.Preferences, spec string) (*Image, error) {
thmb := &Image{
emulationQuit: make(chan bool, 1),
emulationCompleted: make(chan bool, 1),
Render: make(chan *image.RGBA, 1),
}
// emulation has completed, by definition, on startup
thmb.emulationCompleted <- true
// set isEmulating atomic as a boolean
thmb.isEmulating.Store(false)
// create a new television. this will be used during the initialisation of
// the VCS and not referred to directly again
tv, err := television.NewTelevision(spec)
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
tv.AddPixelRenderer(thmb)
tv.SetFPSCap(false)
// create a new VCS emulation
thmb.vcs, err = hardware.NewVCS(imageLabel, tv, nil, prefs)
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
thmb.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
thmb.Reset()
return thmb, nil
}
func (thmb *Image) String() string {
cart := thmb.vcs.Mem.Cart
s := strings.Builder{}
s.WriteString(fmt.Sprintf("%s (%s cartridge)", cart.ShortName, cart.ID()))
if cc := cart.GetContainer(); cc != nil {
s.WriteString(fmt.Sprintf(" [in %s]", cc.ContainerID()))
}
return s.String()
}
func (thmb *Image) wait() {
// drain emulationQuit channel
select {
case <-thmb.emulationQuit:
default:
}
// drain emulationCompleted channel. if there is nothing to drain then send
// a quit signal and wait for emulation to complete
select {
case <-thmb.emulationCompleted:
default:
thmb.emulationQuit <- true
<-thmb.emulationCompleted
}
}
// Create will run the thumbnailer emulation for a single frame using the state
// from another emultion as a starting point
//
// The function must be called in the same goroutine as the emulation that
// generated the rewind.State
func (thmb *Image) Create(state *rewind.State) {
thmb.wait()
defer func() {
thmb.emulationCompleted <- true
}()
// connect state to our thumbnailer vcs
rewind.Plumb(thmb.vcs, state, true)
// 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()
// add yield hook
thmb.vcs.Mem.Cart.SetYieldHook(thmb)
// run until target frame has been generated
tgtFrame := thmb.vcs.TV.GetCoords().Frame + 1
err := thmb.vcs.Run(func() (govern.State, error) {
select {
case <-thmb.emulationQuit:
return govern.Ending, nil
default:
}
if thmb.vcs.TV.GetCoords().Frame >= tgtFrame {
return govern.Ending, nil
}
return govern.Running, nil
})
if err != nil {
logger.Log(logger.Allow, "thumbnailer", err)
return
}
}
// CartYield implements the coprocessor.CartYieldHook interface.
func (thmb *Image) CartYield(yield coprocessor.CoProcYield) coprocessor.YieldHookResponse {
if yield.Type.Normal() {
return coprocessor.YieldHookContinue
}
// an unexpected yield type so end the thumbnail emulation
select {
case thmb.emulationQuit <- true:
default:
}
// indicate that the mapper should return immediately
return coprocessor.YieldHookEnd
}
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
return nil
}
// NewFrame implements the television.PixelRenderer interface
func (thmb *Image) NewFrame(frameInfo television.FrameInfo) error {
thmb.resize(frameInfo, false)
img := *thmb.cropImg
img.Pix = make([]uint8, len(thmb.cropImg.Pix))
copy(img.Pix, thmb.cropImg.Pix)
select {
case thmb.Render <- &img:
default:
}
return nil
}
// NewScanline implements the television.PixelRenderer interface
func (thmb *Image) NewScanline(scanline int) error {
return nil
}
// SetPixels implements the television.PixelRenderer interface
func (thmb *Image) SetPixels(sig []signal.SignalAttributes, last int) error {
var offset int
for i := range sig {
var col color.RGBA
// handle VBLANK by setting pixels to black. we also manually handle
// NoSignal in the same way
if sig[i].VBlank || sig[i].Index == signal.NoSignal {
col = thmb.frameInfo.Spec.GetColor(signal.VideoBlack)
} else {
col = thmb.frameInfo.Spec.GetColor(sig[i].Color)
}
// small cap improves performance, see https://golang.org/issue/27857
s := thmb.img.Pix[offset : offset+3 : offset+3]
s[0] = col.R
s[1] = col.G
s[2] = col.B
offset += 4
}
return nil
}
// 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++ {
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
func (thmb *Image) EndRendering() error {
return nil
}