Browse Source

Clean up scaling code, implemented image size optimization

Ivan Savenko 1 year ago
parent
commit
f29a687234

+ 1 - 1
client/CPlayerInterface.cpp

@@ -1145,7 +1145,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 		if(t)
 		{
 			auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
-			image->scaleFast(Point(35, 23));
+			image->scaleTo(Point(35, 23));
 			images.push_back(image);
 		}
 	}

+ 4 - 2
client/render/IImage.h

@@ -50,7 +50,8 @@ public:
 	//draws image on surface "where" at position
 	virtual void draw(SDL_Surface * where, const Point & pos, const Rect * src = nullptr) const = 0;
 
-	virtual void scaleFast(const Point & size) = 0;
+	virtual void scaleTo(const Point & size) = 0;
+	virtual void scaleInteger(int factor) = 0;
 
 	virtual void exportBitmap(const boost::filesystem::path & path) const = 0;
 
@@ -97,7 +98,8 @@ public:
 
 	virtual std::shared_ptr<ISharedImage> horizontalFlip() const = 0;
 	virtual std::shared_ptr<ISharedImage> verticalFlip() const = 0;
-	virtual std::shared_ptr<ISharedImage> scaleFast(const Point & size, SDL_Palette * palette) const = 0;
+	virtual std::shared_ptr<ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const = 0;
+	virtual std::shared_ptr<ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
 
 
 	virtual ~ISharedImage() = default;

+ 7 - 2
client/renderSDL/ImageScaled.cpp

@@ -37,10 +37,15 @@ std::shared_ptr<ISharedImage> ImageScaled::getSharedImage() const
 	return body;
 }
 
-void ImageScaled::scaleFast(const Point &size)
+void ImageScaled::scaleInteger(int factor)
+{
+	assert(0);
+}
+
+void ImageScaled::scaleTo(const Point & size)
 {
 	if (body)
-		body = body->scaleFast(size, nullptr); // FIXME: adjust for scaling
+		body = body->scaleTo(size, nullptr); // FIXME: adjust for scaling
 }
 
 void ImageScaled::exportBitmap(const boost::filesystem::path &path) const

+ 2 - 1
client/renderSDL/ImageScaled.h

@@ -47,7 +47,8 @@ private:
 public:
 	ImageScaled(const ImageLocator & locator, const std::shared_ptr<ISharedImage> & source, EImageBlitMode mode);
 
-	void scaleFast(const Point & size) override;
+	void scaleInteger(int factor) override;
+	void scaleTo(const Point & size) override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
 	bool isTransparent(const Point & coords) const override;
 	Point dimensions() const override;

+ 1 - 1
client/renderSDL/RenderHandler.cpp

@@ -237,7 +237,7 @@ std::shared_ptr<ISharedImage> RenderHandler::scaleImage(const ImageLocator & loc
 	if (locator.layerBody && locator.playerColored != PlayerColor::CANNOT_DETERMINE)
 		handle->playerColored(locator.playerColored);
 
-	handle->scaleFast(handle->dimensions() * locator.scalingFactor);
+	handle->scaleInteger(locator.scalingFactor);
 
 	// TODO: try to optimize image size (possibly even before scaling?) - trim image borders if they are completely transparent
 	auto result = handle->getSharedImage();

+ 141 - 5
client/renderSDL/SDLImage.cpp

@@ -18,7 +18,9 @@
 #include "../render/CBitmapHandler.h"
 #include "../render/CDefFile.h"
 #include "../render/Graphics.h"
+#include "../xBRZ/xbrz.h"
 
+#include <tbb/parallel_for.h>
 #include <SDL_surface.h>
 
 class SDLImageLoader;
@@ -187,7 +189,131 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin
 		SDL_SetSurfacePalette(surf, originalPalette);
 }
 
-std::shared_ptr<ISharedImage> SDLImageShared::scaleFast(const Point & size, SDL_Palette * palette) const
+void SDLImageShared::optimizeSurface()
+{
+	if (!surf)
+		return;
+
+	int left = surf->w;
+	int top = surf->h;
+	int right = 0;
+	int bottom = 0;
+
+	// locate fully-transparent area around image
+	// H3 hadles this on format level, but mods or images scaled in runtime do not
+	if (surf->format->palette)
+	{
+		for (int y = 0; y < surf->h; ++y)
+		{
+			const uint8_t * row = (uint8_t *)surf->pixels + y * surf->pitch;
+			for (int x = 0; x < surf->w; ++x)
+			{
+				if (row[x] != 0)
+				{
+					// opaque or can be opaque (e.g. disabled shadow)
+					top = std::min(top, y);
+					left = std::min(left, x);
+					right = std::max(right, x);
+					bottom = std::max(bottom, y);
+				}
+			}
+		}
+	}
+	else
+	{
+		for (int y = 0; y < surf->h; ++y)
+		{
+			for (int x = 0; x < surf->w; ++x)
+			{
+				ColorRGBA color;
+				SDL_GetRGBA(CSDL_Ext::getPixel(surf, x, y), surf->format, &color.r, &color.g, &color.b, &color.a);
+
+				if (color.a != SDL_ALPHA_TRANSPARENT)
+				{
+					 // opaque
+					top = std::min(top, y);
+					left = std::min(left, x);
+					right = std::max(right, x);
+					bottom = std::max(bottom, y);
+				}
+			}
+		}
+	}
+
+	if (left == surf->w)
+	{
+		// empty image - simply delete it
+		SDL_FreeSurface(surf);
+		surf = nullptr;
+		return;
+	}
+
+	if (left != 0 || top != 0 || right != surf->w - 1 || bottom != surf->h - 1)
+	{
+		// non-zero border found
+		Rect newDimensions(left, top, right - left + 1, bottom - top + 1);
+		SDL_Rect rectSDL = CSDL_Ext::toSDL(newDimensions);
+		auto newSurface = CSDL_Ext::newSurface(newDimensions.dimensions(), surf);
+		SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE);
+		SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr);
+
+		SDL_FreeSurface(surf);
+		surf = newSurface;
+
+		margins.x += left;
+		margins.y += top;
+	}
+}
+
+std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const
+{
+	if (factor <= 0)
+		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
+
+	if (palette && surf->format->palette)
+		SDL_SetSurfacePalette(surf, palette);
+
+	/// Convert current surface to ARGB format suitable for xBRZ
+	/// TODO: skip its creation if this is format matches current surface (even if unlikely)
+	SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
+	SDL_Surface * scaled = CSDL_Ext::newSurface(Point(surf->w * factor, surf->h * factor), intermediate);
+
+	assert(intermediate->pitch == intermediate->w * 4);
+	assert(scaled->pitch == scaled->w * 4);
+
+	const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
+	uint32_t * dstPixels = static_cast<uint32_t*>(scaled->pixels);
+
+	// avoid excessive granulation - xBRZ prefers at least 8-16 lines per task
+	// TODO: compare performance and size of images, recheck values for potentially better parameters
+	const int granulation = std::clamp(surf->h / 64 * 8, 8, 64);
+
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [&](const tbb::blocked_range<size_t> & r)
+	{
+		xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
+	});
+
+	SDL_FreeSurface(intermediate);
+
+	auto ret = std::make_shared<SDLImageShared>(scaled);
+
+	ret->fullSize.x = fullSize.x * factor;
+	ret->fullSize.y = fullSize.y * factor;
+
+	ret->margins.x = margins.x * factor;
+	ret->margins.y = margins.y * factor;
+	ret->optimizeSurface();
+
+	// erase our own reference
+	SDL_FreeSurface(scaled);
+
+	if (surf->format->palette)
+		SDL_SetSurfacePalette(surf, originalPalette);
+
+	return ret;
+}
+
+std::shared_ptr<ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
 {
 	float scaleX = float(size.x) / dimensions().x;
 	float scaleY = float(size.y) / dimensions().y;
@@ -413,14 +539,24 @@ void SDLImageIndexed::draw(SDL_Surface * where, const Point & pos, const Rect *
 	image->draw(where, currentPalette, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
 }
 
-void SDLImageIndexed::scaleFast(const Point & size)
+void SDLImageIndexed::scaleTo(const Point & size)
+{
+	image = image->scaleTo(size, currentPalette);
+}
+
+void SDLImageRGB::scaleTo(const Point & size)
+{
+	image = image->scaleTo(size, nullptr);
+}
+
+void SDLImageIndexed::scaleInteger(int factor)
 {
-	image = image->scaleFast(size, currentPalette);
+	image = image->scaleInteger(factor, currentPalette);
 }
 
-void SDLImageRGB::scaleFast(const Point & size)
+void SDLImageRGB::scaleInteger(int factor)
 {
-	image = image->scaleFast(size, nullptr);
+	image = image->scaleInteger(factor, nullptr);
 }
 
 void SDLImageBase::exportBitmap(const boost::filesystem::path & path) const

+ 8 - 3
client/renderSDL/SDLImage.h

@@ -38,6 +38,8 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
 
+	void optimizeSurface();
+
 public:
 	//Load image from def file
 	SDLImageShared(CDefFile *data, size_t frame, size_t group=0);
@@ -55,7 +57,8 @@ public:
 	std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) override;
 	std::shared_ptr<ISharedImage> horizontalFlip() const override;
 	std::shared_ptr<ISharedImage> verticalFlip() const override;
-	std::shared_ptr<ISharedImage> scaleFast(const Point & size, SDL_Palette * palette) const override;
+	std::shared_ptr<ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const override;
+	std::shared_ptr<ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
 
 	friend class SDLImageLoader;
 };
@@ -98,7 +101,8 @@ public:
 	void playerColored(PlayerColor player) override;
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
-	void scaleFast(const Point & size) override;
+	void scaleInteger(int factor) override;
+	void scaleTo(const Point & size) override;
 
 	void setShadowEnabled(bool on) override;
 	void setBodyEnabled(bool on) override;
@@ -115,7 +119,8 @@ public:
 	void playerColored(PlayerColor player) override;
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
-	void scaleFast(const Point & size) override;
+	void scaleInteger(int factor) override;
+	void scaleTo(const Point & size) override;
 
 	void setShadowEnabled(bool on) override;
 	void setBodyEnabled(bool on) override;

+ 6 - 4
client/widgets/Images.cpp

@@ -108,7 +108,7 @@ void CPicture::setAlpha(uint8_t value)
 
 void CPicture::scaleTo(Point size)
 {
-	bg->scaleFast(size);
+	bg->scaleTo(size);
 
 	pos.w = bg->width();
 	pos.h = bg->height();
@@ -255,7 +255,7 @@ void CAnimImage::showAll(Canvas & to)
 		if(auto img = anim->getImage(targetFrame, group))
 		{
 			if(isScaled())
-				img->scaleFast(scaledSize);
+				img->scaleTo(scaledSize);
 
 			to.draw(img, pos.topLeft());
 		}
@@ -307,7 +307,7 @@ bool CAnimImage::isPlayerColored() const
 }
 
 CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & PALETTE_ALPHA) ? EImageBlitMode::ALPHA : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::ALPHA : EImageBlitMode::COLORKEY)),
 	group(Group),
 	frame(0),
 	first(0),
@@ -420,6 +420,8 @@ void CShowableAnim::blitImage(size_t frame, size_t group, Canvas & to)
 	auto img = anim->getImage(frame, group);
 	if(img)
 	{
+		if (flags & CREATURE_MODE)
+			img->setShadowEnabled(true);
 		img->setAlpha(alpha);
 		to.draw(img, pos.topLeft(), src);
 	}
@@ -440,7 +442,7 @@ void CShowableAnim::setDuration(int durationMs)
 }
 
 CCreatureAnim::CCreatureAnim(int x, int y, const AnimationPath & name, ui8 flags, ECreatureAnimType type):
-	CShowableAnim(x, y, name, flags | PALETTE_ALPHA, 100, size_t(type)) // H3 uses 100 ms per frame, irregardless of battle speed settings
+	CShowableAnim(x, y, name, flags | CREATURE_MODE, 100, size_t(type)) // H3 uses 100 ms per frame, irregardless of battle speed settings
 {
 	xOffset = 0;
 	yOffset = 0;

+ 1 - 1
client/widgets/Images.h

@@ -145,7 +145,7 @@ public:
 		BASE=1,            //base frame will be blitted before current one
 		HORIZONTAL_FLIP=2, //TODO: will be displayed rotated
 		VERTICAL_FLIP=4,   //TODO: will be displayed rotated
-		PALETTE_ALPHA=8,   // use alpha channel for images with palette. Required for creatures in battle and map objects
+		CREATURE_MODE=8,   // use alpha channel for images with palette. Required for creatures in battle and map objects
 		PLAY_ONCE=32       //play animation only once and stop at last frame
 	};
 protected:

+ 1 - 1
client/windows/CCastleInterface.cpp

@@ -889,7 +889,7 @@ void CCastleBuildings::enterCastleGate()
 			if(settings["general"]["enableUiEnhancements"].Bool())
 			{
 				auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
-				image->scaleFast(Point(35, 23));
+				image->scaleTo(Point(35, 23));
 				images.push_back(image);
 			}
 		}

+ 1 - 1
client/windows/CWindowWithArtifacts.cpp

@@ -238,7 +238,7 @@ void CWindowWithArtifacts::setCursorAnimation(const CArtifactInstance & artInst)
 	{
 		assert(artInst.getScrollSpellID().num >= 0);
 		auto image = GH.renderHandler().loadImage(AnimationPath::builtin("spellscr"), artInst.getScrollSpellID().num, 0, EImageBlitMode::COLORKEY);
-		image->scaleFast(Point(44,34));
+		image->scaleTo(Point(44,34));
 
 		CCS->curh->dragAndDropCursor(image);
 	}