// 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 . 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 loader io.ReadSeekCloser banks []byte state *state } func NewMoviecart(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) { cart := &Moviecart{ env: env, loader: loader.StreamedData, 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.loader.Seek(int64(dataOffset), io.SeekStart) if err != nil { logger.Logf("MVC", "error reading field: %v", err) } n, err := cart.loader.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.loader.Seek(int64(dataOffset), io.SeekStart) if err != nil { logger.Logf("MVC", "error reading field: %v", err) } _, err = cart.loader.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) }