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-2019 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#include "Serializable.hxx"
22#include "bspf.hxx"
23#include "TIAConstants.hxx"
24
25class TIA;
26
27class 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 */
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 */
126 bool isOn() const { return (collision & 0x8000); }
127
128 /**
129 Get the current color.
130 */
131 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 */
153 bool getENABLOld() const { return myIsEnabledOld; }
154 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;
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;
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;
224 uInt32 myCollisionMaskEnabled;
225
226 /**
227 Color value calculated by applyColors().
228 */
229 uInt8 myColor;
230
231 /**
232 Color configured by COLUPF
233 */
234 uInt8 myObjectColor;
235
236 /**
237 Color for debug mode.
238 */
239 uInt8 myDebugColor;
240
241 /**
242 Debug mode enabled?
243 */
244 bool myDebugEnabled;
245
246 /**
247 "old" and "new" values of the enabled flag.
248 */
249 bool myIsEnabledOld;
250 bool myIsEnabledNew;
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;
257
258 /**
259 Is the sprite turned off in the debugger?
260 */
261 bool myIsSuppressed;
262
263 /**
264 Is VDEL active?
265 */
266 bool myIsDelaying;
267
268 /**
269 Is the ball sprite signal currently active?
270 */
271 bool mySignalActive;
272
273 /**
274 HMM clocks before movement stops. Changed by writing to HMBL.
275 */
276 uInt8 myHmmClocks;
277
278 /**
279 The sprite counter
280 */
281 uInt8 myCounter;
282
283 /**
284 Ball width, as configured by CTRLPF.
285 */
286 uInt8 myWidth;
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;
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;
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;
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;
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;
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;
325
326 /**
327 TIA instance. Required for flushing the line cache and requesting collision updates.
328 */
329 TIA* myTIA;
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&&);
337};
338
339// ############################################################################
340// Implementation
341// ############################################################################
342
343// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
344void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
365void 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