openMSX
VDPVRAM.hh
Go to the documentation of this file.
1 #ifndef VDPVRAM_HH
2 #define VDPVRAM_HH
3 
4 #include "VRAMObserver.hh"
5 #include "VDP.hh"
6 #include "VDPCmdEngine.hh"
7 #include "SimpleDebuggable.hh"
8 #include "Ram.hh"
9 #include "Math.hh"
10 #include "openmsx.hh"
11 #include "likely.hh"
12 #include <cassert>
13 
14 namespace openmsx {
15 
16 class DisplayMode;
17 class SpriteChecker;
18 class Renderer;
19 
20 /*
21 Note: The way VRAM is accessed depends a lot on who is doing the accessing.
22 
23 For example, the ranges:
24 - Table access is done using masks.
25 - Command engine work areas are rectangles.
26 - CPU access always spans full memory.
27 
28 Maybe define an interface with multiple subclasses?
29 Or is that too much of a performance hit?
30 If accessed through the interface, a virtual method call is made.
31 But invoking the objects directly should be inlined.
32 
33 Timing:
34 
35 Each window reflects the state of the VRAM at a specified moment in time.
36 
37 Because the CPU has full-range write access, it is incorrect for any window
38 to be ahead in time compared to the CPU. Because multi-cycle operations are
39 implemented as atomic, it is currently possible that a window which starts
40 an operation slightly before CPU time ends up slightly after CPU time.
41 Solutions:
42 - break up operations in 1-cycle suboperations
43  (very hard to reverse engineer accurately)
44 - do not start an operation until its end time is after CPU time
45  (requires minor rewrite of command engine)
46 - make the code that uses the timestamps resilient to after-CPU times
47  (current implementation; investigate if this is correct)
48 
49 Window ranges are not at fixed. But they can only be changed by the CPU, so
50 they are fixed until CPU time, which subsystems will never go beyond anyway.
51 
52 The only two subsystems with write access are CPU and command engine.
53 The command engine can only start executing a new command if instructed so
54 by the CPU. Therefore it is known which area the command engine can write
55 in until CPU time:
56 - empty, if the command engine is not executing a command
57 - the command's reach, if the command engine is executing a command
58 Currently the command's reach is not computed: full VRAM is used.
59 Taking the Y coordinate into account would speed things up a lot, because
60 usually commands execute on invisible pages, so the number of area overlaps
61 between renderer and command engine would be reduced significantly.
62 Also sprite tables are usually not written by commands.
63 
64 Reading through a window is done as follows:
65 A subsystem reads the VRAM because it is updating itself to a certain moment
66 in time T.
67 1. the subsystems syncs the window to T
68 2. VDPVRAM checks overlap of the window with the command write area
69  no overlap -> go to step 6
70 3. VDPVRAM syncs the command engine to T
71 4. the command engine calls VDPVRAM to write each byte it changes in VRAM,
72  call the times this happens C1, C2, C3...
73 5. at the n-th write, VDPVRAM updates any subsystem with the written address
74  in its window to Cn, this can include the original subsystem
75 6. the window has reached T
76  now the subsystem can update itself to T
77 Using this approach instead of syncing on read makes sure there is no
78 re-entrance on the subsystem update methods.
79 
80 Note: command engine reads through write window when doing logic-ops.
81 So "source window" and "destination window" would be better names.
82 
83 Interesting observation:
84 Each window is at the same moment in time as the command engine (C):
85 - if a window doesn't overlap with the command destination window, it is
86  stable from a moment before C until the CPU time T
87 - if a window overlaps with the command destination window, it cannot be
88  before C (incorrect) or after C (uncertainty)
89 Since there is only one time for the entire VRAM, the VRAM itself can be said
90 to be at C. This is a justification for having the sync method in VDPVRAM
91 instead of in Window.
92 
93 Writing through a window is done as follows:
94 - CPU write: sync with all non-CPU windows, including command engine write
95 - command engine write: sync with non-CPU and non-command engine windows
96 Syncing with a window is only necessary if the write falls into that window.
97 
98 If all non-CPU windows are disjunct, then all subsystems function
99 independently (at least until CPU time), no need for syncs.
100 So what is interesting, is which windows overlap.
101 Since windows change position infrequently, it may be beneficial to
102 precalculate overlaps.
103 Not necessarily though, because even if two windows overlap, a single write
104 may not be inside the other window. So precalculated overlaps only speeds up
105 in the case there is no overlap.
106 Maybe it's not necessary to know exactly which windows overlap with cmdwrite,
107 only to know whether there are any. If not, sync can be skipped.
108 
109 Is it possible to read multiple bytes at the same time?
110 In other words, get a pointer to an array instead of reading single bytes.
111 Yes, but only the first 64 bytes are guaranteed to be correct, because that
112 is the granularity of the color table.
113 But since whatever is reading the VRAM knows what it is operating on, it
114 can decide for itself how many bytes to read.
115 
116 */
117 
118 class DummyVRAMOBserver final : public VRAMObserver
119 {
120 public:
121  void updateVRAM(unsigned /*offset*/, EmuTime::param /*time*/) override {}
122  void updateWindow(bool /*enabled*/, EmuTime::param /*time*/) override {}
123 };
124 
136 {
137 public:
138  VRAMWindow(const VRAMWindow&) = delete;
139  VRAMWindow& operator=(const VRAMWindow&) = delete;
140 
146  inline int getMask() const {
147  assert(isEnabled());
148  return effectiveBaseMask;
149  }
150 
163  inline void setMask(int newBaseMask, int newIndexMask,
164  EmuTime::param time) {
165  origBaseMask = newBaseMask;
166  newBaseMask &= sizeMask;
167  if (isEnabled() &&
168  (newBaseMask == effectiveBaseMask) &&
169  (newIndexMask == indexMask)) {
170  return;
171  }
172  observer->updateWindow(true, time);
173  effectiveBaseMask = newBaseMask;
174  indexMask = newIndexMask;
175  baseAddr = effectiveBaseMask & indexMask; // this enables window
176  combiMask = ~effectiveBaseMask | indexMask;
177  }
178 
182  inline void disable(EmuTime::param time) {
183  observer->updateWindow(false, time);
184  baseAddr = -1;
185  }
186 
190  inline bool isContinuous(unsigned index, unsigned size) const {
191  assert(isEnabled());
192  unsigned endIndex = index + size - 1;
193  unsigned areaBits = Math::floodRight(index ^ endIndex);
194  if ((areaBits & effectiveBaseMask) != areaBits) return false;
195  if ((areaBits & ~indexMask) != areaBits) return false;
196  return true;
197  }
198 
207  inline bool isContinuous(unsigned mask) const {
208  assert(isEnabled());
209  assert((mask & ~indexMask) == mask);
210  return (mask & effectiveBaseMask) == mask;
211  }
212 
219  inline const byte* getReadArea(unsigned index, unsigned size) const {
220  assert(isContinuous(index, size)); (void)size;
221  return &data[effectiveBaseMask & (indexMask | index)];
222  }
223 
233  inline void getReadAreaPlanar(
234  unsigned index, unsigned size,
235  const byte*& ptr0, const byte*& ptr1) const {
236  assert((index & 1) == 0);
237  assert((size & 1) == 0);
238  unsigned endIndex = index + size - 1;
239  unsigned areaBits = Math::floodRight(index ^ endIndex);
240  areaBits = ((areaBits << 16) | (areaBits >> 1)) & 0x1FFFF;
241  (void)areaBits;
242  assert((areaBits & effectiveBaseMask) == areaBits);
243  assert((areaBits & ~indexMask) == areaBits);
244  assert(isEnabled());
245  unsigned addr = effectiveBaseMask & (indexMask | (index >> 1));
246  ptr0 = &data[addr | 0x00000];
247  ptr1 = &data[addr | 0x10000];
248  }
249 
253  inline byte readNP(unsigned index) const {
254  assert(isEnabled());
255  return data[effectiveBaseMask & index];
256  }
257 
261  inline byte readPlanar(unsigned index) const {
262  assert(isEnabled());
263  index = ((index & 1) << 16) | ((index & 0x1FFFE) >> 1);
264  unsigned addr = effectiveBaseMask & index;
265  return data[addr];
266  }
267 
270  inline bool hasObserver() const {
271  return observer != &dummyObserver;
272  }
273 
279  inline void setObserver(VRAMObserver* newObserver) {
280  observer = newObserver;
281  }
282 
285  inline void resetObserver() {
286  observer = &dummyObserver;
287  }
288 
296  inline bool isInside(unsigned address) const {
297  return (address & combiMask) == unsigned(baseAddr);
298  }
299 
305  inline void notify(unsigned address, EmuTime::param time) {
306  if (isInside(address)) {
307  observer->updateVRAM(address - baseAddr, time);
308  }
309  }
310 
315  void setSizeMask(unsigned newSizeMask, EmuTime::param time) {
316  sizeMask = newSizeMask;
317  if (isEnabled()) {
318  setMask(origBaseMask, indexMask, time);
319  }
320  }
321 
322  template<typename Archive>
323  void serialize(Archive& ar, unsigned version);
324 
325 private:
326  inline bool isEnabled() const {
327  return baseAddr != -1;
328  }
329 
332  friend class VDPVRAM;
333 
337  explicit VRAMWindow(Ram& vram);
338 
341  byte* data;
342 
347  VRAMObserver* observer;
348 
351  int origBaseMask;
352 
356  int effectiveBaseMask;
357 
360  int indexMask;
361 
365  int baseAddr;
366 
369  int combiMask;
370 
375  int sizeMask;
376 
377  static DummyVRAMOBserver dummyObserver;
378 };
379 
384 class VDPVRAM
385 {
386 public:
387  VDPVRAM(const VDPVRAM&) = delete;
388  VDPVRAM& operator=(const VDPVRAM&) = delete;
389 
390  VDPVRAM(VDP& vdp, unsigned size, EmuTime::param time);
391 
394  void clear();
395 
400  inline void sync(EmuTime::param time) {
401  assert(vdp.isInsideFrame(time));
402  cmdEngine->sync(time);
403  }
404 
411  inline void cmdWrite(unsigned address, byte value, EmuTime::param time) {
412  #ifdef DEBUG
413  // Rewriting history is not allowed.
414  assert(time >= vramTime);
415  #endif
416  assert(vdp.isInsideFrame(time));
417 
418  // handle mirroring and non-present ram chips
419  address &= sizeMask;
420  if (unlikely(address >= actualSize)) {
421  // 192kb vram is mirroring is handled elsewhere
422  assert(address < 0x30000);
423  // only happens in case of 16kb vram while you write
424  // to range [0x4000,0x8000)
425  return;
426  }
427 
428  writeCommon(address, value, time);
429  }
430 
436  inline void cpuWrite(unsigned address, byte value, EmuTime::param time) {
437  #ifdef DEBUG
438  // Rewriting history is not allowed.
439  assert(time >= vramTime);
440  #endif
441  assert(vdp.isInsideFrame(time));
442 
443  // handle mirroring and non-present ram chips
444  address &= sizeMask;
445  if (unlikely(address >= actualSize)) {
446  // 192kb vram is mirroring is handled elsewhere
447  assert(address < 0x30000);
448  // only happens in case of 16kb vram while you write
449  // to range [0x4000,0x8000)
450  return;
451  }
452 
453  // We should still sync with cmdEngine, even if the VRAM already
454  // contains the value we're about to write (e.g. it's possible
455  // syncing with cmdEngine changes that value, and this write
456  // restores it again). This fixes bug:
457  // [2844043] Hinotori - Firebird small graphics corruption
458  if (cmdReadWindow .isInside(address) ||
459  cmdWriteWindow.isInside(address)) {
460  cmdEngine->sync(time);
461  }
462  writeCommon(address, value, time);
463 
464  cmdEngine->stealAccessSlot(time);
465  }
466 
472  inline byte cpuRead(unsigned address, EmuTime::param time) {
473  #ifdef DEBUG
474  // VRAM should never get ahead of CPU.
475  assert(time >= vramTime);
476  #endif
477  assert(vdp.isInsideFrame(time));
478 
479  address &= sizeMask;
480  if (cmdWriteWindow.isInside(address)) {
481  cmdEngine->sync(time);
482  }
483  cmdEngine->stealAccessSlot(time);
484 
485  #ifdef DEBUG
486  vramTime = time;
487  #endif
488  return data[address];
489  }
490 
499  void updateDisplayMode(DisplayMode mode, bool cmdBit, EmuTime::param time);
500 
507  void updateDisplayEnabled(bool enabled, EmuTime::param time);
508 
513  void updateSpritesEnabled(bool enabled, EmuTime::param time);
514 
519  void updateVRMode(bool mode, EmuTime::param time);
520 
521  void setRenderer(Renderer* renderer, EmuTime::param time);
522 
525  unsigned getSize() const {
526  return actualSize;
527  }
528 
531  inline void setSpriteChecker(SpriteChecker* newSpriteChecker) {
532  spriteChecker = newSpriteChecker;
533  }
534 
537  inline void setCmdEngine(VDPCmdEngine* newCmdEngine) {
538  cmdEngine = newCmdEngine;
539  }
540 
544  void change4k8kMapping(bool mapping8k);
545 
546  template<typename Archive>
547  void serialize(Archive& ar, unsigned version);
548 
549 private:
550  /* Common code of cmdWrite() and cpuWrite()
551  */
552  inline void writeCommon(unsigned address, byte value, EmuTime::param time) {
553  #ifdef DEBUG
554  assert(time >= vramTime);
555  vramTime = time;
556  #endif
557 
558  // Check that VRAM will actually be changed.
559  // A lot of costly syncs can be saved if the same value is written.
560  // For example Penguin Adventure always uploads the whole frame,
561  // even if it is the same as the previous frame.
562  if (data[address] == value) return;
563 
564  // Subsystem synchronisation should happen before the commit,
565  // to be able to draw backlog using old state.
566  bitmapVisibleWindow.notify(address, time);
567  spriteAttribTable.notify(address, time);
568  spritePatternTable.notify(address, time);
569 
570  data[address] = value;
571 
572  // Cache dirty marking should happen after the commit,
573  // otherwise the cache could be re-validated based on old state.
574 
575  // these two seem to be unused
576  // bitmapCacheWindow.notify(address, time);
577  // nameTable.notify(address, time);
578  assert(!bitmapCacheWindow.hasObserver());
579  assert(!nameTable.hasObserver());
580 
581  // in the past GLRasterizer observed these two, now there are none
582  assert(!colorTable.hasObserver());
583  assert(!patternTable.hasObserver());
584 
585  /* TODO:
586  There seems to be a significant difference between subsystem sync
587  and cache admin. One example is the code above, the other is
588  updateWindow, where subsystem sync is interested in windows that
589  were enabled before (new state doesn't matter), while cache admin
590  is interested in windows that become enabled (old state doesn't
591  matter).
592  Does this mean it makes sense to have separate VRAMWindow like
593  classes for each category?
594  Note: In the future, sprites may switch category, or fall in both.
595  */
596  }
597 
598  void setSizeMask(EmuTime::param time);
599 
602  VDP& vdp;
603 
606  Ram data;
607 
613  class LogicalVRAMDebuggable final : public SimpleDebuggable {
614  public:
615  explicit LogicalVRAMDebuggable(VDP& vdp);
616  byte read(unsigned address, EmuTime::param time) override;
617  void write(unsigned address, byte value, EmuTime::param time) override;
618  private:
619  unsigned transform(unsigned address);
620  } logicalVRAMDebug;
621 
626  struct PhysicalVRAMDebuggable final : SimpleDebuggable {
627  PhysicalVRAMDebuggable(VDP& vdp, unsigned actualSize);
628  byte read(unsigned address, EmuTime::param time) override;
629  void write(unsigned address, byte value, EmuTime::param time) override;
630  } physicalVRAMDebug;
631 
632  // TODO: Renderer field can be removed, if updateDisplayMode
633  // and updateDisplayEnabled are moved back to VDP.
634  // Is that a good idea?
635  Renderer* renderer;
636 
637  VDPCmdEngine* cmdEngine;
638  SpriteChecker* spriteChecker;
639 
644  #ifdef DEBUG
645  EmuTime vramTime;
646  #endif
647 
652  unsigned sizeMask;
653 
657  const unsigned actualSize;
658 
661  bool vrMode;
662 
663 public:
673 };
674 
675 } // namespace openmsx
676 
677 #endif
bool isInside(unsigned address) const
Test whether an address is inside this window.
Definition: VDPVRAM.hh:296
void setCmdEngine(VDPCmdEngine *newCmdEngine)
Necessary because of circular dependencies.
Definition: VDPVRAM.hh:537
void setSpriteChecker(SpriteChecker *newSpriteChecker)
Necessary because of circular dependencies.
Definition: VDPVRAM.hh:531
bool isContinuous(unsigned index, unsigned size) const
Is the given index range continuous in VRAM (iow there&#39;s no mirroring) Only if the range is continuou...
Definition: VDPVRAM.hh:190
#define unlikely(x)
Definition: likely.hh:15
VDP command engine by Alex Wulms.
Definition: VDPCmdEngine.hh:22
byte readNP(unsigned index) const
Reads a byte from VRAM in its current state.
Definition: VDPVRAM.hh:253
void updateVRAM(unsigned, EmuTime::param) override
Informs the observer of a change in VRAM contents.
Definition: VDPVRAM.hh:121
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
constexpr T floodRight(T x) noexcept
Returns the smallest number of the form 2^n-1 that is greater or equal to the given number...
Definition: Math.hh:68
byte cpuRead(unsigned address, EmuTime::param time)
Read a byte from VRAM though the CPU interface.
Definition: VDPVRAM.hh:472
VRAMWindow patternTable
Definition: VDPVRAM.hh:668
VRAMWindow bitmapVisibleWindow
Definition: VDPVRAM.hh:669
bool isContinuous(unsigned mask) const
Alternative version to check whether a region is continuous in VRAM.
Definition: VDPVRAM.hh:207
Represents a VDP display mode.
Definition: DisplayMode.hh:14
VRAMWindow nameTable
Definition: VDPVRAM.hh:666
VRAMWindow spriteAttribTable
Definition: VDPVRAM.hh:671
void setObserver(VRAMObserver *newObserver)
Register an observer on this VRAM window.
Definition: VDPVRAM.hh:279
void cmdWrite(unsigned address, byte value, EmuTime::param time)
Write a byte from the command engine.
Definition: VDPVRAM.hh:411
constexpr auto data(C &c) -> decltype(c.data())
Definition: span.hh:69
void cpuWrite(unsigned address, byte value, EmuTime::param time)
Write a byte to VRAM through the CPU interface.
Definition: VDPVRAM.hh:436
void setSizeMask(unsigned newSizeMask, EmuTime::param time)
Inform VRAMWindow of changed sizeMask.
Definition: VDPVRAM.hh:315
bool hasObserver() const
Is there an observer registered for this window?
Definition: VDPVRAM.hh:270
Specifies an address range in the VRAM.
Definition: VDPVRAM.hh:135
VRAMWindow colorTable
Definition: VDPVRAM.hh:667
unsigned getSize() const
Returns the size of VRAM in bytes.
Definition: VDPVRAM.hh:525
Abstract base class for Renderers.
Definition: Renderer.hh:22
Interface that can be registered at VRAMWindow, to be called when the contents of the VRAM inside tha...
Definition: VRAMObserver.hh:11
bool isInsideFrame(EmuTime::param time) const
Is the given timestamp inside the current frame? Mainly useful for debugging, because relevant timest...
Definition: VDP.hh:499
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
VRAMWindow spritePatternTable
Definition: VDPVRAM.hh:672
Unified implementation of MSX Video Display Processors (VDPs).
Definition: VDP.hh:61
void disable(EmuTime::param time)
Disable this window: no address will be considered inside.
Definition: VDPVRAM.hh:182
VRAMWindow cmdWriteWindow
Definition: VDPVRAM.hh:665
VRAMWindow cmdReadWindow
Definition: VDPVRAM.hh:664
int getMask() const
Gets the mask for this window.
Definition: VDPVRAM.hh:146
Manages VRAM contents and synchronises the various users of the VRAM.
Definition: VDPVRAM.hh:384
const byte * getReadArea(unsigned index, unsigned size) const
Gets a pointer to a contiguous part of the VRAM.
Definition: VDPVRAM.hh:219
byte readPlanar(unsigned index) const
Similar to readNP, but now with planar addressing.
Definition: VDPVRAM.hh:261
auto transform(InputRange &&range, OutputIter out, UnaryOperation op)
Definition: ranges.hh:161
void notify(unsigned address, EmuTime::param time)
Notifies the observer of this window of a VRAM change, if the changes address is inside this window...
Definition: VDPVRAM.hh:305
VRAMWindow bitmapCacheWindow
Definition: VDPVRAM.hh:670
void getReadAreaPlanar(unsigned index, unsigned size, const byte *&ptr0, const byte *&ptr1) const
Similar to getReadArea(), but now with planar addressing mode.
Definition: VDPVRAM.hh:233
void updateWindow(bool, EmuTime::param) override
Informs the observer that the entire VRAM window will change.
Definition: VDPVRAM.hh:122
void resetObserver()
Unregister the observer of this VRAM window.
Definition: VDPVRAM.hh:285
constexpr auto size(const C &c) -> decltype(c.size())
Definition: span.hh:62
void serialize(Archive &ar, T &t, unsigned version)
void setMask(int newBaseMask, int newIndexMask, EmuTime::param time)
Sets the mask and enables this window.
Definition: VDPVRAM.hh:163
void sync(EmuTime::param time)
Update VRAM state to specified moment in time.
Definition: VDPVRAM.hh:400