mkxp-freebird/src/graphics.cpp
Jonas Kulla 373b90af00 Graphics: Optimize Viewport effect rendering
Using the kitchen sink plane shader for viewport effects, even
if only a small part of them are active, incurs great performance
loss on mobile, so split the rendering into multiple optional
passes which additionally use the blending hardware for faster
mixing (lerping).
Also, don't mirror the PingPong textures if the viewport effect
covers the entire screen area anyway.
2014-12-31 18:52:19 +01:00

1023 lines
19 KiB
C++

/*
** graphics.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 Jonas Kulla <Nyocurio@gmail.com>
**
** mkxp is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 2 of the License, or
** (at your option) any later version.
**
** mkxp is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with mkxp. If not, see <http://www.gnu.org/licenses/>.
*/
#include "graphics.h"
#include "util.h"
#include "gl-util.h"
#include "sharedstate.h"
#include "config.h"
#include "glstate.h"
#include "shader.h"
#include "scene.h"
#include "quad.h"
#include "eventthread.h"
#include "texpool.h"
#include "bitmap.h"
#include "etc-internal.h"
#include "disposable.h"
#include "intrulist.h"
#include "binding.h"
#include "debugwriter.h"
#include <SDL_video.h>
#include <SDL_timer.h>
#include <SDL_image.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
#include <algorithm>
#define DEF_SCREEN_W (rgssVer == 1 ? 640 : 544)
#define DEF_SCREEN_H (rgssVer == 1 ? 480 : 416)
#define DEF_FRAMERATE (rgssVer == 1 ? 40 : 60)
struct PingPong
{
TEXFBO rt[2];
uint8_t srcInd, dstInd;
int screenW, screenH;
PingPong(int screenW, int screenH)
: srcInd(0), dstInd(1),
screenW(screenW), screenH(screenH)
{
for (int i = 0; i < 2; ++i)
{
TEXFBO::init(rt[i]);
TEXFBO::allocEmpty(rt[i], screenW, screenH);
TEXFBO::linkFBO(rt[i]);
gl.ClearColor(0, 0, 0, 1);
FBO::clear();
}
}
~PingPong()
{
for (int i = 0; i < 2; ++i)
TEXFBO::fini(rt[i]);
}
TEXFBO &backBuffer()
{
return rt[srcInd];
}
TEXFBO &frontBuffer()
{
return rt[dstInd];
}
/* Better not call this during render cycles */
void resize(int width, int height)
{
screenW = width;
screenH = height;
for (int i = 0; i < 2; ++i)
{
TEX::bind(rt[i].tex);
TEX::allocEmpty(width, height);
}
}
void startRender()
{
bind();
}
void swapRender()
{
std::swap(srcInd, dstInd);
bind();
}
void clearBuffers()
{
glState.clearColor.pushSet(Vec4(0, 0, 0, 1));
for (int i = 0; i < 2; ++i)
{
FBO::bind(rt[i].fbo);
FBO::clear();
}
glState.clearColor.pop();
}
private:
void bind()
{
FBO::bind(rt[dstInd].fbo);
}
};
class ScreenScene : public Scene
{
public:
ScreenScene(int width, int height)
: pp(width, height)
{
updateReso(width, height);
brightEffect = false;
brightnessQuad.setColor(Vec4());
}
void composite()
{
const int w = geometry.rect.w;
const int h = geometry.rect.h;
shState->prepareDraw();
pp.startRender();
glState.viewport.set(IntRect(0, 0, w, h));
FBO::clear();
Scene::composite();
if (brightEffect)
{
SimpleColorShader &shader = shState->shaders().simpleColor;
shader.bind();
shader.applyViewportProj();
shader.setTranslation(Vec2i());
brightnessQuad.draw();
}
}
void requestViewportRender(Vec4 &c, Vec4 &f, Vec4 &t)
{
const IntRect &viewpRect = glState.scissorBox.get();
const IntRect &screenRect = geometry.rect;
if (t.w != 0.0)
{
pp.swapRender();
if (!viewpRect.encloses(screenRect))
{
/* Scissor test _does_ affect FBO blit operations,
* and since we're inside the draw cycle, it will
* be turned on, so turn it off temporarily */
glState.scissorTest.pushSet(false);
GLMeta::blitBegin(pp.frontBuffer());
GLMeta::blitSource(pp.backBuffer());
GLMeta::blitRectangle(geometry.rect, Vec2i());
GLMeta::blitEnd();
glState.scissorTest.pop();
}
GrayShader &shader = shState->shaders().gray;
shader.bind();
shader.setGray(t.w);
shader.applyViewportProj();
shader.setTexSize(screenRect.size());
TEX::bind(pp.backBuffer().tex);
glState.blend.pushSet(false);
screenQuad.draw();
glState.blend.pop();
}
bool toneEffect = t.xyzHasEffect();
bool colorEffect = c.xyzHasEffect();
bool flashEffect = f.xyzHasEffect();
if (!toneEffect && !colorEffect && !flashEffect)
return;
FlatColorShader &shader = shState->shaders().flatColor;
shader.bind();
shader.applyViewportProj();
/* Apply tone */
if (toneEffect)
{
/* First split up additive / substractive components */
Vec4 add, sub;
if (t.x > 0)
add.x = t.x;
if (t.y > 0)
add.y = t.y;
if (t.z > 0)
add.z = t.z;
if (t.x < 0)
sub.x = -t.x;
if (t.y < 0)
sub.y = -t.y;
if (t.z < 0)
sub.z = -t.z;
/* Then apply them using hardware blending */
gl.BlendFuncSeparate(GL_ONE, GL_ONE, GL_ZERO, GL_ONE);
if (add.xyzHasEffect())
{
gl.BlendEquation(GL_FUNC_ADD);
shader.setColor(add);
screenQuad.draw();
}
if (sub.xyzHasEffect())
{
gl.BlendEquation(GL_FUNC_REVERSE_SUBTRACT);
shader.setColor(sub);
screenQuad.draw();
}
}
if (colorEffect || flashEffect)
{
gl.BlendEquation(GL_FUNC_ADD);
gl.BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
GL_ZERO, GL_ONE);
}
if (colorEffect)
{
shader.setColor(c);
screenQuad.draw();
}
if (flashEffect)
{
shader.setColor(f);
screenQuad.draw();
}
glState.blendMode.refresh();
}
void setBrightness(float norm)
{
brightnessQuad.setColor(Vec4(0, 0, 0, 1.0 - norm));
brightEffect = norm < 1.0;
}
void updateReso(int width, int height)
{
geometry.rect.w = width;
geometry.rect.h = height;
screenQuad.setTexPosRect(geometry.rect, geometry.rect);
brightnessQuad.setTexPosRect(geometry.rect, geometry.rect);
notifyGeometryChange();
}
void setResolution(int width, int height)
{
pp.resize(width, height);
updateReso(width, height);
}
PingPong &getPP()
{
return pp;
}
private:
PingPong pp;
Quad screenQuad;
Quad brightnessQuad;
bool brightEffect;
};
/* Nanoseconds per second */
#define NS_PER_S 1000000000
struct FPSLimiter
{
uint64_t lastTickCount;
/* ticks per frame */
int64_t tpf;
/* Ticks per second */
const uint64_t tickFreq;
/* Ticks per milisecond */
const uint64_t tickFreqMS;
/* Ticks per nanosecond */
const double tickFreqNS;
bool disabled;
/* Data for frame timing adjustment */
struct
{
/* Last tick count */
uint64_t last;
/* How far behind/in front we are for ideal frame timing */
int64_t idealDiff;
bool resetFlag;
} adj;
FPSLimiter(uint16_t desiredFPS)
: lastTickCount(SDL_GetPerformanceCounter()),
tickFreq(SDL_GetPerformanceFrequency()),
tickFreqMS(tickFreq / 1000),
tickFreqNS((double) tickFreq / NS_PER_S),
disabled(false)
{
setDesiredFPS(desiredFPS);
adj.last = SDL_GetPerformanceCounter();
adj.idealDiff = 0;
adj.resetFlag = false;
}
void setDesiredFPS(uint16_t value)
{
tpf = tickFreq / value;
}
void delay()
{
if (disabled)
return;
int64_t tickDelta = SDL_GetPerformanceCounter() - lastTickCount;
int64_t toDelay = tpf - tickDelta;
/* Compensate for the last delta
* to the ideal timestep */
toDelay -= adj.idealDiff;
if (toDelay < 0)
toDelay = 0;
delayTicks(toDelay);
uint64_t now = lastTickCount = SDL_GetPerformanceCounter();
int64_t diff = now - adj.last;
adj.last = now;
/* Recalculate our temporal position
* relative to the ideal timestep */
adj.idealDiff = diff - tpf + adj.idealDiff;
if (adj.resetFlag)
{
adj.idealDiff = 0;
adj.resetFlag = false;
}
}
void resetFrameAdjust()
{
adj.resetFlag = true;
}
/* If we're more than a full frame's worth
* of ticks behind the ideal timestep,
* there's no choice but to skip frame(s)
* to catch up */
bool frameSkipRequired() const
{
if (disabled)
return false;
return adj.idealDiff > tpf;
}
private:
void delayTicks(uint64_t ticks)
{
#if defined(HAVE_NANOSLEEP)
struct timespec req;
uint64_t nsec = ticks / tickFreqNS;
req.tv_sec = nsec / NS_PER_S;
req.tv_nsec = nsec % NS_PER_S;
errno = 0;
while (nanosleep(&req, &req) == -1)
{
int err = errno;
errno = 0;
if (err == EINTR)
continue;
Debug() << "nanosleep failed. errno:" << err;
SDL_Delay(ticks / tickFreqMS);
break;
}
#else
SDL_Delay(ticks / tickFreqMS);
#endif
}
};
struct GraphicsPrivate
{
/* Screen resolution, ie. the resolution at which
* RGSS renders at (settable with Graphics.resize_screen).
* Can only be changed from within RGSS */
Vec2i scRes;
/* Screen size, to which the rendered frames are scaled up.
* This can be smaller than the window size when fixed aspect
* ratio is enforced */
Vec2i scSize;
/* Actual physical size of the game window */
Vec2i winSize;
/* Offset in the game window at which the scaled game screen
* is blitted inside the game window */
Vec2i scOffset;
ScreenScene screen;
RGSSThreadData *threadData;
int frameRate;
int frameCount;
int brightness;
FPSLimiter fpsLimiter;
bool frozen;
TEXFBO frozenScene;
TEXFBO currentScene;
Quad screenQuad;
TEXFBO transBuffer;
/* Global list of all live Disposables
* (disposed on reset) */
IntruList<Disposable> dispList;
GraphicsPrivate(RGSSThreadData *rtData)
: scRes(DEF_SCREEN_W, DEF_SCREEN_H),
scSize(scRes),
winSize(rtData->config.defScreenW, rtData->config.defScreenH),
screen(scRes.x, scRes.y),
threadData(rtData),
frameRate(DEF_FRAMERATE),
frameCount(0),
brightness(255),
fpsLimiter(frameRate),
frozen(false)
{
recalculateScreenSize(rtData);
updateScreenResoRatio(rtData);
TEXFBO::init(frozenScene);
TEXFBO::allocEmpty(frozenScene, scRes.x, scRes.y);
TEXFBO::linkFBO(frozenScene);
TEXFBO::init(currentScene);
TEXFBO::allocEmpty(currentScene, scRes.x, scRes.y);
TEXFBO::linkFBO(currentScene);
FloatRect screenRect(0, 0, scRes.x, scRes.y);
screenQuad.setTexPosRect(screenRect, screenRect);
TEXFBO::init(transBuffer);
TEXFBO::allocEmpty(transBuffer, scRes.x, scRes.y);
TEXFBO::linkFBO(transBuffer);
fpsLimiter.resetFrameAdjust();
}
~GraphicsPrivate()
{
TEXFBO::fini(frozenScene);
TEXFBO::fini(currentScene);
TEXFBO::fini(transBuffer);
}
void updateScreenResoRatio(RGSSThreadData *rtData)
{
Vec2 &ratio = rtData->sizeResoRatio;
ratio.x = (float) scRes.x / scSize.x;
ratio.y = (float) scRes.y / scSize.y;
rtData->screenOffset = scOffset;
}
/* Enforces fixed aspect ratio, if desired */
void recalculateScreenSize(RGSSThreadData *rtData)
{
scSize = winSize;
if (!rtData->config.fixedAspectRatio)
{
scOffset = Vec2i(0, 0);
return;
}
float resRatio = (float) scRes.x / scRes.y;
float winRatio = (float) winSize.x / winSize.y;
if (resRatio > winRatio)
scSize.y = scSize.x / resRatio;
else if (resRatio < winRatio)
scSize.x = scSize.y * resRatio;
scOffset.x = (winSize.x - scSize.x) / 2.f;
scOffset.y = (winSize.y - scSize.y) / 2.f;
}
void checkResize()
{
if (threadData->windowSizeMsg.pollChange(&winSize.x, &winSize.y))
{
/* some GL drivers change the viewport on window resize */
glState.viewport.refresh();
recalculateScreenSize(threadData);
updateScreenResoRatio(threadData);
}
}
void checkShutDownReset()
{
shState->checkShutdown();
shState->checkReset();
}
void shutdown()
{
threadData->rqTermAck.set();
shState->texPool().disable();
scriptBinding->terminate();
}
void swapGLBuffer()
{
fpsLimiter.delay();
SDL_GL_SwapWindow(threadData->window);
++frameCount;
threadData->ethread->notifyFrame();
}
void compositeToBuffer(TEXFBO &buffer)
{
screen.composite();
GLMeta::blitBegin(buffer);
GLMeta::blitSource(screen.getPP().frontBuffer());
GLMeta::blitRectangle(IntRect(0, 0, scRes.x, scRes.y), Vec2i());
GLMeta::blitEnd();
}
void metaBlitBufferFlippedScaled()
{
GLMeta::blitRectangle(IntRect(0, 0, scRes.x, scRes.y),
IntRect(scOffset.x, scSize.y+scOffset.y, scSize.x, -scSize.y),
threadData->config.smoothScaling);
}
void redrawScreen()
{
screen.composite();
GLMeta::blitBeginScreen(winSize);
GLMeta::blitSource(screen.getPP().frontBuffer());
FBO::clear();
metaBlitBufferFlippedScaled();
GLMeta::blitEnd();
swapGLBuffer();
}
};
Graphics::Graphics(RGSSThreadData *data)
{
p = new GraphicsPrivate(data);
if (data->config.fixedFramerate > 0)
p->fpsLimiter.setDesiredFPS(data->config.fixedFramerate);
else if (data->config.fixedFramerate < 0)
p->fpsLimiter.disabled = true;
}
Graphics::~Graphics()
{
delete p;
}
void Graphics::update()
{
p->checkShutDownReset();
if (p->frozen)
return;
if (p->fpsLimiter.frameSkipRequired())
{
if (p->threadData->config.frameSkip)
{
/* Skip frame */
p->fpsLimiter.delay();
++p->frameCount;
p->threadData->ethread->notifyFrame();
return;
}
else
{
/* Just reset frame adjust counter */
p->fpsLimiter.resetFrameAdjust();
}
}
p->checkResize();
p->redrawScreen();
}
void Graphics::freeze()
{
p->frozen = true;
p->checkShutDownReset();
p->checkResize();
/* Capture scene into frozen buffer */
p->compositeToBuffer(p->frozenScene);
}
void Graphics::transition(int duration,
const char *filename,
int vague)
{
if (!p->frozen)
return;
vague = clamp(vague, 0, 512);
Bitmap *transMap = filename ? new Bitmap(filename) : 0;
setBrightness(255);
/* Capture new scene */
p->compositeToBuffer(p->currentScene);
/* If no transition bitmap is provided,
* we can use a simplified shader */
TransShader &transShader = shState->shaders().trans;
SimpleTransShader &simpleShader = shState->shaders().simpleTrans;
if (transMap)
{
TransShader &shader = transShader;
shader.bind();
shader.applyViewportProj();
shader.setFrozenScene(p->frozenScene.tex);
shader.setCurrentScene(p->currentScene.tex);
shader.setTransMap(transMap->getGLTypes().tex);
shader.setVague(vague / 512.0f);
shader.setTexSize(p->scRes);
}
else
{
SimpleTransShader &shader = simpleShader;
shader.bind();
shader.applyViewportProj();
shader.setFrozenScene(p->frozenScene.tex);
shader.setCurrentScene(p->currentScene.tex);
shader.setTexSize(p->scRes);
}
glState.blend.pushSet(false);
for (int i = 0; i < duration; ++i)
{
/* We need to clean up transMap properly before
* a possible longjmp, so we manually test for
* shutdown/reset here */
if (p->threadData->rqTerm)
{
glState.blend.pop();
delete transMap;
p->shutdown();
return;
}
if (p->threadData->rqReset)
{
glState.blend.pop();
delete transMap;
scriptBinding->reset();
return;
}
const float prog = i * (1.0 / duration);
if (transMap)
{
transShader.bind();
transShader.setProg(prog);
}
else
{
simpleShader.bind();
simpleShader.setProg(prog);
}
/* Draw the composed frame to a buffer first
* (we need this because we're skipping PingPong) */
FBO::bind(p->transBuffer.fbo);
FBO::clear();
p->screenQuad.draw();
p->checkResize();
/* Then blit it flipped and scaled to the screen */
FBO::unbind();
FBO::clear();
GLMeta::blitBeginScreen(Vec2i(p->winSize));
GLMeta::blitSource(p->transBuffer);
p->metaBlitBufferFlippedScaled();
GLMeta::blitEnd();
p->swapGLBuffer();
}
glState.blend.pop();
delete transMap;
p->frozen = false;
}
void Graphics::frameReset()
{
p->fpsLimiter.resetFrameAdjust();
}
static void guardDisposed() {}
DEF_ATTR_RD_SIMPLE(Graphics, FrameRate, int, p->frameRate)
DEF_ATTR_SIMPLE(Graphics, FrameCount, int, p->frameCount)
void Graphics::setFrameRate(int value)
{
p->frameRate = clamp(value, 10, 120);
if (p->threadData->config.fixedFramerate > 0)
return;
p->fpsLimiter.setDesiredFPS(p->frameRate);
}
void Graphics::wait(int duration)
{
for (int i = 0; i < duration; ++i)
{
p->checkShutDownReset();
p->redrawScreen();
}
}
void Graphics::fadeout(int duration)
{
FBO::unbind();
float curr = p->brightness;
float diff = 255.0 - curr;
for (int i = duration-1; i > -1; --i)
{
setBrightness(diff + (curr / duration) * i);
if (p->frozen)
{
GLMeta::blitBeginScreen(p->scSize);
GLMeta::blitSource(p->frozenScene);
FBO::clear();
p->metaBlitBufferFlippedScaled();
GLMeta::blitEnd();
p->swapGLBuffer();
}
else
{
update();
}
}
}
void Graphics::fadein(int duration)
{
FBO::unbind();
float curr = p->brightness;
float diff = 255.0 - curr;
for (int i = 1; i <= duration; ++i)
{
setBrightness(curr + (diff / duration) * i);
if (p->frozen)
{
GLMeta::blitBeginScreen(p->scSize);
GLMeta::blitSource(p->frozenScene);
FBO::clear();
p->metaBlitBufferFlippedScaled();
GLMeta::blitEnd();
p->swapGLBuffer();
}
else
{
update();
}
}
}
Bitmap *Graphics::snapToBitmap()
{
Bitmap *bitmap = new Bitmap(width(), height());
p->compositeToBuffer(bitmap->getGLTypes());
/* Taint entire bitmap */
bitmap->taintArea(IntRect(0, 0, width(), height()));
return bitmap;
}
int Graphics::width() const
{
return p->scRes.x;
}
int Graphics::height() const
{
return p->scRes.y;
}
void Graphics::resizeScreen(int width, int height)
{
width = clamp(width, 1, 640);
height = clamp(height, 1, 480);
Vec2i size(width, height);
if (p->scRes == size)
return;
p->scRes = size;
p->screen.setResolution(width, height);
TEX::bind(p->frozenScene.tex);
TEX::allocEmpty(width, height);
TEX::bind(p->currentScene.tex);
TEX::allocEmpty(width, height);
FloatRect screenRect(0, 0, width, height);
p->screenQuad.setTexPosRect(screenRect, screenRect);
TEX::bind(p->transBuffer.tex);
TEX::allocEmpty(width, height);
shState->eThread().requestWindowResize(width, height);
}
DEF_ATTR_RD_SIMPLE(Graphics, Brightness, int, p->brightness)
void Graphics::setBrightness(int value)
{
value = clamp(value, 0, 255);
if (p->brightness == value)
return;
p->brightness = value;
p->screen.setBrightness(value / 255.0);
}
void Graphics::reset()
{
/* Dispose all live Disposables */
IntruListLink<Disposable> *iter;
for (iter = p->dispList.begin();
iter != p->dispList.end();
iter = iter->next)
{
iter->data->dispose();
}
p->dispList.clear();
/* Reset attributes (frame count not included) */
p->fpsLimiter.resetFrameAdjust();
p->frozen = false;
p->screen.getPP().clearBuffers();
setFrameRate(DEF_FRAMERATE);
setBrightness(255);
}
bool Graphics::getFullscreen() const
{
return p->threadData->ethread->getFullscreen();
}
void Graphics::setFullscreen(bool value)
{
p->threadData->ethread->requestFullscreenMode(value);
}
bool Graphics::getShowCursor() const
{
return p->threadData->ethread->getShowCursor();
}
void Graphics::setShowCursor(bool value)
{
p->threadData->ethread->requestShowCursor(value);
}
Scene *Graphics::getScreen() const
{
return &p->screen;
}
void Graphics::repaintWait(const AtomicFlag &exitCond, bool checkReset)
{
if (exitCond)
return;
/* Repaint the screen with the last good frame we drew */
TEXFBO &lastFrame = p->screen.getPP().frontBuffer();
GLMeta::blitBeginScreen(p->winSize);
GLMeta::blitSource(lastFrame);
while (!exitCond)
{
shState->checkShutdown();
if (checkReset)
shState->checkReset();
FBO::clear();
p->metaBlitBufferFlippedScaled();
SDL_GL_SwapWindow(p->threadData->window);
p->fpsLimiter.delay();
p->threadData->ethread->notifyFrame();
}
GLMeta::blitEnd();
}
void Graphics::addDisposable(Disposable *d)
{
p->dispList.append(d->link);
}
void Graphics::remDisposable(Disposable *d)
{
p->dispList.remove(d->link);
}