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;
138 VRAMWindow& operator=(const VRAMWindow&) = delete;
139
145 [[nodiscard]] inline unsigned getMask() const {
146 assert(isEnabled());
147 return effectiveBaseMask;
148 }
149
162 inline void setMask(unsigned newBaseMask, unsigned 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 = unsigned(-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
216 template<size_t size>
217 [[nodiscard]] inline std::span<const byte, size> getReadArea(unsigned index) const {
218 assert(isContinuous(index, size));
219 return std::span<const byte, size>{
220 &data[effectiveBaseMask & (indexMask | index)],
221 size};
222 }
223
232 template<size_t size>
233 [[nodiscard]] inline std::pair<std::span<const byte, size / 2>, std::span<const byte, size / 2>>
234 getReadAreaPlanar(unsigned index) 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 {std::span<const byte, size / 2>{ptr0, size / 2},
248 std::span<const byte, size / 2>{ptr1, size / 2}};
249 }
250
254 [[nodiscard]] inline byte readNP(unsigned index) const {
255 assert(isEnabled());
256 return data[effectiveBaseMask & index];
257 }
258
262 [[nodiscard]] inline byte readPlanar(unsigned index) const {
263 assert(isEnabled());
264 index = ((index & 1) << 16) | ((index & 0x1FFFE) >> 1);
265 unsigned addr = effectiveBaseMask & index;
266 return data[addr];
267 }
268
271 [[nodiscard]] inline bool hasObserver() const {
272 return observer != &dummyObserver;
273 }
274
280 inline void setObserver(VRAMObserver* newObserver) {
281 observer = newObserver;
282 }
283
286 inline void resetObserver() {
287 observer = &dummyObserver;
288 }
289
297 [[nodiscard]] inline bool isInside(unsigned address) const {
298 return (address & combiMask) == baseAddr;
299 }
300
306 inline void notify(unsigned address, EmuTime::param time) {
307 if (isInside(address)) {
308 observer->updateVRAM(address - baseAddr, time);
309 }
310 }
311
316 void setSizeMask(unsigned newSizeMask, EmuTime::param time) {
317 sizeMask = newSizeMask;
318 if (isEnabled()) {
319 setMask(origBaseMask, indexMask, time);
320 }
321 }
322
323 template<typename Archive>
324 void serialize(Archive& ar, unsigned version);
325
326private:
327 [[nodiscard]] inline bool isEnabled() const {
328 return baseAddr != unsigned(-1);
329 }
330
331private:
334 friend class VDPVRAM;
335
339 explicit VRAMWindow(Ram& vram);
340
343 byte* data;
344
349 VRAMObserver* observer = &dummyObserver;
350
353 unsigned origBaseMask = 0;
354
358 unsigned effectiveBaseMask = 0;
359
362 unsigned indexMask = 0;
363
367 unsigned baseAddr = unsigned(-1); // disable window
368
371 unsigned combiMask = 0;
372
377 unsigned sizeMask;
378
379 static inline DummyVRAMObserver dummyObserver;
380};
381
387{
388public:
389 VDPVRAM(const VDPVRAM&) = delete;
390 VDPVRAM& operator=(const VDPVRAM&) = delete;
391
392 VDPVRAM(VDP& vdp, unsigned size, EmuTime::param time);
393
396 void clear();
397
402 inline void sync(EmuTime::param time) {
403 assert(vdp.isInsideFrame(time));
404 cmdEngine->sync(time);
405 }
406
413 inline void cmdWrite(unsigned address, byte value, EmuTime::param time) {
414 #ifdef DEBUG
415 // Rewriting history is not allowed.
416 assert(time >= vramTime);
417 #endif
418 assert(vdp.isInsideFrame(time));
419
420 // handle mirroring and non-present ram chips
421 address &= sizeMask;
422 if (address >= actualSize) [[unlikely]] {
423 // 192kb vram is mirroring is handled elsewhere
424 assert(address < 0x30000);
425 // only happens in case of 16kb vram while you write
426 // to range [0x4000,0x8000)
427 return;
428 }
429
430 writeCommon(address, value, time);
431 }
432
438 inline void cpuWrite(unsigned address, byte value, EmuTime::param time) {
439 #ifdef DEBUG
440 // Rewriting history is not allowed.
441 assert(time >= vramTime);
442 #endif
443 assert(vdp.isInsideFrame(time));
444
445 // handle mirroring and non-present ram chips
446 address &= sizeMask;
447 if (address >= actualSize) [[unlikely]] {
448 // 192kb vram is mirroring is handled elsewhere
449 assert(address < 0x30000);
450 // only happens in case of 16kb vram while you write
451 // to range [0x4000,0x8000)
452 return;
453 }
454
455 // We should still sync with cmdEngine, even if the VRAM already
456 // contains the value we're about to write (e.g. it's possible
457 // syncing with cmdEngine changes that value, and this write
458 // restores it again). This fixes bug:
459 // [2844043] Hinotori - Firebird small graphics corruption
460 if (cmdReadWindow .isInside(address) ||
461 cmdWriteWindow.isInside(address)) {
462 cmdEngine->sync(time);
463 }
464 writeCommon(address, value, time);
465
466 cmdEngine->stealAccessSlot(time);
467 }
468
474 [[nodiscard]] inline byte cpuRead(unsigned address, EmuTime::param time) {
475 #ifdef DEBUG
476 // VRAM should never get ahead of CPU.
477 assert(time >= vramTime);
478 #endif
479 assert(vdp.isInsideFrame(time));
480
481 address &= sizeMask;
482 if (cmdWriteWindow.isInside(address)) {
483 cmdEngine->sync(time);
484 }
485 cmdEngine->stealAccessSlot(time);
486
487 #ifdef DEBUG
488 vramTime = time;
489 #endif
490 return data[address];
491 }
492
501 void updateDisplayMode(DisplayMode mode, bool cmdBit, EmuTime::param time);
502
509 void updateDisplayEnabled(bool enabled, EmuTime::param time);
510
515 void updateSpritesEnabled(bool enabled, EmuTime::param time);
516
521 void updateVRMode(bool mode, EmuTime::param time);
522
523 void setRenderer(Renderer* renderer, EmuTime::param time);
524
527 [[nodiscard]] unsigned getSize() const {
528 return actualSize;
529 }
530
533 inline void setSpriteChecker(SpriteChecker* newSpriteChecker) {
534 spriteChecker = newSpriteChecker;
535 }
536
539 inline void setCmdEngine(VDPCmdEngine* newCmdEngine) {
540 cmdEngine = newCmdEngine;
541 }
542
546 void change4k8kMapping(bool mapping8k);
547
548 template<typename Archive>
549 void serialize(Archive& ar, unsigned version);
550
551private:
552 /* Common code of cmdWrite() and cpuWrite()
553 */
554 inline void writeCommon(unsigned address, byte value, EmuTime::param time) {
555 #ifdef DEBUG
556 assert(time >= vramTime);
557 vramTime = time;
558 #endif
559
560 // Check that VRAM will actually be changed.
561 // A lot of costly syncs can be saved if the same value is written.
562 // For example Penguin Adventure always uploads the whole frame,
563 // even if it is the same as the previous frame.
564 if (data[address] == value) return;
565
566 // Subsystem synchronisation should happen before the commit,
567 // to be able to draw backlog using old state.
568 bitmapVisibleWindow.notify(address, time);
569 spriteAttribTable.notify(address, time);
570 spritePatternTable.notify(address, time);
571
572 data[address] = value;
573
574 // Cache dirty marking should happen after the commit,
575 // otherwise the cache could be re-validated based on old state.
576
577 // these two seem to be unused
578 // bitmapCacheWindow.notify(address, time);
579 // nameTable.notify(address, time);
581 assert(!nameTable.hasObserver());
582
583 // in the past GLRasterizer observed these two, now there are none
584 assert(!colorTable.hasObserver());
585 assert(!patternTable.hasObserver());
586
587 /* TODO:
588 There seems to be a significant difference between subsystem sync
589 and cache admin. One example is the code above, the other is
590 updateWindow, where subsystem sync is interested in windows that
591 were enabled before (new state doesn't matter), while cache admin
592 is interested in windows that become enabled (old state doesn't
593 matter).
594 Does this mean it makes sense to have separate VRAMWindow like
595 classes for each category?
596 Note: In the future, sprites may switch category, or fall in both.
597 */
598 }
599
600 void setSizeMask(EmuTime::param time);
601
602private:
605 VDP& vdp;
606
609 Ram data;
610
616 class LogicalVRAMDebuggable final : public SimpleDebuggable {
617 public:
618 explicit LogicalVRAMDebuggable(VDP& vdp);
619 [[nodiscard]] byte read(unsigned address, EmuTime::param time) override;
620 void write(unsigned address, byte value, EmuTime::param time) override;
621 private:
622 unsigned transform(unsigned address);
623 } logicalVRAMDebug;
624
629 struct PhysicalVRAMDebuggable final : SimpleDebuggable {
630 PhysicalVRAMDebuggable(VDP& vdp, unsigned actualSize);
631 [[nodiscard]] byte read(unsigned address, EmuTime::param time) override;
632 void write(unsigned address, byte value, EmuTime::param time) override;
633 } physicalVRAMDebug;
634
635 // TODO: Renderer field can be removed, if updateDisplayMode
636 // and updateDisplayEnabled are moved back to VDP.
637 // Is that a good idea?
638 Renderer* renderer;
639
640 VDPCmdEngine* cmdEngine;
641 SpriteChecker* spriteChecker;
642
647 #ifdef DEBUG
648 EmuTime vramTime;
649 #endif
650
655 unsigned sizeMask;
656
660 const unsigned actualSize;
661
664 bool vrMode;
665
666public:
676};
677
678} // namespace openmsx
679
680#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:24
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:387
void updateSpritesEnabled(bool enabled, EmuTime::param time)
Used by the VDP to signal sprites enabled changes.
Definition: VDPVRAM.cc:160
VRAMWindow spriteAttribTable
Definition: VDPVRAM.hh:674
void clear()
Initialize VRAM content to power-up state.
Definition: VDPVRAM.cc:131
VDPVRAM(const VDPVRAM &)=delete
void cmdWrite(unsigned address, byte value, EmuTime::param time)
Write a byte from the command engine.
Definition: VDPVRAM.hh:413
VDPVRAM & operator=(const VDPVRAM &)=delete
void setRenderer(Renderer *renderer, EmuTime::param time)
Definition: VDPVRAM.cc:219
VRAMWindow colorTable
Definition: VDPVRAM.hh:670
void updateVRMode(bool mode, EmuTime::param time)
Change between VR=0 and VR=1 mode.
Definition: VDPVRAM.cc:196
VRAMWindow cmdReadWindow
Definition: VDPVRAM.hh:667
VRAMWindow bitmapCacheWindow
Definition: VDPVRAM.hh:673
void updateDisplayEnabled(bool enabled, EmuTime::param time)
Used by the VDP to signal display enabled changes.
Definition: VDPVRAM.cc:152
void setSpriteChecker(SpriteChecker *newSpriteChecker)
Necessary because of circular dependencies.
Definition: VDPVRAM.hh:533
void updateDisplayMode(DisplayMode mode, bool cmdBit, EmuTime::param time)
Used by the VDP to signal display mode changes.
Definition: VDPVRAM.cc:144
VRAMWindow bitmapVisibleWindow
Definition: VDPVRAM.hh:672
void sync(EmuTime::param time)
Update VRAM state to specified moment in time.
Definition: VDPVRAM.hh:402
byte cpuRead(unsigned address, EmuTime::param time)
Read a byte from VRAM though the CPU interface.
Definition: VDPVRAM.hh:474
void serialize(Archive &ar, unsigned version)
Definition: VDPVRAM.cc:315
unsigned getSize() const
Returns the size of VRAM in bytes.
Definition: VDPVRAM.hh:527
VRAMWindow spritePatternTable
Definition: VDPVRAM.hh:675
void cpuWrite(unsigned address, byte value, EmuTime::param time)
Write a byte to VRAM through the CPU interface.
Definition: VDPVRAM.hh:438
VRAMWindow patternTable
Definition: VDPVRAM.hh:671
VRAMWindow cmdWriteWindow
Definition: VDPVRAM.hh:668
void setCmdEngine(VDPCmdEngine *newCmdEngine)
Necessary because of circular dependencies.
Definition: VDPVRAM.hh:539
void change4k8kMapping(bool mapping8k)
TMS99x8 VRAM can be mapped in two ways.
Definition: VDPVRAM.cc:232
VRAMWindow nameTable
Definition: VDPVRAM.hh:669
Unified implementation of MSX Video Display Processors (VDPs).
Definition: VDP.hh:64
bool isInsideFrame(EmuTime::param time) const
Is the given timestamp inside the current frame? Mainly useful for debugging, because relevant timest...
Definition: VDP.hh:517
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
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
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:306
byte readNP(unsigned index) const
Reads a byte from VRAM in its current state.
Definition: VDPVRAM.hh:254
bool hasObserver() const
Is there an observer registered for this window?
Definition: VDPVRAM.hh:271
VRAMWindow & operator=(const VRAMWindow &)=delete
bool isInside(unsigned address) const
Test whether an address is inside this window.
Definition: VDPVRAM.hh:297
void serialize(Archive &ar, unsigned version)
Definition: VDPVRAM.cc:302
void setMask(unsigned newBaseMask, unsigned newIndexMask, EmuTime::param time)
Sets the mask and enables this window.
Definition: VDPVRAM.hh:162
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:316
byte readPlanar(unsigned index) const
Similar to readNP, but now with planar addressing.
Definition: VDPVRAM.hh:262
VRAMWindow(const VRAMWindow &)=delete
unsigned getMask() const
Gets the mask for this window.
Definition: VDPVRAM.hh:145
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:234
void setObserver(VRAMObserver *newObserver)
Register an observer on this VRAM window.
Definition: VDPVRAM.hh:280
std::span< const byte, size > getReadArea(unsigned index) const
Gets a span of a contiguous part of the VRAM.
Definition: VDPVRAM.hh:217
void resetObserver()
Unregister the observer of this VRAM window.
Definition: VDPVRAM.hh:286
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:31
This file implemented 3 utility functions:
Definition: Autofire.cc:9
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
Ram
Definition: Ram.cc:108
auto transform(InputRange &&range, OutputIter out, UnaryOperation op)
Definition: ranges.hh:251
size_t size(std::string_view utf8)