2
0
Эх сурвалжийг харах

Merge pull request #5306 from IvanSavenko/xbrz_foreground

[1.6.4?] Move xbrz upscaling from foreground thread to background
Ivan Savenko 9 сар өмнө
parent
commit
722d68643f
46 өөрчлөгдсөн 1574 нэмэгдсэн , 1296 устгасан
  1. 6 2
      client/CMakeLists.txt
  2. 1 1
      client/CPlayerInterface.cpp
  3. 1 2
      client/adventureMap/CMinimap.cpp
  4. 1 2
      client/battle/BattleFieldController.cpp
  5. 2 3
      client/mapView/MapView.cpp
  6. 2 2
      client/media/CVideoHandler.cpp
  7. 1 1
      client/media/CVideoHandler.h
  8. 2 2
      client/media/IVideoPlayer.h
  9. 31 41
      client/render/AssetGenerator.cpp
  10. 2 2
      client/render/CAnimation.cpp
  11. 24 7
      client/render/Canvas.cpp
  12. 16 3
      client/render/Canvas.h
  13. 63 0
      client/render/CanvasImage.cpp
  14. 41 0
      client/render/CanvasImage.h
  15. 19 10
      client/render/IImage.h
  16. 9 4
      client/render/IRenderHandler.h
  17. 14 81
      client/render/ImageLocator.cpp
  18. 15 18
      client/render/ImageLocator.h
  19. 6 2
      client/renderSDL/CBitmapFont.cpp
  20. 6 2
      client/renderSDL/CursorHardware.cpp
  21. 1 1
      client/renderSDL/CursorSoftware.cpp
  22. 0 172
      client/renderSDL/ImageScaled.cpp
  23. 0 66
      client/renderSDL/ImageScaled.h
  24. 110 188
      client/renderSDL/RenderHandler.cpp
  25. 12 16
      client/renderSDL/RenderHandler.h
  26. 139 401
      client/renderSDL/SDLImage.cpp
  27. 14 63
      client/renderSDL/SDLImage.h
  28. 233 0
      client/renderSDL/SDLImageScaler.cpp
  29. 63 0
      client/renderSDL/SDLImageScaler.h
  30. 1 85
      client/renderSDL/SDL_Extensions.cpp
  31. 0 12
      client/renderSDL/SDL_Extensions.h
  32. 532 0
      client/renderSDL/ScalableImage.cpp
  33. 125 0
      client/renderSDL/ScalableImage.h
  34. 5 4
      client/renderSDL/ScreenHandler.cpp
  35. 9 6
      client/widgets/Images.cpp
  36. 2 0
      client/widgets/Images.h
  37. 1 2
      client/widgets/TextControls.cpp
  38. 2 2
      client/widgets/VideoWidget.cpp
  39. 2 3
      client/windows/CCastleInterface.cpp
  40. 14 15
      client/windows/CMapOverview.cpp
  41. 5 5
      client/windows/CMapOverview.h
  42. 1 2
      client/windows/CQuestLog.cpp
  43. 26 66
      client/windows/CWindowObject.cpp
  44. 1 1
      client/windows/CWindowWithArtifacts.cpp
  45. 9 1
      docs/modders/HD_Graphics.md
  46. 5 0
      lib/Point.h

+ 6 - 2
client/CMakeLists.txt

@@ -88,6 +88,7 @@ set(vcmiclientcommon_SRCS
 	render/CBitmapHandler.cpp
 	render/CDefFile.cpp
 	render/Canvas.cpp
+	render/CanvasImage.cpp
 	render/ColorFilter.cpp
 	render/Colors.cpp
 	render/Graphics.cpp
@@ -99,10 +100,11 @@ set(vcmiclientcommon_SRCS
 	renderSDL/CursorHardware.cpp
 	renderSDL/CursorSoftware.cpp
 	renderSDL/FontChain.cpp
-	renderSDL/ImageScaled.cpp
+	renderSDL/ScalableImage.cpp
 	renderSDL/RenderHandler.cpp
 	renderSDL/SDLImage.cpp
 	renderSDL/SDLImageLoader.cpp
+	renderSDL/SDLImageScaler.cpp
 	renderSDL/SDLRWwrapper.cpp
 	renderSDL/ScreenHandler.cpp
 	renderSDL/SDL_Extensions.cpp
@@ -290,6 +292,7 @@ set(vcmiclientcommon_HEADERS
 	render/CBitmapHandler.h
 	render/CDefFile.h
 	render/Canvas.h
+	render/CanvasImage.h
 	render/ColorFilter.h
 	render/Colors.h
 	render/EFont.h
@@ -307,10 +310,11 @@ set(vcmiclientcommon_HEADERS
 	renderSDL/CursorHardware.h
 	renderSDL/CursorSoftware.h
 	renderSDL/FontChain.h
-	renderSDL/ImageScaled.h
+	renderSDL/ScalableImage.h
 	renderSDL/RenderHandler.h
 	renderSDL/SDLImage.h
 	renderSDL/SDLImageLoader.h
+	renderSDL/SDLImageScaler.h
 	renderSDL/SDLRWwrapper.h
 	renderSDL/ScreenHandler.h
 	renderSDL/SDL_Extensions.h

+ 1 - 1
client/CPlayerInterface.cpp

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

+ 1 - 2
client/adventureMap/CMinimap.cpp

@@ -22,7 +22,6 @@
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/Graphics.h"
-#include "../renderSDL/SDL_Extensions.h"
 #include "../windows/InfoWindows.h"
 
 #include "../../CCallback.h"
@@ -178,7 +177,7 @@ void CMinimap::mouseDragged(const Point & cursorPosition, const Point & lastUpda
 
 void CMinimap::showAll(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), aiShield->pos);
+	CanvasClipRectGuard guard(to, aiShield->pos);
 	CIntObject::showAll(to);
 
 	if(minimap)

+ 1 - 2
client/battle/BattleFieldController.cpp

@@ -26,7 +26,6 @@
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
-#include "../renderSDL/SDL_Extensions.h"
 #include "../render/IRenderHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
@@ -857,7 +856,7 @@ void BattleFieldController::tick(uint32_t msPassed)
 
 void BattleFieldController::show(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
+	CanvasClipRectGuard guard(to, pos);
 
 	renderBattlefield(to);
 

+ 2 - 3
client/mapView/MapView.cpp

@@ -24,7 +24,6 @@
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
-#include "../renderSDL/SDL_Extensions.h"
 #include "../eventsSDL/InputHandler.h"
 
 #include "../../CCallback.h"
@@ -76,7 +75,7 @@ void BasicMapView::tick(uint32_t msPassed)
 
 void BasicMapView::show(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
+	CanvasClipRectGuard guard(to, pos);
 	render(to, false);
 
 	controller->afterRender();
@@ -84,7 +83,7 @@ void BasicMapView::show(Canvas & to)
 
 void BasicMapView::showAll(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
+	CanvasClipRectGuard guard(to, pos);
 	render(to, true);
 }
 

+ 2 - 2
client/media/CVideoHandler.cpp

@@ -381,12 +381,12 @@ Point CVideoInstance::size()
 	return dimensions / GH.screenHandler().getScalingFactor();
 }
 
-void CVideoInstance::show(const Point & position, Canvas & canvas)
+void CVideoInstance::show(const Point & position, SDL_Surface * to)
 {
 	if(sws == nullptr)
 		throw std::runtime_error("No video to show!");
 
-	CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position * GH.screenHandler().getScalingFactor());
+	CSDL_Ext::blitSurface(surface, to, position * GH.screenHandler().getScalingFactor());
 }
 
 double FFMpegStream::getCurrentFrameEndTime() const

+ 1 - 1
client/media/CVideoHandler.h

@@ -98,7 +98,7 @@ public:
 	bool videoEnded() final;
 	Point size() final;
 
-	void show(const Point & position, Canvas & canvas) final;
+	void show(const Point & position, SDL_Surface * to) final;
 	void tick(uint32_t msPassed) final;
 	void activate() final;
 	void deactivate() final;

+ 2 - 2
client/media/IVideoPlayer.h

@@ -11,7 +11,7 @@
 
 #include "../lib/filesystem/ResourcePath.h"
 
-class Canvas;
+struct SDL_Surface;
 
 VCMI_LIB_NAMESPACE_BEGIN
 class Point;
@@ -30,7 +30,7 @@ public:
 	virtual Point size() = 0;
 
 	/// Displays current frame at specified position
-	virtual void show(const Point & position, Canvas & canvas) = 0;
+	virtual void show(const Point & position, SDL_Surface * to) = 0;
 
 	/// Advances video playback by specified duration
 	virtual void tick(uint32_t msPassed) = 0;

+ 31 - 41
client/render/AssetGenerator.cpp

@@ -14,6 +14,7 @@
 #include "../render/IImage.h"
 #include "../render/IImageLoader.h"
 #include "../render/Canvas.h"
+#include "../render/CanvasImage.h"
 #include "../render/ColorFilter.h"
 #include "../render/IRenderHandler.h"
 #include "../render/CAnimation.h"
@@ -58,12 +59,13 @@ void AssetGenerator::createAdventureOptionsCleanBackground()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
+
+	auto image = GH.renderHandler().createImage(Point(575, 585), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
 
-	Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE);
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585));
 	canvas.draw(img, Point(54, 121), Rect(54, 123, 335, 1));
 	canvas.draw(img, Point(158, 84), Rect(156, 84, 2, 37));
@@ -72,8 +74,6 @@ void AssetGenerator::createAdventureOptionsCleanBackground()
 	canvas.draw(img, Point(53, 567), Rect(53, 520, 339, 3));
 	canvas.draw(img, Point(53, 520), Rect(53, 264, 339, 47));
 
-	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
-
 	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
 }
 
@@ -88,11 +88,11 @@ void AssetGenerator::createBigSpellBook()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("SpelBack"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("SpelBack"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
-	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
+	auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
 	// edges
 	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
 	canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141));
@@ -135,8 +135,6 @@ void AssetGenerator::createBigSpellBook()
 	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
 	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
 
-	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
-
 	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
 }
 
@@ -152,10 +150,9 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
 
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("DiBoxBck"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator);
 
 	// transform to make color of brown DIBOX.PCX texture match color of specified player
 	auto filterSettings = VLC->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"];
@@ -199,10 +196,10 @@ void AssetGenerator::createCombatUnitNumberWindow()
 	   !CResourceHandler::get("local")->createResource(savePathNegative.getOriginalName() + ".png"))
 		return;
 
-	auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"), EImageBlitMode::OPAQUE);
+	locator.layer = EImageBlitMode::OPAQUE;
 
-	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator);
 
 	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
 	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
@@ -233,12 +230,12 @@ void AssetGenerator::createCampaignBackground()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE);
+
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
+	auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
-	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
-	
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600));
 
 	// left image
@@ -263,13 +260,10 @@ void AssetGenerator::createCampaignBackground()
 	canvas.draw(img, Point(404, 414), Rect(313, 74, 197, 114));
 
 	// skull
-	auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"));
-	locatorSkull.scalingFactor = 1;
-	std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE);
+	auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"), EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
 	canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19));
 
-	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
-
 	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
 }
 
@@ -290,11 +284,11 @@ void AssetGenerator::createChroniclesCampaignImages()
 			continue;
 		ResourcePath savePath(filename, EResType::IMAGE);
 
-		auto locator = ImageLocator(imgPathBg);
-		locator.scalingFactor = 1;
+		auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE);
 
-		std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
-		Canvas canvas = Canvas(Point(200, 116), CanvasScalingPolicy::IGNORE);
+		std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
+		auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
+		Canvas canvas = image->getCanvas();
 		
 		switch (i)
 		{
@@ -323,9 +317,8 @@ void AssetGenerator::createChroniclesCampaignImages()
 			canvas.draw(img, Point(0, 0), Rect(268, 210, 200, 116));
 
 			//skull
-			auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"));
-			locatorSkull.scalingFactor = 1;
-			std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE);
+			auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"), EImageBlitMode::OPAQUE);
+			std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
 			canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22));
 			canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4));
 			canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4));
@@ -334,8 +327,6 @@ void AssetGenerator::createChroniclesCampaignImages()
 			break;
 		}
 
-		std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
-
 		image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
 	}
 }
@@ -403,8 +394,7 @@ void AssetGenerator::createPaletteShiftedSprites()
 					return;
 
 				auto imgLoc = anim->getImageLocator(j, 0);
-				imgLoc.scalingFactor = 1;
-				auto img = GH.renderHandler().loadImage(imgLoc, EImageBlitMode::COLORKEY);
+				auto img = GH.renderHandler().loadImage(imgLoc);
 				for(int k = 0; k < paletteAnimations[i].size(); k++)
 				{
 					auto element = paletteAnimations[i][k];
@@ -420,9 +410,9 @@ void AssetGenerator::createPaletteShiftedSprites()
 					}
 				}
 				
-				Canvas canvas = Canvas(Point(32, 32), CanvasScalingPolicy::IGNORE);
+				auto image = GH.renderHandler().createImage(Point(32, 32), CanvasScalingPolicy::IGNORE);
+				Canvas canvas = image->getCanvas();
 				canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2));
-				std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
 				image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
 
 				JsonNode node(JsonMap{

+ 2 - 2
client/render/CAnimation.cpp

@@ -30,7 +30,7 @@ bool CAnimation::loadFrame(size_t frame, size_t group, bool verbose)
 	if(auto image = getImageImpl(frame, group, false))
 		return true;
 
-	std::shared_ptr<IImage> image = GH.renderHandler().loadImage(getImageLocator(frame, group), mode);
+	std::shared_ptr<IImage> image = GH.renderHandler().loadImage(getImageLocator(frame, group));
 
 	if(image)
 	{
@@ -224,5 +224,5 @@ ImageLocator CAnimation::getImageLocator(size_t frame, size_t group) const
 		throw std::runtime_error("Frame " + std::to_string(frame) + " of group " + std::to_string(group) + " is missing from animation " + name.getOriginalName() );
 	}
 
-	return ImageLocator(name, frame, group);
+	return ImageLocator(name, frame, group, mode);
 }

+ 24 - 7
client/render/Canvas.cpp

@@ -11,6 +11,7 @@
 #include "Canvas.h"
 
 #include "../gui/CGuiHandler.h"
+#include "../media/IVideoPlayer.h"
 #include "../render/IRenderHandler.h"
 #include "../render/IScreenHandler.h"
 #include "../renderSDL/SDL_Extensions.h"
@@ -102,11 +103,21 @@ Canvas::~Canvas()
 	SDL_FreeSurface(surface);
 }
 
+void Canvas::draw(IVideoInstance & video, const Point & pos)
+{
+	video.show(pos, surface);
+}
+
+void Canvas::draw(const IImage& image, const Point & pos)
+{
+	image.draw(surface, transformPos(pos), nullptr, getScalingFactor());
+}
+
 void Canvas::draw(const std::shared_ptr<IImage>& image, const Point & pos)
 {
 	assert(image);
 	if (image)
-		image->draw(surface, transformPos(pos));
+		image->draw(surface, transformPos(pos), nullptr, getScalingFactor());
 }
 
 void Canvas::draw(const std::shared_ptr<IImage>& image, const Point & pos, const Rect & sourceRect)
@@ -114,7 +125,7 @@ void Canvas::draw(const std::shared_ptr<IImage>& image, const Point & pos, const
 	Rect realSourceRect = sourceRect * getScalingFactor();
 	assert(image);
 	if (image)
-		image->draw(surface, transformPos(pos), &realSourceRect);
+		image->draw(surface, transformPos(pos), &realSourceRect, getScalingFactor());
 }
 
 void Canvas::draw(const Canvas & image, const Point & pos)
@@ -218,16 +229,22 @@ void Canvas::fillTexture(const std::shared_ptr<IImage>& image)
 	for (int y=0; y < surface->h; y+= imageArea.h)
 	{
 		for (int x=0; x < surface->w; x+= imageArea.w)
-			image->draw(surface, Point(renderArea.x + x * getScalingFactor(), renderArea.y + y * getScalingFactor()));
+			image->draw(surface, Point(renderArea.x + x * getScalingFactor(), renderArea.y + y * getScalingFactor()), nullptr, getScalingFactor());
 	}
 }
 
-SDL_Surface * Canvas::getInternalSurface()
+Rect Canvas::getRenderArea() const
 {
-	return surface;
+	return renderArea;
 }
 
-Rect Canvas::getRenderArea() const
+CanvasClipRectGuard::CanvasClipRectGuard(Canvas & canvas, const Rect & rect): surf(canvas.surface)
 {
-	return renderArea;
+	CSDL_Ext::getClipRect(surf, oldRect);
+	CSDL_Ext::setClipRect(surf, rect * GH.screenHandler().getScalingFactor());
+}
+
+CanvasClipRectGuard::~CanvasClipRectGuard()
+{
+	CSDL_Ext::setClipRect(surf, oldRect);
 }

+ 16 - 3
client/render/Canvas.h

@@ -15,6 +15,7 @@
 
 struct SDL_Surface;
 class IImage;
+class IVideoInstance;
 enum EFonts : int8_t;
 
 enum class CanvasScalingPolicy
@@ -27,6 +28,8 @@ enum class CanvasScalingPolicy
 /// Class that represents surface for drawing on
 class Canvas
 {
+	friend class CanvasClipRectGuard;
+
 	/// Upscaler awareness. Must be first member for initialization
 	CanvasScalingPolicy scalingPolicy;
 
@@ -72,6 +75,9 @@ public:
 
 	/// renders image onto this canvas at specified position
 	void draw(const std::shared_ptr<IImage>& image, const Point & pos);
+	void draw(const IImage& image, const Point & pos);
+
+	void draw(IVideoInstance & video, const Point & pos);
 
 	/// renders section of image bounded by sourceRect at specified position
 	void draw(const std::shared_ptr<IImage>& image, const Point & pos, const Rect & sourceRect);
@@ -114,9 +120,16 @@ public:
 
 	int getScalingFactor() const;
 
-	/// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished.
-	SDL_Surface * getInternalSurface();
-
 	/// get the render area
 	Rect getRenderArea() const;
 };
+
+class CanvasClipRectGuard : boost::noncopyable
+{
+	SDL_Surface * surf;
+	Rect oldRect;
+
+public:
+	CanvasClipRectGuard(Canvas & canvas, const Rect & rect);
+	~CanvasClipRectGuard();
+};

+ 63 - 0
client/render/CanvasImage.cpp

@@ -0,0 +1,63 @@
+/*
+ * CanvasImage.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CanvasImage.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../render/IScreenHandler.h"
+#include "../renderSDL/SDL_Extensions.h"
+#include "../renderSDL/SDLImageScaler.h"
+
+#include <SDL_image.h>
+#include <SDL_surface.h>
+
+CanvasImage::CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy)
+	: surface(CSDL_Ext::newSurface(scalingPolicy == CanvasScalingPolicy::IGNORE ? size : (size * GH.screenHandler().getScalingFactor())))
+	, scalingPolicy(scalingPolicy)
+{
+}
+
+void CanvasImage::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const
+{
+	if(src)
+		CSDL_Ext::blitSurface(surface, *src, where, pos);
+	else
+		CSDL_Ext::blitSurface(surface, where, pos);
+}
+
+void CanvasImage::scaleTo(const Point & size, EScalingAlgorithm algorithm)
+{
+	Point scaledSize = size * GH.screenHandler().getScalingFactor();
+
+	SDLImageScaler scaler(surface);
+	scaler.scaleSurface(scaledSize, algorithm);
+	SDL_FreeSurface(surface);
+	surface = scaler.acquireResultSurface();
+}
+
+void CanvasImage::exportBitmap(const boost::filesystem::path & path) const
+{
+	IMG_SavePNG(surface, path.string().c_str());
+}
+
+Canvas CanvasImage::getCanvas()
+{
+	return Canvas::createFromSurface(surface, scalingPolicy);
+}
+
+Rect CanvasImage::contentRect() const
+{
+	return Rect(Point(0, 0), dimensions());
+}
+
+Point CanvasImage::dimensions() const
+{
+	return {surface->w, surface->h};
+}

+ 41 - 0
client/render/CanvasImage.h

@@ -0,0 +1,41 @@
+/*
+ * CanvasImage.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "IImage.h"
+#include "Canvas.h"
+
+class CanvasImage : public IImage
+{
+public:
+	CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy);
+
+	Canvas getCanvas();
+
+	void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override;
+	void scaleTo(const Point & size, EScalingAlgorithm algorithm) override;
+	void exportBitmap(const boost::filesystem::path & path) const override;
+	Rect contentRect() const override;
+	Point dimensions() const override;
+
+	//no-op methods
+
+	bool isTransparent(const Point & coords) const override{ return false;};
+	void setAlpha(uint8_t value) override{};
+	void playerColored(const PlayerColor & player) override{};
+	void setOverlayColor(const ColorRGBA & color) override{};
+	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override{};
+	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override{};
+
+private:
+	SDL_Surface * surface;
+	CanvasScalingPolicy scalingPolicy;
+};
+

+ 19 - 10
client/render/IImage.h

@@ -62,21 +62,28 @@ enum class EImageBlitMode : uint8_t
 	ONLY_OVERLAY,
 };
 
+enum class EScalingAlgorithm : int8_t
+{
+	NEAREST,
+	BILINEAR,
+	XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image. Only for integer scaling
+	XBRZ_ALPHA // xbrz, image edges are considered to be transparent. Only for integer scaling
+};
+
 /// Base class for images for use in client code.
 /// This class represents current state of image, with potential transformations applied, such as player coloring
 class IImage
 {
 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 draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const = 0;
 
-	virtual void scaleTo(const Point & size) = 0;
-	virtual void scaleInteger(int factor) = 0;
+	virtual void scaleTo(const Point & size, EScalingAlgorithm algorithm) = 0;
 
 	virtual void exportBitmap(const boost::filesystem::path & path) const = 0;
 
 	//Change palette to specific player
-	virtual void playerColored(PlayerColor player) = 0;
+	virtual void playerColored(const PlayerColor & player) = 0;
 
 	//test transparency of specific pixel
 	virtual bool isTransparent(const Point & coords) const = 0;
@@ -92,13 +99,10 @@ public:
 	virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0;
 
 	virtual void setAlpha(uint8_t value) = 0;
-	virtual void setBlitMode(EImageBlitMode mode) = 0;
 
 	//only indexed bitmaps with 7 special colors
 	virtual void setOverlayColor(const ColorRGBA & color) = 0;
 
-	virtual std::shared_ptr<const ISharedImage> getSharedImage() const = 0;
-
 	virtual ~IImage() = default;
 };
 
@@ -112,15 +116,20 @@ public:
 	virtual void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const = 0;
 	virtual bool isTransparent(const Point & coords) const = 0;
 	virtual Rect contentRect() const = 0;
+
+	virtual void scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaling, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
 	virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
 
-	[[nodiscard]] virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
+	/// Returns true if this image is still loading and can't be used
+	virtual bool isLoading() const = 0;
+
+	virtual ~ISharedImage() = default;
+
+	virtual const SDL_Palette * getPalette() const = 0;
 
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const = 0;
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
 
-
-	virtual ~ISharedImage() = default;
 };

+ 9 - 4
client/render/IRenderHandler.h

@@ -20,7 +20,10 @@ struct SDL_Surface;
 class IFont;
 class IImage;
 class CAnimation;
+class CanvasImage;
+class SDLImageShared;
 enum class EImageBlitMode : uint8_t;
+enum class CanvasScalingPolicy;
 enum EFonts : int8_t;
 
 class IRenderHandler : public boost::noncopyable
@@ -32,13 +35,15 @@ public:
 	virtual void onLibraryLoadingFinished(const Services * services) = 0;
 
 	/// Loads image using given path
-	virtual std::shared_ptr<IImage> loadImage(const ImageLocator & locator, EImageBlitMode mode) = 0;
+	virtual std::shared_ptr<IImage> loadImage(const ImageLocator & locator) = 0;
 	virtual std::shared_ptr<IImage> loadImage(const ImagePath & path, EImageBlitMode mode) = 0;
 	virtual std::shared_ptr<IImage> loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) = 0;
 
-	/// temporary compatibility method. Creates IImage from existing SDL_Surface
-	/// Surface will be shared, caller must still free it with SDL_FreeSurface
-	virtual std::shared_ptr<IImage> createImage(SDL_Surface * source) = 0;
+	/// Loads single upscaled image without auto-scaling support
+	virtual std::shared_ptr<SDLImageShared> loadScaledImage(const ImageLocator & locator) = 0;
+
+	/// Creates image which can be used as target for drawing on
+	virtual std::shared_ptr<CanvasImage> createImage(const Point & size, CanvasScalingPolicy scalingPolicy) = 0;
 
 	/// Loads animation using given path
 	virtual std::shared_ptr<CAnimation> loadAnimation(const AnimationPath & path, EImageBlitMode mode) = 0;

+ 14 - 81
client/render/ImageLocator.cpp

@@ -15,11 +15,10 @@
 
 #include "../../lib/json/JsonNode.h"
 
-ImageLocator::ImageLocator(const JsonNode & config)
+SharedImageLocator::SharedImageLocator(const JsonNode & config, EImageBlitMode mode)
 	: defFrame(config["defFrame"].Integer())
 	, defGroup(config["defGroup"].Integer())
-	, verticalFlip(config["verticalFlip"].Bool())
-	, horizontalFlip(config["horizontalFlip"].Bool())
+	, layer(mode)
 {
 	if(!config["file"].isNull())
 		image = ImagePath::fromJson(config["file"]);
@@ -28,19 +27,28 @@ ImageLocator::ImageLocator(const JsonNode & config)
 		defFile = AnimationPath::fromJson(config["defFile"]);
 }
 
-ImageLocator::ImageLocator(const ImagePath & path)
+SharedImageLocator::SharedImageLocator(const ImagePath & path, EImageBlitMode mode)
 	: image(path)
+	, layer(mode)
 {
 }
 
-ImageLocator::ImageLocator(const AnimationPath & path, int frame, int group)
+SharedImageLocator::SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 	: defFile(path)
 	, defFrame(frame)
 	, defGroup(group)
+	, layer(mode)
+{
+}
+
+ImageLocator::ImageLocator(const JsonNode & config, EImageBlitMode mode)
+	: SharedImageLocator(config, mode)
+	, verticalFlip(config["verticalFlip"].Bool())
+	, horizontalFlip(config["horizontalFlip"].Bool())
 {
 }
 
-bool ImageLocator::operator<(const ImageLocator & other) const
+bool SharedImageLocator::operator < (const SharedImageLocator & other) const
 {
 	if(image != other.image)
 		return image < other.image;
@@ -50,14 +58,6 @@ bool ImageLocator::operator<(const ImageLocator & other) const
 		return defGroup < other.defGroup;
 	if(defFrame != other.defFrame)
 		return defFrame < other.defFrame;
-	if(verticalFlip != other.verticalFlip)
-		return verticalFlip < other.verticalFlip;
-	if(horizontalFlip != other.horizontalFlip)
-		return horizontalFlip < other.horizontalFlip;
-	if(scalingFactor != other.scalingFactor)
-		return scalingFactor < other.scalingFactor;
-	if(playerColored != other.playerColored)
-		return playerColored < other.playerColored;
 	if(layer != other.layer)
 		return layer < other.layer;
 
@@ -68,70 +68,3 @@ bool ImageLocator::empty() const
 {
 	return !image.has_value() && !defFile.has_value();
 }
-
-ImageLocator ImageLocator::copyFile() const
-{
-	ImageLocator result;
-	result.scalingFactor = 1;
-	result.preScaledFactor = preScaledFactor;
-	result.image = image;
-	result.defFile = defFile;
-	result.defFrame = defFrame;
-	result.defGroup = defGroup;
-	return result;
-}
-
-ImageLocator ImageLocator::copyFileTransform() const
-{
-	ImageLocator result = copyFile();
-	result.horizontalFlip = horizontalFlip;
-	result.verticalFlip = verticalFlip;
-	return result;
-}
-
-ImageLocator ImageLocator::copyFileTransformScale() const
-{
-	return *this; // full copy
-}
-
-std::string ImageLocator::toString() const
-{
-	std::string result;
-	if (empty())
-		return "invalid";
-
-	if (image)
-	{
-		result += image->getOriginalName();
-		assert(!result.empty());
-	}
-
-	if (defFile)
-	{
-		result += defFile->getOriginalName();
-		assert(!result.empty());
-		result += "-" + std::to_string(defGroup);
-		result += "-" + std::to_string(defFrame);
-	}
-
-	if (verticalFlip)
-		result += "-vflip";
-
-	if (horizontalFlip)
-		result += "-hflip";
-
-	if (scalingFactor > 1)
-		result += "-scale" + std::to_string(scalingFactor);
-
-	if (playerColored.isValidPlayer())
-		result += "-player" + playerColored.toString();
-
-	if (layer == EImageBlitMode::ONLY_OVERLAY)
-		result += "-overlay";
-
-	if (layer == EImageBlitMode::ONLY_SHADOW)
-		result += "-shadow";
-
-
-	return result;
-}

+ 15 - 18
client/render/ImageLocator.h

@@ -14,35 +14,32 @@
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 
-struct ImageLocator
+struct SharedImageLocator
 {
 	std::optional<ImagePath> image;
 	std::optional<AnimationPath> defFile;
 	int defFrame = -1;
 	int defGroup = -1;
+	EImageBlitMode layer = EImageBlitMode::OPAQUE;
+
+	SharedImageLocator() = default;
+	SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode layer);
+	SharedImageLocator(const JsonNode & config, EImageBlitMode layer);
+	SharedImageLocator(const ImagePath & path, EImageBlitMode layer);
+
+	bool operator < (const SharedImageLocator & other) const;
+};
 
-	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; // FIXME: treat as identical to blue to avoid double-loading?
+struct ImageLocator : SharedImageLocator
+{
+	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE;
 
 	bool verticalFlip = false;
 	bool horizontalFlip = false;
 	int8_t scalingFactor = 0; // 0 = auto / use default scaling
-	int8_t preScaledFactor = 1;
-	EImageBlitMode layer = EImageBlitMode::OPAQUE;
 
-	ImageLocator() = default;
-	ImageLocator(const AnimationPath & path, int frame, int group);
-	explicit ImageLocator(const JsonNode & config);
-	explicit ImageLocator(const ImagePath & path);
+	using SharedImageLocator::SharedImageLocator;
+	 ImageLocator(const JsonNode & config, EImageBlitMode layer);
 
-	bool operator < (const ImageLocator & other) const;
 	bool empty() const;
-
-	ImageLocator copyFile() const;
-	ImageLocator copyFileTransform() const;
-	ImageLocator copyFileTransformScale() const;
-
-	// generates string representation of this image locator
-	// guaranteed to be a valid file path with no extension
-	// but may contain '/' if source file is in directory
-	std::string toString() const;
 };

+ 6 - 2
client/renderSDL/CBitmapFont.cpp

@@ -11,9 +11,12 @@
 #include "CBitmapFont.h"
 
 #include "SDL_Extensions.h"
+#include "SDLImageScaler.h"
+
 #include "../CGameInfo.h"
 #include "../gui/CGuiHandler.h"
 #include "../render/Colors.h"
+#include "../render/IImage.h"
 #include "../render/IScreenHandler.h"
 
 #include "../../lib/CConfigHandler.h"
@@ -206,9 +209,10 @@ CBitmapFont::CBitmapFont(const std::string & filename):
 
 		auto filterName = settings["video"]["fontUpscalingFilter"].String();
 		EScalingAlgorithm algorithm = filterNameToEnum.at(filterName);
-		auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor(), algorithm);
+		SDLImageScaler scaler(atlasImage);
+		scaler.scaleSurfaceIntegerFactor(GH.screenHandler().getScalingFactor(), algorithm);
 		SDL_FreeSurface(atlasImage);
-		atlasImage = scaledSurface;
+		atlasImage = scaler.acquireResultSurface();
 	}
 
 	logGlobal->debug("Loaded BMP font: '%s', height %d, ascent %d",

+ 6 - 2
client/renderSDL/CursorHardware.cpp

@@ -12,6 +12,7 @@
 #include "CursorHardware.h"
 
 #include "SDL_Extensions.h"
+#include "SDLImageScaler.h"
 
 #include "../gui/CGuiHandler.h"
 #include "../render/IScreenHandler.h"
@@ -59,8 +60,11 @@ void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivot
 
 	CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY));
 
-	image->draw(cursorSurface, Point(0,0));
-	auto cursorSurfaceScaled = CSDL_Ext::scaleSurface(cursorSurface, cursorDimensionsScaled.x, cursorDimensionsScaled.y );
+	image->draw(cursorSurface, Point(0,0), nullptr, GH.screenHandler().getScalingFactor());
+
+	SDLImageScaler scaler(cursorSurface);
+	scaler.scaleSurface(cursorDimensionsScaled, EScalingAlgorithm::BILINEAR);
+	SDL_Surface	* cursorSurfaceScaled = scaler.acquireResultSurface();
 
 	auto oldCursor = cursor;
 	cursor = SDL_CreateColorCursor(cursorSurfaceScaled, pivotOffsetScaled.x, pivotOffsetScaled.y);

+ 1 - 1
client/renderSDL/CursorSoftware.cpp

@@ -65,7 +65,7 @@ void CursorSoftware::updateTexture()
 
 	CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY));
 
-	cursorImage->draw(cursorSurface, Point(0,0));
+	cursorImage->draw(cursorSurface, Point(0,0), nullptr, GH.screenHandler().getScalingFactor());
 	SDL_UpdateTexture(cursorTexture, nullptr, cursorSurface->pixels, cursorSurface->pitch);
 	needUpdate = false;
 }

+ 0 - 172
client/renderSDL/ImageScaled.cpp

@@ -1,172 +0,0 @@
-/*
- * ImageScaled.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "ImageScaled.h"
-
-#include "SDLImage.h"
-#include "SDL_Extensions.h"
-
-#include "../gui/CGuiHandler.h"
-#include "../render/IScreenHandler.h"
-#include "../render/Colors.h"
-
-#include "../../lib/constants/EntityIdentifiers.h"
-
-#include <SDL_surface.h>
-
-ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode)
-	: source(source)
-	, locator(inputLocator)
-	, colorMultiplier(Colors::WHITE_TRUE)
-	, alphaValue(SDL_ALPHA_OPAQUE)
-	, blitMode(mode)
-{
-	prepareImages();
-}
-
-std::shared_ptr<const ISharedImage> ImageScaled::getSharedImage() const
-{
-	return body;
-}
-
-void ImageScaled::scaleInteger(int factor)
-{
-	assert(0);
-}
-
-void ImageScaled::scaleTo(const Point & size)
-{
-	if (source)
-		source = source->scaleTo(size, nullptr);
-
-	if (body)
-		body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr);
-}
-
-void ImageScaled::exportBitmap(const boost::filesystem::path &path) const
-{
-	source->exportBitmap(path, nullptr);
-}
-
-bool ImageScaled::isTransparent(const Point &coords) const
-{
-	return source->isTransparent(coords);
-}
-
-Rect ImageScaled::contentRect() const
-{
-	return source->contentRect();
-}
-
-Point ImageScaled::dimensions() const
-{
-	return source->dimensions();
-}
-
-void ImageScaled::setAlpha(uint8_t value)
-{
-	alphaValue = value;
-}
-
-void ImageScaled::setBlitMode(EImageBlitMode mode)
-{
-	blitMode = mode;
-}
-
-void ImageScaled::draw(SDL_Surface *where, const Point &pos, const Rect *src) const
-{
-	if (shadow)
-		shadow->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-	if (body)
-		body->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-	if (overlay)
-		overlay->draw(where, nullptr, pos, src, colorMultiplier, colorMultiplier.a * alphaValue / 255, blitMode);
-}
-
-void ImageScaled::setOverlayColor(const ColorRGBA & color)
-{
-	colorMultiplier = color;
-}
-
-void ImageScaled::playerColored(PlayerColor player)
-{
-	playerColor = player;
-	prepareImages();
-}
-
-void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
-{
-	// TODO: implement
-}
-
-void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSkipMask)
-{
-	// TODO: implement
-}
-
-void ImageScaled::prepareImages()
-{
-	switch(blitMode)
-	{
-		case EImageBlitMode::OPAQUE:
-		case EImageBlitMode::COLORKEY:
-		case EImageBlitMode::SIMPLE:
-			locator.layer = blitMode;
-			locator.playerColored = playerColor;
-			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-		case EImageBlitMode::ONLY_BODY:
-			locator.layer = EImageBlitMode::ONLY_BODY;
-			locator.playerColored = playerColor;
-			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-
-		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
-			locator.layer = EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY;
-			locator.playerColored = playerColor;
-			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_OVERLAY:
-			body = nullptr;
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			locator.layer = EImageBlitMode::ONLY_SHADOW;
-			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-			shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-		default:
-			shadow = nullptr;
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::ONLY_OVERLAY:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			locator.layer = EImageBlitMode::ONLY_OVERLAY;
-			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-			overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-		default:
-			overlay = nullptr;
-			break;
-	}
-}

+ 0 - 66
client/renderSDL/ImageScaled.h

@@ -1,66 +0,0 @@
-/*
- * ImageScaled.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../render/IImage.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../lib/Color.h"
-#include "../../lib/constants/EntityIdentifiers.h"
-
-struct SDL_Palette;
-
-class SDLImageShared;
-
-// Upscaled image with several mechanisms to emulate H3 palette effects
-class ImageScaled final : public IImage
-{
-private:
-
-	/// Original unscaled image
-	std::shared_ptr<const ISharedImage> source;
-
-	/// Upscaled shadow of our image, may be null
-	std::shared_ptr<const ISharedImage> shadow;
-
-	/// Upscaled main part of our image, may be null
-	std::shared_ptr<const ISharedImage> body;
-
-	/// Upscaled overlay (player color, selection highlight) of our image, may be null
-	std::shared_ptr<const ISharedImage> overlay;
-
-	ImageLocator locator;
-
-	ColorRGBA colorMultiplier;
-	PlayerColor playerColor = PlayerColor::CANNOT_DETERMINE;
-
-	uint8_t alphaValue;
-	EImageBlitMode blitMode;
-
-	void prepareImages();
-public:
-	ImageScaled(const ImageLocator & locator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode);
-
-	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;
-	Rect contentRect() const override;
-	Point dimensions() const override;
-	void setAlpha(uint8_t value) override;
-	void setBlitMode(EImageBlitMode mode) override;
-	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
-	void setOverlayColor(const ColorRGBA & color) override;
-	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;
-
-	std::shared_ptr<const ISharedImage> getSharedImage() const override;
-};

+ 110 - 188
client/renderSDL/RenderHandler.cpp

@@ -11,19 +11,22 @@
 #include "RenderHandler.h"
 
 #include "SDLImage.h"
-#include "ImageScaled.h"
+#include "ScalableImage.h"
 #include "FontChain.h"
 
 #include "../gui/CGuiHandler.h"
 
 #include "../render/CAnimation.h"
+#include "../render/CanvasImage.h"
 #include "../render/CDefFile.h"
 #include "../render/Colors.h"
 #include "../render/ColorFilter.h"
 #include "../render/IScreenHandler.h"
 #include "../../lib/json/JsonUtils.h"
+#include "../../lib/CThreadHelper.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/VCMIDirs.h"
+#include "../../lib/constants/StringConstants.h"
 
 #include <vcmi/ArtifactService.h>
 #include <vcmi/CreatureService.h>
@@ -55,60 +58,7 @@ std::shared_ptr<CDefFile> RenderHandler::getAnimationFile(const AnimationPath &
 	return result;
 }
 
-std::optional<ResourcePath> RenderHandler::getPathForScaleFactor(const ResourcePath & path, const std::string & factor)
-{
-	if(path.getType() == EResType::IMAGE)
-	{
-		auto p = ImagePath::builtin(path.getName());
-		if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
-		if(CResourceHandler::get()->existsResource(p.addPrefix("DATA" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("DATA" + factor + "X/"));
-	}
-	else
-	{
-		auto p = AnimationPath::builtin(path.getName());
-		auto pJson = p.toType<EResType::JSON>();
-		if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
-		if(CResourceHandler::get()->existsResource(pJson))
-			return std::optional<ResourcePath>(p);
-		if(CResourceHandler::get()->existsResource(pJson.addPrefix("SPRITES" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
-	}
-
-	return std::nullopt;
-}
-
-std::pair<ResourcePath, int> RenderHandler::getScalePath(const ResourcePath & p)
-{
-	auto path = p;
-	int scaleFactor = 1;
-	if(getScalingFactor() > 1)
-	{
-		std::vector<int> factorsToCheck = {getScalingFactor(), 4, 3, 2};
-		for(auto factorToCheck : factorsToCheck)
-		{
-			std::string name = boost::algorithm::to_upper_copy(p.getName());
-			boost::replace_all(name, "SPRITES/", std::string("SPRITES") + std::to_string(factorToCheck) + std::string("X/"));
-			boost::replace_all(name, "DATA/", std::string("DATA") + std::to_string(factorToCheck) + std::string("X/"));
-			ResourcePath scaledPath = ImagePath::builtin(name);
-			if(p.getType() != EResType::IMAGE)
-				scaledPath = AnimationPath::builtin(name);
-			auto tmpPath = getPathForScaleFactor(scaledPath, std::to_string(factorToCheck));
-			if(tmpPath)
-			{
-				path = *tmpPath;
-				scaleFactor = factorToCheck;
-				break;
-			}
-		}
-	}
-
-	return std::pair<ResourcePath, int>(path, scaleFactor);
-};
-
-void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config)
+void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config, EImageBlitMode mode)
 {
 	std::string basepath;
 	basepath = config["basepath"].String();
@@ -128,7 +78,7 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c
 			JsonNode toAdd = frame;
 			JsonUtils::inherit(toAdd, base);
 			toAdd["file"].String() = basepath + frame.String();
-			source[groupID].emplace_back(toAdd);
+			source[groupID].emplace_back(toAdd, mode);
 		}
 	}
 
@@ -149,15 +99,26 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c
 		if (toAdd.Struct().count("defFile"))
 			toAdd["defFile"].String() = basepath + node["defFile"].String();
 
-		source[group][frame] = ImageLocator(toAdd);
+		source[group][frame] = ImageLocator(toAdd, mode);
 	}
 }
 
-RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path)
+RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode)
 {
-	auto tmp = getScalePath(path);
-	auto animPath = AnimationPath::builtin(tmp.first.getName());
-	AnimationPath actualPath = boost::starts_with(animPath.getName(), "SPRITES") ? animPath : animPath.addPrefix("SPRITES/");
+	static constexpr std::array scaledSpritesPath = {
+		"", // 0x
+		"SPRITES/",
+		"SPRITES2X/",
+		"SPRITES3X/",
+		"SPRITES4X/",
+	};
+
+	std::string pathString = path.getName();
+
+	if (boost::starts_with(pathString, "SPRITES/"))
+		pathString = pathString.substr(std::string("SPRITES/").length());
+
+	AnimationPath actualPath = AnimationPath::builtin(scaledSpritesPath.at(scalingFactor) + pathString);
 
 	auto it = animationLayouts.find(actualPath);
 
@@ -184,15 +145,11 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim
 		std::unique_ptr<ui8[]> textData(new ui8[stream->getSize()]);
 		stream->read(textData.get(), stream->getSize());
 
-		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), animPath.getOriginalName());
+		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), path.getOriginalName());
 
-		initFromJson(result, config);
+		initFromJson(result, config, mode);
 	}
 
-	for(auto & g : result)
-		for(auto & i : g.second)
-			i.preScaledFactor = tmp.second;
-
 	animationLayouts[actualPath] = result;
 	return animationLayouts[actualPath];
 }
@@ -202,209 +159,174 @@ int RenderHandler::getScalingFactor() const
 	return GH.screenHandler().getScalingFactor();
 }
 
-ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group)
+ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, int scaling, EImageBlitMode mode)
 {
-	const auto & layout = getAnimationLayout(path);
+	const auto & layout = getAnimationLayout(path, scaling, mode);
 	if (!layout.count(group))
-		return ImageLocator(ImagePath::builtin("DEFAULT"));
+		return ImageLocator();
 
 	if (frame >= layout.at(group).size())
-		return ImageLocator(ImagePath::builtin("DEFAULT"));
+		return ImageLocator();
 
 	const auto & locator = layout.at(group).at(frame);
 	if (locator.image || locator.defFile)
 		return locator;
 
-	return ImageLocator(path, frame, group);
+	return ImageLocator(path, frame, group, mode);
 }
 
-std::shared_ptr<const ISharedImage> RenderHandler::loadImageImpl(const ImageLocator & locator)
+std::shared_ptr<ScalableImageShared> RenderHandler::loadImageImpl(const ImageLocator & locator)
 {
 	auto it = imageFiles.find(locator);
 	if (it != imageFiles.end())
 		return it->second;
 
-	// TODO: order should be different:
-	// 1) try to find correctly scaled image
-	// 2) if fails -> try to find correctly transformed
-	// 3) if also fails -> try to find image from correct file
-	// 4) load missing part of the sequence
-	// TODO: check whether (load -> transform -> scale) or (load -> scale -> transform) order should be used for proper loading of pre-scaled data
-	auto imageFromFile = loadImageFromFile(locator.copyFile());
-	auto transformedImage = transformImage(locator.copyFileTransform(), imageFromFile);
-	auto scaledImage = scaleImage(locator.copyFileTransformScale(), transformedImage);
+	auto sdlImage = loadImageFromFileUncached(locator);
+	auto scaledImage = std::make_shared<ScalableImageShared>(locator, sdlImage);
 
+	storeCachedImage(locator, scaledImage);
 	return scaledImage;
 }
 
-std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
+std::shared_ptr<SDLImageShared> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
 {
 	if(locator.image)
 	{
 		// TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load
-		return std::make_shared<SDLImageShared>(*locator.image, locator.preScaledFactor);
+		return std::make_shared<SDLImageShared>(*locator.image);
 	}
 
 	if(locator.defFile)
 	{
 		auto defFile = getAnimationFile(*locator.defFile);
-		int preScaledFactor = locator.preScaledFactor;
-		if(!defFile) // no prescale for this frame
-		{
-			auto tmpPath = (*locator.defFile).getName();
-			boost::algorithm::replace_all(tmpPath, "SPRITES2X/", "SPRITES/");
-			boost::algorithm::replace_all(tmpPath, "SPRITES3X/", "SPRITES/");
-			boost::algorithm::replace_all(tmpPath, "SPRITES4X/", "SPRITES/");
-			preScaledFactor = 1;
-			defFile = getAnimationFile(AnimationPath::builtin(tmpPath));
-		}
 		if(defFile->hasFrame(locator.defFrame, locator.defGroup))
-			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor);
+			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup);
 		else
 		{
 			logGlobal->error("Frame %d in group %d not found in file: %s", 
 				locator.defFrame, locator.defGroup, locator.defFile->getName().c_str());
-			return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"), locator.preScaledFactor);
+			return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"));
 		}
 	}
 
 	throw std::runtime_error("Invalid image locator received!");
 }
 
-void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
+void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<ScalableImageShared> image)
 {
 	imageFiles[locator] = image;
-
-#if 0
-	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "imageCache" / (locator.toString() + ".png");
-	boost::filesystem::path outDir = outPath;
-	outDir.remove_filename();
-	boost::filesystem::create_directories(outDir);
-	image->exportBitmap(outPath , nullptr);
-#endif
 }
 
-std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFile(const ImageLocator & locator)
+std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocator & locator)
 {
-	if (imageFiles.count(locator))
-		return imageFiles.at(locator);
+	static constexpr std::array scaledDataPath = {
+		"", // 0x
+		"DATA/",
+		"DATA2X/",
+		"DATA3X/",
+		"DATA4X/",
+	};
+
+	static constexpr std::array scaledSpritesPath = {
+		"", // 0x
+		"SPRITES/",
+		"SPRITES2X/",
+		"SPRITES3X/",
+		"SPRITES4X/",
+	};
+
+	ImagePath pathToLoad;
 
-	auto result = loadImageFromFileUncached(locator);
-	storeCachedImage(locator, result);
-	return result;
-}
+	if(locator.defFile)
+	{
+		auto remappedLocator = getLocatorForAnimationFrame(*locator.defFile, locator.defFrame, locator.defGroup, locator.scalingFactor, locator.layer);
+		// we expect that .def's are only used for 1x data, upscaled assets should use standalone images
+		if (!remappedLocator.image)
+			return nullptr;
 
-std::shared_ptr<const ISharedImage> RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
-{
-	if (imageFiles.count(locator))
-		return imageFiles.at(locator);
+		pathToLoad = *remappedLocator.image;
+	}
 
-	auto result = image;
+	if(locator.image)
+		pathToLoad = *locator.image;
 
-	if (locator.verticalFlip)
-		result = result->verticalFlip();
+	if (pathToLoad.empty())
+		return nullptr;
 
-	if (locator.horizontalFlip)
-		result = result->horizontalFlip();
+	std::string imagePathString = pathToLoad.getName();
 
-	storeCachedImage(locator, result);
-	return result;
-}
+	if(locator.layer == EImageBlitMode::ONLY_OVERLAY)
+		imagePathString += "-OVERLAY";
+	if(locator.layer == EImageBlitMode::ONLY_SHADOW)
+		imagePathString += "-SHADOW";
+	if(locator.playerColored.isValidPlayer())
+		imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]);
+	if(locator.playerColored == PlayerColor::NEUTRAL)
+		imagePathString += "-NEUTRAL";
 
-std::shared_ptr<const ISharedImage> RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
-{
-	if (imageFiles.count(locator))
-		return imageFiles.at(locator);
+	auto imagePath = ImagePath::builtin(imagePathString);
+	auto imagePathSprites = ImagePath::builtin(imagePathString).addPrefix(scaledSpritesPath.at(locator.scalingFactor));
+	auto imagePathData = ImagePath::builtin(imagePathString).addPrefix(scaledDataPath.at(locator.scalingFactor));
 
-	auto handle = image->createImageReference(locator.layer);
+	if(CResourceHandler::get()->existsResource(imagePathSprites))
+		return std::make_shared<SDLImageShared>(imagePathSprites);
 
-	assert(locator.scalingFactor != 1); // should be filtered-out before
-	if (locator.playerColored != PlayerColor::CANNOT_DETERMINE)
-		handle->playerColored(locator.playerColored);
+	if(CResourceHandler::get()->existsResource(imagePathData))
+		return std::make_shared<SDLImageShared>(imagePathData);
 
-	handle->scaleInteger(locator.scalingFactor);
+	if(CResourceHandler::get()->existsResource(imagePath))
+		return std::make_shared<SDLImageShared>(imagePath);
 
-	auto result = handle->getSharedImage();
-	storeCachedImage(locator, result);
-	return result;
+	return nullptr;
 }
 
-std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode)
+std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator)
 {
 	ImageLocator adjustedLocator = locator;
 
-	if(adjustedLocator.image)
-	{
-		std::string imgPath = (*adjustedLocator.image).getName();
-		if(adjustedLocator.layer == EImageBlitMode::ONLY_OVERLAY)
-			imgPath += "-OVERLAY";
-		if(adjustedLocator.layer == EImageBlitMode::ONLY_SHADOW)
-			imgPath += "-SHADOW";
-
-		if(CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath)) ||
-		   CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("DATA/")) ||
-		   CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("SPRITES/")))
-			adjustedLocator.image = ImagePath::builtin(imgPath);
-	}
-
-	if(adjustedLocator.defFile && adjustedLocator.scalingFactor == 0)
-	{
-		auto tmp = getScalePath(*adjustedLocator.defFile);
-		adjustedLocator.defFile = AnimationPath::builtin(tmp.first.getName());
-		adjustedLocator.preScaledFactor = tmp.second;
-	}
-	if(adjustedLocator.image && adjustedLocator.scalingFactor == 0)
-	{
-		auto tmp = getScalePath(*adjustedLocator.image);
-		adjustedLocator.image = ImagePath::builtin(tmp.first.getName());
-		adjustedLocator.preScaledFactor = tmp.second;
-	}
-
-	if (adjustedLocator.scalingFactor == 0 && getScalingFactor() != 1 )
-	{
-		auto unscaledLocator = adjustedLocator;
-		auto scaledLocator = adjustedLocator;
-
-		unscaledLocator.scalingFactor = 1;
-		scaledLocator.scalingFactor = getScalingFactor();
-		auto unscaledImage = loadImageImpl(unscaledLocator);
-
-		return std::make_shared<ImageScaled>(scaledLocator, unscaledImage, mode);
-	}
+	std::shared_ptr<ScalableImageInstance> result;
 
 	if (adjustedLocator.scalingFactor == 0)
 	{
 		auto scaledLocator = adjustedLocator;
 		scaledLocator.scalingFactor = getScalingFactor();
 
-		return loadImageImpl(scaledLocator)->createImageReference(mode);
+		result = loadImageImpl(scaledLocator)->createImageReference();
 	}
 	else
-		return loadImageImpl(adjustedLocator)->createImageReference(mode);
+		result = loadImageImpl(adjustedLocator)->createImageReference();
+
+	if (locator.horizontalFlip)
+		result->horizontalFlip();
+	if (locator.verticalFlip)
+		result->verticalFlip();
+
+	return result;
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 {
-	auto tmp = getScalePath(path);
-	ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(tmp.first.getName()), frame, group);
-	locator.preScaledFactor = tmp.second;
-	return loadImage(locator, mode);
+	ImageLocator locator = getLocatorForAnimationFrame(path, frame, group, 1, mode);
+	if (!locator.empty())
+		return loadImage(locator);
+	else
+		return loadImage(ImageLocator(ImagePath::builtin("DEFAULT"), mode));
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode)
 {
-	ImageLocator locator(path);
-	return loadImage(locator, mode);
+	ImageLocator locator(path, mode);
+	return loadImage(locator);
 }
 
-std::shared_ptr<IImage> RenderHandler::createImage(SDL_Surface * source)
+std::shared_ptr<CanvasImage> RenderHandler::createImage(const Point & size, CanvasScalingPolicy scalingPolicy)
 {
-	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::SIMPLE);
+	return std::make_shared<CanvasImage>(size, scalingPolicy);
 }
 
 std::shared_ptr<CAnimation> RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode)
 {
-	return std::make_shared<CAnimation>(path, getAnimationLayout(path), mode);
+	return std::make_shared<CAnimation>(path, getAnimationLayout(path, 1, mode), mode);
 }
 
 void RenderHandler::addImageListEntries(const EntityService * service)
@@ -416,7 +338,7 @@ void RenderHandler::addImageListEntries(const EntityService * service)
 			if (imageName.empty())
 				return;
 
-			auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName));
+			auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::SIMPLE);
 
 			JsonNode entry;
 			entry["file"].String() = imageName;
@@ -424,7 +346,7 @@ void RenderHandler::addImageListEntries(const EntityService * service)
 			if (index >= layout[group].size())
 				layout[group].resize(index + 1);
 
-			layout[group][index] = ImageLocator(entry);
+			layout[group][index] = ImageLocator(entry, EImageBlitMode::SIMPLE);
 		});
 	});
 }

+ 12 - 16
client/renderSDL/RenderHandler.h

@@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_END
 
 class CDefFile;
 class SDLImageShared;
-class ISharedImage;
+class ScalableImageShared;
 
 class RenderHandler : public IRenderHandler
 {
@@ -25,28 +25,22 @@ class RenderHandler : public IRenderHandler
 
 	std::map<AnimationPath, std::shared_ptr<CDefFile>> animationFiles;
 	std::map<AnimationPath, AnimationLayoutMap> animationLayouts;
-	std::map<ImageLocator, std::shared_ptr<const ISharedImage>> imageFiles;
+	std::map<SharedImageLocator, std::shared_ptr<ScalableImageShared>> imageFiles;
 	std::map<EFonts, std::shared_ptr<const IFont>> fonts;
 
 	std::shared_ptr<CDefFile> getAnimationFile(const AnimationPath & path);
-	std::optional<ResourcePath> getPathForScaleFactor(const ResourcePath & path, const std::string & factor);
-	std::pair<ResourcePath, int> getScalePath(const ResourcePath & p);
-	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path);
-	void initFromJson(AnimationLayoutMap & layout, const JsonNode & config);
+	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode);
+	void initFromJson(AnimationLayoutMap & layout, const JsonNode & config, EImageBlitMode mode);
 
 	void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName);
 	void addImageListEntries(const EntityService * service);
-	void storeCachedImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
+	void storeCachedImage(const ImageLocator & locator, std::shared_ptr<ScalableImageShared> image);
 
-	std::shared_ptr<const ISharedImage> loadImageImpl(const ImageLocator & config);
+	std::shared_ptr<ScalableImageShared> loadImageImpl(const ImageLocator & config);
 
-	std::shared_ptr<const ISharedImage> loadImageFromFileUncached(const ImageLocator & locator);
-	std::shared_ptr<const ISharedImage> loadImageFromFile(const ImageLocator & locator);
+	std::shared_ptr<SDLImageShared> loadImageFromFileUncached(const ImageLocator & locator);
 
-	std::shared_ptr<const ISharedImage> transformImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
-	std::shared_ptr<const ISharedImage> scaleImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
-
-	ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group);
+	ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, int scaling, EImageBlitMode mode);
 
 	int getScalingFactor() const;
 
@@ -55,13 +49,15 @@ public:
 	// IRenderHandler implementation
 	void onLibraryLoadingFinished(const Services * services) override;
 
-	std::shared_ptr<IImage> loadImage(const ImageLocator & locator, EImageBlitMode mode) override;
+	std::shared_ptr<IImage> loadImage(const ImageLocator & locator) override;
 	std::shared_ptr<IImage> loadImage(const ImagePath & path, EImageBlitMode mode) override;
 	std::shared_ptr<IImage> loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) override;
 
+	std::shared_ptr<SDLImageShared> loadScaledImage(const ImageLocator & locator) override;
+
 	std::shared_ptr<CAnimation> loadAnimation(const AnimationPath & path, EImageBlitMode mode) override;
 
-	std::shared_ptr<IImage> createImage(SDL_Surface * source) override;
+	std::shared_ptr<CanvasImage> createImage(const Point & size, CanvasScalingPolicy scalingPolicy) override;
 
 	/// Returns font with specified identifer
 	std::shared_ptr<const IFont> loadFont(EFonts font) override;

+ 139 - 401
client/renderSDL/SDLImage.cpp

@@ -11,76 +11,24 @@
 #include "SDLImage.h"
 
 #include "SDLImageLoader.h"
+#include "SDLImageScaler.h"
 #include "SDL_Extensions.h"
 
 #include "../render/ColorFilter.h"
-#include "../render/Colors.h"
 #include "../render/CBitmapHandler.h"
 #include "../render/CDefFile.h"
-#include "../render/Graphics.h"
-#include "../xBRZ/xbrz.h"
 #include "../gui/CGuiHandler.h"
 #include "../render/IScreenHandler.h"
 
 #include <tbb/parallel_for.h>
-#include <SDL_surface.h>
+#include <tbb/task_arena.h>
+
 #include <SDL_image.h>
+#include <SDL_surface.h>
+#include <SDL_version.h>
 
 class SDLImageLoader;
 
-//First 8 colors in def palette used for transparency
-static constexpr std::array<SDL_Color, 8> sourcePalette = {{
-	{0,   255, 255, SDL_ALPHA_OPAQUE},
-	{255, 150, 255, SDL_ALPHA_OPAQUE},
-	{255, 100, 255, SDL_ALPHA_OPAQUE},
-	{255, 50,  255, SDL_ALPHA_OPAQUE},
-	{255, 0,   255, SDL_ALPHA_OPAQUE},
-	{255, 255, 0,   SDL_ALPHA_OPAQUE},
-	{180, 0,   255, SDL_ALPHA_OPAQUE},
-	{0,   255, 0,   SDL_ALPHA_OPAQUE}
-}};
-
-static constexpr std::array<ColorRGBA, 8> targetPalette = {{
-	{0, 0, 0, 0  }, // 0 - transparency                  ( used in most images )
-	{0, 0, 0, 64 }, // 1 - shadow border                 ( used in battle, adventure map def's )
-	{0, 0, 0, 64 }, // 2 - shadow border                 ( used in fog-of-war def's )
-	{0, 0, 0, 128}, // 3 - shadow body                   ( used in fog-of-war def's )
-	{0, 0, 0, 128}, // 4 - shadow body                   ( used in battle, adventure map def's )
-	{0, 0, 0, 0  }, // 5 - selection / owner flag        ( used in battle, adventure map def's )
-	{0, 0, 0, 128}, // 6 - shadow body   below selection ( used in battle def's )
-	{0, 0, 0, 64 }  // 7 - shadow border below selection ( used in battle def's )
-}};
-
-static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
-{
-	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
-}
-
-static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
-{
-	return ColorRGBA(
-		mixChannels(over.r, base.r, over.a, base.a),
-		mixChannels(over.g, base.g, over.a, base.a),
-		mixChannels(over.b, base.b, over.a, base.a),
-		static_cast<ui8>(over.a + base.a * (255 - over.a) / 256)
-		);
-}
-
-static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs)
-{
-	// it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow
-	// exact logic is not clear and requires extensive testing with image editing
-	// potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component
-	static const int threshold = 8;
-
-	int diffR = static_cast<int>(lhs.r) - rhs.r;
-	int diffG = static_cast<int>(lhs.g) - rhs.g;
-	int diffB = static_cast<int>(lhs.b) - rhs.b;
-	int diffA = static_cast<int>(lhs.a) - rhs.a;
-
-	return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold;
-}
-
 int IImage::width() const
 {
 	return dimensions().x;
@@ -91,12 +39,11 @@ int IImage::height() const
 	return dimensions().y;
 }
 
-SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group, int preScaleFactor)
+SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr),
-	preScaleFactor(preScaleFactor)
+	originalPalette(nullptr)
 {
 	SDLImageLoader loader(this);
 	data->loadFrame(frame, group, loader);
@@ -104,12 +51,11 @@ SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group
 	savePalette();
 }
 
-SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor)
+SDLImageShared::SDLImageShared(SDL_Surface * from)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr),
-	preScaleFactor(preScaleFactor)
+	originalPalette(nullptr)
 {
 	surf = from;
 	if (surf == nullptr)
@@ -122,12 +68,11 @@ SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor)
 	fullSize.y = surf->h;
 }
 
-SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor)
+SDLImageShared::SDLImageShared(const ImagePath & filename)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr),
-	preScaleFactor(preScaleFactor)
+	originalPalette(nullptr)
 {
 	surf = BitmapHandler::loadBitmap(filename);
 
@@ -146,9 +91,70 @@ SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor)
 	}
 }
 
+void SDLImageShared::scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaleTo, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const
+{
+	assert(upscalingInProgress == false);
+	if (!surf)
+		return;
+
+	Rect sourceRect(0, 0, surf->w, surf->h);
+	Point destShift(0, 0);
+	Point destScale = Point(surf->w, surf->h) * scaleTo / dimensions();
+	Point marginsScaled = margins * scaleTo / dimensions();
+
+	if(src)
+	{
+		Rect srcUnscaled(Point(src->topLeft() * dimensions() / scaleTo), Point(src->dimensions() * dimensions() / scaleTo));
+
+		if(srcUnscaled.x < margins.x)
+			destShift.x += marginsScaled.x - src->x;
+
+		if(srcUnscaled.y < margins.y)
+			destShift.y += marginsScaled.y - src->y;
+
+		sourceRect = Rect(srcUnscaled).intersect(Rect(margins.x, margins.y, surf->w, surf->h));
+
+		destScale.x = std::min(destScale.x, sourceRect.w * scaleTo.x / dimensions().x);
+		destScale.y = std::min(destScale.y, sourceRect.h * scaleTo.y / dimensions().y);
+
+		sourceRect -= margins;
+	}
+	else
+		destShift = marginsScaled;
+
+	destShift += dest;
+
+	SDL_SetSurfaceColorMod(surf, colorMultiplier.r, colorMultiplier.g, colorMultiplier.b);
+	SDL_SetSurfaceAlphaMod(surf, alpha);
+
+	if (alpha != SDL_ALPHA_OPAQUE || (mode != EImageBlitMode::OPAQUE && surf->format->Amask != 0))
+		SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND);
+	else
+		SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE);
+
+	if (palette && surf->format->palette)
+		SDL_SetSurfacePalette(surf, palette);
+
+	SDL_Rect srcRect = CSDL_Ext::toSDL(sourceRect);
+	SDL_Rect dstRect = CSDL_Ext::toSDL(Rect(destShift, destScale));
+
+	if (sourceRect.dimensions() * scaleTo / dimensions() != destScale)
+		logGlobal->info("???");
+
+	SDL_Surface * tempSurface = SDL_ConvertSurface(surf, where->format, 0);
+	int result = SDL_BlitScaled(tempSurface, &srcRect, where, &dstRect);
+
+	SDL_FreeSurface(tempSurface);
+	if (result != 0)
+		logGlobal->error("SDL_BlitScaled failed! %s", SDL_GetError());
+
+	if (surf->format->palette)
+		SDL_SetSurfacePalette(surf, originalPalette);
+}
 
 void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const
 {
+	assert(upscalingInProgress == false);
 	if (!surf)
 		return;
 
@@ -199,89 +205,23 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin
 
 void SDLImageShared::optimizeSurface()
 {
+	assert(upscalingInProgress == false);
 	if (!surf)
 		return;
 
-	int left = surf->w;
-	int top = surf->h;
-	int right = 0;
-	int bottom = 0;
+	SDLImageOptimizer optimizer(surf, Rect(margins, fullSize));
 
-	// 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 = static_cast<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);
-
-		if (SDL_HasColorKey(surf))
-		{
-			uint32_t colorKey;
-			SDL_GetColorKey(surf, &colorKey);
-			SDL_SetColorKey(newSurface, SDL_TRUE, colorKey);
-		}
-
-		SDL_FreeSurface(surf);
-		surf = newSurface;
+	optimizer.optimizeSurface(surf);
+	SDL_FreeSurface(surf);
 
-		margins.x += left;
-		margins.y += top;
-	}
+	surf = optimizer.acquireResultSurface();
+	margins = optimizer.getResultDimensions().topLeft();
+	fullSize = optimizer.getResultDimensions().dimensions();
 }
 
 std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode mode) const
 {
+	assert(upscalingInProgress == false);
 	if (factor <= 0)
 		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
 
@@ -291,47 +231,58 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL
 	if (palette && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
-	SDL_Surface * scaled = nullptr;
-	if(preScaleFactor == factor)
-		return shared_from_this();
-	else if(preScaleFactor == 1)
-	{
-		// dump heuristics to differentiate tileable UI elements from map object / combat assets
-		if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE)
-			scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE);
-		else
-			scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA);
-	}
+	// simple heuristics to differentiate tileable UI elements from map object / combat assets
+	EScalingAlgorithm algorithm;
+	if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE)
+		algorithm = EScalingAlgorithm::XBRZ_OPAQUE;
 	else
-		scaled = CSDL_Ext::scaleSurface(surf, (int) round((float)surf->w * factor / preScaleFactor), (int) round((float)surf->h * factor / preScaleFactor));
+		algorithm = EScalingAlgorithm::XBRZ_ALPHA;
 
-	auto ret = std::make_shared<SDLImageShared>(scaled, preScaleFactor);
+	auto result = std::make_shared<SDLImageShared>(this, factor, algorithm);
 
-	ret->fullSize.x = fullSize.x * factor;
-	ret->fullSize.y = fullSize.y * factor;
+	if (surf->format->palette)
+		SDL_SetSurfacePalette(surf, originalPalette);
 
-	ret->margins.x = (int) round((float)margins.x * factor / preScaleFactor);
-	ret->margins.y = (int) round((float)margins.y * factor / preScaleFactor);
-	ret->optimizeSurface();
+	return result;
+}
 
-	// erase our own reference
-	SDL_FreeSurface(scaled);
+SDLImageShared::SDLImageShared(const SDLImageShared * from, int integerScaleFactor, EScalingAlgorithm algorithm)
+{
+	static tbb::task_arena upscalingArena;
 
-	if (surf->format->palette)
-		SDL_SetSurfacePalette(surf, originalPalette);
+	upscalingInProgress = true;
 
-	return ret;
+	auto scaler = std::make_shared<SDLImageScaler>(from->surf, Rect(from->margins, from->fullSize));
+
+	const auto & scalingTask = [this, algorithm, scaler]()
+	{
+		scaler->scaleSurfaceIntegerFactor(GH.screenHandler().getScalingFactor(), algorithm);
+		surf = scaler->acquireResultSurface();
+		fullSize = scaler->getResultDimensions().dimensions();
+		margins = scaler->getResultDimensions().topLeft();
+
+		upscalingInProgress = false;
+	};
+
+	upscalingArena.enqueue(scalingTask);
 }
 
-std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
+bool SDLImageShared::isLoading() const
 {
-	float scaleX = static_cast<float>(size.x) / fullSize.x;
-	float scaleY = static_cast<float>(size.y) / fullSize.y;
+	return upscalingInProgress;
+}
 
+std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
+{
+	assert(upscalingInProgress == false);
 	if (palette && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
-	auto scaled = CSDL_Ext::scaleSurface(surf, (int)(surf->w * scaleX), (int)(surf->h * scaleY));
+	SDLImageScaler scaler(surf, Rect(margins, fullSize));
+
+	scaler.scaleSurface(size, EScalingAlgorithm::XBRZ_ALPHA);
+
+	auto scaled = scaler.acquireResultSurface();
 
 	if (scaled->format && scaled->format->palette) // fix color keying, because SDL loses it at this point
 		CSDL_Ext::setColorKey(scaled, scaled->format->palette->colors[0]);
@@ -340,13 +291,9 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size,
 	else
 		CSDL_Ext::setDefaultColorKey(scaled);//just in case
 
-	auto ret = std::make_shared<SDLImageShared>(scaled, preScaleFactor);
-
-	ret->fullSize.x = (int) round((float)fullSize.x * scaleX);
-	ret->fullSize.y = (int) round((float)fullSize.y * scaleY);
-
-	ret->margins.x = (int) round((float)margins.x * scaleX);
-	ret->margins.y = (int) round((float)margins.y * scaleY);
+	auto ret = std::make_shared<SDLImageShared>(scaled);
+	ret->fullSize = scaler.getResultDimensions().dimensions();
+	ret->margins = scaler.getResultDimensions().topLeft();
 
 	// erase our own reference
 	SDL_FreeSurface(scaled);
@@ -359,6 +306,7 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size,
 
 void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palette * palette) const
 {
+	assert(upscalingInProgress == false);
 	if (!surf)
 		return;
 
@@ -369,13 +317,9 @@ void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palet
 		SDL_SetSurfacePalette(surf, originalPalette);
 }
 
-void SDLImageIndexed::playerColored(PlayerColor player)
-{
-	graphics->setPlayerPalette(currentPalette, player);
-}
-
 bool SDLImageShared::isTransparent(const Point & coords) const
 {
+	assert(upscalingInProgress == false);
 	if (surf)
 		return CSDL_Ext::isTransparent(surf, coords.x - margins.x, coords.y	- margins.y);
 	else
@@ -384,31 +328,34 @@ bool SDLImageShared::isTransparent(const Point & coords) const
 
 Rect SDLImageShared::contentRect() const
 {
-	auto tmpMargins = margins / preScaleFactor;
-	auto tmpSize = Point(surf->w, surf->h) / preScaleFactor;
+	assert(upscalingInProgress == false);
+	auto tmpMargins = margins;
+	auto tmpSize = Point(surf->w, surf->h);
 	return Rect(tmpMargins, tmpSize);
 }
 
-Point SDLImageShared::dimensions() const
+const SDL_Palette * SDLImageShared::getPalette() const
 {
-	return fullSize / preScaleFactor;
+	assert(upscalingInProgress == false);
+	if (!surf)
+		return nullptr;
+	return surf->format->palette;
 }
 
-std::shared_ptr<IImage> SDLImageShared::createImageReference(EImageBlitMode mode) const
+Point SDLImageShared::dimensions() const
 {
-	if (surf && surf->format->palette)
-		return std::make_shared<SDLImageIndexed>(shared_from_this(), originalPalette, mode);
-	else
-		return std::make_shared<SDLImageRGB>(shared_from_this(), mode);
+	assert(upscalingInProgress == false);
+	return fullSize;
 }
 
 std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 {
+	assert(upscalingInProgress == false);
 	if (!surf)
 		return shared_from_this();
 
 	SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf);
-	auto ret = std::make_shared<SDLImageShared>(flipped, preScaleFactor);
+	auto ret = std::make_shared<SDLImageShared>(flipped);
 	ret->fullSize = fullSize;
 	ret->margins.x = margins.x;
 	ret->margins.y = fullSize.y - surf->h - margins.y;
@@ -419,11 +366,12 @@ std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 
 std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 {
+	assert(upscalingInProgress == false);
 	if (!surf)
 		return shared_from_this();
 
 	SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf);
-	auto ret = std::make_shared<SDLImageShared>(flipped, preScaleFactor);
+	auto ret = std::make_shared<SDLImageShared>(flipped);
 	ret->fullSize = fullSize;
 	ret->margins.x = fullSize.x - surf->w - margins.x;
 	ret->margins.y = margins.y;
@@ -435,6 +383,7 @@ std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 // Keep the original palette, in order to do color switching operation
 void SDLImageShared::savePalette()
 {
+	assert(upscalingInProgress == false);
 	// For some images that don't have palette, skip this
 	if(surf->format->palette == nullptr)
 		return;
@@ -445,219 +394,8 @@ void SDLImageShared::savePalette()
 	SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, surf->format->palette->ncolors);
 }
 
-void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
-{
-	std::vector<SDL_Color> shifterColors(colorsToMove);
-
-	for(uint32_t i=0; i<colorsToMove; ++i)
-		shifterColors[(i+distanceToMove)%colorsToMove] = originalPalette->colors[firstColorID + i];
-
-	SDL_SetPaletteColors(currentPalette, shifterColors.data(), firstColorID, colorsToMove);
-}
-
-void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
-{
-	// If shadow is enabled, following colors must be skipped unconditionally
-	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
-		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
-
-	// Note: here we skip first colors in the palette that are predefined in H3 images
-	for(int i = 0; i < currentPalette->ncolors; i++)
-	{
-		if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i]))
-			continue;
-
-		if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
-			continue;
-
-		currentPalette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i])));
-	}
-}
-
-SDLImageIndexed::SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * originalPalette, EImageBlitMode mode)
-	:SDLImageBase::SDLImageBase(image, mode)
-	,originalPalette(originalPalette)
-{
-	currentPalette = SDL_AllocPalette(originalPalette->ncolors);
-	SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors);
-
-	preparePalette();
-}
-
-SDLImageIndexed::~SDLImageIndexed()
-{
-	SDL_FreePalette(currentPalette);
-}
-
-void SDLImageIndexed::setShadowTransparency(float factor)
-{
-	ColorRGBA shadow50(0, 0, 0, 128 * factor);
-	ColorRGBA shadow25(0, 0, 0,  64 * factor);
-
-	std::array<SDL_Color, 5> colorsSDL = {
-		originalPalette->colors[0],
-		originalPalette->colors[1],
-		originalPalette->colors[2],
-		originalPalette->colors[3],
-		originalPalette->colors[4]
-	};
-
-	// seems to be used unconditionally
-	colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
-	colorsSDL[1] = CSDL_Ext::toSDL(shadow25);
-	colorsSDL[4] = CSDL_Ext::toSDL(shadow50);
-
-	// seems to be used only if color matches
-	if (colorsSimilar(originalPalette->colors[2], sourcePalette[2]))
-		colorsSDL[2] = CSDL_Ext::toSDL(shadow25);
-
-	if (colorsSimilar(originalPalette->colors[3], sourcePalette[3]))
-		colorsSDL[3] = CSDL_Ext::toSDL(shadow50);
-
-	SDL_SetPaletteColors(currentPalette, colorsSDL.data(), 0, colorsSDL.size());
-}
-
-void SDLImageIndexed::setOverlayColor(const ColorRGBA & color)
-{
-	currentPalette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
-
-	for (int i : {6,7})
-	{
-		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
-			currentPalette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
-	}
-}
-
-void SDLImageIndexed::preparePalette()
-{
-	switch(blitMode)
-	{
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_OVERLAY:
-			adjustPalette(ColorFilter::genAlphaShifter(0), 0);
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::SIMPLE:
-		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			setShadowTransparency(1.0);
-			break;
-		case EImageBlitMode::ONLY_BODY:
-		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
-		case EImageBlitMode::ONLY_OVERLAY:
-			setShadowTransparency(0.0);
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::ONLY_OVERLAY:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			setOverlayColor(Colors::WHITE_TRUE);
-			break;
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_BODY:
-			setOverlayColor(Colors::TRANSPARENCY);
-			break;
-	}
-}
-
 SDLImageShared::~SDLImageShared()
 {
 	SDL_FreeSurface(surf);
 	SDL_FreePalette(originalPalette);
 }
-
-SDLImageBase::SDLImageBase(const std::shared_ptr<const ISharedImage> & image, EImageBlitMode mode)
-	:image(image)
-	, alphaValue(SDL_ALPHA_OPAQUE)
-	, blitMode(mode)
-{}
-
-std::shared_ptr<const ISharedImage> SDLImageBase::getSharedImage() const
-{
-	return image;
-}
-
-void SDLImageRGB::draw(SDL_Surface * where, const Point & pos, const Rect * src) const
-{
-	image->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-}
-
-void SDLImageIndexed::draw(SDL_Surface * where, const Point & pos, const Rect * src) const
-{
-	image->draw(where, currentPalette, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-}
-
-void SDLImageIndexed::exportBitmap(const boost::filesystem::path & path) const
-{
-	image->exportBitmap(path, currentPalette);
-}
-
-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->scaleInteger(factor, currentPalette, blitMode);
-}
-
-void SDLImageRGB::scaleInteger(int factor)
-{
-	image = image->scaleInteger(factor, nullptr, blitMode);
-}
-
-void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const
-{
-	image->exportBitmap(path, nullptr);
-}
-
-bool SDLImageBase::isTransparent(const Point & coords) const
-{
-	return image->isTransparent(coords);
-}
-
-Rect SDLImageBase::contentRect() const
-{
-	return image->contentRect();
-}
-
-Point SDLImageBase::dimensions() const
-{
-	return image->dimensions();
-}
-
-void SDLImageBase::setAlpha(uint8_t value)
-{
-	alphaValue = value;
-}
-
-void SDLImageBase::setBlitMode(EImageBlitMode mode)
-{
-	blitMode = mode;
-}
-
-void SDLImageRGB::setOverlayColor(const ColorRGBA & color)
-{}
-
-void SDLImageRGB::playerColored(PlayerColor player)
-{}
-
-void SDLImageRGB::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
-{}
-
-void SDLImageRGB::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
-{}
-
-

+ 14 - 63
client/renderSDL/SDLImage.h

@@ -27,16 +27,15 @@ struct SDL_Palette;
 class SDLImageShared final : public ISharedImage, public std::enable_shared_from_this<SDLImageShared>, boost::noncopyable
 {
 	//Surface without empty borders
-	SDL_Surface * surf;
+	SDL_Surface * surf = nullptr;
 
-	SDL_Palette * originalPalette;
+	SDL_Palette * originalPalette = nullptr;
 	//size of left and top borders
 	Point margins;
 	//total size including borders
 	Point fullSize;
 
-	//pre scaled image
-	int preScaleFactor;
+	std::atomic_bool upscalingInProgress = false;
 
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
@@ -45,20 +44,27 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 
 public:
 	//Load image from def file
-	SDLImageShared(const CDefFile *data, size_t frame, size_t group=0, int preScaleFactor=1);
+	SDLImageShared(const CDefFile *data, size_t frame, size_t group=0);
 	//Load from bitmap file
-	SDLImageShared(const ImagePath & filename, int preScaleFactor=1);
+	SDLImageShared(const ImagePath & filename);
 	//Create using existing surface, extraRef will increase refcount on SDL_Surface
-	SDLImageShared(SDL_Surface * from, int preScaleFactor=1);
+	SDLImageShared(SDL_Surface * from);
+	/// Creates image at specified scaling factor from source image
+	SDLImageShared(const SDLImageShared * from, int integerScaleFactor, EScalingAlgorithm algorithm);
 	~SDLImageShared();
 
+	void scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaling, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override;
 	void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override;
 
 	void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override;
 	Point dimensions() const override;
 	bool isTransparent(const Point & coords) const override;
 	Rect contentRect() const override;
-	[[nodiscard]] std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
+
+	bool isLoading() const override;
+
+	const SDL_Palette * getPalette() const override;
+
 	[[nodiscard]] std::shared_ptr<const ISharedImage> horizontalFlip() const override;
 	[[nodiscard]] std::shared_ptr<const ISharedImage> verticalFlip() const override;
 	[[nodiscard]] std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override;
@@ -66,58 +72,3 @@ public:
 
 	friend class SDLImageLoader;
 };
-
-class SDLImageBase : public IImage, boost::noncopyable
-{
-protected:
-	std::shared_ptr<const ISharedImage> image;
-
-	uint8_t alphaValue;
-	EImageBlitMode blitMode;
-
-public:
-	SDLImageBase(const std::shared_ptr<const ISharedImage> & image, EImageBlitMode mode);
-
-	bool isTransparent(const Point & coords) const override;
-	Rect contentRect() const override;
-	Point dimensions() const override;
-	void setAlpha(uint8_t value) override;
-	void setBlitMode(EImageBlitMode mode) override;
-	std::shared_ptr<const ISharedImage> getSharedImage() const override;
-};
-
-class SDLImageIndexed final : public SDLImageBase
-{
-	SDL_Palette * currentPalette = nullptr;
-	SDL_Palette * originalPalette = nullptr;
-
-	void setShadowTransparency(float factor);
-	void preparePalette();
-public:
-	SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * palette, EImageBlitMode mode);
-	~SDLImageIndexed();
-
-	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
-	void setOverlayColor(const ColorRGBA & color) override;
-	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 scaleInteger(int factor) override;
-	void scaleTo(const Point & size) override;
-	void exportBitmap(const boost::filesystem::path & path) const override;
-};
-
-class SDLImageRGB final : public SDLImageBase
-{
-public:
-	using SDLImageBase::SDLImageBase;
-
-	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
-	void setOverlayColor(const ColorRGBA & color) override;
-	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 scaleInteger(int factor) override;
-	void scaleTo(const Point & size) override;
-	void exportBitmap(const boost::filesystem::path & path) const override;
-};

+ 233 - 0
client/renderSDL/SDLImageScaler.cpp

@@ -0,0 +1,233 @@
+/*
+ * SDLImageScaler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "SDLImageScaler.h"
+
+#include "SDL_Extensions.h"
+
+#include "../CMT.h"
+#include "../xBRZ/xbrz.h"
+
+#include <tbb/parallel_for.h>
+#include <SDL_surface.h>
+
+SDLImageOptimizer::SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDimensions)
+	: surf(surf)
+	, virtualDimensions(virtualDimensions)
+{
+}
+
+void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface)
+{
+	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 = static_cast<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);
+				}
+			}
+		}
+	}
+
+	// empty image
+	if (left == surf->w)
+		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(), formatSourceSurface);
+		SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE);
+		SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr);
+
+		if (SDL_HasColorKey(surf))
+		{
+			uint32_t colorKey;
+			SDL_GetColorKey(surf, &colorKey);
+			SDL_SetColorKey(newSurface, SDL_TRUE, colorKey);
+		}
+		output = newSurface;
+
+		virtualDimensions.x += left;
+		virtualDimensions.y += top;
+	}
+	else
+	{
+		output = surf;
+		output->refcount += 1;
+	}
+}
+
+SDL_Surface * SDLImageOptimizer::acquireResultSurface()
+{
+	SDL_Surface * result = output;
+	output = nullptr;
+	return result;
+}
+
+const Rect & SDLImageOptimizer::getResultDimensions() const
+{
+	return virtualDimensions;
+}
+
+void SDLImageScaler::scaleSurface(Point targetDimensions, EScalingAlgorithm algorithm)
+{
+	if(!targetDimensions.x || !targetDimensions.y)
+		throw std::runtime_error("invalid scaling dimensions!");
+
+	Point inputSurfaceSize(intermediate->w, intermediate->h);
+	Point outputSurfaceSize = targetDimensions * inputSurfaceSize / virtualDimensionsInput.dimensions();
+	Point outputMargins = targetDimensions * virtualDimensionsInput.topLeft() / virtualDimensionsInput.dimensions();
+
+	// TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%?
+	// Need to investigate which is optimal	for performance and for visuals
+	ret = CSDL_Ext::newSurface(Point(outputSurfaceSize.x, outputSurfaceSize.y), intermediate);
+
+	virtualDimensionsOutput = Rect(outputMargins, targetDimensions); // TODO: account for input virtual size
+
+	const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
+	uint32_t * dstPixels = static_cast<uint32_t*>(ret->pixels);
+
+	if (algorithm == EScalingAlgorithm::NEAREST)
+		xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
+	else
+		xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
+}
+
+void SDLImageScaler::scaleSurfaceIntegerFactor(int factor, EScalingAlgorithm algorithm)
+{
+	if(factor == 0)
+		throw std::runtime_error("invalid scaling factor!");
+
+	int newWidth = intermediate->w * factor;
+	int newHight = intermediate->h * factor;
+
+	virtualDimensionsOutput = virtualDimensionsInput * factor;
+
+	ret = CSDL_Ext::newSurface(Point(newWidth, newHight), intermediate);
+
+	assert(intermediate->pitch == intermediate->w * 4);
+	assert(ret->pitch == ret->w * 4);
+
+	const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
+	uint32_t * dstPixels = static_cast<uint32_t*>(ret->pixels);
+
+	switch (algorithm)
+	{
+		case EScalingAlgorithm::NEAREST:
+			xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
+			break;
+		case EScalingAlgorithm::BILINEAR:
+			xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
+			break;
+		case EScalingAlgorithm::XBRZ_ALPHA:
+		case EScalingAlgorithm::XBRZ_OPAQUE:
+		{
+			auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB;
+
+			if(intermediate->h < 32)
+			{
+				// for tiny images tbb incurs too high overhead
+				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {});
+			}
+			else
+			{
+				// xbrz recommends granulation of 16, but according to tests, for smaller images granulation of 4 is actually the best option
+				const int granulation = intermediate->h > 400 ? 16 : 4;
+				tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [this, factor, srcPixels, dstPixels, format](const tbb::blocked_range<size_t> & r)
+								  {
+									  xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end());
+								  });
+			}
+
+			break;
+		}
+		default:
+			throw std::runtime_error("invalid scaling algorithm!");
+	}
+}
+
+SDLImageScaler::SDLImageScaler(SDL_Surface * surf)
+	:SDLImageScaler(surf, Rect(0,0,surf->w, surf->h))
+{
+}
+
+SDLImageScaler::SDLImageScaler(SDL_Surface * surf, const Rect & virtualDimensions)
+{
+	SDLImageOptimizer optimizer(surf, virtualDimensions);
+	optimizer.optimizeSurface(screen);
+	intermediate = optimizer.acquireResultSurface();
+	virtualDimensionsInput = optimizer.getResultDimensions();
+
+	if (intermediate == surf)
+	{
+		SDL_FreeSurface(intermediate);
+		intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
+	}
+}
+
+SDLImageScaler::~SDLImageScaler()
+{
+	SDL_FreeSurface(intermediate);
+	SDL_FreeSurface(ret);
+}
+
+SDL_Surface * SDLImageScaler::acquireResultSurface()
+{
+	SDL_Surface * result = ret;
+	ret = nullptr;
+	return result;
+}
+
+const Rect & SDLImageScaler::getResultDimensions() const
+{
+	return virtualDimensionsOutput;
+}

+ 63 - 0
client/renderSDL/SDLImageScaler.h

@@ -0,0 +1,63 @@
+/*
+ * SDLImageScaler.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+#include "../render/IImage.h"
+#include "../../lib/Rect.h"
+
+class SDLImageOptimizer : boost::noncopyable
+{
+	SDL_Surface * surf = nullptr;
+	SDL_Surface * output = nullptr;
+	Rect virtualDimensions = Rect(0,0,0,0);
+public:
+	SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDimensions);
+
+	void optimizeSurface(SDL_Surface * formatSourceSurface);
+
+	/// Aquires resulting surface and transfers surface ownership to the caller
+	/// May return nullptr if input image was empty
+	SDL_Surface * acquireResultSurface();
+
+	/// Returns adjusted virtual dimensions of resulting surface
+	const Rect & getResultDimensions() const;
+};
+
+/// Class that performs scaling of SDL surfaces
+/// Object construction MUST be performed while holding UI lock
+/// but task execution can be performed asynchronously if needed
+class SDLImageScaler : boost::noncopyable
+{
+	SDL_Surface * intermediate = nullptr;
+	SDL_Surface * ret = nullptr;
+	Rect virtualDimensionsInput = Rect(0,0,0,0);
+	Rect virtualDimensionsOutput = Rect(0,0,0,0);
+
+public:
+	SDLImageScaler(SDL_Surface * surf);
+	SDLImageScaler(SDL_Surface * surf, const Rect & virtualDimensions);
+	~SDLImageScaler();
+
+	/// Performs upscaling or downscaling to a requested dimensions
+	/// Aspect ratio is NOT maintained.
+	/// xbrz algorithm is not supported in this mode
+	void scaleSurface(Point dimensions, EScalingAlgorithm algorithm);
+
+	/// Performs upscaling by specified integral factor, potentially using xbrz algorithm if requested
+	void scaleSurfaceIntegerFactor(int factor, EScalingAlgorithm algorithm);
+
+	/// Aquires resulting surface and transfers surface ownership to the caller
+	/// May return nullptr if input image was empty
+	SDL_Surface * acquireResultSurface();
+
+	/// Returns adjusted virtual dimensions of resulting surface
+	const Rect & getResultDimensions() const;
+};

+ 1 - 85
client/renderSDL/SDL_Extensions.cpp

@@ -14,6 +14,7 @@
 
 #include "../gui/CGuiHandler.h"
 #include "../render/Graphics.h"
+#include "../render/IImage.h"
 #include "../render/IScreenHandler.h"
 #include "../render/Colors.h"
 #include "../CMT.h"
@@ -630,86 +631,6 @@ void CSDL_Ext::convertToGrayscale( SDL_Surface * surf, const Rect & rect )
 	}
 }
 
-// scaling via bilinear interpolation algorithm.
-// NOTE: best results are for scaling in range 50%...200%.
-// And upscaling looks awful right now - should be fixed somehow
-SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height)
-{
-	if(!surf || !width || !height)
-		return nullptr;
-
-	// TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%?
-	// Need to investigate which is optimal	for performance and for visuals
-
-	SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0);
-	SDL_Surface * ret = newSurface(Point(width, height), intermediate);
-
-#if SDL_VERSION_ATLEAST(2,0,16)
-	SDL_SoftStretchLinear(intermediate, nullptr, ret, nullptr);
-#else
-	SDL_SoftStretch(intermediate, nullptr, ret, nullptr);
-#endif
-	SDL_FreeSurface(intermediate);
-
-	return ret;
-}
-
-SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm algorithm)
-{
-	if(surf == nullptr || factor == 0)
-		return nullptr;
-
-	int newWidth = surf->w * factor;
-	int newHight = surf->h * factor;
-
-	SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
-	SDL_Surface * ret = newSurface(Point(newWidth, newHight), intermediate);
-
-	assert(intermediate->pitch == intermediate->w * 4);
-	assert(ret->pitch == ret->w * 4);
-
-	const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
-	uint32_t * dstPixels = static_cast<uint32_t*>(ret->pixels);
-
-	switch (algorithm)
-	{
-		case EScalingAlgorithm::NEAREST:
-			xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
-			break;
-		case EScalingAlgorithm::BILINEAR:
-			xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
-			break;
-		case EScalingAlgorithm::XBRZ_ALPHA:
-		case EScalingAlgorithm::XBRZ_OPAQUE:
-		{
-			auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB;
-
-			if(intermediate->h < 32)
-			{
-				// for tiny images tbb incurs too high overhead
-				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {});
-			}
-			else
-			{
-				// xbrz recommends granulation of 16, but according to tests, for smaller images granulation of 4 is actually the best option
-				const int granulation = intermediate->h > 400 ? 16 : 4;
-				tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate, format](const tbb::blocked_range<size_t> & r)
-				{
-					xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end());
-				});
-			}
-
-			break;
-		}
-		default:
-			throw std::runtime_error("invalid scaling algorithm!");
-	}
-
-	SDL_FreeSurface(intermediate);
-
-	return ret;
-}
-
 void CSDL_Ext::blitSurface(SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPoint)
 {
 	SDL_Rect srcRect = CSDL_Ext::toSDL(srcRectInput);
@@ -799,10 +720,5 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other)
 	other = CSDL_Ext::fromSDL(rect);
 }
 
-int CSDL_Ext::CClipRectGuard::getScalingFactor() const
-{
-	return GH.screenHandler().getScalingFactor();
-}
-
 template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int);
 template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int);

+ 0 - 12
client/renderSDL/SDL_Extensions.h

@@ -27,14 +27,6 @@ class Point;
 
 VCMI_LIB_NAMESPACE_END
 
-enum class EScalingAlgorithm : int8_t
-{
-	NEAREST,
-	BILINEAR,
-	XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image
-	XBRZ_ALPHA // xbrz, image edges are considered to be transparent
-};
-
 namespace CSDL_Ext
 {
 
@@ -98,10 +90,6 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &,
 	template<int bpp>
 	SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value
 
-	// bilinear filtering. Always returns rgba surface
-	SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height);
-	SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm scaler);
-
 	template<int bpp>
 	void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect);
 	void convertToGrayscale(SDL_Surface * surf, const Rect & rect);

+ 532 - 0
client/renderSDL/ScalableImage.cpp

@@ -0,0 +1,532 @@
+/*
+ * ScalableImage.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "ScalableImage.h"
+
+#include "SDLImage.h"
+#include "SDL_Extensions.h"
+
+#include "../gui/CGuiHandler.h"
+
+#include "../render/ColorFilter.h"
+#include "../render/Colors.h"
+#include "../render/Graphics.h"
+#include "../render/IRenderHandler.h"
+#include "../render/IScreenHandler.h"
+#include "../render/CanvasImage.h"
+
+#include "../../lib/constants/EntityIdentifiers.h"
+
+#include <SDL_surface.h>
+
+//First 8 colors in def palette used for transparency
+static constexpr std::array<SDL_Color, 8> sourcePalette = {{
+	{0,   255, 255, SDL_ALPHA_OPAQUE},
+	{255, 150, 255, SDL_ALPHA_OPAQUE},
+	{255, 100, 255, SDL_ALPHA_OPAQUE},
+	{255, 50,  255, SDL_ALPHA_OPAQUE},
+	{255, 0,   255, SDL_ALPHA_OPAQUE},
+	{255, 255, 0,   SDL_ALPHA_OPAQUE},
+	{180, 0,   255, SDL_ALPHA_OPAQUE},
+	{0,   255, 0,   SDL_ALPHA_OPAQUE}
+}};
+
+static constexpr std::array<ColorRGBA, 8> targetPalette = {{
+	{0, 0, 0, 0  }, // 0 - transparency                  ( used in most images )
+	{0, 0, 0, 64 }, // 1 - shadow border                 ( used in battle, adventure map def's )
+	{0, 0, 0, 64 }, // 2 - shadow border                 ( used in fog-of-war def's )
+	{0, 0, 0, 128}, // 3 - shadow body                   ( used in fog-of-war def's )
+	{0, 0, 0, 128}, // 4 - shadow body                   ( used in battle, adventure map def's )
+	{0, 0, 0, 0  }, // 5 - selection / owner flag        ( used in battle, adventure map def's )
+	{0, 0, 0, 128}, // 6 - shadow body   below selection ( used in battle def's )
+	{0, 0, 0, 64 }  // 7 - shadow border below selection ( used in battle def's )
+}};
+
+static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
+{
+	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
+}
+
+static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
+{
+	return ColorRGBA(
+		mixChannels(over.r, base.r, over.a, base.a),
+		mixChannels(over.g, base.g, over.a, base.a),
+		mixChannels(over.b, base.b, over.a, base.a),
+		static_cast<ui8>(over.a + base.a * (255 - over.a) / 256)
+		);
+}
+static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs)
+{
+	// it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow
+	// exact logic is not clear and requires extensive testing with image editing
+	// potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component
+	static const int threshold = 8;
+
+	int diffR = static_cast<int>(lhs.r) - rhs.r;
+	int diffG = static_cast<int>(lhs.g) - rhs.g;
+	int diffB = static_cast<int>(lhs.b) - rhs.b;
+	int diffA = static_cast<int>(lhs.a) - rhs.a;
+
+	return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold;
+}
+
+ScalableImageParameters::ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode)
+{
+	if (originalPalette)
+	{
+		palette = SDL_AllocPalette(originalPalette->ncolors);
+		SDL_SetPaletteColors(palette, originalPalette->colors, 0, originalPalette->ncolors);
+		preparePalette(originalPalette, blitMode);
+	}
+}
+
+ScalableImageParameters::~ScalableImageParameters()
+{
+	SDL_FreePalette(palette);
+}
+
+void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode)
+{
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_OVERLAY:
+			adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0);
+			break;
+	}
+
+	switch(blitMode)
+	{
+		case EImageBlitMode::SIMPLE:
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setShadowTransparency(originalPalette, 1.0);
+			break;
+		case EImageBlitMode::ONLY_BODY:
+		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+		case EImageBlitMode::ONLY_OVERLAY:
+			setShadowTransparency(originalPalette, 0.0);
+			break;
+	}
+
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setOverlayColor(originalPalette, Colors::WHITE_TRUE);
+			break;
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_BODY:
+			setOverlayColor(originalPalette, Colors::TRANSPARENCY);
+			break;
+	}
+}
+
+void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color)
+{
+	palette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
+
+	for (int i : {6,7})
+	{
+		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
+			palette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
+	}
+}
+
+void ScalableImageParameters::shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
+{
+	std::vector<SDL_Color> shifterColors(colorsToMove);
+
+	for(uint32_t i=0; i<colorsToMove; ++i)
+		shifterColors[(i+distanceToMove)%colorsToMove] = originalPalette->colors[firstColorID + i];
+
+	SDL_SetPaletteColors(palette, shifterColors.data(), firstColorID, colorsToMove);
+}
+
+void ScalableImageParameters::setShadowTransparency(const SDL_Palette * originalPalette, float factor)
+{
+	ColorRGBA shadow50(0, 0, 0, 128 * factor);
+	ColorRGBA shadow25(0, 0, 0,  64 * factor);
+
+	std::array<SDL_Color, 5> colorsSDL = {
+		originalPalette->colors[0],
+		originalPalette->colors[1],
+		originalPalette->colors[2],
+		originalPalette->colors[3],
+		originalPalette->colors[4]
+	};
+
+	// seems to be used unconditionally
+	colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
+	colorsSDL[1] = CSDL_Ext::toSDL(shadow25);
+	colorsSDL[4] = CSDL_Ext::toSDL(shadow50);
+
+	// seems to be used only if color matches
+	if (colorsSimilar(originalPalette->colors[2], sourcePalette[2]))
+		colorsSDL[2] = CSDL_Ext::toSDL(shadow25);
+
+	if (colorsSimilar(originalPalette->colors[3], sourcePalette[3]))
+		colorsSDL[3] = CSDL_Ext::toSDL(shadow50);
+
+	SDL_SetPaletteColors(palette, colorsSDL.data(), 0, colorsSDL.size());
+}
+
+void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask)
+{
+	// If shadow is enabled, following colors must be skipped unconditionally
+	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
+		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
+
+	// Note: here we skip first colors in the palette that are predefined in H3 images
+	for(int i = 0; i < palette->ncolors; i++)
+	{
+		if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i]))
+			continue;
+
+		if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
+			continue;
+
+		palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i])));
+	}
+}
+
+ScalableImageShared::ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr<const ISharedImage> & baseImage)
+	:locator(locator)
+{
+	scaled[1].body[0] = baseImage;
+	assert(scaled[1].body[0] != nullptr);
+
+	loadScaledImages(GH.screenHandler().getScalingFactor(), PlayerColor::CANNOT_DETERMINE);
+}
+
+Point ScalableImageShared::dimensions() const
+{
+	return scaled[1].body[0]->dimensions();
+}
+
+void ScalableImageShared::exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const
+{
+	scaled[1].body[0]->exportBitmap(path, parameters.palette);
+}
+
+bool ScalableImageShared::isTransparent(const Point & coords) const
+{
+	return scaled[1].body[0]->isTransparent(coords);
+}
+
+Rect ScalableImageShared::contentRect() const
+{
+	return scaled[1].body[0]->contentRect();
+}
+
+void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor)
+{
+	const auto & getFlippedImage = [&](FlippedImages & images){
+		int index = 0;
+		if (parameters.flipVertical)
+		{
+			if (!images[index|1])
+				images[index|1] = images[index]->verticalFlip();
+
+			index |= 1;
+		}
+
+		if (parameters.flipHorizontal)
+		{
+			if (!images[index|2])
+				images[index|2] = images[index]->horizontalFlip();
+
+			index |= 2;
+		}
+
+		return images[index];
+	};
+
+	const auto & flipAndDraw = [&](FlippedImages & images, const ColorRGBA & colorMultiplier, uint8_t alphaValue){
+
+		getFlippedImage(images)->draw(where, parameters.palette, dest, src, colorMultiplier, alphaValue, locator.layer);
+	};
+
+	bool shadowLoading = scaled.at(scalingFactor).shadow.at(0) && scaled.at(scalingFactor).shadow.at(0)->isLoading();
+	bool bodyLoading = scaled.at(scalingFactor).body.at(0) && scaled.at(scalingFactor).body.at(0)->isLoading();
+	bool overlayLoading = scaled.at(scalingFactor).overlay.at(0) && scaled.at(scalingFactor).overlay.at(0)->isLoading();
+	bool playerLoading = parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()) && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->isLoading();
+
+	if (shadowLoading || bodyLoading || overlayLoading || playerLoading)
+	{
+		getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer);
+		return;
+	}
+
+	if (scaled.at(scalingFactor).shadow.at(0))
+		flipAndDraw(scaled.at(scalingFactor).shadow, Colors::WHITE_TRUE, parameters.alphaValue);
+
+	if (parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()))
+	{
+		scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->draw(where, parameters.palette, dest, src, Colors::WHITE_TRUE, parameters.alphaValue, locator.layer);
+	}
+	else
+	{
+		if (scaled.at(scalingFactor).body.at(0))
+			flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue);
+	}
+
+	if (scaled.at(scalingFactor).overlay.at(0))
+		flipAndDraw(scaled.at(scalingFactor).overlay, parameters.ovelayColorMultiplier, static_cast<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255);
+}
+
+const SDL_Palette * ScalableImageShared::getPalette() const
+{
+	return scaled[1].body[0]->getPalette();
+}
+
+std::shared_ptr<ScalableImageInstance> ScalableImageShared::createImageReference()
+{
+	return std::make_shared<ScalableImageInstance>(shared_from_this(), locator.layer);
+}
+
+ScalableImageInstance::ScalableImageInstance(const std::shared_ptr<ScalableImageShared> & image, EImageBlitMode blitMode)
+	:image(image)
+	,parameters(image->getPalette(), blitMode)
+	,blitMode(blitMode)
+{
+	assert(image);
+}
+
+void ScalableImageInstance::scaleTo(const Point & size, EScalingAlgorithm algorithm)
+{
+	scaledImage = nullptr;
+
+	auto newScaledImage = GH.renderHandler().createImage(dimensions(), CanvasScalingPolicy::AUTO);
+
+	newScaledImage->getCanvas().draw(*this, Point(0, 0));
+	newScaledImage->scaleTo(size, algorithm);
+	scaledImage = newScaledImage;
+}
+
+void ScalableImageInstance::exportBitmap(const boost::filesystem::path & path) const
+{
+	image->exportBitmap(path, parameters);
+}
+
+bool ScalableImageInstance::isTransparent(const Point & coords) const
+{
+	return image->isTransparent(coords);
+}
+
+Rect ScalableImageInstance::contentRect() const
+{
+	return image->contentRect();
+}
+
+Point ScalableImageInstance::dimensions() const
+{
+	if (scaledImage)
+		return scaledImage->dimensions() / GH.screenHandler().getScalingFactor();
+	return image->dimensions();
+}
+
+void ScalableImageInstance::setAlpha(uint8_t value)
+{
+	parameters.alphaValue = value;
+}
+
+void ScalableImageInstance::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const
+{
+	if (scaledImage)
+		scaledImage->draw(where, pos, src, scalingFactor);
+	else
+		image->draw(where, pos, src, parameters, scalingFactor);
+}
+
+void ScalableImageInstance::setOverlayColor(const ColorRGBA & color)
+{
+	parameters.ovelayColorMultiplier = color;
+
+	if (parameters.palette)
+		parameters.setOverlayColor(image->getPalette(), color);
+}
+
+void ScalableImageInstance::playerColored(const PlayerColor & player)
+{
+	parameters.player = player;
+
+	if (parameters.palette)
+		parameters.playerColored(player);
+
+	image->preparePlayerColoredImage(player);
+}
+
+void ScalableImageParameters::playerColored(PlayerColor player)
+{
+	graphics->setPlayerPalette(palette, player);
+}
+
+void ScalableImageInstance::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
+{
+	if (parameters.palette)
+		parameters.shiftPalette(image->getPalette(),firstColorID, colorsToMove, distanceToMove);
+}
+
+void ScalableImageInstance::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
+{
+	if (parameters.palette)
+		parameters.adjustPalette(image->getPalette(), blitMode, shifter, colorsToSkipMask);
+}
+
+void ScalableImageInstance::horizontalFlip()
+{
+	parameters.flipHorizontal = !parameters.flipHorizontal;
+}
+
+void ScalableImageInstance::verticalFlip()
+{
+	parameters.flipVertical = !parameters.flipVertical;
+}
+
+std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const
+{
+	ImageLocator loadingLocator;
+
+	loadingLocator.image = locator.image;
+	loadingLocator.defFile = locator.defFile;
+	loadingLocator.defFrame = locator.defFrame;
+	loadingLocator.defGroup = locator.defGroup;
+	loadingLocator.layer = mode;
+	loadingLocator.scalingFactor = scalingFactor;
+	loadingLocator.playerColored = color;
+
+	// best case - requested image is already available in filesystem
+	auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator);
+	if (loadedImage)
+		return loadedImage;
+
+	if (scalingFactor == 1)
+	{
+		// optional images for 1x resolution - only try load them, don't attempt to generate
+		// this block should never be called for 'body' layer - that image is loaded unconditionally before construction
+		assert(mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE);
+		return nullptr;
+	}
+
+	// alternatively, find largest pre-scaled image, load it and rescale to desired scaling
+	for (int8_t scaling = 4; scaling > 0; --scaling)
+	{
+		loadingLocator.scalingFactor = scaling;
+		auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator);
+		if (loadedImage)
+		{
+			if (scaling == 1)
+			{
+				if (mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE)
+				{
+					ScalableImageParameters parameters(getPalette(), mode);
+					return loadedImage->scaleInteger(scalingFactor, parameters.palette, mode);
+				}
+			}
+			else
+			{
+				Point targetSize = scaled[1].body[0]->dimensions() * scalingFactor;
+				return loadedImage->scaleTo(targetSize, nullptr);
+			}
+		}
+	}
+
+	ScalableImageParameters parameters(getPalette(), mode);
+	// if all else fails - use base (presumably, indexed) image and convert it to desired form
+	if (color != PlayerColor::CANNOT_DETERMINE)
+		parameters.playerColored(color);
+
+	if (upscalingSource)
+		return upscalingSource->scaleInteger(scalingFactor, parameters.palette, mode);
+	else
+		return scaled[1].body[0]->scaleInteger(scalingFactor, parameters.palette, mode);
+}
+
+void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor color)
+{
+	if (scaled[scalingFactor].body[0] == nullptr && scalingFactor != 1)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::OPAQUE:
+			case EImageBlitMode::COLORKEY:
+			case EImageBlitMode::SIMPLE:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(locator.layer, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			case EImageBlitMode::ONLY_BODY:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW:
+			case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+				break;
+		}
+	}
+
+	if (color != PlayerColor::CANNOT_DETERMINE && scaled[scalingFactor].playerColored[1+color.getNum()] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::OPAQUE:
+			case EImageBlitMode::COLORKEY:
+			case EImageBlitMode::SIMPLE:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			case EImageBlitMode::ONLY_BODY:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW:
+			case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+				break;
+		}
+	}
+
+	if (scaled[scalingFactor].shadow[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::WITH_SHADOW:
+			case EImageBlitMode::ONLY_SHADOW:
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
+				break;
+			default:
+				break;
+		}
+	}
+
+	if (scaled[scalingFactor].overlay[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::ONLY_OVERLAY:
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
+				break;
+			default:
+				break;
+		}
+	}
+}
+
+void ScalableImageShared::preparePlayerColoredImage(PlayerColor color)
+{
+	loadScaledImages(GH.screenHandler().getScalingFactor(), color);
+}

+ 125 - 0
client/renderSDL/ScalableImage.h

@@ -0,0 +1,125 @@
+/*
+ * ScalableImage.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../render/IImage.h"
+#include "../render/ImageLocator.h"
+#include "../render/Colors.h"
+
+#include "../../lib/Color.h"
+
+struct SDL_Palette;
+
+class ScalableImageInstance;
+class CanvasImage;
+
+struct ScalableImageParameters : boost::noncopyable
+{
+	SDL_Palette * palette = nullptr;
+
+	ColorRGBA colorMultiplier = Colors::WHITE_TRUE;
+	ColorRGBA ovelayColorMultiplier = Colors::WHITE_TRUE;
+
+	PlayerColor player = PlayerColor::CANNOT_DETERMINE;
+	uint8_t alphaValue = 255;
+
+	bool flipVertical = false;
+	bool flipHorizontal = false;
+
+	ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode);
+	~ScalableImageParameters();
+
+	void setShadowTransparency(const SDL_Palette * originalPalette, float factor);
+	void shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove);
+	void playerColored(PlayerColor player);
+	void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color);
+	void preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode);
+	void adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask);
+};
+
+class ScalableImageShared final : public std::enable_shared_from_this<ScalableImageShared>, boost::noncopyable
+{
+	static constexpr int scalingSize = 5; // 0-4 range. TODO: switch to 1-4 since there is no '0' scaling
+	static constexpr int maxFlips = 4;
+
+	using ImageType = std::shared_ptr<const ISharedImage>;
+	using FlippedImages = std::array<ImageType, maxFlips>;
+	using PlayerColoredImages = std::array<ImageType, PlayerColor::PLAYER_LIMIT_I + 1>; // all valid colors+neutral
+
+	struct ScaledImage
+	{
+		/// Upscaled shadow of our image, may be null
+		FlippedImages shadow;
+
+		/// Upscaled main part of our image, may be null
+		FlippedImages body;
+
+		/// Upscaled overlay (player color, selection highlight) of our image, may be null
+		FlippedImages overlay;
+
+		/// player-colored images of this particular scale, mostly for UI. These are never flipped in h3
+		PlayerColoredImages playerColored;
+	};
+
+	/// 1x-4x images. body for 1x scaling is guaranteed to be loaded
+	std::array<ScaledImage, scalingSize> scaled;
+
+	/// Locator of this image, for loading additional (e.g. upscaled) images
+	const SharedImageLocator locator;
+
+	std::shared_ptr<const ISharedImage> loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const;
+
+	void loadScaledImages(int8_t scalingFactor, PlayerColor color);
+
+public:
+	ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr<const ISharedImage> & baseImage);
+
+	Point dimensions() const;
+	void exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const;
+	bool isTransparent(const Point & coords) const;
+	Rect contentRect() const;
+	void draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor);
+
+	const SDL_Palette * getPalette() const;
+
+	std::shared_ptr<ScalableImageInstance> createImageReference();
+
+	void preparePlayerColoredImage(PlayerColor color);
+};
+
+class ScalableImageInstance final : public IImage
+{
+	friend class ScalableImageShared;
+
+	std::shared_ptr<ScalableImageShared> image;
+	std::shared_ptr<CanvasImage> scaledImage;
+
+	ScalableImageParameters parameters;
+	EImageBlitMode blitMode;
+
+public:
+	ScalableImageInstance(const std::shared_ptr<ScalableImageShared> & image, EImageBlitMode blitMode);
+
+	void scaleTo(const Point & size, EScalingAlgorithm algorithm) override;
+	void exportBitmap(const boost::filesystem::path & path) const override;
+	bool isTransparent(const Point & coords) const override;
+	Rect contentRect() const override;
+	Point dimensions() const override;
+	void setAlpha(uint8_t value) override;
+	void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override;
+	void setOverlayColor(const ColorRGBA & color) override;
+	void playerColored(const 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 horizontalFlip();
+	void verticalFlip();
+};
+

+ 5 - 4
client/renderSDL/ScreenHandler.cpp

@@ -11,13 +11,14 @@
 #include "StdInc.h"
 #include "ScreenHandler.h"
 
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/constants/StringConstants.h"
-#include "../gui/CGuiHandler.h"
 #include "../eventsSDL/NotificationHandler.h"
+#include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
+#include "../renderSDL/SDL_Extensions.h"
 #include "CMT.h"
-#include "SDL_Extensions.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/constants/StringConstants.h"
 
 #ifdef VCMI_ANDROID
 #include "../lib/CAndroidVMHelper.h"

+ 9 - 6
client/widgets/Images.cpp

@@ -13,7 +13,6 @@
 #include "MiscWidgets.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../renderSDL/SDL_Extensions.h"
 #include "../render/AssetGenerator.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
@@ -53,8 +52,8 @@ CPicture::CPicture( const ImagePath & bmpname )
 	: CPicture(bmpname, Point(0,0))
 {}
 
-CPicture::CPicture( const ImagePath & bmpname, const Point & position )
-	: bg(GH.renderHandler().loadImage(bmpname, EImageBlitMode::COLORKEY))
+CPicture::CPicture( const ImagePath & bmpname, const Point & position, EImageBlitMode mode )
+	: bg(GH.renderHandler().loadImage(bmpname, mode))
 	, needRefresh(false)
 {
 	pos.x += position.x;
@@ -74,6 +73,10 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position )
 	addUsedEvents(SHOW_POPUP);
 }
 
+CPicture::CPicture( const ImagePath & bmpname, const Point & position )
+	:CPicture(bmpname, position, EImageBlitMode::COLORKEY)
+{}
+
 CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
 	: CPicture(bmpname, Point(x,y))
 {
@@ -118,7 +121,7 @@ void CPicture::setAlpha(uint8_t value)
 
 void CPicture::scaleTo(Point size)
 {
-	bg->scaleTo(size);
+	bg->scaleTo(size, EScalingAlgorithm::BILINEAR);
 
 	pos.w = bg->width();
 	pos.h = bg->height();
@@ -160,7 +163,7 @@ CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position, Rect
 
 void CFilledTexture::showAll(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
+	CanvasClipRectGuard guard(to, pos);
 
 	for (int y=pos.top(); y < pos.bottom(); y+= imageArea.h)
 	{
@@ -266,7 +269,7 @@ void CAnimImage::showAll(Canvas & to)
 		if(auto img = anim->getImage(targetFrame, group))
 		{
 			if(isScaled())
-				img->scaleTo(scaledSize);
+				img->scaleTo(scaledSize, EScalingAlgorithm::BILINEAR);
 
 			to.draw(img, pos.topLeft());
 		}

+ 2 - 0
client/widgets/Images.h

@@ -21,6 +21,7 @@ class CAnimImage;
 class CLabel;
 class CAnimation;
 class IImage;
+enum class EImageBlitMode : uint8_t;
 
 // Image class
 class CPicture : public CIntObject
@@ -49,6 +50,7 @@ public:
 
 	/// Loads image from specified file name
 	CPicture(const ImagePath & bmpname);
+	CPicture(const ImagePath & bmpname, const Point & position, EImageBlitMode mode);
 	CPicture(const ImagePath & bmpname, const Point & position);
 	CPicture(const ImagePath & bmpname, int x, int y);
 

+ 1 - 2
client/widgets/TextControls.cpp

@@ -18,7 +18,6 @@
 #include "../windows/CMessage.h"
 #include "../windows/InfoWindows.h"
 #include "../adventureMap/CInGameConsole.h"
-#include "../renderSDL/SDL_Extensions.h"
 #include "../render/Canvas.h"
 #include "../render/Graphics.h"
 #include "../render/IFont.h"
@@ -298,7 +297,7 @@ void CMultiLineLabel::showAll(Canvas & to)
 	Point lineStart = getTextLocation().topLeft() - visibleSize + Point(0, beginLine * fontPtr->getLineHeight());
 	Point lineSize = Point(getTextLocation().w, fontPtr->getLineHeight());
 
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), getTextLocation()); // to properly trim text that is too big to fit
+	CanvasClipRectGuard guard(to, getTextLocation()); // to properly trim text that is too big to fit
 
 	for(int i = beginLine; i < std::min(totalLines, endLine); i++)
 	{

+ 2 - 2
client/widgets/VideoWidget.cpp

@@ -86,7 +86,7 @@ void VideoWidgetBase::playVideo(const VideoPath & fileToPlay)
 void VideoWidgetBase::show(Canvas & to)
 {
 	if(videoInstance)
-		videoInstance->show(pos.topLeft(), to);
+		to.draw(*videoInstance, pos.topLeft());
 	if(subTitle)
 		subTitle->showAll(to);
 }
@@ -162,7 +162,7 @@ void VideoWidgetBase::deactivate()
 void VideoWidgetBase::showAll(Canvas & to)
 {
 	if(videoInstance)
-		videoInstance->show(pos.topLeft(), to);
+		to.draw(*videoInstance, pos.topLeft());
 	if(subTitle)
 		subTitle->showAll(to);
 }

+ 2 - 3
client/windows/CCastleInterface.cpp

@@ -570,9 +570,8 @@ CCastleBuildings::CCastleBuildings(const CGTownInstance* Town):
 {
 	OBJECT_CONSTRUCTION;
 
-	background = std::make_shared<CPicture>(town->getTown()->clientInfo.townBackground);
+	background = std::make_shared<CPicture>(town->getTown()->clientInfo.townBackground, Point(0,0), EImageBlitMode::OPAQUE);
 	background->needRefresh = true;
-	background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
 	pos.w = background->pos.w;
 	pos.h = background->pos.h;
 
@@ -974,7 +973,7 @@ void CCastleBuildings::enterCastleGate(BuildingID building)
 			if(settings["general"]["enableUiEnhancements"].Bool())
 			{
 				auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
-				image->scaleTo(Point(35, 23));
+				image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
 				images.push_back(image);
 			}
 		}

+ 14 - 15
client/windows/CMapOverview.cpp

@@ -20,7 +20,7 @@
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
-#include "../render/Canvas.h"
+#include "../render/CanvasImage.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
 #include "../render/Graphics.h"
@@ -58,9 +58,10 @@ CMapOverview::CMapOverview(const std::string & mapName, const std::string & file
 	fitToScreen(10);
 }
 
-Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr<CMap> & map, int layer) const
+std::shared_ptr<CanvasImage> CMapOverviewWidget::createMinimapForLayer(std::unique_ptr<CMap> & map, int layer) const
 {
-	Canvas canvas = Canvas(Point(map->width, map->height), CanvasScalingPolicy::IGNORE);
+	auto canvasImage = GH.renderHandler().createImage(Point(map->width, map->height), CanvasScalingPolicy::IGNORE);
+	auto canvas = canvasImage->getCanvas();
 
 	for (int y = 0; y < map->height; ++y)
 		for (int x = 0; x < map->width; ++x)
@@ -91,12 +92,12 @@ Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr<CMap> & map, in
 			canvas.drawPoint(Point(x, y), color);
 		}
 	
-	return canvas;
+	return canvasImage;
 }
 
-std::vector<Canvas> CMapOverviewWidget::createMinimaps(ResourcePath resource) const
+std::vector<std::shared_ptr<CanvasImage>> CMapOverviewWidget::createMinimaps(ResourcePath resource) const
 {
-	auto ret = std::vector<Canvas>();
+	std::vector<std::shared_ptr<CanvasImage>> ret;
 
 	CMapService mapService;
 	std::unique_ptr<CMap> map;
@@ -113,9 +114,9 @@ std::vector<Canvas> CMapOverviewWidget::createMinimaps(ResourcePath resource) co
 	return createMinimaps(map);
 }
 
-std::vector<Canvas> CMapOverviewWidget::createMinimaps(std::unique_ptr<CMap> & map) const
+std::vector<std::shared_ptr<CanvasImage>> CMapOverviewWidget::createMinimaps(std::unique_ptr<CMap> & map) const
 {
-	auto ret = std::vector<Canvas>();
+	std::vector<std::shared_ptr<CanvasImage>> ret;
 
 	for(int i = 0; i < (map->twoLevel ? 2 : 1); i++)
 		ret.push_back(createMinimapForLayer(map, i));
@@ -133,17 +134,15 @@ std::shared_ptr<CPicture> CMapOverviewWidget::buildDrawMinimap(const JsonNode &
 	if(id >= minimaps.size())
 		return nullptr;
 
-	Rect minimapRect = minimaps[id].getRenderArea();
-	double maxSideLengthSrc = std::max(minimapRect.w, minimapRect.h);
+	Point minimapRect = minimaps[id]->dimensions();
+	double maxSideLengthSrc = std::max(minimapRect.x, minimapRect.y);
 	double maxSideLengthDst = std::max(rect.w, rect.h);
 	double resize = maxSideLengthSrc / maxSideLengthDst;
-	Point newMinimapSize = Point(minimapRect.w / resize, minimapRect.h / resize);
+	Point newMinimapSize = Point(minimapRect.x / resize, minimapRect.y / resize);
 
-	Canvas canvasScaled = Canvas(Point(rect.w, rect.h), CanvasScalingPolicy::AUTO);
-	canvasScaled.drawScaled(minimaps[id], Point((rect.w - newMinimapSize.x) / 2, (rect.h - newMinimapSize.y) / 2), newMinimapSize);
-	std::shared_ptr<IImage> img = GH.renderHandler().createImage(canvasScaled.getInternalSurface());
+	minimaps[id]->scaleTo(newMinimapSize, EScalingAlgorithm::NEAREST); // for sharp-looking minimap
 
-	return std::make_shared<CPicture>(img, Point(rect.x, rect.y));
+	return std::make_shared<CPicture>(minimaps[id], Point(rect.x, rect.y));
 }
 
 CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent):

+ 5 - 5
client/windows/CMapOverview.h

@@ -22,7 +22,7 @@ class CPicture;
 class CFilledTexture;
 class CTextBox;
 class IImage;
-class Canvas;
+class CanvasImage;
 class TransparentFilledRectangle;
 enum class ESelectionScreen : ui8;
 
@@ -33,11 +33,11 @@ class CMapOverviewWidget : public InterfaceObjectConfigurable
 	CMapOverview& p;
 
 	bool drawPlayerElements;
-	std::vector<Canvas> minimaps;
+	std::vector<std::shared_ptr<CanvasImage>> minimaps;
 
-	Canvas createMinimapForLayer(std::unique_ptr<CMap> & map, int layer) const;
-	std::vector<Canvas> createMinimaps(ResourcePath resource) const;
-	std::vector<Canvas> createMinimaps(std::unique_ptr<CMap> & map) const;
+	std::shared_ptr<CanvasImage> createMinimapForLayer(std::unique_ptr<CMap> & map, int layer) const;
+	std::vector<std::shared_ptr<CanvasImage>> createMinimaps(ResourcePath resource) const;
+	std::vector<std::shared_ptr<CanvasImage>> createMinimaps(std::unique_ptr<CMap> & map) const;
 
 	std::shared_ptr<CPicture> buildDrawMinimap(const JsonNode & config) const;
 public:

+ 1 - 2
client/windows/CQuestLog.cpp

@@ -21,7 +21,6 @@
 #include "../adventureMap/AdventureMapInterface.h"
 #include "../adventureMap/CMinimap.h"
 #include "../render/Canvas.h"
-#include "../renderSDL/SDL_Extensions.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CArtHandler.h"
@@ -61,7 +60,7 @@ void CQuestIcon::clickPressed(const Point & cursorPosition)
 
 void CQuestIcon::showAll(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), parent->pos);
+	CanvasClipRectGuard guard(to, parent->pos);
 	CAnimImage::showAll(to);
 }
 

+ 26 - 66
client/windows/CWindowObject.cpp

@@ -23,6 +23,7 @@
 #include "../render/IScreenHandler.h"
 #include "../render/IRenderHandler.h"
 #include "../render/Canvas.h"
+#include "../render/CanvasImage.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
@@ -87,8 +88,7 @@ std::shared_ptr<CPicture> CWindowObject::createBg(const ImagePath & imageName, b
 	if(imageName.empty())
 		return nullptr;
 
-	auto image = std::make_shared<CPicture>(imageName);
-	image->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
+	auto image = std::make_shared<CPicture>(imageName, Point(0,0), EImageBlitMode::OPAQUE);
 	if(playerColored)
 		image->setPlayerColor(LOCPLINT->playerID);
 	return image;
@@ -116,8 +116,7 @@ void CWindowObject::updateShadow()
 void CWindowObject::setShadow(bool on)
 {
 	//size of shadow
-	int sizeOriginal = 8;
-	int size = sizeOriginal * GH.screenHandler().getScalingFactor();
+	int size = 8;
 
 	if(on == !shadowParts.empty())
 		return;
@@ -130,61 +129,12 @@ void CWindowObject::setShadow(bool on)
 
 	if(on)
 	{
-
-		//helper to set last row
-		auto blitAlphaRow = [](SDL_Surface *surf, size_t row)
-		{
-			uint8_t * ptr = (uint8_t*)surf->pixels + surf->pitch * (row);
-
-			for (size_t i=0; i< surf->w; i++)
-			{
-				Channels::px<4>::a.set(ptr, 128);
-				ptr+=4;
-			}
-		};
-
-		// helper to set last column
-		auto blitAlphaCol = [](SDL_Surface *surf, size_t col)
-		{
-			uint8_t * ptr = (uint8_t*)surf->pixels + 4 * (col);
-
-			for (size_t i=0; i< surf->h; i++)
-			{
-				Channels::px<4>::a.set(ptr, 128);
-				ptr+= surf->pitch;
-			}
-		};
-
-		static SDL_Surface * shadowCornerTempl = nullptr;
-		static SDL_Surface * shadowBottomTempl = nullptr;
-		static SDL_Surface * shadowRightTempl = nullptr;
-
-		//one-time initialization
-		if(!shadowCornerTempl)
-		{
-			//create "template" surfaces
-			shadowCornerTempl = CSDL_Ext::createSurfaceWithBpp<4>(size, size);
-			shadowBottomTempl = CSDL_Ext::createSurfaceWithBpp<4>(1, size);
-			shadowRightTempl  = CSDL_Ext::createSurfaceWithBpp<4>(size, 1);
-
-			//fill with shadow body color
-			CSDL_Ext::fillSurface(shadowCornerTempl, { 0, 0, 0, 192 } );
-			CSDL_Ext::fillSurface(shadowBottomTempl, { 0, 0, 0, 192 } );
-			CSDL_Ext::fillSurface(shadowRightTempl,  { 0, 0, 0, 192 } );
-
-			//fill last row and column with more transparent color
-			blitAlphaCol(shadowRightTempl , size-1);
-			blitAlphaCol(shadowCornerTempl, size-1);
-			blitAlphaRow(shadowBottomTempl, size-1);
-			blitAlphaRow(shadowCornerTempl, size-1);
-		}
-
 		//FIXME: do something with this points
 		Point shadowStart;
 		if (options & BORDERED)
-			shadowStart = Point(sizeOriginal - 14, sizeOriginal - 14);
+			shadowStart = Point(size - 14, size - 14);
 		else
-			shadowStart = Point(sizeOriginal, sizeOriginal);
+			shadowStart = Point(size, size);
 
 		Point shadowPos;
 		if (options & BORDERED)
@@ -198,26 +148,36 @@ void CWindowObject::setShadow(bool on)
 		else
 			fullsize = Point(pos.w, pos.h);
 
+		Point sizeCorner(size, size);
+		Point sizeRight(fullsize.x - size, size);
+		Point sizeBottom(size, fullsize.y - size);
+
 		//create base 8x8 piece of shadow
-		SDL_Surface * shadowCorner = CSDL_Ext::copySurface(shadowCornerTempl);
-		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - sizeOriginal) * GH.screenHandler().getScalingFactor(), size);
-		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, (fullsize.y - sizeOriginal) * GH.screenHandler().getScalingFactor());
+		auto imageCorner = GH.renderHandler().createImage(sizeCorner, CanvasScalingPolicy::AUTO);
+		auto imageRight  = GH.renderHandler().createImage(sizeRight,  CanvasScalingPolicy::AUTO);
+		auto imageBottom = GH.renderHandler().createImage(sizeBottom, CanvasScalingPolicy::AUTO);
+
+		Canvas canvasCorner = imageCorner->getCanvas();
+		Canvas canvasRight = imageRight->getCanvas();
+		Canvas canvasBottom = imageBottom->getCanvas();
+
+		canvasCorner.drawColor(Rect(Point(0,0), sizeCorner), { 0, 0, 0, 128 });
+		canvasRight.drawColor(Rect(Point(0,0), sizeRight), { 0, 0, 0, 128 });
+		canvasBottom.drawColor(Rect(Point(0,0), sizeBottom), { 0, 0, 0, 128 });
 
-		blitAlphaCol(shadowBottom, 0);
-		blitAlphaRow(shadowRight, 0);
+		canvasCorner.drawColor(Rect(Point(0,0), sizeCorner - Point(1,1)), { 0, 0, 0, 192 });
+		canvasRight.drawColor(Rect(Point(0,0),   sizeRight - Point(0,1)), { 0, 0, 0, 192 });
+		canvasBottom.drawColor(Rect(Point(0,0), sizeBottom - Point(1,0)), { 0, 0, 0, 192 });
 
 		//generate "shadow" object with these 3 pieces in it
 		{
 			OBJECT_CONSTRUCTION;
 
-			shadowParts.push_back(std::make_shared<CPicture>( GH.renderHandler().createImage(shadowCorner), Point(shadowPos.x,   shadowPos.y)));
-			shadowParts.push_back(std::make_shared<CPicture>( GH.renderHandler().createImage(shadowRight ),  Point(shadowPos.x,   shadowStart.y)));
-			shadowParts.push_back(std::make_shared<CPicture>( GH.renderHandler().createImage(shadowBottom), Point(shadowStart.x, shadowPos.y)));
+			shadowParts.push_back(std::make_shared<CPicture>( imageCorner, Point(shadowPos.x,   shadowPos.y)));
+			shadowParts.push_back(std::make_shared<CPicture>( imageRight, Point(shadowStart.x, shadowPos.y)));
+			shadowParts.push_back(std::make_shared<CPicture>( imageBottom,  Point(shadowPos.x,   shadowStart.y)));
 
 		}
-		SDL_FreeSurface(shadowCorner);
-		SDL_FreeSurface(shadowBottom);
-		SDL_FreeSurface(shadowRight);
 	}
 }
 

+ 1 - 1
client/windows/CWindowWithArtifacts.cpp

@@ -236,7 +236,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->scaleTo(Point(44,34));
+		image->scaleTo(Point(44,34), EScalingAlgorithm::BILINEAR);
 
 		CCS->curh->dragAndDropCursor(image);
 	}

+ 9 - 1
docs/modders/HD_Graphics.md

@@ -20,7 +20,7 @@ For upscaled images you have to use following folders (next to `sprites`, `data`
 
 The sprites should have the same name and folder structure as in `sprites`, `data` and `video` folder. All images that are missing in the upscaled folders are scaled with the selected upscaling filter instead of using prescaled images.
 
-### Shadows / Overlays
+### Shadows / Overlays / Player-colored images
 
 It's also possible (but not necessary) to add high-definition shadows: Just place a image next to the normal upscaled image with the suffix `-shadow`. E.g. `TestImage.png` and `TestImage-shadow.png`.
 In future, such shadows will likely become required to correctly exclude shadow from effects such as Clone spell.
@@ -36,3 +36,11 @@ Currently needed for:
 
 - Flaggable adventure map objects. Overlay must contain a transparent image with white flags on it and will be used to colorize flags to owning player
 - Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover
+
+For images that are used for player-colored interface, it is possible to provide custom images for each player. For example `HeroScr4-red.png` will be used for hero window of red player.
+
+- Currently needed for all UI elements that are player-colored in HoMM3.
+- Can NOT be used for player-owned adventure objects. Use `-overlay` images for such objects.
+- Possible suffixes are `red`, `blue`, `tan`, `green`, `orange`, `purple`, `teal`, `pink`, `neutral` (used only for turn order queue in combat)
+
+It is possible to use such additional images for both upscaled (xbrz) graphics, as well as for original / 1x images. When using this feature for original / 1x image, make sure that your base image (without suffix) is rgb/rgba image, and not indexed / with palette

+ 5 - 0
lib/Point.h

@@ -54,6 +54,11 @@ public:
 		return Point(x*mul, y*mul);
 	}
 
+	constexpr Point operator/(const Point &b) const
+	{
+		return Point(x/b.x,y/b.y);
+	}
+
 	constexpr Point operator*(const Point &b) const
 	{
 		return Point(x*b.x,y*b.y);