diff --git a/dependencies/subprojects/animationwindow/include/Image.h b/dependencies/subprojects/animationwindow/include/Image.h index 621e96f..d756bbd 100644 --- a/dependencies/subprojects/animationwindow/include/Image.h +++ b/dependencies/subprojects/animationwindow/include/Image.h @@ -1,28 +1,91 @@ #pragma once -#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..6dbde8c 100644 --- a/dependencies/subprojects/animationwindow/src/Image.cpp +++ b/dependencies/subprojects/animationwindow/src/Image.cpp @@ -1,27 +1,149 @@ #include "Image.h" +#include "SDL_surface.h" #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) const; + SDL_Surface *getSurface(const std::filesystem::path &path) const; + + bool isTextureCached(SDL_Texture *texture) const; + bool isSurfaceCached(SDL_Surface *texture) const; + + ~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; + 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; + 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) const { + return textureMembershipSet.contains(texture); +} + +bool TDT4102::ImageCache::isSurfaceCached(SDL_Surface *surface) const { + return surfaceMembershipSet.contains(surface); +} + + +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) const { + 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); } }