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#include "Player.hxx"
19#include "DrawCounterDecodes.hxx"
20#include "TIA.hxx"
21
22// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
23Player::Player(uInt32 collisionMask)
24 : myCollisionMaskDisabled(collisionMask),
25 myCollisionMaskEnabled(0xFFFF),
26 myIsSuppressed(false),
27 myDecodesOffset(0),
28 myTIA(nullptr)
29{
30 reset();
31}
32
33// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
34void Player::reset()
35{
36 myDecodes = DrawCounterDecodes::get().playerDecodes()[myDecodesOffset];
37 myHmmClocks = 0;
38 myCounter = 0;
39 isMoving = false;
40 myIsRendering = false;
41 myRenderCounter = 0;
42 myPatternOld = 0;
43 myPatternNew = 0;
44 myIsReflected = 0;
45 myIsDelaying = false;
46 myColor = myObjectColor = myDebugColor = 0;
47 myDebugEnabled = false;
48 collision = myCollisionMaskDisabled;
49 mySampleCounter = 0;
50 myDividerPending = 0;
51 myDividerChangeCounter = -1;
52 myInvertedPhaseClock = false;
53 myUseInvertedPhaseClock = false;
54 myPattern = 0;
55
56 setDivider(1);
57}
58
59// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
60void Player::grp(uInt8 pattern)
61{
62 const uInt8 oldPatternNew = myPatternNew;
63
64 myPatternNew = pattern;
65
66 if (!myIsDelaying && myPatternNew != oldPatternNew) {
67 myTIA->flushLineCache();
68 updatePattern();
69 }
70}
71
72// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
73void Player::hmp(uInt8 value)
74{
75 myHmmClocks = (value >> 4) ^ 0x08;
76}
77
78// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
79void Player::nusiz(uInt8 value, bool hblank)
80{
81 myDecodesOffset = value & 0x07;
82
83 switch (myDecodesOffset) {
84 case 5:
85 myDividerPending = 2;
86 break;
87
88 case 7:
89 myDividerPending = 4;
90 break;
91
92 default:
93 myDividerPending = 1;
94 break;
95 }
96
97 const uInt8* oldDecodes = myDecodes;
98
99 myDecodes = DrawCounterDecodes::get().playerDecodes()[myDecodesOffset];
100
101 if (
102 myDecodes != oldDecodes &&
103 myIsRendering &&
104 (myRenderCounter - Count::renderCounterOffset) < 2 &&
105 !myDecodes[(myCounter - myRenderCounter + Count::renderCounterOffset + TIAConstants::H_PIXEL - 1) % TIAConstants::H_PIXEL]
106 ) {
107 myIsRendering = false;
108 }
109
110 if (myDividerPending == myDivider) return;
111
112 // The following is an effective description of the effects of NUSIZ during
113 // decode and rendering.
114
115 if (myIsRendering) {
116 Int8 delta = myRenderCounter - Count::renderCounterOffset;
117
118 switch ((myDivider << 4) | myDividerPending) {
119 case 0x12:
120 case 0x14:
121 if (hblank) {
122 if (delta < 4)
123 setDivider(myDividerPending);
124 else
125 myDividerChangeCounter = (delta < 5 ? 1 : 0);
126 } else {
127 if (delta < 3)
128 setDivider(myDividerPending);
129 else
130 myDividerChangeCounter = 1;
131 }
132
133 break;
134
135 case 0x21:
136 case 0x41:
137 if (delta < (hblank ? 4 : 3)) {
138 setDivider(myDividerPending);
139 } else if (delta < (hblank ? 6 : 5)) {
140 setDivider(myDividerPending);
141 --myRenderCounter;
142 } else {
143 myDividerChangeCounter = (hblank ? 0 : 1);
144 }
145
146 break;
147
148 case 0x42:
149 case 0x24:
150 if (myRenderCounter < 1 || (hblank && (myRenderCounter % myDivider == 1)))
151 setDivider(myDividerPending);
152 else
153 myDividerChangeCounter = (myDivider - (myRenderCounter - 1) % myDivider);
154 break;
155
156 default:
157 // should never happen
158 setDivider(myDividerPending);
159 break;
160 }
161
162 } else {
163 setDivider(myDividerPending);
164 }
165}
166
167// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
168void Player::resp(uInt8 counter)
169{
170 myCounter = counter;
171
172 // This tries to account for the effects of RESP during draw counter decode as
173 // described in Andrew Towers' notes. Still room for tuning.'
174 if (myIsRendering && (myRenderCounter - Count::renderCounterOffset) < 4)
175 myRenderCounter = Count::renderCounterOffset + (counter - 157);
176}
177
178// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
179void Player::refp(uInt8 value)
180{
181 const bool oldIsReflected = myIsReflected;
182
183 myIsReflected = (value & 0x08) > 0;
184
185 if (oldIsReflected != myIsReflected) {
186 myTIA->flushLineCache();
187 updatePattern();
188 }
189}
190
191// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
192void Player::vdelp(uInt8 value)
193{
194 const bool oldIsDelaying = myIsDelaying;
195
196 myIsDelaying = (value & 0x01) > 0;
197
198 if (oldIsDelaying != myIsDelaying) {
199 myTIA->flushLineCache();
200 updatePattern();
201 }
202}
203
204// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
205void Player::toggleEnabled(bool enabled)
206{
207 const bool oldIsSuppressed = myIsSuppressed;
208
209 myIsSuppressed = !enabled;
210
211 if (oldIsSuppressed != myIsSuppressed) updatePattern();
212}
213
214// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
215void Player::toggleCollisions(bool enabled)
216{
217 myCollisionMaskEnabled = enabled ? 0xFFFF : (0x8000 | myCollisionMaskDisabled);
218}
219
220// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
221void Player::setColor(uInt8 color)
222{
223 if (color != myObjectColor && myPattern) myTIA->flushLineCache();
224
225 myObjectColor = color;
226 applyColors();
227}
228
229// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
230void Player::setDebugColor(uInt8 color)
231{
232 myTIA->flushLineCache();
233 myDebugColor = color;
234 applyColors();
235}
236
237// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
238void Player::enableDebugColors(bool enabled)
239{
240 myTIA->flushLineCache();
241 myDebugEnabled = enabled;
242 applyColors();
243}
244
245// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
246void Player::applyColorLoss()
247{
248 myTIA->flushLineCache();
249 applyColors();
250}
251
252// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
253void Player::setInvertedPhaseClock(bool enable)
254{
255 myUseInvertedPhaseClock = enable;
256}
257
258// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
259void Player::startMovement()
260{
261 isMoving = true;
262}
263
264// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
265void Player::nextLine()
266{
267 if (!myIsRendering || myRenderCounter < myRenderCounterTripPoint)
268 collision = myCollisionMaskDisabled;
269 else
270 collision = (myPattern & (1 << mySampleCounter)) ? myCollisionMaskEnabled : myCollisionMaskDisabled;
271}
272
273// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
274void Player::shufflePatterns()
275{
276 const uInt8 oldPatternOld = myPatternOld;
277
278 myPatternOld = myPatternNew;
279
280 if (myIsDelaying && myPatternOld != oldPatternOld) {
281 myTIA->flushLineCache();
282 updatePattern();
283 }
284}
285
286// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
287uInt8 Player::getRespClock() const
288{
289 switch (myDivider)
290 {
291 case 1:
292 return (myCounter + TIAConstants::H_PIXEL - 5) % TIAConstants::H_PIXEL;
293
294 case 2:
295 return (myCounter + TIAConstants::H_PIXEL - 9) % TIAConstants::H_PIXEL;
296
297 case 4:
298 return (myCounter + TIAConstants::H_PIXEL - 12) % TIAConstants::H_PIXEL;
299
300 default:
301 throw runtime_error("invalid width");
302 }
303}
304
305// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
306void Player::setGRPOld(uInt8 pattern)
307{
308 myTIA->flushLineCache();
309
310 myPatternOld = pattern;
311 updatePattern();
312}
313
314// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
315void Player::updatePattern()
316{
317 if (myIsSuppressed) {
318 myPattern = 0;
319 return;
320 }
321
322 myPattern = myIsDelaying ? myPatternOld : myPatternNew;
323
324 if (!myIsReflected) {
325 myPattern = (
326 ((myPattern & 0x01) << 7) |
327 ((myPattern & 0x02) << 5) |
328 ((myPattern & 0x04) << 3) |
329 ((myPattern & 0x08) << 1) |
330 ((myPattern & 0x10) >> 1) |
331 ((myPattern & 0x20) >> 3) |
332 ((myPattern & 0x40) >> 5) |
333 ((myPattern & 0x80) >> 7)
334 );
335 }
336
337 if (myIsRendering && myRenderCounter >= myRenderCounterTripPoint) {
338 collision = (myPattern & (1 << mySampleCounter)) ? myCollisionMaskEnabled : myCollisionMaskDisabled;
339 myTIA->scheduleCollisionUpdate();
340 }
341}
342
343// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
344void Player::setDivider(uInt8 divider)
345{
346 myDivider = divider;
347 myRenderCounterTripPoint = divider == 1 ? 0 : 1;
348}
349
350// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
351void Player::applyColors()
352{
353 if (!myDebugEnabled)
354 {
355 if (myTIA->colorLossActive()) myObjectColor |= 0x01;
356 else myObjectColor &= 0xfe;
357 myColor = myObjectColor;
358 }
359 else
360 myColor = myDebugColor;
361}
362
363// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
364uInt8 Player::getPosition() const
365{
366 // Wide players are shifted by one pixel to the right
367 const uInt8 shift = myDivider == 1 ? 0 : 1;
368
369 // position =
370 // current playfield x +
371 // (current counter - 156 (the decode clock of copy 0)) +
372 // clock count after decode until first pixel +
373 // shift (accounts for wide player shift) +
374 // 1 (it'll take another cycle after the decode for the render counter to start ticking)
375 //
376 // The result may be negative, so we add TIA::H_PIXEL and do the modulus -> 317 = 156 + TIA::H_PIXEL + 1
377 //
378 // Mind the sign of renderCounterOffset: it's defined negative above
379 return (317 - myCounter - Count::renderCounterOffset + shift + myTIA->getPosition()) % TIAConstants::H_PIXEL;
380}
381
382// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
383void Player::setPosition(uInt8 newPosition)
384{
385 myTIA->flushLineCache();
386
387 const uInt8 shift = myDivider == 1 ? 0 : 1;
388
389 // See getPosition for an explanation
390 myCounter = (317 - newPosition - Count::renderCounterOffset + shift + myTIA->getPosition()) % TIAConstants::H_PIXEL;
391}
392
393// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
394bool Player::save(Serializer& out) const
395{
396 try
397 {
398 out.putInt(collision);
399 out.putInt(myCollisionMaskDisabled);
400 out.putInt(myCollisionMaskEnabled);
401
402 out.putByte(myColor);
403 out.putByte(myObjectColor); out.putByte(myDebugColor);
404 out.putBool(myDebugEnabled);
405
406 out.putBool(myIsSuppressed);
407
408 out.putByte(myHmmClocks);
409 out.putByte(myCounter);
410 out.putBool(isMoving);
411
412 out.putBool(myIsRendering);
413 out.putByte(myRenderCounter);
414 out.putByte(myRenderCounterTripPoint);
415 out.putByte(myDivider);
416 out.putByte(myDividerPending);
417 out.putByte(mySampleCounter);
418 out.putByte(myDividerChangeCounter);
419
420 out.putByte(myDecodesOffset);
421
422 out.putByte(myPatternOld);
423 out.putByte(myPatternNew);
424 out.putByte(myPattern);
425
426 out.putBool(myIsReflected);
427 out.putBool(myIsDelaying);
428 out.putBool(myInvertedPhaseClock);
429 }
430 catch(...)
431 {
432 cerr << "ERROR: TIA_Player::save" << endl;
433 return false;
434 }
435
436 return true;
437}
438
439// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
440bool Player::load(Serializer& in)
441{
442 try
443 {
444 collision = in.getInt();
445 myCollisionMaskDisabled = in.getInt();
446 myCollisionMaskEnabled = in.getInt();
447
448 myColor = in.getByte();
449 myObjectColor = in.getByte(); myDebugColor = in.getByte();
450 myDebugEnabled = in.getBool();
451
452 myIsSuppressed = in.getBool();
453
454 myHmmClocks = in.getByte();
455 myCounter = in.getByte();
456 isMoving = in.getBool();
457
458 myIsRendering = in.getBool();
459 myRenderCounter = in.getByte();
460 myRenderCounterTripPoint = in.getByte();
461 myDivider = in.getByte();
462 myDividerPending = in.getByte();
463 mySampleCounter = in.getByte();
464 myDividerChangeCounter = in.getByte();
465
466 myDecodesOffset = in.getByte();
467 myDecodes = DrawCounterDecodes::get().playerDecodes()[myDecodesOffset];
468
469 myPatternOld = in.getByte();
470 myPatternNew = in.getByte();
471 myPattern = in.getByte();
472
473 myIsReflected = in.getBool();
474 myIsDelaying = in.getBool();
475 myInvertedPhaseClock = in.getBool();
476
477 applyColors();
478 }
479 catch(...)
480 {
481 cerr << "ERROR: TIA_Player::load" << endl;
482 return false;
483 }
484
485 return true;
486}
487