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