feat: make image loading async

This commit is contained in:
2026-10-24 02:03:10 +02:00
parent aa3eef259c
commit 355dfa7851
11 changed files with 696 additions and 361 deletions
+128 -10
View File
@@ -1,9 +1,12 @@
#include <destrum/Graphics/Caches/ImageCache.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Threading/JobSystem.h>
#include "spdlog/spdlog.h"
#include <tracy/Tracy.hpp>
#include <algorithm>
#include <chrono>
#include <limits>
#include <stdexcept>
@@ -16,14 +19,9 @@ ImageID ImageCache::loadImageFromFile(
bool mipMap,
TextureIntent intent)
{
for (const auto& [id, info] : loadedImagesInfo) {
if (info.path == path &&
info.intent == intent &&
info.usage == usage &&
info.mipMap == mipMap)
{
return id;
}
if (const auto existing = findLoadedImage(path, usage, mipMap, intent)) {
finishPendingLoad(*existing);
return *existing;
}
auto imageOpt = gfxDevice.loadImageFromFileRaw(path, usage, mipMap, intent);
@@ -63,6 +61,120 @@ ImageID ImageCache::loadImageFromFile(
}
}
ImageID ImageCache::requestImage(
const std::filesystem::path& path,
VkImageUsageFlags usage,
bool mipMap,
TextureIntent intent)
{
if (const auto existing = findLoadedImage(path, usage, mipMap, intent)) {
return *existing;
}
const ImageID id = reserveImageId();
try {
loadedImagesInfo.emplace(
id,
LoadedImageInfo{
.path = path,
.intent = intent,
.usage = usage,
.mipMap = mipMap,
});
pendingImages.push_back(PendingImage{
.id = id,
.data = JobSystem::GetInstance().Async(
[path, intent] { return util::loadImage(path, intent); },
JobPriority::High),
});
return id;
} catch (...) {
loadedImagesInfo.erase(id);
freeIds->push_back(id);
throw;
}
}
void ImageCache::finishPendingLoads()
{
ZoneScopedN("ImageCache::finishPendingLoads");
// Upload in completion order so decoded pixels don't pile up in memory
while (!pendingImages.empty()) {
const auto ready = std::ranges::find_if(pendingImages, [](const PendingImage& pending) {
return pending.data.wait_for(std::chrono::seconds{0}) == std::future_status::ready;
});
if (ready == pendingImages.end()) {
pendingImages.front().data.wait_for(std::chrono::milliseconds{1});
continue;
}
PendingImage pending = std::move(*ready);
pendingImages.erase(ready);
completePendingLoad(std::move(pending));
}
}
std::optional<ImageID> ImageCache::findLoadedImage(
const std::filesystem::path& path,
VkImageUsageFlags usage,
bool mipMap,
TextureIntent intent) const
{
for (const auto& [id, info] : loadedImagesInfo) {
if (info.path == path &&
info.intent == intent &&
info.usage == usage &&
info.mipMap == mipMap)
{
return id;
}
}
return std::nullopt;
}
ImageID ImageCache::reserveImageId()
{
const ImageID id = getFreeImageId();
if (const auto it = std::find(freeIds->begin(), freeIds->end(), id); it != freeIds->end()) {
freeIds->erase(it);
}
if (id == images.size()) {
images.emplace_back();
}
return id;
}
void ImageCache::finishPendingLoad(ImageID id)
{
const auto it = std::ranges::find_if(pendingImages, [id](const PendingImage& pending) { return pending.id == id; });
if (it == pendingImages.end()) {
return;
}
PendingImage pending = std::move(*it);
pendingImages.erase(it);
completePendingLoad(std::move(pending));
}
void ImageCache::completePendingLoad(PendingImage pending)
{
const LoadedImageInfo& info = loadedImagesInfo.at(pending.id);
std::optional<GPUImage> image;
try {
image = gfxDevice.createImageFromData(pending.data.get(), info.usage, info.mipMap, info.path.string());
} catch (const std::exception& exception) {
spdlog::error("Failed to load image '{}': {}", info.path.string(), exception.what());
}
if (!image) {
// Materials already hold this ID, so the slot shows the error texture instead
spdlog::warn("Using error texture for failed image load: '{}'", info.path.string());
if (errorImageId != NULL_IMAGE_ID) {
bindlessSetManager.addImage(gfxDevice.getDevice(), pending.id, images.at(errorImageId).imageView);
}
return;
}
addImage(pending.id, std::move(*image));
}
ImageID ImageCache::addImage(GPUImage image) {
try {
return addImage(getFreeImageId(), std::move(image));
@@ -113,11 +225,16 @@ const ImageCache::LoadedImageInfo* ImageCache::findLoadedImageInfo(ImageID id) c
}
void ImageCache::removeImage(ImageID id) {
if (id >= images.size() || images[id].image == VK_NULL_HANDLE) {
if (id >= images.size()) {
return;
}
// Pending and failed loads own a slot without an image
const bool tracked = loadedImagesInfo.erase(id) > 0;
std::erase_if(pendingImages, [id](const PendingImage& pending) { return pending.id == id; });
if (images[id].image == VK_NULL_HANDLE && !tracked) {
return;
}
loadedImagesInfo.erase(id);
gfxDevice.destroyImage(images[id]);
// In-flight frames may still sample this bindless slot, so it only becomes
@@ -146,6 +263,7 @@ ImageID ImageCache::getFreeImageId() const {
}
void ImageCache::destroyImages() {
pendingImages.clear();
for (auto& image: images) {
gfxDevice.destroyImage(image);
}
+11 -4
View File
@@ -130,13 +130,20 @@ std::optional<GPUImage> ImageManager::loadImageFromFile(
bool mipMap,
TextureIntent intent) const
{
const auto data = util::loadImage(path, intent);
return createImageFromData(util::loadImage(path, intent), usage, mipMap, path.string());
}
std::optional<GPUImage> ImageManager::createImageFromData(
const ImageData& data,
VkImageUsageFlags usage,
bool mipMap,
const std::string& debugName) const
{
if (data.vkFormat == VK_FORMAT_UNDEFINED ||
data.byteSize == 0 ||
(data.hdr ? data.hdrPixels == nullptr : data.pixels == nullptr))
{
spdlog::error("Failed to load image '{}'", path.string());
spdlog::error("Failed to load image '{}'", debugName);
return std::nullopt;
}
@@ -151,7 +158,7 @@ std::optional<GPUImage> ImageManager::loadImageFromFile(
spdlog::warn(
"Format {} does not support linear mip generation, loading '{}' without mips",
static_cast<int>(data.vkFormat),
path.string());
debugName);
mipMap = false;
}
}
@@ -178,7 +185,7 @@ std::optional<GPUImage> ImageManager::loadImageFromFile(
throw;
}
image.debugName = path.string();
image.debugName = debugName;
vkutil::addDebugLabel(device, image.image, image.debugName.c_str());
const std::string viewName = image.debugName + " (view)";
vkutil::addDebugLabel(device, image.imageView, viewName.c_str());
+37 -1
View File
@@ -204,13 +204,49 @@ ImageID RenderResources::loadMaterialTexture(
TextureIntent intent)
{
(void)gfxDevice;
const ImageID id = imageCache->loadImageFromFile(path, VK_IMAGE_USAGE_SAMPLED_BIT, true, intent);
const ImageID id = deferredTextureLoads > 0
? imageCache->requestImage(path, VK_IMAGE_USAGE_SAMPLED_BIT, true, intent)
: imageCache->loadImageFromFile(path, VK_IMAGE_USAGE_SAMPLED_BIT, true, intent);
if (id != errorImageId) {
materialTextures.insert(id);
}
return id;
}
void RenderResources::beginDeferredTextureLoads()
{
++deferredTextureLoads;
}
void RenderResources::endDeferredTextureLoads()
{
if (deferredTextureLoads > 0 && --deferredTextureLoads == 0 && imageCache) {
imageCache->finishPendingLoads();
}
}
DeferredTextureLoadScope::DeferredTextureLoadScope(RenderResources* resources)
: resources(resources)
{
if (resources != nullptr) {
resources->beginDeferredTextureLoads();
}
}
DeferredTextureLoadScope::~DeferredTextureLoadScope()
{
if (resources == nullptr) {
return;
}
try {
resources->endDeferredTextureLoads();
} catch (const std::exception& exception) {
spdlog::error("Finishing deferred texture loads failed: {}", exception.what());
} catch (...) {
spdlog::error("Finishing deferred texture loads failed");
}
}
RenderResources::ReleaseStats RenderResources::releaseUnused(
GfxDevice& gfxDevice,
const std::unordered_set<MeshID>& usedMeshes,
@@ -16,6 +16,8 @@
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Serialization/ObjectSerialization.h>
#include <destrum/Threading/JobSystem.h>
#include <destrum/Graphics/Renderer.h>
#include <destrum/Util/GameState.h>
using json = nlohmann::json;
@@ -207,6 +209,10 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
std::vector<GameObject*> registeredObjects;
std::string context = "scene";
auto& gameState = GameState::GetInstance();
const DeferredTextureLoadScope textureLoads{
gameState.HasRenderer() && gameState.HasGfxDevice() ? gameState.Renderer().getResources() : nullptr};
try {
json objects = root.at("objects");
if (root.contains("materials")) {