123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 |
- /*
- TODO:
- - Implement blinking (of page mask) in bitmap modes.
- */
- #include "PixelRenderer.hh"
- #include "Rasterizer.hh"
- #include "PostProcessor.hh"
- #include "Display.hh"
- #include "VideoSystem.hh"
- #include "RenderSettings.hh"
- #include "VideoSourceSetting.hh"
- #include "IntegerSetting.hh"
- #include "VDP.hh"
- #include "VDPVRAM.hh"
- #include "SpriteChecker.hh"
- #include "EventDistributor.hh"
- #include "FinishFrameEvent.hh"
- #include "RealTime.hh"
- #include "SpeedManager.hh"
- #include "ThrottleManager.hh"
- #include "GlobalSettings.hh"
- #include "MSXMotherBoard.hh"
- #include "Reactor.hh"
- #include "Timer.hh"
- #include "one_of.hh"
- #include "unreachable.hh"
- #include <algorithm>
- #include <cassert>
- #include <cmath>
- namespace openmsx {
- void PixelRenderer::draw(
- int startX, int startY, int endX, int endY, DrawType drawType, bool atEnd)
- {
- if (drawType == DRAW_BORDER) {
- rasterizer->drawBorder(startX, startY, endX, endY);
- } else {
- assert(drawType == DRAW_DISPLAY);
- // Calculate display coordinates.
- int zero = vdp.getLineZero();
- int displayX = (startX - vdp.getLeftSprites()) / 2;
- int displayY = startY - zero;
- if (!vdp.getDisplayMode().isTextMode()) {
- displayY += vdp.getVerticalScroll();
- } else {
- // this is not what the real VDP does, but it is good
- // enough for "Boring scroll" demo part of "Relax"
- displayY = (displayY & 7) | (textModeCounter * 8);
- if (atEnd && (drawType == DRAW_DISPLAY)) {
- int low = std::max(0, (startY - zero)) / 8;
- int high = std::max(0, (endY - zero)) / 8;
- textModeCounter += (high - low);
- }
- }
- displayY &= 255; // Page wrap.
- int displayWidth = (endX - (startX & ~1)) / 2;
- int displayHeight = endY - startY;
- assert(0 <= displayX);
- assert(displayX + displayWidth <= 512);
- rasterizer->drawDisplay(
- startX, startY,
- displayX - vdp.getHorizontalScrollLow() * 2, displayY,
- displayWidth, displayHeight
- );
- if (vdp.spritesEnabled() && !renderSettings.getDisableSprites()) {
- rasterizer->drawSprites(
- startX, startY,
- displayX / 2, displayY,
- (displayWidth + 1) / 2, displayHeight);
- }
- }
- }
- void PixelRenderer::subdivide(
- int startX, int startY, int endX, int endY, int clipL, int clipR,
- DrawType drawType )
- {
- // Partial first line.
- if (startX > clipL) {
- bool atEnd = (startY != endY) || (endX >= clipR);
- if (startX < clipR) {
- draw(startX, startY, (atEnd ? clipR : endX),
- startY + 1, drawType, atEnd);
- }
- if (startY == endY) return;
- startY++;
- }
- // Partial last line.
- bool drawLast = false;
- if (endX >= clipR) {
- endY++;
- } else if (endX > clipL) {
- drawLast = true;
- }
- // Full middle lines.
- if (startY < endY) {
- draw(clipL, startY, clipR, endY, drawType, true);
- }
- // Actually draw last line if necessary.
- // The point of keeping top-to-bottom draw order is that it increases
- // the locality of memory references, which generally improves cache
- // hit rates.
- if (drawLast) draw(clipL, endY, endX, endY + 1, drawType, false);
- }
- PixelRenderer::PixelRenderer(VDP& vdp_, Display& display)
- : vdp(vdp_), vram(vdp.getVRAM())
- , eventDistributor(vdp.getReactor().getEventDistributor())
- , realTime(vdp.getMotherBoard().getRealTime())
- , speedManager(
- vdp.getReactor().getGlobalSettings().getSpeedManager())
- , throttleManager(
- vdp.getReactor().getGlobalSettings().getThrottleManager())
- , renderSettings(display.getRenderSettings())
- , videoSourceSetting(vdp.getMotherBoard().getVideoSource())
- , spriteChecker(vdp.getSpriteChecker())
- , rasterizer(display.getVideoSystem().createRasterizer(vdp))
- {
- // In case of loadstate we can't yet query any state from the VDP
- // (because that object is not yet fully deserialized). But
- // VDP::serialize() will call Renderer::reInit() again when it is
- // safe to query.
- reInit();
- finishFrameDuration = 0;
- frameSkipCounter = 999; // force drawing of frame
- prevRenderFrame = false;
- renderSettings.getMaxFrameSkipSetting().attach(*this);
- renderSettings.getMinFrameSkipSetting().attach(*this);
- }
- PixelRenderer::~PixelRenderer()
- {
- renderSettings.getMinFrameSkipSetting().detach(*this);
- renderSettings.getMaxFrameSkipSetting().detach(*this);
- }
- PostProcessor* PixelRenderer::getPostProcessor() const
- {
- return rasterizer->getPostProcessor();
- }
- void PixelRenderer::reInit()
- {
- // Don't draw before frameStart() is called.
- // This for example can happen after a loadstate or after switching
- // renderer in the middle of a frame.
- renderFrame = false;
- rasterizer->reset();
- displayEnabled = vdp.isDisplayEnabled();
- }
- void PixelRenderer::updateDisplayEnabled(bool enabled, EmuTime::param time)
- {
- sync(time, true);
- displayEnabled = enabled;
- }
- void PixelRenderer::frameStart(EmuTime::param time)
- {
- if (!rasterizer->isActive()) {
- frameSkipCounter = 999;
- renderFrame = false;
- prevRenderFrame = false;
- paintFrame = false;
- return;
- }
- prevRenderFrame = renderFrame;
- if (vdp.isInterlaced() && renderSettings.getDeinterlace()
- && vdp.getEvenOdd() && vdp.isEvenOddEnabled()) {
- // Deinterlaced odd frame: do same as even frame.
- paintFrame = prevRenderFrame;
- } else if (throttleManager.isThrottled()) {
- // Note: min/maxFrameSkip control the number of skipped frames, but
- // for every series of skipped frames there is also one painted
- // frame, so our boundary checks are offset by one.
- int counter = int(frameSkipCounter);
- if (counter < renderSettings.getMinFrameSkip()) {
- paintFrame = false;
- } else if (counter > renderSettings.getMaxFrameSkip()) {
- paintFrame = true;
- } else {
- paintFrame = realTime.timeLeft(
- unsigned(finishFrameDuration), time);
- }
- frameSkipCounter += 1.0f / float(speedManager.getSpeed());
- } else {
- // We need to render a frame every now and then,
- // to show the user what is happening.
- paintFrame = (Timer::getTime() - lastPaintTime) >= 100000; // 10 fps
- }
- if (paintFrame) {
- frameSkipCounter = std::remainder(frameSkipCounter, 1.0f);
- } else if (!rasterizer->isRecording()) {
- renderFrame = false;
- return;
- }
- renderFrame = true;
- rasterizer->frameStart(time);
- accuracy = renderSettings.getAccuracy();
- nextX = 0;
- nextY = 0;
- // This is not what the real VDP does, but it is good enough
- // for the "Boring scroll" demo part of ANMA's "Relax" demo.
- textModeCounter = 0;
- }
- void PixelRenderer::frameEnd(EmuTime::param time)
- {
- if (renderFrame) {
- // Render changes from this last frame.
- sync(time, true);
- // Let underlying graphics system finish rendering this frame.
- auto time1 = Timer::getTime();
- rasterizer->frameEnd();
- auto time2 = Timer::getTime();
- auto current = time2 - time1;
- const float ALPHA = 0.2f;
- finishFrameDuration = finishFrameDuration * (1 - ALPHA) +
- current * ALPHA;
- if (vdp.isInterlaced() && vdp.isEvenOddEnabled()
- && renderSettings.getDeinterlace() && !prevRenderFrame) {
- // Don't paint in deinterlace mode when previous frame
- // was not rendered.
- paintFrame = false;
- }
- if (paintFrame) {
- lastPaintTime = time2;
- }
- }
- if (vdp.getMotherBoard().isActive() &&
- !vdp.getMotherBoard().isFastForwarding()) {
- eventDistributor.distributeEvent(
- std::make_shared<FinishFrameEvent>(
- rasterizer->getPostProcessor()->getVideoSource(),
- videoSourceSetting.getSource(),
- !paintFrame));
- }
- }
- void PixelRenderer::updateHorizontalScrollLow(
- byte scroll, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- rasterizer->setHorizontalScrollLow(scroll);
- }
- void PixelRenderer::updateHorizontalScrollHigh(
- byte /*scroll*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateBorderMask(
- bool masked, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- rasterizer->setBorderMask(masked);
- }
- void PixelRenderer::updateMultiPage(
- bool /*multiPage*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateTransparency(
- bool enabled, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- rasterizer->setTransparency(enabled);
- }
- void PixelRenderer::updateSuperimposing(
- const RawFrame* videoSource, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- rasterizer->setSuperimposeVideoFrame(videoSource);
- }
- void PixelRenderer::updateForegroundColor(
- int /*color*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateBackgroundColor(
- int color, EmuTime::param time)
- {
- sync(time);
- rasterizer->setBackgroundColor(color);
- }
- void PixelRenderer::updateBlinkForegroundColor(
- int /*color*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateBlinkBackgroundColor(
- int /*color*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateBlinkState(
- bool /*enabled*/, EmuTime::param /*time*/)
- {
- // TODO: When the sync call is enabled, the screen flashes on
- // every call to this method.
- // I don't know why exactly, but it's probably related to
- // being called at frame start.
- //sync(time);
- }
- void PixelRenderer::updatePalette(
- int index, int grb, EmuTime::param time)
- {
- if (displayEnabled) {
- sync(time);
- } else {
- // Only sync if border color changed.
- DisplayMode mode = vdp.getDisplayMode();
- if (mode.getBase() == DisplayMode::GRAPHIC5) {
- int bgColor = vdp.getBackgroundColor();
- if (index == one_of(bgColor & 3, bgColor >> 2)) {
- sync(time);
- }
- } else if (mode.getByte() != DisplayMode::GRAPHIC7) {
- if (index == vdp.getBackgroundColor()) {
- sync(time);
- }
- }
- }
- rasterizer->setPalette(index, grb);
- }
- void PixelRenderer::updateVerticalScroll(
- int /*scroll*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateHorizontalAdjust(
- int adjust, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- rasterizer->setHorizontalAdjust(adjust);
- }
- void PixelRenderer::updateDisplayMode(
- DisplayMode mode, EmuTime::param time)
- {
- // Sync if in display area or if border drawing process changes.
- DisplayMode oldMode = vdp.getDisplayMode();
- if (displayEnabled
- || oldMode.getByte() == DisplayMode::GRAPHIC5
- || oldMode.getByte() == DisplayMode::GRAPHIC7
- || mode.getByte() == DisplayMode::GRAPHIC5
- || mode.getByte() == DisplayMode::GRAPHIC7) {
- sync(time, true);
- }
- rasterizer->setDisplayMode(mode);
- }
- void PixelRenderer::updateNameBase(
- int /*addr*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updatePatternBase(
- int /*addr*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateColorBase(
- int /*addr*/, EmuTime::param time)
- {
- if (displayEnabled) sync(time);
- }
- void PixelRenderer::updateSpritesEnabled(
- bool /*enabled*/, EmuTime::param time
- ) {
- if (displayEnabled) sync(time);
- }
- static inline bool overlap(
- int displayY0, // start of display region, inclusive
- int displayY1, // end of display region, exclusive
- int vramLine0, // start of VRAM region, inclusive
- int vramLine1 // end of VRAM region, exclusive
- // Note: Display region can wrap around: 256 -> 0.
- // VRAM region cannot wrap around.
- ) {
- if (displayY0 <= displayY1) {
- if (vramLine1 > displayY0) {
- if (vramLine0 <= displayY1) return true;
- }
- } else {
- if (vramLine1 > displayY0) return true;
- if (vramLine0 <= displayY1) return true;
- }
- return false;
- }
- inline bool PixelRenderer::checkSync(int offset, EmuTime::param time)
- {
- // TODO: Because range is entire VRAM, offset == address.
- // If display is disabled, VRAM changes will not affect the
- // renderer output, therefore sync is not necessary.
- // TODO: Have bitmapVisibleWindow disabled in this case.
- if (!displayEnabled) return false;
- //if (frameSkipCounter != 0) return false; // TODO
- if (accuracy == RenderSettings::ACC_SCREEN) return false;
- // Calculate what display lines are scanned between current
- // renderer time and update-to time.
- // Note: displayY1 is inclusive.
- int deltaY = vdp.getVerticalScroll() - vdp.getLineZero();
- int limitY = vdp.getTicksThisFrame(time) / VDP::TICKS_PER_LINE;
- int displayY0 = (nextY + deltaY) & 255;
- int displayY1 = (limitY + deltaY) & 255;
- switch(vdp.getDisplayMode().getBase()) {
- case DisplayMode::GRAPHIC2:
- case DisplayMode::GRAPHIC3:
- if (vram.colorTable.isInside(offset)) {
- int vramQuarter = (offset & 0x1800) >> 11;
- int mask = (vram.colorTable.getMask() & 0x1800) >> 11;
- for (int i = 0; i < 4; i++) {
- if ( (i & mask) == vramQuarter
- && overlap(displayY0, displayY1, i * 64, (i + 1) * 64) ) {
- /*fprintf(stderr,
- "color table: %05X %04X - quarter %d\n",
- offset, offset & 0x1FFF, i
- );*/
- return true;
- }
- }
- }
- if (vram.patternTable.isInside(offset)) {
- int vramQuarter = (offset & 0x1800) >> 11;
- int mask = (vram.patternTable.getMask() & 0x1800) >> 11;
- for (int i = 0; i < 4; i++) {
- if ( (i & mask) == vramQuarter
- && overlap(displayY0, displayY1, i * 64, (i + 1) * 64) ) {
- /*fprintf(stderr,
- "pattern table: %05X %04X - quarter %d\n",
- offset, offset & 0x1FFF, i
- );*/
- return true;
- }
- }
- }
- if (vram.nameTable.isInside(offset)) {
- int vramLine = ((offset & 0x3FF) / 32) * 8;
- if (overlap(displayY0, displayY1, vramLine, vramLine + 8)) {
- /*fprintf(stderr,
- "name table: %05X %03X - line %d\n",
- offset, offset & 0x3FF, vramLine
- );*/
- return true;
- }
- }
- return false;
- case DisplayMode::GRAPHIC4:
- case DisplayMode::GRAPHIC5: {
- if (vdp.isFastBlinkEnabled()) {
- // TODO could be improved
- return true;
- }
- // Is the address inside the visual page(s)?
- // TODO: Also look at which lines are touched inside pages.
- int visiblePage = vram.nameTable.getMask()
- & (0x10000 | (vdp.getEvenOddMask() << 7));
- if (vdp.isMultiPageScrolling()) {
- return (offset & 0x18000) == visiblePage
- || (offset & 0x18000) == (visiblePage & 0x10000);
- } else {
- return (offset & 0x18000) == visiblePage;
- }
- }
- case DisplayMode::GRAPHIC6:
- case DisplayMode::GRAPHIC7:
- return true; // TODO: Implement better detection.
- default:
- // Range unknown; assume full range.
- return vram.nameTable.isInside(offset)
- || vram.colorTable.isInside(offset)
- || vram.patternTable.isInside(offset);
- }
- }
- void PixelRenderer::updateVRAM(unsigned offset, EmuTime::param time)
- {
- // Note: No need to sync if display is disabled, because then the
- // output does not depend on VRAM (only on background color).
- if (renderFrame && displayEnabled && checkSync(offset, time)) {
- //fprintf(stderr, "vram sync @ line %d\n",
- // vdp.getTicksThisFrame(time) / VDP::TICKS_PER_LINE);
- renderUntil(time);
- }
- }
- void PixelRenderer::updateWindow(bool /*enabled*/, EmuTime::param /*time*/)
- {
- // The bitmapVisibleWindow has moved to a different area.
- // This update is redundant: Renderer will be notified in another way
- // as well (updateDisplayEnabled or updateNameBase, for example).
- // TODO: Can this be used as the main update method instead?
- }
- void PixelRenderer::sync(EmuTime::param time, bool force)
- {
- if (!renderFrame) return;
- // Synchronisation is done in two phases:
- // 1. update VRAM
- // 2. update other subsystems
- // Note that as part of step 1, type 2 updates can be triggered.
- // Executing step 2 takes care of the subsystem changes that occur
- // after the last VRAM update.
- // This scheme makes sure type 2 routines such as renderUntil and
- // checkUntil are not re-entered, which was causing major pain in
- // the past.
- // TODO: I wonder if it's possible to enforce this synchronisation
- // scheme at a higher level. Probably. But how...
- //if ((frameSkipCounter == 0) && TODO
- if (accuracy != RenderSettings::ACC_SCREEN || force) {
- vram.sync(time);
- renderUntil(time);
- }
- }
- void PixelRenderer::renderUntil(EmuTime::param time)
- {
- // Translate from time to pixel position.
- int limitTicks = vdp.getTicksThisFrame(time);
- assert(limitTicks <= vdp.getTicksPerFrame());
- int limitX, limitY;
- switch (accuracy) {
- case RenderSettings::ACC_PIXEL: {
- limitX = limitTicks % VDP::TICKS_PER_LINE;
- limitY = limitTicks / VDP::TICKS_PER_LINE;
- break;
- }
- case RenderSettings::ACC_LINE:
- case RenderSettings::ACC_SCREEN: {
- // Note: I'm not sure the rounding point is optimal.
- // It used to be based on the left margin, but that doesn't work
- // because the margin can change which leads to a line being
- // rendered even though the time doesn't advance.
- limitX = 0;
- limitY =
- (limitTicks + VDP::TICKS_PER_LINE - 400) / VDP::TICKS_PER_LINE;
- break;
- }
- default:
- UNREACHABLE;
- limitX = limitY = 0; // avoid warning
- }
- // Stop here if there is nothing to render.
- // This ensures that no pixels are rendered in a series of updates that
- // happen at exactly the same time; the VDP subsystem states may be
- // inconsistent until all updates are performed.
- // Also it is a small performance optimisation.
- if (limitX == nextX && limitY == nextY) return;
- if (displayEnabled) {
- if (vdp.spritesEnabled()) {
- // Update sprite checking, so that rasterizer can call getSprites.
- spriteChecker.checkUntil(time);
- }
- // Calculate start and end of borders in ticks since start of line.
- // The 0..7 extra horizontal scroll low pixels should be drawn in
- // border color. These will be drawn together with the border,
- // but sprites above these pixels are clipped at the actual border
- // rather than the end of the border colored area.
- // TODO: Move these calculations and getDisplayLeft() to VDP.
- int borderL = vdp.getLeftBorder();
- int displayL =
- vdp.isBorderMasked() ? borderL : vdp.getLeftBackground();
- int borderR = vdp.getRightBorder();
- // It's important that right border is drawn last (after left
- // border and display area). See comment in SDLRasterizer::drawBorder().
- // Left border.
- subdivide(nextX, nextY, limitX, limitY,
- 0, displayL, DRAW_BORDER);
- // Display area.
- subdivide(nextX, nextY, limitX, limitY,
- displayL, borderR, DRAW_DISPLAY);
- // Right border.
- subdivide(nextX, nextY, limitX, limitY,
- borderR, VDP::TICKS_PER_LINE, DRAW_BORDER);
- } else {
- subdivide(nextX, nextY, limitX, limitY,
- 0, VDP::TICKS_PER_LINE, DRAW_BORDER);
- }
- nextX = limitX;
- nextY = limitY;
- }
- void PixelRenderer::update(const Setting& setting)
- {
- assert(&setting == one_of(&renderSettings.getMinFrameSkipSetting(),
- &renderSettings.getMaxFrameSkipSetting()));
- (void)setting;
- // Force drawing of frame.
- frameSkipCounter = 999;
- }
- } // namespace openmsx
|