mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
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
258 lines
6.9 KiB
Go
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
|
|
}
|