mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 12:08:01 -04:00
da83fc311b
all cartridge data is read through cartridgeloader io.Reader interface
1124 lines
30 KiB
Go
1124 lines
30 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 moviecart
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
|
"github.com/jetsetilly/gopher2600/environment"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
|
|
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
"github.com/jetsetilly/gopher2600/notifications"
|
|
)
|
|
|
|
const (
|
|
improveControls = false
|
|
|
|
// both of these options are now effectively enabled in recent changes made
|
|
// to the firmware and stella by lodef
|
|
improveNeatStartImageAfterRewind = true
|
|
improveMuteAudioAtStart = true
|
|
)
|
|
|
|
// default preference values.
|
|
const (
|
|
titleCycles = 1000000
|
|
osdColor = 0x9a // blue
|
|
osdDuration = 180
|
|
)
|
|
|
|
// size of each video field in bytes.
|
|
const fieldSize = 4096
|
|
|
|
// levels for both volume and brightness.
|
|
const (
|
|
levelDefault = 6
|
|
levelMax = 11
|
|
)
|
|
|
|
// colours from the color stream are ajdusted accordin the brightness level.
|
|
var brightLevels = [...]uint8{0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15}
|
|
|
|
// volume can take 11 levels. data from the audio stream can be one of 16
|
|
// levels, the actual value written to the VCS is looked up from the correct
|
|
// volume array.
|
|
var volumeLevels = [levelMax][16]uint8{
|
|
/* 0.0000 */
|
|
{8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8},
|
|
/* 0.1667 */
|
|
{6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 9, 9},
|
|
/* 0.3333 */
|
|
{5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10},
|
|
/* 0.5000 */
|
|
{4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11},
|
|
/* 0.6667 */
|
|
{3, 3, 4, 5, 5, 6, 7, 7, 8, 9, 9, 10, 11, 11, 12, 13},
|
|
/* 0.8333 */
|
|
{1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14},
|
|
/* 1.0000 */
|
|
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
|
|
/* 1.3611 */
|
|
{0, 0, 0, 1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 15, 15},
|
|
/* 1.7778 */
|
|
{0, 0, 0, 0, 1, 3, 5, 7, 8, 10, 12, 14, 15, 15, 15, 15},
|
|
/* 2.2500 */
|
|
{0, 0, 0, 0, 0, 2, 4, 6, 9, 11, 13, 15, 15, 15, 15, 15},
|
|
/* 2.7778 */
|
|
{0, 0, 0, 0, 0, 1, 3, 6, 9, 12, 14, 15, 15, 15, 15, 15},
|
|
}
|
|
|
|
// the state machine moves from condition to condition processing the movie
|
|
// stream.
|
|
type stateMachineCondition int
|
|
|
|
// list of valid state machine conditionss.
|
|
const (
|
|
stateMachineRight stateMachineCondition = iota
|
|
stateMachineLeft
|
|
stateMachineNewField
|
|
)
|
|
|
|
// the current control mode for the OSD.
|
|
type controlMode int
|
|
|
|
// list of control modes.
|
|
const (
|
|
modeVolume controlMode = iota
|
|
modeBrightness
|
|
modeTime
|
|
modeMax
|
|
)
|
|
|
|
// a frame consists of two interlaced fields.
|
|
const numFields = 2
|
|
|
|
type format struct {
|
|
// version [4]byte // ('M', 'V', 'C', '0')
|
|
// format byte // (1-------)
|
|
// timecode [4]byte // (hour, minute, second, frame)
|
|
vsync byte // eg 3
|
|
vblank byte // eg 37
|
|
overscan byte // eg 30
|
|
visible byte // eg 192
|
|
rate byte // eg 60
|
|
}
|
|
|
|
type state struct {
|
|
// the state of the address pins
|
|
a7 bool
|
|
a10 bool
|
|
a11 bool
|
|
a12 bool
|
|
a10Count uint8
|
|
|
|
// memory in the moviecart hardware. the core is copied to this and
|
|
// modified as required during movie playback.
|
|
sram []byte
|
|
|
|
// data for current field and the indexes into it
|
|
streamBuffer [numFields][]byte
|
|
streamIndex int
|
|
streamAudio int
|
|
streamGraph int
|
|
streamTimecode int
|
|
streamColor int
|
|
streamBackground int
|
|
endOfStream bool
|
|
|
|
// field number is part of the timecode stream. this is the extracted value
|
|
fieldNumber int
|
|
|
|
// odd field uses the frame component of the timecode stream to decide if
|
|
// the field is an odd number or not
|
|
oddField bool
|
|
|
|
// format for each field. parsed data from streamBuffer
|
|
format [numFields]format
|
|
|
|
// the audio value to carry to the next frame
|
|
audioCarry uint8
|
|
|
|
// which chunk of the buffer to read next
|
|
streamChunk int
|
|
|
|
// total number of address cycles the movie has been playing for
|
|
totalCycles int
|
|
|
|
// how many lines remaining for the current TIA frame. ranges from
|
|
// linesThisFrame to zero
|
|
lines int
|
|
|
|
// the number of lines int the TIA frame
|
|
linesThisFrame int
|
|
|
|
// state machine
|
|
state stateMachineCondition
|
|
|
|
justReset bool
|
|
|
|
// is playback paused. pauseStep is used to allow frame-by-frame stepping.
|
|
paused bool
|
|
pauseStep int
|
|
|
|
// volume of audio
|
|
volume int
|
|
|
|
// brightness of image
|
|
brightness int
|
|
|
|
// force of color or B&W
|
|
forceBW bool
|
|
|
|
// mask bottom edge of the screen when the OSD is visible
|
|
blankPartialLines bool
|
|
|
|
// transport
|
|
directionLast transportDirection
|
|
buttonsLast transportButtons
|
|
|
|
// the amount to move the stream. usually one.
|
|
fieldAdv int
|
|
|
|
// the mode being edited with the stick
|
|
controlMode controlMode
|
|
|
|
// the length of time the joystick has been help left or right
|
|
controlRepeat int
|
|
|
|
// the OSD is to be displayed for the remaining duration
|
|
osdDuration int
|
|
|
|
// what part of the osdControl is being shown at the current line of the field
|
|
osdControl osdControl
|
|
|
|
// index into the static osd data
|
|
osdIdx int
|
|
|
|
// whether the cartridge loader has indicated that it wants to shorten the
|
|
// title card duration
|
|
shortTitleCard bool
|
|
|
|
// the stream has failed because bad data has been encountered
|
|
streamFail bool
|
|
|
|
// the most recent rotation instruction. can only ever changes for version 2 streams
|
|
rotation byte
|
|
}
|
|
|
|
// which control is currently being displayed on screen
|
|
type osdControl int
|
|
|
|
const (
|
|
osdNone osdControl = iota
|
|
osdLabel
|
|
osdLevels
|
|
osdTime
|
|
)
|
|
|
|
func newState() *state {
|
|
s := &state{
|
|
sram: make([]byte, 0x400),
|
|
}
|
|
s.streamBuffer[0] = make([]byte, fieldSize)
|
|
s.streamBuffer[1] = make([]byte, fieldSize)
|
|
return s
|
|
}
|
|
|
|
// Snapshot implements the mapper.CartMapper interface.
|
|
func (s *state) Snapshot() *state {
|
|
n := *s
|
|
return &n
|
|
}
|
|
|
|
func (s *state) initialise() {
|
|
if s.shortTitleCard {
|
|
// shorten title card. we don't want to eliminate it entirely so say
|
|
// that it has run 75% of the normal duration already on initialisation
|
|
s.totalCycles = titleCycles * 0.75
|
|
} else {
|
|
s.totalCycles = 0
|
|
}
|
|
copy(s.sram, coreData)
|
|
s.state = stateMachineNewField
|
|
s.paused = false
|
|
s.streamChunk = 0
|
|
s.volume = levelDefault
|
|
s.brightness = levelDefault
|
|
s.fieldAdv = 1
|
|
s.a10Count = 0
|
|
s.justReset = true
|
|
|
|
// starting control mode depends on alternativeControls preference
|
|
if improveControls {
|
|
s.controlMode = modeTime
|
|
} else {
|
|
s.controlMode = modeVolume
|
|
}
|
|
}
|
|
|
|
type Moviecart struct {
|
|
env *environment.Environment
|
|
|
|
specID string
|
|
mappingID string
|
|
|
|
data io.ReadSeeker
|
|
banks []byte
|
|
|
|
state *state
|
|
}
|
|
|
|
func NewMoviecart(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) {
|
|
cart := &Moviecart{
|
|
env: env,
|
|
data: loader,
|
|
mappingID: "MVC",
|
|
}
|
|
|
|
cart.state = newState()
|
|
cart.banks = make([]byte, 4096)
|
|
|
|
// shorten title card sequence if this is not the main emulation
|
|
if !env.IsEmulation(environment.MainEmulation) {
|
|
cart.state.shortTitleCard = true
|
|
cart.state.initialise()
|
|
}
|
|
|
|
// put core data into bank ROM. this is so that we have something to
|
|
// disassemble.
|
|
//
|
|
// TODO: get rid of the banks[] array and just use the state.sram array
|
|
copy(cart.banks, coreData)
|
|
copy(cart.banks[len(coreData):], coreData)
|
|
copy(cart.banks[len(coreData)*2:], coreData)
|
|
copy(cart.banks[len(coreData)*3:], coreData)
|
|
|
|
// field starts off in the end position
|
|
cart.state.streamIndex = 1
|
|
|
|
// read next field straight away. this has the advantage of triggering the
|
|
// first screen rotation in time for the attract screen
|
|
cart.nextField()
|
|
|
|
return cart, nil
|
|
}
|
|
|
|
// MappedBanks implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) MappedBanks() string {
|
|
return fmt.Sprintf("Field: %d", cart.state.fieldNumber)
|
|
}
|
|
|
|
// ID implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) ID() string {
|
|
return "MVC"
|
|
}
|
|
|
|
// Snapshot implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) Snapshot() mapper.CartMapper {
|
|
n := *cart
|
|
n.state = cart.state.Snapshot()
|
|
return &n
|
|
}
|
|
|
|
// Plumb implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) Plumb(env *environment.Environment) {
|
|
cart.env = env
|
|
}
|
|
|
|
// Reset implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) Reset() {
|
|
}
|
|
|
|
// Access implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) Access(addr uint16, _ bool) (data uint8, mask uint8, err error) {
|
|
// TODO: this is called far too frequently but it'll do for now
|
|
cart.setConsoleTiming()
|
|
|
|
return cart.state.sram[addr&0x3ff], mapper.CartDrivenPins, nil
|
|
}
|
|
|
|
// AccessVolatile implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) AccessVolatile(addr uint16, data uint8, _ bool) error {
|
|
return nil
|
|
}
|
|
|
|
// NumBanks implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) NumBanks() int {
|
|
return 1
|
|
}
|
|
|
|
// GetBank implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) GetBank(addr uint16) mapper.BankInfo {
|
|
return mapper.BankInfo{}
|
|
}
|
|
|
|
// AccessPassive implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) AccessPassive(addr uint16, data uint8) error {
|
|
cart.processAddress(addr)
|
|
return nil
|
|
}
|
|
|
|
// Step implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) Step(clock float32) {
|
|
}
|
|
|
|
// CopyBanks implements the mapper.CartMapper interface.
|
|
func (cart *Moviecart) CopyBanks() []mapper.BankContent {
|
|
// note that as it is, this will be a copy of the core program without any
|
|
// of the updates that happen as a result of playing a movie
|
|
c := make([]mapper.BankContent, len(cart.banks))
|
|
c[0] = mapper.BankContent{Number: 0,
|
|
Data: cart.banks,
|
|
Origins: []uint16{memorymap.OriginCart},
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (cart *Moviecart) processAddress(addr uint16) {
|
|
// it's possible for a moviecart stream to produce bad data. rather than
|
|
// having bounds checks in the writeAudioData(), etc. we allow the program
|
|
// to panic in this exceptional situation and recover from it with a log
|
|
// entry
|
|
//
|
|
// once an error is encountered no more data is processed
|
|
if cart.state.streamFail {
|
|
return
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
cart.state.streamFail = true
|
|
logger.Logf("MVC", "serious data error in moviecart stream")
|
|
}
|
|
}()
|
|
|
|
if addr >= 0x1ffa {
|
|
cart.state.initialise()
|
|
}
|
|
|
|
cart.state.a12 = addr&(1<<12) == 1<<12
|
|
cart.state.a11 = addr&(1<<11) == 1<<11
|
|
|
|
if cart.state.a11 {
|
|
cart.state.a7 = addr&(1<<7) == 1<<7
|
|
}
|
|
|
|
a10 := addr&(1<<10) == 1<<10
|
|
if a10 && !cart.state.a10 {
|
|
cart.state.a10Count++
|
|
}
|
|
cart.state.a10 = a10
|
|
|
|
cart.state.totalCycles++
|
|
if cart.state.totalCycles == titleCycles {
|
|
// stop title screen
|
|
cart.write8bit(addrTitleLoop, 0x18)
|
|
|
|
err := cart.env.Notifications.Notify(notifications.NotifyMovieCartStarted)
|
|
if err != nil {
|
|
logger.Logf("moviecart", err.Error())
|
|
}
|
|
|
|
} else if cart.state.totalCycles > titleCycles {
|
|
cart.runStateMachine()
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) updateDirection() {
|
|
var direction transportDirection
|
|
t := transportDirection(cart.state.a10Count)
|
|
t = ^(t & 0x1e)
|
|
t &= 0x1e
|
|
direction = t
|
|
|
|
if cart.state.justReset {
|
|
cart.state.directionLast = direction
|
|
cart.state.justReset = false
|
|
return
|
|
}
|
|
|
|
// change control mode with the up/down direction of the stick
|
|
//
|
|
// if alternativeControls are active then the change only occurs when the
|
|
// OSD is display
|
|
if direction.isUp() && !cart.state.directionLast.isUp() {
|
|
if cart.state.osdDuration > 0 || !improveControls {
|
|
if cart.state.controlMode == 0 {
|
|
cart.state.controlMode = modeMax - 1
|
|
} else {
|
|
cart.state.controlMode--
|
|
}
|
|
}
|
|
cart.state.osdDuration = osdDuration
|
|
} else if direction.isDown() && !cart.state.directionLast.isDown() {
|
|
if cart.state.osdDuration > 0 || !improveControls {
|
|
cart.state.controlMode++
|
|
if cart.state.controlMode == modeMax {
|
|
cart.state.controlMode = 0
|
|
}
|
|
}
|
|
cart.state.osdDuration = osdDuration
|
|
}
|
|
|
|
if direction.isLeft() || direction.isRight() {
|
|
cart.state.controlRepeat++
|
|
if cart.state.controlRepeat > 10 {
|
|
cart.state.controlRepeat = 0
|
|
|
|
switch cart.state.controlMode {
|
|
case modeTime:
|
|
cart.state.osdDuration = osdDuration
|
|
if cart.state.paused {
|
|
// allow pause to step repeatedly if alternative control
|
|
// method is activated
|
|
if improveControls {
|
|
if direction.isLeft() {
|
|
cart.state.pauseStep = -1
|
|
} else if direction.isRight() {
|
|
cart.state.pauseStep = 1
|
|
}
|
|
}
|
|
} else {
|
|
if direction.isLeft() {
|
|
cart.state.fieldAdv -= 4
|
|
} else if direction.isRight() {
|
|
cart.state.fieldAdv += 4
|
|
}
|
|
}
|
|
case modeVolume:
|
|
cart.state.osdDuration = osdDuration
|
|
if direction.isLeft() {
|
|
if cart.state.volume > 0 {
|
|
cart.state.volume--
|
|
}
|
|
} else if direction.isRight() {
|
|
if cart.state.volume < levelMax-1 {
|
|
cart.state.volume++
|
|
}
|
|
}
|
|
case modeBrightness:
|
|
cart.state.osdDuration = osdDuration
|
|
if direction.isLeft() {
|
|
if cart.state.brightness > 0 {
|
|
cart.state.brightness--
|
|
}
|
|
} else if direction.isRight() {
|
|
if cart.state.brightness < levelMax-1 {
|
|
cart.state.brightness++
|
|
}
|
|
}
|
|
}
|
|
} else if cart.state.paused {
|
|
// if the left/right direction is not being held and if movie is
|
|
// paused then move one frame foreward/back only if the control mode
|
|
// is in time mode
|
|
switch cart.state.controlMode {
|
|
case modeTime:
|
|
if direction.isLeft() {
|
|
if !cart.state.directionLast.isLeft() {
|
|
cart.state.osdDuration = osdDuration
|
|
cart.state.pauseStep = -1
|
|
} else {
|
|
cart.state.pauseStep = 0
|
|
}
|
|
} else if direction.isRight() {
|
|
if !cart.state.directionLast.isRight() {
|
|
cart.state.osdDuration = osdDuration
|
|
cart.state.pauseStep = 1
|
|
} else {
|
|
cart.state.pauseStep = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
cart.state.controlRepeat = 0
|
|
cart.state.fieldAdv = 1
|
|
cart.state.pauseStep = 0
|
|
}
|
|
|
|
cart.state.directionLast = direction
|
|
}
|
|
|
|
func (cart *Moviecart) updateButtons() {
|
|
var buttons transportButtons
|
|
|
|
t := transportButtons(cart.state.a10Count)
|
|
t = ^(t & 0x17)
|
|
t &= 0x17
|
|
buttons = t
|
|
|
|
// B&W switch
|
|
cart.state.forceBW = buttons.isBW()
|
|
|
|
// reset switch
|
|
if buttons.isReset() {
|
|
cart.state.streamChunk = 0
|
|
cart.state.paused = false
|
|
return
|
|
}
|
|
|
|
// pause on button release
|
|
if buttons.isButton() && !cart.state.buttonsLast.isButton() {
|
|
cart.state.paused = !cart.state.paused
|
|
}
|
|
|
|
cart.state.buttonsLast = buttons
|
|
}
|
|
|
|
func (cart *Moviecart) updateTransport() {
|
|
// alternate between direction and button servicing
|
|
if cart.state.streamIndex == 1 {
|
|
cart.updateDirection()
|
|
} else {
|
|
cart.updateButtons()
|
|
}
|
|
|
|
// we're done with a10 count now so reset it
|
|
cart.state.a10Count = 0
|
|
|
|
// rewind/fast-forward movie stream unless playback is paused
|
|
//
|
|
// frame-by-frame stepping when playback is paused is handled in the
|
|
// nextField() function
|
|
if !cart.state.paused {
|
|
if cart.state.endOfStream {
|
|
if cart.state.fieldAdv < 0 {
|
|
cart.state.streamChunk += cart.state.fieldAdv
|
|
cart.state.endOfStream = false
|
|
}
|
|
} else {
|
|
cart.state.streamChunk += cart.state.fieldAdv
|
|
}
|
|
|
|
// bounds check for stream chunk
|
|
if improveNeatStartImageAfterRewind {
|
|
if cart.state.streamChunk < 0 {
|
|
cart.state.streamChunk = cart.state.streamIndex
|
|
}
|
|
} else {
|
|
if cart.state.streamChunk < -1 {
|
|
cart.state.streamChunk = -1
|
|
}
|
|
}
|
|
|
|
if cart.state.controlMode == modeMax {
|
|
cart.state.controlMode = modeTime
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) runStateMachine() {
|
|
// set blankPartialLines flag
|
|
switch cart.state.lines {
|
|
case 0:
|
|
cart.state.blankPartialLines = cart.state.oddField
|
|
case int(cart.state.format[cart.state.streamIndex].visible - 1):
|
|
cart.state.blankPartialLines = !cart.state.oddField
|
|
default:
|
|
cart.state.blankPartialLines = false
|
|
}
|
|
|
|
switch cart.state.state {
|
|
case stateMachineRight:
|
|
if !cart.state.a7 {
|
|
break // switch
|
|
}
|
|
|
|
if cart.state.osdDuration > 0 {
|
|
switch cart.state.controlMode {
|
|
case modeTime:
|
|
if cart.state.lines == 11 {
|
|
cart.state.osdDuration--
|
|
cart.state.osdControl = osdTime
|
|
cart.state.osdIdx = cart.state.streamTimecode
|
|
}
|
|
|
|
default:
|
|
if cart.state.lines == 21 {
|
|
cart.state.osdDuration--
|
|
cart.state.osdControl = osdLabel
|
|
cart.state.osdIdx = 0
|
|
}
|
|
|
|
if cart.state.lines == 7 {
|
|
cart.state.osdControl = osdLevels
|
|
switch cart.state.controlMode {
|
|
case modeBrightness:
|
|
cart.state.osdIdx = cart.state.brightness * 40
|
|
case modeVolume:
|
|
cart.state.osdIdx = cart.state.volume * 40
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cart.fillAddrRightLine()
|
|
cart.state.lines--
|
|
cart.state.state = stateMachineLeft
|
|
case stateMachineLeft:
|
|
if cart.state.a7 {
|
|
break // switch
|
|
}
|
|
|
|
if cart.state.lines >= 1 {
|
|
if cart.state.osdDuration > 0 {
|
|
switch cart.state.controlMode {
|
|
case modeTime:
|
|
cart.state.blankPartialLines = cart.state.lines == 12 && cart.state.oddField
|
|
default:
|
|
cart.state.blankPartialLines = cart.state.lines == 22 && cart.state.oddField
|
|
}
|
|
}
|
|
cart.fillAddrLeftLine(true)
|
|
cart.state.lines--
|
|
cart.state.state = stateMachineRight
|
|
} else {
|
|
cart.fillAddrLeftLine(false)
|
|
cart.fillAddrEndLines()
|
|
|
|
cart.fillAddrBlankLines()
|
|
|
|
// swap stream indexes
|
|
cart.state.streamIndex++
|
|
if cart.state.streamIndex >= numFields {
|
|
cart.state.streamIndex = 0
|
|
}
|
|
|
|
cart.updateTransport()
|
|
cart.state.state = stateMachineNewField
|
|
}
|
|
case stateMachineNewField:
|
|
if !cart.state.a7 {
|
|
break // switch
|
|
}
|
|
|
|
cart.nextField()
|
|
cart.state.lines = int(cart.state.format[cart.state.streamIndex].visible - 1)
|
|
cart.state.state = stateMachineRight
|
|
cart.state.osdControl = osdNone
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) fillAddrRightLine() {
|
|
cart.writeAudio(addrSetAudRight + 1)
|
|
|
|
cart.writeGraph(addrSetGData5 + 1)
|
|
cart.writeGraph(addrSetGData6 + 1)
|
|
cart.writeGraph(addrSetGData7 + 1)
|
|
cart.writeGraph(addrSetGData8 + 1)
|
|
cart.writeGraph(addrSetGData9 + 1)
|
|
|
|
cart.writeColor(addrSetGCol5 + 1) // col 1/9
|
|
cart.writeColor(addrSetGCol6 + 1) // col 3/9
|
|
cart.writeColor(addrSetGCol7 + 1) // col 5/9
|
|
cart.writeColor(addrSetGCol8 + 1) // col 7/9
|
|
cart.writeColor(addrSetGCol9 + 1) // col 9/9
|
|
|
|
cart.writeBackground(addrSetBkColR + 1)
|
|
}
|
|
|
|
func (cart *Moviecart) fillAddrLeftLine(again bool) {
|
|
cart.writeAudio(addrSetAudLeft + 1)
|
|
|
|
cart.writeGraph(addrSetGData0 + 1)
|
|
cart.writeGraph(addrSetGData1 + 1)
|
|
cart.writeGraph(addrSetGData2 + 1)
|
|
cart.writeGraph(addrSetGData3 + 1)
|
|
cart.writeGraph(addrSetGData4 + 1)
|
|
|
|
cart.writeColor(addrSetGCol0 + 1) // col 0/9
|
|
cart.writeColor(addrSetGCol1 + 1) // col 2/9
|
|
cart.writeColor(addrSetGCol2 + 1) // col 4/9
|
|
cart.writeColor(addrSetGCol3 + 1) // col 6/9
|
|
cart.writeColor(addrSetGCol4 + 1) // col 8/9
|
|
|
|
cart.writeBackground(addrSetPfColL + 1)
|
|
|
|
if again {
|
|
cart.writeJMPaddr(addrPickContinue+1, addrRightLine)
|
|
} else {
|
|
cart.writeJMPaddr(addrPickContinue+1, addrEndLines)
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) fillAddrEndLines() {
|
|
cart.writeAudio(addrSetAudEndlines + 1)
|
|
|
|
// different details for the end kernel every other frame
|
|
if cart.state.oddField {
|
|
cart.write8bit(addrSetOverscanSize+1, cart.state.format[cart.state.streamIndex].overscan-1)
|
|
cart.write8bit(addrSetVBlankSize+1, cart.state.format[cart.state.streamIndex].vblank-1)
|
|
} else {
|
|
cart.state.audioCarry = cart.state.streamBuffer[cart.state.streamIndex][cart.state.streamAudio]
|
|
cart.write8bit(addrSetOverscanSize+1, cart.state.format[cart.state.streamIndex].overscan)
|
|
cart.write8bit(addrSetVBlankSize+1, cart.state.format[cart.state.streamIndex].vblank)
|
|
}
|
|
|
|
if cart.state.streamIndex == 0 {
|
|
cart.writeJMPaddr(addrPickTransport+1, addrTransportDirection)
|
|
} else {
|
|
cart.writeJMPaddr(addrPickTransport+1, addrTransportButtons)
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) fillAddrBlankLines() {
|
|
blankLineSize := int(cart.state.format[cart.state.streamIndex].overscan + cart.state.format[cart.state.streamIndex].vsync + cart.state.format[cart.state.streamIndex].vblank - 1)
|
|
|
|
// slightly different number of trailing blank line every other frame
|
|
if !cart.state.oddField {
|
|
cart.writeAudioData(addrAudioBank, cart.state.audioCarry)
|
|
for i := 1; i < blankLineSize+1; i++ {
|
|
cart.writeAudio(addrAudioBank + uint16(i))
|
|
}
|
|
} else {
|
|
for i := 0; i < blankLineSize-1; i++ {
|
|
cart.writeAudio(addrAudioBank + uint16(i))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) writeAudio(addr uint16) {
|
|
b := cart.state.streamBuffer[cart.state.streamIndex][cart.state.streamAudio]
|
|
cart.state.streamAudio++
|
|
cart.writeAudioData(addr, b)
|
|
}
|
|
|
|
func (cart *Moviecart) writeAudioData(addr uint16, data uint8) {
|
|
// special handling of improveMuteAudioAtStart
|
|
if improveMuteAudioAtStart && cart.state.fieldAdv < 0 {
|
|
if improveNeatStartImageAfterRewind {
|
|
if cart.state.streamChunk <= 1 {
|
|
cart.write8bit(addr, 0)
|
|
return
|
|
}
|
|
} else {
|
|
if cart.state.streamChunk < 0 {
|
|
cart.write8bit(addr, 0)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// output silence if playback is paused or we have reached the end of the stream
|
|
if cart.state.paused || cart.state.endOfStream {
|
|
cart.write8bit(addr, 0)
|
|
} else {
|
|
b := volumeLevels[cart.state.volume][data]
|
|
cart.write8bit(addr, b)
|
|
}
|
|
}
|
|
|
|
func (cart *Moviecart) writeGraph(addr uint16) {
|
|
var b byte
|
|
|
|
// check if we need to draw OSD using stats graphics data
|
|
switch cart.state.osdControl {
|
|
case osdTime:
|
|
b = cart.state.streamBuffer[cart.state.streamIndex][cart.state.osdIdx]
|
|
cart.state.osdIdx++
|
|
case osdLevels:
|
|
if cart.state.osdIdx < levelBarsLen {
|
|
if cart.state.oddField {
|
|
b = levelBarsOddData[cart.state.osdIdx]
|
|
} else {
|
|
b = levelBarsEvenData[cart.state.osdIdx]
|
|
}
|
|
cart.state.osdIdx++
|
|
}
|
|
case osdLabel:
|
|
switch cart.state.controlMode {
|
|
case modeBrightness:
|
|
if cart.state.osdIdx < brightLabelLen {
|
|
if cart.state.oddField {
|
|
b = brightLabelOdd[cart.state.osdIdx]
|
|
} else {
|
|
b = brightLabelEven[cart.state.osdIdx]
|
|
}
|
|
cart.state.osdIdx++
|
|
}
|
|
|
|
case modeVolume:
|
|
if cart.state.osdIdx < volumeLabelLen {
|
|
if cart.state.oddField {
|
|
b = volumeLabelOdd[cart.state.osdIdx]
|
|
} else {
|
|
b = volumeLabelEven[cart.state.osdIdx]
|
|
}
|
|
cart.state.osdIdx++
|
|
}
|
|
}
|
|
case osdNone:
|
|
// use graphics from current field
|
|
b = cart.state.streamBuffer[cart.state.streamIndex][cart.state.streamGraph]
|
|
cart.state.streamGraph++
|
|
}
|
|
|
|
// emit black when blankPartialLines flag is true
|
|
if cart.state.blankPartialLines {
|
|
b = 0x00
|
|
}
|
|
|
|
cart.write8bit(addr, b)
|
|
}
|
|
|
|
func (cart *Moviecart) writeColor(addr uint16) {
|
|
b := cart.state.streamBuffer[cart.state.streamIndex][cart.state.streamColor]
|
|
cart.state.streamColor++
|
|
|
|
// adjust brightness
|
|
brightIdx := int(b & 0x0f)
|
|
brightIdx += cart.state.brightness
|
|
b = b&0xf0 | brightLevels[brightIdx]
|
|
|
|
// forcing a particular color means we've been drawing pixels from a timecode
|
|
// or OSD label or level meter.
|
|
if cart.state.osdControl != osdNone {
|
|
b = osdColor
|
|
}
|
|
|
|
// best effort conversion of color to B&W
|
|
if cart.state.forceBW {
|
|
b &= 0x0f
|
|
}
|
|
|
|
// emit black when blankPartialLines flag is true
|
|
if cart.state.blankPartialLines {
|
|
b = 0x00
|
|
}
|
|
|
|
cart.write8bit(addr, b)
|
|
}
|
|
|
|
func (cart *Moviecart) writeBackground(addr uint16) {
|
|
var b byte
|
|
|
|
// emit black when OSD is enabled
|
|
if cart.state.osdControl != osdNone {
|
|
b = 0x00
|
|
cart.write8bit(addr, b)
|
|
return
|
|
}
|
|
|
|
// stream next background byte
|
|
if cart.state.osdControl == osdNone {
|
|
b = cart.state.streamBuffer[cart.state.streamIndex][cart.state.streamBackground]
|
|
cart.state.streamBackground++
|
|
}
|
|
|
|
// emit black when blankPartialLines flag is true. note that we've streamed
|
|
// the background byte in this case and that we're discarding the streamed
|
|
// value
|
|
if cart.state.blankPartialLines {
|
|
b = 0x00
|
|
cart.write8bit(addr, b)
|
|
return
|
|
}
|
|
|
|
// adjust brightness
|
|
brightIdx := int(b & 0x0f)
|
|
brightIdx += cart.state.brightness
|
|
if brightIdx >= len(brightLevels) {
|
|
brightIdx = len(brightLevels) - 1
|
|
}
|
|
b = b&0xf0 | brightLevels[brightIdx]
|
|
|
|
// best effort conversion of color to B&W
|
|
if cart.state.forceBW {
|
|
b &= 0x0f
|
|
}
|
|
|
|
cart.write8bit(addr, b)
|
|
}
|
|
|
|
const chunkSize = 8 * 512
|
|
|
|
// nextField is an amalgamation of the readField() and swapField() functions in
|
|
// the reference implementation by Rob Bairos. in the reference swapField() is
|
|
// called during the stateMachineLeft phase of the state machine and readField()
|
|
// is called during the stateMachineNewField phase. it was found that this
|
|
// division was unnecessary in this implementation
|
|
func (cart *Moviecart) nextField() {
|
|
// the usual playback condition
|
|
if !cart.state.paused && cart.state.streamChunk >= 0 {
|
|
dataOffset := cart.state.streamChunk * chunkSize
|
|
_, err := cart.data.Seek(int64(dataOffset), io.SeekStart)
|
|
if err != nil {
|
|
logger.Logf("MVC", "error reading field: %v", err)
|
|
}
|
|
n, err := cart.data.Read(cart.state.streamBuffer[cart.state.streamIndex])
|
|
if err != nil {
|
|
logger.Logf("MVC", "error reading field: %v", err)
|
|
}
|
|
cart.state.endOfStream = n < fieldSize
|
|
}
|
|
|
|
// if playback is paused and pauseStep is not zero then handle
|
|
// frame-by-frame stepping especially
|
|
if cart.state.paused && cart.state.pauseStep != 0 {
|
|
for fld := 0; fld < numFields; fld++ {
|
|
cart.state.streamChunk += cart.state.pauseStep
|
|
if cart.state.streamChunk < 0 {
|
|
cart.state.streamChunk = 0
|
|
}
|
|
|
|
dataOffset := cart.state.streamChunk * chunkSize
|
|
_, err := cart.data.Seek(int64(dataOffset), io.SeekStart)
|
|
if err != nil {
|
|
logger.Logf("MVC", "error reading field: %v", err)
|
|
}
|
|
_, err = cart.data.Read(cart.state.streamBuffer[fld])
|
|
if err != nil {
|
|
logger.Logf("MVC", "error reading field: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// magic string check
|
|
if cart.state.streamBuffer[cart.state.streamIndex][0] != 'M' ||
|
|
cart.state.streamBuffer[cart.state.streamIndex][1] != 'V' ||
|
|
cart.state.streamBuffer[cart.state.streamIndex][2] != 'C' ||
|
|
cart.state.streamBuffer[cart.state.streamIndex][3] != 0x00 {
|
|
logger.Logf("MVC", "unrecognised version string in chunk %d", cart.state.streamChunk)
|
|
return
|
|
}
|
|
|
|
// version number
|
|
switch cart.state.streamBuffer[cart.state.streamIndex][4] & 0x80 {
|
|
case 0x80:
|
|
rotation := cart.state.streamBuffer[cart.state.streamIndex][4] & 0b11
|
|
if rotation != cart.state.rotation {
|
|
switch rotation {
|
|
case 0b00:
|
|
cart.env.TV.SetRotation(specification.NormalRotation)
|
|
case 0b01:
|
|
cart.env.TV.SetRotation(specification.RightRotation)
|
|
case 0b10:
|
|
cart.env.TV.SetRotation(specification.FlippedRotation)
|
|
case 0b11:
|
|
cart.env.TV.SetRotation(specification.LeftRotation)
|
|
}
|
|
cart.state.rotation = rotation
|
|
}
|
|
|
|
cart.state.format[cart.state.streamIndex].vsync = byte(cart.state.streamBuffer[cart.state.streamIndex][9])
|
|
cart.state.format[cart.state.streamIndex].vblank = byte(cart.state.streamBuffer[cart.state.streamIndex][10])
|
|
cart.state.format[cart.state.streamIndex].overscan = byte(cart.state.streamBuffer[cart.state.streamIndex][11])
|
|
cart.state.format[cart.state.streamIndex].visible = byte(cart.state.streamBuffer[cart.state.streamIndex][12])
|
|
cart.state.format[cart.state.streamIndex].rate = byte(cart.state.streamBuffer[cart.state.streamIndex][13])
|
|
|
|
lines := int(cart.state.format[cart.state.streamIndex].vsync) +
|
|
int(cart.state.format[cart.state.streamIndex].vblank) +
|
|
int(cart.state.format[cart.state.streamIndex].visible) +
|
|
int(cart.state.format[cart.state.streamIndex].overscan)
|
|
cart.state.streamAudio = 14
|
|
cart.state.streamGraph = cart.state.streamAudio + lines
|
|
cart.state.streamColor = cart.state.streamGraph + 5*int(cart.state.format[cart.state.streamIndex].visible)
|
|
cart.state.streamBackground = cart.state.streamColor + 5*int(cart.state.format[cart.state.streamIndex].visible)
|
|
cart.state.streamTimecode = cart.state.streamBackground + int(cart.state.format[cart.state.streamIndex].visible)
|
|
|
|
cart.state.fieldNumber = int(cart.state.streamBuffer[cart.state.streamIndex][6]) << 16
|
|
cart.state.fieldNumber |= int(cart.state.streamBuffer[cart.state.streamIndex][7]) << 8
|
|
cart.state.fieldNumber |= int(cart.state.streamBuffer[cart.state.streamIndex][8])
|
|
cart.state.oddField = cart.state.fieldNumber&0x01 != 0x01
|
|
|
|
case 0x00:
|
|
// offsets into each part of the field (audio, color, etc.)
|
|
const (
|
|
offsetVersion = 0
|
|
offsetFieldNumber = 4
|
|
offsetAudioData = 7
|
|
offsetGraphData = 269
|
|
offsetTimecodeData = 1229
|
|
offsetColorData = 1289
|
|
offsetColorBkData = 2249
|
|
offsetEndData = 2441
|
|
)
|
|
|
|
cart.state.format[cart.state.streamIndex].vsync = 3
|
|
cart.state.format[cart.state.streamIndex].vblank = 37
|
|
cart.state.format[cart.state.streamIndex].overscan = 30
|
|
cart.state.format[cart.state.streamIndex].visible = 192
|
|
|
|
// reset stream indexes
|
|
cart.state.streamAudio = offsetAudioData
|
|
cart.state.streamGraph = offsetGraphData
|
|
cart.state.streamTimecode = offsetTimecodeData
|
|
cart.state.streamColor = offsetColorData
|
|
cart.state.streamBackground = offsetColorBkData
|
|
|
|
cart.state.fieldNumber = int(cart.state.streamBuffer[cart.state.streamIndex][offsetFieldNumber]) << 16
|
|
cart.state.fieldNumber |= int(cart.state.streamBuffer[cart.state.streamIndex][offsetFieldNumber+1]) << 8
|
|
cart.state.fieldNumber |= int(cart.state.streamBuffer[cart.state.streamIndex][offsetFieldNumber+2])
|
|
cart.state.oddField = cart.state.fieldNumber&0x01 == 0x01
|
|
}
|
|
|
|
if cart.state.oddField {
|
|
cart.state.streamBackground++
|
|
}
|
|
}
|
|
|
|
// write 8bits of data to SRAM. the address in sram is made up of the current
|
|
// writePage value and the lo argument, which will for the lower 7bits of the
|
|
// address.
|
|
func (cart *Moviecart) write8bit(addr uint16, data uint8) {
|
|
cart.state.sram[addr&0x3ff] = data
|
|
}
|
|
|
|
// write 16bits of data to SRAM. the address in sram is made up of the current
|
|
// writePage value and the lo argument, which will for the lower 7bits of the
|
|
// address.
|
|
func (cart *Moviecart) writeJMPaddr(addr uint16, jmpAddr uint16) {
|
|
cart.state.sram[addr&0x3ff] = uint8(jmpAddr & 0xff)
|
|
cart.state.sram[(addr+1)&0x3ff] = uint8(jmpAddr>>8) | 0x10
|
|
}
|
|
|
|
// adjust program to reflect console timing
|
|
func (cart *Moviecart) setConsoleTiming() {
|
|
id := cart.env.TV.GetSpecID()
|
|
|
|
// do nothing if specification hasn't changed
|
|
if cart.specID == id {
|
|
return
|
|
}
|
|
|
|
const rainbowHeight = 30
|
|
const titleHeight = 12
|
|
|
|
var lines uint8
|
|
|
|
switch id {
|
|
case "SECAM":
|
|
lines = 242
|
|
case "PAL":
|
|
lines = 242
|
|
case "PAL-60":
|
|
lines = 192
|
|
case "NTSC":
|
|
lines = 192
|
|
default:
|
|
lines = 192
|
|
}
|
|
|
|
val := (lines - rainbowHeight - rainbowHeight - titleHeight*2) / 2
|
|
|
|
cart.write8bit(addrTitleGap1+1, val)
|
|
cart.write8bit(addrTitleGap2+1, val)
|
|
}
|