feat: make image loading async
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
Reference in New Issue
Block a user