1 //============================================================================
2 //
3 //   SSSS    tt          lll  lll
4 //  SS  SS   tt           ll   ll
5 //  SS     tttttt  eeee   ll   ll   aaaa
6 //   SSSS    tt   ee  ee  ll   ll      aa
7 //      SS   tt   eeeeee  ll   ll   aaaaa  --  "An Atari 2600 VCS Emulator"
8 //  SS  SS   tt   ee      ll   ll  aa  aa
9 //   SSSS     ttt  eeeee llll llll  aaaaa
10 //
11 // Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
12 // and the Stella Team
13 //
14 // See the file "License.txt" for information on usage and redistribution of
15 // this file, and for a DISCLAIMER OF ALL WARRANTIES.
16 //============================================================================
17 
18 #ifndef TIA_BALL
19 #define TIA_BALL
20 
21 class TIA;
22 
23 #include "bspf.hxx"
24 #include "TIAConstants.hxx"
25 #include "Serializable.hxx"
26 
27 class Ball : public Serializable
28 {
29   public:
30 
31     /**
32       The collision mask is injected at construction
33      */
34     explicit Ball(uInt32 collisionMask);
35 
36   public:
37 
38     /**
39       Set the TIA instance
40      */
setTIA(TIA * tia)41     void setTIA(TIA* tia) { myTIA = tia; }
42 
43     /**
44       Reset to initial state.
45      */
46     void reset();
47 
48     /**
49       ENABL write.
50      */
51     void enabl(uInt8 value);
52 
53     /**
54       HMBL write.
55      */
56     void hmbl(uInt8 value);
57 
58     /**
59       RESBL write.
60      */
61     void resbl(uInt8 counter);
62 
63     /**
64       CTRLPF write.
65       */
66     void ctrlpf(uInt8 value);
67 
68     /**
69       VDELBL write.
70      */
71     void vdelbl(uInt8 value);
72 
73     /**
74       Enable / disable ball display (debugging only, not used during normal emulation).
75      */
76     void toggleEnabled(bool enabled);
77 
78     /**
79       Enable / disable ball collisions (debugging only, not used during normal emulation).
80      */
81     void toggleCollisions(bool enabled);
82 
83     /**
84       Set color PF.
85      */
86     void setColor(uInt8 color);
87 
88     /**
89       Set the color used in "debug colors" mode.
90      */
91     void setDebugColor(uInt8 color);
92 
93     /**
94       Enable "debug colors" mode.
95      */
96 
97     void enableDebugColors(bool enabled);
98 
99     /**
100       Update internal state to use the color loss palette.
101      */
102     void applyColorLoss();
103 
104     /**
105       Switch to "inverted phase" mode. This mode emulates the phase shift
106       between movement and ordinary clock pulses that is exhibited by some
107       TIA revisions and that give rise to glitches like the infamous Cool
108       Aid Man bug on some Jr. models.
109      */
110     void setInvertedPhaseClock(bool enable);
111 
112     /**
113       Start movement --- this is triggered by strobing HMOVE.
114      */
115     void startMovement();
116 
117     /**
118       Notify ball of line change.
119      */
120     void nextLine();
121 
122     /**
123       Is the ball visible? This is determined by looking at bit 15
124       of the collision mask.
125      */
isOn() const126     bool isOn() const { return (collision & 0x8000); }
127 
128     /**
129       Get the current color.
130      */
getColor() const131     uInt8 getColor() const { return myColor; }
132 
133     /**
134       Shuffle the enabled flag. This is called in VDELBL mode when GRP1 is
135       written (with a delay of one cycle).
136      */
137     void shuffleStatus();
138 
139     /**
140       Calculate the sprite position from the counter. Used by the debugger only.
141      */
142     uInt8 getPosition() const;
143 
144     /**
145       Set the counter and place the sprite at a specified position. Used by the debugger
146       only.
147      */
148     void setPosition(uInt8 newPosition);
149 
150     /**
151       Get the "old" and "new" values of the enabled flag. Used by the debuggger only.
152      */
getENABLOld() const153     bool getENABLOld() const { return myIsEnabledOld; }
getENABLNew() const154     bool getENABLNew() const { return myIsEnabledNew; }
155 
156     /**
157       Directly set the "old" value of the enabled flag. Used by the debugger only.
158      */
159     void setENABLOld(bool enabled);
160 
161     /**
162       Serializable methods (see that class for more information).
163     */
164     bool save(Serializer& out) const override;
165     bool load(Serializer& in) override;
166 
167     /**
168       Process a single movement tick. Inline for performance (implementation below).
169      */
170     inline void movementTick(uInt32 clock, bool hblank);
171 
172     /**
173       Tick one color clock. Inline for performance (implementation below).
174      */
175     inline void tick(bool isReceivingRegularClock = true);
176 
177   public:
178 
179     /**
180       16 bit Collision mask. Each sprite is represented by a single bit in the mask
181       (1 = active, 0 = inactive). All other bits are always 1. The highest bit is
182       abused to store visibility (as the actual collision bit will always be zero
183       if collisions are disabled).
184      */
185     uInt32 collision{0};
186 
187     /**
188       The movement flag. This corresponds to the state of the movement latch for
189       this sprite --- true while movement is active and ticks are still propagated
190       to the counters, false otherwise.
191      */
192     bool isMoving{false};
193 
194   private:
195 
196     /**
197       Recalculate enabled / disabled state. This is not the same as the enabled / disabled
198       flag, but rather calculated from the flag and the corresponding debug setting.
199      */
200     void updateEnabled();
201 
202     /**
203       Recalculate ball color based on COLUPF, debug colors, color loss, etc.
204      */
205     void applyColors();
206 
207   private:
208 
209     /**
210       Offset of the render counter when rendering starts. Actual display starts at zero,
211       so this amounts to a delay.
212      */
213     enum Count: Int8 {
214       renderCounterOffset = -4
215     };
216 
217   private:
218 
219     /**
220       Collision mask values for active / inactive states. Disabling collisions
221       will change those.
222      */
223     uInt32 myCollisionMaskDisabled{0};
224     uInt32 myCollisionMaskEnabled{0xFFFF};
225 
226     /**
227       Color value calculated by applyColors().
228      */
229     uInt8 myColor{0};
230 
231     /**
232       Color configured by COLUPF
233      */
234     uInt8 myObjectColor{0};
235 
236     /**
237       Color for debug mode.
238      */
239     uInt8 myDebugColor{0};
240 
241     /**
242       Debug mode enabled?
243      */
244     bool myDebugEnabled{false};
245 
246     /**
247       "old" and "new" values of the enabled flag.
248      */
249     bool myIsEnabledOld{false};
250     bool myIsEnabledNew{false};
251 
252     /**
253       Actual value of the enabled flag. Determined from the "old" and "new" values
254       VDEL, debug settings etc.
255      */
256     bool myIsEnabled{false};
257 
258     /**
259       Is the sprite turned off in the debugger?
260      */
261     bool myIsSuppressed{false};
262 
263     /**
264       Is VDEL active?
265      */
266     bool myIsDelaying{false};
267 
268     /**
269      Is the ball sprite signal currently active?
270     */
271     bool mySignalActive{false};
272 
273     /**
274       HMM clocks before movement stops. Changed by writing to HMBL.
275      */
276     uInt8 myHmmClocks{0};
277 
278     /**
279       The sprite counter
280      */
281     uInt8 myCounter{0};
282 
283     /**
284       Ball width, as configured by CTRLPF.
285      */
286     uInt8 myWidth{1};
287 
288     /**
289       Effective width used for drawing. This is usually the same as myWidth,
290       but my differ in starfield mode.
291      */
292     uInt8 myEffectiveWidth{1};
293 
294     /**
295       The value of the counter value at which the last movement tick occurred. This is
296       used for simulating the starfield pattern.
297      */
298     uInt8 myLastMovementTick{0};
299 
300     /**
301       Are we currently rendering? This is latched when the counter hits it decode value,
302       or when RESBL is strobed. It is turned off once the render counter reaches its
303       maximum (i.e. when the sprite has been fully displayed).
304      */
305     bool myIsRendering{false};
306 
307     /**
308       Rendering counter. It starts counting (below zero) when the counter hits the decode value,
309       and the actual signal becomes active once it reaches 0.
310      */
311     Int8 myRenderCounter{0};
312 
313     /**
314       This memorizes a movement tick outside HBLANK in inverted clock mode. It is latched
315       durin ::movementTick() and processed during ::tick() where it inhibits the clock
316       pulse.
317      */
318     bool myInvertedPhaseClock{false};
319 
320     /**
321       Use "inverted movement clock phase" mode? This emulates an idiosyncracy of several
322       newer TIA revisions (see the setter above for a deeper explanation).
323      */
324     bool myUseInvertedPhaseClock{false};
325 
326     /**
327       TIA instance. Required for flushing the line cache and requesting collision updates.
328      */
329     TIA* myTIA{nullptr};
330 
331   private:
332     Ball() = delete;
333     Ball(const Ball&) = delete;
334     Ball(Ball&&) = delete;
335     Ball& operator=(const Ball&) = delete;
336     Ball& operator=(Ball&&) = delete;
337 };
338 
339 // ############################################################################
340 // Implementation
341 // ############################################################################
342 
343 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
movementTick(uInt32 clock,bool hblank)344 void Ball::movementTick(uInt32 clock, bool hblank)
345 {
346   myLastMovementTick = myCounter;
347 
348   // Stop movement once the number of clocks according to HMBL is reached
349   if (clock == myHmmClocks)
350     isMoving = false;
351 
352   if(isMoving)
353   {
354     // Process the tick if we are in hblank. Otherwise, the tick is either masked
355     // by an ordinary tick or merges two consecutive ticks into a single tick (inverted
356     // movement clock phase mode).
357     if (hblank) tick(false);
358 
359     // Track a tick outside hblank for later processing
360     myInvertedPhaseClock = !hblank;
361   }
362 }
363 
364 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
tick(bool isReceivingRegularClock)365 void Ball::tick(bool isReceivingRegularClock)
366 {
367   // If we are in inverted movement clock phase mode and a movement tick occurred, it
368   // will supress the tick.
369   if(myUseInvertedPhaseClock && myInvertedPhaseClock)
370   {
371     myInvertedPhaseClock = false;
372     return;
373   }
374 
375   // Turn on the signal if the render counter reaches the threshold
376   mySignalActive = myIsRendering && myRenderCounter >= 0;
377 
378   // Consider enabled status and the signal to determine visibility (as represented
379   // by the collision mask)
380   collision = (mySignalActive && myIsEnabled) ? myCollisionMaskEnabled : myCollisionMaskDisabled;
381 
382   // Regular clock pulse during movement -> starfield mode
383   bool starfieldEffect = isMoving && isReceivingRegularClock;
384 
385   // Decode value that triggers rendering
386   if (myCounter == 156) {
387     myIsRendering = true;
388     myRenderCounter = renderCounterOffset;
389 
390     // What follows is an effective description of ball width in starfield mode.
391     uInt8 starfieldDelta = (myCounter + TIAConstants::H_PIXEL - myLastMovementTick) % 4;
392     if (starfieldEffect && starfieldDelta == 3 && myWidth < 4) ++myRenderCounter;
393 
394     switch (starfieldDelta) {
395       case 3:
396         myEffectiveWidth = myWidth == 1 ? 2 : myWidth;
397         break;
398 
399       case 2:
400         myEffectiveWidth = 0;
401         break;
402 
403       default:
404         myEffectiveWidth = myWidth;
405         break;
406     }
407 
408   } else if (myIsRendering && ++myRenderCounter >= (starfieldEffect ? myEffectiveWidth : myWidth))
409     myIsRendering = false;
410 
411   if (++myCounter >= TIAConstants::H_PIXEL)
412       myCounter = 0;
413 }
414 
415 #endif // TIA_BALL
416