EDIT: IBL async
This commit is contained in:
@@ -13,7 +13,7 @@ Data Flow
|
|||||||
- `VulkanEngine::init_vulkan()` creates an `IBLManager`, calls `init(context)`, and publishes it via `EngineContext::ibl`.
|
- `VulkanEngine::init_vulkan()` creates an `IBLManager`, calls `init(context)`, and publishes it via `EngineContext::ibl`.
|
||||||
- The engine optionally loads default IBL assets (`IBLPaths` in `src/core/engine.cpp`), typically a BRDF LUT plus a specular environment `.ktx2`.
|
- The engine optionally loads default IBL assets (`IBLPaths` in `src/core/engine.cpp`), typically a BRDF LUT plus a specular environment `.ktx2`.
|
||||||
- Loading (IBLManager):
|
- Loading (IBLManager):
|
||||||
- `IBLManager::load(const IBLPaths&)`:
|
- `IBLManager::load(const IBLPaths&)` (synchronous, mostly used in tools/tests):
|
||||||
- Specular:
|
- Specular:
|
||||||
- Tries `ktxutil::load_ktx2_cubemap` first. If successful, uploads via `ResourceManager::create_image_compressed_layers` with `VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT`.
|
- Tries `ktxutil::load_ktx2_cubemap` first. If successful, uploads via `ResourceManager::create_image_compressed_layers` with `VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT`.
|
||||||
- If cubemap loading fails, falls back to 2D `.ktx2` via `ktxutil::load_ktx2_2d` and `create_image_compressed`. The image is treated as equirectangular with prefiltered mips.
|
- If cubemap loading fails, falls back to 2D `.ktx2` via `ktxutil::load_ktx2_2d` and `create_image_compressed`. The image is treated as equirectangular with prefiltered mips.
|
||||||
@@ -28,6 +28,12 @@ Data Flow
|
|||||||
- Loaded as 2D `.ktx2` via `ktxutil::load_ktx2_2d` and uploaded with `create_image_compressed`.
|
- Loaded as 2D `.ktx2` via `ktxutil::load_ktx2_2d` and uploaded with `create_image_compressed`.
|
||||||
- Fallbacks:
|
- Fallbacks:
|
||||||
- If `diffuseCube` is missing but a specular env exists, `_diff` is aliased to `_spec`.
|
- If `diffuseCube` is missing but a specular env exists, `_diff` is aliased to `_spec`.
|
||||||
|
- `IBLManager::load_async(const IBLPaths&)` + `IBLManager::pump_async()` (runtime path used by the engine):
|
||||||
|
- `load_async` runs KTX2 file I/O and SH bake on a worker thread and stores a prepared CPU-side description (`PreparedIBLData`).
|
||||||
|
- `pump_async` is called on the main thread once per frame (after the previous frame is idle) to:
|
||||||
|
- Destroy old IBL images/SH via `destroy_images_and_sh()`.
|
||||||
|
- Create new GPU images with `create_image_compressed(_layers)` and upload the SH buffer.
|
||||||
|
- This avoids stalls in the main/game loop when switching IBL volumes or loading the default environment at startup.
|
||||||
- `IBLManager::unload()` releases GPU images, the SH buffer, and the descriptor set layout.
|
- `IBLManager::unload()` releases GPU images, the SH buffer, and the descriptor set layout.
|
||||||
- Descriptor layout:
|
- Descriptor layout:
|
||||||
- `IBLManager::ensureLayout()` builds a descriptor set layout (set=3) with:
|
- `IBLManager::ensureLayout()` builds a descriptor set layout (set=3) with:
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Image‑Based Lighting (IBL) Textures
|
|||||||
- Specular:
|
- Specular:
|
||||||
- If `specularCube` is a cubemap `.ktx2`, `IBLManager` uses `ktxutil::load_ktx2_cubemap` and uploads via `ResourceManager::create_image_compressed_layers`, preserving the file’s format and mip chain.
|
- If `specularCube` is a cubemap `.ktx2`, `IBLManager` uses `ktxutil::load_ktx2_cubemap` and uploads via `ResourceManager::create_image_compressed_layers`, preserving the file’s format and mip chain.
|
||||||
- If cubemap load fails, it falls back to 2D `.ktx2` via `ktxutil::load_ktx2_2d` + `ResourceManager::create_image_compressed`. The image is treated as equirectangular with prefiltered mips and sampled with explicit LOD in shaders.
|
- If cubemap load fails, it falls back to 2D `.ktx2` via `ktxutil::load_ktx2_2d` + `ResourceManager::create_image_compressed`. The image is treated as equirectangular with prefiltered mips and sampled with explicit LOD in shaders.
|
||||||
- If the format is float HDR (`R16G16B16A16_SFLOAT` or `R32G32B32A32_SFLOAT`) and the aspect ratio is 2:1, `IBLManager` additionally computes 2nd‑order SH coefficients (9×`vec3`) on the CPU for diffuse irradiance and uploads them to a UBO (`_shBuffer`).
|
- If the format is float HDR (`R16G16B16A16_SFLOAT` or `R32G32B32A32_SFLOAT`) and the aspect ratio is 2:1, `IBLManager` additionally computes 2nd‑order SH coefficients (9×`vec3`) on a worker thread and uploads them to a UBO (`_shBuffer`) when `pump_async()` is called on the main thread.
|
||||||
- Diffuse (optional):
|
- Diffuse (optional):
|
||||||
- If `diffuseCube` is provided and valid, it is uploaded as a cubemap using `create_image_compressed_layers`. Current shaders use the SH buffer for diffuse; this cubemap can be wired into a future path if you want to sample it directly.
|
- If `diffuseCube` is provided and valid, it is uploaded as a cubemap using `create_image_compressed_layers`. Current shaders use the SH buffer for diffuse; this cubemap can be wired into a future path if you want to sample it directly.
|
||||||
- BRDF LUT:
|
- BRDF LUT:
|
||||||
|
|||||||
@@ -8,85 +8,62 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <ktx.h>
|
#include <ktx.h>
|
||||||
#include <SDL_stdinc.h>
|
#include <SDL_stdinc.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
#include "core/device/device.h"
|
#include "core/device/device.h"
|
||||||
#include "core/assets/texture_cache.h"
|
#include "core/assets/texture_cache.h"
|
||||||
|
|
||||||
bool IBLManager::load(const IBLPaths &paths)
|
struct PreparedIBLData
|
||||||
{
|
{
|
||||||
if (_ctx == nullptr || _ctx->getResources() == nullptr) return false;
|
IBLPaths paths{};
|
||||||
ResourceManager *rm = _ctx->getResources();
|
|
||||||
|
|
||||||
// When uploads are deferred into the RenderGraph, any previously queued
|
bool has_spec{false};
|
||||||
// image uploads might still reference VkImage handles owned by this
|
bool spec_is_cubemap{false};
|
||||||
// manager. Before destroying or recreating IBL images, flush those
|
ktxutil::KtxCubemap spec_cubemap{};
|
||||||
// uploads via the immediate path so we never record barriers or copies
|
ktxutil::Ktx2D spec_2d{};
|
||||||
// for images that have been destroyed.
|
|
||||||
if (rm->deferred_uploads() && rm->has_pending_uploads())
|
|
||||||
{
|
|
||||||
rm->process_queued_uploads_immediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow reloading at runtime: destroy previous images/SH but keep layout.
|
bool has_diffuse{false};
|
||||||
destroy_images_and_sh();
|
ktxutil::KtxCubemap diff_cubemap{};
|
||||||
ensureLayout();
|
|
||||||
|
|
||||||
// Load specular environment: prefer cubemap; fallback to 2D equirect with mips.
|
bool has_background{false};
|
||||||
// Also hint the TextureCache (if present) so future switches are cheap.
|
ktxutil::Ktx2D background_2d{};
|
||||||
if (!paths.specularCube.empty())
|
|
||||||
|
bool has_brdf{false};
|
||||||
|
ktxutil::Ktx2D brdf_2d{};
|
||||||
|
|
||||||
|
bool has_sh{false};
|
||||||
|
glm::vec4 sh[9]{};
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace
|
||||||
{
|
{
|
||||||
// Try as cubemap first
|
static bool compute_sh_from_ktx2_equirect(const char *path, glm::vec4 out_sh[9])
|
||||||
ktxutil::KtxCubemap kcm{};
|
|
||||||
if (ktxutil::load_ktx2_cubemap(paths.specularCube.c_str(), kcm))
|
|
||||||
{
|
{
|
||||||
_spec = rm->create_image_compressed_layers(
|
if (path == nullptr) return false;
|
||||||
kcm.bytes.data(), kcm.bytes.size(),
|
|
||||||
kcm.fmt, kcm.mipLevels, kcm.layers,
|
|
||||||
kcm.copies,
|
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT,
|
|
||||||
kcm.imgFlags
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ktxutil::Ktx2D k2d{};
|
|
||||||
if (ktxutil::load_ktx2_2d(paths.specularCube.c_str(), k2d))
|
|
||||||
{
|
|
||||||
std::vector<ResourceManager::MipLevelCopy> lv;
|
|
||||||
lv.reserve(k2d.mipLevels);
|
|
||||||
for (uint32_t mip = 0; mip < k2d.mipLevels; ++mip)
|
|
||||||
{
|
|
||||||
const auto &r = k2d.copies[mip];
|
|
||||||
lv.push_back(ResourceManager::MipLevelCopy{
|
|
||||||
.offset = r.bufferOffset,
|
|
||||||
.length = 0,
|
|
||||||
.width = r.imageExtent.width,
|
|
||||||
.height = r.imageExtent.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_spec = rm->create_image_compressed(k2d.bytes.data(), k2d.bytes.size(), k2d.fmt, lv,
|
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT);
|
|
||||||
|
|
||||||
ktxTexture2 *ktex = nullptr;
|
ktxTexture2 *ktex = nullptr;
|
||||||
if (ktxTexture2_CreateFromNamedFile(paths.specularCube.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT,
|
if (ktxTexture2_CreateFromNamedFile(path, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktex) != KTX_SUCCESS || !ktex)
|
||||||
&ktex) == KTX_SUCCESS && ktex)
|
|
||||||
{
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
const VkFormat fmt = static_cast<VkFormat>(ktex->vkFormat);
|
const VkFormat fmt = static_cast<VkFormat>(ktex->vkFormat);
|
||||||
const bool isFloat16 = fmt == VK_FORMAT_R16G16B16A16_SFLOAT;
|
const bool isFloat16 = fmt == VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||||
const bool isFloat32 = fmt == VK_FORMAT_R32G32B32A32_SFLOAT;
|
const bool isFloat32 = fmt == VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
if (!ktxTexture2_NeedsTranscoding(ktex) && (isFloat16 || isFloat32) && ktex->baseWidth == 2 * ktex->
|
if (!ktxTexture2_NeedsTranscoding(ktex) && (isFloat16 || isFloat32) && ktex->baseWidth == 2 * ktex->baseHeight)
|
||||||
baseHeight)
|
|
||||||
{
|
{
|
||||||
const uint32_t W = ktex->baseWidth;
|
const uint32_t W = ktex->baseWidth;
|
||||||
const uint32_t H = ktex->baseHeight;
|
const uint32_t H = ktex->baseHeight;
|
||||||
const uint8_t *dataPtr = reinterpret_cast<const uint8_t *>(
|
const uint8_t *dataPtr = reinterpret_cast<const uint8_t *>(ktxTexture_GetData(ktxTexture(ktex)));
|
||||||
ktxTexture_GetData(ktxTexture(ktex)));
|
|
||||||
|
|
||||||
// Compute 9 SH coefficients (irradiance) from equirect HDR
|
|
||||||
struct Vec3
|
struct Vec3
|
||||||
{
|
{
|
||||||
float x, y, z;
|
float x, y, z;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto half_to_float = [](uint16_t h) -> float {
|
auto half_to_float = [](uint16_t h) -> float {
|
||||||
uint16_t h_exp = (h & 0x7C00u) >> 10;
|
uint16_t h_exp = (h & 0x7C00u) >> 10;
|
||||||
uint16_t h_sig = h & 0x03FFu;
|
uint16_t h_sig = h & 0x03FFu;
|
||||||
@@ -101,7 +78,6 @@ bool IBLManager::load(const IBLPaths &paths)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// subnormals
|
|
||||||
int e = -1;
|
int e = -1;
|
||||||
uint16_t sig = h_sig;
|
uint16_t sig = h_sig;
|
||||||
while ((sig & 0x0400u) == 0)
|
while ((sig & 0x0400u) == 0)
|
||||||
@@ -143,16 +119,14 @@ bool IBLManager::load(const IBLPaths &paths)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr int L = 2; // 2nd order (9 coeffs)
|
|
||||||
const float dtheta = float(M_PI) / float(H);
|
const float dtheta = float(M_PI) / float(H);
|
||||||
const float dphi = 2.f * float(M_PI) / float(W);
|
const float dphi = 2.f * float(M_PI) / float(W);
|
||||||
// Accumulate RGB SH coeffs
|
|
||||||
std::array<glm::vec3, 9> c{};
|
std::array<glm::vec3, 9> c{};
|
||||||
for (auto &v: c) v = glm::vec3(0);
|
for (auto &v : c) v = glm::vec3(0.0f);
|
||||||
|
|
||||||
auto sh_basis = [](const glm::vec3 &d) -> std::array<float, 9> {
|
auto sh_basis = [](const glm::vec3 &d) -> std::array<float, 9> {
|
||||||
const float x = d.x, y = d.y, z = d.z;
|
const float x = d.x, y = d.y, z = d.z;
|
||||||
// Real SH, unnormalized constants
|
|
||||||
const float c0 = 0.2820947918f;
|
const float c0 = 0.2820947918f;
|
||||||
const float c1 = 0.4886025119f;
|
const float c1 = 0.4886025119f;
|
||||||
const float c2 = 1.0925484306f;
|
const float c2 = 1.0925484306f;
|
||||||
@@ -173,23 +147,23 @@ bool IBLManager::load(const IBLPaths &paths)
|
|||||||
|
|
||||||
for (uint32_t y = 0; y < H; ++y)
|
for (uint32_t y = 0; y < H; ++y)
|
||||||
{
|
{
|
||||||
float theta = (y + 0.5f) * dtheta; // [0,pi]
|
float theta = (y + 0.5f) * dtheta;
|
||||||
float sinT = std::sin(theta);
|
float sinT = std::sin(theta);
|
||||||
for (uint32_t x = 0; x < W; ++x)
|
for (uint32_t x = 0; x < W; ++x)
|
||||||
{
|
{
|
||||||
float phi = (x + 0.5f) * dphi; // [0,2pi]
|
float phi = (x + 0.5f) * dphi;
|
||||||
glm::vec3 dir = glm::vec3(std::cos(phi) * sinT, std::cos(theta), std::sin(phi) * sinT);
|
glm::vec3 dir = glm::vec3(std::cos(phi) * sinT, std::cos(theta), std::sin(phi) * sinT);
|
||||||
auto Lrgb = sample_at(x, y);
|
auto Lrgb = sample_at(x, y);
|
||||||
glm::vec3 Lvec(Lrgb.x, Lrgb.y, Lrgb.z);
|
glm::vec3 Lvec(Lrgb.x, Lrgb.y, Lrgb.z);
|
||||||
auto Y = sh_basis(dir);
|
auto Y = sh_basis(dir);
|
||||||
float dOmega = dphi * dtheta * sinT; // solid angle per pixel
|
float dOmega = dphi * dtheta * sinT;
|
||||||
for (int i = 0; i < 9; ++i)
|
for (int i = 0; i < 9; ++i)
|
||||||
{
|
{
|
||||||
c[i] += Lvec * (Y[i] * dOmega);
|
c[i] += Lvec * (Y[i] * dOmega);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Convolve with Lambert kernel via per-band scale
|
|
||||||
const float A0 = float(M_PI);
|
const float A0 = float(M_PI);
|
||||||
const float A1 = 2.f * float(M_PI) / 3.f;
|
const float A1 = 2.f * float(M_PI) / 3.f;
|
||||||
const float A2 = float(M_PI) / 4.f;
|
const float A2 = float(M_PI) / 4.f;
|
||||||
@@ -198,101 +172,262 @@ bool IBLManager::load(const IBLPaths &paths)
|
|||||||
{
|
{
|
||||||
int band = (i == 0) ? 0 : (i < 4 ? 1 : 2);
|
int band = (i == 0) ? 0 : (i < 4 ? 1 : 2);
|
||||||
c[i] *= Aband[band];
|
c[i] *= Aband[band];
|
||||||
|
out_sh[i] = glm::vec4(c[i], 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
_shBuffer = rm->create_buffer(sizeof(glm::vec4) * 9, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
ok = true;
|
||||||
VMA_MEMORY_USAGE_CPU_TO_GPU);
|
}
|
||||||
|
|
||||||
|
ktxTexture_Destroy(ktxTexture(ktex));
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool prepare_ibl_cpu(const IBLPaths &paths, PreparedIBLData &outData, std::string &outError)
|
||||||
|
{
|
||||||
|
outData = PreparedIBLData{};
|
||||||
|
outData.paths = paths;
|
||||||
|
outError.clear();
|
||||||
|
|
||||||
|
if (!paths.specularCube.empty())
|
||||||
|
{
|
||||||
|
ktxutil::KtxCubemap cube{};
|
||||||
|
if (ktxutil::load_ktx2_cubemap(paths.specularCube.c_str(), cube))
|
||||||
|
{
|
||||||
|
outData.has_spec = true;
|
||||||
|
outData.spec_is_cubemap = true;
|
||||||
|
outData.spec_cubemap = std::move(cube);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ktxutil::Ktx2D k2d{};
|
||||||
|
if (ktxutil::load_ktx2_2d(paths.specularCube.c_str(), k2d))
|
||||||
|
{
|
||||||
|
outData.has_spec = true;
|
||||||
|
outData.spec_is_cubemap = false;
|
||||||
|
outData.spec_2d = std::move(k2d);
|
||||||
|
|
||||||
|
glm::vec4 sh[9]{};
|
||||||
|
if (compute_sh_from_ktx2_equirect(paths.specularCube.c_str(), sh))
|
||||||
|
{
|
||||||
|
outData.has_sh = true;
|
||||||
for (int i = 0; i < 9; ++i)
|
for (int i = 0; i < 9; ++i)
|
||||||
{
|
{
|
||||||
glm::vec4 v(c[i], 0.0f);
|
outData.sh[i] = sh[i];
|
||||||
std::memcpy(reinterpret_cast<char *>(_shBuffer.info.pMappedData) + i * sizeof(glm::vec4),
|
|
||||||
&v, sizeof(glm::vec4));
|
|
||||||
}
|
}
|
||||||
vmaFlushAllocation(_ctx->getDevice()->allocator(), _shBuffer.allocation, 0,
|
|
||||||
sizeof(glm::vec4) * 9);
|
|
||||||
}
|
}
|
||||||
ktxTexture_Destroy(ktxTexture(ktex));
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outError = "Failed to load specular IBL as cubemap or 2D KTX2";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diffuse cubemap (optional; if missing, reuse specular)
|
|
||||||
if (!paths.diffuseCube.empty())
|
if (!paths.diffuseCube.empty())
|
||||||
{
|
{
|
||||||
ktxutil::KtxCubemap kcm{};
|
ktxutil::KtxCubemap diff{};
|
||||||
if (ktxutil::load_ktx2_cubemap(paths.diffuseCube.c_str(), kcm))
|
if (ktxutil::load_ktx2_cubemap(paths.diffuseCube.c_str(), diff))
|
||||||
{
|
{
|
||||||
_diff = rm->create_image_compressed_layers(
|
outData.has_diffuse = true;
|
||||||
kcm.bytes.data(), kcm.bytes.size(),
|
outData.diff_cubemap = std::move(diff);
|
||||||
kcm.fmt, kcm.mipLevels, kcm.layers,
|
|
||||||
kcm.copies,
|
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT,
|
|
||||||
kcm.imgFlags
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_diff.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE)
|
|
||||||
{
|
|
||||||
_diff = _spec;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!paths.background2D.empty())
|
if (!paths.background2D.empty())
|
||||||
{
|
{
|
||||||
ktxutil::Ktx2D bg{};
|
ktxutil::Ktx2D bg{};
|
||||||
if (ktxutil::load_ktx2_2d(paths.background2D.c_str(), bg))
|
if (ktxutil::load_ktx2_2d(paths.background2D.c_str(), bg))
|
||||||
{
|
{
|
||||||
std::vector<ResourceManager::MipLevelCopy> lv;
|
outData.has_background = true;
|
||||||
lv.reserve(bg.mipLevels);
|
outData.background_2d = std::move(bg);
|
||||||
for (uint32_t mip = 0; mip < bg.mipLevels; ++mip)
|
|
||||||
{
|
|
||||||
const auto &r = bg.copies[mip];
|
|
||||||
lv.push_back(ResourceManager::MipLevelCopy{
|
|
||||||
.offset = r.bufferOffset,
|
|
||||||
.length = 0,
|
|
||||||
.width = r.imageExtent.width,
|
|
||||||
.height = r.imageExtent.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_background = rm->create_image_compressed(
|
|
||||||
bg.bytes.data(), bg.bytes.size(), bg.fmt, lv,
|
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_background.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE)
|
|
||||||
{
|
|
||||||
_background = _spec;
|
|
||||||
}
|
|
||||||
|
|
||||||
// BRDF LUT
|
|
||||||
if (!paths.brdfLut2D.empty())
|
if (!paths.brdfLut2D.empty())
|
||||||
{
|
{
|
||||||
ktxutil::Ktx2D lut{};
|
ktxutil::Ktx2D lut{};
|
||||||
if (ktxutil::load_ktx2_2d(paths.brdfLut2D.c_str(), lut))
|
if (ktxutil::load_ktx2_2d(paths.brdfLut2D.c_str(), lut))
|
||||||
{
|
{
|
||||||
std::vector<ResourceManager::MipLevelCopy> lv;
|
outData.has_brdf = true;
|
||||||
lv.reserve(lut.mipLevels);
|
outData.brdf_2d = std::move(lut);
|
||||||
for (uint32_t mip = 0; mip < lut.mipLevels; ++mip)
|
|
||||||
{
|
|
||||||
const auto &r = lut.copies[mip];
|
|
||||||
lv.push_back(ResourceManager::MipLevelCopy{
|
|
||||||
.offset = r.bufferOffset,
|
|
||||||
.length = 0,
|
|
||||||
.width = r.imageExtent.width,
|
|
||||||
.height = r.imageExtent.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_brdf = rm->create_image_compressed(lut.bytes.data(), lut.bytes.size(), lut.fmt, lv,
|
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (_spec.image != VK_NULL_HANDLE) && (_diff.image != VK_NULL_HANDLE);
|
// Success is defined by having a specular environment; diffuse/background/BRDF are optional.
|
||||||
|
if (!outData.has_spec)
|
||||||
|
{
|
||||||
|
if (outError.empty())
|
||||||
|
{
|
||||||
|
outError = "Specular IBL KTX2 not found or invalid";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IBLManager::AsyncStateData
|
||||||
|
{
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable cv;
|
||||||
|
bool shutdown{false};
|
||||||
|
|
||||||
|
bool requestPending{false};
|
||||||
|
IBLPaths requestPaths{};
|
||||||
|
uint64_t requestId{0};
|
||||||
|
|
||||||
|
bool resultReady{false};
|
||||||
|
bool resultSuccess{false};
|
||||||
|
PreparedIBLData readyData{};
|
||||||
|
std::string lastError;
|
||||||
|
uint64_t resultId{0};
|
||||||
|
|
||||||
|
std::thread worker;
|
||||||
|
};
|
||||||
|
|
||||||
|
IBLManager::~IBLManager()
|
||||||
|
{
|
||||||
|
shutdown_async();
|
||||||
|
}
|
||||||
|
|
||||||
|
void IBLManager::init(EngineContext *ctx)
|
||||||
|
{
|
||||||
|
_ctx = ctx;
|
||||||
|
|
||||||
|
if (_async != nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_async = new AsyncStateData();
|
||||||
|
AsyncStateData *state = _async;
|
||||||
|
|
||||||
|
state->worker = std::thread([this, state]() {
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
IBLPaths paths{};
|
||||||
|
uint64_t jobId = 0;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(state->mutex);
|
||||||
|
state->cv.wait(lock, [state]() { return state->shutdown || state->requestPending; });
|
||||||
|
if (state->shutdown)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
paths = state->requestPaths;
|
||||||
|
jobId = state->requestId;
|
||||||
|
state->requestPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedIBLData data{};
|
||||||
|
std::string error;
|
||||||
|
bool ok = prepare_ibl_cpu(paths, data, error);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state->mutex);
|
||||||
|
if (state->shutdown)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Drop results for superseded jobs.
|
||||||
|
if (jobId != state->requestId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->readyData = std::move(data);
|
||||||
|
state->lastError = std::move(error);
|
||||||
|
state->resultSuccess = ok;
|
||||||
|
state->resultReady = true;
|
||||||
|
state->resultId = jobId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IBLManager::load(const IBLPaths &paths)
|
||||||
|
{
|
||||||
|
if (_ctx == nullptr || _ctx->getResources() == nullptr) return false;
|
||||||
|
|
||||||
|
PreparedIBLData data{};
|
||||||
|
std::string error;
|
||||||
|
if (!prepare_ibl_cpu(paths, data, error))
|
||||||
|
{
|
||||||
|
if (!error.empty())
|
||||||
|
{
|
||||||
|
fmt::println("[IBL] load failed: {}", error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit_prepared(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IBLManager::load_async(const IBLPaths &paths)
|
||||||
|
{
|
||||||
|
if (_ctx == nullptr || _ctx->getResources() == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_async == nullptr)
|
||||||
|
{
|
||||||
|
init(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncStateData *state = _async;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state->mutex);
|
||||||
|
state->requestPaths = paths;
|
||||||
|
state->requestPending = true;
|
||||||
|
state->requestId++;
|
||||||
|
// Invalidate any previous ready result; it will be superseded by this job.
|
||||||
|
state->resultReady = false;
|
||||||
|
}
|
||||||
|
state->cv.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBLManager::AsyncResult IBLManager::pump_async()
|
||||||
|
{
|
||||||
|
AsyncResult out{};
|
||||||
|
|
||||||
|
if (_async == nullptr || _ctx == nullptr || _ctx->getResources() == nullptr)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncStateData *state = _async;
|
||||||
|
|
||||||
|
PreparedIBLData data{};
|
||||||
|
bool success = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state->mutex);
|
||||||
|
if (!state->resultReady)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
data = std::move(state->readyData);
|
||||||
|
success = state->resultSuccess;
|
||||||
|
state->resultReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.completed = true;
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
out.success = false;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit GPU resources on the main thread.
|
||||||
|
out.success = commit_prepared(data);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IBLManager::unload()
|
void IBLManager::unload()
|
||||||
{
|
{
|
||||||
|
shutdown_async();
|
||||||
|
|
||||||
if (_ctx == nullptr || _ctx->getResources() == nullptr) return;
|
if (_ctx == nullptr || _ctx->getResources() == nullptr) return;
|
||||||
|
|
||||||
// Destroy images and SH buffer first.
|
// Destroy images and SH buffer first.
|
||||||
@@ -363,3 +498,150 @@ void IBLManager::destroy_images_and_sh()
|
|||||||
_background = {};
|
_background = {};
|
||||||
_brdf = {};
|
_brdf = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IBLManager::shutdown_async()
|
||||||
|
{
|
||||||
|
if (_async == nullptr) return;
|
||||||
|
|
||||||
|
AsyncStateData *state = _async;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state->mutex);
|
||||||
|
state->shutdown = true;
|
||||||
|
state->requestPending = false;
|
||||||
|
}
|
||||||
|
state->cv.notify_all();
|
||||||
|
if (state->worker.joinable())
|
||||||
|
{
|
||||||
|
state->worker.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete _async;
|
||||||
|
_async = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IBLManager::commit_prepared(const PreparedIBLData &data)
|
||||||
|
{
|
||||||
|
if (_ctx == nullptr || _ctx->getResources() == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceManager *rm = _ctx->getResources();
|
||||||
|
|
||||||
|
if (rm->deferred_uploads() && rm->has_pending_uploads())
|
||||||
|
{
|
||||||
|
rm->process_queued_uploads_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_images_and_sh();
|
||||||
|
ensureLayout();
|
||||||
|
|
||||||
|
if (data.has_spec)
|
||||||
|
{
|
||||||
|
if (data.spec_is_cubemap)
|
||||||
|
{
|
||||||
|
const auto &kcm = data.spec_cubemap;
|
||||||
|
_spec = rm->create_image_compressed_layers(
|
||||||
|
kcm.bytes.data(), kcm.bytes.size(),
|
||||||
|
kcm.fmt, kcm.mipLevels, kcm.layers,
|
||||||
|
kcm.copies,
|
||||||
|
VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||||
|
kcm.imgFlags);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto &k2d = data.spec_2d;
|
||||||
|
std::vector<ResourceManager::MipLevelCopy> lv;
|
||||||
|
lv.reserve(k2d.mipLevels);
|
||||||
|
for (uint32_t mip = 0; mip < k2d.mipLevels; ++mip)
|
||||||
|
{
|
||||||
|
const auto &r = k2d.copies[mip];
|
||||||
|
lv.push_back(ResourceManager::MipLevelCopy{
|
||||||
|
.offset = r.bufferOffset,
|
||||||
|
.length = 0,
|
||||||
|
.width = r.imageExtent.width,
|
||||||
|
.height = r.imageExtent.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_spec = rm->create_image_compressed(
|
||||||
|
k2d.bytes.data(), k2d.bytes.size(), k2d.fmt, lv,
|
||||||
|
VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||||
|
|
||||||
|
if (data.has_sh)
|
||||||
|
{
|
||||||
|
_shBuffer = rm->create_buffer(sizeof(glm::vec4) * 9,
|
||||||
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||||
|
VMA_MEMORY_USAGE_CPU_TO_GPU);
|
||||||
|
for (int i = 0; i < 9; ++i)
|
||||||
|
{
|
||||||
|
std::memcpy(reinterpret_cast<char *>(_shBuffer.info.pMappedData) + i * sizeof(glm::vec4),
|
||||||
|
&data.sh[i], sizeof(glm::vec4));
|
||||||
|
}
|
||||||
|
vmaFlushAllocation(_ctx->getDevice()->allocator(), _shBuffer.allocation, 0,
|
||||||
|
sizeof(glm::vec4) * 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.has_diffuse)
|
||||||
|
{
|
||||||
|
const auto &kcm = data.diff_cubemap;
|
||||||
|
_diff = rm->create_image_compressed_layers(
|
||||||
|
kcm.bytes.data(), kcm.bytes.size(),
|
||||||
|
kcm.fmt, kcm.mipLevels, kcm.layers,
|
||||||
|
kcm.copies,
|
||||||
|
VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||||
|
kcm.imgFlags);
|
||||||
|
}
|
||||||
|
if (_diff.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
_diff = _spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.has_background)
|
||||||
|
{
|
||||||
|
const auto &bg = data.background_2d;
|
||||||
|
std::vector<ResourceManager::MipLevelCopy> lv;
|
||||||
|
lv.reserve(bg.mipLevels);
|
||||||
|
for (uint32_t mip = 0; mip < bg.mipLevels; ++mip)
|
||||||
|
{
|
||||||
|
const auto &r = bg.copies[mip];
|
||||||
|
lv.push_back(ResourceManager::MipLevelCopy{
|
||||||
|
.offset = r.bufferOffset,
|
||||||
|
.length = 0,
|
||||||
|
.width = r.imageExtent.width,
|
||||||
|
.height = r.imageExtent.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_background = rm->create_image_compressed(
|
||||||
|
bg.bytes.data(), bg.bytes.size(), bg.fmt, lv,
|
||||||
|
VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_background.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
_background = _spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.has_brdf)
|
||||||
|
{
|
||||||
|
const auto &lut = data.brdf_2d;
|
||||||
|
std::vector<ResourceManager::MipLevelCopy> lv;
|
||||||
|
lv.reserve(lut.mipLevels);
|
||||||
|
for (uint32_t mip = 0; mip < lut.mipLevels; ++mip)
|
||||||
|
{
|
||||||
|
const auto &r = lut.copies[mip];
|
||||||
|
lv.push_back(ResourceManager::MipLevelCopy{
|
||||||
|
.offset = r.bufferOffset,
|
||||||
|
.length = 0,
|
||||||
|
.width = r.imageExtent.width,
|
||||||
|
.height = r.imageExtent.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_brdf = rm->create_image_compressed(
|
||||||
|
lut.bytes.data(), lut.bytes.size(), lut.fmt, lv,
|
||||||
|
VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (_spec.image != VK_NULL_HANDLE) && (_diff.image != VK_NULL_HANDLE);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class TextureCache;
|
|||||||
|
|
||||||
class EngineContext;
|
class EngineContext;
|
||||||
|
|
||||||
|
struct PreparedIBLData;
|
||||||
|
|
||||||
struct IBLPaths
|
struct IBLPaths
|
||||||
{
|
{
|
||||||
std::string specularCube; // .ktx2 (GPU-ready BC6H or R16G16B16A16)
|
std::string specularCube; // .ktx2 (GPU-ready BC6H or R16G16B16A16)
|
||||||
@@ -20,13 +22,35 @@ struct IBLPaths
|
|||||||
class IBLManager
|
class IBLManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void init(EngineContext *ctx) { _ctx = ctx; }
|
IBLManager() = default;
|
||||||
|
~IBLManager();
|
||||||
|
|
||||||
|
void init(EngineContext *ctx);
|
||||||
|
|
||||||
void set_texture_cache(TextureCache *cache) { _cache = cache; }
|
void set_texture_cache(TextureCache *cache) { _cache = cache; }
|
||||||
|
|
||||||
// Load all three textures. Returns true when specular+diffuse (and optional LUT) are resident.
|
// Load all three textures. Returns true when specular+diffuse (and optional LUT) are resident.
|
||||||
bool load(const IBLPaths &paths);
|
bool load(const IBLPaths &paths);
|
||||||
|
|
||||||
|
// Asynchronous IBL load:
|
||||||
|
// - Performs KTX2 file I/O and SH bake on a background thread.
|
||||||
|
// - GPU image creation and SH upload are deferred to pump_async() on the main thread.
|
||||||
|
// Returns false if the job could not be queued.
|
||||||
|
bool load_async(const IBLPaths &paths);
|
||||||
|
|
||||||
|
struct AsyncResult
|
||||||
|
{
|
||||||
|
// True when an async job finished since the last pump_async() call.
|
||||||
|
bool completed{false};
|
||||||
|
// True when the finished job successfully produced new GPU IBL resources.
|
||||||
|
bool success{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main-thread integration: if a completed async job is pending, destroy the
|
||||||
|
// previous IBL images/SH and upload the new ones. Must be called only when
|
||||||
|
// the GPU is idle for the previous frame.
|
||||||
|
AsyncResult pump_async();
|
||||||
|
|
||||||
// Release GPU memory and patch to fallbacks handled by the caller.
|
// Release GPU memory and patch to fallbacks handled by the caller.
|
||||||
void unload();
|
void unload();
|
||||||
|
|
||||||
@@ -57,6 +81,13 @@ private:
|
|||||||
VkDescriptorSetLayout _iblSetLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout _iblSetLayout = VK_NULL_HANDLE;
|
||||||
AllocatedBuffer _shBuffer{}; // 9*vec4 coefficients (RGB in .xyz)
|
AllocatedBuffer _shBuffer{}; // 9*vec4 coefficients (RGB in .xyz)
|
||||||
|
|
||||||
|
struct AsyncStateData;
|
||||||
|
AsyncStateData *_async{nullptr};
|
||||||
|
|
||||||
|
bool commit_prepared(const PreparedIBLData &data);
|
||||||
|
|
||||||
// Destroy current GPU images/SH buffer but keep descriptor layout alive.
|
// Destroy current GPU images/SH buffer but keep descriptor layout alive.
|
||||||
void destroy_images_and_sh();
|
void destroy_images_and_sh();
|
||||||
|
|
||||||
|
void shutdown_async();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ void VulkanEngine::init()
|
|||||||
// Publish to context for passes and pipeline layout assembly
|
// Publish to context for passes and pipeline layout assembly
|
||||||
_context->ibl = _iblManager.get();
|
_context->ibl = _iblManager.get();
|
||||||
|
|
||||||
// Try to load default IBL assets if present
|
// Try to load default IBL assets if present (async)
|
||||||
{
|
{
|
||||||
IBLPaths ibl{};
|
IBLPaths ibl{};
|
||||||
ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2");
|
ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2");
|
||||||
@@ -262,15 +262,23 @@ void VulkanEngine::init()
|
|||||||
// Treat this as the global/fallback IBL used outside any local volume.
|
// Treat this as the global/fallback IBL used outside any local volume.
|
||||||
_globalIBLPaths = ibl;
|
_globalIBLPaths = ibl;
|
||||||
_activeIBLVolume = -1;
|
_activeIBLVolume = -1;
|
||||||
bool ibl_ok = _iblManager->load(ibl);
|
_hasGlobalIBL = false;
|
||||||
_hasGlobalIBL = ibl_ok;
|
if (_iblManager)
|
||||||
if (!ibl_ok)
|
|
||||||
{
|
{
|
||||||
fmt::println("[Engine] Warning: failed to load default IBL (specular='{}', brdfLut='{}'). IBL lighting will be disabled until a valid IBL is loaded.",
|
if (_iblManager->load_async(ibl))
|
||||||
|
{
|
||||||
|
_pendingIBLRequest.active = true;
|
||||||
|
_pendingIBLRequest.targetVolume = -1;
|
||||||
|
_pendingIBLRequest.paths = ibl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fmt::println("[Engine] Warning: failed to enqueue default IBL load (specular='{}', brdfLut='{}'). IBL lighting will be disabled until a valid IBL is loaded.",
|
||||||
ibl.specularCube,
|
ibl.specularCube,
|
||||||
ibl.brdfLut2D);
|
ibl.brdfLut2D);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init_frame_resources();
|
init_frame_resources();
|
||||||
|
|
||||||
@@ -436,6 +444,56 @@ bool VulkanEngine::addGLTFInstance(const std::string &instanceName,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VulkanEngine::addPrimitiveInstance(const std::string &instanceName,
|
||||||
|
AssetManager::MeshGeometryDesc::Type geomType,
|
||||||
|
const glm::mat4 &transform,
|
||||||
|
const AssetManager::MeshMaterialDesc &material,
|
||||||
|
std::optional<BoundsType> boundsTypeOverride)
|
||||||
|
{
|
||||||
|
if (!_assetManager || !_sceneManager)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a cache key for the primitive mesh so multiple instances
|
||||||
|
// share the same GPU buffers.
|
||||||
|
std::string meshName;
|
||||||
|
switch (geomType)
|
||||||
|
{
|
||||||
|
case AssetManager::MeshGeometryDesc::Type::Cube:
|
||||||
|
meshName = "Primitive.Cube";
|
||||||
|
break;
|
||||||
|
case AssetManager::MeshGeometryDesc::Type::Sphere:
|
||||||
|
meshName = "Primitive.Sphere";
|
||||||
|
break;
|
||||||
|
case AssetManager::MeshGeometryDesc::Type::Plane:
|
||||||
|
meshName = "Primitive.Plane";
|
||||||
|
break;
|
||||||
|
case AssetManager::MeshGeometryDesc::Type::Capsule:
|
||||||
|
meshName = "Primitive.Capsule";
|
||||||
|
break;
|
||||||
|
case AssetManager::MeshGeometryDesc::Type::Provided:
|
||||||
|
default:
|
||||||
|
// Provided geometry requires explicit vertex/index data; not supported here.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetManager::MeshCreateInfo ci{};
|
||||||
|
ci.name = meshName;
|
||||||
|
ci.geometry.type = geomType;
|
||||||
|
ci.material = material;
|
||||||
|
ci.boundsType = boundsTypeOverride;
|
||||||
|
|
||||||
|
auto mesh = _assetManager->createMesh(ci);
|
||||||
|
if (!mesh)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sceneManager->addMeshInstance(instanceName, mesh, transform, boundsTypeOverride);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t VulkanEngine::loadGLTFAsync(const std::string &sceneName,
|
uint32_t VulkanEngine::loadGLTFAsync(const std::string &sceneName,
|
||||||
const std::string &modelRelativePath,
|
const std::string &modelRelativePath,
|
||||||
const glm::mat4 &transform,
|
const glm::mat4 &transform,
|
||||||
@@ -627,6 +685,7 @@ void VulkanEngine::draw()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newVolume != _activeIBLVolume)
|
if (newVolume != _activeIBLVolume)
|
||||||
{
|
{
|
||||||
const IBLPaths *paths = nullptr;
|
const IBLPaths *paths = nullptr;
|
||||||
@@ -639,17 +698,25 @@ void VulkanEngine::draw()
|
|||||||
paths = &_globalIBLPaths;
|
paths = &_globalIBLPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paths)
|
// Avoid enqueueing duplicate jobs for the same target volume.
|
||||||
|
const bool alreadyPendingForTarget =
|
||||||
|
_pendingIBLRequest.active && _pendingIBLRequest.targetVolume == newVolume;
|
||||||
|
|
||||||
|
if (paths && !alreadyPendingForTarget)
|
||||||
{
|
{
|
||||||
bool ibl_ok = _iblManager->load(*paths);
|
if (_iblManager->load_async(*paths))
|
||||||
if (!ibl_ok)
|
|
||||||
{
|
{
|
||||||
fmt::println("[Engine] Warning: failed to load IBL for {} (specular='{}')",
|
_pendingIBLRequest.active = true;
|
||||||
|
_pendingIBLRequest.targetVolume = newVolume;
|
||||||
|
_pendingIBLRequest.paths = *paths;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fmt::println("[Engine] Warning: failed to enqueue IBL load for {} (specular='{}')",
|
||||||
(newVolume >= 0) ? "volume" : "global environment",
|
(newVolume >= 0) ? "volume" : "global environment",
|
||||||
paths->specularCube);
|
paths->specularCube);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_activeIBLVolume = newVolume;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,6 +1185,33 @@ void VulkanEngine::run()
|
|||||||
// Safe to destroy any BLAS queued for deletion now that the previous frame is idle.
|
// Safe to destroy any BLAS queued for deletion now that the previous frame is idle.
|
||||||
if (_rayManager) { _rayManager->flushPendingDeletes(); }
|
if (_rayManager) { _rayManager->flushPendingDeletes(); }
|
||||||
|
|
||||||
|
// Commit any completed async IBL load now that the GPU is idle.
|
||||||
|
if (_iblManager && _pendingIBLRequest.active)
|
||||||
|
{
|
||||||
|
IBLManager::AsyncResult iblRes = _iblManager->pump_async();
|
||||||
|
if (iblRes.completed)
|
||||||
|
{
|
||||||
|
if (iblRes.success)
|
||||||
|
{
|
||||||
|
if (_pendingIBLRequest.targetVolume >= 0)
|
||||||
|
{
|
||||||
|
_activeIBLVolume = _pendingIBLRequest.targetVolume;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_activeIBLVolume = -1;
|
||||||
|
_hasGlobalIBL = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fmt::println("[Engine] Warning: async IBL load failed (specular='{}')",
|
||||||
|
_pendingIBLRequest.paths.specularCube);
|
||||||
|
}
|
||||||
|
_pendingIBLRequest.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager)
|
if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager)
|
||||||
{
|
{
|
||||||
vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t));
|
vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t));
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ public:
|
|||||||
// User-defined local IBL volumes and currently active index (-1 = global).
|
// User-defined local IBL volumes and currently active index (-1 = global).
|
||||||
std::vector<IBLVolume> _iblVolumes;
|
std::vector<IBLVolume> _iblVolumes;
|
||||||
int _activeIBLVolume{-1};
|
int _activeIBLVolume{-1};
|
||||||
|
// Pending async IBL request (global or volume). targetVolume = -1 means global.
|
||||||
|
struct PendingIBLRequest
|
||||||
|
{
|
||||||
|
bool active{false};
|
||||||
|
int targetVolume{-1};
|
||||||
|
IBLPaths paths{};
|
||||||
|
} _pendingIBLRequest;
|
||||||
|
|
||||||
struct PickInfo
|
struct PickInfo
|
||||||
{
|
{
|
||||||
@@ -206,6 +213,20 @@ public:
|
|||||||
const glm::mat4 &transform = glm::mat4(1.f),
|
const glm::mat4 &transform = glm::mat4(1.f),
|
||||||
bool preloadTextures = false);
|
bool preloadTextures = false);
|
||||||
|
|
||||||
|
// Spawn a runtime primitive mesh instance (cube/sphere/plane/capsule).
|
||||||
|
// - instanceName is the unique key for this object in SceneManager.
|
||||||
|
// - geomType selects which analytic primitive to build.
|
||||||
|
// - material controls whether the primitive uses the default PBR material
|
||||||
|
// or a textured material (see AssetManager::MeshMaterialDesc).
|
||||||
|
// - boundsTypeOverride can force a specific bounds type for picking.
|
||||||
|
// The underlying mesh is cached in AssetManager using a per-primitive name,
|
||||||
|
// so multiple instances share GPU buffers.
|
||||||
|
bool addPrimitiveInstance(const std::string &instanceName,
|
||||||
|
AssetManager::MeshGeometryDesc::Type geomType,
|
||||||
|
const glm::mat4 &transform = glm::mat4(1.f),
|
||||||
|
const AssetManager::MeshMaterialDesc &material = {},
|
||||||
|
std::optional<BoundsType> boundsTypeOverride = {});
|
||||||
|
|
||||||
// Asynchronous glTF load that reports progress via AsyncAssetLoader.
|
// Asynchronous glTF load that reports progress via AsyncAssetLoader.
|
||||||
// Returns a JobID that can be queried via AsyncAssetLoader.
|
// Returns a JobID that can be queried via AsyncAssetLoader.
|
||||||
// If preloadTextures is true, textures will be immediately marked for loading to VRAM.
|
// If preloadTextures is true, textures will be immediately marked for loading to VRAM.
|
||||||
|
|||||||
@@ -242,19 +242,27 @@ namespace
|
|||||||
{
|
{
|
||||||
if (eng->_iblManager && vol.enabled)
|
if (eng->_iblManager && vol.enabled)
|
||||||
{
|
{
|
||||||
eng->_iblManager->load(vol.paths);
|
if (eng->_iblManager->load_async(vol.paths))
|
||||||
eng->_activeIBLVolume = static_cast<int>(i);
|
{
|
||||||
|
eng->_pendingIBLRequest.active = true;
|
||||||
|
eng->_pendingIBLRequest.targetVolume = static_cast<int>(i);
|
||||||
|
eng->_pendingIBLRequest.paths = vol.paths;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Set As Global IBL"))
|
if (ImGui::Button("Set As Global IBL"))
|
||||||
{
|
{
|
||||||
eng->_globalIBLPaths = vol.paths;
|
eng->_globalIBLPaths = vol.paths;
|
||||||
eng->_hasGlobalIBL = true;
|
|
||||||
eng->_activeIBLVolume = -1;
|
|
||||||
if (eng->_iblManager)
|
if (eng->_iblManager)
|
||||||
{
|
{
|
||||||
eng->_iblManager->load(eng->_globalIBLPaths);
|
if (eng->_iblManager->load_async(eng->_globalIBLPaths))
|
||||||
|
{
|
||||||
|
eng->_pendingIBLRequest.active = true;
|
||||||
|
eng->_pendingIBLRequest.targetVolume = -1;
|
||||||
|
eng->_pendingIBLRequest.paths = eng->_globalIBLPaths;
|
||||||
|
eng->_hasGlobalIBL = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user