From 7eae917497cb4a0e433fe05c23bdb3480946e7d9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:50:16 +0200 Subject: [PATCH 01/14] basic algorithm --- client/render/ImageLocator.cpp | 10 + client/render/ImageLocator.h | 16 ++ client/renderSDL/RenderHandler.cpp | 31 ++- client/renderSDL/SDLImage.cpp | 41 ++++ client/renderSDL/SDLImage.h | 3 + client/renderSDL/SDL_Extensions.cpp | 310 ++++++++++++++++++++++++++++ client/renderSDL/SDL_Extensions.h | 3 + client/renderSDL/ScalableImage.cpp | 2 + 8 files changed, 407 insertions(+), 9 deletions(-) diff --git a/client/render/ImageLocator.cpp b/client/render/ImageLocator.cpp index af6343c19..a4c1a20e1 100644 --- a/client/render/ImageLocator.cpp +++ b/client/render/ImageLocator.cpp @@ -25,6 +25,12 @@ SharedImageLocator::SharedImageLocator(const JsonNode & config, EImageBlitMode m if(!config["defFile"].isNull()) defFile = AnimationPath::fromJson(config["defFile"]); + + if(!config["generateShadow"].isNull()) + generateShadow = static_cast(config["generateShadow"].Integer()); + + if(!config["generateOverlay"].isNull()) + generateOverlay = static_cast(config["generateOverlay"].Integer()); } SharedImageLocator::SharedImageLocator(const ImagePath & path, EImageBlitMode mode) @@ -60,6 +66,10 @@ bool SharedImageLocator::operator < (const SharedImageLocator & other) const return defFrame < other.defFrame; if(layer != other.layer) return layer < other.layer; + if(generateShadow != other.generateShadow) + return generateShadow < other.generateShadow; + if(generateOverlay != other.generateOverlay) + return generateOverlay < other.generateOverlay; return false; } diff --git a/client/render/ImageLocator.h b/client/render/ImageLocator.h index 59754b2b6..7b3f2cc1b 100644 --- a/client/render/ImageLocator.h +++ b/client/render/ImageLocator.h @@ -16,12 +16,28 @@ struct SharedImageLocator { + enum ShadowMode + { + SHADOW_NONE, + SHADOW_NORMAL, + SHADOW_SHEAR + }; + enum OverlayMode + { + OVERLAY_NONE, + OVERLAY_OUTLINE, + OVERLAY_FLAG + }; + std::optional image; std::optional defFile; int defFrame = -1; int defGroup = -1; EImageBlitMode layer = EImageBlitMode::OPAQUE; + std::optional generateShadow; + std::optional generateOverlay; + SharedImageLocator() = default; SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode layer); SharedImageLocator(const JsonNode & config, EImageBlitMode layer); diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 6f9bb8aa9..96d0a21b8 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -291,9 +291,14 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato std::string imagePathString = pathToLoad.getName(); - if(locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION) + bool generateShadow = locator.generateShadow && (*locator.generateShadow) != SharedImageLocator::ShadowMode::SHADOW_NONE; + bool generateOverlay = locator.generateOverlay && (*locator.generateOverlay) != SharedImageLocator::OverlayMode::OVERLAY_NONE; + bool isShadow = locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR; + bool isOverlay = locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION; + + if(isOverlay && !generateOverlay) imagePathString += "-OVERLAY"; - if(locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR) + if(isShadow && !generateShadow) imagePathString += "-SHADOW"; if(locator.playerColored.isValidPlayer()) imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]); @@ -304,16 +309,24 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato auto imagePathSprites = ImagePath::builtin(imagePathString).addPrefix(scaledSpritesPath.at(locator.scalingFactor)); auto imagePathData = ImagePath::builtin(imagePathString).addPrefix(scaledDataPath.at(locator.scalingFactor)); + std::shared_ptr img = nullptr; + if(CResourceHandler::get()->existsResource(imagePathSprites)) - return std::make_shared(imagePathSprites); + img = std::make_shared(imagePathSprites); + else if(CResourceHandler::get()->existsResource(imagePathData)) + img = std::make_shared(imagePathData); + else if(CResourceHandler::get()->existsResource(imagePath)) + img = std::make_shared(imagePath); - if(CResourceHandler::get()->existsResource(imagePathData)) - return std::make_shared(imagePathData); + if(img) + { + if(isShadow && generateShadow) + img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEER); + if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE) + img = img->drawOutline(Colors::WHITE, getScalingFactor()); + } - if(CResourceHandler::get()->existsResource(imagePath)) - return std::make_shared(imagePath); - - return nullptr; + return img; } std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator) diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 9be02d437..424dd1531 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -429,6 +429,47 @@ std::shared_ptr SDLImageShared::verticalFlip() const return ret; } +std::shared_ptr SDLImageShared::drawShadow(bool doSheer) const +{ + if(upscalingInProgress) + throw std::runtime_error("Attempt to access images that is still being loaded!"); + + if (!surf) + return shared_from_this(); + + SDL_Surface * shadow = CSDL_Ext::drawShadow(surf, doSheer); + auto ret = std::make_shared(shadow); + ret->fullSize = fullSize; + ret->margins.x = margins.x; + ret->margins.y = margins.y; + + // erase our own reference + SDL_FreeSurface(shadow); + + return ret; +} + +std::shared_ptr SDLImageShared::drawOutline(const ColorRGBA & color, int thickness) const +{ + if(upscalingInProgress) + throw std::runtime_error("Attempt to access images that is still being loaded!"); + + if (!surf) + return shared_from_this(); + + SDL_Color sdlColor = { color.r, color.g, color.b, color.a }; + SDL_Surface * outline = CSDL_Ext::drawOutline(surf, sdlColor, thickness); + auto ret = std::make_shared(outline); + ret->fullSize = fullSize; + ret->margins.x = margins.x; + ret->margins.y = margins.y; + + // erase our own reference + SDL_FreeSurface(outline); + + return ret; +} + // Keep the original palette, in order to do color switching operation void SDLImageShared::savePalette() { diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index bd81d01cf..3adf46733 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -71,5 +71,8 @@ public: [[nodiscard]] std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override; [[nodiscard]] std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const override; + std::shared_ptr drawShadow(bool doSheer) const; + std::shared_ptr drawOutline(const ColorRGBA & color, int thickness) const; + friend class SDLImageLoader; }; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 8e953f04a..e7d73dd24 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -685,3 +685,313 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) other = CSDL_Ext::fromSDL(rect); } + +SDL_Surface * CSDL_Ext::drawOutline(SDL_Surface * source, const SDL_Color & color, int thickness) +{ + // ensure format + SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); + SDL_Surface *destSurface = newSurface(Point(source->w, source->h)); + + int width = sourceSurface->w; + int height = sourceSurface->h; + + // Iterate through the pixels of the image + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + Uint8 maxPixel = 0; + Uint8 minPixel = 255; + int halfThickness = (thickness + 1) / 2; + + // Loop over the neighborhood around (x, y) + for(int offsetY = -halfThickness; offsetY <= halfThickness; offsetY++) + { + for(int offsetX = -halfThickness; offsetX <= halfThickness; offsetX++) + { + // Circle instead of rectangle + if(offsetX * offsetX + offsetY * offsetY > halfThickness * halfThickness) + continue; + + int neighborX = x + offsetX; + int neighborY = y + offsetY; + + // Check image bounds + if(neighborX >= 0 && neighborX < destSurface->w && neighborY >= 0 && neighborY < destSurface->h) + { + // Get the pixel at the neighbor position + Uint32 pixel = *((Uint32*)sourceSurface->pixels + neighborY * width + neighborX); + Uint8 r, g, b, a; + SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a); + + // Compare the pixel alpha value to find the maximum and maximum + if(a > maxPixel) + maxPixel = a; + if(a < minPixel) + minPixel = a; + } + } + } + + Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, maxPixel - minPixel); + *((Uint32*)destSurface->pixels + y * width + x) = newPixel; + } + } + + SDL_FreeSurface(sourceSurface); + + return destSurface; +} + +void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b, double c, double d, double tx, double ty) +{ + assert(src->format->format == SDL_PIXELFORMAT_ARGB8888); + assert(dst->format->format == SDL_PIXELFORMAT_ARGB8888); + + // Lock surfaces for direct pixel access + if (SDL_MUSTLOCK(src)) SDL_LockSurface(src); + if (SDL_MUSTLOCK(dst)) SDL_LockSurface(dst); + + // Calculate inverse matrix M_inv for mapping dst -> src + double det = a * d - b * c; + if (det == 0) + throw std::runtime_error("Singular transform matrix!"); + double invDet = 1.0 / det; + double ia = d * invDet; + double ib = -b * invDet; + double ic = -c * invDet; + double id = a * invDet; + + // For each pixel in the destination image + for(int y = 0; y < dst->h; y++) + { + for(int x = 0; x < dst->w; x++) + { + // Map destination pixel (x,y) back to source coordinates (srcX, srcY) + double srcX = ia * (x - tx) + ib * (y - ty); + double srcY = ic * (x - tx) + id * (y - ty); + + // Nearest neighbor sampling (can be improved to bilinear) + int srcXi = static_cast(round(srcX)); + int srcYi = static_cast(round(srcY)); + + // Check bounds + if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) + { + Uint32* srcPixels = (Uint32*)src->pixels; + Uint32* dstPixels = (Uint32*)dst->pixels; + + Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; + dstPixels[y * dst->w + x] = pixel; + } + else + { + // Outside source bounds: set transparent or black + Uint32* dstPixels = (Uint32*)dst->pixels; + dstPixels[y * dst->w + x] = 0x00000000; // transparent black + } + } + } + + if (SDL_MUSTLOCK(src)) SDL_UnlockSurface(src); + if (SDL_MUSTLOCK(dst)) SDL_UnlockSurface(dst); +} + +int getLowestNonTransparentY(SDL_Surface* surface) +{ + assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); + + if(SDL_MUSTLOCK(surface)) + SDL_LockSurface(surface); + + int w = surface->w; + int h = surface->h; + int bpp = surface->format->BytesPerPixel; + Uint8* pixels = (Uint8*)surface->pixels; + + for(int y = h - 1; y >= 0; --y) + { + Uint8* row = pixels + y * surface->pitch; + + for(int x = 0; x < w; ++x) + { + Uint32 pixel = *(Uint32*)(row + x * bpp); + + Uint8 r, g, b, a; + SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); + + if (a > 0) + { + if(SDL_MUSTLOCK(surface)) + SDL_UnlockSurface(surface); + return y; + } + } + } + + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); + return -1; // fully transparent +} + +void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uint8 a) +{ + assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); + + if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); + + Uint32* pixels = (Uint32*)surface->pixels; + int pixelCount = surface->w * surface->h; + + for (int i = 0; i < pixelCount; i++) + { + Uint32 pixel = pixels[i]; + + Uint8 pr, pg, pb, pa; + // Extract existing RGBA components using SDL_GetRGBA + SDL_GetRGBA(pixel, surface->format, &pr, &pg, &pb, &pa); + + Uint32 newPixel = SDL_MapRGBA(surface->format, r, g, b, a); + if(pa == 0) + newPixel = SDL_MapRGBA(surface->format, 0, 0, 0, 0); + + pixels[i] = newPixel; + } + + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); +} + +void gaussianBlur(SDL_Surface* surface, int amount) +{ + assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); + + if (!surface || amount <= 0) return; + + if (SDL_MUSTLOCK(surface)) + { + if (SDL_LockSurface(surface) != 0) + throw std::runtime_error("Failed to lock surface!"); + } + + int width = surface->w; + int height = surface->h; + int pixelCount = width * height; + + Uint32* pixels = static_cast(surface->pixels); + + std::vector srcR(pixelCount); + std::vector srcG(pixelCount); + std::vector srcB(pixelCount); + std::vector srcA(pixelCount); + + std::vector dstR(pixelCount); + std::vector dstG(pixelCount); + std::vector dstB(pixelCount); + std::vector dstA(pixelCount); + + // Initialize src channels from surface pixels + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + Uint32 pixel = pixels[y * width + x]; + Uint8 r, g, b, a; + SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); + + int idx = y * width + x; + srcR[idx] = r; + srcG[idx] = g; + srcB[idx] = b; + srcA[idx] = a; + } + } + + // 3x3 Gaussian kernel + float kernel[3][3] = { + {1.f/16, 2.f/16, 1.f/16}, + {2.f/16, 4.f/16, 2.f/16}, + {1.f/16, 2.f/16, 1.f/16} + }; + + // Apply the blur 'amount' times for stronger blur + for (int iteration = 0; iteration < amount; ++iteration) { + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + float sumR = 0.f, sumG = 0.f, sumB = 0.f, sumA = 0.f; + + for (int ky = -1; ky <= 1; ++ky) { + for (int kx = -1; kx <= 1; ++kx) { + int nx = x + kx; + int ny = y + ky; + + // Clamp edges + if (nx < 0) nx = 0; + else if (nx >= width) nx = width - 1; + if (ny < 0) ny = 0; + else if (ny >= height) ny = height - 1; + + int nIdx = ny * width + nx; + float kval = kernel[ky + 1][kx + 1]; + + sumR += srcR[nIdx] * kval; + sumG += srcG[nIdx] * kval; + sumB += srcB[nIdx] * kval; + sumA += srcA[nIdx] * kval; + } + } + + int idx = y * width + x; + dstR[idx] = static_cast(sumR); + dstG[idx] = static_cast(sumG); + dstB[idx] = static_cast(sumB); + dstA[idx] = static_cast(sumA); + } + } + // Swap src and dst for next iteration (blur chaining) + srcR.swap(dstR); + srcG.swap(dstG); + srcB.swap(dstB); + srcA.swap(dstA); + } + + // After final iteration, write back to surface pixels + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int idx = y * width + x; + pixels[idx] = SDL_MapRGBA(surface->format, srcR[idx], srcG[idx], srcB[idx], srcA[idx]); + } + } + + if (SDL_MUSTLOCK(surface)) { + SDL_UnlockSurface(surface); + } +} + +SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer) +{ + SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); + SDL_Surface *destSurface = newSurface(Point(source->w, source->h)); + + assert(destSurface->format->format == SDL_PIXELFORMAT_ARGB8888); + + double shearX = doSheer ? 0.5 : 0.0; + double scaleY = 0.25; + + int lowestSource = getLowestNonTransparentY(sourceSurface); + int lowestTransformed = lowestSource * scaleY; + + // Parameters for applyAffineTransform + double a = 1.0; + double b = shearX; + double c = 0.0; + double d = scaleY; + double tx = -shearX * lowestSource; + double ty = lowestSource - lowestTransformed; + + applyAffineTransform(sourceSurface, destSurface, a, b, c, d, tx, ty); + fillAlphaPixelWithRGBA(destSurface, 0, 0, 0, 128); + gaussianBlur(destSurface, 1); + + SDL_FreeSurface(sourceSurface); + + return destSurface; +} diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index daa70e28f..d90934926 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -76,4 +76,7 @@ SDL_Color toSDL(const ColorRGBA & color); void setDefaultColorKey(SDL_Surface * surface); ///set key-color to 0,255,255 only if it exactly mapped void setDefaultColorKeyPresize(SDL_Surface * surface); + + SDL_Surface * drawOutline(SDL_Surface * source, const SDL_Color & color, int thickness); + SDL_Surface * drawShadow(SDL_Surface * source, bool doSheer); } diff --git a/client/renderSDL/ScalableImage.cpp b/client/renderSDL/ScalableImage.cpp index a7a8746f8..b490994a7 100644 --- a/client/renderSDL/ScalableImage.cpp +++ b/client/renderSDL/ScalableImage.cpp @@ -440,6 +440,8 @@ std::shared_ptr ScalableImageShared::loadOrGenerateImage(EIm loadingLocator.image = locator.image; loadingLocator.defFile = locator.defFile; + loadingLocator.generateShadow = locator.generateShadow; + loadingLocator.generateOverlay = locator.generateOverlay; loadingLocator.defFrame = locator.defFrame; loadingLocator.defGroup = locator.defGroup; loadingLocator.layer = mode; From 79dead4ad6e4a6b2e8e7b91519afd4684b699293 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Jul 2025 23:03:46 +0200 Subject: [PATCH 02/14] fix --- client/renderSDL/RenderHandler.cpp | 2 +- client/renderSDL/SDLImage.cpp | 8 ++++---- client/renderSDL/SDLImage.h | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 96d0a21b8..a5d3c7329 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -321,7 +321,7 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato if(img) { if(isShadow && generateShadow) - img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEER); + img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEAR); if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE) img = img->drawOutline(Colors::WHITE, getScalingFactor()); } diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 424dd1531..3a8a57f93 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -429,13 +429,13 @@ std::shared_ptr SDLImageShared::verticalFlip() const return ret; } -std::shared_ptr SDLImageShared::drawShadow(bool doSheer) const +std::shared_ptr SDLImageShared::drawShadow(bool doSheer) const { if(upscalingInProgress) throw std::runtime_error("Attempt to access images that is still being loaded!"); if (!surf) - return shared_from_this(); + return nullptr; SDL_Surface * shadow = CSDL_Ext::drawShadow(surf, doSheer); auto ret = std::make_shared(shadow); @@ -449,13 +449,13 @@ std::shared_ptr SDLImageShared::drawShadow(bool doSheer) con return ret; } -std::shared_ptr SDLImageShared::drawOutline(const ColorRGBA & color, int thickness) const +std::shared_ptr SDLImageShared::drawOutline(const ColorRGBA & color, int thickness) const { if(upscalingInProgress) throw std::runtime_error("Attempt to access images that is still being loaded!"); if (!surf) - return shared_from_this(); + return nullptr; SDL_Color sdlColor = { color.r, color.g, color.b, color.a }; SDL_Surface * outline = CSDL_Ext::drawOutline(surf, sdlColor, thickness); diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 3adf46733..32bf7c67e 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -71,8 +71,8 @@ public: [[nodiscard]] std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override; [[nodiscard]] std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const override; - std::shared_ptr drawShadow(bool doSheer) const; - std::shared_ptr drawOutline(const ColorRGBA & color, int thickness) const; + std::shared_ptr drawShadow(bool doSheer) const; + std::shared_ptr drawOutline(const ColorRGBA & color, int thickness) const; friend class SDLImageLoader; }; From 3c3eebb9d40f49f45299b6f14844b72dc7e0fba9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Jul 2025 23:44:31 +0200 Subject: [PATCH 03/14] docs --- docs/modders/Animation_Format.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 8b3c44082..0519eee35 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -45,7 +45,13 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def "frame" : 0, // Filename for this frame - "file" : "filename.png" + "file" : "filename.png", + + // Automatically create shadow for this frame if required. Optional, 0 = None, 1 = Normal Shadow, 2 = Sheared Shadow (e.g. for adventure map) + "generateShadow" : 1, + + // Automatically create overlay for this frame if required. Optional, 0 = None, 1 = Outline + "generateOverlay" : 1, }. ... ] From 2f00524439786d383bad3bc0f73be1c5ae44b167 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Jul 2025 00:45:49 +0200 Subject: [PATCH 04/14] dont cut overlays & shadows --- client/renderSDL/RenderHandler.cpp | 7 ++++--- client/renderSDL/SDLImage.cpp | 7 +++++-- client/renderSDL/SDLImage.h | 2 +- client/renderSDL/SDL_Extensions.cpp | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index a5d3c7329..95ccf4f82 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -295,6 +295,7 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato bool generateOverlay = locator.generateOverlay && (*locator.generateOverlay) != SharedImageLocator::OverlayMode::OVERLAY_NONE; bool isShadow = locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR; bool isOverlay = locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION; + bool optimizeImage = !(isShadow && generateShadow) && !(isOverlay && generateOverlay); // images needs to expanded if(isOverlay && !generateOverlay) imagePathString += "-OVERLAY"; @@ -312,11 +313,11 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato std::shared_ptr img = nullptr; if(CResourceHandler::get()->existsResource(imagePathSprites)) - img = std::make_shared(imagePathSprites); + img = std::make_shared(imagePathSprites, optimizeImage); else if(CResourceHandler::get()->existsResource(imagePathData)) - img = std::make_shared(imagePathData); + img = std::make_shared(imagePathData, optimizeImage); else if(CResourceHandler::get()->existsResource(imagePath)) - img = std::make_shared(imagePath); + img = std::make_shared(imagePath, optimizeImage); if(img) { diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 3a8a57f93..a9096b410 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -70,7 +70,7 @@ SDLImageShared::SDLImageShared(SDL_Surface * from) fullSize.y = surf->h; } -SDLImageShared::SDLImageShared(const ImagePath & filename) +SDLImageShared::SDLImageShared(const ImagePath & filename, bool optimizeImage) : surf(nullptr), margins(0, 0), fullSize(0, 0), @@ -89,7 +89,8 @@ SDLImageShared::SDLImageShared(const ImagePath & filename) fullSize.x = surf->w; fullSize.y = surf->h; - optimizeSurface(); + if(optimizeImage) + optimizeSurface(); } } @@ -442,6 +443,7 @@ std::shared_ptr SDLImageShared::drawShadow(bool doSheer) const ret->fullSize = fullSize; ret->margins.x = margins.x; ret->margins.y = margins.y; + ret->optimizeSurface(); // erase our own reference SDL_FreeSurface(shadow); @@ -463,6 +465,7 @@ std::shared_ptr SDLImageShared::drawOutline(const ColorRGBA & co ret->fullSize = fullSize; ret->margins.x = margins.x; ret->margins.y = margins.y; + ret->optimizeSurface(); // erase our own reference SDL_FreeSurface(outline); diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 32bf7c67e..56f99a8cf 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -46,7 +46,7 @@ public: //Load image from def file SDLImageShared(const CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file - SDLImageShared(const ImagePath & filename); + SDLImageShared(const ImagePath & filename, bool optimizeImage=true); //Create using existing surface, extraRef will increase refcount on SDL_Surface SDLImageShared(SDL_Surface * from); ~SDLImageShared(); diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index e7d73dd24..cdc403598 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -974,7 +974,7 @@ SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer) assert(destSurface->format->format == SDL_PIXELFORMAT_ARGB8888); double shearX = doSheer ? 0.5 : 0.0; - double scaleY = 0.25; + double scaleY = doSheer ? 0.5 : 0.25; int lowestSource = getLowestNonTransparentY(sourceSurface); int lowestTransformed = lowestSource * scaleY; From f0c24c0ffa0b229daa8727332cdb98bd0b3746f1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Jul 2025 13:23:46 +0200 Subject: [PATCH 05/14] fixing some sonar cloud issues --- client/render/ImageLocator.h | 4 +- client/renderSDL/SDL_Extensions.cpp | 95 ++++++++++++++++++----------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/client/render/ImageLocator.h b/client/render/ImageLocator.h index 7b3f2cc1b..f8d26d0eb 100644 --- a/client/render/ImageLocator.h +++ b/client/render/ImageLocator.h @@ -16,13 +16,13 @@ struct SharedImageLocator { - enum ShadowMode + enum class ShadowMode { SHADOW_NONE, SHADOW_NORMAL, SHADOW_SHEAR }; - enum OverlayMode + enum class OverlayMode { OVERLAY_NONE, OVERLAY_OUTLINE, diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index cdc403598..67bc11c59 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -692,6 +692,10 @@ SDL_Surface * CSDL_Ext::drawOutline(SDL_Surface * source, const SDL_Color & colo SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); SDL_Surface *destSurface = newSurface(Point(source->w, source->h)); + // Lock surfaces for direct pixel access + if (SDL_MUSTLOCK(sourceSurface)) SDL_LockSurface(sourceSurface); + if (SDL_MUSTLOCK(destSurface)) SDL_LockSurface(destSurface); + int width = sourceSurface->w; int height = sourceSurface->h; @@ -721,7 +725,10 @@ SDL_Surface * CSDL_Ext::drawOutline(SDL_Surface * source, const SDL_Color & colo { // Get the pixel at the neighbor position Uint32 pixel = *((Uint32*)sourceSurface->pixels + neighborY * width + neighborX); - Uint8 r, g, b, a; + Uint8 r; + Uint8 g; + Uint8 b; + Uint8 a; SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a); // Compare the pixel alpha value to find the maximum and maximum @@ -738,6 +745,9 @@ SDL_Surface * CSDL_Ext::drawOutline(SDL_Surface * source, const SDL_Color & colo } } + if (SDL_MUSTLOCK(sourceSurface)) SDL_UnlockSurface(sourceSurface); + if (SDL_MUSTLOCK(destSurface)) SDL_UnlockSurface(destSurface); + SDL_FreeSurface(sourceSurface); return destSurface; @@ -754,7 +764,7 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b // Calculate inverse matrix M_inv for mapping dst -> src double det = a * d - b * c; - if (det == 0) + if (static_cast(det) == 0) throw std::runtime_error("Singular transform matrix!"); double invDet = 1.0 / det; double ia = d * invDet; @@ -772,14 +782,14 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b double srcY = ic * (x - tx) + id * (y - ty); // Nearest neighbor sampling (can be improved to bilinear) - int srcXi = static_cast(round(srcX)); - int srcYi = static_cast(round(srcY)); + auto srcXi = static_cast(round(srcX)); + auto srcYi = static_cast(round(srcY)); // Check bounds if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) { - Uint32* srcPixels = (Uint32*)src->pixels; - Uint32* dstPixels = (Uint32*)dst->pixels; + auto srcPixels = (Uint32*)src->pixels; + auto dstPixels = (Uint32*)dst->pixels; Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; dstPixels[y * dst->w + x] = pixel; @@ -787,7 +797,7 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b else { // Outside source bounds: set transparent or black - Uint32* dstPixels = (Uint32*)dst->pixels; + auto dstPixels = (Uint32*)dst->pixels; dstPixels[y * dst->w + x] = 0x00000000; // transparent black } } @@ -801,13 +811,12 @@ int getLowestNonTransparentY(SDL_Surface* surface) { assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); - if(SDL_MUSTLOCK(surface)) - SDL_LockSurface(surface); + if(SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); int w = surface->w; int h = surface->h; int bpp = surface->format->BytesPerPixel; - Uint8* pixels = (Uint8*)surface->pixels; + auto pixels = (Uint8*)surface->pixels; for(int y = h - 1; y >= 0; --y) { @@ -817,7 +826,10 @@ int getLowestNonTransparentY(SDL_Surface* surface) { Uint32 pixel = *(Uint32*)(row + x * bpp); - Uint8 r, g, b, a; + Uint8 r; + Uint8 g; + Uint8 b; + Uint8 a; SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); if (a > 0) @@ -839,14 +851,16 @@ void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uin if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); - Uint32* pixels = (Uint32*)surface->pixels; + auto pixels = (Uint32*)surface->pixels; int pixelCount = surface->w * surface->h; for (int i = 0; i < pixelCount; i++) { Uint32 pixel = pixels[i]; - - Uint8 pr, pg, pb, pa; + Uint8 pr; + Uint8 pg; + Uint8 pb; + Uint8 pa; // Extract existing RGBA components using SDL_GetRGBA SDL_GetRGBA(pixel, surface->format, &pr, &pg, &pb, &pa); @@ -866,17 +880,13 @@ void gaussianBlur(SDL_Surface* surface, int amount) if (!surface || amount <= 0) return; - if (SDL_MUSTLOCK(surface)) - { - if (SDL_LockSurface(surface) != 0) - throw std::runtime_error("Failed to lock surface!"); - } + if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); int width = surface->w; int height = surface->h; int pixelCount = width * height; - Uint32* pixels = static_cast(surface->pixels); + auto pixels = static_cast(surface->pixels); std::vector srcR(pixelCount); std::vector srcG(pixelCount); @@ -894,7 +904,10 @@ void gaussianBlur(SDL_Surface* surface, int amount) for (int x = 0; x < width; ++x) { Uint32 pixel = pixels[y * width + x]; - Uint8 r, g, b, a; + Uint8 r; + Uint8 g; + Uint8 b; + Uint8 a; SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); int idx = y * width + x; @@ -906,20 +919,28 @@ void gaussianBlur(SDL_Surface* surface, int amount) } // 3x3 Gaussian kernel - float kernel[3][3] = { - {1.f/16, 2.f/16, 1.f/16}, - {2.f/16, 4.f/16, 2.f/16}, - {1.f/16, 2.f/16, 1.f/16} - }; + std::array, 3> kernel = {{ + {{1.f/16, 2.f/16, 1.f/16}}, + {{2.f/16, 4.f/16, 2.f/16}}, + {{1.f/16, 2.f/16, 1.f/16}} + }}; // Apply the blur 'amount' times for stronger blur - for (int iteration = 0; iteration < amount; ++iteration) { - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - float sumR = 0.f, sumG = 0.f, sumB = 0.f, sumA = 0.f; + for (int iteration = 0; iteration < amount; ++iteration) + { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + float sumR = 0.f; + float sumG = 0.f; + float sumB = 0.f; + float sumA = 0.f; - for (int ky = -1; ky <= 1; ++ky) { - for (int kx = -1; kx <= 1; ++kx) { + for (int ky = -1; ky <= 1; ++ky) + { + for (int kx = -1; kx <= 1; ++kx) + { int nx = x + kx; int ny = y + ky; @@ -954,16 +975,16 @@ void gaussianBlur(SDL_Surface* surface, int amount) } // After final iteration, write back to surface pixels - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { int idx = y * width + x; pixels[idx] = SDL_MapRGBA(surface->format, srcR[idx], srcG[idx], srcB[idx], srcA[idx]); } } - if (SDL_MUSTLOCK(surface)) { - SDL_UnlockSurface(surface); - } + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); } SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer) From e43df04ae1de22f08f261ed768ec6703814e68c6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:29:11 +0200 Subject: [PATCH 06/14] better outline matching H3 --- client/renderSDL/RenderHandler.cpp | 2 +- client/renderSDL/SDL_Extensions.cpp | 86 ++++++++++++++--------------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 95ccf4f82..56e79ca69 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -324,7 +324,7 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato if(isShadow && generateShadow) img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEAR); if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE) - img = img->drawOutline(Colors::WHITE, getScalingFactor()); + img = img->drawOutline(Colors::WHITE, 1); } return img; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 67bc11c59..a8cd1869d 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -686,70 +686,68 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) other = CSDL_Ext::fromSDL(rect); } -SDL_Surface * CSDL_Ext::drawOutline(SDL_Surface * source, const SDL_Color & color, int thickness) +SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* source, const SDL_Color& color, int thickness) { - // ensure format - SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); - SDL_Surface *destSurface = newSurface(Point(source->w, source->h)); + if(thickness < 1) + return nullptr; - // Lock surfaces for direct pixel access - if (SDL_MUSTLOCK(sourceSurface)) SDL_LockSurface(sourceSurface); - if (SDL_MUSTLOCK(destSurface)) SDL_LockSurface(destSurface); + SDL_Surface* sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); + SDL_Surface* destSurface = newSurface(Point(source->w, source->h)); + + if(SDL_MUSTLOCK(sourceSurface)) SDL_LockSurface(sourceSurface); + if(SDL_MUSTLOCK(destSurface)) SDL_LockSurface(destSurface); int width = sourceSurface->w; int height = sourceSurface->h; - // Iterate through the pixels of the image - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - Uint8 maxPixel = 0; - Uint8 minPixel = 255; - int halfThickness = (thickness + 1) / 2; + // Helper lambda to get alpha + auto getAlpha = [&](int x, int y) -> Uint8 { + if (x < 0 || x >= width || y < 0 || y >= height) + return 0; + Uint32 pixel = *((Uint32*)sourceSurface->pixels + y * width + x); + Uint8 r, g, b, a; + SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a); + return a; + }; - // Loop over the neighborhood around (x, y) - for(int offsetY = -halfThickness; offsetY <= halfThickness; offsetY++) + // Go through all transparent pixels and check if they border an opaque region + for(int y = 0; y < height; y++) + { + for(int x = 0; x < width; x++) + { + if(getAlpha(x, y) != 0) + continue; // Skip opaque pixels + + bool isOutline = false; + + for(int dy = -thickness; dy <= thickness && !isOutline; ++dy) { - for(int offsetX = -halfThickness; offsetX <= halfThickness; offsetX++) + for(int dx = -thickness; dx <= thickness; ++dx) { - // Circle instead of rectangle - if(offsetX * offsetX + offsetY * offsetY > halfThickness * halfThickness) + // circular neighborhood + if(dx * dx + dy * dy > thickness * thickness) continue; - int neighborX = x + offsetX; - int neighborY = y + offsetY; - - // Check image bounds - if(neighborX >= 0 && neighborX < destSurface->w && neighborY >= 0 && neighborY < destSurface->h) + if(getAlpha(x + dx, y + dy) > 0) { - // Get the pixel at the neighbor position - Uint32 pixel = *((Uint32*)sourceSurface->pixels + neighborY * width + neighborX); - Uint8 r; - Uint8 g; - Uint8 b; - Uint8 a; - SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a); - - // Compare the pixel alpha value to find the maximum and maximum - if(a > maxPixel) - maxPixel = a; - if(a < minPixel) - minPixel = a; + isOutline = true; + break; // break inner loop only } } } - Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, maxPixel - minPixel); - *((Uint32*)destSurface->pixels + y * width + x) = newPixel; + if(isOutline) + { + Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, color.a); + *((Uint32*)destSurface->pixels + y * width + x) = newPixel; + } } } - if (SDL_MUSTLOCK(sourceSurface)) SDL_UnlockSurface(sourceSurface); - if (SDL_MUSTLOCK(destSurface)) SDL_UnlockSurface(destSurface); + if(SDL_MUSTLOCK(sourceSurface)) SDL_UnlockSurface(sourceSurface); + if(SDL_MUSTLOCK(destSurface)) SDL_UnlockSurface(destSurface); SDL_FreeSurface(sourceSurface); - return destSurface; } @@ -764,7 +762,7 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b // Calculate inverse matrix M_inv for mapping dst -> src double det = a * d - b * c; - if (static_cast(det) == 0) + if (std::abs(det) < 1e-10) throw std::runtime_error("Singular transform matrix!"); double invDet = 1.0 / det; double ia = d * invDet; From 7677791217018fd90fb50bf83d675ab6032a1e18 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:30:58 +0200 Subject: [PATCH 07/14] sonarcloud --- client/renderSDL/SDL_Extensions.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index a8cd1869d..106de29a4 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -705,7 +705,10 @@ SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* source, const SDL_Color& color, if (x < 0 || x >= width || y < 0 || y >= height) return 0; Uint32 pixel = *((Uint32*)sourceSurface->pixels + y * width + x); - Uint8 r, g, b, a; + Uint8 r; + Uint8 g; + Uint8 b; + Uint8 a; SDL_GetRGBA(pixel, sourceSurface->format, &r, &g, &b, &a); return a; }; From 448a97995cdc71e61ebd974e073c55686f39786d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Jul 2025 12:57:01 +0200 Subject: [PATCH 08/14] use tbb --- client/renderSDL/SDL_Extensions.cpp | 234 +++++++++++++++------------- 1 file changed, 126 insertions(+), 108 deletions(-) diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 106de29a4..ae69904fe 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -713,39 +713,42 @@ SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* source, const SDL_Color& color, return a; }; - // Go through all transparent pixels and check if they border an opaque region - for(int y = 0; y < height; y++) + tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) { - for(int x = 0; x < width; x++) + // Go through all transparent pixels and check if they border an opaque region + for(int y = r.begin(); y != r.end(); ++y) { - if(getAlpha(x, y) != 0) - continue; // Skip opaque pixels - - bool isOutline = false; - - for(int dy = -thickness; dy <= thickness && !isOutline; ++dy) + for(int x = 0; x < width; x++) { - for(int dx = -thickness; dx <= thickness; ++dx) - { - // circular neighborhood - if(dx * dx + dy * dy > thickness * thickness) - continue; + if(getAlpha(x, y) != 0) + continue; // Skip opaque pixels - if(getAlpha(x + dx, y + dy) > 0) + bool isOutline = false; + + for(int dy = -thickness; dy <= thickness && !isOutline; ++dy) + { + for(int dx = -thickness; dx <= thickness; ++dx) { - isOutline = true; - break; // break inner loop only + // circular neighborhood + if(dx * dx + dy * dy > thickness * thickness) + continue; + + if(getAlpha(x + dx, y + dy) > 0) + { + isOutline = true; + break; // break inner loop only + } } } - } - if(isOutline) - { - Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, color.a); - *((Uint32*)destSurface->pixels + y * width + x) = newPixel; + if(isOutline) + { + Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, color.a); + *((Uint32*)destSurface->pixels + y * width + x) = newPixel; + } } } - } + }); if(SDL_MUSTLOCK(sourceSurface)) SDL_UnlockSurface(sourceSurface); if(SDL_MUSTLOCK(destSurface)) SDL_UnlockSurface(destSurface); @@ -773,36 +776,39 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b double ic = -c * invDet; double id = a * invDet; - // For each pixel in the destination image - for(int y = 0; y < dst->h; y++) + tbb::parallel_for(tbb::blocked_range(0, dst->h), [&](const tbb::blocked_range& r) { - for(int x = 0; x < dst->w; x++) + // For each pixel in the destination image + for(int y = r.begin(); y != r.end(); ++y) { - // Map destination pixel (x,y) back to source coordinates (srcX, srcY) - double srcX = ia * (x - tx) + ib * (y - ty); - double srcY = ic * (x - tx) + id * (y - ty); - - // Nearest neighbor sampling (can be improved to bilinear) - auto srcXi = static_cast(round(srcX)); - auto srcYi = static_cast(round(srcY)); - - // Check bounds - if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) + for(int x = 0; x < dst->w; x++) { - auto srcPixels = (Uint32*)src->pixels; - auto dstPixels = (Uint32*)dst->pixels; + // Map destination pixel (x,y) back to source coordinates (srcX, srcY) + double srcX = ia * (x - tx) + ib * (y - ty); + double srcY = ic * (x - tx) + id * (y - ty); - Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; - dstPixels[y * dst->w + x] = pixel; - } - else - { - // Outside source bounds: set transparent or black - auto dstPixels = (Uint32*)dst->pixels; - dstPixels[y * dst->w + x] = 0x00000000; // transparent black + // Nearest neighbor sampling (can be improved to bilinear) + auto srcXi = static_cast(round(srcX)); + auto srcYi = static_cast(round(srcY)); + + // Check bounds + if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) + { + auto srcPixels = (Uint32*)src->pixels; + auto dstPixels = (Uint32*)dst->pixels; + + Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; + dstPixels[y * dst->w + x] = pixel; + } + else + { + // Outside source bounds: set transparent or black + auto dstPixels = (Uint32*)dst->pixels; + dstPixels[y * dst->w + x] = 0x00000000; // transparent black + } } } - } + }); if (SDL_MUSTLOCK(src)) SDL_UnlockSurface(src); if (SDL_MUSTLOCK(dst)) SDL_UnlockSurface(dst); @@ -855,22 +861,25 @@ void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uin auto pixels = (Uint32*)surface->pixels; int pixelCount = surface->w * surface->h; - for (int i = 0; i < pixelCount; i++) + tbb::parallel_for(tbb::blocked_range(0, pixelCount), [&](const tbb::blocked_range& range) { - Uint32 pixel = pixels[i]; - Uint8 pr; - Uint8 pg; - Uint8 pb; - Uint8 pa; - // Extract existing RGBA components using SDL_GetRGBA - SDL_GetRGBA(pixel, surface->format, &pr, &pg, &pb, &pa); + for(int i = range.begin(); i != range.end(); ++i) + { + Uint32 pixel = pixels[i]; + Uint8 pr; + Uint8 pg; + Uint8 pb; + Uint8 pa; + // Extract existing RGBA components using SDL_GetRGBA + SDL_GetRGBA(pixel, surface->format, &pr, &pg, &pb, &pa); - Uint32 newPixel = SDL_MapRGBA(surface->format, r, g, b, a); - if(pa == 0) - newPixel = SDL_MapRGBA(surface->format, 0, 0, 0, 0); + Uint32 newPixel = SDL_MapRGBA(surface->format, r, g, b, a); + if(pa == 0) + newPixel = SDL_MapRGBA(surface->format, 0, 0, 0, 0); - pixels[i] = newPixel; - } + pixels[i] = newPixel; + } + }); if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); } @@ -900,24 +909,27 @@ void gaussianBlur(SDL_Surface* surface, int amount) std::vector dstA(pixelCount); // Initialize src channels from surface pixels - for (int y = 0; y < height; ++y) + tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) { - for (int x = 0; x < width; ++x) + for(int y = r.begin(); y != r.end(); ++y) { - Uint32 pixel = pixels[y * width + x]; - Uint8 r; - Uint8 g; - Uint8 b; - Uint8 a; - SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); + for (int x = 0; x < width; ++x) + { + Uint32 pixel = pixels[y * width + x]; + Uint8 r; + Uint8 g; + Uint8 b; + Uint8 a; + SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); - int idx = y * width + x; - srcR[idx] = r; - srcG[idx] = g; - srcB[idx] = b; - srcA[idx] = a; + int idx = y * width + x; + srcR[idx] = r; + srcG[idx] = g; + srcB[idx] = b; + srcA[idx] = a; + } } - } + }); // 3x3 Gaussian kernel std::array, 3> kernel = {{ @@ -929,45 +941,48 @@ void gaussianBlur(SDL_Surface* surface, int amount) // Apply the blur 'amount' times for stronger blur for (int iteration = 0; iteration < amount; ++iteration) { - for (int y = 0; y < height; ++y) + tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) { - for (int x = 0; x < width; ++x) + for(int y = r.begin(); y != r.end(); ++y) { - float sumR = 0.f; - float sumG = 0.f; - float sumB = 0.f; - float sumA = 0.f; - - for (int ky = -1; ky <= 1; ++ky) + for (int x = 0; x < width; ++x) { - for (int kx = -1; kx <= 1; ++kx) + float sumR = 0.f; + float sumG = 0.f; + float sumB = 0.f; + float sumA = 0.f; + + for (int ky = -1; ky <= 1; ++ky) { - int nx = x + kx; - int ny = y + ky; + for (int kx = -1; kx <= 1; ++kx) + { + int nx = x + kx; + int ny = y + ky; - // Clamp edges - if (nx < 0) nx = 0; - else if (nx >= width) nx = width - 1; - if (ny < 0) ny = 0; - else if (ny >= height) ny = height - 1; + // Clamp edges + if (nx < 0) nx = 0; + else if (nx >= width) nx = width - 1; + if (ny < 0) ny = 0; + else if (ny >= height) ny = height - 1; - int nIdx = ny * width + nx; - float kval = kernel[ky + 1][kx + 1]; + int nIdx = ny * width + nx; + float kval = kernel[ky + 1][kx + 1]; - sumR += srcR[nIdx] * kval; - sumG += srcG[nIdx] * kval; - sumB += srcB[nIdx] * kval; - sumA += srcA[nIdx] * kval; + sumR += srcR[nIdx] * kval; + sumG += srcG[nIdx] * kval; + sumB += srcB[nIdx] * kval; + sumA += srcA[nIdx] * kval; + } } - } - int idx = y * width + x; - dstR[idx] = static_cast(sumR); - dstG[idx] = static_cast(sumG); - dstB[idx] = static_cast(sumB); - dstA[idx] = static_cast(sumA); + int idx = y * width + x; + dstR[idx] = static_cast(sumR); + dstG[idx] = static_cast(sumG); + dstB[idx] = static_cast(sumB); + dstA[idx] = static_cast(sumA); + } } - } + }); // Swap src and dst for next iteration (blur chaining) srcR.swap(dstR); srcG.swap(dstG); @@ -976,14 +991,17 @@ void gaussianBlur(SDL_Surface* surface, int amount) } // After final iteration, write back to surface pixels - for (int y = 0; y < height; ++y) + tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) { - for (int x = 0; x < width; ++x) + for(int y = r.begin(); y != r.end(); ++y) { - int idx = y * width + x; - pixels[idx] = SDL_MapRGBA(surface->format, srcR[idx], srcG[idx], srcB[idx], srcA[idx]); + for (int x = 0; x < width; ++x) + { + int idx = y * width + x; + pixels[idx] = SDL_MapRGBA(surface->format, srcR[idx], srcG[idx], srcB[idx], srcA[idx]); + } } - } + }); if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); } From 1cada2ba1eba28519a3cfdbc5a15a3839c7eb399 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:43:06 +0200 Subject: [PATCH 09/14] using box blur & parrallelism to speed up --- client/renderSDL/SDL_Extensions.cpp | 188 +++++++++------------------- 1 file changed, 61 insertions(+), 127 deletions(-) diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index ae69904fe..e7795f4f5 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -23,6 +23,7 @@ #include "../../lib/GameConstants.h" #include +#include #include #include @@ -816,40 +817,44 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b int getLowestNonTransparentY(SDL_Surface* surface) { - assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); + assert(surface && surface->format->format == SDL_PIXELFORMAT_ARGB8888); + if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); - if(SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); + const int w = surface->w; + const int h = surface->h; + const int bpp = surface->format->BytesPerPixel; + auto pixels = static_cast(surface->pixels); - int w = surface->w; - int h = surface->h; - int bpp = surface->format->BytesPerPixel; - auto pixels = (Uint8*)surface->pixels; - - for(int y = h - 1; y >= 0; --y) - { - Uint8* row = pixels + y * surface->pitch; - - for(int x = 0; x < w; ++x) + // Use parallel_reduce to find the max y with non-transparent pixel + int lowestY = tbb::parallel_reduce( + tbb::blocked_range(0, h), + -1, // initial lowestY = -1 (fully transparent) + [&](const tbb::blocked_range& r, int localMaxY) -> int { - Uint32 pixel = *(Uint32*)(row + x * bpp); - - Uint8 r; - Uint8 g; - Uint8 b; - Uint8 a; - SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); - - if (a > 0) + for (int y = r.begin(); y != r.end(); ++y) { - if(SDL_MUSTLOCK(surface)) - SDL_UnlockSurface(surface); - return y; + Uint8* row = pixels + y * surface->pitch; + for (int x = 0; x < w; ++x) + { + Uint32 pixel = *(Uint32*)(row + x * bpp); + Uint8 a = (pixel >> 24) & 0xFF; // Fast path for ARGB8888 + if (a > 0) + { + localMaxY = std::max(localMaxY, y); + break; // no need to scan rest of row + } + } } + return localMaxY; + }, + [](int a, int b) -> int + { + return std::max(a, b); } - } + ); if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); - return -1; // fully transparent + return lowestY; } void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uint8 a) @@ -884,125 +889,54 @@ void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uin if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); } -void gaussianBlur(SDL_Surface* surface, int amount) +void boxBlur(SDL_Surface* surface) { - assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); - - if (!surface || amount <= 0) return; - + assert(surface && surface->format->format == SDL_PIXELFORMAT_ARGB8888); if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); int width = surface->w; int height = surface->h; int pixelCount = width * height; - auto pixels = static_cast(surface->pixels); + Uint32* pixels = static_cast(surface->pixels); + std::vector temp(pixelCount); - std::vector srcR(pixelCount); - std::vector srcG(pixelCount); - std::vector srcB(pixelCount); - std::vector srcA(pixelCount); - - std::vector dstR(pixelCount); - std::vector dstG(pixelCount); - std::vector dstB(pixelCount); - std::vector dstA(pixelCount); - - // Initialize src channels from surface pixels - tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) + tbb::parallel_for(0, height, [&](int y) { - for(int y = r.begin(); y != r.end(); ++y) + for (int x = 0; x < width; ++x) { - for (int x = 0; x < width; ++x) + int sumR = 0; + int sumG = 0; + int sumB = 0; + int sumA = 0; + int count = 0; + + for (int ky = -1; ky <= 1; ++ky) { - Uint32 pixel = pixels[y * width + x]; - Uint8 r; - Uint8 g; - Uint8 b; - Uint8 a; - SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); - - int idx = y * width + x; - srcR[idx] = r; - srcG[idx] = g; - srcB[idx] = b; - srcA[idx] = a; - } - } - }); - - // 3x3 Gaussian kernel - std::array, 3> kernel = {{ - {{1.f/16, 2.f/16, 1.f/16}}, - {{2.f/16, 4.f/16, 2.f/16}}, - {{1.f/16, 2.f/16, 1.f/16}} - }}; - - // Apply the blur 'amount' times for stronger blur - for (int iteration = 0; iteration < amount; ++iteration) - { - tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) - { - for(int y = r.begin(); y != r.end(); ++y) - { - for (int x = 0; x < width; ++x) + int ny = std::clamp(y + ky, 0, height - 1); + for (int kx = -1; kx <= 1; ++kx) { - float sumR = 0.f; - float sumG = 0.f; - float sumB = 0.f; - float sumA = 0.f; + int nx = std::clamp(x + kx, 0, width - 1); + Uint32 pixel = pixels[ny * width + nx]; - for (int ky = -1; ky <= 1; ++ky) - { - for (int kx = -1; kx <= 1; ++kx) - { - int nx = x + kx; - int ny = y + ky; - - // Clamp edges - if (nx < 0) nx = 0; - else if (nx >= width) nx = width - 1; - if (ny < 0) ny = 0; - else if (ny >= height) ny = height - 1; - - int nIdx = ny * width + nx; - float kval = kernel[ky + 1][kx + 1]; - - sumR += srcR[nIdx] * kval; - sumG += srcG[nIdx] * kval; - sumB += srcB[nIdx] * kval; - sumA += srcA[nIdx] * kval; - } - } - - int idx = y * width + x; - dstR[idx] = static_cast(sumR); - dstG[idx] = static_cast(sumG); - dstB[idx] = static_cast(sumB); - dstA[idx] = static_cast(sumA); + sumA += (pixel >> 24) & 0xFF; + sumR += (pixel >> 16) & 0xFF; + sumG += (pixel >> 8) & 0xFF; + sumB += (pixel >> 0) & 0xFF; + ++count; } } - }); - // Swap src and dst for next iteration (blur chaining) - srcR.swap(dstR); - srcG.swap(dstG); - srcB.swap(dstB); - srcA.swap(dstA); - } - // After final iteration, write back to surface pixels - tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) - { - for(int y = r.begin(); y != r.end(); ++y) - { - for (int x = 0; x < width; ++x) - { - int idx = y * width + x; - pixels[idx] = SDL_MapRGBA(surface->format, srcR[idx], srcG[idx], srcB[idx], srcA[idx]); - } + Uint8 a = sumA / count; + Uint8 r = sumR / count; + Uint8 g = sumG / count; + Uint8 b = sumB / count; + temp[y * width + x] = (a << 24) | (r << 16) | (g << 8) | b; } }); + std::copy(temp.begin(), temp.end(), pixels); + if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); } @@ -1029,7 +963,7 @@ SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer) applyAffineTransform(sourceSurface, destSurface, a, b, c, d, tx, ty); fillAlphaPixelWithRGBA(destSurface, 0, 0, 0, 128); - gaussianBlur(destSurface, 1); + boxBlur(destSurface); SDL_FreeSurface(sourceSurface); From be56d6eff451ad3327ffce4787dd3bf4a644a7fd Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Jul 2025 15:13:33 +0200 Subject: [PATCH 10/14] optimize running the algorithms on smaller image --- client/renderSDL/RenderHandler.cpp | 8 ++++---- client/renderSDL/SDLImage.cpp | 9 ++++----- client/renderSDL/SDLImage.h | 4 ++-- client/renderSDL/SDLImageScaler.cpp | 7 ++++++- client/renderSDL/SDLImageScaler.h | 2 +- client/renderSDL/SDL_Extensions.cpp | 9 +++------ 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 56e79ca69..d526304c8 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -295,7 +295,7 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato bool generateOverlay = locator.generateOverlay && (*locator.generateOverlay) != SharedImageLocator::OverlayMode::OVERLAY_NONE; bool isShadow = locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR; bool isOverlay = locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION; - bool optimizeImage = !(isShadow && generateShadow) && !(isOverlay && generateOverlay); // images needs to expanded + int requiredBorderAfterOptimize = (isShadow && generateShadow) || (isOverlay && generateOverlay) ? 50 * locator.scalingFactor : 0; if(isOverlay && !generateOverlay) imagePathString += "-OVERLAY"; @@ -313,11 +313,11 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato std::shared_ptr img = nullptr; if(CResourceHandler::get()->existsResource(imagePathSprites)) - img = std::make_shared(imagePathSprites, optimizeImage); + img = std::make_shared(imagePathSprites, requiredBorderAfterOptimize); else if(CResourceHandler::get()->existsResource(imagePathData)) - img = std::make_shared(imagePathData, optimizeImage); + img = std::make_shared(imagePathData, requiredBorderAfterOptimize); else if(CResourceHandler::get()->existsResource(imagePath)) - img = std::make_shared(imagePath, optimizeImage); + img = std::make_shared(imagePath, requiredBorderAfterOptimize); if(img) { diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index a9096b410..0292a7713 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -70,7 +70,7 @@ SDLImageShared::SDLImageShared(SDL_Surface * from) fullSize.y = surf->h; } -SDLImageShared::SDLImageShared(const ImagePath & filename, bool optimizeImage) +SDLImageShared::SDLImageShared(const ImagePath & filename, int keepBorder) : surf(nullptr), margins(0, 0), fullSize(0, 0), @@ -89,8 +89,7 @@ SDLImageShared::SDLImageShared(const ImagePath & filename, bool optimizeImage) fullSize.x = surf->w; fullSize.y = surf->h; - if(optimizeImage) - optimizeSurface(); + optimizeSurface(keepBorder); } } @@ -210,7 +209,7 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin SDL_SetSurfacePalette(surf, originalPalette); } -void SDLImageShared::optimizeSurface() +void SDLImageShared::optimizeSurface(int keepBorder) { assert(upscalingInProgress == false); if (!surf) @@ -218,7 +217,7 @@ void SDLImageShared::optimizeSurface() SDLImageOptimizer optimizer(surf, Rect(margins, fullSize)); - optimizer.optimizeSurface(surf); + optimizer.optimizeSurface(surf, keepBorder); SDL_FreeSurface(surf); surf = optimizer.acquireResultSurface(); diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 56f99a8cf..7129b9ecf 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -40,13 +40,13 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from // Keep the original palette, in order to do color switching operation void savePalette(); - void optimizeSurface(); + void optimizeSurface(int keepBorder = 0); public: //Load image from def file SDLImageShared(const CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file - SDLImageShared(const ImagePath & filename, bool optimizeImage=true); + SDLImageShared(const ImagePath & filename, int keepBorder = 0); //Create using existing surface, extraRef will increase refcount on SDL_Surface SDLImageShared(SDL_Surface * from); ~SDLImageShared(); diff --git a/client/renderSDL/SDLImageScaler.cpp b/client/renderSDL/SDLImageScaler.cpp index c52c92004..aab8376a6 100644 --- a/client/renderSDL/SDLImageScaler.cpp +++ b/client/renderSDL/SDLImageScaler.cpp @@ -24,7 +24,7 @@ SDLImageOptimizer::SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDim { } -void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface) +void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface, int keepBorder) { if (!surf) return; @@ -79,6 +79,11 @@ void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface) if (left == surf->w) return; + left = std::max(0, left - keepBorder); + top = std::max(0, top - keepBorder); + right = std::min(surf->w - 1, right + keepBorder); + bottom = std::min(surf->h - 1, bottom + keepBorder); + if (left != 0 || top != 0 || right != surf->w - 1 || bottom != surf->h - 1) { // non-zero border found diff --git a/client/renderSDL/SDLImageScaler.h b/client/renderSDL/SDLImageScaler.h index 796203029..7c1dea52f 100644 --- a/client/renderSDL/SDLImageScaler.h +++ b/client/renderSDL/SDLImageScaler.h @@ -21,7 +21,7 @@ class SDLImageOptimizer : boost::noncopyable public: SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDimensions); - void optimizeSurface(SDL_Surface * formatSourceSurface); + void optimizeSurface(SDL_Surface * formatSourceSurface, int keepBorder = 0); /// Aquires resulting surface and transfers surface ownership to the caller /// May return nullptr if input image was empty diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index e7795f4f5..85944c2dd 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -777,6 +777,9 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b double ic = -c * invDet; double id = a * invDet; + auto srcPixels = (Uint32*)src->pixels; + auto dstPixels = (Uint32*)dst->pixels; + tbb::parallel_for(tbb::blocked_range(0, dst->h), [&](const tbb::blocked_range& r) { // For each pixel in the destination image @@ -795,18 +798,12 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b // Check bounds if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) { - auto srcPixels = (Uint32*)src->pixels; - auto dstPixels = (Uint32*)dst->pixels; Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; dstPixels[y * dst->w + x] = pixel; } else - { - // Outside source bounds: set transparent or black - auto dstPixels = (Uint32*)dst->pixels; dstPixels[y * dst->w + x] = 0x00000000; // transparent black - } } } }); From 2607767ab7da35258f2e0da0632ab2bdc612930c Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Jul 2025 01:40:56 +0200 Subject: [PATCH 11/14] revert algoritm on optimized --- client/renderSDL/RenderHandler.cpp | 8 ++++---- client/renderSDL/SDLImage.cpp | 9 +++++---- client/renderSDL/SDLImage.h | 4 ++-- client/renderSDL/SDLImageScaler.cpp | 7 +------ client/renderSDL/SDLImageScaler.h | 2 +- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index d526304c8..56e79ca69 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -295,7 +295,7 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato bool generateOverlay = locator.generateOverlay && (*locator.generateOverlay) != SharedImageLocator::OverlayMode::OVERLAY_NONE; bool isShadow = locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR; bool isOverlay = locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION; - int requiredBorderAfterOptimize = (isShadow && generateShadow) || (isOverlay && generateOverlay) ? 50 * locator.scalingFactor : 0; + bool optimizeImage = !(isShadow && generateShadow) && !(isOverlay && generateOverlay); // images needs to expanded if(isOverlay && !generateOverlay) imagePathString += "-OVERLAY"; @@ -313,11 +313,11 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato std::shared_ptr img = nullptr; if(CResourceHandler::get()->existsResource(imagePathSprites)) - img = std::make_shared(imagePathSprites, requiredBorderAfterOptimize); + img = std::make_shared(imagePathSprites, optimizeImage); else if(CResourceHandler::get()->existsResource(imagePathData)) - img = std::make_shared(imagePathData, requiredBorderAfterOptimize); + img = std::make_shared(imagePathData, optimizeImage); else if(CResourceHandler::get()->existsResource(imagePath)) - img = std::make_shared(imagePath, requiredBorderAfterOptimize); + img = std::make_shared(imagePath, optimizeImage); if(img) { diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 0292a7713..a9096b410 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -70,7 +70,7 @@ SDLImageShared::SDLImageShared(SDL_Surface * from) fullSize.y = surf->h; } -SDLImageShared::SDLImageShared(const ImagePath & filename, int keepBorder) +SDLImageShared::SDLImageShared(const ImagePath & filename, bool optimizeImage) : surf(nullptr), margins(0, 0), fullSize(0, 0), @@ -89,7 +89,8 @@ SDLImageShared::SDLImageShared(const ImagePath & filename, int keepBorder) fullSize.x = surf->w; fullSize.y = surf->h; - optimizeSurface(keepBorder); + if(optimizeImage) + optimizeSurface(); } } @@ -209,7 +210,7 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin SDL_SetSurfacePalette(surf, originalPalette); } -void SDLImageShared::optimizeSurface(int keepBorder) +void SDLImageShared::optimizeSurface() { assert(upscalingInProgress == false); if (!surf) @@ -217,7 +218,7 @@ void SDLImageShared::optimizeSurface(int keepBorder) SDLImageOptimizer optimizer(surf, Rect(margins, fullSize)); - optimizer.optimizeSurface(surf, keepBorder); + optimizer.optimizeSurface(surf); SDL_FreeSurface(surf); surf = optimizer.acquireResultSurface(); diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 7129b9ecf..56f99a8cf 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -40,13 +40,13 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from // Keep the original palette, in order to do color switching operation void savePalette(); - void optimizeSurface(int keepBorder = 0); + void optimizeSurface(); public: //Load image from def file SDLImageShared(const CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file - SDLImageShared(const ImagePath & filename, int keepBorder = 0); + SDLImageShared(const ImagePath & filename, bool optimizeImage=true); //Create using existing surface, extraRef will increase refcount on SDL_Surface SDLImageShared(SDL_Surface * from); ~SDLImageShared(); diff --git a/client/renderSDL/SDLImageScaler.cpp b/client/renderSDL/SDLImageScaler.cpp index aab8376a6..c52c92004 100644 --- a/client/renderSDL/SDLImageScaler.cpp +++ b/client/renderSDL/SDLImageScaler.cpp @@ -24,7 +24,7 @@ SDLImageOptimizer::SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDim { } -void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface, int keepBorder) +void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface) { if (!surf) return; @@ -79,11 +79,6 @@ void SDLImageOptimizer::optimizeSurface(SDL_Surface * formatSourceSurface, int k if (left == surf->w) return; - left = std::max(0, left - keepBorder); - top = std::max(0, top - keepBorder); - right = std::min(surf->w - 1, right + keepBorder); - bottom = std::min(surf->h - 1, bottom + keepBorder); - if (left != 0 || top != 0 || right != surf->w - 1 || bottom != surf->h - 1) { // non-zero border found diff --git a/client/renderSDL/SDLImageScaler.h b/client/renderSDL/SDLImageScaler.h index 7c1dea52f..796203029 100644 --- a/client/renderSDL/SDLImageScaler.h +++ b/client/renderSDL/SDLImageScaler.h @@ -21,7 +21,7 @@ class SDLImageOptimizer : boost::noncopyable public: SDLImageOptimizer(SDL_Surface * surf, const Rect & virtualDimensions); - void optimizeSurface(SDL_Surface * formatSourceSurface, int keepBorder = 0); + void optimizeSurface(SDL_Surface * formatSourceSurface); /// Aquires resulting surface and transfers surface ownership to the caller /// May return nullptr if input image was empty From e799db4546aed7e171905733cdca46c2c33d1353 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:47:14 +0200 Subject: [PATCH 12/14] code review --- client/renderSDL/RenderHandler.cpp | 2 + client/renderSDL/SDL_Extensions.cpp | 61 ++++++++++--------------- lib/texts/TextLocalizationContainer.cpp | 2 +- 3 files changed, 28 insertions(+), 37 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 59e149082..42c221472 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -321,6 +321,8 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato if(img) { + // TODO: Performance improvement - Run algorithm on optimized ("trimmed") images + // Not implemented yet because different frame image sizes seems to cause wobbeling shadow -> needs a way around this if(isShadow && generateShadow) img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEAR); if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE) diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 85944c2dd..eebb3db62 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -687,13 +687,12 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) other = CSDL_Ext::fromSDL(rect); } -SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* source, const SDL_Color& color, int thickness) +SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* sourceSurface, const SDL_Color& color, int thickness) { if(thickness < 1) return nullptr; - SDL_Surface* sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); - SDL_Surface* destSurface = newSurface(Point(source->w, source->h)); + SDL_Surface* destSurface = newSurface(Point(sourceSurface->w, sourceSurface->h)); if(SDL_MUSTLOCK(sourceSurface)) SDL_LockSurface(sourceSurface); if(SDL_MUSTLOCK(destSurface)) SDL_LockSurface(destSurface); @@ -716,35 +715,38 @@ SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* source, const SDL_Color& color, tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range& r) { - // Go through all transparent pixels and check if they border an opaque region - for(int y = r.begin(); y != r.end(); ++y) + for (int y = r.begin(); y != r.end(); ++y) { - for(int x = 0; x < width; x++) + for (int x = 0; x < width; x++) { - if(getAlpha(x, y) != 0) - continue; // Skip opaque pixels + Uint8 alpha = getAlpha(x, y); + if (alpha != 0) + continue; // Skip opaque or semi-transparent pixels - bool isOutline = false; + Uint8 maxNearbyAlpha = 0; - for(int dy = -thickness; dy <= thickness && !isOutline; ++dy) + for (int dy = -thickness; dy <= thickness; ++dy) { - for(int dx = -thickness; dx <= thickness; ++dx) + for (int dx = -thickness; dx <= thickness; ++dx) { - // circular neighborhood - if(dx * dx + dy * dy > thickness * thickness) + if (dx * dx + dy * dy > thickness * thickness) + continue; // circular area + + int nx = x + dx; + int ny = y + dy; + if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue; - if(getAlpha(x + dx, y + dy) > 0) - { - isOutline = true; - break; // break inner loop only - } + Uint8 neighborAlpha = getAlpha(nx, ny); + if (neighborAlpha > maxNearbyAlpha) + maxNearbyAlpha = neighborAlpha; } } - if(isOutline) + if (maxNearbyAlpha > 0) { - Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, color.a); + Uint8 finalAlpha = maxNearbyAlpha - alpha; // alpha is 0 here, so effectively maxNearbyAlpha + Uint32 newPixel = SDL_MapRGBA(destSurface->format, color.r, color.g, color.b, finalAlpha); *((Uint32*)destSurface->pixels + y * width + x) = newPixel; } } @@ -754,15 +756,11 @@ SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* source, const SDL_Color& color, if(SDL_MUSTLOCK(sourceSurface)) SDL_UnlockSurface(sourceSurface); if(SDL_MUSTLOCK(destSurface)) SDL_UnlockSurface(destSurface); - SDL_FreeSurface(sourceSurface); return destSurface; } void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b, double c, double d, double tx, double ty) { - assert(src->format->format == SDL_PIXELFORMAT_ARGB8888); - assert(dst->format->format == SDL_PIXELFORMAT_ARGB8888); - // Lock surfaces for direct pixel access if (SDL_MUSTLOCK(src)) SDL_LockSurface(src); if (SDL_MUSTLOCK(dst)) SDL_LockSurface(dst); @@ -814,7 +812,6 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b int getLowestNonTransparentY(SDL_Surface* surface) { - assert(surface && surface->format->format == SDL_PIXELFORMAT_ARGB8888); if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); const int w = surface->w; @@ -856,8 +853,6 @@ int getLowestNonTransparentY(SDL_Surface* surface) void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { - assert(surface->format->format == SDL_PIXELFORMAT_ARGB8888); - if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); auto pixels = (Uint32*)surface->pixels; @@ -876,7 +871,7 @@ void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uin SDL_GetRGBA(pixel, surface->format, &pr, &pg, &pb, &pa); Uint32 newPixel = SDL_MapRGBA(surface->format, r, g, b, a); - if(pa == 0) + if(pa < 128) newPixel = SDL_MapRGBA(surface->format, 0, 0, 0, 0); pixels[i] = newPixel; @@ -888,7 +883,6 @@ void fillAlphaPixelWithRGBA(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b, Uin void boxBlur(SDL_Surface* surface) { - assert(surface && surface->format->format == SDL_PIXELFORMAT_ARGB8888); if (SDL_MUSTLOCK(surface)) SDL_LockSurface(surface); int width = surface->w; @@ -937,12 +931,9 @@ void boxBlur(SDL_Surface* surface) if (SDL_MUSTLOCK(surface)) SDL_UnlockSurface(surface); } -SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer) +SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * sourceSurface, bool doSheer) { - SDL_Surface *sourceSurface = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_ARGB8888, 0); - SDL_Surface *destSurface = newSurface(Point(source->w, source->h)); - - assert(destSurface->format->format == SDL_PIXELFORMAT_ARGB8888); + SDL_Surface *destSurface = newSurface(Point(sourceSurface->w, sourceSurface->h)); double shearX = doSheer ? 0.5 : 0.0; double scaleY = doSheer ? 0.5 : 0.25; @@ -962,7 +953,5 @@ SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * source, bool doSheer) fillAlphaPixelWithRGBA(destSurface, 0, 0, 0, 128); boxBlur(destSurface); - SDL_FreeSurface(sourceSurface); - return destSurface; } diff --git a/lib/texts/TextLocalizationContainer.cpp b/lib/texts/TextLocalizationContainer.cpp index be8a5d1e1..370889285 100644 --- a/lib/texts/TextLocalizationContainer.cpp +++ b/lib/texts/TextLocalizationContainer.cpp @@ -112,7 +112,7 @@ void TextLocalizationContainer::registerString(const std::string & identifierMod assert(!identifierModContext.empty()); assert(!localizedStringModContext.empty()); assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string - assert(stringsLocalizations.count(UID.get()) == 0 || boost::algorithm::starts_with(UID.get(), "map") || boost::algorithm::starts_with(UID.get(), "header")); // registering already registered string? FIXME: "header" is a workaround. VMAP needs proper integration in translation system + //assert(stringsLocalizations.count(UID.get()) == 0 || boost::algorithm::starts_with(UID.get(), "map") || boost::algorithm::starts_with(UID.get(), "header")); // registering already registered string? FIXME: "header" is a workaround. VMAP needs proper integration in translation system if(stringsLocalizations.count(UID.get()) > 0) { From 23fe336048c44e2f144a6d9853f02fdc5607d152 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 18 Jul 2025 00:56:46 +0200 Subject: [PATCH 13/14] Update TextLocalizationContainer.cpp --- lib/texts/TextLocalizationContainer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/texts/TextLocalizationContainer.cpp b/lib/texts/TextLocalizationContainer.cpp index 370889285..be8a5d1e1 100644 --- a/lib/texts/TextLocalizationContainer.cpp +++ b/lib/texts/TextLocalizationContainer.cpp @@ -112,7 +112,7 @@ void TextLocalizationContainer::registerString(const std::string & identifierMod assert(!identifierModContext.empty()); assert(!localizedStringModContext.empty()); assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string - //assert(stringsLocalizations.count(UID.get()) == 0 || boost::algorithm::starts_with(UID.get(), "map") || boost::algorithm::starts_with(UID.get(), "header")); // registering already registered string? FIXME: "header" is a workaround. VMAP needs proper integration in translation system + assert(stringsLocalizations.count(UID.get()) == 0 || boost::algorithm::starts_with(UID.get(), "map") || boost::algorithm::starts_with(UID.get(), "header")); // registering already registered string? FIXME: "header" is a workaround. VMAP needs proper integration in translation system if(stringsLocalizations.count(UID.get()) > 0) { From 26b62b2655c082fb045e1448fcf1d9472dd8db06 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:06:00 +0200 Subject: [PATCH 14/14] use native SDL functions when possible --- client/renderSDL/SDL_Extensions.cpp | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index eebb3db62..2382a90df 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -761,13 +761,41 @@ SDL_Surface* CSDL_Ext::drawOutline(SDL_Surface* sourceSurface, const SDL_Color& void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b, double c, double d, double tx, double ty) { + // Check if the transform is purely scaling (and optionally translation) + bool isPureScaling = vstd::isAlmostZero(b) && vstd::isAlmostZero(c); + + if (isPureScaling) + { + // Calculate target dimensions + int scaledW = static_cast(src->w * a); + int scaledH = static_cast(src->h * d); + + SDL_Rect srcRect = { 0, 0, src->w, src->h }; + SDL_Rect dstRect = { static_cast(tx), static_cast(ty), scaledW, scaledH }; + + // Convert surfaces to same format if needed + if (src->format->format != dst->format->format) + { + SDL_Surface* converted = SDL_ConvertSurface(src, dst->format, 0); + if (!converted) + throw std::runtime_error("SDL_ConvertSurface failed!"); + + SDL_BlitScaled(converted, &srcRect, dst, &dstRect); + SDL_FreeSurface(converted); + } + else + SDL_BlitScaled(src, &srcRect, dst, &dstRect); + + return; + } + // Lock surfaces for direct pixel access if (SDL_MUSTLOCK(src)) SDL_LockSurface(src); if (SDL_MUSTLOCK(dst)) SDL_LockSurface(dst); // Calculate inverse matrix M_inv for mapping dst -> src double det = a * d - b * c; - if (std::abs(det) < 1e-10) + if (vstd::isAlmostZero(det)) throw std::runtime_error("Singular transform matrix!"); double invDet = 1.0 / det; double ia = d * invDet; @@ -781,7 +809,7 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b tbb::parallel_for(tbb::blocked_range(0, dst->h), [&](const tbb::blocked_range& r) { // For each pixel in the destination image - for(int y = r.begin(); y != r.end(); ++y) + for(int y = r.begin(); y != r.end(); ++y) { for(int x = 0; x < dst->w; x++) { @@ -796,7 +824,6 @@ void applyAffineTransform(SDL_Surface* src, SDL_Surface* dst, double a, double b // Check bounds if (srcXi >= 0 && srcXi < src->w && srcYi >= 0 && srcYi < src->h) { - Uint32 pixel = srcPixels[srcYi * src->w + srcXi]; dstPixels[y * dst->w + x] = pixel; }