diff --git a/binding/bitmap-binding.cpp b/binding/bitmap-binding.cpp
index ee77646c..5af6deb4 100644
--- a/binding/bitmap-binding.cpp
+++ b/binding/bitmap-binding.cpp
@@ -49,6 +49,12 @@ void bitmapInitProps(Bitmap *b, VALUE self) {
b->setInitFont(font);
rb_iv_set(self, "font", fontObj);
+
+ // Leave property as default nil if hasHires() is false.
+ if (b->hasHires()) {
+ b->assumeRubyGC();
+ wrapProperty(self, b->getHires(), "hires", BitmapType);
+ }
}
RB_METHOD(bitmapInitialize) {
@@ -94,6 +100,8 @@ RB_METHOD(bitmapHeight) {
return INT2FIX(value);
}
+DEF_GFX_PROP_OBJ_REF(Bitmap, Bitmap, Hires, "hires")
+
RB_METHOD(bitmapRect) {
RB_UNUSED_PARAM;
@@ -741,6 +749,9 @@ void bitmapBindingInit() {
_rb_define_method(klass, "width", bitmapWidth);
_rb_define_method(klass, "height", bitmapHeight);
+
+ INIT_PROP_BIND(Bitmap, Hires, "hires");
+
_rb_define_method(klass, "rect", bitmapRect);
_rb_define_method(klass, "blt", bitmapBlt);
_rb_define_method(klass, "stretch_blt", bitmapStretchBlt);
diff --git a/binding/graphics-binding.cpp b/binding/graphics-binding.cpp
index 75095c2c..6873f35e 100644
--- a/binding/graphics-binding.cpp
+++ b/binding/graphics-binding.cpp
@@ -19,6 +19,7 @@
** along with mkxp. If not, see .
*/
+#include "config.h"
#include "graphics.h"
#include "sharedstate.h"
#include "binding-util.h"
diff --git a/mkxp.json b/mkxp.json
index afa41c51..8a3649c7 100644
--- a/mkxp.json
+++ b/mkxp.json
@@ -91,6 +91,35 @@
// "lanczos3Scaling": false,
+ // Replace the game's Bitmap files with external high-res files
+ // provided in the "Hires" directory.
+ // (You'll also need to set the below Scaling Factors.)
+ // (default: disabled)
+ //
+ // "enableHires": false,
+
+
+ // Scaling factor for textures (e.g. Bitmaps)
+ // (higher values will look better if you use high-res textures)
+ // (default: 1.0)
+ //
+ // "textureScalingFactor": 4.0,
+
+
+ // Scaling factor for screen framebuffer
+ // (higher values will look better if you use high-res textures)
+ // (default: 1.0)
+ //
+ // "framebufferScalingFactor": 4.0,
+
+
+ // Scaling factor for tileset atlas
+ // (higher values will look better if you use high-res textures)
+ // (default: 1.0)
+ //
+ // "atlasScalingFactor": 4.0,
+
+
// Sync screen redraws to the monitor refresh rate
// (default: disabled)
//
diff --git a/src/config.cpp b/src/config.cpp
index 9048c6d7..58932abc 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -136,6 +136,10 @@ void Config::read(int argc, char *argv[]) {
{"fixedAspectRatio", true},
{"smoothScaling", false},
{"lanczos3Scaling", false},
+ {"enableHires", false},
+ {"textureScalingFactor", 1.},
+ {"framebufferScalingFactor", 1.},
+ {"atlasScalingFactor", 1.},
{"vsync", false},
{"defScreenW", 0},
{"defScreenH", 0},
@@ -261,6 +265,10 @@ try { exp } catch (...) {}
SET_OPT(fixedAspectRatio, boolean);
SET_OPT(smoothScaling, boolean);
SET_OPT(lanczos3Scaling, boolean);
+ SET_OPT(enableHires, boolean);
+ SET_OPT(textureScalingFactor, number);
+ SET_OPT(framebufferScalingFactor, number);
+ SET_OPT(atlasScalingFactor, number);
SET_OPT(winResizable, boolean);
SET_OPT(vsync, boolean);
SET_STRINGOPT(windowTitle, windowTitle);
diff --git a/src/config.h b/src/config.h
index ade1e64f..328df348 100644
--- a/src/config.h
+++ b/src/config.h
@@ -45,6 +45,10 @@ struct Config {
bool fixedAspectRatio;
bool smoothScaling;
bool lanczos3Scaling;
+ bool enableHires;
+ double textureScalingFactor;
+ double framebufferScalingFactor;
+ double atlasScalingFactor;
bool vsync;
int defScreenW;
diff --git a/src/display/bitmap.cpp b/src/display/bitmap.cpp
index e97252b7..847539f1 100644
--- a/src/display/bitmap.cpp
+++ b/src/display/bitmap.cpp
@@ -237,11 +237,19 @@ struct BitmapPrivate
* in the texture and blit to it directly, saving
* ourselves the expensive blending calculation */
pixman_region16_t tainted;
+
+ // For high-resolution texture replacement.
+ Bitmap *selfHires;
+ Bitmap *selfLores;
+ bool assumingRubyGC;
BitmapPrivate(Bitmap *self)
: self(self),
megaSurface(0),
- surface(0)
+ selfHires(0),
+ selfLores(0),
+ surface(0),
+ assumingRubyGC(false)
{
format = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888);
@@ -326,16 +334,30 @@ struct BitmapPrivate
return result != PIXMAN_REGION_OUT;
}
- void bindTexture(ShaderBase &shader)
+ void bindTexture(ShaderBase &shader, bool substituteLoresSize = true)
{
+ if (selfHires) {
+ selfHires->bindTex(shader);
+ return;
+ }
+
if (animation.enabled) {
+ if (selfLores) {
+ Debug() << "BUG: High-res BitmapPrivate bindTexture for animations not implemented";
+ }
+
TEXFBO cframe = animation.currentFrame();
TEX::bind(cframe.tex);
shader.setTexSize(Vec2i(cframe.width, cframe.height));
return;
}
TEX::bind(gl.tex);
- shader.setTexSize(Vec2i(gl.width, gl.height));
+ if (selfLores && substituteLoresSize) {
+ shader.setTexSize(Vec2i(selfLores->width(), selfLores->height()));
+ }
+ else {
+ shader.setTexSize(Vec2i(gl.width, gl.height));
+ }
}
void bindFBO()
@@ -468,6 +490,24 @@ struct BitmapOpenHandler : FileSystem::OpenHandler
Bitmap::Bitmap(const char *filename)
{
+ std::string hiresPrefix = "Hires/";
+ std::string filenameStd = filename;
+ Bitmap *hiresBitmap = nullptr;
+ // TODO: once C++20 is required, switch to filenameStd.starts_with(hiresPrefix)
+ if (shState->config().enableHires && filenameStd.compare(0, hiresPrefix.size(), hiresPrefix) != 0) {
+ // Look for a high-res version of the file.
+ std::string hiresFilename = hiresPrefix + filenameStd;
+ try {
+ hiresBitmap = new Bitmap(hiresFilename.c_str());
+ hiresBitmap->setLores(this);
+ }
+ catch (const Exception &e)
+ {
+ Debug() << "No high-res Bitmap found at" << hiresFilename;
+ hiresBitmap = nullptr;
+ }
+ }
+
BitmapOpenHandler handler;
shState->fileSystem().openRead(handler, filename);
@@ -482,6 +522,8 @@ Bitmap::Bitmap(const char *filename)
if (handler.gif) {
p = new BitmapPrivate(this);
+
+ p->selfHires = hiresBitmap;
if (handler.gif->width >= (uint32_t)glState.caps.maxTexSize || handler.gif->height > (uint32_t)glState.caps.maxTexSize)
{
@@ -510,6 +552,9 @@ Bitmap::Bitmap(const char *filename)
delete handler.gif_data;
p->gl = texfbo;
+ if (p->selfHires != nullptr) {
+ p->gl.selfHires = &p->selfHires->getGLTypes();
+ }
p->addTaintedArea(rect());
return;
}
@@ -583,6 +628,7 @@ Bitmap::Bitmap(const char *filename)
{
/* Mega surface */
p = new BitmapPrivate(this);
+ p->selfHires = hiresBitmap;
p->megaSurface = imgSurf;
SDL_SetSurfaceBlendMode(p->megaSurface, SDL_BLENDMODE_NONE);
}
@@ -602,7 +648,11 @@ Bitmap::Bitmap(const char *filename)
}
p = new BitmapPrivate(this);
+ p->selfHires = hiresBitmap;
p->gl = tex;
+ if (p->selfHires != nullptr) {
+ p->gl.selfHires = &p->selfHires->getGLTypes();
+ }
TEX::bind(p->gl.tex);
TEX::uploadImage(p->gl.width, p->gl.height, imgSurf->pixels, GL_RGBA);
@@ -613,15 +663,30 @@ Bitmap::Bitmap(const char *filename)
p->addTaintedArea(rect());
}
-Bitmap::Bitmap(int width, int height)
+Bitmap::Bitmap(int width, int height, bool isHires)
{
if (width <= 0 || height <= 0)
throw Exception(Exception::RGSSError, "failed to create bitmap");
+ Bitmap *hiresBitmap = nullptr;
+
+ if (shState->config().enableHires && !isHires) {
+ // Create a high-res version as well.
+ double scalingFactor = shState->config().textureScalingFactor;
+ int hiresWidth = (int)lround(scalingFactor * width);
+ int hiresHeight = (int)lround(scalingFactor * height);
+ hiresBitmap = new Bitmap(hiresWidth, hiresHeight, true);
+ hiresBitmap->setLores(this);
+ }
+
TEXFBO tex = shState->texPool().request(width, height);
p = new BitmapPrivate(this);
p->gl = tex;
+ if (p->selfHires != nullptr) {
+ p->gl.selfHires = &p->selfHires->getGLTypes();
+ }
+ p->selfHires = hiresBitmap;
clear();
}
@@ -679,6 +744,10 @@ Bitmap::Bitmap(const Bitmap &other, int frame)
other.ensureNonMega();
if (frame > -2) other.ensureAnimated();
+ if (other.hasHires()) {
+ Debug() << "BUG: High-res Bitmap from animation not implemented";
+ }
+
p = new BitmapPrivate(this);
// TODO: Clean me up
@@ -762,6 +831,28 @@ int Bitmap::height() const
return p->gl.height;
}
+bool Bitmap::hasHires() const{
+ guardDisposed();
+
+ return p->selfHires;
+}
+
+DEF_ATTR_RD_SIMPLE(Bitmap, Hires, Bitmap*, p->selfHires)
+
+void Bitmap::setHires(Bitmap *hires) {
+ guardDisposed();
+
+ Debug() << "BUG: High-res Bitmap setHires not fully implemented, expect bugs";
+ hires->setLores(this);
+ p->selfHires = hires;
+}
+
+void Bitmap::setLores(Bitmap *lores) {
+ guardDisposed();
+
+ p->selfLores = lores;
+}
+
bool Bitmap::isMega() const{
guardDisposed();
@@ -809,13 +900,35 @@ void Bitmap::stretchBlt(const IntRect &destRect,
int opacity)
{
guardDisposed();
-
+
// Don't need this, right? This function is fine with megasurfaces it seems
//GUARD_MEGA;
-
+
if (source.isDisposed())
return;
-
+
+ if (hasHires()) {
+ int destX, destY, destWidth, destHeight;
+ destX = destRect.x * p->selfHires->width() / width();
+ destY = destRect.y * p->selfHires->height() / height();
+ destWidth = destRect.w * p->selfHires->width() / width();
+ destHeight = destRect.h * p->selfHires->height() / height();
+
+ p->selfHires->stretchBlt(IntRect(destX, destY, destWidth, destHeight), source, sourceRect, opacity);
+ return;
+ }
+
+ if (source.hasHires()) {
+ int sourceX, sourceY, sourceWidth, sourceHeight;
+ sourceX = sourceRect.x * source.getHires()->width() / source.width();
+ sourceY = sourceRect.y * source.getHires()->height() / source.height();
+ sourceWidth = sourceRect.w * source.getHires()->width() / source.width();
+ sourceHeight = sourceRect.h * source.getHires()->height() / source.height();
+
+ stretchBlt(destRect, *source.getHires(), IntRect(sourceX, sourceY, sourceWidth, sourceHeight), opacity);
+ return;
+ }
+
opacity = clamp(opacity, 0, 255);
if (opacity == 0)
@@ -936,7 +1049,7 @@ void Bitmap::stretchBlt(const IntRect &destRect,
quad.setTexPosRect(sourceRect, destRect);
quad.setColor(Vec4(1, 1, 1, normOpacity));
- source.p->bindTexture(shader);
+ source.p->bindTexture(shader, false);
p->bindFBO();
p->pushSetViewport(shader);
@@ -963,6 +1076,16 @@ void Bitmap::fillRect(const IntRect &rect, const Vec4 &color)
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ int destX, destY, destWidth, destHeight;
+ destX = rect.x * p->selfHires->width() / width();
+ destY = rect.y * p->selfHires->height() / height();
+ destWidth = rect.w * p->selfHires->width() / width();
+ destHeight = rect.h * p->selfHires->height() / height();
+
+ p->selfHires->fillRect(IntRect(destX, destY, destWidth, destHeight), color);
+ }
+
p->fillRect(rect, color);
if (color.w == 0)
@@ -992,6 +1115,16 @@ void Bitmap::gradientFillRect(const IntRect &rect,
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ int destX, destY, destWidth, destHeight;
+ destX = rect.x * p->selfHires->width() / width();
+ destY = rect.y * p->selfHires->height() / height();
+ destWidth = rect.w * p->selfHires->width() / width();
+ destHeight = rect.h * p->selfHires->height() / height();
+
+ p->selfHires->gradientFillRect(IntRect(destX, destY, destWidth, destHeight), color1, color2, vertical);
+ }
+
SimpleColorShader &shader = shState->shaders().simpleColor;
shader.bind();
shader.setTranslation(Vec2i());
@@ -1039,6 +1172,16 @@ void Bitmap::clearRect(const IntRect &rect)
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ int destX, destY, destWidth, destHeight;
+ destX = rect.x * p->selfHires->width() / width();
+ destY = rect.y * p->selfHires->height() / height();
+ destWidth = rect.w * p->selfHires->width() / width();
+ destHeight = rect.h * p->selfHires->height() / height();
+
+ p->selfHires->clearRect(IntRect(destX, destY, destWidth, destHeight));
+ }
+
p->fillRect(rect, Vec4());
p->onModified();
@@ -1051,6 +1194,12 @@ void Bitmap::blur()
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ p->selfHires->blur();
+ }
+
+ // TODO: Is there some kind of blur radius that we need to handle for high-res mode?
+
Quad &quad = shState->gpQuad();
FloatRect rect(0, 0, width(), height());
quad.setTexPosRect(rect, rect);
@@ -1097,6 +1246,11 @@ void Bitmap::radialBlur(int angle, int divisions)
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ p->selfHires->radialBlur(angle, divisions);
+ return;
+ }
+
angle = clamp(angle, 0, 359);
divisions = clamp(divisions, 2, 100);
@@ -1161,7 +1315,7 @@ void Bitmap::radialBlur(int angle, int divisions)
SimpleMatrixShader &shader = shState->shaders().simpleMatrix;
shader.bind();
- p->bindTexture(shader);
+ p->bindTexture(shader, false);
TEX::setSmooth(true);
p->pushSetViewport(shader);
@@ -1193,6 +1347,10 @@ void Bitmap::clear()
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ p->selfHires->clear();
+ }
+
p->bindFBO();
glState.clearColor.pushSet(Vec4());
@@ -1221,9 +1379,52 @@ Color Bitmap::getPixel(int x, int y) const
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ Debug() << "GAME BUG: Game is calling getPixel on low-res Bitmap; you may want to patch the game to improve graphics quality.";
+
+ int xHires = x * p->selfHires->width() / width();
+ int yHires = y * p->selfHires->height() / height();
+
+ // We take the average color from the high-res Bitmap.
+ // RGB channels skip fully transparent pixels when averaging.
+ int w = p->selfHires->width() / width();
+ int h = p->selfHires->height() / height();
+
+ if (w >= 1 && h >= 1) {
+ double rSum = 0.;
+ double gSum = 0.;
+ double bSum = 0.;
+ double aSum = 0.;
+
+ long long rgbCount = 0;
+ long long aCount = 0;
+
+ for (int thisX = xHires; thisX < xHires+w && thisX < p->selfHires->width(); thisX++) {
+ for (int thisY = yHires; thisY < yHires+h && thisY < p->selfHires->height(); thisY++) {
+ Color thisColor = p->selfHires->getPixel(thisX, thisY);
+ if (thisColor.getAlpha() >= 1.0) {
+ rSum += thisColor.getRed();
+ gSum += thisColor.getGreen();
+ bSum += thisColor.getBlue();
+ rgbCount++;
+ }
+ aSum += thisColor.getAlpha();
+ aCount++;
+ }
+ }
+
+ double rAvg = rSum / (double)rgbCount;
+ double gAvg = gSum / (double)rgbCount;
+ double bAvg = bSum / (double)rgbCount;
+ double aAvg = aSum / (double)aCount;
+
+ return Color(rAvg, gAvg, bAvg, aAvg);
+ }
+ }
+
if (x < 0 || y < 0 || x >= width() || y >= height())
return Vec4();
-
+
if (!p->surface)
{
p->allocSurface();
@@ -1252,6 +1453,24 @@ void Bitmap::setPixel(int x, int y, const Color &color)
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ Debug() << "GAME BUG: Game is calling setPixel on low-res Bitmap; you may want to patch the game to improve graphics quality.";
+
+ int xHires = x * p->selfHires->width() / width();
+ int yHires = y * p->selfHires->height() / height();
+
+ int w = p->selfHires->width() / width();
+ int h = p->selfHires->height() / height();
+
+ if (w >= 1 && h >= 1) {
+ for (int thisX = xHires; thisX < xHires+w && thisX < p->selfHires->width(); thisX++) {
+ for (int thisY = yHires; thisY < yHires+h && thisY < p->selfHires->height(); thisY++) {
+ p->selfHires->setPixel(thisX, thisY, color);
+ }
+ }
+ }
+ }
+
uint8_t pixel[] =
{
(uint8_t) clamp(color.red, 0, 255),
@@ -1283,6 +1502,10 @@ bool Bitmap::getRaw(void *output, int output_size)
guardDisposed();
+ if (hasHires()) {
+ Debug() << "GAME BUG: Game is calling getRaw on low-res Bitmap; you may want to patch the game to improve graphics quality.";
+ }
+
if (!p->animation.enabled && (p->surface || p->megaSurface)) {
void *src = (p->megaSurface) ? p->megaSurface->pixels : p->surface->pixels;
memcpy(output, src, output_size);
@@ -1300,6 +1523,10 @@ void Bitmap::replaceRaw(void *pixel_data, int size)
GUARD_MEGA;
+ if (hasHires()) {
+ Debug() << "GAME BUG: Game is calling replaceRaw on low-res Bitmap; you may want to patch the game to improve graphics quality.";
+ }
+
int w = width();
int h = height();
int requiredsize = w*h*4;
@@ -1318,6 +1545,10 @@ void Bitmap::saveToFile(const char *filename)
{
guardDisposed();
+ if (hasHires()) {
+ Debug() << "GAME BUG: Game is calling saveToFile on low-res Bitmap; you may want to patch the game to improve graphics quality.";
+ }
+
SDL_Surface *surf;
if (p->surface || p->megaSurface) {
@@ -1377,6 +1608,11 @@ void Bitmap::hueChange(int hue)
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ p->selfHires->hueChange(hue);
+ return;
+ }
+
if ((hue % 360) == 0)
return;
@@ -1395,7 +1631,7 @@ void Bitmap::hueChange(int hue)
FBO::bind(newTex.fbo);
p->pushSetViewport(shader);
- p->bindTexture(shader);
+ p->bindTexture(shader, false);
p->blitQuad(quad);
@@ -1525,6 +1761,26 @@ void Bitmap::drawText(const IntRect &rect, const char *str, int align)
GUARD_MEGA;
GUARD_ANIMATED;
+ if (hasHires()) {
+ Font &loresFont = getFont();
+ Font &hiresFont = p->selfHires->getFont();
+ // Disable the illegal font size check when creating a high-res font.
+ hiresFont.setSize(loresFont.getSize() * p->selfHires->width() / width(), false);
+ hiresFont.setBold(loresFont.getBold());
+ hiresFont.setColor(loresFont.getColor());
+ hiresFont.setItalic(loresFont.getItalic());
+ hiresFont.setShadow(loresFont.getShadow());
+ hiresFont.setOutline(loresFont.getOutline());
+ hiresFont.setOutColor(loresFont.getOutColor());
+
+ int rectX = rect.x * p->selfHires->width() / width();
+ int rectY = rect.y * p->selfHires->height() / height();
+ int rectWidth = rect.w * p->selfHires->width() / width();
+ int rectHeight = rect.h * p->selfHires->height() / height();
+
+ p->selfHires->drawText(IntRect(rectX, rectY, rectWidth, rectHeight), str, align);
+ }
+
std::string fixed = fixupString(str);
str = fixed.c_str();
@@ -1564,15 +1820,20 @@ void Bitmap::drawText(const IntRect &rect, const char *str, int align)
SDL_Color co = outColor.toSDLColor();
co.a = 255;
SDL_Surface *outline;
+ // Handle high-res for outline.
+ int scaledOutlineSize = OUTLINE_SIZE;
+ if (p->selfLores) {
+ scaledOutlineSize = scaledOutlineSize * width() / p->selfLores->width();
+ }
/* set the next font render to render the outline */
- TTF_SetFontOutline(font, OUTLINE_SIZE);
+ TTF_SetFontOutline(font, scaledOutlineSize);
if (p->font->isSolid())
outline = TTF_RenderUTF8_Solid(font, str, co);
else
outline = TTF_RenderUTF8_Blended(font, str, co);
p->ensureFormat(outline, SDL_PIXELFORMAT_ABGR8888);
- SDL_Rect outRect = {OUTLINE_SIZE, OUTLINE_SIZE, txtSurf->w, txtSurf->h};
+ SDL_Rect outRect = {scaledOutlineSize, scaledOutlineSize, txtSurf->w, txtSurf->h};
SDL_SetSurfaceBlendMode(txtSurf, SDL_BLENDMODE_BLEND);
SDL_BlitSurface(txtSurf, NULL, outline, &outRect);
@@ -1787,6 +2048,9 @@ IntRect Bitmap::textSize(const char *str)
GUARD_MEGA;
GUARD_ANIMATED;
+ // TODO: High-res Bitmap textSize not implemented, but I think it's the same as low-res?
+ // Need to double-check this.
+
TTF_Font *font = p->font->getSdlFont();
std::string fixed = fixupString(str);
@@ -1811,11 +2075,19 @@ DEF_ATTR_RD_SIMPLE(Bitmap, Font, Font&, *p->font)
void Bitmap::setFont(Font &value)
{
+ // High-res support handled in drawText, not here.
*p->font = value;
}
void Bitmap::setInitFont(Font *value)
{
+ if (hasHires()) {
+ Font *hiresFont = new Font(*value);
+ // Disable the illegal font size check when creating a high-res font.
+ hiresFont->setSize(hiresFont->getSize() * p->selfHires->width() / width(), false);
+ p->selfHires->setInitFont(hiresFont);
+ }
+
p->font = value;
}
@@ -1826,11 +2098,24 @@ TEXFBO &Bitmap::getGLTypes() const
SDL_Surface *Bitmap::surface() const
{
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap surface not implemented";
+ }
+
return p->surface;
}
SDL_Surface *Bitmap::megaSurface() const
{
+ if (hasHires()) {
+ if (p->megaSurface) {
+ Debug() << "BUG: High-res Bitmap megaSurface not implemented (low-res has megaSurface)";
+ }
+ if (p->selfHires->megaSurface()) {
+ Debug() << "BUG: High-res Bitmap megaSurface not implemented (high-res has megaSurface)";
+ }
+ }
+
return p->megaSurface;
}
@@ -1865,6 +2150,10 @@ void Bitmap::stop()
GUARD_UNANIMATED;
if (!p->animation.playing) return;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap stop not implemented";
+ }
+
p->animation.stop();
}
@@ -1874,6 +2163,11 @@ void Bitmap::play()
GUARD_UNANIMATED;
if (p->animation.playing) return;
+
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap play not implemented";
+ }
+
p->animation.play();
}
@@ -1881,6 +2175,10 @@ bool Bitmap::isPlaying() const
{
guardDisposed();
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap isPlaying not implemented";
+ }
+
if (!p->animation.playing)
return false;
@@ -1896,6 +2194,10 @@ void Bitmap::gotoAndStop(int frame)
GUARD_UNANIMATED;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap gotoAndStop not implemented";
+ }
+
p->animation.stop();
p->animation.seek(frame);
}
@@ -1905,6 +2207,10 @@ void Bitmap::gotoAndPlay(int frame)
GUARD_UNANIMATED;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap gotoAndPlay not implemented";
+ }
+
p->animation.stop();
p->animation.seek(frame);
p->animation.play();
@@ -1914,6 +2220,10 @@ int Bitmap::numFrames() const
{
guardDisposed();
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap numFrames not implemented";
+ }
+
if (!p->animation.enabled) return 1;
return (int)p->animation.frames.size();
}
@@ -1922,6 +2232,10 @@ int Bitmap::currentFrameI() const
{
guardDisposed();
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap currentFrameI not implemented";
+ }
+
if (!p->animation.enabled) return 0;
return p->animation.currentFrameI();
}
@@ -1932,6 +2246,14 @@ int Bitmap::addFrame(Bitmap &source, int position)
GUARD_MEGA;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap addFrame dest not implemented";
+ }
+
+ if (source.hasHires()) {
+ Debug() << "BUG: High-res Bitmap addFrame source not implemented";
+ }
+
if (source.height() != height() || source.width() != width())
throw Exception(Exception::MKXPError, "Animations with varying dimensions are not supported (%ix%i vs %ix%i)",
source.width(), source.height(), width(), height());
@@ -1989,6 +2311,10 @@ void Bitmap::removeFrame(int position) {
GUARD_UNANIMATED;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap removeFrame not implemented";
+ }
+
int pos = (position < 0) ? (int)p->animation.frames.size() - 1 : clamp(position, 0, (int)(p->animation.frames.size() - 1));
shState->texPool().release(p->animation.frames[pos]);
p->animation.frames.erase(p->animation.frames.begin() + pos);
@@ -2016,6 +2342,10 @@ void Bitmap::nextFrame()
GUARD_UNANIMATED;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap nextFrame not implemented";
+ }
+
stop();
if ((uint32_t)p->animation.lastFrame >= p->animation.frames.size() - 1) {
if (!p->animation.loop) return;
@@ -2032,6 +2362,10 @@ void Bitmap::previousFrame()
GUARD_UNANIMATED;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap previousFrame not implemented";
+ }
+
stop();
if (p->animation.lastFrame <= 0) {
if (!p->animation.loop) {
@@ -2051,6 +2385,10 @@ void Bitmap::setAnimationFPS(float FPS)
GUARD_MEGA;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap setAnimationFPS not implemented";
+ }
+
bool restart = p->animation.playing;
p->animation.stop();
p->animation.fps = (FPS < 0) ? 0 : FPS;
@@ -2059,6 +2397,10 @@ void Bitmap::setAnimationFPS(float FPS)
std::vector &Bitmap::getFrames() const
{
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap getFrames not implemented";
+ }
+
return p->animation.frames;
}
@@ -2068,6 +2410,10 @@ float Bitmap::getAnimationFPS() const
GUARD_MEGA;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap getAnimationFPS not implemented";
+ }
+
return p->animation.fps;
}
@@ -2077,6 +2423,10 @@ void Bitmap::setLooping(bool loop)
GUARD_MEGA;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap setLooping not implemented";
+ }
+
p->animation.loop = loop;
}
@@ -2086,16 +2436,32 @@ bool Bitmap::getLooping() const
GUARD_MEGA;
+ if (hasHires()) {
+ Debug() << "BUG: High-res Bitmap getLooping not implemented";
+ }
+
return p->animation.loop;
}
void Bitmap::bindTex(ShaderBase &shader)
{
+ // Hires mode is handled by p->bindTexture.
+
p->bindTexture(shader);
}
void Bitmap::taintArea(const IntRect &rect)
{
+ if (hasHires()) {
+ int destX, destY, destWidth, destHeight;
+ destX = rect.x * p->selfHires->width() / width();
+ destY = rect.y * p->selfHires->height() / height();
+ destWidth = rect.w * p->selfHires->width() / width();
+ destHeight = rect.h * p->selfHires->height() / height();
+
+ p->selfHires->taintArea(IntRect(destX, destY, destWidth, destHeight));
+ }
+
p->addTaintedArea(rect);
}
@@ -2121,8 +2487,17 @@ bool Bitmap::invalid() const {
return p == 0;
}
+void Bitmap::assumeRubyGC()
+{
+ p->assumingRubyGC = true;
+}
+
void Bitmap::releaseResources()
{
+ if (p->selfHires && !p->assumingRubyGC) {
+ delete p->selfHires;
+ }
+
if (p->megaSurface)
SDL_FreeSurface(p->megaSurface);
else if (p->animation.enabled) {
diff --git a/src/display/bitmap.h b/src/display/bitmap.h
index 6ca72c13..44760bb6 100644
--- a/src/display/bitmap.h
+++ b/src/display/bitmap.h
@@ -39,7 +39,7 @@ class Bitmap : public Disposable
{
public:
Bitmap(const char *filename);
- Bitmap(int width, int height);
+ Bitmap(int width, int height, bool isHires = false);
Bitmap(void *pixeldata, int width, int height);
/* Clone constructor */
@@ -49,6 +49,9 @@ public:
int width() const;
int height() const;
+ bool hasHires() const;
+ DECL_ATTR(Hires, Bitmap*)
+ void setLores(Bitmap *lores);
bool isMega() const;
bool isAnimated() const;
@@ -161,6 +164,8 @@ public:
bool invalid() const;
+ void assumeRubyGC();
+
private:
void releaseResources();
const char *klassName() const { return "bitmap"; }
diff --git a/src/display/font.cpp b/src/display/font.cpp
index 98d4033e..d0aa8d4b 100644
--- a/src/display/font.cpp
+++ b/src/display/font.cpp
@@ -399,14 +399,17 @@ void Font::setName(const std::vector &names)
p->sdlFont = 0;
}
-void Font::setSize(int value)
+void Font::setSize(int value, bool checkIllegal)
{
if (p->size == value)
return;
/* Catch illegal values (according to RMXP) */
- if (value < 6 || value > 96)
- throw Exception(Exception::ArgumentError, "%s", "bad value for size");
+ if (value < 6 || value > 96) {
+ if (checkIllegal) {
+ throw Exception(Exception::ArgumentError, "%s", "bad value for size");
+ }
+ }
p->size = value;
p->sdlFont = 0;
diff --git a/src/display/font.h b/src/display/font.h
index f76b8421..fe7d9751 100644
--- a/src/display/font.h
+++ b/src/display/font.h
@@ -76,7 +76,9 @@ public:
const Font &operator=(const Font &o);
- DECL_ATTR( Size, int )
+ int getSize() const;
+ void setSize(int value, bool checkIllegal=true);
+
DECL_ATTR( Bold, bool )
DECL_ATTR( Italic, bool )
DECL_ATTR( Color, Color& )
diff --git a/src/display/gl/gl-meta.cpp b/src/display/gl/gl-meta.cpp
index 1f39dc8d..f80050c1 100644
--- a/src/display/gl/gl-meta.cpp
+++ b/src/display/gl/gl-meta.cpp
@@ -26,6 +26,11 @@
#include "quad.h"
#include "config.h"
+namespace FBO
+{
+ ID boundFramebufferID;
+}
+
namespace GLMeta
{
@@ -138,6 +143,7 @@ static void _blitBegin(FBO::ID fbo, const Vec2i &size)
{
if (HAVE_NATIVE_BLIT)
{
+ FBO::boundFramebufferID = fbo;
gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.gl);
}
else
@@ -164,18 +170,56 @@ static void _blitBegin(FBO::ID fbo, const Vec2i &size)
}
}
-void blitBegin(TEXFBO &target)
+int blitDstWidthLores = 1;
+int blitDstWidthHires = 1;
+int blitDstHeightLores = 1;
+int blitDstHeightHires = 1;
+
+int blitSrcWidthLores = 1;
+int blitSrcWidthHires = 1;
+int blitSrcHeightLores = 1;
+int blitSrcHeightHires = 1;
+
+void blitBegin(TEXFBO &target, bool preferHires)
{
- _blitBegin(target.fbo, Vec2i(target.width, target.height));
+ blitDstWidthLores = target.width;
+ blitDstHeightLores = target.height;
+
+ if (preferHires && target.selfHires != nullptr) {
+ blitDstWidthHires = target.selfHires->width;
+ blitDstHeightHires = target.selfHires->height;
+ _blitBegin(target.selfHires->fbo, Vec2i(target.selfHires->width, target.selfHires->height));
+ }
+ else {
+ blitDstWidthHires = blitDstWidthLores;
+ blitDstHeightHires = blitDstHeightLores;
+ _blitBegin(target.fbo, Vec2i(target.width, target.height));
+ }
}
void blitBeginScreen(const Vec2i &size)
{
+ blitDstWidthLores = 1;
+ blitDstWidthHires = 1;
+ blitDstHeightLores = 1;
+ blitDstHeightHires = 1;
+
_blitBegin(FBO::ID(0), size);
}
void blitSource(TEXFBO &source)
{
+ blitSrcWidthLores = source.width;
+ blitSrcHeightLores = source.height;
+ if (source.selfHires != nullptr) {
+ blitSrcWidthHires = source.selfHires->width;
+ blitSrcHeightHires = source.selfHires->height;
+ }
+ else {
+ blitSrcWidthHires = blitSrcWidthLores;
+ blitSrcHeightHires = blitSrcHeightLores;
+ }
+
if (HAVE_NATIVE_BLIT)
{
gl.BindFramebuffer(GL_READ_FRAMEBUFFER, source.fbo.gl);
@@ -186,15 +230,20 @@ void blitSource(TEXFBO &source)
{
Lanczos3Shader &shader = shState->shaders().lanczos3;
shader.bind();
- shader.setTexSize(Vec2i(source.width, source.height));
+ shader.setTexSize(Vec2i(blitSrcWidthHires, blitSrcHeightHires));
}
else
{
SimpleShader &shader = shState->shaders().simple;
shader.bind();
- shader.setTexSize(Vec2i(source.width, source.height));
+ shader.setTexSize(Vec2i(blitSrcWidthHires, blitSrcHeightHires));
+ }
+ if (source.selfHires != nullptr) {
+ TEX::bind(source.selfHires->tex);
+ }
+ else {
+ TEX::bind(source.tex);
}
- TEX::bind(source.tex);
}
}
@@ -205,10 +254,24 @@ void blitRectangle(const IntRect &src, const Vec2i &dstPos)
void blitRectangle(const IntRect &src, const IntRect &dst, bool smooth)
{
+ // Handle high-res dest
+ int scaledDstX = dst.x * blitDstWidthHires / blitDstWidthLores;
+ int scaledDstY = dst.y * blitDstHeightHires / blitDstHeightLores;
+ int scaledDstWidth = dst.w * blitDstWidthHires / blitDstWidthLores;
+ int scaledDstHeight = dst.h * blitDstHeightHires / blitDstHeightLores;
+ IntRect dstScaled(scaledDstX, scaledDstY, scaledDstWidth, scaledDstHeight);
+
+ // Handle high-res source
+ int scaledSrcX = src.x * blitSrcWidthHires / blitSrcWidthLores;
+ int scaledSrcY = src.y * blitSrcHeightHires / blitSrcHeightLores;
+ int scaledSrcWidth = src.w * blitSrcWidthHires / blitSrcWidthLores;
+ int scaledSrcHeight = src.h * blitSrcHeightHires / blitSrcHeightLores;
+ IntRect srcScaled(scaledSrcX, scaledSrcY, scaledSrcWidth, scaledSrcHeight);
+
if (HAVE_NATIVE_BLIT)
{
- gl.BlitFramebuffer(src.x, src.y, src.x+src.w, src.y+src.h,
- dst.x, dst.y, dst.x+dst.w, dst.y+dst.h,
+ gl.BlitFramebuffer(srcScaled.x, srcScaled.y, srcScaled.x+srcScaled.w, srcScaled.y+srcScaled.h,
+ dstScaled.x, dstScaled.y, dstScaled.x+dstScaled.w, dstScaled.y+dstScaled.h,
GL_COLOR_BUFFER_BIT, smooth ? GL_LINEAR : GL_NEAREST);
}
else
@@ -218,7 +281,7 @@ void blitRectangle(const IntRect &src, const IntRect &dst, bool smooth)
glState.blend.pushSet(false);
Quad &quad = shState->gpQuad();
- quad.setTexPosRect(src, dst);
+ quad.setTexPosRect(srcScaled, dstScaled);
quad.draw();
glState.blend.pop();
@@ -229,8 +292,19 @@ void blitRectangle(const IntRect &src, const IntRect &dst, bool smooth)
void blitEnd()
{
- if (!HAVE_NATIVE_BLIT)
+ blitDstWidthLores = 1;
+ blitDstWidthHires = 1;
+ blitDstHeightLores = 1;
+ blitDstHeightHires = 1;
+
+ blitSrcWidthLores = 1;
+ blitSrcWidthHires = 1;
+ blitSrcHeightLores = 1;
+ blitSrcHeightHires = 1;
+
+ if (!HAVE_NATIVE_BLIT) {
glState.viewport.pop();
+ }
}
}
diff --git a/src/display/gl/gl-meta.h b/src/display/gl/gl-meta.h
index fd8cf8ab..13ef1ffd 100644
--- a/src/display/gl/gl-meta.h
+++ b/src/display/gl/gl-meta.h
@@ -65,7 +65,7 @@ void vaoBind(VAO &vao);
void vaoUnbind(VAO &vao);
/* EXT_framebuffer_blit */
-void blitBegin(TEXFBO &target);
+void blitBegin(TEXFBO &target, bool preferHires = false);
void blitBeginScreen(const Vec2i &size);
void blitSource(TEXFBO &source);
void blitRectangle(const IntRect &src, const Vec2i &dstPos);
diff --git a/src/display/gl/gl-util.h b/src/display/gl/gl-util.h
index 945af787..212040e2 100644
--- a/src/display/gl/gl-util.h
+++ b/src/display/gl/gl-util.h
@@ -109,6 +109,8 @@ namespace FBO
{
DEF_GL_ID
+ extern ID boundFramebufferID;
+
inline ID gen()
{
ID id;
@@ -124,6 +126,7 @@ namespace FBO
static inline void bind(ID id)
{
+ boundFramebufferID = id;
gl.BindFramebuffer(GL_FRAMEBUFFER, id.gl);
}
@@ -203,8 +206,10 @@ struct TEXFBO
FBO::ID fbo;
int width, height;
+ TEXFBO *selfHires;
+
TEXFBO()
- : tex(0), fbo(0), width(0), height(0)
+ : tex(0), fbo(0), width(0), height(0), selfHires(nullptr)
{}
bool operator==(const TEXFBO &other) const
diff --git a/src/display/gl/glstate.cpp b/src/display/gl/glstate.cpp
index 6bf78d74..d021d887 100644
--- a/src/display/gl/glstate.cpp
+++ b/src/display/gl/glstate.cpp
@@ -23,7 +23,9 @@
#include "config.h"
#include "etc.h"
#include "gl-fun.h"
+#include "graphics.h"
#include "shader.h"
+#include "sharedstate.h"
#include
@@ -36,7 +38,19 @@ void GLClearColor::apply(const Vec4 &value) {
}
void GLScissorBox::apply(const IntRect &value) {
- gl.Scissor(value.x, value.y, value.w, value.h);
+ // High-res: scale the scissorbox if we're rendering to the PingPong framebuffer.
+ if (shState) {
+ const double framebufferScalingFactor = shState->config().framebufferScalingFactor;
+ if (shState->config().enableHires && shState->graphics().isPingPongFramebufferActive()) {
+ gl.Scissor((int)lround(framebufferScalingFactor * value.x), (int)lround(framebufferScalingFactor * value.y), (int)lround(framebufferScalingFactor * value.w), (int)lround(framebufferScalingFactor * value.h));
+ }
+ else {
+ gl.Scissor(value.x, value.y, value.w, value.h);
+ }
+ }
+ else {
+ gl.Scissor(value.x, value.y, value.w, value.h);
+ }
}
void GLScissorBox::setIntersect(const IntRect &value) {
diff --git a/src/display/gl/quad.h b/src/display/gl/quad.h
index d200c6b5..ecff83e4 100644
--- a/src/display/gl/quad.h
+++ b/src/display/gl/quad.h
@@ -22,6 +22,8 @@
#ifndef QUAD_H
#define QUAD_H
+#include "config.h"
+#include "graphics.h"
#include "vertex.h"
#include "gl-util.h"
#include "gl-meta.h"
diff --git a/src/display/gl/shader.cpp b/src/display/gl/shader.cpp
index 53aea311..28bb2ed0 100644
--- a/src/display/gl/shader.cpp
+++ b/src/display/gl/shader.cpp
@@ -20,6 +20,8 @@
*/
#include "shader.h"
+#include "config.h"
+#include "graphics.h"
#include "sharedstate.h"
#include "glstate.h"
#include "exception.h"
@@ -293,8 +295,19 @@ void ShaderBase::init()
void ShaderBase::applyViewportProj()
{
+ // High-res: scale the matrix if we're rendering to the PingPong framebuffer.
const IntRect &vp = glState.viewport.get();
- projMat.set(Vec2i(vp.w, vp.h));
+ if (shState->config().enableHires && shState->graphics().isPingPongFramebufferActive() && framebufferScalingAllowed()) {
+ projMat.set(Vec2i(shState->graphics().width(), shState->graphics().height()));
+ }
+ else {
+ projMat.set(Vec2i(vp.w, vp.h));
+ }
+}
+
+bool ShaderBase::framebufferScalingAllowed()
+{
+ return true;
}
void ShaderBase::setTexSize(const Vec2i &value)
@@ -593,6 +606,13 @@ GrayShader::GrayShader()
GET_U(gray);
}
+bool GrayShader::framebufferScalingAllowed()
+{
+ // This shader is used with input textures that have already had a
+ // framebuffer scale applied. So we don't want to double-apply it.
+ return false;
+}
+
void GrayShader::setGray(float value)
{
gl.Uniform1f(u_gray, value);
diff --git a/src/display/gl/shader.h b/src/display/gl/shader.h
index aeec2cfb..fd79dfbc 100644
--- a/src/display/gl/shader.h
+++ b/src/display/gl/shader.h
@@ -91,6 +91,7 @@ public:
protected:
void init();
+ virtual bool framebufferScalingAllowed();
GLint u_texSizeInv, u_translation;
};
@@ -226,6 +227,9 @@ public:
void setGray(float value);
+protected:
+ virtual bool framebufferScalingAllowed();
+
private:
GLint u_gray;
};
diff --git a/src/display/gl/tileatlasvx.cpp b/src/display/gl/tileatlasvx.cpp
index 116fc305..aa6a78f8 100644
--- a/src/display/gl/tileatlasvx.cpp
+++ b/src/display/gl/tileatlasvx.cpp
@@ -24,6 +24,7 @@
#include "tilemap-common.h"
#include "bitmap.h"
#include "table.h"
+#include "debugwriter.h"
#include "etc-internal.h"
#include "gl-util.h"
#include "gl-meta.h"
@@ -273,7 +274,7 @@ void build(TEXFBO &tf, Bitmap *bitmaps[BM_COUNT])
{
assert(tf.width == ATLASVX_W && tf.height == ATLASVX_H);
- GLMeta::blitBegin(tf);
+ GLMeta::blitBegin(tf, true);
glState.clearColor.pushSet(Vec4());
FBO::clear();
@@ -282,13 +283,36 @@ void build(TEXFBO &tf, Bitmap *bitmaps[BM_COUNT])
if (rgssVer >= 3)
{
SDL_Surface *shadow = createShadowSet();
- TEX::bind(tf.tex);
- TEX::uploadSubImage(shadowArea.x*32, shadowArea.y*32,
- shadow->w, shadow->h, shadow->pixels, GL_RGBA);
+ if (tf.selfHires != nullptr) {
+ SDL_Rect srcRect({0, 0, shadow->w, shadow->h});
+ int destX = shadowArea.x*32 * tf.selfHires->width / tf.width;
+ int destY = shadowArea.y*32 * tf.selfHires->height / tf.height;
+ int destWidth = shadow->w * tf.selfHires->width / tf.width;
+ int destHeight = shadow->h * tf.selfHires->height / tf.height;
+
+ int bpp;
+ Uint32 rMask, gMask, bMask, aMask;
+ SDL_PixelFormatEnumToMasks(SDL_PIXELFORMAT_ABGR8888,
+ &bpp, &rMask, &gMask, &bMask, &aMask);
+ SDL_Surface *blitTemp =
+ SDL_CreateRGBSurface(0, destWidth, destHeight, bpp, rMask, gMask, bMask, aMask);
+
+ SDL_BlitScaled(shadow, &srcRect, blitTemp, 0);
+
+ TEX::bind(tf.selfHires->tex);
+ TEX::uploadSubImage(destX, destY,
+ blitTemp->w, blitTemp->h, blitTemp->pixels, GL_RGBA);
+ }
+ else {
+ TEX::bind(tf.tex);
+ TEX::uploadSubImage(shadowArea.x*32, shadowArea.y*32,
+ shadow->w, shadow->h, shadow->pixels, GL_RGBA);
+ }
SDL_FreeSurface(shadow);
}
Bitmap *bm;
+
#define EXEC_BLITS(part) \
if (!nullOrDisposed(bm = bitmaps[BM_##part])) \
{ \
diff --git a/src/display/graphics.cpp b/src/display/graphics.cpp
index 366fe980..2a9fe935 100644
--- a/src/display/graphics.cpp
+++ b/src/display/graphics.cpp
@@ -772,6 +772,7 @@ struct GraphicsPrivate {
* RGSS renders at (settable with Graphics.resize_screen).
* Can only be changed from within RGSS */
Vec2i scRes;
+ Vec2i scResLores;
/* Screen size, to which the rendered frames are scaled up.
* This can be smaller than the window size when fixed aspect
@@ -828,7 +829,7 @@ struct GraphicsPrivate {
IntruList dispList;
GraphicsPrivate(RGSSThreadData *rtData)
- : scRes(DEF_SCREEN_W, DEF_SCREEN_H), scSize(scRes),
+ : scRes(DEF_SCREEN_W, DEF_SCREEN_H), scResLores(scRes), scSize(scRes),
winSize(rtData->config.defScreenW, rtData->config.defScreenH),
screen(scRes.x, scRes.y), threadData(rtData),
glCtx(SDL_GL_GetCurrentContext()), multithreadedMode(true),
@@ -999,11 +1000,15 @@ struct GraphicsPrivate {
}
void compositeToBuffer(TEXFBO &buffer) {
+ compositeToBufferScaled(buffer, scRes.x, scRes.y);
+ }
+
+ void compositeToBufferScaled(TEXFBO &buffer, int destWidth, int destHeight) {
screen.composite();
GLMeta::blitBegin(buffer);
GLMeta::blitSource(screen.getPP().frontBuffer());
- GLMeta::blitRectangle(IntRect(0, 0, scRes.x, scRes.y), Vec2i());
+ GLMeta::blitRectangle(IntRect(0, 0, scRes.x, scRes.y), IntRect(0, 0, destWidth, destHeight));
GLMeta::blitEnd();
}
@@ -1229,22 +1234,28 @@ void Graphics::transition(int duration, const char *filename, int vague) {
TransShader &transShader = shState->shaders().trans;
SimpleTransShader &simpleShader = shState->shaders().simpleTrans;
+ // Handle high-res.
+ Vec2i transSize(p->scResLores.x, p->scResLores.y);
+
if (transMap) {
TransShader &shader = transShader;
shader.bind();
shader.applyViewportProj();
shader.setFrozenScene(p->frozenScene.tex);
shader.setCurrentScene(currentScene.tex);
+ if (transMap->hasHires()) {
+ Debug() << "BUG: High-res Graphics transMap not implemented";
+ }
shader.setTransMap(transMap->getGLTypes().tex);
shader.setVague(vague / 256.0f);
- shader.setTexSize(p->scRes);
+ shader.setTexSize(transSize);
} else {
SimpleTransShader &shader = simpleShader;
shader.bind();
shader.applyViewportProj();
shader.setFrozenScene(p->frozenScene.tex);
shader.setCurrentScene(currentScene.tex);
- shader.setTexSize(p->scRes);
+ shader.setTexSize(transSize);
}
glState.blend.pushSet(false);
@@ -1391,16 +1402,36 @@ void Graphics::fadein(int duration) {
Bitmap *Graphics::snapToBitmap() {
Bitmap *bitmap = new Bitmap(width(), height());
- p->compositeToBuffer(bitmap->getGLTypes());
+ if (bitmap->hasHires()) {
+ p->compositeToBufferScaled(bitmap->getHires()->getGLTypes(), bitmap->getHires()->width(), bitmap->getHires()->height());
+ }
+
+ p->compositeToBufferScaled(bitmap->getGLTypes(), bitmap->width(), bitmap->height());
/* Taint entire bitmap */
bitmap->taintArea(IntRect(0, 0, width(), height()));
return bitmap;
}
-int Graphics::width() const { return p->scRes.x; }
+int Graphics::width() const { return p->scResLores.x; }
-int Graphics::height() const { return p->scRes.y; }
+int Graphics::height() const { return p->scResLores.y; }
+
+int Graphics::widthHires() const { return p->scRes.x; }
+
+int Graphics::heightHires() const { return p->scRes.y; }
+
+bool Graphics::isPingPongFramebufferActive() const {
+ return p->screen.getPP().frontBuffer().fbo == FBO::boundFramebufferID || p->screen.getPP().backBuffer().fbo == FBO::boundFramebufferID;
+}
+
+int Graphics::displayContentWidth() const {
+ return p->scSize.x;
+}
+
+int Graphics::displayContentHeight() const {
+ return p->scSize.y;
+}
int Graphics::displayWidth() const {
SDL_DisplayMode dm{};
@@ -1418,12 +1449,21 @@ void Graphics::resizeScreen(int width, int height) {
p->threadData->rqWindowAdjust.wait();
p->checkResize(true);
+ Vec2i sizeLores(width, height);
+
+ if (shState->config().enableHires) {
+ double framebufferScalingFactor = shState->config().framebufferScalingFactor;
+ width = (int)lround(framebufferScalingFactor * width);
+ height = (int)lround(framebufferScalingFactor * height);
+ }
+
Vec2i size(width, height);
if (p->scRes == size)
return;
p->scRes = size;
+ p->scResLores = sizeLores;
p->screen.setResolution(width, height);
@@ -1459,6 +1499,10 @@ bool Graphics::updateMovieInput(Movie *movie) {
}
void Graphics::playMovie(const char *filename, int volume_, bool skippable) {
+ if (shState->config().enableHires) {
+ Debug() << "BUG: High-res Graphics playMovie not implemented";
+ }
+
Movie *movie = new Movie(skippable);
MovieOpenHandler handler(movie->srcOps);
shState->fileSystem().openRead(handler, filename);
diff --git a/src/display/graphics.h b/src/display/graphics.h
index 90bcd74a..130478f3 100644
--- a/src/display/graphics.h
+++ b/src/display/graphics.h
@@ -58,6 +58,11 @@ public:
int width() const;
int height() const;
+ int widthHires() const;
+ int heightHires() const;
+ bool isPingPongFramebufferActive() const;
+ int displayContentWidth() const;
+ int displayContentHeight() const;
int displayWidth() const;
int displayHeight() const;
void resizeScreen(int width, int height);
diff --git a/src/display/sprite.cpp b/src/display/sprite.cpp
index 5c76297d..5b7a3766 100644
--- a/src/display/sprite.cpp
+++ b/src/display/sprite.cpp
@@ -23,6 +23,7 @@
#include "sharedstate.h"
#include "bitmap.h"
+#include "debugwriter.h"
#include "etc.h"
#include "etc-internal.h"
#include "util.h"
@@ -605,6 +606,10 @@ void Sprite::draw()
shader.setBushOpacity(p->bushOpacity.norm);
if (p->pattern && p->patternOpacity > 0) {
+ if (p->pattern->hasHires()) {
+ Debug() << "BUG: High-res Sprite pattern not implemented";
+ }
+
shader.setPattern(p->pattern->getGLTypes().tex, Vec2(p->pattern->width(), p->pattern->height()));
shader.setPatternBlendType(p->patternBlendType);
shader.setPatternTile(p->patternTile);
diff --git a/src/display/tilemap.cpp b/src/display/tilemap.cpp
index 2cd30c46..bc2da7df 100644
--- a/src/display/tilemap.cpp
+++ b/src/display/tilemap.cpp
@@ -27,6 +27,7 @@
#include "sharedstate.h"
#include "config.h"
+#include "debugwriter.h"
#include "glstate.h"
#include "gl-util.h"
#include "gl-meta.h"
@@ -550,6 +551,10 @@ struct TilemapPrivate
int blitW = std::min(atW, atAreaW);
int blitH = std::min(atH, autotileH);
+ if (autotile->hasHires()) {
+ Debug() << "BUG: High-res Tilemap blit autotiles not implemented";
+ }
+
GLMeta::blitSource(autotile->getGLTypes());
if (atW <= autotileW && tiles.animated && !atlas.smallATs[atInd])
@@ -639,6 +644,10 @@ struct TilemapPrivate
}
else
{
+ if (tileset->hasHires()) {
+ Debug() << "BUG: High-res Tilemap regular tileset not implemented";
+ }
+
/* Regular tileset */
GLMeta::blitBegin(atlas.gl);
GLMeta::blitSource(tileset->getGLTypes());
diff --git a/src/display/tilemapvx.cpp b/src/display/tilemapvx.cpp
index 6dfe2849..678f1a60 100644
--- a/src/display/tilemapvx.cpp
+++ b/src/display/tilemapvx.cpp
@@ -72,6 +72,8 @@ struct TilemapVXPrivate : public ViewportElement, TileAtlasVX::Reader
VBO::ID vbo;
GLMeta::VAO vao;
+ TEXFBO atlasHires;
+
size_t allocQuads;
size_t groundQuads;
@@ -132,6 +134,14 @@ struct TilemapVXPrivate : public ViewportElement, TileAtlasVX::Reader
shState->requestAtlasTex(ATLASVX_W, ATLASVX_H, atlas);
+ if (shState->config().enableHires) {
+ double scalingFactor = shState->config().atlasScalingFactor;
+ int hiresWidth = (int)lround(scalingFactor * ATLASVX_W);
+ int hiresHeight = (int)lround(scalingFactor * ATLASVX_H);
+ shState->requestAtlasTex(hiresWidth, hiresHeight, atlasHires);
+ atlas.selfHires = &atlasHires;
+ }
+
vbo = VBO::gen();
GLMeta::vaoFillInVertexData(vao);
@@ -151,6 +161,9 @@ struct TilemapVXPrivate : public ViewportElement, TileAtlasVX::Reader
VBO::del(vbo);
shState->releaseAtlasTex(atlas);
+ if (shState->config().enableHires) {
+ shState->releaseAtlasTex(atlasHires);
+ }
prepareCon.disconnect();
@@ -310,7 +323,12 @@ struct TilemapVXPrivate : public ViewportElement, TileAtlasVX::Reader
shader->applyViewportProj();
shader->setTranslation(dispPos);
- TEX::bind(atlas.tex);
+ if (atlas.selfHires != nullptr) {
+ TEX::bind(atlas.selfHires->tex);
+ }
+ else {
+ TEX::bind(atlas.tex);
+ }
GLMeta::vaoBind(vao);
gl.DrawElements(GL_TRIANGLES, groundQuads*6, _GL_INDEX_TYPE, 0);
@@ -329,7 +347,12 @@ struct TilemapVXPrivate : public ViewportElement, TileAtlasVX::Reader
shader.applyViewportProj();
shader.setTranslation(dispPos);
- TEX::bind(atlas.tex);
+ if (atlas.selfHires != nullptr) {
+ TEX::bind(atlas.selfHires->tex);
+ }
+ else {
+ TEX::bind(atlas.tex);
+ }
GLMeta::vaoBind(vao);
gl.DrawElements(GL_TRIANGLES, aboveQuads*6, _GL_INDEX_TYPE,
diff --git a/tests/hires-bitmap/Graphics/Pictures/children-alpha-lo.jxl b/tests/hires-bitmap/Graphics/Pictures/children-alpha-lo.jxl
new file mode 100644
index 00000000..69fa96e3
Binary files /dev/null and b/tests/hires-bitmap/Graphics/Pictures/children-alpha-lo.jxl differ
diff --git a/tests/hires-bitmap/Graphics/Pictures/children-alpha.jxl b/tests/hires-bitmap/Graphics/Pictures/children-alpha.jxl
new file mode 100644
index 00000000..69fa96e3
Binary files /dev/null and b/tests/hires-bitmap/Graphics/Pictures/children-alpha.jxl differ
diff --git a/tests/hires-bitmap/Graphics/Pictures/tree_alpha_16bit-lo.jxl b/tests/hires-bitmap/Graphics/Pictures/tree_alpha_16bit-lo.jxl
new file mode 100644
index 00000000..f2640c59
Binary files /dev/null and b/tests/hires-bitmap/Graphics/Pictures/tree_alpha_16bit-lo.jxl differ
diff --git a/tests/hires-bitmap/Graphics/Pictures/tree_alpha_16bit.jxl b/tests/hires-bitmap/Graphics/Pictures/tree_alpha_16bit.jxl
new file mode 100644
index 00000000..f2640c59
Binary files /dev/null and b/tests/hires-bitmap/Graphics/Pictures/tree_alpha_16bit.jxl differ
diff --git a/tests/hires-bitmap/Hires/Graphics/Pictures/children-alpha.jxl b/tests/hires-bitmap/Hires/Graphics/Pictures/children-alpha.jxl
new file mode 100644
index 00000000..c70c55d8
Binary files /dev/null and b/tests/hires-bitmap/Hires/Graphics/Pictures/children-alpha.jxl differ
diff --git a/tests/hires-bitmap/Hires/Graphics/Pictures/tree_alpha_16bit.jxl b/tests/hires-bitmap/Hires/Graphics/Pictures/tree_alpha_16bit.jxl
new file mode 100644
index 00000000..bd0ce9d4
Binary files /dev/null and b/tests/hires-bitmap/Hires/Graphics/Pictures/tree_alpha_16bit.jxl differ
diff --git a/tests/hires-bitmap/hires-bitmap-test.rb b/tests/hires-bitmap/hires-bitmap-test.rb
new file mode 100755
index 00000000..3a62d0f9
--- /dev/null
+++ b/tests/hires-bitmap/hires-bitmap-test.rb
@@ -0,0 +1,412 @@
+# Test suite for mkxp-z high-res Bitmap replacement.
+# Bitmap tests.
+# Copyright 2023 Splendide Imaginarius.
+# License GPLv2+.
+# Test images are from https://github.com/xinntao/Real-ESRGAN/
+#
+# Run the suite via the "customScript" field in mkxp.json.
+# Use RGSS v3 for best results.
+
+def dump(bmp, spr, desc)
+ spr.bitmap = bmp
+ Graphics.wait(1)
+ bmp.to_file("test-results/" + desc + "-lo.png")
+ if !bmp.hires.nil?
+ bmp.hires.to_file("test-results/" + desc + "-hi.png")
+ end
+ System::puts("Finished " + desc)
+end
+
+# Setup graphics
+Graphics.resize_screen(640, 480)
+
+# Setup font
+fnt = Font.new("Liberation Sans", 100)
+
+# Setup splash screen
+bmp = Bitmap.new(640, 480)
+bmp.fill_rect(0, 0, 640, 480, Color.new(0, 0, 0))
+
+bmp.font = fnt
+bmp.draw_text(0, 0, 640, 240, "High-Res Test Suite", 1)
+bmp.draw_text(0, 240, 640, 240, "Starting Now", 1)
+
+spr = Sprite.new()
+spr.bitmap = bmp
+
+Graphics.wait(1 * 60)
+
+# Tests start here
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+dump(bmp, spr, "constructor-filename")
+
+# TODO: Filename GIF constructor
+
+bmp = Bitmap.new(640, 480)
+bmp.clear
+dump(bmp, spr, "constructor-dimensions")
+
+# TODO: Animation constructor
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.clear
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-clear-tree-lo-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.clear
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-clear-tree-lo-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.clear
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-clear-tree-lo-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.clear
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-clear-tree-hi-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.clear
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-clear-tree-hi-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.clear
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-clear-tree-hi-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.fill_rect(bmp.rect, Color.new(0, 0, 0))
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-black-tree-lo-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.fill_rect(bmp.rect, Color.new(0, 0, 0))
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-black-tree-lo-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.fill_rect(bmp.rect, Color.new(0, 0, 0))
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-black-tree-lo-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.fill_rect(bmp.rect, Color.new(0, 0, 0))
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-black-tree-hi-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.fill_rect(bmp.rect, Color.new(0, 0, 0))
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-black-tree-hi-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp.fill_rect(bmp.rect, Color.new(0, 0, 0))
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-black-tree-hi-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit-lo")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-lo-tree-lo-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit-lo")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-lo-tree-lo-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit-lo")
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-lo-tree-lo-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit-lo")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-lo-tree-hi-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit-lo")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-lo-tree-hi-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit-lo")
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-lo-tree-hi-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-hi-tree-lo-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-hi-tree-lo-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-hi-tree-lo-children-quarter-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect)
+dump(bmp, spr, "stretch-blt-hi-tree-hi-children-full-opaque")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.stretch_blt(bmp.rect, bmp2, bmp2.rect, 127)
+dump(bmp, spr, "stretch-blt-hi-tree-hi-children-full-semitransparent")
+
+bmp = Bitmap.new("Graphics/Pictures/tree_alpha_16bit")
+rect = bmp.rect
+rect.width /= 2
+rect.height /= 2
+rect.x = rect.width
+rect.y = rect.height
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+rect2 = bmp2.rect
+rect2.width /= 2
+rect2.height /= 2
+rect2.x = rect2.width
+bmp.stretch_blt(rect, bmp2, rect2, 127)
+dump(bmp, spr, "stretch-blt-hi-tree-hi-children-quarter-semitransparent")
+
+bmp = Bitmap.new(640, 480)
+bmp.fill_rect(100, 200, 450, 300, Color.new(0, 0, 0))
+bmp.fill_rect(50, 100, 220, 150, Color.new(255, 0, 0))
+dump(bmp, spr, "fill-rect")
+
+bmp = Bitmap.new(640, 480)
+bmp.gradient_fill_rect(100, 200, 450, 300, Color.new(0, 0, 0), Color.new(0, 0, 255))
+bmp.gradient_fill_rect(50, 100, 220, 150, Color.new(255, 0, 0), Color.new(255, 255, 0))
+dump(bmp, spr, "gradient-fill-rect-horizontal")
+
+bmp = Bitmap.new(640, 480)
+bmp.gradient_fill_rect(100, 200, 450, 300, Color.new(0, 0, 0), Color.new(0, 0, 255), true)
+bmp.gradient_fill_rect(50, 100, 220, 150, Color.new(255, 0, 0), Color.new(255, 255, 0), true)
+dump(bmp, spr, "gradient-fill-rect-vertical")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.clear_rect(300, 175, 100, 150)
+dump(bmp, spr, "clear-rect-lo-children")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.clear_rect(300, 175, 100, 150)
+dump(bmp, spr, "clear-rect-hi-children")
+
+# TODO: linear-blur is arguably passing but maybe should have stronger blur?
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.blur
+dump(bmp, spr, "linear-blur")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.radial_blur(0, 10)
+dump(bmp, spr, "radial-blur-0-lo-children")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.radial_blur(0, 10)
+dump(bmp, spr, "radial-blur-0-hi-children")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.radial_blur(3, 10)
+dump(bmp, spr, "radial-blur-3-lo-children")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.radial_blur(3, 10)
+dump(bmp, spr, "radial-blur-3-hi-children")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.clear
+dump(bmp, spr, "clear-full")
+
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp = Bitmap.new(bmp2.width, bmp2.height)
+for x in (0...bmp2.width)
+ for y in (0...bmp2.height)
+ bmp.set_pixel(x, y, bmp2.get_pixel(x, y))
+ end
+end
+dump(bmp, spr, "get-set-pixel-dimensions")
+
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.clear
+for x in (0...bmp2.width)
+ for y in (0...bmp2.height)
+ bmp.set_pixel(x, y, bmp2.get_pixel(x, y))
+ end
+end
+dump(bmp, spr, "get-set-pixel-clear")
+
+bmp2 = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.hires.clear
+for x in (0...bmp2.hires.width)
+ for y in (0...bmp2.hires.height)
+ bmp.hires.set_pixel(x, y, bmp2.hires.get_pixel(x, y))
+ end
+end
+dump(bmp, spr, "get-set-pixel-direct")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha-lo")
+bmp.hue_change(180)
+dump(bmp, spr, "hue-change-lo-children")
+
+bmp = Bitmap.new("Graphics/Pictures/children-alpha")
+bmp.hue_change(180)
+dump(bmp, spr, "hue-change-hi-children")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 100)
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 1)
+dump(bmp, spr, "draw-text-plain")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 15)
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 0)
+dump(bmp, spr, "draw-text-left")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 15)
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 2)
+dump(bmp, spr, "draw-text-right")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 100)
+fnt.bold = true
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 1)
+dump(bmp, spr, "draw-text-bold")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 100)
+fnt.italic = true
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 1)
+dump(bmp, spr, "draw-text-italic")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 100)
+fnt.color = Color.new(255, 0, 0)
+fnt.outline = false
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 1)
+dump(bmp, spr, "draw-text-red-no-outline")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 100)
+fnt.color = Color.new(255, 127, 127)
+fnt.shadow = true
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 1)
+dump(bmp, spr, "draw-text-pink-shadow")
+
+bmp = Bitmap.new(640, 480)
+fnt = Font.new("Liberation Sans", 100)
+fnt.out_color = Color.new(0, 255, 0)
+bmp.font = fnt
+bmp.draw_text(100, 200, 450, 300, "We <3 Real-ESRGAN", 1)
+dump(bmp, spr, "draw-text-green-outline")
+
+# TODO: Animation tests, if we can find a good way to test them.
+
+# Tests are finished, show exit screen
+
+bmp = Bitmap.new(640, 480)
+bmp.fill_rect(0, 0, 640, 480, Color.new(0, 0, 0))
+
+fnt = Font.new("Liberation Sans", 100)
+
+bmp.font = fnt
+bmp.draw_text(0, 0, 640, 240, "High-Res Test Suite", 1)
+bmp.draw_text(0, 240, 640, 240, "Has Finished", 1)
+spr.bitmap = bmp
+
+Graphics.wait(1 * 60)
+
+exit
diff --git a/tests/hires-bitmap/test-results/.RESULTS WILL GO HERE b/tests/hires-bitmap/test-results/.RESULTS WILL GO HERE
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/hires-sprite/Graphics/Pictures/OST_009-Big.jxl b/tests/hires-sprite/Graphics/Pictures/OST_009-Big.jxl
new file mode 100644
index 00000000..d969d7c8
Binary files /dev/null and b/tests/hires-sprite/Graphics/Pictures/OST_009-Big.jxl differ
diff --git a/tests/hires-sprite/Graphics/Pictures/OST_009-Small.jxl b/tests/hires-sprite/Graphics/Pictures/OST_009-Small.jxl
new file mode 100644
index 00000000..3d55e0ee
Binary files /dev/null and b/tests/hires-sprite/Graphics/Pictures/OST_009-Small.jxl differ
diff --git a/tests/hires-sprite/Graphics/Pictures/OST_009.jxl b/tests/hires-sprite/Graphics/Pictures/OST_009.jxl
new file mode 100644
index 00000000..3d55e0ee
Binary files /dev/null and b/tests/hires-sprite/Graphics/Pictures/OST_009.jxl differ
diff --git a/tests/hires-sprite/Hires/Graphics/Pictures/OST_009.jxl b/tests/hires-sprite/Hires/Graphics/Pictures/OST_009.jxl
new file mode 100644
index 00000000..d969d7c8
Binary files /dev/null and b/tests/hires-sprite/Hires/Graphics/Pictures/OST_009.jxl differ
diff --git a/tests/hires-sprite/hires-sprite-test.rb b/tests/hires-sprite/hires-sprite-test.rb
new file mode 100755
index 00000000..7c99680b
--- /dev/null
+++ b/tests/hires-sprite/hires-sprite-test.rb
@@ -0,0 +1,82 @@
+# Test suite for mkxp-z high-res Bitmap replacement.
+# Sprite tests.
+# Copyright 2023 Splendide Imaginarius.
+# License GPLv2+.
+# Test images are from https://github.com/xinntao/Real-ESRGAN/
+#
+# Run the suite via the "customScript" field in mkxp.json.
+# Use RGSS v3 for best results.
+
+def dump2(bmp, spr, desc)
+ spr.bitmap = bmp
+ Graphics.wait(1)
+ #Graphics.wait(5*60)
+ #Graphics.screenshot("test-results/" + desc + ".png")
+ shot = Graphics.snap_to_bitmap
+ shot.to_file("test-results/" + desc + "-lo.png")
+ if !shot.hires.nil?
+ shot.hires.to_file("test-results/" + desc + "-hi.png")
+ end
+ System::puts("Finished " + desc)
+end
+
+def dump(bmp, spr, desc)
+ spr.viewport = nil
+ dump2(bmp, spr, desc + "-direct")
+ spr.tone.gray = 128
+ dump2(bmp, spr, desc + "-directtonegray")
+ spr.tone.gray = 0
+ $vp.ox = 0
+ spr.viewport = $vp
+ dump2(bmp, spr, desc + "-viewport")
+ $vp.ox = 250
+ dump2(bmp, spr, desc + "-viewportshift")
+ $vp.ox = 0
+ $vp.rect.width = 320
+ dump2(bmp, spr, desc + "-viewportsquash")
+ $vp.rect.width = 640
+ $vp.tone.green = -128
+ dump2(bmp, spr, desc + "-viewporttonegreen")
+ $vp.tone.green = 0
+ $vp.tone.gray = 128
+ dump2(bmp, spr, desc + "-viewporttonegray")
+ $vp.tone.gray = 0
+end
+
+# Setup graphics
+Graphics.resize_screen(448, 640)
+
+$vp = Viewport.new()
+
+spr = Sprite.new()
+
+bmp = Bitmap.new("Graphics/Pictures/OST_009-Small")
+spr.zoom_x = 1.0
+spr.zoom_y = 1.0
+dump(bmp, spr, "Small")
+
+bmp = Bitmap.new("Graphics/Pictures/OST_009-Big")
+spr.zoom_x = 448.0 / 1792.0
+spr.zoom_y = 448.0 / 1792.0
+dump(bmp, spr, "Big")
+
+bmp = Bitmap.new("Graphics/Pictures/OST_009")
+spr.zoom_x = 1.0
+spr.zoom_y = 1.0
+dump(bmp, spr, "Substituted")
+
+bmp = Bitmap.new("Graphics/Pictures/OST_009")
+spr.zoom_x = 1.5
+spr.zoom_y = 1.5
+dump(bmp, spr, "Substituted-Zoomed")
+
+bmp = Bitmap.new("Graphics/Pictures/OST_009")
+spr.zoom_x = 448.0 / 1792.0
+spr.zoom_y = 448.0 / 1792.0
+dump(bmp.hires, spr, "Substituted-Explicit")
+
+# Test for null pointer
+spr_null = Sprite.new()
+spr_null.src_rect = Rect.new(0, 0, 448, 640)
+
+exit
diff --git a/tests/hires-sprite/test-results/.RESULTS WILL GO HERE b/tests/hires-sprite/test-results/.RESULTS WILL GO HERE
new file mode 100644
index 00000000..e69de29b