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
13namespace openmsx {
14
15class DisplayMode;
16class SpriteChecker;
17class Renderer;
18
19/*
20Note: The way VRAM is accessed depends a lot on who is doing the accessing.
21
22For 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
27Maybe define an interface with multiple subclasses?
28Or is that too much of a performance hit?
29If accessed through the interface, a virtual method call is made.
30But invoking the objects directly should be inlined.
31
32Timing:
33
34Each window reflects the state of the VRAM at a specified moment in time.
35
36Because the CPU has full-range write access, it is incorrect for any window
37to be ahead in time compared to the CPU. Because multi-cycle operations are
38implemented as atomic, it is currently possible that a window which starts
39an operation slightly before CPU time ends up slightly after CPU time.
40Solutions:
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
48Window ranges are not at fixed. But they can only be changed by the CPU, so
49they are fixed until CPU time, which subsystems will never go beyond anyway.
50
51The only two subsystems with write access are CPU and command engine.
52The command engine can only start executing a new command if instructed so
53by the CPU. Therefore it is known which area the command engine can write
54in 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
57Currently the command's reach is not computed: full VRAM is used.
58Taking the Y coordinate into account would speed things up a lot, because
59usually commands execute on invisible pages, so the number of area overlaps
60between renderer and command engine would be reduced significantly.
61Also sprite tables are usually not written by commands.
62
63Reading through a window is done as follows:
64A subsystem reads the VRAM because it is updating itself to a certain moment
65in time T.
661. the subsystems syncs the window to T
672. VDPVRAM checks overlap of the window with the command write area
68 no overlap -> go to step 6
693. VDPVRAM syncs the command engine to T
704. the command engine calls VDPVRAM to write each byte it changes in VRAM,
71 call the times this happens C1, C2, C3...
725. 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
746. the window has reached T
75 now the subsystem can update itself to T
76Using this approach instead of syncing on read makes sure there is no
77re-entrance on the subsystem update methods.
78
79Note: command engine reads through write window when doing logic-ops.
80So "source window" and "destination window" would be better names.
81
82Interesting observation:
83Each 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)
88Since there is only one time for the entire VRAM, the VRAM itself can be said
89to be at C. This is a justification for having the sync method in VDPVRAM
90instead of in Window.
91
92Writing 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
95Syncing with a window is only necessary if the write falls into that window.
96
97If all non-CPU windows are disjunct, then all subsystems function
98independently (at least until CPU time), no need for syncs.
99So what is interesting, is which windows overlap.
100Since windows change position infrequently, it may be beneficial to
101precalculate overlaps.
102Not necessarily though, because even if two windows overlap, a single write
103may not be inside the other window. So precalculated overlaps only speeds up
104in the case there is no overlap.
105Maybe it's not necessary to know exactly which windows overlap with cmdWrite,
106only to know whether there are any. If not, sync can be skipped.
107
108Is it possible to read multiple bytes at the same time?
109In other words, get a pointer to an array instead of reading single bytes.
110Yes, but only the first 64 bytes are guaranteed to be correct, because that
111is the granularity of the color table.
112But since whatever is reading the VRAM knows what it is operating on, it
113can decide for itself how many bytes to read.
114
115*/
116
117class DummyVRAMObserver final : public VRAMObserver
118{
119public:
120 void updateVRAM(unsigned /*offset*/, EmuTime::param /*time*/) override {}
121 void updateWindow(bool /*enabled*/, EmuTime::param /*time*/) override {}
122};
123
135{
136public:
137 VRAMWindow(const VRAMWindow&) = delete;
139 VRAMWindow& operator=(const VRAMWindow&) = delete;
141
147 [[nodiscard]] inline unsigned getMask() const {
148 assert(isEnabled());
149 return effectiveBaseMask;
150 }
151
165 inline void setMask(unsigned newBaseMask, unsigned newIndexMask,
166 unsigned newSizeMask, EmuTime::param time) {
167 origBaseMask = newBaseMask;
168 newBaseMask &= newSizeMask;
169 if (isEnabled() &&
170 (newBaseMask == effectiveBaseMask) &&
171 (newIndexMask == indexMask)) {
172 return;
173 }
174 observer->updateWindow(true, time);
175 effectiveBaseMask = newBaseMask;
176 indexMask = newIndexMask;
177 baseAddr = effectiveBaseMask & indexMask; // this enables window
178 combiMask = ~effectiveBaseMask | indexMask;
179 }
180
184 inline void setMask(unsigned newBaseMask, unsigned newIndexMask,
185 EmuTime::param time) {
186 setMask(newBaseMask, newIndexMask, sizeMask, time);
187 }
188
192 inline void disable(EmuTime::param time) {
193 observer->updateWindow(false, time);
194 baseAddr = unsigned(-1);
195 }
196
200 [[nodiscard]] inline bool isContinuous(unsigned index, unsigned size) const {
201 assert(isEnabled());
202 unsigned endIndex = index + size - 1;
203 unsigned areaBits = Math::floodRight(index ^ endIndex);
204 if ((areaBits & effectiveBaseMask) != areaBits) return false;
205 if ((areaBits & ~indexMask) != areaBits) return false;
206 return true;
207 }
208
217 [[nodiscard]] inline bool isContinuous(unsigned mask) const {
218 assert(isEnabled());
219 assert((mask & ~indexMask) == mask);
220 return (mask & effectiveBaseMask) == mask;
221 }
222
227 template<size_t size>
228 [[nodiscard]] inline std::span<const byte, size> getReadArea(unsigned index) const {
229 assert(isContinuous(index, size));
230 return std::span<const byte, size>{
231 &data[effectiveBaseMask & (indexMask | index)],
232 size};
233 }
234
243 template<size_t size>
244 [[nodiscard]] inline std::pair<std::span<const byte, size / 2>, std::span<const byte, size / 2>>
245 getReadAreaPlanar(unsigned index) const {
246 assert((index & 1) == 0);
247 assert((size & 1) == 0);
248 unsigned endIndex = index + size - 1;
249 unsigned areaBits = Math::floodRight(index ^ endIndex);
250 areaBits = ((areaBits << 16) | (areaBits >> 1)) & 0x1FFFF & sizeMask;
251 (void)areaBits;
252 assert((areaBits & effectiveBaseMask) == areaBits);
253 assert((areaBits & ~indexMask) == areaBits);
254 assert(isEnabled());
255 unsigned addr = effectiveBaseMask & (indexMask | (index >> 1));
256 const byte* ptr0 = &data[addr | 0x00000];
257 const byte* ptr1 = &data[addr | 0x10000];
258 return {std::span<const byte, size / 2>{ptr0, size / 2},
259 std::span<const byte, size / 2>{ptr1, size / 2}};
260 }
261
265 [[nodiscard]] inline byte readNP(unsigned index) const {
266 assert(isEnabled());
267 return data[effectiveBaseMask & index];
268 }
269
273 [[nodiscard]] inline byte readPlanar(unsigned index) const {
274 assert(isEnabled());
275 index = ((index & 1) << 16) | ((index & 0x1FFFE) >> 1);
276 unsigned addr = effectiveBaseMask & index;
277 return data[addr];
278 }
279
282 [[nodiscard]] inline bool hasObserver() const {
283 return observer != &dummyObserver;
284 }
285
291 inline void setObserver(VRAMObserver* newObserver) {
292 observer = newObserver;
293 }
294
297 inline void resetObserver() {
298 observer = &dummyObserver;
299 }
300
308 [[nodiscard]] inline bool isInside(unsigned address) const {
309 return (address & combiMask) == baseAddr;
310 }
311
317 inline void notify(unsigned address, EmuTime::param time) {
318 if (isInside(address)) {
319 observer->updateVRAM(address - baseAddr, time);
320 }
321 }
322
327 void setSizeMask(unsigned newSizeMask, EmuTime::param time) {
328 if (isEnabled()) {
329 setMask(origBaseMask, indexMask, newSizeMask, time);
330 }
331 // only apply sizeMask after observers have been notified
332 sizeMask = newSizeMask;
333 }
334
335 template<typename Archive>
336 void serialize(Archive& ar, unsigned version);
337
338private:
339 [[nodiscard]] inline bool isEnabled() const {
340 return baseAddr != unsigned(-1);
341 }
342
343private:
346 friend class VDPVRAM;
347
351 explicit VRAMWindow(Ram& vram);
352
355 byte* data;
356
361 VRAMObserver* observer = &dummyObserver;
362
365 unsigned origBaseMask = 0;
366
370 unsigned effectiveBaseMask = 0;
371
374 unsigned indexMask = 0;
375
379 unsigned baseAddr = unsigned(-1); // disable window
380
383 unsigned combiMask = 0;
384
389 unsigned sizeMask;
390
391 static inline DummyVRAMObserver dummyObserver;
392};
393
399{
400public:
401 VDPVRAM(const VDPVRAM&) = delete;
402 VDPVRAM(VDPVRAM&&) = delete;
403 VDPVRAM& operator=(const VDPVRAM&) = delete;
405
406 VDPVRAM(VDP& vdp, unsigned size, EmuTime::param time);
407
410 void clear();
411
416 inline void sync(EmuTime::param time) {
417 assert(vdp.isInsideFrame(time));
418 cmdEngine->sync(time);
419 }
420
427 inline void cmdWrite(unsigned address, byte value, EmuTime::param time) {
428 #ifdef DEBUG
429 // Rewriting history is not allowed.
430 assert(time >= vramTime);
431 #endif
432 assert(vdp.isInsideFrame(time));
433
434 // handle mirroring and non-present ram chips
435 address &= sizeMask;
436 if (address >= actualSize) [[unlikely]] {
437 // 192kb vram is mirroring is handled elsewhere
438 assert(address < 0x30000);
439 // only happens in case of 16kb vram while you write
440 // to range [0x4000,0x8000)
441 return;
442 }
443
444 writeCommon(address, value, time);
445 }
446
452 inline void cpuWrite(unsigned address, byte value, EmuTime::param time) {
453 #ifdef DEBUG
454 // Rewriting history is not allowed.
455 assert(time >= vramTime);
456 #endif
457 assert(vdp.isInsideFrame(time));
458
459 // handle mirroring and non-present ram chips
460 address &= sizeMask;
461 if (address >= actualSize) [[unlikely]] {
462 // 192kb vram is mirroring is handled elsewhere
463 assert(address < 0x30000);
464 // only happens in case of 16kb vram while you write
465 // to range [0x4000,0x8000)
466 return;
467 }
468
469 // We should still sync with cmdEngine, even if the VRAM already
470 // contains the value we're about to write (e.g. it's possible
471 // syncing with cmdEngine changes that value, and this write
472 // restores it again). This fixes bug:
473 // [2844043] Hinotori - Firebird small graphics corruption
474 if (cmdReadWindow .isInside(address) ||
475 cmdWriteWindow.isInside(address)) {
476 cmdEngine->sync(time);
477 }
478 writeCommon(address, value, time);
479
480 cmdEngine->stealAccessSlot(time);
481 }
482
488 [[nodiscard]] inline byte cpuRead(unsigned address, EmuTime::param time) {
489 #ifdef DEBUG
490 // VRAM should never get ahead of CPU.
491 assert(time >= vramTime);
492 #endif
493 assert(vdp.isInsideFrame(time));
494
495 address &= sizeMask;
496 if (cmdWriteWindow.isInside(address)) {
497 cmdEngine->sync(time);
498 }
499 cmdEngine->stealAccessSlot(time);
500
501 #ifdef DEBUG
502 vramTime = time;
503 #endif
504 return data[address];
505 }
506
515 void updateDisplayMode(DisplayMode mode, bool cmdBit, EmuTime::param time);
516
523 void updateDisplayEnabled(bool enabled, EmuTime::param time);
524
529 void updateSpritesEnabled(bool enabled, EmuTime::param time);
530
535 void updateVRMode(bool mode, EmuTime::param time);
536
537 void setRenderer(Renderer* renderer, EmuTime::param time);
538
541 [[nodiscard]] unsigned getSize() const {
542 return actualSize;
543 }
544
547 inline void setSpriteChecker(SpriteChecker* newSpriteChecker) {
548 spriteChecker = newSpriteChecker;
549 }
550
553 inline void setCmdEngine(VDPCmdEngine* newCmdEngine) {
554 cmdEngine = newCmdEngine;
555 }
556
560 void change4k8kMapping(bool mapping8k);
561
564 [[nodiscard]] std::span<const uint8_t> getData() const {
565 return {data.data(), data.size()};
566 }
567
568 template<typename Archive>
569 void serialize(Archive& ar, unsigned version);
570
571private:
572 /* Common code of cmdWrite() and cpuWrite()
573 */
574 inline void writeCommon(unsigned address, byte value, EmuTime::param time) {
575 #ifdef DEBUG
576 assert(time >= vramTime);
577 vramTime = time;
578 #endif
579
580 // Check that VRAM will actually be changed.
581 // A lot of costly syncs can be saved if the same value is written.
582 // For example Penguin Adventure always uploads the whole frame,
583 // even if it is the same as the previous frame.
584 if (data[address] == value) return;
585
586 // Subsystem synchronisation should happen before the commit,
587 // to be able to draw backlog using old state.
588 bitmapVisibleWindow.notify(address, time);
589 spriteAttribTable.notify(address, time);
590 spritePatternTable.notify(address, time);
591
592 data[address] = value;
593
594 // Cache dirty marking should happen after the commit,
595 // otherwise the cache could be re-validated based on old state.
596
597 // these two seem to be unused
598 // bitmapCacheWindow.notify(address, time);
599 // nameTable.notify(address, time);
601 assert(!nameTable.hasObserver());
602
603 // in the past GLRasterizer observed these two, now there are none
604 assert(!colorTable.hasObserver());
605 assert(!patternTable.hasObserver());
606
607 /* TODO:
608 There seems to be a significant difference between subsystem sync
609 and cache admin. One example is the code above, the other is
610 updateWindow, where subsystem sync is interested in windows that
611 were enabled before (new state doesn't matter), while cache admin
612 is interested in windows that become enabled (old state doesn't
613 matter).
614 Does this mean it makes sense to have separate VRAMWindow like
615 classes for each category?
616 Note: In the future, sprites may switch category, or fall in both.
617 */
618 }
619
620 void setSizeMask(EmuTime::param time);
621
622private:
625 VDP& vdp;
626
629 Ram data;
630
636 class LogicalVRAMDebuggable final : public SimpleDebuggable {
637 public:
638 explicit LogicalVRAMDebuggable(const VDP& vdp);
639 [[nodiscard]] byte read(unsigned address, EmuTime::param time) override;
640 void write(unsigned address, byte value, EmuTime::param time) override;
641 private:
642 unsigned transform(unsigned address);
643 } logicalVRAMDebug;
644
649 struct PhysicalVRAMDebuggable final : SimpleDebuggable {
650 PhysicalVRAMDebuggable(const VDP& vdp, unsigned actualSize);
651 [[nodiscard]] byte read(unsigned address, EmuTime::param time) override;
652 void write(unsigned address, byte value, EmuTime::param time) override;
653 } physicalVRAMDebug;
654
655 // TODO: Renderer field can be removed, if updateDisplayMode
656 // and updateDisplayEnabled are moved back to VDP.
657 // Is that a good idea?
658 Renderer* renderer;
659
660 VDPCmdEngine* cmdEngine;
661 SpriteChecker* spriteChecker;
662
667 #ifdef DEBUG
668 EmuTime vramTime = EmuTime::zero();
669 #endif
670
675 unsigned sizeMask;
676
680 const unsigned actualSize;
681
684 bool vrMode;
685
686public:
696};
697
698} // namespace openmsx
699
700#endif
Represents a VDP display mode.
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
auto size() const
Definition Ram.hh:44
auto data()
Definition Ram.hh:45
Abstract base class for Renderers.
Definition Renderer.hh:24
byte read(unsigned address) override
void write(unsigned address, byte value) override
VDP command engine by Alex Wulms.
void stealAccessSlot(EmuTime::param time)
Steal a VRAM access slot from the CmdEngine.
void sync(EmuTime::param time)
Synchronizes the command engine with the VDP.
Manages VRAM contents and synchronizes the various users of the VRAM.
Definition VDPVRAM.hh:399
void updateSpritesEnabled(bool enabled, EmuTime::param time)
Used by the VDP to signal sprites enabled changes.
Definition VDPVRAM.cc:157
VRAMWindow spriteAttribTable
Definition VDPVRAM.hh:694
void clear()
Initialize VRAM content to power-up state.
Definition VDPVRAM.cc:128
VDPVRAM(const VDPVRAM &)=delete
void cmdWrite(unsigned address, byte value, EmuTime::param time)
Write a byte from the command engine.
Definition VDPVRAM.hh:427
VDPVRAM & operator=(const VDPVRAM &)=delete
void setRenderer(Renderer *renderer, EmuTime::param time)
Definition VDPVRAM.cc:218
VRAMWindow colorTable
Definition VDPVRAM.hh:690
VDPVRAM & operator=(VDPVRAM &&)=delete
void updateVRMode(bool mode, EmuTime::param time)
Change between VR=0 and VR=1 mode.
Definition VDPVRAM.cc:195
VRAMWindow cmdReadWindow
Definition VDPVRAM.hh:687
VRAMWindow bitmapCacheWindow
Definition VDPVRAM.hh:693
void updateDisplayEnabled(bool enabled, EmuTime::param time)
Used by the VDP to signal display enabled changes.
Definition VDPVRAM.cc:149
void setSpriteChecker(SpriteChecker *newSpriteChecker)
Necessary because of circular dependencies.
Definition VDPVRAM.hh:547
void updateDisplayMode(DisplayMode mode, bool cmdBit, EmuTime::param time)
Used by the VDP to signal display mode changes.
Definition VDPVRAM.cc:141
VDPVRAM(VDPVRAM &&)=delete
VRAMWindow bitmapVisibleWindow
Definition VDPVRAM.hh:692
void sync(EmuTime::param time)
Update VRAM state to specified moment in time.
Definition VDPVRAM.hh:416
byte cpuRead(unsigned address, EmuTime::param time)
Read a byte from VRAM though the CPU interface.
Definition VDPVRAM.hh:488
void serialize(Archive &ar, unsigned version)
Definition VDPVRAM.cc:314
unsigned getSize() const
Returns the size of VRAM in bytes.
Definition VDPVRAM.hh:541
std::span< const uint8_t > getData() const
Only used by debugger.
Definition VDPVRAM.hh:564
VRAMWindow spritePatternTable
Definition VDPVRAM.hh:695
void cpuWrite(unsigned address, byte value, EmuTime::param time)
Write a byte to VRAM through the CPU interface.
Definition VDPVRAM.hh:452
VRAMWindow patternTable
Definition VDPVRAM.hh:691
VRAMWindow cmdWriteWindow
Definition VDPVRAM.hh:688
void setCmdEngine(VDPCmdEngine *newCmdEngine)
Necessary because of circular dependencies.
Definition VDPVRAM.hh:553
void change4k8kMapping(bool mapping8k)
TMS99x8 VRAM can be mapped in two ways.
Definition VDPVRAM.cc:231
VRAMWindow nameTable
Definition VDPVRAM.hh:689
Unified implementation of MSX Video Display Processors (VDPs).
Definition VDP.hh:66
bool isInsideFrame(EmuTime::param time) const
Is the given timestamp inside the current frame? Mainly useful for debugging, because relevant timest...
Definition VDP.hh:578
Interface that can be registered at VRAMWindow, to be called when the contents of the VRAM inside tha...
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 setMask(unsigned newBaseMask, unsigned newIndexMask, unsigned newSizeMask, EmuTime::param time)
Sets the mask and enables this window.
Definition VDPVRAM.hh:165
void disable(EmuTime::param time)
Disable this window: no address will be considered inside.
Definition VDPVRAM.hh:192
VRAMWindow(VRAMWindow &&)=delete
VRAMWindow & operator=(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:200
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:317
byte readNP(unsigned index) const
Reads a byte from VRAM in its current state.
Definition VDPVRAM.hh:265
bool hasObserver() const
Is there an observer registered for this window?
Definition VDPVRAM.hh:282
VRAMWindow & operator=(const VRAMWindow &)=delete
bool isInside(unsigned address) const
Test whether an address is inside this window.
Definition VDPVRAM.hh:308
void serialize(Archive &ar, unsigned version)
Definition VDPVRAM.cc:301
void setMask(unsigned newBaseMask, unsigned newIndexMask, EmuTime::param time)
Same as above, but 'sizeMask' doesn't change.
Definition VDPVRAM.hh:184
bool isContinuous(unsigned mask) const
Alternative version to check whether a region is continuous in VRAM.
Definition VDPVRAM.hh:217
void setSizeMask(unsigned newSizeMask, EmuTime::param time)
Inform VRAMWindow of changed sizeMask.
Definition VDPVRAM.hh:327
byte readPlanar(unsigned index) const
Similar to readNP, but now with planar addressing.
Definition VDPVRAM.hh:273
VRAMWindow(const VRAMWindow &)=delete
unsigned getMask() const
Gets the mask for this window.
Definition VDPVRAM.hh:147
std::pair< std::span< const byte, size/2 >, std::span< const byte, size/2 > > getReadAreaPlanar(unsigned index) const
Similar to getReadArea(), but now with planar addressing mode.
Definition VDPVRAM.hh:245
void setObserver(VRAMObserver *newObserver)
Register an observer on this VRAM window.
Definition VDPVRAM.hh:291
std::span< const byte, size > getReadArea(unsigned index) const
Gets a span of a contiguous part of the VRAM.
Definition VDPVRAM.hh:228
void resetObserver()
Unregister the observer of this VRAM window.
Definition VDPVRAM.hh:297
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:32
This file implemented 3 utility functions:
Definition Autofire.cc:11
uint8_t byte
8 bit unsigned integer
Definition openmsx.hh:26