mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
o player sprite
- player sprite now works fully for Pitfall Keystone player test cards - tv debugging output is accurate
This commit is contained in:
parent
c424a4a24b
commit
dfde00462b
17 changed files with 562 additions and 377 deletions
4
FUTURE
4
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
|
||||
----------
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue