New ImageResourceDisplay implementation that should decrease RAM usage

This commit is contained in:
Yousef Khadadeh 2026-02-19 15:21:47 +03:00
parent a69d9e3c6c
commit fec602e46a
11 changed files with 246 additions and 98 deletions

View file

@ -11,6 +11,8 @@
#include <include/core/SkCanvas.h>
#include "../MainProgram.hpp"
#include <Helpers/Logger.hpp>
#include <include/core/SkImageInfo.h>
#include <include/core/SkSamplingOptions.h>
#ifdef USE_SKIA_BACKEND_GRAPHITE
#include <include/gpu/graphite/Surface.h>
@ -19,7 +21,7 @@
#include <include/gpu/ganesh/SkSurfaceGanesh.h>
#endif
std::array<const SkCodecs::Decoder, 7> decoders = {
std::array<const SkCodecs::Decoder, 7> decoders2 = {
SkBmpDecoder::Decoder(),
SkGifDecoder::Decoder(),
SkIcoDecoder::Decoder(),
@ -30,60 +32,50 @@ std::array<const SkCodecs::Decoder, 7> decoders = {
SkWebpDecoder::Decoder()
};
void ImageResourceDisplay::load_thread() {
for(int i = 0; i < totalFramesToLoad; i++) {
if(shutdownLoadThread)
break;
SkCodec::Options imageOptions;
imageOptions.fFrameIndex = i;
SkCodec::FrameInfo frameInfo;
float frameTime = -1.0f;
if(codec->getFrameInfo(i, &frameInfo))
frameTime = frameInfo.fDuration;
auto decodeResult = codec->getImage(imageInfo, &imageOptions);
if(std::get<1>(decodeResult) == SkCodec::Result::kSuccess)
frames[i] = {std::get<0>(decodeResult), frameTime};
else {
Logger::get().log("WORLDFATAL", "Could not decode image, got error code " + std::to_string(std::get<1>(decodeResult)) + ". Image has dimensions " + std::to_string(imageInfo.width()) + " " + std::to_string(imageInfo.height()));
break;
}
loadedFrames++;
}
imageRawData = nullptr;
codec = nullptr;
}
std::unordered_map<std::shared_ptr<std::string>, sk_sp<SkImage>> ImageResourceDisplay::screenshotCache;
std::atomic<int> ImageResourceDisplay::imageLoadThreadCount = 0;
#ifdef __EMSCRIPTEN__
int ImageResourceDisplay::IMAGE_LOAD_THREAD_COUNT_MAX = 1;
#else
int ImageResourceDisplay::IMAGE_LOAD_THREAD_COUNT_MAX = 4;
#endif
bool ImageResourceDisplay::update_draw() const {
return mustUpdateDraw;
}
void ImageResourceDisplay::update(World& w) {
for(auto& frame : frames) {
std::erase_if(frame.mipmapLevels, [](const auto& mipmapPair) {
static constexpr std::chrono::duration TIME_TO_REMOVE_MIPMAP_CACHE = std::chrono::seconds(40);
return std::chrono::steady_clock::now() - mipmapPair.second.timeLastUsed > TIME_TO_REMOVE_MIPMAP_CACHE;
});
screenshotCache.clear();
for(size_t mipmapLevel = 0; mipmapLevel < mipmapLevelsStatus.size(); mipmapLevel++) {
if(mipmapLevelsStatus[mipmapLevel] == MipmapLevelStatus::ALLOCATED_JUST_SET)
mipmapLevelsStatus[mipmapLevel] = MipmapLevelStatus::ALLOCATED_WILL_REMOVE;
else if(mipmapLevelsStatus[mipmapLevel] == MipmapLevelStatus::ALLOCATED_WILL_REMOVE) {
mipmapLevelsStatus[mipmapLevel] = MipmapLevelStatus::UNALLOCATED;
for(auto& frame : frames)
frame.mipmapLevels[mipmapLevel] = nullptr;
}
}
mustUpdateDraw = false;
if(!loadedFirstFrame && loadedFrames >= 1) {
if(mustUpdateDrawLoadThread) {
mustUpdateDraw = true;
loadedFirstFrame = true;
mustUpdateDrawLoadThread = false;
}
if(loadedFrames <= 1)
else
mustUpdateDraw = false;
if(frames.size() == 1)
return;
currentTime += w.main.deltaTime * 1000.0;
currentTime += w.main.deltaTime;
for(;;) {
if(currentTime >= frames[frameIndex].duration) {
currentTime -= frames[frameIndex].duration;
frameIndex++;
if(frameIndex >= loadedFrames) {
if(loadedFrames != totalFramesToLoad) // Dont loop the GIF if it's not completely loaded yet
frameIndex--;
else
frameIndex = 0;
}
mustUpdateDraw = true;
if(frameIndex >= static_cast<int>(frames.size()))
frameIndex = 0;
}
else
return;
@ -92,64 +84,185 @@ void ImageResourceDisplay::update(World& w) {
}
}
bool ImageResourceDisplay::load(ResourceManager& rMan, const std::string& fileName, const std::string& fileData) {
imageRawData = SkData::MakeWithoutCopy(fileData.c_str(), fileData.size());
codec = SkCodec::MakeFromData(imageRawData, decoders);
sk_sp<SkImage> ImageResourceDisplay::load_frame_with_codec(const std::unique_ptr<SkCodec>& codec, unsigned width, unsigned height, unsigned frameIndexToLoad) {
SkCodec::Options imageOptions;
imageOptions.fFrameIndex = frameIndexToLoad;
auto decodeResult = codec->getImage(imageInfo, &imageOptions);
if(std::get<1>(decodeResult) != SkCodec::Result::kSuccess)
throw std::runtime_error("Could not decode image, got error code " + std::to_string(std::get<1>(decodeResult)) + ". Image decoded with dimensions " + std::to_string(imageInfo.width()) + " " + std::to_string(imageInfo.height()));
sk_sp<SkImage> ogImage = std::get<0>(decodeResult);
SkImageInfo scaledImageInfo = imageInfo.makeDimensions(SkISize(width, height));
auto scaledImage = ogImage->makeScaled(scaledImageInfo, {SkFilterMode::kLinear, SkMipmapMode::kNone});
if(!scaledImage)
throw std::runtime_error("Could not scale image.");
return scaledImage;
}
void ImageResourceDisplay::camera_view_update(const CoordSpaceHelper& compCoords, const SCollision::AABB<WorldScalar>& compAABB, const DrawData& drawData, const SkRect& imRect) {
if(!smallestMipmapLevelLoaded)
attempt_load_mipmap_in_separate_thread(get_smallest_mipmap_level());
else {
if(SCollision::collide(compAABB, drawData.cam.viewingAreaGenerousCollider)) {
Vector2f transformedImRectDimensions;
transformedImRectDimensions.x() = drawData.cam.c.scalar_to_space(compCoords.scalar_from_space(imRect.width()));
transformedImRectDimensions.y() = drawData.cam.c.scalar_to_space(compCoords.scalar_from_space(imRect.height()));
int mipmapLevel = get_exact_mipmap_level_for_dimensions(transformedImRectDimensions.cast<int>());
if(mipmapLevel != get_smallest_mipmap_level()) {
if(mipmapLevelsStatus[mipmapLevel] == MipmapLevelStatus::UNALLOCATED) {
attempt_load_mipmap_in_separate_thread(mipmapLevel);
int nextBestMipmapLevel = get_best_allocated_mipmap_level(mipmapLevel);
if(nextBestMipmapLevel != get_smallest_mipmap_level())
mipmapLevelsStatus[nextBestMipmapLevel] = MipmapLevelStatus::ALLOCATED_JUST_SET;
}
else
mipmapLevelsStatus[mipmapLevel] = MipmapLevelStatus::ALLOCATED_JUST_SET;
}
}
}
}
bool ImageResourceDisplay::load(ResourceManager& rMan, const std::string& fileName, const std::shared_ptr<std::string>& fileData) {
this->fileData = fileData;
auto codec = SkCodec::MakeFromData(SkData::MakeWithoutCopy(this->fileData->c_str(), this->fileData->size()), decoders2);
mustUpdateDraw = true;
mustUpdateDrawLoadThread = true;
if(codec) {
auto origin = codec->getOrigin();
bool rotate = (origin == kLeftTop_SkEncodedOrigin || origin == kRightTop_SkEncodedOrigin || origin == kRightBottom_SkEncodedOrigin || origin == kLeftBottom_SkEncodedOrigin);
totalFramesToLoad = codec->getFrameCount();
imageInfo = codec->getInfo();
frames.resize(totalFramesToLoad);
loadedFrames = 0;
mipmapLevelsStatus = std::vector<std::atomic<MipmapLevelStatus>>(calculate_smallest_mipmap_level());
for(auto& mipmapLevelStatus : mipmapLevelsStatus)
mipmapLevelStatus = MipmapLevelStatus::UNALLOCATED;
frames.resize(codec->getFrameCount());
for(size_t i = 0; i < frames.size(); i++) {
auto& frame = frames[i];
frame.mipmapLevels.resize(get_smallest_mipmap_level());
SkCodec::FrameInfo frameInfo;
if(codec->getFrameInfo(i, &frameInfo))
frame.duration = static_cast<float>(frameInfo.fDuration) / 1000.0f; // get frametime in seconds
else
frame.duration = -1.0f;
}
if(rotate) {
Logger::get().log("INFO", "Image loaded is rotated, so must rotate dimensions");
imageInfo = imageInfo.makeDimensions(SkISize(imageInfo.height(), imageInfo.width()));
}
try{
loadThread = std::make_unique<std::thread>(&ImageResourceDisplay::load_thread, this);
}
catch(...) {
Logger::get().log("INFO", "[ImageResourceDisplay::load] Failed to create load thread, loading on main thread");
load_thread();
}
codec = nullptr;
attempt_load_mipmap_in_separate_thread(get_smallest_mipmap_level());
return true;
}
return false;
}
void ImageResourceDisplay::attempt_load_mipmap_in_separate_thread(int mipmapLevel) {
if(imageLoadThreadCount >= IMAGE_LOAD_THREAD_COUNT_MAX)
return;
if(!loadThread) {
shutdownLoadThread = false;
imageLoadThreadCount++;
loadThread = std::make_unique<std::thread>(&ImageResourceDisplay::load_thread_func, this, mipmapLevel);
}
else if(shutdownLoadThread) {
loadThread->join();
shutdownLoadThread = false;
imageLoadThreadCount++;
loadThread = std::make_unique<std::thread>(&ImageResourceDisplay::load_thread_func, this, mipmapLevel);
}
}
void ImageResourceDisplay::load_thread_func(int mipmapLevel) {
auto codec = SkCodec::MakeFromData(SkData::MakeWithoutCopy(this->fileData->c_str(), this->fileData->size()), decoders2);
Vector2i imDim = get_mipmap_level_image_dimensions(mipmapLevel);
for(size_t i = 0; i < frames.size(); i++) {
if(shutdownLoadThread)
return;
auto& frame = frames[i];
auto& mipmapImage = (mipmapLevel == get_smallest_mipmap_level()) ? frame.smallestMipmapLevel : frame.mipmapLevels[mipmapLevel];
mipmapImage = load_frame_with_codec(codec, imDim.x(), imDim.y(), i);
}
if(mipmapLevel == get_smallest_mipmap_level())
smallestMipmapLevelLoaded = true;
else
mipmapLevelsStatus[mipmapLevel] = MipmapLevelStatus::ALLOCATED_JUST_SET;
mustUpdateDrawLoadThread = true;
imageLoadThreadCount--;
shutdownLoadThread = true;
}
void ImageResourceDisplay::draw(SkCanvas* canvas, const DrawData& drawData, const SkRect& imRect) {
if(loadedFrames == 0)
if(!smallestMipmapLevelLoaded)
canvas->drawRect(imRect, SkPaint({0.5f, 0.5f, 0.5f, 0.5f}));
else {
SkRect imRectPixelSize = canvas->getLocalToDeviceAs3x3().mapRect(imRect);
float imWidth = std::max(imRectPixelSize.width(), 32.0f);
unsigned divisionLevel = imageInfo.width() / imWidth;
int mipmapLevel = get_exact_mipmap_level_for_dimensions({imRectPixelSize.width(), imRectPixelSize.height()});
auto& frame = frames[frameIndex];
if(divisionLevel <= 1)
canvas->drawImageRect(frame.data, imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
else {
unsigned mipmapLevel = std::log2(divisionLevel);
auto mipmapIt = frame.mipmapLevels.find(mipmapLevel);
if(mipmapIt == frame.mipmapLevels.end()) {
unsigned divideResolutionBy = std::exp2(mipmapLevel);
SkISize mipmapResolution(imageInfo.width() / divideResolutionBy, imageInfo.height() / divideResolutionBy);
SkImageInfo mipmapImgInfo = imageInfo.makeDimensions(mipmapResolution);
sk_sp<SkImage> mipmapImage = frame.data->makeScaled(mipmapImgInfo, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone));
frame.mipmapLevels.emplace(mipmapLevel, MipmapLevelData{mipmapImage, std::chrono::steady_clock::now()});
canvas->drawImageRect(mipmapImage, imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
int closestMipmapLevel = get_best_allocated_mipmap_level(mipmapLevel);
if(drawData.main->takingScreenshot) {
if(closestMipmapLevel == mipmapLevel) {
if(closestMipmapLevel == get_smallest_mipmap_level())
canvas->drawImageRect(frame.smallestMipmapLevel, imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
else
canvas->drawImageRect(frame.mipmapLevels[closestMipmapLevel], imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
}
else {
mipmapIt->second.timeLastUsed = std::chrono::steady_clock::now();
canvas->drawImageRect(mipmapIt->second.imageData, imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
if(screenshotCache.contains(fileData))
canvas->drawImageRect(screenshotCache[fileData], imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
else {
auto codec = SkCodec::MakeFromData(SkData::MakeWithoutCopy(this->fileData->c_str(), this->fileData->size()), decoders2);
screenshotCache[fileData] = load_frame_with_codec(codec, imageInfo.width(), imageInfo.height(), frameIndex);
canvas->drawImageRect(screenshotCache[fileData], imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
}
}
}
else {
if(closestMipmapLevel == get_smallest_mipmap_level())
canvas->drawImageRect(frame.smallestMipmapLevel, imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
else
canvas->drawImageRect(frame.mipmapLevels[closestMipmapLevel], imRect, {SkFilterMode::kLinear, SkMipmapMode::kNone});
}
}
}
int ImageResourceDisplay::get_exact_mipmap_level_for_dimensions(const Vector2i& dim) {
int minimumImageDim = std::min(imageInfo.width(), imageInfo.height());
int minimumRectDim = std::min(dim.x(), dim.y());
int imDim = std::max<int>(minimumRectDim, SMALLEST_MIPMAP_RESOLUTION);
int divisionLevel = minimumImageDim / imDim;
return (divisionLevel <= 1) ? 0 : std::log2(divisionLevel);
}
int ImageResourceDisplay::get_best_allocated_mipmap_level(int mipmapLevel) {
if(mipmapLevel == get_smallest_mipmap_level())
return mipmapLevel;
for(int i = mipmapLevel; i >= 0; i--) {
if(mipmapLevelsStatus[i] != MipmapLevelStatus::UNALLOCATED)
return i;
}
for(int i = mipmapLevel; i < get_smallest_mipmap_level(); i++) {
if(mipmapLevelsStatus[i] != MipmapLevelStatus::UNALLOCATED)
return i;
}
return get_smallest_mipmap_level();
}
int ImageResourceDisplay::calculate_smallest_mipmap_level() {
int minimumImageDim = std::min(imageInfo.width(), imageInfo.height());
int divisionLevel = minimumImageDim / SMALLEST_MIPMAP_RESOLUTION;
return (divisionLevel <= 1) ? 0 : std::log2(divisionLevel);
}
int ImageResourceDisplay::get_smallest_mipmap_level() const {
return mipmapLevelsStatus.size();
}
Vector2i ImageResourceDisplay::get_mipmap_level_image_dimensions(int mipmapLevel) const {
unsigned divideResolutionBy = std::exp2(mipmapLevel);
return {imageInfo.width() / divideResolutionBy, imageInfo.height() / divideResolutionBy};
}
Vector2f ImageResourceDisplay::get_dimensions() const {
return {imageInfo.width(), imageInfo.height()};
}