openMSX
SpriteChecker.cc
Go to the documentation of this file.
1 /*
2 TODO:
3 - Verify model for 5th sprite number calculation.
4  For example, does it have the right value in text mode?
5 - Further investigate sprite collision registers:
6  - If there is NO collision, the value of these registers constantly changes.
7  Could this be some kind of indication for the scanline XY coords???
8  - Bit 9 of the Y coord (odd/even page??) is not yet implemented.
9 */
10 
11 #include "SpriteChecker.hh"
12 #include "RenderSettings.hh"
13 #include "BooleanSetting.hh"
14 #include "serialize.hh"
15 #include <algorithm>
16 #include <cassert>
17 
18 namespace openmsx {
19 
21  EmuTime::param time)
22  : vdp(vdp_), vram(vdp.getVRAM())
23  , limitSpritesSetting(renderSettings.getLimitSpritesSetting())
24  , frameStartTime(time)
25 {
26  vram.spriteAttribTable.setObserver(this);
28 }
29 
30 void SpriteChecker::reset(EmuTime::param time)
31 {
32  vdp.setSpriteStatus(0); // TODO 0x00 or 0x1F (blueMSX has 0x1F)
33  collisionX = 0;
34  collisionY = 0;
35 
36  frameStart(time);
37 
38  updateSpritesMethod = &SpriteChecker::updateSprites1;
39 }
40 
41 static constexpr SpriteChecker::SpritePattern doublePattern(SpriteChecker::SpritePattern a)
42 {
43  // bit-pattern "abcd...." gets expanded to "aabbccdd"
44  // upper 16 bits (of a 32 bit number) contain the pattern
45  // lower 16 bits must be zero
46  // // abcdefghijklmnop0000000000000000
47  a = (a | (a >> 8)) & 0xFF00FF00; // abcdefgh00000000ijklmnop00000000
48  a = (a | (a >> 4)) & 0xF0F0F0F0; // abcd0000efgh0000ijkl0000mnop0000
49  a = (a | (a >> 2)) & 0xCCCCCCCC; // ab00cd00ef00gh00ij00kl00mn00op00
50  a = (a | (a >> 1)) & 0xAAAAAAAA; // a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0
51  return a | (a >> 1); // aabbccddeeffgghhiijjkkllmmnnoopp
52 }
53 
54 inline SpriteChecker::SpritePattern SpriteChecker::calculatePatternNP(
55  unsigned patternNr, unsigned y)
56 {
57  const byte* patternPtr = vram.spritePatternTable.getReadArea(0, 256 * 8);
58  unsigned index = patternNr * 8 + y;
59  SpritePattern pattern = patternPtr[index] << 24;
60  if (vdp.getSpriteSize() == 16) {
61  pattern |= patternPtr[index + 16] << 16;
62  }
63  return !vdp.isSpriteMag() ? pattern : doublePattern(pattern);
64 }
65 inline SpriteChecker::SpritePattern SpriteChecker::calculatePatternPlanar(
66  unsigned patternNr, unsigned y)
67 {
68  auto [ptr0, ptr1] = vram.spritePatternTable.getReadAreaPlanar(0, 256 * 8);
69  unsigned index = patternNr * 8 + y;
70  const byte* patternPtr = (index & 1) ? ptr1 : ptr0;
71  index /= 2;
72  SpritePattern pattern = patternPtr[index] << 24;
73  if (vdp.getSpriteSize() == 16) {
74  pattern |= patternPtr[index + (16 / 2)] << 16;
75  }
76  return !vdp.isSpriteMag() ? pattern : doublePattern(pattern);
77 }
78 
79 void SpriteChecker::updateSprites1(int limit)
80 {
81  if (vdp.spritesEnabledFast()) {
82  if (vdp.isDisplayEnabled()) {
83  // in display area
84  checkSprites1(currentLine, limit);
85  } else {
86  // in border, only check last line of top border
87  int l0 = vdp.getLineZero() - 1;
88  if ((currentLine <= l0) && (l0 < limit)) {
89  checkSprites1(l0, l0 + 1);
90  }
91  }
92  }
93  currentLine = limit;
94 }
95 
96 inline void SpriteChecker::checkSprites1(int minLine, int maxLine)
97 {
98  // This implementation contains a double for-loop. The outer loop goes
99  // over the sprites, the inner loop over the to-be-checked lines. This
100  // is not the order in which the real VDP performs this operation: the
101  // real VDP renders line-per-line and for each line checks all 32
102  // sprites.
103  //
104  // Though this 'reverse' order allows to skip over very large regions
105  // of the inner loop: we only have to process the lines were a
106  // particular sprite is actually visible. I measured this makes this
107  // routine 4x-5x faster!
108  //
109  // This routine also needs to detect the sprite number of the 'first'
110  // 5th-sprite-condition. With 'first' meaning the first line where this
111  // condition occurs. Because our loops are swapped compared to the real
112  // VDP, we need some extra fixup logic to correctly detect this.
113 
114  // Calculate display line.
115  // This is the line sprites are checked at; the line they are displayed
116  // at is one lower.
117  int displayDelta = vdp.getVerticalScroll() - vdp.getLineZero();
118 
119  // Get sprites for this line and detect 5th sprite if any.
120  bool limitSprites = limitSpritesSetting.getBoolean();
121  int size = vdp.getSpriteSize();
122  bool mag = vdp.isSpriteMag();
123  int magSize = (mag + 1) * size;
124  const byte* attributePtr = vram.spriteAttribTable.getReadArea(0, 32 * 4);
125  byte patternIndexMask = size == 16 ? 0xFC : 0xFF;
126  int fifthSpriteNum = -1; // no 5th sprite detected yet
127  int fifthSpriteLine = 999; // larger than any possible valid line
128 
129  int sprite = 0;
130  for (; sprite < 32; ++sprite) {
131  int y = attributePtr[4 * sprite + 0];
132  if (y == 208) break;
133 
134  for (int line = minLine; line < maxLine; ++line) { // 'line' changes in loop
135  // Calculate line number within the sprite.
136  int displayLine = line + displayDelta;
137  int spriteLine = (displayLine - y) & 0xFF;
138  if (spriteLine >= magSize) {
139  // Skip ahead till sprite becomes visible.
140  line += 256 - spriteLine - 1; // -1 because of for-loop
141  continue;
142  }
143 
144  int visibleIndex = spriteCount[line];
145  if (visibleIndex == 4) {
146  // Find earliest line where this condition occurs.
147  if (line < fifthSpriteLine) {
148  fifthSpriteLine = line;
149  fifthSpriteNum = sprite;
150  }
151  if (limitSprites) continue;
152  }
153 
154  SpriteInfo& sip = spriteBuffer[line][visibleIndex];
155  int patternIndex = attributePtr[4 * sprite + 2] & patternIndexMask;
156  if (mag) spriteLine /= 2;
157  sip.pattern = calculatePatternNP(patternIndex, spriteLine);
158  sip.x = attributePtr[4 * sprite + 1];
159  byte colorAttrib = attributePtr[4 * sprite + 3];
160  if (colorAttrib & 0x80) sip.x -= 32;
161  sip.colorAttrib = colorAttrib;
162 
163  spriteCount[line] = visibleIndex + 1;
164  }
165  }
166 
167  // Update status register.
168  byte status = vdp.getStatusReg0();
169  if (fifthSpriteNum != -1) {
170  // Five sprites on a line.
171  // According to TMS9918.pdf 5th sprite detection is only
172  // active when F flag is zero.
173  if ((status & 0xC0) == 0) {
174  status = 0x40 | (status & 0x20) | fifthSpriteNum;
175  }
176  }
177  if (~status & 0x40) {
178  // No 5th sprite detected, store number of latest sprite processed.
179  status = (status & 0x20) | std::min(sprite, 31);
180  }
181  vdp.setSpriteStatus(status);
182 
183  // Optimisation:
184  // If collision already occurred,
185  // that state is stable until it is reset by a status reg read,
186  // so no need to execute the checks.
187  // The spriteBuffer array is filled now, so we can bail out.
188  if (vdp.getStatusReg0() & 0x20) return;
189 
190  /*
191  Model for sprite collision: (or "coincidence" in TMS9918 data sheet)
192  - Reset when status reg is read.
193  - Set when sprite patterns overlap.
194  - ??? Color doesn't matter: sprites of color 0 can collide.
195  ??? This conflicts with: https://github.com/openMSX/openMSX/issues/1198
196  - Sprites that are partially off-screen position can collide, but only
197  on the in-screen pixels. In other words: sprites cannot collide in
198  the left or right border, only in the visible screen area. Though
199  they can collide in the V9958 extra border mask. This behaviour is
200  the same in sprite mode 1 and 2.
201 
202  Implemented by checking every pair for collisions.
203  For large numbers of sprites that would be slow,
204  but there are max 4 sprites and therefore max 6 pairs.
205  If any collision is found, method returns at once.
206  */
207  bool can0collide = vdp.canSpriteColor0Collide();
208  for (auto line : xrange(minLine, maxLine)) {
209  int minXCollision = 999;
210  for (int i = std::min<int>(4, spriteCount[line]); --i >= 1; ) {
211  auto color1 = spriteBuffer[line][i].colorAttrib & 0xf;
212  if (!can0collide && (color1 == 0)) continue;
213  int x_i = spriteBuffer[line][i].x;
214  SpritePattern pattern_i = spriteBuffer[line][i].pattern;
215  for (int j = i; --j >= 0; ) {
216  auto color2 = spriteBuffer[line][j].colorAttrib & 0xf;
217  if (!can0collide && (color2 == 0)) continue;
218  // Do sprite i and sprite j collide?
219  int x_j = spriteBuffer[line][j].x;
220  int dist = x_j - x_i;
221  if ((-magSize < dist) && (dist < magSize)) {
222  SpritePattern pattern_j = spriteBuffer[line][j].pattern;
223  if (dist < 0) {
224  pattern_j <<= -dist;
225  } else {
226  pattern_j >>= dist;
227  }
228  SpritePattern colPat = pattern_i & pattern_j;
229  if (x_i < 0) {
230  assert(x_i >= -32);
231  colPat &= (1 << (32 + x_i)) - 1;
232  }
233  if (colPat) {
234  int xCollision = x_i + Math::countLeadingZeros(colPat);
235  assert(xCollision >= 0);
236  minXCollision = std::min(minXCollision, xCollision);
237  }
238  }
239  }
240  }
241  if (minXCollision < 256) {
242  vdp.setSpriteStatus(vdp.getStatusReg0() | 0x20);
243  // verified: collision coords are also filled
244  // in for sprite mode 1
245  // x-coord should be increased by 12
246  // y-coord 8
247  collisionX = minXCollision + 12;
248  collisionY = line - vdp.getLineZero() + 8;
249  return; // don't check lines with higher Y-coord
250  }
251  }
252 }
253 
254 void SpriteChecker::updateSprites2(int limit)
255 {
256  // TODO merge this with updateSprites1()?
257  if (vdp.spritesEnabledFast()) {
258  if (vdp.isDisplayEnabled()) {
259  // in display area
260  checkSprites2(currentLine, limit);
261  } else {
262  // in border, only check last line of top border
263  int l0 = vdp.getLineZero() - 1;
264  if ((currentLine <= l0) && (l0 < limit)) {
265  checkSprites2(l0, l0 + 1);
266  }
267  }
268  }
269  currentLine = limit;
270 }
271 
272 inline void SpriteChecker::checkSprites2(int minLine, int maxLine)
273 {
274  // See comment in checkSprites1() about order of inner and outer loops.
275 
276  // Calculate display line.
277  // This is the line sprites are checked at; the line they are displayed
278  // at is one lower.
279  int displayDelta = vdp.getVerticalScroll() - vdp.getLineZero();
280 
281  // Get sprites for this line and detect 5th sprite if any.
282  bool limitSprites = limitSpritesSetting.getBoolean();
283  int size = vdp.getSpriteSize();
284  bool mag = vdp.isSpriteMag();
285  int magSize = (mag + 1) * size;
286  int patternIndexMask = (size == 16) ? 0xFC : 0xFF;
287  int ninthSpriteNum = -1; // no 9th sprite detected yet
288  int ninthSpriteLine = 999; // larger than any possible valid line
289 
290  // Because it gave a measurable performance boost, we duplicated the
291  // code for planar and non-planar modes.
292  int sprite = 0;
293  if (planar) {
294  auto [attributePtr0, attributePtr1] =
295  vram.spriteAttribTable.getReadAreaPlanar(512, 32 * 4);
296  // TODO: Verify CC implementation.
297  for (; sprite < 32; ++sprite) {
298  int y = attributePtr0[2 * sprite + 0];
299  if (y == 216) break;
300 
301  for (int line = minLine; line < maxLine; ++line) { // 'line' changes in loop
302  // Calculate line number within the sprite.
303  int displayLine = line + displayDelta;
304  int spriteLine = (displayLine - y) & 0xFF;
305  if (spriteLine >= magSize) {
306  // Skip ahead till sprite is visible.
307  line += 256 - spriteLine - 1;
308  continue;
309  }
310 
311  int visibleIndex = spriteCount[line];
312  if (visibleIndex == 8) {
313  // Find earliest line where this condition occurs.
314  if (line < ninthSpriteLine) {
315  ninthSpriteLine = line;
316  ninthSpriteNum = sprite;
317  }
318  if (limitSprites) continue;
319  }
320 
321  if (mag) spriteLine /= 2;
322  int colorIndex = (~0u << 10) | (sprite * 16 + spriteLine);
323  byte colorAttrib =
324  vram.spriteAttribTable.readPlanar(colorIndex);
325 
326  SpriteInfo& sip = spriteBuffer[line][visibleIndex];
327  int patternIndex = attributePtr0[2 * sprite + 1] & patternIndexMask;
328  sip.pattern = calculatePatternPlanar(patternIndex, spriteLine);
329  sip.x = attributePtr1[2 * sprite + 0];
330  if (colorAttrib & 0x80) sip.x -= 32;
331  sip.colorAttrib = colorAttrib;
332 
333  // set sentinel (see below)
334  spriteBuffer[line][visibleIndex + 1].colorAttrib = 0;
335  spriteCount[line] = visibleIndex + 1;
336  }
337  }
338  } else {
339  const byte* attributePtr0 =
340  vram.spriteAttribTable.getReadArea(512, 32 * 4);
341  // TODO: Verify CC implementation.
342  for (; sprite < 32; ++sprite) {
343  int y = attributePtr0[4 * sprite + 0];
344  if (y == 216) break;
345 
346  for (int line = minLine; line < maxLine; ++line) { // 'line' changes in loop
347  // Calculate line number within the sprite.
348  int displayLine = line + displayDelta;
349  int spriteLine = (displayLine - y) & 0xFF;
350  if (spriteLine >= magSize) {
351  // Skip ahead till sprite is visible.
352  line += 256 - spriteLine - 1;
353  continue;
354  }
355 
356  int visibleIndex = spriteCount[line];
357  if (visibleIndex == 8) {
358  // Find earliest line where this condition occurs.
359  if (line < ninthSpriteLine) {
360  ninthSpriteLine = line;
361  ninthSpriteNum = sprite;
362  }
363  if (limitSprites) continue;
364  }
365 
366  if (mag) spriteLine /= 2;
367  int colorIndex = (~0u << 10) | (sprite * 16 + spriteLine);
368  byte colorAttrib =
369  vram.spriteAttribTable.readNP(colorIndex);
370  // Sprites with CC=1 are only visible if preceded by
371  // a sprite with CC=0. However they DO contribute towards
372  // the max-8-sprites-per-line limit, so we can't easily
373  // filter them here. See also
374  // https://github.com/openMSX/openMSX/issues/497
375 
376  SpriteInfo& sip = spriteBuffer[line][visibleIndex];
377  int patternIndex = attributePtr0[4 * sprite + 2] & patternIndexMask;
378  sip.pattern = calculatePatternNP(patternIndex, spriteLine);
379  sip.x = attributePtr0[4 * sprite + 1];
380  if (colorAttrib & 0x80) sip.x -= 32;
381  sip.colorAttrib = colorAttrib;
382 
383  // Set sentinel. Sentinel is actually only
384  // needed for sprites with CC=1.
385  // In the past we set the sentinel (for all
386  // lines) at the end. But it's slightly faster
387  // to do it only for lines that actually
388  // contain sprites (even if sentinel gets
389  // overwritten a couple of times for lines with
390  // many sprites).
391  spriteBuffer[line][visibleIndex + 1].colorAttrib = 0;
392  spriteCount[line] = visibleIndex + 1;
393  }
394  }
395  }
396 
397  // Update status register.
398  byte status = vdp.getStatusReg0();
399  if (ninthSpriteNum != -1) {
400  // Nine sprites on a line.
401  // According to TMS9918.pdf 5th sprite detection is only
402  // active when F flag is zero. Stuck to this for V9938.
403  // Dragon Quest 2 needs this.
404  if ((status & 0xC0) == 0) {
405  status = 0x40 | (status & 0x20) | ninthSpriteNum;
406  }
407  }
408  if (~status & 0x40) {
409  // No 9th sprite detected, store number of latest sprite processed.
410  status = (status & 0x20) | std::min(sprite, 31);
411  }
412  vdp.setSpriteStatus(status);
413 
414  // Optimisation:
415  // If collision already occurred,
416  // that state is stable until it is reset by a status reg read,
417  // so no need to execute the checks.
418  // The visibleSprites array is filled now, so we can bail out.
419  if (vdp.getStatusReg0() & 0x20) return;
420 
421  /*
422  Model for sprite collision: (or "coincidence" in TMS9918 data sheet)
423  - Reset when status reg is read.
424  - Set when sprite patterns overlap.
425  - ??? Color doesn't matter: sprites of color 0 can collide.
426  ??? TODO: V9938 data book denies this (page 98).
427  ??? This conflicts with: https://github.com/openMSX/openMSX/issues/1198
428  - Sprites that are partially off-screen position can collide, but only
429  on the in-screen pixels. In other words: sprites cannot collide in
430  the left or right border, only in the visible screen area. Though
431  they can collide in the V9958 extra border mask. This behaviour is
432  the same in sprite mode 1 and 2.
433 
434  Implemented by checking every pair for collisions.
435  For large numbers of sprites that would be slow.
436  There are max 8 sprites and therefore max 42 pairs.
437  TODO: Maybe this is slow... Think of something faster.
438  Probably new approach is needed anyway for OR-ing.
439  */
440  bool can0collide = vdp.canSpriteColor0Collide();
441  for (auto line : xrange(minLine, maxLine)) {
442  int minXCollision = 999; // no collision
443  SpriteInfo* visibleSprites = spriteBuffer[line];
444  for (int i = std::min<int>(8, spriteCount[line]); --i >= 1; ) {
445  auto colorAttrib1 = visibleSprites[i].colorAttrib;
446  if (!can0collide && ((colorAttrib1 & 0xf) == 0)) continue;
447  // If CC or IC is set, this sprite cannot collide.
448  if (colorAttrib1 & 0x60) continue;
449 
450  int x_i = visibleSprites[i].x;
451  SpritePattern pattern_i = visibleSprites[i].pattern;
452  for (int j = i; --j >= 0; ) {
453  auto colorAttrib2 = visibleSprites[j].colorAttrib;
454  if (!can0collide && ((colorAttrib2 & 0xf) == 0)) continue;
455  // If CC or IC is set, this sprite cannot collide.
456  if (colorAttrib2 & 0x60) continue;
457 
458  // Do sprite i and sprite j collide?
459  int x_j = visibleSprites[j].x;
460  int dist = x_j - x_i;
461  if ((-magSize < dist) && (dist < magSize)) {
462  SpritePattern pattern_j = visibleSprites[j].pattern;
463  if (dist < 0) {
464  pattern_j <<= -dist;
465  } else {
466  pattern_j >>= dist;
467  }
468  SpritePattern colPat = pattern_i & pattern_j;
469  if (x_i < 0) {
470  assert(x_i >= -32);
471  colPat &= (1 << (32 + x_i)) - 1;
472  }
473  if (colPat) {
474  int xCollision = x_i + Math::countLeadingZeros(colPat);
475  assert(xCollision >= 0);
476  minXCollision = std::min(minXCollision, xCollision);
477  }
478  }
479  }
480  }
481  if (minXCollision < 256) {
482  vdp.setSpriteStatus(vdp.getStatusReg0() | 0x20);
483  // x-coord should be increased by 12
484  // y-coord 8
485  collisionX = minXCollision + 12;
486  collisionY = line - vdp.getLineZero() + 8;
487  return; // don't check lines with higher Y-coord
488  }
489  }
490 }
491 
492 // version 1: initial version
493 // version 2: bug fix: also serialize 'currentLine'
494 template<typename Archive>
495 void SpriteChecker::serialize(Archive& ar, unsigned version)
496 {
497  if (ar.isLoader()) {
498  // Recalculate from VDP state:
499  // - frameStartTime
500  frameStartTime.reset(vdp.getFrameStartTime());
501  // - updateSpritesMethod, planar
502  setDisplayMode(vdp.getDisplayMode());
503 
504  // We don't serialize spriteCount[] and spriteBuffer[].
505  // These are only used to draw the MSX screen, they don't have
506  // any influence on the MSX state. So the effect of not
507  // serializing these two is that no sprites will be shown in the
508  // first (partial) frame after loadstate.
509  ranges::fill(spriteCount, 0);
510  // content of spriteBuffer[] doesn't matter if spriteCount[] is 0
511  }
512  ar.serialize("collisionX", collisionX,
513  "collisionY", collisionY);
514  if (ar.versionAtLeast(version, 2)) {
515  ar.serialize("currentLine", currentLine);
516  } else {
517  currentLine = 0;
518  }
519 }
521 
522 } // namespace openmsx
bool getBoolean() const noexcept
constexpr void reset(EmuTime::param e)
Reset the clock to start ticking at the given time.
Definition: Clock.hh:102
Class containing all settings for renderers.
void frameStart(EmuTime::param time)
Signals the start of a new frame.
void reset(EmuTime::param time)
Puts the sprite checker in its initial state.
void serialize(Archive &ar, unsigned version)
uint32_t SpritePattern
Bitmap of length 32 describing a sprite pattern.
SpriteChecker(VDP &vdp, RenderSettings &renderSettings, EmuTime::param time)
Create a sprite checker.
VRAMWindow spriteAttribTable
Definition: VDPVRAM.hh:674
VRAMWindow spritePatternTable
Definition: VDPVRAM.hh:675
Unified implementation of MSX Video Display Processors (VDPs).
Definition: VDP.hh:63
int getSpriteSize() const
Gets the sprite size in pixels (8/16).
Definition: VDP.hh:472
byte getStatusReg0() const
Should only be used by SpriteChecker.
Definition: VDP.hh:564
bool spritesEnabledFast() const
Same as spritesEnabled(), but may only be called in sprite mode 1 or 2.
Definition: VDP.hh:265
void setSpriteStatus(byte value)
Should only be used by SpriteChecker.
Definition: VDP.hh:574
DisplayMode getDisplayMode() const
Get the display mode the VDP is in.
Definition: VDP.hh:141
int getLineZero() const
Get the absolute line number of display line zero.
Definition: VDP.hh:325
byte getVerticalScroll() const
Gets the current vertical scroll (line displayed at Y=0).
Definition: VDP.hh:280
EmuTime::param getFrameStartTime() const
Definition: VDP.hh:466
bool canSpriteColor0Collide() const
Can a sprite which has color=0 collide with some other sprite?
Definition: VDP.hh:180
bool isSpriteMag() const
Are sprites magnified?
Definition: VDP.hh:478
bool isDisplayEnabled() const
Is the display enabled? Both the regular border and forced blanking by clearing the display enable bi...
Definition: VDP.hh:248
byte readNP(unsigned index) const
Reads a byte from VRAM in its current state.
Definition: VDPVRAM.hh:254
byte readPlanar(unsigned index) const
Similar to readNP, but now with planar addressing.
Definition: VDPVRAM.hh:262
const byte * getReadArea(unsigned index, unsigned size) const
Gets a pointer to a contiguous part of the VRAM.
Definition: VDPVRAM.hh:219
void setObserver(VRAMObserver *newObserver)
Register an observer on this VRAM window.
Definition: VDPVRAM.hh:280
std::pair< const byte *, const byte * > getReadAreaPlanar(unsigned index, unsigned size) const
Similar to getReadArea(), but now with planar addressing mode.
Definition: VDPVRAM.hh:234
constexpr unsigned countLeadingZeros(unsigned x)
Count the number of leading zero-bits in the given word.
Definition: Math.hh:192
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:269
This file implemented 3 utility functions:
Definition: Autofire.cc:5
void fill(ForwardRange &&range, const T &value)
Definition: ranges.hh:197
size_t size(std::string_view utf8)
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:983
SpritePattern pattern
Pattern of this sprite line, corrected for magnification.
byte colorAttrib
Bit 3..0 are index in palette.
int16_t x
X-coordinate of sprite, corrected for early clock.
constexpr auto xrange(T e)
Definition: xrange.hh:155