Gopher2600/thumbnailer/anim.go
JetSetIlly 685bf7ccc7 added support for stella.pro files
added properties package

ROM select window display property information
2024-02-05 11:41:17 +00:00

338 lines
9.4 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 (
"errors"
"fmt"
"image"
"image/color"
"strings"
"sync/atomic"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
"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/notifications"
"github.com/jetsetilly/gopher2600/preview"
"github.com/jetsetilly/gopher2600/setup"
)
// Anim type handles the emulation necessary for thumbnail image
// generation.
type Anim struct {
vcs *hardware.VCS
preview *preview.Emulation
frameInfo television.FrameInfo
img *image.RGBA
cropImg *image.RGBA
isEmulating atomic.Value
emulationQuit chan bool
emulationCompleted chan bool
Render chan *image.RGBA
snapshot chan bool
snapshotVCS chan *hardware.VCS
}
// NewAnim is the preferred method of initialisation for the Anim type
func NewAnim(prefs *preferences.Preferences) (*Anim, error) {
thmb := &Anim{
emulationQuit: make(chan bool, 1),
emulationCompleted: make(chan bool, 1),
Render: make(chan *image.RGBA, 60),
snapshot: make(chan bool, 1),
snapshotVCS: make(chan *hardware.VCS, 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("AUTO")
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
tv.AddPixelRenderer(thmb)
tv.SetFPSCap(true)
// create a new VCS emulation
thmb.vcs, err = hardware.NewVCS(tv, prefs)
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
thmb.vcs.Env.Label = thumbnailerEnv
thmb.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
thmb.Reset()
// create preview emulation
thmb.preview, err = preview.NewEmulation(thmb.vcs.Env.Prefs)
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
return thmb, nil
}
func (thmb *Anim) 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()
}
// IsEmulating returns true if the thumbnail emulator is working. Useful for
// testing whether the cartridgeloader was an emulatable file
func (thmb *Anim) IsEmulating() bool {
return thmb.isEmulating.Load().(bool)
}
// EndCreation ends a running emulation that is creating a stream of
// thumbnails. Safe to use even when no emulation is running
func (thmb *Anim) EndCreation() {
select {
case thmb.emulationQuit <- true:
default:
}
}
func (thmb *Anim) wait() {
// drain existing 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 cause images to be generated by a running emulation initialised
// with the specified cartridge loader. The emulation will run for a number of
// frames before ending
func (thmb *Anim) Create(cartload cartridgeloader.Loader, numFrames int) {
thmb.wait()
// loading hook support required for supercharger
cartload.NotificationHook = func(cart mapper.CartMapper, event notifications.Notify, args ...interface{}) error {
if _, ok := cart.(*supercharger.Supercharger); ok {
switch event {
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
thmb.vcs.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
thmb.vcs.CPU.LastResult.Final = true
// call function to complete tape loading procedure
callback := args[0].(supercharger.FastLoaded)
err := callback(thmb.vcs.CPU, thmb.vcs.Mem.RAM, thmb.vcs.RIOT.Timer)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
return thmb.vcs.TV.Reset(true)
}
}
return nil
}
go func() {
defer func() {
thmb.emulationCompleted <- true
thmb.isEmulating.Store(false)
}()
// not resetting with attach cartridge becasue
err := setup.AttachCartridge(thmb.vcs, cartload, false)
if err != nil {
logger.Logf("thumbnailer", err.Error())
return
}
// add yield hook
thmb.vcs.Mem.Cart.SetYieldHook(thmb)
// reset after setting the yield hook
thmb.vcs.Reset()
// run preview emulation
err = thmb.preview.Run(cartload.Filename)
if err == nil || errors.Is(err, cartridgeloader.NoFilename) {
thmb.vcs.TV.SetVisible(thmb.preview.GetFrameInfo())
}
// if we get to this point then we can be reasonably sure that the
// cartridgeloader is emulatable
thmb.isEmulating.Store(true)
// run until target frame has been generated
tgtFrame := thmb.vcs.TV.GetCoords().Frame + numFrames
err = thmb.vcs.Run(func() (govern.State, error) {
select {
case <-thmb.emulationQuit:
return govern.Ending, nil
case <-thmb.snapshot:
default:
}
if numFrames != UndefinedNumFrames && thmb.vcs.TV.GetCoords().Frame >= tgtFrame {
return govern.Ending, nil
}
return govern.Running, nil
})
if err != nil {
logger.Logf("thumbnailer", err.Error())
return
}
}()
}
// CartYield implements the coprocessor.CartYieldHook interface.
func (thmb *Anim) CartYield(yield coprocessor.CoProcYieldType) coprocessor.YieldHookResponse {
if yield.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 *Anim) resize(frameInfo television.FrameInfo) {
if thmb.frameInfo.IsDifferent(frameInfo) {
thmb.cropImg = thmb.img.SubImage(frameInfo.Crop()).(*image.RGBA)
}
thmb.frameInfo = frameInfo
}
// NewFrame implements the television.PixelRenderer interface
func (thmb *Anim) NewFrame(frameInfo television.FrameInfo) error {
thmb.resize(frameInfo)
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 *Anim) NewScanline(scanline int) error {
return nil
}
// SetPixels implements the television.PixelRenderer interface
func (thmb *Anim) SetPixels(sig []signal.SignalAttributes, last int) error {
var col color.RGBA
var offset int
for i := range sig {
// handle VBLANK by setting pixels to black
if sig[i]&signal.VBlank == signal.VBlank {
col = color.RGBA{R: 0, G: 0, B: 0}
} else {
px := signal.ColorSignal((sig[i] & signal.Color) >> signal.ColorShift)
col = thmb.frameInfo.Spec.GetColor(px)
}
// 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 *Anim) Reset() {
// start with a NTSC television as default
thmb.resize(television.NewFrameInfo(specification.SpecNTSC))
// 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 *Anim) EndRendering() error {
return nil
}
// Snapshot safely returns a copy of the emulation
func (thmb *Anim) Snapshot() *hardware.VCS {
thmb.snapshot <- true
return <-thmb.snapshotVCS
}