mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-01 19:48:01 -04:00
da83fc311b
all cartridge data is read through cartridgeloader io.Reader interface
436 lines
12 KiB
Go
436 lines
12 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/debugger/govern"
|
|
"github.com/jetsetilly/gopher2600/environment"
|
|
"github.com/jetsetilly/gopher2600/hardware"
|
|
"github.com/jetsetilly/gopher2600/hardware/preferences"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
|
"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
|
|
previewResults *preview.Results
|
|
previewUpdate chan bool
|
|
|
|
frameInfo television.FrameInfo
|
|
|
|
img *image.RGBA
|
|
cropImg *image.RGBA
|
|
|
|
isEmulating atomic.Value
|
|
emulationQuit chan bool
|
|
emulationCompleted chan bool
|
|
|
|
Render chan *image.RGBA
|
|
|
|
// monitorCount is part of the adhoc monitor system. see SetPixels()
|
|
// function for details
|
|
monitorActive bool
|
|
monitorCount int
|
|
monitorInput func()
|
|
monitorInputDelay int
|
|
}
|
|
|
|
// 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),
|
|
previewUpdate: make(chan bool, 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, thmb, prefs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("thumbnailer: %w", err)
|
|
}
|
|
thmb.vcs.Env.Label = environment.Label("thumbnail_anim")
|
|
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
|
|
}
|
|
|
|
// empty the render queue
|
|
var drained bool
|
|
for !drained {
|
|
select {
|
|
case <-thmb.Render:
|
|
default:
|
|
drained = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify implements the notifications.Notify interface
|
|
func (thmb *Anim) Notify(notice notifications.Notice) error {
|
|
switch notice {
|
|
case notifications.NotifySuperchargerFastload:
|
|
// 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
|
|
fastload := thmb.vcs.Mem.Cart.GetSuperchargerFastLoad()
|
|
err := fastload.Fastload(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
|
|
}
|
|
|
|
// 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
|
|
//
|
|
// Returns when the preview has completed (so PreviewResults() is safe to call
|
|
// once the function has returned)
|
|
func (thmb *Anim) Create(cartload cartridgeloader.Loader, numFrames int, monitor bool) {
|
|
thmb.wait()
|
|
|
|
// reset fields
|
|
thmb.previewResults = nil
|
|
thmb.monitorActive = monitor
|
|
thmb.monitorCount = 0
|
|
thmb.monitorInput = nil
|
|
thmb.monitorInputDelay = 0
|
|
|
|
// reset function is usually called from the television. we call it here
|
|
// because it's useful for clearing the image and to put the now empty
|
|
// image in the render queue at the start of the animation
|
|
thmb.Reset()
|
|
|
|
go func() {
|
|
defer func() {
|
|
thmb.emulationCompleted <- true
|
|
thmb.isEmulating.Store(false)
|
|
}()
|
|
|
|
// attach cartridge using the setup system
|
|
err := setup.AttachCartridge(thmb.vcs, cartload, true)
|
|
if err != nil {
|
|
logger.Logf("thumbnailer", err.Error())
|
|
return
|
|
}
|
|
|
|
// run preview for just one frame. this is enough to give us basic
|
|
// information like the cartridge mapper and detected controllers
|
|
_ = thmb.preview.RunN(cartload, 1)
|
|
|
|
// indicate that the first part of the preview has completed and that
|
|
// the preview results should be updated
|
|
select {
|
|
case thmb.previewUpdate <- true:
|
|
default:
|
|
}
|
|
|
|
// run preview some more in order to get excellent frame information
|
|
err = thmb.preview.Run(cartload)
|
|
if err == nil || errors.Is(err, cartridgeloader.NoFilename) {
|
|
thmb.vcs.TV.SetVisible(thmb.preview.Results().FrameInfo)
|
|
}
|
|
|
|
// indicate that the second part of the preview has completed
|
|
select {
|
|
case thmb.previewUpdate <- true:
|
|
default:
|
|
}
|
|
|
|
// 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
|
|
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
|
|
}
|
|
}()
|
|
}
|
|
|
|
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 {
|
|
// act on monitor input
|
|
if thmb.monitorActive && thmb.monitorInputDelay > 0 {
|
|
thmb.monitorInputDelay--
|
|
if thmb.monitorInputDelay == 0 {
|
|
thmb.monitorInput()
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// this adhoc "monitor" system looks for changes in pixels and uses that
|
|
// information to insert input into the emulation
|
|
var monitorChanges bool
|
|
var monitorPixels int
|
|
var monitorBoringPixels int
|
|
var monitorPrev color.RGBA
|
|
|
|
var offset int
|
|
for i := range sig {
|
|
// note vblank signal for later
|
|
vblank := sig[i]&signal.VBlank == signal.VBlank
|
|
|
|
// handle VBLANK by setting pixels to black
|
|
if 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]
|
|
offset += 4
|
|
|
|
// monitor pixels
|
|
if thmb.monitorActive && !vblank && i%specification.ClksScanline > specification.ClksHBlank {
|
|
monitorPixels++
|
|
if s[0] != col.R || s[1] != col.G || s[2] != col.B {
|
|
monitorChanges = true
|
|
}
|
|
if col == monitorPrev {
|
|
monitorBoringPixels++
|
|
}
|
|
}
|
|
monitorPrev = col
|
|
|
|
// set new color
|
|
s[0] = col.R
|
|
s[1] = col.G
|
|
s[2] = col.B
|
|
}
|
|
|
|
const (
|
|
monitorBoringThreshold = 0.98
|
|
monitorCountThreshold = 60
|
|
)
|
|
|
|
// act on monitor information
|
|
if thmb.monitorActive && !monitorChanges &&
|
|
float32(monitorBoringPixels)/float32(monitorPixels) > monitorBoringThreshold {
|
|
|
|
// increase monitor count and once threshold has been reached insert the input
|
|
thmb.monitorCount++
|
|
if thmb.monitorCount >= monitorCountThreshold {
|
|
thmb.monitorCount = 0
|
|
|
|
// monitorInput works best as a chain. the input function is run
|
|
// after delay number of frames. the function issues the input and
|
|
// sets the delay and function to indicate the next input
|
|
thmb.monitorInputDelay = 1
|
|
thmb.monitorInput = func() {
|
|
thmb.vcs.RIOT.Ports.HandleInputEvent(
|
|
ports.InputEvent{Port: plugging.PortPanel,
|
|
Ev: ports.PanelReset,
|
|
D: true})
|
|
|
|
// add the next link in the input chain
|
|
thmb.monitorInputDelay = 1
|
|
thmb.monitorInput = func() {
|
|
thmb.vcs.RIOT.Ports.HandleInputEvent(
|
|
ports.InputEvent{Port: plugging.PortPanel,
|
|
Ev: ports.PanelReset,
|
|
D: false})
|
|
|
|
// disable monitor at end of input chain
|
|
thmb.monitorActive = false
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
thmb.monitorCount = 0
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// PreviewResults returns the results of the preview emulation
|
|
func (thmb *Anim) PreviewResults() *preview.Results {
|
|
select {
|
|
case <-thmb.previewUpdate:
|
|
thmb.previewResults = thmb.preview.Results()
|
|
default:
|
|
}
|
|
return thmb.previewResults
|
|
}
|