mkxp-freebird/src/graphics.cpp

1024 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);
}