From dfde00462bce2485e85583da25f1dfdec882e59b Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 18 Jul 2019 20:48:42 +0100 Subject: [PATCH] o player sprite - player sprite now works fully for Pitfall Keystone player test cards - tv debugging output is accurate --- FUTURE | 4 + debugger/commands.go | 26 +- debugger/help.go | 1 + hardware/tia/delay/future/event.go | 64 +++-- hardware/tia/delay/future/ticker.go | 4 +- hardware/tia/phaseclock/phaseclock.go | 71 +++--- hardware/tia/polycounter/polycounter.go | 15 +- hardware/tia/tia.go | 153 +++++++----- hardware/tia/video/ball.go | 5 +- hardware/tia/video/compareHMOVE.go | 15 +- hardware/tia/video/missile.go | 5 +- hardware/tia/video/player.go | 316 +++++++++++++++++------- hardware/tia/video/playfield.go | 79 +++--- hardware/tia/video/sprite.go | 22 +- hardware/tia/video/video.go | 81 +++--- hardware/vcs.go | 74 ++++-- television/specifications.go | 4 +- 17 files changed, 562 insertions(+), 377 deletions(-) diff --git a/FUTURE b/FUTURE index 0a732e38..40fa2ede 100644 --- a/FUTURE +++ b/FUTURE @@ -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 ---------- diff --git a/debugger/commands.go b/debugger/commands.go index 221fa85c..c80c7827 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -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() } diff --git a/debugger/help.go b/debugger/help.go index 249b7a6c..77755d15 100644 --- a/debugger/help.go +++ b/debugger/help.go @@ -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", diff --git a/hardware/tia/delay/future/event.go b/hardware/tia/delay/future/event.go index 8c257a37..ecd493e5 100644 --- a/hardware/tia/delay/future/event.go +++ b/hardware/tia/delay/future/event.go @@ -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 } diff --git a/hardware/tia/delay/future/ticker.go b/hardware/tia/delay/future/ticker.go index f8013aa1..63713aed 100644 --- a/hardware/tia/delay/future/ticker.go +++ b/hardware/tia/delay/future/ticker.go @@ -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 { diff --git a/hardware/tia/phaseclock/phaseclock.go b/hardware/tia/phaseclock/phaseclock.go index 9a83b774..f7df54a7 100644 --- a/hardware/tia/phaseclock/phaseclock.go +++ b/hardware/tia/phaseclock/phaseclock.go @@ -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 +} diff --git a/hardware/tia/polycounter/polycounter.go b/hardware/tia/polycounter/polycounter.go index 0efa55b3..150f8c19 100644 --- a/hardware/tia/polycounter/polycounter.go +++ b/hardware/tia/polycounter/polycounter.go @@ -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() -} diff --git a/hardware/tia/tia.go b/hardware/tia/tia.go index ab23b2ad..33c2bce2 100644 --- a/hardware/tia/tia.go +++ b/hardware/tia/tia.go @@ -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) diff --git a/hardware/tia/video/ball.go b/hardware/tia/video/ball.go index bbbc46f0..161d7b69 100644 --- a/hardware/tia/video/ball.go +++ b/hardware/tia/video/ball.go @@ -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 } diff --git a/hardware/tia/video/compareHMOVE.go b/hardware/tia/video/compareHMOVE.go index ac448691..e08bc43d 100644 --- a/hardware/tia/video/compareHMOVE.go +++ b/hardware/tia/video/compareHMOVE.go @@ -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 } diff --git a/hardware/tia/video/missile.go b/hardware/tia/video/missile.go index 057e8161..f5bd6b0f 100644 --- a/hardware/tia/video/missile.go +++ b/hardware/tia/video/missile.go @@ -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 } diff --git a/hardware/tia/video/player.go b/hardware/tia/video/player.go index 0b17d7bc..1181d1a3 100644 --- a/hardware/tia/video/player.go +++ b/hardware/tia/video/player.go @@ -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 } diff --git a/hardware/tia/video/playfield.go b/hardware/tia/video/playfield.go index fb3b144e..2cc938df 100644 --- a/hardware/tia/video/playfield.go +++ b/hardware/tia/video/playfield.go @@ -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 } diff --git a/hardware/tia/video/sprite.go b/hardware/tia/video/sprite.go index 6ce1480c..1799b48e 100644 --- a/hardware/tia/video/sprite.go +++ b/hardware/tia/video/sprite.go @@ -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 } diff --git a/hardware/tia/video/video.go b/hardware/tia/video/video.go index e3f111c0..59c19cca 100644 --- a/hardware/tia/video/video.go +++ b/hardware/tia/video/video.go @@ -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": diff --git a/hardware/vcs.go b/hardware/vcs.go index bcc7645c..5ac92351 100644 --- a/hardware/vcs.go +++ b/hardware/vcs.go @@ -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 diff --git a/television/specifications.go b/television/specifications.go index 23ed019a..6587641d 100644 --- a/television/specifications.go +++ b/television/specifications.go @@ -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