From 936c7767547e7b0f828b89ee7d4f43cb72dff6cc Mon Sep 17 00:00:00 2001 From: Tor Andre Haugdahl Date: Tue, 30 Sep 2025 16:22:46 +0200 Subject: [PATCH 1/3] Adds image cache and image buffer-manipulations --- .../animationwindow/include/Image.h | 71 +++++- .../subprojects/animationwindow/src/Image.cpp | 208 +++++++++++++++++- 2 files changed, 266 insertions(+), 13 deletions(-) diff --git a/dependencies/subprojects/animationwindow/include/Image.h b/dependencies/subprojects/animationwindow/include/Image.h index 621e96f..1fad162 100644 --- a/dependencies/subprojects/animationwindow/include/Image.h +++ b/dependencies/subprojects/animationwindow/include/Image.h @@ -1,28 +1,95 @@ #pragma once +#include #include +#include +#include +#include #include "Point.h" +#include "Color.h" #include #include "SDL_render.h" namespace TDT4102 { + + struct ImageCache; + struct Image { // Allow the texture to be read out and drawn despite being private friend class AnimationWindow; + friend class ImageCache; public: explicit Image(); explicit Image(std::filesystem::path pathToImageFile); + + Image &operator=(Image &&other) { + this->~Image(); + + state = other.state; + texture = other.texture; + surface = other.surface; + + other.reset(); + + return *this; + } + + Image(Image &&other) + : texture{other.texture}, surface{other.surface}, path{std::move(other.path)}, state{other.state} + { + other.reset(); + } + ~Image(); + /// Creates an image instance from a vector of colors. + /// \see TDT4102::Image::bytes + static Image fromPixels(const std::vector &bytes, + int imageWidth, int imageHeight); + + static std::vector loadPixels(std::filesystem::path pathToImageFile); + int width = 0; int height = 0; + + std::vector getPixels(); + void setPixels(const std::vector &, int width_, int height_); + private: + enum class ImageState { + Loaded, + FromFile, + FromBuffer, + Invalidated + }; + SDL_Texture *texture = nullptr; - std::filesystem::path src = "non existent file"; + + /// Trade-off. Allows image manipulation at the cost of memory. + SDL_Surface *surface = nullptr; + + std::filesystem::path path{"nonexistent"}; + + /// The state of the image + ImageState state = ImageState::Invalidated; void load(SDL_Renderer* renderer); - void draw(SDL_Renderer* renderer, TDT4102::Point location, int imageWidth = 0, int imageHeight = 0); + void draw(SDL_Renderer* renderer, TDT4102::Point location, + int imageWidth = 0, int imageHeight = 0); + + /// Does conditional texture cleanup. + /// Unsets the associated texture pointer + void cleanupTexture(); + + /// Conditionally cleans up the SDL_Surface + /// Unsets the associated SDL_Surface pointer + void cleanupSurface(); + + /// Resets the Image. For use with move-operators + void reset(); + + static ImageCache cache; }; } diff --git a/dependencies/subprojects/animationwindow/src/Image.cpp b/dependencies/subprojects/animationwindow/src/Image.cpp index 8655f9d..ca8c006 100644 --- a/dependencies/subprojects/animationwindow/src/Image.cpp +++ b/dependencies/subprojects/animationwindow/src/Image.cpp @@ -1,27 +1,149 @@ #include "Image.h" +#include "OverloadSet.h" +#include "SDL_surface.h" #include +#include +#include +#include +#include +#include + +namespace TDT4102 { +/// A caching mechanism to avoid processing images loaded from files + struct ImageCache { + /// Two-way lookup + std::unordered_map> pathToTexture; + std::unordered_set textureMembershipSet; + std::unordered_set surfaceMembershipSet; + + /// Loads an SDL_Texture from the file path and caches it + SDL_Texture *insert(SDL_Renderer *renderer, SDL_Surface *surface, const std::filesystem::path &filePath); + + SDL_Texture *getTexture(const std::filesystem::path &path); + SDL_Surface *getSurface(const std::filesystem::path &path); + + bool isTextureCached(SDL_Texture *texture); + bool isSurfaceCached(SDL_Surface *texture); + + ~ImageCache(); + + private: + ImageCache() = default; + friend class Image; + }; +} + +constexpr uint32_t GetPixelFormat() { + return SDL_PIXELFORMAT_ARGB8888; +} TDT4102::Image::Image() {} TDT4102::Image::Image(const std::filesystem::path pathToImageFile) { - src = pathToImageFile; + // storage = pathToImageFile; + path = pathToImageFile; + + SDL_Surface *cachedSurface = cache.getSurface(pathToImageFile); + + if ( cachedSurface == nullptr ) { + surface = IMG_Load(pathToImageFile.c_str()); + surface = SDL_ConvertSurfaceFormat(surface, GetPixelFormat(), 0); + width = surface->w; + height = surface->h; + } else { + surface = cachedSurface; + width = surface->w; + height = surface->h; + } + + state = ImageState::FromFile; } void TDT4102::Image::load(SDL_Renderer* renderer) { - if(!std::filesystem::exists(src)) { - throw std::runtime_error("The image file located at: " + src.string() + "\ncould not be found."); + // We need a reference to SDL_Renderer to perform the loading operation + // This requires loading it once it gets used + if ( state == ImageState::FromFile ) { + texture = cache.insert(renderer, surface, path); + state = texture ? ImageState::Loaded : ImageState::Invalidated; + } else if ( state == ImageState::FromBuffer ) { + texture = SDL_CreateTextureFromSurface(renderer, surface); + state = texture ? ImageState::Loaded : ImageState::Invalidated; + } +} + +TDT4102::Image TDT4102::Image::fromPixels(const std::vector &pixels, int imageWidth, int imageHeight) +{ + TDT4102::Image result = TDT4102::Image{}; + result.setPixels(pixels, imageWidth, imageHeight); + result.state = ImageState::FromBuffer; + return result; +} + +std::vector TDT4102::Image::getPixels() { + + std::vector result; + result.resize(width * height); + + // Convert the pixel format before copying + if ( surface->format->format != GetPixelFormat()) { + SDL_Surface *oldSurface = surface; + SDL_Surface *newSurface = SDL_ConvertSurfaceFormat(oldSurface, GetPixelFormat(), 0); + + if ( newSurface != nullptr ) { + SDL_FreeSurface(oldSurface); + surface = newSurface; + } + } + + // Early return if we're left with no surface to pull pixels from + if ( surface == nullptr ) { + return result; + } + + // Respect pitch + unsigned char *pixels = static_cast(surface->pixels); + int pitch = surface->pitch; + + for ( int y = 0; y < height; y++ ) { + std::memcpy(&result[y*width], &pixels[y*pitch], sizeof(TDT4102::Color)*width); + } + return result; +} + +void TDT4102::Image::setPixels(const std::vector &pixels, int width_, int height_) { + + width = width_; + height = height_; + + cleanupTexture(); + cleanupSurface(); + + surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, GetPixelFormat()); + + if ( surface == nullptr ) { + throw std::runtime_error{"Failed to create SDL Surface when setting pixels"}; + state = ImageState::Invalidated; + return; } - texture = IMG_LoadTexture(renderer, src.string().c_str()); - if(width == 0 && height == 0) { - SDL_QueryTexture(texture, nullptr, nullptr, &width, &height); + unsigned char *pixelData = static_cast(surface->pixels); + int pitch = surface->pitch; + + for ( int y = 0; y < height; y++ ) { + std::memcpy( + &pixelData[y*pitch], + &pixels[y*width], + width * sizeof(TDT4102::Color) + ); } + + // Invalidate the image. Reload texture + state = ImageState::FromBuffer; } + void TDT4102::Image::draw(SDL_Renderer *renderer, TDT4102::Point location, int imageWidth, int imageHeight) { - // We need a reference to SDL_Renderer to perform the loading operation - // This requires loading it once it gets used - if(texture == nullptr) { + if ( texture == nullptr ) { load(renderer); } @@ -36,8 +158,72 @@ void TDT4102::Image::draw(SDL_Renderer *renderer, TDT4102::Point location, int i } TDT4102::Image::~Image() { - if (texture != nullptr) { + cleanupTexture(); + cleanupSurface(); +} + +void TDT4102::Image::cleanupTexture() { + if (texture != nullptr && ! cache.isTextureCached(texture)) { SDL_DestroyTexture(texture); - texture = nullptr; + } + texture = nullptr; +} + +void TDT4102::Image::cleanupSurface() { + if ( surface != nullptr && !cache.isSurfaceCached(surface)) { + SDL_FreeSurface(surface); + } + surface = nullptr; +} + +void TDT4102::Image::reset() { + state = ImageState::Invalidated; + texture = nullptr; + surface = nullptr; +} + + +TDT4102::ImageCache TDT4102::Image::cache = TDT4102::ImageCache{}; + +SDL_Texture *TDT4102::ImageCache::insert(SDL_Renderer *renderer, SDL_Surface *surface, const std::filesystem::path &filePath) { + // TODO: Normalize cache key + if ( pathToTexture.contains(filePath) ) { + return pathToTexture[filePath].second; + } else { + SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); + pathToTexture.insert(std::make_pair(filePath, std::make_pair(surface, texture))); + textureMembershipSet.insert(texture); + surfaceMembershipSet.insert(surface); + return texture; + } +} + +bool TDT4102::ImageCache::isTextureCached(SDL_Texture *texture) { + return textureMembershipSet.contains(texture); +} + +bool TDT4102::ImageCache::isSurfaceCached(SDL_Surface *surface) { + return surfaceMembershipSet.contains(surface); +} + + +SDL_Surface *TDT4102::ImageCache::getSurface(const std::filesystem::path &path) { + if ( pathToTexture.contains(path) ) { + return pathToTexture[path].first; + } + return nullptr; +} + +SDL_Texture *TDT4102::ImageCache::getTexture(const std::filesystem::path &path) { + if ( pathToTexture.contains(path) ) { + return pathToTexture[path].second; + } + return nullptr; +} + +TDT4102::ImageCache::~ImageCache() { + for ( auto &[key, value] : pathToTexture ) { + SDL_FreeSurface(value.first); + SDL_DestroyTexture(value.second); } } From 09e03e43a07fd138c944abe637b83d1488bf4ace Mon Sep 17 00:00:00 2001 From: Tor Andre Haugdahl Date: Tue, 30 Sep 2025 16:30:17 +0200 Subject: [PATCH 2/3] Clean up includes --- .../animationwindow/include/Image.h | 4 ---- .../subprojects/animationwindow/src/Image.cpp | 20 ++++++++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/dependencies/subprojects/animationwindow/include/Image.h b/dependencies/subprojects/animationwindow/include/Image.h index 1fad162..d756bbd 100644 --- a/dependencies/subprojects/animationwindow/include/Image.h +++ b/dependencies/subprojects/animationwindow/include/Image.h @@ -1,9 +1,5 @@ #pragma once -#include -#include -#include -#include #include #include "Point.h" #include "Color.h" diff --git a/dependencies/subprojects/animationwindow/src/Image.cpp b/dependencies/subprojects/animationwindow/src/Image.cpp index ca8c006..8f23623 100644 --- a/dependencies/subprojects/animationwindow/src/Image.cpp +++ b/dependencies/subprojects/animationwindow/src/Image.cpp @@ -1,11 +1,7 @@ #include "Image.h" -#include "OverloadSet.h" #include "SDL_surface.h" #include -#include #include -#include -#include #include namespace TDT4102 { @@ -19,11 +15,11 @@ namespace TDT4102 { /// Loads an SDL_Texture from the file path and caches it SDL_Texture *insert(SDL_Renderer *renderer, SDL_Surface *surface, const std::filesystem::path &filePath); - SDL_Texture *getTexture(const std::filesystem::path &path); - SDL_Surface *getSurface(const std::filesystem::path &path); + SDL_Texture *getTexture(const std::filesystem::path &path) const; + SDL_Surface *getSurface(const std::filesystem::path &path) const; - bool isTextureCached(SDL_Texture *texture); - bool isSurfaceCached(SDL_Surface *texture); + bool isTextureCached(SDL_Texture *texture) const; + bool isSurfaceCached(SDL_Surface *texture) const; ~ImageCache(); @@ -198,23 +194,23 @@ SDL_Texture *TDT4102::ImageCache::insert(SDL_Renderer *renderer, SDL_Surface *su } } -bool TDT4102::ImageCache::isTextureCached(SDL_Texture *texture) { +bool TDT4102::ImageCache::isTextureCached(SDL_Texture *texture) const { return textureMembershipSet.contains(texture); } -bool TDT4102::ImageCache::isSurfaceCached(SDL_Surface *surface) { +bool TDT4102::ImageCache::isSurfaceCached(SDL_Surface *surface) const { return surfaceMembershipSet.contains(surface); } -SDL_Surface *TDT4102::ImageCache::getSurface(const std::filesystem::path &path) { +SDL_Surface *TDT4102::ImageCache::getSurface(const std::filesystem::path &path) const { if ( pathToTexture.contains(path) ) { return pathToTexture[path].first; } return nullptr; } -SDL_Texture *TDT4102::ImageCache::getTexture(const std::filesystem::path &path) { +SDL_Texture *TDT4102::ImageCache::getTexture(const std::filesystem::path &path) const { if ( pathToTexture.contains(path) ) { return pathToTexture[path].second; } From 288ba445967c827a474d6d90f48067af5c66cdfe Mon Sep 17 00:00:00 2001 From: Tor Andre Haugdahl Date: Tue, 30 Sep 2025 16:34:34 +0200 Subject: [PATCH 3/3] Reintroduce image load exception --- dependencies/subprojects/animationwindow/src/Image.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dependencies/subprojects/animationwindow/src/Image.cpp b/dependencies/subprojects/animationwindow/src/Image.cpp index 8f23623..6dbde8c 100644 --- a/dependencies/subprojects/animationwindow/src/Image.cpp +++ b/dependencies/subprojects/animationwindow/src/Image.cpp @@ -36,12 +36,16 @@ constexpr uint32_t GetPixelFormat() { TDT4102::Image::Image() {} TDT4102::Image::Image(const std::filesystem::path pathToImageFile) { - // storage = pathToImageFile; path = pathToImageFile; SDL_Surface *cachedSurface = cache.getSurface(pathToImageFile); if ( cachedSurface == nullptr ) { + + if ( ! std::filesystem::exists(path) ) { + throw std::runtime_error("The image file located at " + path.string() + "\ncould not be found"); + } + surface = IMG_Load(pathToImageFile.c_str()); surface = SDL_ConvertSurfaceFormat(surface, GetPixelFormat(), 0); width = surface->w;