o player sprite

- player sprite now works fully for
	Pitfall
	Keystone
	player test cards
    - tv debugging output is accurate
This commit is contained in:
steve 2019-07-18 20:48:42 +01:00
parent c424a4a24b
commit dfde00462b
17 changed files with 562 additions and 377 deletions

4
FUTURE
View file

@ -1,6 +1,8 @@
debugger
--------
o RESET command to work when mid-instruction (during video step)
o custom error messages for command line package
- for example "unrecognised argument" command for HELP should be something
like "no help available for ..."
@ -32,6 +34,8 @@ o commandline
o display of colors in the terminal (check for 256 color terminal)
o MachineInfoDebug() in addition to Terse and Verbose
sdl screen
----------

View file

@ -25,6 +25,7 @@ const (
cmdCPU = "CPU"
cmdCartridge = "CARTRIDGE"
cmdClear = "CLEAR"
cmdClocks = "CLOCKS"
cmdDebuggerState = "DEBUGGERSTATE"
cmdDigest = "DIGEST"
cmdDisassembly = "DISASSEMBLY"
@ -71,6 +72,7 @@ var commandTemplate = []string{
cmdCPU + " (SET [PC|A|X|Y|SP] [%N])",
cmdCartridge + " (ANALYSIS)",
cmdClear + " [BREAKS|TRAPS|WATCHES|ALL]",
cmdClocks,
cmdDebuggerState,
cmdDigest + " (RESET)",
cmdDisassembly,
@ -100,7 +102,7 @@ var commandTemplate = []string{
cmdGranularity + " (CPU|VIDEO)",
cmdStick + " [0|1] [LEFT|RIGHT|UP|DOWN|FIRE|NOLEFT|NORIGHT|NOUP|NODOWN|NOFIRE]",
cmdSymbol + " [%S (ALL|MIRRORS)|LIST (LOCATIONS|READ|WRITE)]",
cmdTIA + " (DELAY|CLOCK)",
cmdTIA + " (DELAY|DELAYS)",
cmdTV + " (SPEC)",
cmdTerse,
cmdTrap + " [%S] {%S}",
@ -245,7 +247,7 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
keyword = strings.ToUpper(keyword)
helpTxt, ok := Help[keyword]
if ok == false {
if !ok {
dbg.print(console.StyleHelp, "no help for %s", keyword)
} else {
helpTxt = fmt.Sprintf("%s\n\n Usage: %s", helpTxt, (*debuggerCommandsIdx)[keyword].String())
@ -289,14 +291,9 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
if dbg.scriptScribe.IsActive() {
// if we're currently recording a script we want to write this
// command to the new script file...
if err != nil {
return doNothing, err
}
// ... but indicate that we'll be entering a new script and so
// don't want to repeat the commands from that script
// command to the new script file but indicate that we'll be
// entering a new script and so don't want to repeat the
// commands from that script
dbg.scriptScribe.StartPlayback()
defer func() {
@ -813,6 +810,9 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
case cmdRAM:
dbg.printMachineInfo(dbg.vcs.Mem.PIA)
case cmdClocks:
dbg.print(console.StyleMachineInfo, "not implemented yet")
case cmdRIOT:
option, present := tokens.Get()
if present {
@ -837,10 +837,8 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
// for convience asking for TIA delays also prints delays for
// the sprites
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0.SprDelay)
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1.SprDelay)
case "CLOCK":
dbg.print(console.StyleError, "not supported yet")
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0.Delay)
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1.Delay)
default:
// already caught by command line ValidateTokens()
}

View file

@ -7,6 +7,7 @@ var Help = map[string]string{
cmdCPU: "Display the current state of the CPU",
cmdCartridge: "Display information about the current cartridge",
cmdClear: "Clear all entries in BREAKS and TRAPS",
cmdClocks: "The current state of the VCS clocks",
cmdDebuggerState: "Display summary of debugger options",
cmdDigest: "Return the cryptographic hash of the current screen",
cmdDisassembly: "Print the full cartridge disassembly",

View file

@ -1,6 +1,9 @@
package future
import "fmt"
import (
"fmt"
"strings"
)
// Event represents a single occurence (contained in payload) that is to be
// deployed in the future
@ -17,45 +20,66 @@ type Event struct {
// the number of remaining ticks before the pending action is resolved
RemainingCycles int
paused bool
// the value that is to be the result of the pending action
payload func()
// arguments to the payload function
args []interface{}
}
func (ins Event) String() string {
return fmt.Sprintf("%s -> %d", ins.label, ins.RemainingCycles)
func (ev Event) String() string {
label := strings.TrimSpace(ev.label)
if label == "" {
label = "[unlabelled event]"
}
return fmt.Sprintf("%s -> %d", label, ev.RemainingCycles)
}
func schedule(ticker *Ticker, cycles int, payload func(), label string) *Event {
return &Event{ticker: ticker, label: label, initialCycles: cycles, RemainingCycles: cycles, payload: payload}
}
// Tick event forward one cycle
func (ev *Event) Tick() bool {
if ev.paused {
return false
}
func (ins *Event) tick() bool {
// 0 is the trigger state
if ins.RemainingCycles == 0 {
ins.RemainingCycles--
ins.payload()
if ev.RemainingCycles == 0 {
ev.RemainingCycles--
ev.payload()
return true
}
// -1 is the off state
if ins.RemainingCycles != -1 {
ins.RemainingCycles--
if ev.RemainingCycles != -1 {
ev.RemainingCycles--
}
return false
}
// Force can be used to immediately run the event's payload
func (ins *Event) Force() {
ins.payload()
ins.ticker.Drop(ins)
func (ev *Event) Force() {
ev.payload()
ev.ticker.Drop(ev)
}
// Drop can be used to remove the event from the ticker queue without running
// the payload
func (ins *Event) Drop() {
ins.ticker.Drop(ins)
func (ev *Event) Drop() {
ev.ticker.Drop(ev)
}
// Pause prevents the event from ticking any further until Resume or Restart is
// called
func (ev *Event) Pause() {
ev.paused = true
}
// Resume a previously paused event
func (ev *Event) Resume() {
ev.paused = false
}
// Restart an event
func (ev *Event) Restart() {
ev.RemainingCycles = ev.initialCycles
ev.paused = false
}

View file

@ -40,7 +40,7 @@ func (tck Ticker) MachineInfoTerse() string {
// Schedule the pending future action
func (tck *Ticker) Schedule(cycles int, payload func(), label string) *Event {
ins := schedule(tck, cycles, payload, label)
ins := &Event{ticker: tck, label: label, initialCycles: cycles, RemainingCycles: cycles, payload: payload}
tck.events.PushBack(ins)
return ins
}
@ -56,7 +56,7 @@ func (tck *Ticker) Tick() bool {
e := tck.events.Front()
for e != nil {
t := e.Value.(*Event).tick()
t := e.Value.(*Event).Tick()
r = r || t
if t {

View file

@ -14,14 +14,19 @@ import "strings"
// __ __ __
// _______| |_________| |_________| |___ PHASE-2 (H@2)
// PhaseClock is four-phase ticker
// PhaseClock is four-phase ticker. even though Phi1 and Phi2 are independent
// these types of clocks never overlap (the skew margin is always positive).
// this means that we can simply count from one to four to account for all
// possible outputs.
//
// note that the labels H@1 and H@2 are used in the TIA schematics for the
// HSYNC circuit. the phase clocks for the other polycounters are labelled
// differently, eg. P@1 and P@2 for the player sprites. to avoid confusion,
// we're using the labels Phi1 and Phi2, applicable to all polycounter
// phaseclocks.
type PhaseClock int
// valid PhaseClock values/states. we are ordering the states differently to
// that suggested by the diagram above and the String() function below. this is
// because the clock starts at the beginning of Phase-2 and as such, it is more
// convenient to think of risingPhi2 as the first state, rather than
// risingPhi1.
// valid PhaseClock values/states
const (
risingPhi1 PhaseClock = iota
fallingPhi1
@ -32,7 +37,7 @@ const (
// NumStates is the number of phases the clock can be in
const NumStates = 4
// String creates a two line ASCII representation of the current state of
// String creates a single line ASCII representation of the current state of
// the PhaseClock
func (clk PhaseClock) String() string {
s := strings.Builder{}
@ -56,16 +61,32 @@ func (clk PhaseClock) MachineInfoTerse() string {
// MachineInfo returns the PhaseClock information in verbose format
func (clk PhaseClock) MachineInfo() string {
return clk.String()
s := strings.Builder{}
switch clk {
case risingPhi1:
s.WriteString("_*--._______\n")
s.WriteString("_______.--._\n")
case fallingPhi1:
s.WriteString("_.--*_______\n")
s.WriteString("_______.--._\n")
case risingPhi2:
s.WriteString("_.--._______\n")
s.WriteString("_______*--._\n")
case fallingPhi2:
s.WriteString("_.--._______\n")
s.WriteString("_______.--*_\n")
}
return s.String()
}
// Reset puts the clock into a known initial state
func (clk *PhaseClock) Reset(outOfPhase bool) {
if outOfPhase {
*clk = risingPhi1
} else {
*clk = risingPhi2
}
// Align the phaseclock with the master clock
func (clk *PhaseClock) Align() {
*clk = risingPhi1
}
// Reset the phaseclock to the rise of Phi2
func (clk *PhaseClock) Reset() {
*clk = risingPhi2
}
// Tick moves PhaseClock to next state
@ -87,18 +108,12 @@ func (clk PhaseClock) Count() int {
return int(clk)
}
// InPhase returns true if the clock is at the tick point that polycounters
// should be advanced
func (clk PhaseClock) InPhase() bool {
return clk == risingPhi2
}
// OutOfPhase returns true if the clock suggests that events goverened by MOTCK
// should take place. from TIA_HW_Notes.txt:
//
// "The [MOTCK] (motion clock?) line supplies the CLK signals
// for all movable graphics objects during the visible part of
// the scanline. It is an inverted (out of phase) CLK signal."
func (clk PhaseClock) OutOfPhase() bool {
// Phi1 returns true if the Phi1 clock is on its rising edge
func (clk PhaseClock) Phi1() bool {
return clk == risingPhi1
}
// Phi2 returns true if the Phi2 clock is on its rising edge
func (clk PhaseClock) Phi2() bool {
return clk == risingPhi2
}

View file

@ -1,17 +1,10 @@
package polycounter
// polycounter implements the counting method used in the VCS TIA chip and as
// described in TIA_HW_Notes.txt
//
// there's nothing particularly noteworthy about the implementation except that
// the Count value can be used to index the predefined polycounter table, which
// maybe useful for debugging.
//
// intended to be used in conjunction with Phaseclock
// described in "TIA_HW_Notes.txt"
import (
"fmt"
"gopher2600/hardware/tia/phaseclock"
)
// Polycounter counts from 0 to Limit. can be used to index a polycounter
@ -40,9 +33,3 @@ func (pcnt *Polycounter) Tick() bool {
}
return false
}
// NumSteps uses the Phaseclock (that is driving the polycounter) to figure out the
// number of steps taken since the Reset point
func (pcnt Polycounter) NumSteps(clk *phaseclock.PhaseClock) int {
return (pcnt.Count * phaseclock.NumStates) + clk.Count()
}

View file

@ -36,6 +36,15 @@ type TIA struct {
// counters are ticked.
hblank bool
// a flag to say if the hblank will be turning off on the next cycle
// -- used by the sprite objects to apply the correct delay for position
// resets (see sprite code for details)
hblankOffNext bool
// the MOTCK signal is sent to the sprite objects every cycle when hblank
// is false. the schematics show a one cycle delay after hblank is changed
motck bool
// wsync records whether the cpu is to halt until hsync resets to 000000
wsync bool
@ -43,15 +52,6 @@ type TIA struct {
hmoveLatch bool
hmoveCt int
// "Beside each counter there is a two-phase clock generator. This
// takes the incoming 3.58 MHz colour clock (CLK) and divides by
// 4 using a couple of flip-flops. Two AND gates are then used to
// generate two independent clock signals"
//
// we use tiaClk by waiting for InPhase() signals and then ticking the
// hsync counter.
tiaClk phaseclock.PhaseClock
// TIA_HW_Notes.txt describes the hsync counter:
//
// "The HSync counter counts from 0 to 56 once for every TV scan-line
@ -59,6 +59,7 @@ type TIA struct {
// The counter decodes shown below provide all the horizontal timing for
// the control lines used to construct a valid TV signal."
hsync polycounter.Polycounter
pclk phaseclock.PhaseClock
// TIA_HW_Notes.txt talks about there being a delay when altering some
// video objects/attributes. the following future.Group ticks every color
@ -80,15 +81,11 @@ func (tia TIA) MachineInfo() string {
// map String to MachineInfo
func (tia TIA) String() string {
s := strings.Builder{}
s.WriteString(fmt.Sprintf("%s %s %03d %04.01f %d",
s.WriteString(fmt.Sprintf("%s %03d %d %04.01f",
tia.hsync,
tia.tiaClk.String(),
tia.pclk.Count(),
tia.videoCycles,
tia.cpuCycles,
// pixel information below is not the same as the pixel column in
// TIA_HW_Notes
tia.hsync.NumSteps(&tia.tiaClk),
))
// NOTE: TIA_HW_Notes also includes playfield and control information.
@ -100,11 +97,14 @@ func (tia TIA) String() string {
// NewTIA creates a TIA, to be used in a VCS emulation
func NewTIA(tv television.Television, mem memory.ChipBus) *TIA {
tia := TIA{tv: tv, mem: mem, hblank: true}
tia.pclk.Reset()
tia.tiaClk.Reset(false)
tia.hmoveCt = -1
tia.Video = video.NewVideo(&tia.tiaClk, &tia.hsync, &tia.TIAdelay, mem, tv)
tia.Video = video.NewVideo(&tia.pclk, &tia.hsync,
&tia.TIAdelay,
mem, tv,
&tia.hblank, &tia.hblankOffNext, &tia.hmoveLatch)
if tia.Video == nil {
return nil
}
@ -147,9 +147,10 @@ func (tia *TIA) ReadMemory() {
return
case "RSYNC":
tia.tiaClk.Reset(true)
tia.TIAdelay.Schedule(5, func() {
tia.pclk.Align()
tia.TIAdelay.Schedule(4, func() {
tia.hsync.Reset()
tia.pclk.Reset()
// the same as what happens at SHB
tia.hblank = true
@ -162,9 +163,13 @@ func (tia *TIA) ReadMemory() {
return
case "HMOVE":
tia.Video.PrepareSpritesForHMOVE()
tia.hmoveLatch = true
tia.hmoveCt = 15
// TODO: the schematics definitely show a delay but I'm not sure if
// it's 4 cycles.
tia.TIAdelay.Schedule(4, func() {
tia.Video.PrepareSpritesForHMOVE()
tia.hmoveLatch = true
tia.hmoveCt = 15
}, "HMOVE")
return
}
@ -187,10 +192,17 @@ func (tia *TIA) ReadMemory() {
// these parts is important. the currently defined steps and the ordering are
// as follows:
//
// !!TODO: summary of steps
// 1. tick two-phase clock
// 2. if clock is now on the rising edge of Phi2
// 2.1. tick hsync counter
// 2.2. schedule hsync events as required
// 3. tick delayed events
// 4. tick sprites
// 5. adjust HMOVE value
// 6. send signal to television
//
// steps 2.0 and 6.0 contain a lot more work important to the correct operation
// of the TIA but from this perspective each step is monolithic
// step 4 contains a lot more work important to the correct operation of the
// TIA but from this perspective the step is monolithic
//
// note that there is no TickPlayfield(). earlier versions of the code required
// us to tick the playfield explicitely but because the playfield is so closely
@ -202,15 +214,27 @@ func (tia *TIA) Step() (bool, error) {
tia.videoCycles++
tia.cpuCycles = float64(tia.videoCycles) / 3.0
// update "two-phase clock generator"
tia.tiaClk.Tick()
tia.pclk.Tick()
// hsyncDelay is the number of cycles required before, for example, hblank
// is reset
const hsyncDelay = 4
// when phase clock reaches the correct state, tick hsync counter
if tia.tiaClk.InPhase() {
// the TIA schematics for the MOTCK signal show a one cycle delay after
// HBLANK has been changed
const motckDelay = 1
// tick hsync counter when the Phi2 clock is raised. from TIA_HW_Notes.txt:
//
// "This table shows the elapsed number of CLK, CPU cycles, Playfield
// (PF) bits and Playfield pixels at the start of each counter state
// (ie when the counter changes to this state on the rising edge of
// the H@2 clock)."
//
// the context of this passage is the Horizontal Sync Counter. It is
// explicitely saying that the HSYNC counter ticks forward on the rising
// edge of Phi2.
if tia.pclk.Phi2() {
tia.hsync.Tick()
// this switch statement is based on the "Horizontal Sync Counter"
@ -232,16 +256,20 @@ func (tia *TIA) Step() (bool, error) {
// the CPU's WSYNC concludes at the beginning of a scanline
// from the TIA_1A document:
//
// "...WYNC latch is automatically reset to zero by the leading
// edge of the next horizontal blank timing signal, releasing
// the RDY line"
//
// the reutrn value of this Step() function is the RDY line
// "...WSYNC latch is automatically reset to zero by the
// leading edge of the next horizontal blank timing signal,
// releasing the RDY line"
tia.wsync = false
// start HBLANK. start of new scanline for the TIA. turn hblank on
// start HBLANK. start of new scanline for the TIA. turn hblank
// on
tia.hblank = true
// MOTCK is one cycle behind the HBALNK state
tia.TIAdelay.Schedule(motckDelay, func() {
tia.motck = false
}, "MOTCK [reset]")
// not sure when to reset HMOVE latch but here seems good
tia.hmoveLatch = false
@ -249,12 +277,12 @@ func (tia *TIA) Step() (bool, error) {
tia.videoCycles = 0
tia.cpuCycles = 0
// rather than include the reset signal in the delay, we will
// manually reset hsync counter when it reaches a count of 57
// see SignalAttributes type definition for notes about the
// HSyncSimple attribute
tia.sig.HSyncSimple = true
// rather than include the reset signal in the delay, we will
// manually reset hsync counter when it reaches a count of 57
}, "RESET")
case 1:
@ -297,8 +325,23 @@ func (tia *TIA) Step() (bool, error) {
case 16: // [RHB]
// early HBLANK off if hmoveLatch is false
if !tia.hmoveLatch {
// one cycle before HBLANK is turned off raise the
// hblankOffNext flag. we'll lower it next cycle when HBLANK is
// actually turned off
tia.TIAdelay.Schedule(hsyncDelay-1, func() {
tia.hblankOffNext = true
}, "")
tia.TIAdelay.Schedule(hsyncDelay, func() {
tia.hblank = false
tia.hblankOffNext = false
// the signal used to tick the sprites is one cycle behind
// the HBLANK state
tia.TIAdelay.Schedule(motckDelay, func() {
tia.motck = true
}, "MOTCK")
}, "HRB")
}
@ -306,9 +349,18 @@ func (tia *TIA) Step() (bool, error) {
case 18:
// late HBLANK off if hmoveLatch is true
//
// see swtich-case 16 for commentary
if tia.hmoveLatch {
tia.TIAdelay.Schedule(hsyncDelay-1, func() {
tia.hblankOffNext = true
}, "")
tia.TIAdelay.Schedule(hsyncDelay, func() {
tia.hblank = false
tia.hblankOffNext = false
tia.TIAdelay.Schedule(motckDelay, func() {
tia.motck = true
}, "MOTCK [late]")
}, "LHRB")
}
}
@ -326,30 +378,7 @@ func (tia *TIA) Step() (bool, error) {
// we always call TickSprites but whether or not (and how) the tick
// actually occurs is left for the sprite object to decide based on the
// arguments passed here.
//
// the first argument is whether or not we're in the visible part of the
// screen. from TIA_HW_Notes.txt:
//
// "The most important thing to note about the player counter is
// that it only receives CLK signals during the visible part of
// each scanline, when HBlank is off; exactly 160 CLK per scanline
// (except during HMOVE)"
//
// from this we can say that the concept of the visible screen coincides
// exactly with when HBLANK is disabled.
//
// the second argument is the current hmove counter value. from
// TIA_HW_Notes.txt:
//
// "In this case the extra HMOVE clock pulses act to perform
// 'plugging' instead of the normal 'stuffing'; by this I mean that
// the extra pulses plug up the gaps in the normal [MOTCK] pulses,
// preventing them from counting as clock pulses. This only works
// because the extra HMOVE pulses are derived from the two-phase
// clock on the HSync counter, which is itself derived from CLK
// (the TIA colour clock input), whereas [MOTCK] is an inverted CLK
// signal - so they are more or less precisely out of phase :)"
tia.Video.TickSprites(!tia.hblank, uint8(tia.hmoveCt)&0x0f)
tia.Video.Tick(tia.motck, uint8(tia.hmoveCt)&0x0f)
// update HMOVE counter. leaving the value as -1 (the binary for -1 is of
// course 0b11111111)

View file

@ -4,7 +4,6 @@ import (
"fmt"
"gopher2600/hardware/tia/delay"
"gopher2600/hardware/tia/delay/future"
"gopher2600/hardware/tia/phaseclock"
"strings"
)
@ -19,9 +18,9 @@ type ballSprite struct {
enablePrev bool
}
func newBallSprite(label string, tiaclk *phaseclock.PhaseClock) *ballSprite {
func newBallSprite(label string) *ballSprite {
bs := new(ballSprite)
bs.sprite = newSprite(label, tiaclk, bs.tick)
bs.sprite = newSprite(label, bs.tick)
return bs
}

View file

@ -3,14 +3,13 @@ package video
// CompareHMOVE tests to variables of type uint8 and checks to see if any of
// the bits in the lower nibble differ. returns false if no bits are the same,
// true otherwise
//
// returns true if any corresponding bits in the lower nibble are the same.
// from TIA_HW_Notes.txt:
//
// "When the comparator for a given object detects that none of the 4 bits
// match the bits in the counter state, it clears this latch"
//
func compareHMOVE(a uint8, b uint8) bool {
// return true if any corresponding bits in the lower nibble are the same.
// from TIA_HW_Notes.txt:
//
// "When the comparator for a given object detects that none of the 4 bits
// match the bits in the counter state, it clears this latch"
//
// at first sight this seems to be saying "a&b!=0" but after some thought,
// I don't believe it is.
return a&0x08 == b&0x08 || a&0x04 == b&0x04 || a&0x02 == b&0x02 || a&0x01 == b&0x01
}

View file

@ -4,7 +4,6 @@ import (
"fmt"
"gopher2600/hardware/tia/delay"
"gopher2600/hardware/tia/delay/future"
"gopher2600/hardware/tia/phaseclock"
"strings"
)
@ -35,9 +34,9 @@ type missileSprite struct {
parentPlayer *playerSprite
}
func newMissileSprite(label string, tiaclk *phaseclock.PhaseClock) *missileSprite {
func newMissileSprite(label string) *missileSprite {
ms := new(missileSprite)
ms.sprite = newSprite(label, tiaclk, ms.tick)
ms.sprite = newSprite(label, ms.tick)
return ms
}

View file

@ -10,20 +10,38 @@ import (
"strings"
)
type scanCounter int
type scanCounter struct {
offset int
latches int
}
const scanCounterLimit scanCounter = 7
const scanCounterLimit int = 7
func (sc *scanCounter) start() {
*sc = scanCounterLimit
func (sc *scanCounter) start(size uint8) {
if size == 0x05 || size == 0x07 {
sc.latches = 2
} else {
sc.latches = 1
}
}
func (sc scanCounter) active() bool {
return sc >= 0 && sc <= scanCounterLimit
return sc.offset >= 0 && sc.offset <= scanCounterLimit
}
func (sc scanCounter) latching() bool {
return sc.latches > 0
}
func (sc *scanCounter) tick() {
*sc--
if sc.latches > 0 {
sc.latches--
if sc.latches == 0 {
sc.offset = scanCounterLimit
}
} else {
sc.offset--
}
}
type playerSprite struct {
@ -39,39 +57,49 @@ type playerSprite struct {
// of confusion.
tv television.Television
hblank *bool
hblankOffNext *bool
hmoveLatch *bool
// ^^^ references to other parts of the VCS ^^^
// position of the sprite as a polycounter value - the basic principle
// behind VCS sprites is to begin drawing of the sprite when position
// circulates to zero
//
// why do we have an additional phaseclock (in addition to the TIA phase
// clock that is)? from TIA_HW_Notes.txt:
//
// "Beside each counter there is a two-phase clock generator..."
//
// I've interpreted that to mean that each sprite has it's own phase clock
// that can be reset and ticked indpendently. It seems to be correct.
sprClk phaseclock.PhaseClock
position polycounter.Polycounter
// "Beside each counter there is a two-phase clock generator..."
pclk phaseclock.PhaseClock
// in addition to the TIA-wide tiaDelay each sprite keeps track of its own
// delays. this way, we can carefully control when the delayed sprite
// events tick forwards - taking into consideration sprite specific
// conditions
SprDelay future.Ticker
//
// sprites mainly use their own delay but some operations require the
// TIA-wide delay. for those instances a future.Scheduler instance is
// passed to the required function
Delay future.Ticker
// horizontal movement
moreHMOVE bool
hmove uint8
// the following attributes are used for information purposes only
//
// o the name of the sprite instance (eg. "player 0")
// o the pixel at which the sprite was reset
// o the pixel at which the sprite was reset plus any HMOVE modification
//
// see prepareForHMOVE() for a note on the presentation of hmovedPixel
label string
resetPixel int
// the following attributes are used for information purposes only:
// the name of the sprite instance (eg. "player 0")
label string
// the pixel at which the sprite was reset. in the case of the ball and
// missile sprites the scan counter starts at the resetPixel. for the
// player sprite however, there is additional latching to consider. rather
// than introducing an additional variable keeping track of the start
// pixel, the resetPixel is modified according to the player sprite's
// current NUSIZ.
resetPixel int
// the pixel at which the sprite was reset plus any HMOVE modification see
// prepareForHMOVE() for a note on the presentation of hmovedPixel
hmovedPixel int
// ^^^ the above are common to all sprite types ^^^
@ -108,8 +136,14 @@ type playerSprite struct {
resetEvent *future.Event
}
func newPlayerSprite(label string, tv television.Television) *playerSprite {
ps := playerSprite{label: label, tv: tv}
func newPlayerSprite(label string, tv television.Television, hblank, hblankOffNext, hmoveLatch *bool) *playerSprite {
ps := playerSprite{
label: label,
tv: tv,
hblank: hblank,
hblankOffNext: hblankOffNext,
hmoveLatch: hmoveLatch,
}
ps.position.Reset()
return &ps
}
@ -121,12 +155,24 @@ func (ps playerSprite) MachineInfoTerse() string {
// MachineInfo returns the player sprite information in verbose format
func (ps playerSprite) MachineInfo() string {
return ps.String()
s := strings.Builder{}
s.WriteString(ps.String())
s.WriteString("\n")
s.WriteString(fmt.Sprintf("gfx new: %08b", ps.gfxDataNew))
if !ps.verticalDelay {
s.WriteString(" *")
}
s.WriteString("\n")
s.WriteString(fmt.Sprintf("gfx old: %08b", ps.gfxDataOld))
if ps.verticalDelay {
s.WriteString(" *")
}
return s.String()
}
func (ps playerSprite) String() string {
s := strings.Builder{}
s.WriteString(fmt.Sprintf("%s %s [%03d ", ps.position, ps.sprClk, ps.resetPixel))
s.WriteString(fmt.Sprintf("%s %d [%03d ", ps.position, ps.pclk.Count(), ps.resetPixel))
s.WriteString(fmt.Sprintf("(%d)", int(ps.hmove)))
s.WriteString(fmt.Sprintf(" %03d", ps.hmovedPixel))
if ps.moreHMOVE && ps.hmove != 8 {
@ -136,31 +182,45 @@ func (ps playerSprite) String() string {
}
// notes
extra := false
if ps.moreHMOVE {
s.WriteString(" hmoving")
extra = true
}
if ps.scanCounter.active() {
// add a comma if we've already noted something else
if ps.moreHMOVE {
if extra {
s.WriteString(",")
}
s.WriteString(fmt.Sprintf(" drw (px %d)", ps.scanCounter.offset))
extra = true
}
s.WriteString(fmt.Sprintf(" drw (px %d)", ps.scanCounter))
if ps.verticalDelay {
if extra {
s.WriteString(",")
}
s.WriteString(" vdel")
//extra = true
}
return s.String()
}
// tick moves the counters (both position and graphics scan) along for the
// player sprite depending on whether HBLANK is active (visibleScreen) and the
// condition of the sprite's HMOVE counter
func (ps *playerSprite) tick(visibleScreen bool, hmoveCt uint8) {
// tick moves the sprite counters along (both position and graphics scan).
//
// note that the extra clock value caused by an active HMOVE, is not supplied
// directly. that the existance of the extra clock is derived in this tick
// function, depending on the supplied hmoveCt and the whether the sprite's own
// HMOVE value suggests that there should be more movement. see compareHMOVE()
// for details
func (ps *playerSprite) tick(motck bool, hmoveCt uint8) {
// check to see if there is more movement required for this sprite
ps.moreHMOVE = ps.moreHMOVE && compareHMOVE(hmoveCt, ps.hmove)
if visibleScreen || ps.moreHMOVE {
if motck || ps.moreHMOVE {
// tick graphics scan counter during visible screen and during HMOVE.
// from TIA_HW_Notes.txt:
//
@ -178,56 +238,42 @@ func (ps *playerSprite) tick(visibleScreen bool, hmoveCt uint8) {
// together allow 1 in 2 CLK through (2x stretch)."
switch ps.size {
case 0x05:
if ps.sprClk.InPhase() || ps.sprClk.OutOfPhase() {
if ps.pclk.Phi2() || ps.pclk.Phi1() || ps.scanCounter.latching() {
ps.scanCounter.tick()
}
case 0x07:
if ps.sprClk.InPhase() {
if ps.pclk.Phi2() || ps.scanCounter.latching() {
ps.scanCounter.tick()
}
default:
ps.scanCounter.tick()
}
// from TIA_HW_Notes.txt:
//
// "The [MOTCK] (motion clock?) line supplies the CLK signals
// for all movable graphics objects during the visible part of
// the scanline. It is an inverted (out of phase) CLK signal."
ps.sprClk.Tick()
if ps.sprClk.OutOfPhase() {
// as per the comment above we only tick the position counter when the
// sprite's clock is out of phase
ps.pclk.Tick()
// I cannot find a direct reference that describes when position
// counters are ticked forward. however, TIA_HW_Notes.txt does say the
// HSYNC counter ticks forward on the rising edge of Phi2. it is
// reasonable to assume that the sprite position counters do likewise.
if ps.pclk.Phi2() {
ps.position.Tick()
// startDrawingEvent is delayed by 5 ticks. from TIA_HW_Notes.txt:
//
// "Each START decode is delayed by 4 CLK in decoding, plus a
// further 1 CLK to latch the graphics scan counter..."
const startDelay = 5
// I have not seen any mention, in TIA_HW_Notes or anywhere else,
// of a need for a delay to drawing in the event of a reset.
// however, through observation, particularly of
// "my_test_rom/player/testCards", the need for the following
// conditions are clear. I'd be interested to know if it is all
// encompassing and accurate in all instances.
// startDrawingEvent := func() {
// ps.startDrawingEvent = nil
// if ps.resetEvent == nil || ps.resetEvent.RemainingCycles < 3 {
// ps.scanCounter.start()
// } else {
// ps.startDrawingEvent = ps.SprDelay.Schedule(8-ps.resetEvent.RemainingCycles, func() {
// ps.startDrawingEvent = nil
// ps.scanCounter.start()
// }, fmt.Sprintf("start delayed drawing %s", ps.label))
// }
// }
//
// the "further 1 CLK" is actually a further 2 CLKs in the case of
// 2x and 4x size sprites. we'll handle the additional latching in
// the scan counter
//
// note that the additional latching has an impact of what we
// report as being the reset pixel.
const startDelay = 4
startDrawingEvent := func() {
ps.startDrawingEvent = nil
ps.scanCounter.start()
ps.scanCounter.start(ps.size)
}
// "... The START decodes are ANDed with flags from the NUSIZ register
@ -235,18 +281,18 @@ func (ps *playerSprite) tick(visibleScreen bool, hmoveCt uint8) {
switch ps.position.Count {
case 3:
if ps.size == 0x01 || ps.size == 0x03 {
ps.startDrawingEvent = ps.SprDelay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
ps.startDrawingEvent = ps.Delay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
}
case 7:
if ps.size == 0x03 || ps.size == 0x02 || ps.size == 0x06 {
ps.startDrawingEvent = ps.SprDelay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
ps.startDrawingEvent = ps.Delay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
}
case 15:
if ps.size == 0x04 || ps.size == 0x06 {
ps.startDrawingEvent = ps.SprDelay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
ps.startDrawingEvent = ps.Delay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
}
case 39:
ps.startDrawingEvent = ps.SprDelay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
ps.startDrawingEvent = ps.Delay.Schedule(startDelay, startDrawingEvent, fmt.Sprintf("start drawing %s", ps.label))
case 40:
ps.position.Reset()
@ -254,7 +300,7 @@ func (ps *playerSprite) tick(visibleScreen bool, hmoveCt uint8) {
}
// tick future events that are goverened by the sprite
ps.SprDelay.Tick()
ps.Delay.Tick()
}
}
@ -262,14 +308,14 @@ func (ps *playerSprite) prepareForHMOVE() {
ps.moreHMOVE = true
// adjust hmoved pixel now, with the caveat that the value is not valid
// until the HMOVE has completed. presentation of this value should be
// annotated suitably if HMOVE is in progress
// until the HMOVE has completed. in the MachineInfo() function this value
// is annotated with a "*" to indicate that HMOVE is still in progress
ps.hmovedPixel -= int(ps.hmove) - 8
// adjust for screen boundary. silently ignoring values that are outside
// the normal/expected range
if ps.hmovedPixel < 0 {
ps.hmovedPixel = ps.hmovedPixel + 160
ps.hmovedPixel += ps.tv.GetSpec().ClocksPerVisible
}
}
@ -282,12 +328,57 @@ func (ps *playerSprite) resetPosition() {
// There are 5 CLK worth of clocking/latching to take into account,
// so the actual position ends up 5 pixels to the right of the
// reset pixel (approx. 9 pixels after the start of STA RESP0)."
ps.resetEvent = ps.SprDelay.Schedule(5, func() {
delay := 5
// if we're scheduling the reset during a HBLANK however there are extra
// conditions: if during the NEXT cycle HBLANK is still active and there has
// been no HMOVE then the delay is just 1 CLK; if there has been a HMOVE
// then the delay is 2 CLKs
//
// NOTE: some other combinations too
//
// these figures have been gleaned through observation. with some
// supporting notes from the following thread
//
// https://atariage.com/forums/topic/207444-questionproblem-about-sprite-positioning-during-hblank/
//
if *ps.hblank && *ps.hblankOffNext && !*ps.hmoveLatch {
delay = 4
} else if *ps.hblank && !*ps.hblankOffNext && *ps.hmoveLatch {
delay = 2
} else if *ps.hblank && !*ps.hblankOffNext && !*ps.hmoveLatch {
delay = 1
}
// pause pending start drawing events
if ps.startDrawingEvent != nil {
ps.startDrawingEvent.Pause()
}
scheduledDuringHBLANK := *ps.hblank && ps.startDrawingEvent == nil
ps.resetEvent = ps.Delay.Schedule(delay, func() {
// the pixel at which the sprite has been reset, in relation to the
// left edge of the screen
ps.resetPixel, _ = ps.tv.GetState(television.ReqHorizPos)
// no need to adjust for screen boundaries
// resetPixel adjusted because the tv is not yet at the position of the
// new pixel (+1) and another +1 because of the additional clock
// for player sprites after the start signal
ps.resetPixel += 2
// if size is 2x or 4x then we need an additional pixel
//
// note that we need to monkey with resetPixel whenever NUSIZ changes.
// see setNUSIZ() function below
if ps.size == 0x05 || ps.size == 0x07 {
ps.resetPixel++
}
// adjust resetPixel for screen boundaries
if ps.resetPixel > ps.tv.GetSpec().ClocksPerVisible {
ps.resetPixel -= ps.tv.GetSpec().ClocksPerVisible
}
// by definition the current pixel is the same as the reset pixel at
// the moment of reset
@ -295,15 +386,27 @@ func (ps *playerSprite) resetPosition() {
// reset both sprite position and clock
ps.position.Reset()
ps.sprClk.Reset(true)
ps.pclk.Reset()
// drop a running startDrawingEvent from the delay queue
// a player reset doesn't normally start drawing straight away unless
// one was a about to start (within 2 cycles from when the reset was first
// triggered)
//
// if a pending drawing event was more than two cycles away it is
// dropped
//
// rule discovered through observation
if ps.startDrawingEvent != nil {
ps.startDrawingEvent.Drop()
ps.startDrawingEvent = nil
if ps.startDrawingEvent.RemainingCycles <= 2 && !scheduledDuringHBLANK {
ps.startDrawingEvent.Force()
} else {
ps.startDrawingEvent.Drop()
ps.startDrawingEvent = nil
}
}
ps.resetEvent = nil
}, fmt.Sprintf("%s resetting position", ps.label))
}
@ -323,7 +426,7 @@ func (ps *playerSprite) pixel() (bool, uint8) {
// pick the pixel from the gfxData register
if ps.scanCounter.active() {
if gfxData>>uint8(ps.scanCounter)&0x01 == 0x01 {
if gfxData>>uint8(ps.scanCounter.offset)&0x01 == 0x01 {
return true, ps.color
}
}
@ -333,7 +436,7 @@ func (ps *playerSprite) pixel() (bool, uint8) {
return false, ps.color
}
func (ps *playerSprite) setGfxData(data uint8) {
func (ps *playerSprite) setGfxData(delay future.Scheduler, data uint8) {
// no delay necessary. from TIA_HW_Notes.txt:
//
// "Writes to GRP0 always modify the "new" P0 value, and the
@ -342,12 +445,14 @@ func (ps *playerSprite) setGfxData(data uint8) {
// "new" P1 value, and the contents of the "new" P1 are copied
// into "old" P1 whenever GRP0 is written). It is safe to modify
// GRPn at any time, with immediate effect."
ps.otherPlayer.gfxDataOld = ps.otherPlayer.gfxDataNew
ps.gfxDataNew = data
delay.Schedule(2, func() {
ps.otherPlayer.gfxDataOld = ps.otherPlayer.gfxDataNew
ps.gfxDataNew = data
}, fmt.Sprintf("%s GFX", ps.label))
}
func (ps *playerSprite) setVerticalDelay(vdelay bool) {
// no delay necessary. from TIA_HW_Notes.txt:
// from TIA_HW_Notes.txt:
//
// "Vertical Delay bit - this is also read every time a pixel is
// generated and used to select which of the "new" (0) or "old" (1)
@ -355,7 +460,13 @@ func (ps *playerSprite) setVerticalDelay(vdelay bool) {
// the pixel is retrieved from both registers in parallel, and
// this flag used to choose between them at the graphics output).
// It is safe to modify VDELPn at any time, with immediate effect."
ps.verticalDelay = vdelay
//
// the phrase "any time, with immediate effect" suggests that no delay is
// required. however, observations suggests that a delay of 1 cycle is
// needed.
ps.Delay.Schedule(1, func() {
ps.verticalDelay = vdelay
}, fmt.Sprintf("%s VDEL", ps.label))
}
func (ps *playerSprite) setHmoveValue(value uint8) {
@ -386,16 +497,39 @@ func (ps *playerSprite) setReflection(value bool) {
}
func (ps *playerSprite) setNUSIZ(value uint8) {
// if size is 2x or 4x currently then take off the additional pixel. we'll
// add it back on afterwards if needs be
if ps.size == 0x05 || ps.size == 0x07 {
ps.resetPixel--
ps.hmovedPixel--
}
// no delay necessary. from TIA_HW_Notes.txt:
//
// "The NUSIZ register can be changed at any time in order to alter
// the counting frequency, since it is read every graphics CLK.
// This should allow possible player graphics warp effects etc."
ps.size = value & 0x07
ps.Delay.Schedule(2, func() {
ps.size = value & 0x07
}, fmt.Sprintf("%s NUSIZ", ps.label))
// if size is 2x or 4x then we need to record an additional pixel on the
// reset point value
if ps.size == 0x05 || ps.size == 0x07 {
ps.resetPixel++
ps.hmovedPixel++
}
// adjust for screen boundaries
if ps.resetPixel > ps.tv.GetSpec().ClocksPerVisible {
ps.resetPixel -= ps.tv.GetSpec().ClocksPerVisible
}
if ps.hmovedPixel > ps.tv.GetSpec().ClocksPerVisible {
ps.hmovedPixel -= ps.tv.GetSpec().ClocksPerVisible
}
}
func (ps *playerSprite) setColor(value uint8) {
// there is nothing in TIA_HW_Notes.txt about the color registers but I
// don't believe there is a need for a delay
// there is nothing in TIA_HW_Notes.txt about the color registers
ps.color = value
}

View file

@ -2,7 +2,6 @@ package video
import (
"fmt"
"gopher2600/hardware/tia/delay"
"gopher2600/hardware/tia/delay/future"
"gopher2600/hardware/tia/phaseclock"
"gopher2600/hardware/tia/polycounter"
@ -10,11 +9,8 @@ import (
)
type playfield struct {
tiaClk *phaseclock.PhaseClock
hsync *polycounter.Polycounter
// tiaDelay is not currently used
tiaDelay future.Scheduler
pclk *phaseclock.PhaseClock
hsync *polycounter.Polycounter
// the color for the when playfield is on/off
foregroundColor uint8
@ -52,8 +48,8 @@ type playfield struct {
currentPixelIsOn bool
}
func newPlayfield(tiaClk *phaseclock.PhaseClock, hsync *polycounter.Polycounter, tiaDelay future.Scheduler) *playfield {
pf := playfield{tiaClk: tiaClk, hsync: hsync, tiaDelay: tiaDelay}
func newPlayfield(pclk *phaseclock.PhaseClock, hsync *polycounter.Polycounter) *playfield {
pf := playfield{pclk: pclk, hsync: hsync}
return &pf
}
@ -144,9 +140,7 @@ func (pf *playfield) pixel() (bool, uint8) {
newPixel := false
if pf.tiaClk.InPhase() {
newPixel = true
if pf.pclk.Phi2() {
// RSYNC can monkey with the current hsync value unexpectedly and
// because of this we need an extra effort to make sure we're in the
// correct screen region.
@ -196,41 +190,40 @@ func (pf *playfield) pixel() (bool, uint8) {
}
func (pf *playfield) scheduleWrite(segment int, value uint8, futureWrite future.Scheduler) {
var f func()
switch segment {
case 0:
f = func() {
pf.pf0 = value & 0xf0
pf.data[0] = pf.pf0&0x10 == 0x10
pf.data[1] = pf.pf0&0x20 == 0x20
pf.data[2] = pf.pf0&0x40 == 0x40
pf.data[3] = pf.pf0&0x80 == 0x80
}
pf.pf0 = value & 0xf0
pf.data[0] = pf.pf0&0x10 == 0x10
pf.data[1] = pf.pf0&0x20 == 0x20
pf.data[2] = pf.pf0&0x40 == 0x40
pf.data[3] = pf.pf0&0x80 == 0x80
case 1:
f = func() {
pf.pf1 = value
pf.data[4] = pf.pf1&0x80 == 0x80
pf.data[5] = pf.pf1&0x40 == 0x40
pf.data[6] = pf.pf1&0x20 == 0x20
pf.data[7] = pf.pf1&0x10 == 0x10
pf.data[8] = pf.pf1&0x08 == 0x08
pf.data[9] = pf.pf1&0x04 == 0x04
pf.data[10] = pf.pf1&0x02 == 0x02
pf.data[11] = pf.pf1&0x01 == 0x01
}
pf.pf1 = value
pf.data[4] = pf.pf1&0x80 == 0x80
pf.data[5] = pf.pf1&0x40 == 0x40
pf.data[6] = pf.pf1&0x20 == 0x20
pf.data[7] = pf.pf1&0x10 == 0x10
pf.data[8] = pf.pf1&0x08 == 0x08
pf.data[9] = pf.pf1&0x04 == 0x04
pf.data[10] = pf.pf1&0x02 == 0x02
pf.data[11] = pf.pf1&0x01 == 0x01
case 2:
f = func() {
pf.pf2 = value
pf.data[12] = pf.pf2&0x01 == 0x01
pf.data[13] = pf.pf2&0x02 == 0x02
pf.data[14] = pf.pf2&0x04 == 0x04
pf.data[15] = pf.pf2&0x08 == 0x08
pf.data[16] = pf.pf2&0x10 == 0x10
pf.data[17] = pf.pf2&0x20 == 0x20
pf.data[18] = pf.pf2&0x40 == 0x40
pf.data[19] = pf.pf2&0x80 == 0x80
}
pf.pf2 = value
pf.data[12] = pf.pf2&0x01 == 0x01
pf.data[13] = pf.pf2&0x02 == 0x02
pf.data[14] = pf.pf2&0x04 == 0x04
pf.data[15] = pf.pf2&0x08 == 0x08
pf.data[16] = pf.pf2&0x10 == 0x10
pf.data[17] = pf.pf2&0x20 == 0x20
pf.data[18] = pf.pf2&0x40 == 0x40
pf.data[19] = pf.pf2&0x80 == 0x80
}
futureWrite.Schedule(delay.WritePlayfield, f, "writing")
}
func (pf *playfield) setColor(col uint8) {
pf.foregroundColor = col
}
func (pf *playfield) setBackground(col uint8) {
pf.backgroundColor = col
}

View file

@ -2,7 +2,6 @@ package video
import (
"gopher2600/hardware/tia/delay/future"
"gopher2600/hardware/tia/phaseclock"
"gopher2600/hardware/tia/polycounter"
"strings"
)
@ -15,8 +14,6 @@ type sprite struct {
// missile 1)
label string
tiaclk *phaseclock.PhaseClock
// position of the sprite as a polycounter value - the basic principle
// behind VCS sprites is to begin drawing of the sprite when position
// circulates to zero
@ -51,8 +48,8 @@ type sprite struct {
resetFuture *future.Event
}
func newSprite(label string, tiaclk *phaseclock.PhaseClock, spriteTick func()) *sprite {
sp := sprite{label: label, tiaclk: tiaclk, spriteTick: spriteTick}
func newSprite(label string, spriteTick func()) *sprite {
sp := sprite{label: label, spriteTick: spriteTick}
// the direction of count and max is important - don't monkey with it
sp.graphicsScanMax = 8
@ -78,24 +75,11 @@ func (sp *sprite) resetPosition() {
sp.position.Reset()
// note reset position of sprite, in pixels
sp.resetPixel = -68 + int((sp.position.Count * 4)) + int(*sp.tiaclk)
//sp.resetPixel = -68 + int((sp.position.Count * 4)) + int(*sp.pclk)
sp.currentPixel = sp.resetPixel
}
func (sp *sprite) checkForGfxStart(triggerList []int) (bool, bool) {
if sp.tiaclk.InPhase() {
if sp.position.Tick() {
return true, false
}
// check for start positions of additional copies of the sprite
for _, v := range triggerList {
if v == int(sp.position.Count) {
return true, true
}
}
}
return false, false
}

View file

@ -24,7 +24,7 @@ type Video struct {
Missile1 *missileSprite
Ball *ballSprite
tiaDelay future.Scheduler
Delay future.Scheduler
}
// colors to use for debugging - these are the same colours used by the Stella
@ -41,48 +41,49 @@ const (
// NewVideo is the preferred method of initialisation for the Video structure
//
// the playfield and sprite objects have access to both tiaClk and hsync.
// the playfield and sprite objects have access to both pclk and hsync.
// in the case of the playfield, they are used to decide which part of the
// playfield is to be drawn. in the case of the the sprite objects, they
// are used only for information purposes - namely the reset and current
// pisel locatoin of the sprites in relation to the hsync counter (or
// screen)
//
// the tiaDelay scheduler is used to queue up sprite reset events and a few
// the Delay scheduler is used to queue up sprite reset events and a few
// other events (!!TODO: figuring out what is delayed and how is not yet
// completed)
func NewVideo(tiaClk *phaseclock.PhaseClock,
func NewVideo(pclk *phaseclock.PhaseClock,
hsync *polycounter.Polycounter,
tiaDelay future.Scheduler,
Delay future.Scheduler,
mem memory.ChipBus,
tv television.Television) *Video {
tv television.Television,
hblank, hblankOffNext, hmoveLatch *bool) *Video {
vd := &Video{tiaDelay: tiaDelay}
vd := &Video{Delay: Delay}
// collision matrix
vd.collisions = newCollision(mem)
// playfield
vd.Playfield = newPlayfield(tiaClk, hsync, tiaDelay)
vd.Playfield = newPlayfield(pclk, hsync)
// sprite objects
vd.Player0 = newPlayerSprite("player0", tv)
vd.Player0 = newPlayerSprite("player0", tv, hblank, hblankOffNext, hmoveLatch)
if vd.Player0 == nil {
return nil
}
vd.Player1 = newPlayerSprite("player1", tv)
vd.Player1 = newPlayerSprite("player1", tv, hblank, hblankOffNext, hmoveLatch)
if vd.Player1 == nil {
return nil
}
vd.Missile0 = newMissileSprite("missile0", tiaClk)
vd.Missile0 = newMissileSprite("missile0")
if vd.Missile0 == nil {
return nil
}
vd.Missile1 = newMissileSprite("missile1", tiaClk)
vd.Missile1 = newMissileSprite("missile1")
if vd.Missile1 == nil {
return nil
}
vd.Ball = newBallSprite("ball", tiaClk)
vd.Ball = newBallSprite("ball")
if vd.Ball == nil {
return nil
}
@ -98,11 +99,11 @@ func NewVideo(tiaClk *phaseclock.PhaseClock,
return vd
}
// TickSprites moves all video elements forward one video cycle and is only
// Tick moves all video elements forward one video cycle and is only
// called when motion clock is active
func (vd *Video) TickSprites(visibleScreen bool, hmoveCt uint8) {
vd.Player0.tick(visibleScreen, hmoveCt)
vd.Player1.tick(visibleScreen, hmoveCt)
func (vd *Video) Tick(motck bool, hmoveCt uint8) {
vd.Player0.tick(motck, hmoveCt)
vd.Player1.tick(motck, hmoveCt)
vd.Missile0.tick()
vd.Missile1.tick()
vd.Ball.tick()
@ -243,7 +244,7 @@ func (vd *Video) Resolve() (uint8, uint8) {
col = blc
dcol = debugColBall
} else if pfu {
if vd.Playfield.scoremode == true {
if vd.Playfield.scoremode {
if vd.Playfield.screenRegion == 2 {
col = p1c
} else {
@ -276,21 +277,17 @@ func (vd *Video) ReadMemory(register string, value uint8) bool {
// colour
case "COLUP0":
vd.Player0.setColor(value & 0xfe)
vd.Missile0.scheduleSetColor(value&0xfe, vd.tiaDelay)
vd.Missile0.scheduleSetColor(value&0xfe, vd.Delay)
case "COLUP1":
vd.Player1.setColor(value & 0xfe)
vd.Missile1.scheduleSetColor(value&0xfe, vd.tiaDelay)
vd.Missile1.scheduleSetColor(value&0xfe, vd.Delay)
// playfield / color
case "COLUBK":
// vd.onFutureColorClock.Schedule(delay.WritePlayfieldColor, func() {
vd.Playfield.backgroundColor = value & 0xfe
// }, "setting COLUBK")
vd.Playfield.setBackground(value & 0xfe)
case "COLUPF":
// vd.onFutureColorClock.Schedule(delay.WritePlayfieldColor, func() {
vd.Playfield.foregroundColor = value & 0xfe
vd.Playfield.setColor(value & 0xfe)
vd.Ball.color = value & 0xfe
// }, "setting COLUPF")
// playfield
case "CTRLPF":
@ -300,25 +297,25 @@ func (vd *Video) ReadMemory(register string, value uint8) bool {
vd.Playfield.scoremode = value&0x02 == 0x02
vd.Playfield.priority = value&0x04 == 0x04
case "PF0":
vd.Playfield.scheduleWrite(0, value, vd.tiaDelay)
vd.Playfield.scheduleWrite(0, value, vd.Delay)
case "PF1":
vd.Playfield.scheduleWrite(1, value, vd.tiaDelay)
vd.Playfield.scheduleWrite(1, value, vd.Delay)
case "PF2":
vd.Playfield.scheduleWrite(2, value, vd.tiaDelay)
vd.Playfield.scheduleWrite(2, value, vd.Delay)
// ball sprite
case "ENABL":
vd.Ball.scheduleEnable(value&0x02 == 0x02, vd.tiaDelay)
vd.Ball.scheduleEnable(value&0x02 == 0x02, vd.Delay)
case "RESBL":
vd.Ball.scheduleReset(vd.tiaDelay)
vd.Ball.scheduleReset(vd.Delay)
case "VDELBL":
vd.Ball.scheduleVerticalDelay(value&0x01 == 0x01, vd.tiaDelay)
vd.Ball.scheduleVerticalDelay(value&0x01 == 0x01, vd.Delay)
// player sprites
case "GRP0":
vd.Player0.setGfxData(value)
vd.Player0.setGfxData(vd.Delay, value)
case "GRP1":
vd.Player1.setGfxData(value)
vd.Player1.setGfxData(vd.Delay, value)
case "RESP0":
vd.Player0.resetPosition()
case "RESP1":
@ -334,25 +331,25 @@ func (vd *Video) ReadMemory(register string, value uint8) bool {
// missile sprites
case "ENAM0":
vd.Missile0.scheduleEnable(value&0x02 == 0x02, vd.tiaDelay)
vd.Missile0.scheduleEnable(value&0x02 == 0x02, vd.Delay)
case "ENAM1":
vd.Missile1.scheduleEnable(value&0x02 == 0x02, vd.tiaDelay)
vd.Missile1.scheduleEnable(value&0x02 == 0x02, vd.Delay)
case "RESM0":
vd.Missile0.scheduleReset(vd.tiaDelay)
vd.Missile0.scheduleReset(vd.Delay)
case "RESM1":
vd.Missile1.scheduleReset(vd.tiaDelay)
vd.Missile1.scheduleReset(vd.Delay)
case "RESMP0":
vd.Missile0.scheduleResetToPlayer(value&0x02 == 0x002, vd.tiaDelay)
vd.Missile0.scheduleResetToPlayer(value&0x02 == 0x002, vd.Delay)
case "RESMP1":
vd.Missile1.scheduleResetToPlayer(value&0x02 == 0x002, vd.tiaDelay)
vd.Missile1.scheduleResetToPlayer(value&0x02 == 0x002, vd.Delay)
// player & missile sprites
case "NUSIZ0":
vd.Player0.setNUSIZ(value)
vd.Missile0.scheduleSetNUSIZ(value, vd.tiaDelay)
vd.Missile0.scheduleSetNUSIZ(value, vd.Delay)
case "NUSIZ1":
vd.Player1.setNUSIZ(value)
vd.Missile1.scheduleSetNUSIZ(value, vd.tiaDelay)
vd.Missile1.scheduleSetNUSIZ(value, vd.Delay)
// clear collisions
case "CXCLR":

View file

@ -31,8 +31,7 @@ type VCS struct {
func NewVCS(tv television.Television) (*VCS, error) {
var err error
vcs := new(VCS)
vcs.TV = tv
vcs := &VCS{TV: tv}
vcs.Mem, err = memory.NewVCSMemory()
if err != nil {
@ -140,48 +139,68 @@ func (vcs *VCS) Step(videoCycleCallback func(*result.Instruction) error) (*resul
var r *result.Instruction
var err error
// the cpu calls the cycleVCS function after every CPU cycle. the cycleVCS
// function defines the order of operation for the rest of the VCS for
// every CPU cycle.
// the cpu calls the videoCycle function after every CPU cycle. the
// videoCycle function defines the order of operation for the rest of the
// VCS for every CPU cycle.
//
// this block represents the Q0 cycle
//
// !!TODO: the following would be a good test case for the proposed try()
// function, coming in a future language version
cycleVCS := func(r *result.Instruction) error {
videoCycle := func(r *result.Instruction) error {
// ensure controllers have updated their input
if err := vcs.strobeUserInput(); err != nil {
return err
}
// read riot memory and step once per CPU cycle
// update RIOT memory and step
//
vcs.RIOT.ReadMemory()
vcs.RIOT.Step()
// read tia memory once per cpu cycle
// three color clocks per CPU cycle so we run video cycle three times.
// step one ...
vcs.CPU.RdyFlg, err = vcs.TIA.Step()
if err != nil {
return err
}
_ = videoCycleCallback(r)
// update TIA from memory. from "TIA 1A" document:
//
// "if the read-write line is low, the data [...] will be writted in
// the addressed write location when the Q2 clock goes from high to
// low."
//
// from my understanding, we can say that this always happens after the
// first TIA step and before the second.
vcs.TIA.ReadMemory()
// three color clocks per CPU cycle so we run video cycle three times
// ... tia step two ...
vcs.CPU.RdyFlg, err = vcs.TIA.Step()
if err != nil {
return err
}
videoCycleCallback(r)
_ = videoCycleCallback(r)
// ... tia step three
vcs.CPU.RdyFlg, err = vcs.TIA.Step()
if err != nil {
return err
}
videoCycleCallback(r)
_ = videoCycleCallback(r)
vcs.CPU.RdyFlg, err = vcs.TIA.Step()
if err != nil {
return err
}
videoCycleCallback(r)
// also from the "TIA 1A" document:
//
// "If the read-write line is high, the addressed location can be read
// by the microprocessor..."
//
// we don't need to do anything here. any writes that have happened are
// sitting in memory ready for the CPU.
return nil
}
r, err = vcs.CPU.ExecuteInstruction(cycleVCS)
r, err = vcs.CPU.ExecuteInstruction(videoCycle)
if err != nil {
return nil, err
}
@ -189,7 +208,7 @@ func (vcs *VCS) Step(videoCycleCallback func(*result.Instruction) error) (*resul
// CPU has been left in the unready state - continue cycling the VCS hardware
// until the CPU is ready
for !vcs.CPU.RdyFlg {
cycleVCS(r)
_ = videoCycle(r)
}
return r, nil
@ -201,24 +220,24 @@ func (vcs *VCS) Step(videoCycleCallback func(*result.Instruction) error) (*resul
func (vcs *VCS) Run(continueCheck func() (bool, error)) error {
var err error
cycleVCS := func(r *result.Instruction) error {
// ensure controllers have updated their inpu
videoCycle := func(r *result.Instruction) error {
// see videoCycle in Step() function for an explanation for what's
// going on here
if err := vcs.strobeUserInput(); err != nil {
return err
}
_, _ = vcs.TIA.Step()
vcs.TIA.ReadMemory()
vcs.RIOT.ReadMemory()
vcs.RIOT.Step()
vcs.TIA.ReadMemory()
vcs.TIA.Step()
vcs.TIA.Step()
_, _ = vcs.TIA.Step()
vcs.CPU.RdyFlg, err = vcs.TIA.Step()
return err
}
cont := true
for cont {
_, err = vcs.CPU.ExecuteInstruction(cycleVCS)
_, err = vcs.CPU.ExecuteInstruction(videoCycle)
if err != nil {
return err
}
@ -241,6 +260,9 @@ func (vcs *VCS) RunForFrameCount(numFrames int) error {
for fn != targetFrame {
_, err = vcs.Step(func(*result.Instruction) error { return nil })
if err != nil {
return err
}
fn, err = vcs.TV.GetState(television.ReqFramenum)
if err != nil {
return err

View file

@ -33,7 +33,7 @@ func init() {
SpecNTSC = new(Specification)
SpecNTSC.ID = "NTSC"
SpecNTSC.ClocksPerHblank = 68
SpecNTSC.ClocksPerVisible = 160
SpecNTSC.ClocksPerVisible = 160 // counting from 0
SpecNTSC.ClocksPerScanline = 228
SpecNTSC.ScanlinesPerVSync = 3
SpecNTSC.ScanlinesPerVBlank = 37
@ -47,7 +47,7 @@ func init() {
SpecPAL = new(Specification)
SpecPAL.ID = "PAL"
SpecPAL.ClocksPerHblank = 68
SpecPAL.ClocksPerVisible = 160
SpecPAL.ClocksPerVisible = 160 // counting from 0
SpecPAL.ClocksPerScanline = 228
SpecPAL.ScanlinesPerVSync = 3
SpecPAL.ScanlinesPerVBlank = 45