openMSX
PixelRenderer.cc
Go to the documentation of this file.
1 /*
2 TODO:
3 - Implement blinking (of page mask) in bitmap modes.
4 */
5 
6 #include "PixelRenderer.hh"
7 #include "Rasterizer.hh"
8 #include "PostProcessor.hh"
9 #include "Display.hh"
10 #include "VideoSystem.hh"
11 #include "RenderSettings.hh"
12 #include "VideoSourceSetting.hh"
13 #include "IntegerSetting.hh"
14 #include "BooleanSetting.hh"
15 #include "EnumSetting.hh"
16 #include "VDP.hh"
17 #include "VDPVRAM.hh"
18 #include "SpriteChecker.hh"
19 #include "EventDistributor.hh"
20 #include "FinishFrameEvent.hh"
21 #include "RealTime.hh"
22 #include "MSXMotherBoard.hh"
23 #include "Reactor.hh"
24 #include "Timer.hh"
25 #include "unreachable.hh"
26 #include <algorithm>
27 #include <cassert>
28 
29 namespace openmsx {
30 
31 void PixelRenderer::draw(
32  int startX, int startY, int endX, int endY, DrawType drawType, bool atEnd)
33 {
34  if (drawType == DRAW_BORDER) {
35  rasterizer->drawBorder(startX, startY, endX, endY);
36  } else {
37  assert(drawType == DRAW_DISPLAY);
38 
39  // Calculate display coordinates.
40  int zero = vdp.getLineZero();
41  int displayX = (startX - vdp.getLeftSprites()) / 2;
42  int displayY = startY - zero;
43  if (!vdp.getDisplayMode().isTextMode()) {
44  displayY += vdp.getVerticalScroll();
45  } else {
46  // this is not what the real VDP does, but it is good
47  // enough for "Boring scroll" demo part of "Relax"
48  displayY = (displayY & 7) | (textModeCounter * 8);
49  if (atEnd && (drawType == DRAW_DISPLAY)) {
50  int low = std::max(0, (startY - zero)) / 8;
51  int high = std::max(0, (endY - zero)) / 8;
52  textModeCounter += (high - low);
53  }
54  }
55 
56  displayY &= 255; // Page wrap.
57  int displayWidth = (endX - (startX & ~1)) / 2;
58  int displayHeight = endY - startY;
59 
60  assert(0 <= displayX);
61  assert(displayX + displayWidth <= 512);
62 
63  rasterizer->drawDisplay(
64  startX, startY,
65  displayX - vdp.getHorizontalScrollLow() * 2, displayY,
66  displayWidth, displayHeight
67  );
68  if (vdp.spritesEnabled() &&
69  !renderSettings.getDisableSprites().getValue()) {
70  rasterizer->drawSprites(
71  startX, startY,
72  displayX / 2, displayY,
73  (displayWidth + 1) / 2, displayHeight);
74  }
75  }
76 }
77 
78 void PixelRenderer::subdivide(
79  int startX, int startY, int endX, int endY, int clipL, int clipR,
80  DrawType drawType )
81 {
82  // Partial first line.
83  if (startX > clipL) {
84  bool atEnd = (startY != endY) || (endX >= clipR);
85  if (startX < clipR) {
86  draw(startX, startY, (atEnd ? clipR : endX),
87  startY + 1, drawType, atEnd);
88  }
89  if (startY == endY) return;
90  startY++;
91  }
92  // Partial last line.
93  bool drawLast = false;
94  if (endX >= clipR) {
95  endY++;
96  } else if (endX > clipL) {
97  drawLast = true;
98  }
99  // Full middle lines.
100  if (startY < endY) {
101  draw(clipL, startY, clipR, endY, drawType, true);
102  }
103  // Actually draw last line if necessary.
104  // The point of keeping top-to-bottom draw order is that it increases
105  // the locality of memory references, which generally improves cache
106  // hit rates.
107  if (drawLast) draw(clipL, endY, endX, endY + 1, drawType, false);
108 }
109 
111  : vdp(vdp_), vram(vdp.getVRAM())
112  , eventDistributor(vdp.getReactor().getEventDistributor())
113  , realTime(vdp.getMotherBoard().getRealTime())
114  , renderSettings(display.getRenderSettings())
115  , videoSourceSetting(vdp.getMotherBoard().getVideoSource())
116  , spriteChecker(vdp.getSpriteChecker())
117  , rasterizer(display.getVideoSystem().createRasterizer(vdp))
118 {
119  // In case of loadstate we can't yet query any state from the VDP
120  // (because that object is not yet fully deserialized). But
121  // VDP::serialize() will call Renderer::reInit() again when it is
122  // safe to query.
123  reInit();
124 
125  finishFrameDuration = 0;
126  frameSkipCounter = 999; // force drawing of frame
127  prevRenderFrame = false;
128 
129  renderSettings.getMaxFrameSkip().attach(*this);
130  renderSettings.getMinFrameSkip().attach(*this);
131 }
132 
134 {
135  renderSettings.getMinFrameSkip().detach(*this);
136  renderSettings.getMaxFrameSkip().detach(*this);
137 }
138 
140 {
141  return rasterizer->getPostProcessor();
142 }
143 
145 {
146  // Don't draw before frameStart() is called.
147  // This for example can happen after a loadstate or after switching
148  // renderer in the middle of a frame.
149  renderFrame = false;
150 
151  rasterizer->reset();
152  displayEnabled = vdp.isDisplayEnabled();
153 }
154 
156 {
157  sync(time, true);
158  displayEnabled = enabled;
159 }
160 
162 {
163  if (!rasterizer->isActive()) {
164  frameSkipCounter = 999;
165  renderFrame = false;
166  prevRenderFrame = false;
167  return;
168  }
169  prevRenderFrame = renderFrame;
170  if (vdp.isInterlaced() && renderSettings.getDeinterlace().getValue() &&
171  vdp.getEvenOdd() && vdp.isEvenOddEnabled()) {
172  // deinterlaced odd frame, do same as even frame
173  } else {
174  if (frameSkipCounter <
175  renderSettings.getMinFrameSkip().getValue()) {
176  ++frameSkipCounter;
177  renderFrame = false;
178  } else if (frameSkipCounter >=
179  renderSettings.getMaxFrameSkip().getValue()) {
180  frameSkipCounter = 0;
181  renderFrame = true;
182  } else {
183  ++frameSkipCounter;
184  if (rasterizer->isRecording()) {
185  renderFrame = true;
186  } else {
187  renderFrame = realTime.timeLeft(
188  unsigned(finishFrameDuration), time);
189  }
190  if (renderFrame) {
191  frameSkipCounter = 0;
192  }
193  }
194  }
195  if (!renderFrame) return;
196 
197  rasterizer->frameStart(time);
198 
199  accuracy = renderSettings.getAccuracy().getValue();
200 
201  nextX = 0;
202  nextY = 0;
203  // This is not what the real VDP does, but it is good enough
204  // for the "Boring scroll" demo part of ANMA's "Relax" demo.
205  textModeCounter = 0;
206 }
207 
209 {
210  bool skipEvent = !renderFrame;
211  if (renderFrame) {
212  // Render changes from this last frame.
213  sync(time, true);
214 
215  // Let underlying graphics system finish rendering this frame.
216  auto time1 = Timer::getTime();
217  rasterizer->frameEnd();
218  auto time2 = Timer::getTime();
219  auto current = time2 - time1;
220  const double ALPHA = 0.2;
221  finishFrameDuration = finishFrameDuration * (1 - ALPHA) +
222  current * ALPHA;
223 
224  if (vdp.isInterlaced() && vdp.isEvenOddEnabled() &&
225  renderSettings.getDeinterlace().getValue() &&
226  !prevRenderFrame) {
227  // dont send event in deinterlace mode when
228  // previous frame was not rendered
229  skipEvent = true;
230  }
231  }
232  eventDistributor.distributeEvent(
233  std::make_shared<FinishFrameEvent>(
234  rasterizer->getPostProcessor()->getVideoSource(),
235  videoSourceSetting.getValue(),
236  skipEvent));
237 }
238 
240  byte /*scroll*/, EmuTime::param time
241 ) {
242  if (displayEnabled) sync(time);
243 }
244 
246  byte /*scroll*/, EmuTime::param time
247 ) {
248  if (displayEnabled) sync(time);
249 }
250 
252  bool /*masked*/, EmuTime::param time
253 ) {
254  if (displayEnabled) sync(time);
255 }
256 
258  bool /*multiPage*/, EmuTime::param time
259 ) {
260  if (displayEnabled) sync(time);
261 }
262 
264  bool enabled, EmuTime::param time)
265 {
266  if (displayEnabled) sync(time);
267  rasterizer->setTransparency(enabled);
268 }
269 
271  const RawFrame* videoSource, EmuTime::param time)
272 {
273  if (displayEnabled) sync(time);
274  rasterizer->setSuperimposeVideoFrame(videoSource);
275 }
276 
278  int /*color*/, EmuTime::param time)
279 {
280  if (displayEnabled) sync(time);
281 }
282 
284  int color, EmuTime::param time)
285 {
286  sync(time);
288  rasterizer->setBackgroundColor(color);
289  }
290 }
291 
293  int /*color*/, EmuTime::param time)
294 {
295  if (displayEnabled) sync(time);
296 }
297 
299  int /*color*/, EmuTime::param time)
300 {
301  if (displayEnabled) sync(time);
302 }
303 
305  bool /*enabled*/, EmuTime::param /*time*/)
306 {
307  // TODO: When the sync call is enabled, the screen flashes on
308  // every call to this method.
309  // I don't know why exactly, but it's probably related to
310  // being called at frame start.
311  //sync(time);
312 }
313 
315  int index, int grb, EmuTime::param time)
316 {
317  if (displayEnabled) {
318  sync(time);
319  } else {
320  // Only sync if border color changed.
321  DisplayMode mode = vdp.getDisplayMode();
322  if (mode.getBase() == DisplayMode::GRAPHIC5) {
323  int bgColor = vdp.getBackgroundColor();
324  if (index == (bgColor & 3) || (index == (bgColor >> 2))) {
325  sync(time);
326  }
327  } else if (mode.getByte() != DisplayMode::GRAPHIC7) {
328  if (index == vdp.getBackgroundColor()) {
329  sync(time);
330  }
331  }
332  }
333  rasterizer->setPalette(index, grb);
334 }
335 
337  int /*scroll*/, EmuTime::param time)
338 {
339  if (displayEnabled) sync(time);
340 }
341 
343  int /*adjust*/, EmuTime::param time)
344 {
345  if (displayEnabled) sync(time);
346 }
347 
349  DisplayMode mode, EmuTime::param time)
350 {
351  // Sync if in display area or if border drawing process changes.
352  DisplayMode oldMode = vdp.getDisplayMode();
353  if (displayEnabled
354  || oldMode.getByte() == DisplayMode::GRAPHIC5
355  || oldMode.getByte() == DisplayMode::GRAPHIC7
356  || mode.getByte() == DisplayMode::GRAPHIC5
357  || mode.getByte() == DisplayMode::GRAPHIC7) {
358  sync(time, true);
359  }
360  rasterizer->setDisplayMode(mode);
361 }
362 
364  int /*addr*/, EmuTime::param time)
365 {
366  if (displayEnabled) sync(time);
367 }
368 
370  int /*addr*/, EmuTime::param time)
371 {
372  if (displayEnabled) sync(time);
373 }
374 
376  int /*addr*/, EmuTime::param time)
377 {
378  if (displayEnabled) sync(time);
379 }
380 
382  bool /*enabled*/, EmuTime::param time
383 ) {
384  if (displayEnabled) sync(time);
385 }
386 
387 static inline bool overlap(
388  int displayY0, // start of display region, inclusive
389  int displayY1, // end of display region, exclusive
390  int vramLine0, // start of VRAM region, inclusive
391  int vramLine1 // end of VRAM region, exclusive
392  // Note: Display region can wrap around: 256 -> 0.
393  // VRAM region cannot wrap around.
394 ) {
395  if (displayY0 <= displayY1) {
396  if (vramLine1 > displayY0) {
397  if (vramLine0 <= displayY1) return true;
398  }
399  } else {
400  if (vramLine1 > displayY0) return true;
401  if (vramLine0 <= displayY1) return true;
402  }
403  return false;
404 }
405 
406 inline bool PixelRenderer::checkSync(int offset, EmuTime::param time)
407 {
408  // TODO: Because range is entire VRAM, offset == address.
409 
410  // If display is disabled, VRAM changes will not affect the
411  // renderer output, therefore sync is not necessary.
412  // TODO: Have bitmapVisibleWindow disabled in this case.
413  if (!displayEnabled) return false;
414  //if (frameSkipCounter != 0) return false; // TODO
415  if (accuracy == RenderSettings::ACC_SCREEN) return false;
416 
417  // Calculate what display lines are scanned between current
418  // renderer time and update-to time.
419  // Note: displayY1 is inclusive.
420  int deltaY = vdp.getVerticalScroll() - vdp.getLineZero();
421  int limitY = vdp.getTicksThisFrame(time) / VDP::TICKS_PER_LINE;
422  int displayY0 = (nextY + deltaY) & 255;
423  int displayY1 = (limitY + deltaY) & 255;
424 
425  switch(vdp.getDisplayMode().getBase()) {
428  if (vram.colorTable.isInside(offset)) {
429  int vramQuarter = (offset & 0x1800) >> 11;
430  int mask = (vram.colorTable.getMask() & 0x1800) >> 11;
431  for (int i = 0; i < 4; i++) {
432  if ( (i & mask) == vramQuarter
433  && overlap(displayY0, displayY1, i * 64, (i + 1) * 64) ) {
434  /*fprintf(stderr,
435  "color table: %05X %04X - quarter %d\n",
436  offset, offset & 0x1FFF, i
437  );*/
438  return true;
439  }
440  }
441  }
442  if (vram.patternTable.isInside(offset)) {
443  int vramQuarter = (offset & 0x1800) >> 11;
444  int mask = (vram.patternTable.getMask() & 0x1800) >> 11;
445  for (int i = 0; i < 4; i++) {
446  if ( (i & mask) == vramQuarter
447  && overlap(displayY0, displayY1, i * 64, (i + 1) * 64) ) {
448  /*fprintf(stderr,
449  "pattern table: %05X %04X - quarter %d\n",
450  offset, offset & 0x1FFF, i
451  );*/
452  return true;
453  }
454  }
455  }
456  if (vram.nameTable.isInside(offset)) {
457  int vramLine = ((offset & 0x3FF) / 32) * 8;
458  if (overlap(displayY0, displayY1, vramLine, vramLine + 8)) {
459  /*fprintf(stderr,
460  "name table: %05X %03X - line %d\n",
461  offset, offset & 0x3FF, vramLine
462  );*/
463  return true;
464  }
465  }
466  return false;
468  case DisplayMode::GRAPHIC5: {
469  // Is the address inside the visual page(s)?
470  // TODO: Also look at which lines are touched inside pages.
471  int visiblePage = vram.nameTable.getMask()
472  & (0x10000 | (vdp.getEvenOddMask() << 7));
473  if (vdp.isMultiPageScrolling()) {
474  return (offset & 0x18000) == visiblePage
475  || (offset & 0x18000) == (visiblePage & 0x10000);
476  } else {
477  return (offset & 0x18000) == visiblePage;
478  }
479  }
482  return true; // TODO: Implement better detection.
483  default:
484  // Range unknown; assume full range.
485  return vram.nameTable.isInside(offset)
486  || vram.colorTable.isInside(offset)
487  || vram.patternTable.isInside(offset);
488  }
489 }
490 
491 void PixelRenderer::updateVRAM(unsigned offset, EmuTime::param time)
492 {
493  // Note: No need to sync if display is disabled, because then the
494  // output does not depend on VRAM (only on background color).
495  if (renderFrame && displayEnabled && checkSync(offset, time)) {
496  //fprintf(stderr, "vram sync @ line %d\n",
497  // vdp.getTicksThisFrame(time) / VDP::TICKS_PER_LINE);
498  renderUntil(time);
499  }
500 }
501 
502 void PixelRenderer::updateWindow(bool /*enabled*/, EmuTime::param /*time*/)
503 {
504  // The bitmapVisibleWindow has moved to a different area.
505  // This update is redundant: Renderer will be notified in another way
506  // as well (updateDisplayEnabled or updateNameBase, for example).
507  // TODO: Can this be used as the main update method instead?
508 }
509 
510 void PixelRenderer::sync(EmuTime::param time, bool force)
511 {
512  if (!renderFrame) return;
513 
514  // Synchronisation is done in two phases:
515  // 1. update VRAM
516  // 2. update other subsystems
517  // Note that as part of step 1, type 2 updates can be triggered.
518  // Executing step 2 takes care of the subsystem changes that occur
519  // after the last VRAM update.
520  // This scheme makes sure type 2 routines such as renderUntil and
521  // checkUntil are not re-entered, which was causing major pain in
522  // the past.
523  // TODO: I wonder if it's possible to enforce this synchronisation
524  // scheme at a higher level. Probably. But how...
525  //if ((frameSkipCounter == 0) && TODO
526  if (accuracy != RenderSettings::ACC_SCREEN || force) {
527  vram.sync(time);
528  renderUntil(time);
529  }
530 }
531 
532 void PixelRenderer::renderUntil(EmuTime::param time)
533 {
534  // Translate from time to pixel position.
535  int limitTicks = vdp.getTicksThisFrame(time);
536  assert(limitTicks <= vdp.getTicksPerFrame());
537  int limitX, limitY;
538  switch (accuracy) {
540  limitX = limitTicks % VDP::TICKS_PER_LINE;
541  limitY = limitTicks / VDP::TICKS_PER_LINE;
542  break;
543  }
546  // Note: I'm not sure the rounding point is optimal.
547  // It used to be based on the left margin, but that doesn't work
548  // because the margin can change which leads to a line being
549  // rendered even though the time doesn't advance.
550  limitX = 0;
551  limitY =
552  (limitTicks + VDP::TICKS_PER_LINE - 400) / VDP::TICKS_PER_LINE;
553  break;
554  }
555  default:
556  UNREACHABLE;
557  limitX = limitY = 0; // avoid warning
558  }
559 
560  // Stop here if there is nothing to render.
561  // This ensures that no pixels are rendered in a series of updates that
562  // happen at exactly the same time; the VDP subsystem states may be
563  // inconsistent until all updates are performed.
564  // Also it is a small performance optimisation.
565  if (limitX == nextX && limitY == nextY) return;
566 
567  if (displayEnabled) {
568  if (vdp.spritesEnabled()) {
569  // Update sprite checking, so that rasterizer can call getSprites.
570  spriteChecker.checkUntil(time);
571  }
572 
573  // Calculate start and end of borders in ticks since start of line.
574  // The 0..7 extra horizontal scroll low pixels should be drawn in
575  // border color. These will be drawn together with the border,
576  // but sprites above these pixels are clipped at the actual border
577  // rather than the end of the border colored area.
578  // TODO: Move these calculations and getDisplayLeft() to VDP.
579  int borderL = vdp.getLeftBorder();
580  int displayL =
581  vdp.isBorderMasked() ? borderL : vdp.getLeftBackground();
582  int borderR = vdp.getRightBorder();
583 
584  // Left border.
585  subdivide(nextX, nextY, limitX, limitY,
586  0, displayL, DRAW_BORDER );
587  // Display area.
588  subdivide(nextX, nextY, limitX, limitY,
589  displayL, borderR, DRAW_DISPLAY );
590  // Right border.
591  subdivide(nextX, nextY, limitX, limitY,
592  borderR, VDP::TICKS_PER_LINE, DRAW_BORDER );
593  } else {
594  subdivide(nextX, nextY, limitX, limitY,
595  0, VDP::TICKS_PER_LINE, DRAW_BORDER );
596  }
597 
598  nextX = limitX;
599  nextY = limitY;
600 }
601 
602 void PixelRenderer::update(const Setting& setting)
603 {
604  if (&setting == &renderSettings.getMinFrameSkip()
605  || &setting == &renderSettings.getMaxFrameSkip() ) {
606  // Force drawing of frame.
607  frameSkipCounter = 999;
608  } else {
609  UNREACHABLE;
610  }
611 }
612 
613 } // namespace openmsx