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