From 6c1efce278a685e63512626dad2f05f8c702f5a1 Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Mon, 22 Apr 2019 23:24:28 +0200 Subject: [PATCH] More TIA documentation. --- src/emucore/tia/Ball.cxx | 15 ++- src/emucore/tia/Ball.hxx | 235 ++++++++++++++++++++++++++++++++-- src/emucore/tia/Playfield.hxx | 8 +- 3 files changed, 236 insertions(+), 22 deletions(-) diff --git a/src/emucore/tia/Ball.cxx b/src/emucore/tia/Ball.cxx index 9269a6e26..3fb95449f 100644 --- a/src/emucore/tia/Ball.cxx +++ b/src/emucore/tia/Ball.cxx @@ -37,7 +37,7 @@ void Ball::reset() myIsEnabledNew = false; myIsEnabled = false; myIsDelaying = false; - myIsVisible = false; + mySignalActive = false; myHmmClocks = 0; myCounter = 0; isMoving = false; @@ -168,8 +168,11 @@ void Ball::startMovement() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Ball::nextLine() { - myIsVisible = myIsRendering && myRenderCounter >= 0; - collision = (myIsVisible && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled; + // Reevalute the collision mask in order to properly account for collisions during + // hblank. Usually, this will be taken care off in the next tick, but there is no + // next tick before hblank ends. + mySignalActive = myIsRendering && myRenderCounter >= 0; + collision = (mySignalActive && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -199,7 +202,7 @@ void Ball::updateEnabled() { myIsEnabled = !myIsSuppressed && (myIsDelaying ? myIsEnabledOld : myIsEnabledNew); - collision = (myIsVisible && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled; + collision = (mySignalActive && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled; myTIA->scheduleCollisionUpdate(); } @@ -259,7 +262,7 @@ bool Ball::save(Serializer& out) const out.putBool(myIsEnabled); out.putBool(myIsSuppressed); out.putBool(myIsDelaying); - out.putBool(myIsVisible); + out.putBool(mySignalActive); out.putByte(myHmmClocks); out.putByte(myCounter); @@ -300,7 +303,7 @@ bool Ball::load(Serializer& in) myIsEnabled = in.getBool(); myIsSuppressed = in.getBool(); myIsDelaying = in.getBool(); - myIsVisible = in.getBool(); + mySignalActive = in.getBool(); myHmmClocks = in.getByte(); myCounter = in.getByte(); diff --git a/src/emucore/tia/Ball.hxx b/src/emucore/tia/Ball.hxx index 274206827..7bfe77cda 100644 --- a/src/emucore/tia/Ball.hxx +++ b/src/emucore/tia/Ball.hxx @@ -28,52 +28,134 @@ class Ball : public Serializable { public: + /** + The collision mask is injected at construction + */ explicit Ball(uInt32 collisionMask); public: + /** + Set the TIA instance + */ void setTIA(TIA* tia) { myTIA = tia; } + /** + Reset to initial state. + */ void reset(); + /** + ENABL write. + */ void enabl(uInt8 value); + /** + HMBL write. + */ void hmbl(uInt8 value); + /** + RESBL write. + */ void resbl(uInt8 counter); + /** + CTRLPF write. + */ void ctrlpf(uInt8 value); + /** + VDELBL write. + */ void vdelbl(uInt8 value); - void toggleCollisions(bool enabled); - + /** + Enable / disable ball display (debugging only, not used during normal emulation). + */ void toggleEnabled(bool enabled); + /** + Enable / disable ball collisions (debugging only, not used during normal emulation). + */ + void toggleCollisions(bool enabled); + + /** + Set color PF. + */ void setColor(uInt8 color); + /** + Set the color used in "debug colors" mode. + */ void setDebugColor(uInt8 color); + + /** + Enable "debug colors" mode. + */ + void enableDebugColors(bool enabled); + /** + Update internal state to use the color loss palette. + */ void applyColorLoss(); + /** + Switch to "inverted phase" mode. This mode emulates the phase shift + between movement and ordinary clock pulses that is exhibited by some + TIA revisions and that give rise to glitches like the infamous Cool + Aid Man bug on some Jr. models. + */ void setInvertedPhaseClock(bool enable); + /** + Start movement --- this is triggered by strobing HMOVE. + */ void startMovement(); + /** + Notify ball of line change. + */ void nextLine(); + /** + Is the ball visible? This is determined by looking at bit 15 + of the collision mask. + */ bool isOn() const { return (collision & 0x8000); } + + /** + Get the current color. + */ uInt8 getColor() const { return myColor; } + /** + Shuffle the enabled flag. This is called in VDELBL mode when GRP1 is + written (with a delay of one cycle). + */ void shuffleStatus(); + /** + Calculate the sprite position from the counter. Used by the debugger only. + */ uInt8 getPosition() const; + + /** + Set the counter and place the sprite at a specified position. Used by the debugger + only. + */ void setPosition(uInt8 newPosition); + /** + Get the "old" and "new" values of the enabled flag. Used by the debuggger only. + */ bool getENABLOld() const { return myIsEnabledOld; } bool getENABLNew() const { return myIsEnabledNew; } + /** + Directly set the "old" value of the enabled flag. Used by the debugger only. + */ void setENABLOld(bool enabled); /** @@ -82,54 +164,168 @@ class Ball : public Serializable bool save(Serializer& out) const override; bool load(Serializer& in) override; + /** + Process a single movement tick. Inline for performance (implementation below). + */ inline void movementTick(uInt32 clock, bool hblank); - inline void tick(bool isReceivingMclock = true); + /** + Tick one color clock. Inline for performance (implementation below). + */ + inline void tick(bool isReceivingRegularClock = true); public: + /** + 16 bit Collision mask. Each sprite is represented by a single bit in the mask + (1 = active, 0 = inactive). All other bits are always 1. The highest bit is + abused to store visibility (as the actual collision bit will always be zero + if collisions are disabled). + */ uInt32 collision; + + /** + The movement flag. This corresponds to the state of the movement latch for + this sprite --- true while movement is active and ticks are still propagated + to the counters, false otherwise. + */ bool isMoving; private: + /** + Recalculate enabled / disabled state. This is not the same as the enabled / disabled + flag, but rather calculated from the flag and the corresponding debug setting. + */ void updateEnabled(); + + /** + Recalculate ball color based on COLUPF, debug colors, color loss, etc. + */ void applyColors(); private: + /** + Offset of the render counter when rendering starts. Actual display starts at zero, + so this amounts to a delay. + */ enum Count: Int8 { renderCounterOffset = -4 }; private: + /** + Collision mask values for active / inactive states. Disabling collisions + will change those. + */ uInt32 myCollisionMaskDisabled; uInt32 myCollisionMaskEnabled; + /** + Color value calculated by applyColors(). + */ uInt8 myColor; - uInt8 myObjectColor, myDebugColor; + + /** + Color configured by COLUPF + */ + uInt8 myObjectColor; + + /** + Color for debug mode. + */ + uInt8 myDebugColor; + + /** + Debug mode enabled? + */ bool myDebugEnabled; + /** + "old" and "new" values of the enabled flag. + */ bool myIsEnabledOld; bool myIsEnabledNew; - bool myIsEnabled; - bool myIsSuppressed; - bool myIsDelaying; - bool myIsVisible; + /** + Actual value of the enabled flag. Determined from the "old" and "new" values + VDEL, debug settings etc. + */ + bool myIsEnabled; + + /** + Is the sprite turned off in the debugger? + */ + bool myIsSuppressed; + + /** + Is VDEL active? + */ + bool myIsDelaying; + + /** + Is the ball sprite signal currently active? + */ + bool mySignalActive; + + /** + HMM clocks before movement stops. Changed by writing to HMBL. + */ uInt8 myHmmClocks; + + /** + The sprite counter + */ uInt8 myCounter; + + /** + Ball width, as configured by CTRLPF. + */ uInt8 myWidth; + + /** + Effective width used for drawing. This is usually the same as myWidth, + but my differ in starfield mode. + */ uInt8 myEffectiveWidth; + + /** + The value of the counter value at which the last movement tick occurred. This is + used for simulating the starfield pattern. + */ uInt8 myLastMovementTick; + /** + Are we currently rendering? This is latched when the counter hits it decode value, + or when RESBL is strobed. It is turned off once the render counter reaches its + maximum (i.e. when the sprite has been fully displayed). + */ bool myIsRendering; + + /** + Rendering counter. It starts counting (below zero) when the counter hits the decode value, + and the actual signal becomes active once it reaches 0. + */ Int8 myRenderCounter; + /** + This memorizes a movement tick outside HBLANK in inverted clock mode. It is latched + durin ::movementTick() and processed during ::tick() where it inhibits the clock + pulse. + */ bool myInvertedPhaseClock; + + /** + Use "inverted movement clock phase" mode? This emulates an idiosyncracy of several + newer TIA revisions (see the setter above for a deeper explanation). + */ bool myUseInvertedPhaseClock; + /** + TIA instance. Required for flushing the line cache and requesting collision updates. + */ TIA* myTIA; private: @@ -149,34 +345,49 @@ void Ball::movementTick(uInt32 clock, bool hblank) { myLastMovementTick = myCounter; + // Stop movement once the number of clocks according to HMBL is reached if (clock == myHmmClocks) isMoving = false; if(isMoving) { + // Process the tick if we are in hblank. Otherwise, the tick is either masked + // by an ordinary tick or merges two consecutive ticks into a single tick (inverted + // movement clock phase mode). if (hblank) tick(false); + + // Track a tick outside hblank for later processing myInvertedPhaseClock = !hblank; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Ball::tick(bool isReceivingMclock) +void Ball::tick(bool isReceivingRegularClock) { + // If we are in inverted movement clock phase mode and a movement tick occurred, it + // will supress the tick. if(myUseInvertedPhaseClock && myInvertedPhaseClock) { myInvertedPhaseClock = false; return; } - myIsVisible = myIsRendering && myRenderCounter >= 0; - collision = (myIsVisible && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled; + // Turn on the signal if the render counter reaches the threshold + mySignalActive = myIsRendering && myRenderCounter >= 0; - bool starfieldEffect = isMoving && isReceivingMclock; + // Consider enabled status and the signal to determine visibility (as represented + // by the collision mask) + collision = (mySignalActive && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled; + // Regular clock pulse during movement -> starfield mode + bool starfieldEffect = isMoving && isReceivingRegularClock; + + // Decode value that triggers rendering if (myCounter == 156) { myIsRendering = true; myRenderCounter = renderCounterOffset; + // What follows is an effective description of ball width in starfield mode. uInt8 starfieldDelta = (myCounter + TIAConstants::H_PIXEL - myLastMovementTick) % 4; if (starfieldEffect && starfieldDelta == 3 && myWidth < 4) ++myRenderCounter; diff --git a/src/emucore/tia/Playfield.hxx b/src/emucore/tia/Playfield.hxx index 5bc0b547b..c8389b90b 100644 --- a/src/emucore/tia/Playfield.hxx +++ b/src/emucore/tia/Playfield.hxx @@ -110,7 +110,7 @@ class Playfield : public Serializable void nextLine(); /** - Is the playfield signal active? This is determined by looking at bit 8 + Is the playfield visible? This is determined by looking at bit 15 of the collision mask. */ bool isOn() const { return (collision & 0x8000); } @@ -136,8 +136,8 @@ class Playfield : public Serializable /** 16 bit Collision mask. Each sprite is represented by a single bit in the mask (1 = active, 0 = inactive). All other bits are always 1. The highest bit is - abused to store the active / inactive state (as the actual collision bit will - always be zero if collisions are disabled). + abused to store visibility (as the actual collision bit will always be zero + if collisions are disabled). */ uInt32 collision; @@ -234,7 +234,7 @@ class Playfield : public Serializable uInt32 myX; /** - TIA instance. + TIA instance. Required for flushing the line cache. */ TIA* myTIA;