mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-28 23:03:21 +01:00
Frame Studio - Gaussian splats (PLY, SPZ v2-v4), PLY vertex colours, panorama skies - Copy, cut, paste and duplicate for objects, splats and world items - Walkable-area rebake moved off the UI thread; viewport one frame sooner; refresh-locked pacing (FS_PACE=timer for the old one) - Fixed: bundle thumbnail use-after-free crash, auto-save deleting environments, saves into scratch copies, edits lost on non-ASCII paths, unopenable manifests with inf/nan, bundle save data loss, opening a home marking it changed, gizmo NaN, long paths, many malformed-file crashes - Updater verifies sha256, journals the swap and never leaves the exe missing Frame Home - Seats turn you to face their way - Draw batching (Hand Tutorials up to ~20% less GPU time), cut-out EQUAL pass, late head-pose read before submit - Proper queue-family hand-over to the compositor layer; layer descriptor generations and seqlocked slots; structural SPIR-V check - Fixed: hangs/crashes on damaged .fenv files and shaders, render-pass validation error, sync hazard in the foveation smoothing pass, unverified update downloads, half-done installs Tests: tests_loco/env_share/spv_check wired into CMake (FRAME_TESTS) and CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsaXGU27gsrXjDoFtyFJP9
4769 lines
278 KiB
C++
4769 lines
278 KiB
C++
#include <atomic>
|
|
#include "hsl_renderer.h"
|
|
#include "selection_spirv.h"
|
|
#include "guide_spirv.h"
|
|
#include "debug_spirv.h"
|
|
#include <unordered_set>
|
|
#include <set>
|
|
|
|
#include "loaders/rendtxtr_parser.h"
|
|
#include "core/parallel.h" // par::forEach: animated meshes rebuilt across cores
|
|
#include "fov_spirv.h" // the smooth-foveation pass
|
|
#include "shader_bake.h" // prepared and specialized shaders (made at bake, or here for APK homes)
|
|
#include "spv_opt.h" // the SPIR-V optimizer over every program's shaders
|
|
#include "spv_check.h" // SPIR-V from a .fenv checked before the driver sees it
|
|
#include "tex_cache.h" // mip chains for textures shipped without one, built once
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h> // SetThreadPriority for the decode workers
|
|
#else
|
|
#include <sys/resource.h>
|
|
#include <sys/syscall.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
|
|
#define LOG(...) do { std::fprintf(stderr, "[render] " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
|
|
#define VK_OK(x) ([&]() { VkResult r_ = (x); if (r_ != VK_SUCCESS) { LOG("%s failed: %d", #x, r_); return false; } return true; }())
|
|
|
|
namespace {
|
|
|
|
// SPIR-V a home brought (its .fenv, or made from it) fit to hand the driver: spv_check.h. A malformed module is said
|
|
// and its meshes skipped, as a module the driver refuses is.
|
|
bool shaderOk(const std::vector<uint32_t>& code, const std::string& program) {
|
|
std::string why;
|
|
if (spvcheck::check(code, &why)) return true;
|
|
LOG("shader of %s is malformed SPIR-V (%s): not handed to the driver", program.c_str(), why.c_str());
|
|
return false;
|
|
}
|
|
|
|
std::string lower(std::string s) {
|
|
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return s;
|
|
}
|
|
bool has(const std::string& s, const char* sub) { return s.find(sub) != std::string::npos; }
|
|
|
|
float fromHalf(uint16_t h) {
|
|
const uint32_t sign = (h & 0x8000u) << 16, exp = (h >> 10) & 0x1Fu, man = h & 0x3FFu;
|
|
uint32_t bits;
|
|
if (exp == 0) {
|
|
if (man == 0) bits = sign;
|
|
else { // subnormal
|
|
int e = -1;
|
|
uint32_t m = man;
|
|
do { m <<= 1; ++e; } while (!(m & 0x400u));
|
|
bits = sign | static_cast<uint32_t>(127 - 15 - e) << 23 | (m & 0x3FFu) << 13;
|
|
}
|
|
} else if (exp == 31) bits = sign | 0x7F800000u | man << 13;
|
|
else bits = sign | (exp + 112) << 23 | man << 13;
|
|
float f;
|
|
std::memcpy(&f, &bits, 4);
|
|
return f;
|
|
}
|
|
|
|
// Largest texture side kept on a GPU without native ASTC (the PC editor). HSR Studio likewise keeps most textures
|
|
// at 512-2048 and full 4K only where it fills the screen; 4K RGBA after ASTC decode pushed VRAM into paging.
|
|
// FH_MAX_TEX overrides (0 = no cap).
|
|
std::atomic<uint32_t> g_textureCap{0xFFFFFFFEu}; // unset: FH_MAX_TEX or 2048 (setTextureCap, below)
|
|
// Halves an RGBA8 image (2x2 box; an odd last row / column folds into its neighbour).
|
|
void halveRGBA(std::vector<u8>& px, u32& w, u32& h) {
|
|
const u32 nw = std::max(1u, w / 2), nh = std::max(1u, h / 2);
|
|
std::vector<u8> out(static_cast<size_t>(nw) * nh * 4);
|
|
for (u32 y = 0; y < nh; ++y)
|
|
for (u32 x = 0; x < nw; ++x) {
|
|
const u32 x0 = std::min(w - 1, x * 2), x1 = std::min(w - 1, x * 2 + 1), y0 = std::min(h - 1, y * 2), y1 = std::min(h - 1, y * 2 + 1);
|
|
for (int c = 0; c < 4; ++c)
|
|
out[(static_cast<size_t>(y) * nw + x) * 4 + c] = static_cast<u8>(
|
|
(px[(static_cast<size_t>(y0) * w + x0) * 4 + c] + px[(static_cast<size_t>(y0) * w + x1) * 4 + c] +
|
|
px[(static_cast<size_t>(y1) * w + x0) * 4 + c] + px[(static_cast<size_t>(y1) * w + x1) * 4 + c] + 2) / 4);
|
|
}
|
|
px.swap(out);
|
|
w = nw;
|
|
h = nh;
|
|
}
|
|
|
|
uint32_t maxDecodedSide() {
|
|
static const uint32_t envCap = [] {
|
|
const char* e = std::getenv("FH_MAX_TEX");
|
|
return e ? static_cast<uint32_t>(std::atoi(e)) : 2048u;
|
|
}();
|
|
const uint32_t set = g_textureCap.load(), cap = set == 0xFFFFFFFEu ? envCap : set;
|
|
return cap ? cap : 0xFFFFFFFFu;
|
|
}
|
|
|
|
uint16_t toHalf(float f) {
|
|
uint32_t x; std::memcpy(&x, &f, 4);
|
|
uint32_t sign = (x >> 16) & 0x8000, exp = (x >> 23) & 0xFF, mant = x & 0x7FFFFF;
|
|
if (exp == 0xFF) return static_cast<uint16_t>(sign | 0x7C00 | (mant ? 0x200 : 0));
|
|
int e = static_cast<int>(exp) - 127 + 15;
|
|
if (e >= 31) return static_cast<uint16_t>(sign | 0x7C00);
|
|
if (e <= 0) return static_cast<uint16_t>(sign);
|
|
return static_cast<uint16_t>(sign | (e << 10) | (mant >> 13));
|
|
}
|
|
|
|
VkDescriptorType descriptorType(spv::ResKind k) {
|
|
switch (k) {
|
|
case spv::ResKind::UniformBuffer: return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
case spv::ResKind::StorageBuffer: return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
|
case spv::ResKind::Sampler: return VK_DESCRIPTOR_TYPE_SAMPLER;
|
|
case spv::ResKind::SampledImage: return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
|
|
case spv::ResKind::CombinedImageSampler: return VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
case spv::ResKind::StorageImage: return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
|
}
|
|
return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
}
|
|
|
|
// FH_DEBUG=1 prints the bindings of the first meshes; FH_DEBUG=<name part> those of matching meshes.
|
|
static std::atomic<int> g_allocCount{0}; // live-ish vkAllocateMemory count (diagnostics)
|
|
|
|
bool debugMesh(const MeshData& md) {
|
|
static const char* d = std::getenv("FH_DEBUG");
|
|
static int shown = 0;
|
|
if (!d) return false;
|
|
if (std::string(d) == "1") return shown++ < 6;
|
|
for (std::string rest = d; !rest.empty();) { // comma-separated name parts
|
|
const size_t c = rest.find(',');
|
|
const std::string part = rest.substr(0, c);
|
|
if (!part.empty() && md.name.find(part) != std::string::npos) return true;
|
|
rest = c == std::string::npos ? std::string() : rest.substr(c + 1);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isBuffer(spv::ResKind k) { return k == spv::ResKind::UniformBuffer || k == spv::ResKind::StorageBuffer; }
|
|
|
|
// FH_SKY_FIRST=1: the old sky order (first, no depth test) for comparisons.
|
|
bool skyFirst() { static const bool v = std::getenv("FH_SKY_FIRST") != nullptr; return v; }
|
|
|
|
} // namespace
|
|
|
|
// ── Vulkan plumbing ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
uint32_t HslRenderer::memoryType(uint32_t bits, VkMemoryPropertyFlags flags) {
|
|
VkPhysicalDeviceMemoryProperties mp;
|
|
vkGetPhysicalDeviceMemoryProperties(physical_, &mp);
|
|
for (uint32_t i = 0; i < mp.memoryTypeCount; ++i)
|
|
if ((bits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & flags) == flags) return i;
|
|
return 0;
|
|
}
|
|
|
|
// Staging / uniform memory: plain system memory, never the small device-local BAR heap (filling that with
|
|
// texture staging made the driver evict and stall).
|
|
uint32_t HslRenderer::hostMemoryType(uint32_t bits) {
|
|
VkPhysicalDeviceMemoryProperties mp;
|
|
vkGetPhysicalDeviceMemoryProperties(physical_, &mp);
|
|
const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
|
for (uint32_t i = 0; i < mp.memoryTypeCount; ++i)
|
|
if ((bits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want &&
|
|
!(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT))
|
|
return i;
|
|
return memoryType(bits, want);
|
|
}
|
|
|
|
HslRenderer::MemRange HslRenderer::allocMemory(const VkMemoryRequirements& req, uint32_t type, bool linear, bool map) {
|
|
static constexpr VkDeviceSize kBlock = 256ull << 20, kDedicated = 128ull << 20;
|
|
MemRange r;
|
|
if (req.size >= kDedicated) { // big render targets / 4K textures: their own allocation
|
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
|
ai.allocationSize = req.size;
|
|
ai.memoryTypeIndex = type;
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
if (vkAllocateMemory(device_, &ai, nullptr, &r.mem) != VK_SUCCESS) return MemRange{};
|
|
LOG("dedicated %.1f MB: %.1f ms", req.size / 1048576.0, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
|
|
++g_allocCount;
|
|
r.size = req.size;
|
|
if (map) vkMapMemory(device_, r.mem, 0, VK_WHOLE_SIZE, 0, &r.mapped);
|
|
return r;
|
|
}
|
|
const VkDeviceSize align = std::max<VkDeviceSize>(req.alignment, 16);
|
|
std::lock_guard<std::mutex> l(allocMutex_);
|
|
auto take = [&](MemBlock& b) -> bool {
|
|
for (auto it = b.free.begin(); it != b.free.end(); ++it) {
|
|
const VkDeviceSize start = it->first, len = it->second;
|
|
const VkDeviceSize at = (start + align - 1) / align * align;
|
|
if (at + req.size > start + len) continue;
|
|
b.free.erase(it);
|
|
if (at > start) b.free[start] = at - start;
|
|
if (at + req.size < start + len) b.free[at + req.size] = start + len - (at + req.size);
|
|
r.mem = b.mem;
|
|
r.offset = at;
|
|
r.size = req.size;
|
|
r.mapped = b.mapped ? static_cast<u8*>(b.mapped) + at : nullptr;
|
|
r.pooled = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
for (MemBlock& b : blocks_)
|
|
if (b.type == type && b.linear == linear && (b.mapped != nullptr) == map && take(b)) return r;
|
|
MemBlock nb;
|
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
|
ai.allocationSize = kBlock;
|
|
ai.memoryTypeIndex = type;
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
if (vkAllocateMemory(device_, &ai, nullptr, &nb.mem) != VK_SUCCESS) return MemRange{};
|
|
LOG("memory block %zu: type %u %s, %.1f ms", blocks_.size(), type, linear ? "buffers" : "images",
|
|
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
|
|
++g_allocCount;
|
|
nb.size = kBlock;
|
|
nb.type = type;
|
|
nb.linear = linear;
|
|
if (map) vkMapMemory(device_, nb.mem, 0, VK_WHOLE_SIZE, 0, &nb.mapped);
|
|
nb.free[0] = kBlock;
|
|
blocks_.push_back(std::move(nb));
|
|
take(blocks_.back());
|
|
return r;
|
|
}
|
|
|
|
void HslRenderer::freeMemory(VkDeviceMemory mem, VkDeviceSize offset, VkDeviceSize size, bool pooled, bool mapped) {
|
|
if (!mem) return;
|
|
if (!pooled) {
|
|
if (mapped) vkUnmapMemory(device_, mem);
|
|
vkFreeMemory(device_, mem, nullptr);
|
|
--g_allocCount;
|
|
return;
|
|
}
|
|
std::lock_guard<std::mutex> l(allocMutex_);
|
|
for (MemBlock& b : blocks_) {
|
|
if (b.mem != mem) continue;
|
|
auto next = b.free.lower_bound(offset);
|
|
if (next != b.free.end() && offset + size == next->first) { // merge with the range after
|
|
size += next->second;
|
|
next = b.free.erase(next);
|
|
}
|
|
if (next != b.free.begin()) { // and the range before
|
|
auto prev = std::prev(next);
|
|
if (prev->first + prev->second == offset) { prev->second += size; return; }
|
|
}
|
|
b.free[offset] = size;
|
|
return;
|
|
}
|
|
}
|
|
|
|
HslRenderer::Buffer HslRenderer::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, bool hostVisible) {
|
|
Buffer b;
|
|
b.size = size;
|
|
VkBufferCreateInfo ci = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
|
ci.size = size;
|
|
ci.usage = usage;
|
|
// Failure (out of memory, a size from a damaged file) is an empty Buffer{}: callers skip what needed it.
|
|
if (!size || vkCreateBuffer(device_, &ci, nullptr, &b.buf) != VK_SUCCESS) { LOG("buffer of %llu bytes refused", static_cast<unsigned long long>(size)); return Buffer{}; }
|
|
VkMemoryRequirements req;
|
|
vkGetBufferMemoryRequirements(device_, b.buf, &req);
|
|
const uint32_t type = hostVisible ? hostMemoryType(req.memoryTypeBits)
|
|
: memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
const MemRange m = allocMemory(req, type, true, hostVisible);
|
|
b.mem = m.mem;
|
|
b.memOffset = m.offset;
|
|
b.memSize = m.size;
|
|
b.pooled = m.pooled;
|
|
if (hostVisible) b.mapped = m.mapped;
|
|
if (!b.mem || (hostVisible && !b.mapped) || vkBindBufferMemory(device_, b.buf, b.mem, b.memOffset) != VK_SUCCESS) {
|
|
LOG("buffer of %llu bytes: no memory", static_cast<unsigned long long>(size));
|
|
destroy(b);
|
|
return Buffer{};
|
|
}
|
|
if (hostVisible) std::memset(b.mapped, 0, static_cast<size_t>(size));
|
|
return b;
|
|
}
|
|
|
|
HslRenderer::Buffer HslRenderer::createDeviceBuffer(const void* data, VkDeviceSize size, VkBufferUsageFlags usage) {
|
|
Buffer staging = createBuffer(size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
if (!staging.buf) return Buffer{};
|
|
std::memcpy(staging.mapped, data, static_cast<size_t>(size));
|
|
Buffer b = createBuffer(size, usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, false);
|
|
if (!b.buf) { destroy(staging); return Buffer{}; }
|
|
oneShot([&](VkCommandBuffer c) {
|
|
VkBufferCopy region = {0, 0, size};
|
|
vkCmdCopyBuffer(c, staging.buf, b.buf, 1, ®ion);
|
|
});
|
|
retire(staging);
|
|
return b;
|
|
}
|
|
|
|
void HslRenderer::destroy(Buffer& b) {
|
|
if (b.buf) vkDestroyBuffer(device_, b.buf, nullptr);
|
|
freeMemory(b.mem, b.memOffset, b.memSize, b.pooled, b.mapped != nullptr);
|
|
b = Buffer{};
|
|
}
|
|
|
|
// A staging buffer an upload still reads: freed now, or (recorded into the frame) once that frame is done.
|
|
void HslRenderer::retire(Buffer& b) {
|
|
if (recordInto_ && recordInto_ == loadCmd_) { loadStaging_.push_back(b); loadStaged_ += b.size; }
|
|
else if (recordInto_) frameStaging_.push_back(b);
|
|
else destroy(b);
|
|
b = Buffer{};
|
|
}
|
|
|
|
// Load-time batching: every upload of the meshes a loadStep brings in is recorded into one command buffer and
|
|
// submitted once (each mesh's buffers and textures used to be a submit + queue-idle wait of their own: thousands
|
|
// of GPU round trips per home).
|
|
void HslRenderer::beginUploadBatch() {
|
|
if (loadCmd_ || recordInto_) return; // already batching, or inside a frame (uploads ride in it)
|
|
VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
|
ai.commandPool = pool_;
|
|
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
ai.commandBufferCount = 1;
|
|
if (vkAllocateCommandBuffers(device_, &ai, &loadCmd_) != VK_SUCCESS) { loadCmd_ = VK_NULL_HANDLE; return; }
|
|
VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
vkBeginCommandBuffer(loadCmd_, &bi);
|
|
recordInto_ = loadCmd_;
|
|
loadStaged_ = 0;
|
|
}
|
|
|
|
void HslRenderer::flushUploadBatch() {
|
|
if (!loadCmd_) return;
|
|
// Buffer copies carry no barrier of their own: make every transfer write visible to whatever reads it next.
|
|
VkMemoryBarrier mb = {VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
mb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
mb.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT |
|
|
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT;
|
|
vkCmdPipelineBarrier(loadCmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr);
|
|
vkEndCommandBuffer(loadCmd_);
|
|
VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
si.commandBufferCount = 1;
|
|
si.pCommandBuffers = &loadCmd_;
|
|
vkQueueSubmit(queue_, 1, &si, VK_NULL_HANDLE);
|
|
vkQueueWaitIdle(queue_);
|
|
vkFreeCommandBuffers(device_, pool_, 1, &loadCmd_);
|
|
loadCmd_ = VK_NULL_HANDLE;
|
|
recordInto_ = VK_NULL_HANDLE;
|
|
for (Buffer& b : loadStaging_) destroy(b);
|
|
loadStaging_.clear();
|
|
loadStaged_ = 0;
|
|
}
|
|
|
|
void HslRenderer::destroy(Image& i) {
|
|
if (i.view) vkDestroyImageView(device_, i.view, nullptr);
|
|
if (i.image) vkDestroyImage(device_, i.image, nullptr);
|
|
freeMemory(i.mem, i.memOffset, i.memSize, i.pooled, false);
|
|
i = Image{};
|
|
}
|
|
|
|
VkImageView HslRenderer::createPicture(const u8* rgba, uint32_t w, uint32_t h) {
|
|
Image i = createTexture(rgba, w, h, false);
|
|
if (!i.view) return VK_NULL_HANDLE;
|
|
pictures_.push_back(i);
|
|
return i.view;
|
|
}
|
|
|
|
void HslRenderer::releasePicture(VkImageView view) {
|
|
for (size_t k = 0; k < pictures_.size(); ++k)
|
|
if (pictures_[k].view == view) { destroy(pictures_[k]); pictures_.erase(pictures_.begin() + static_cast<std::ptrdiff_t>(k)); return; }
|
|
}
|
|
|
|
template <typename F> void HslRenderer::oneShot(F&& record) {
|
|
if (recordInto_) { // streaming: ride along in the frame's command buffer, no stall
|
|
record(recordInto_);
|
|
return;
|
|
}
|
|
VkCommandBuffer c;
|
|
VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
|
ai.commandPool = pool_;
|
|
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
ai.commandBufferCount = 1;
|
|
vkAllocateCommandBuffers(device_, &ai, &c);
|
|
VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
vkBeginCommandBuffer(c, &bi);
|
|
record(c);
|
|
vkEndCommandBuffer(c);
|
|
VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
si.commandBufferCount = 1;
|
|
si.pCommandBuffers = &c;
|
|
vkQueueSubmit(queue_, 1, &si, VK_NULL_HANDLE);
|
|
vkQueueWaitIdle(queue_);
|
|
vkFreeCommandBuffers(device_, pool_, 1, &c);
|
|
}
|
|
|
|
static void imageBarrier(VkCommandBuffer c, VkImage img, VkImageAspectFlags aspect, uint32_t baseMip, uint32_t mips,
|
|
uint32_t layers, VkImageLayout from, VkImageLayout to, VkAccessFlags srcA, VkAccessFlags dstA,
|
|
VkPipelineStageFlags srcS, VkPipelineStageFlags dstS) {
|
|
VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
b.oldLayout = from;
|
|
b.newLayout = to;
|
|
b.srcAccessMask = srcA;
|
|
b.dstAccessMask = dstA;
|
|
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
b.image = img;
|
|
b.subresourceRange = {aspect, baseMip, mips, 0, layers};
|
|
vkCmdPipelineBarrier(c, srcS, dstS, 0, 0, nullptr, 0, nullptr, 1, &b);
|
|
}
|
|
|
|
// A queue family ownership transfer of a frame shared with the compositor (all its layers, GENERAL throughout): the
|
|
// release half (from our family to VK_QUEUE_FAMILY_EXTERNAL) or the acquire half (the reverse).
|
|
static void ownershipBarrier(VkCommandBuffer c, VkImage img, uint32_t layers, uint32_t fromFamily, uint32_t toFamily,
|
|
VkAccessFlags srcA, VkAccessFlags dstA, VkPipelineStageFlags srcS, VkPipelineStageFlags dstS) {
|
|
VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
b.srcAccessMask = srcA;
|
|
b.dstAccessMask = dstA;
|
|
b.srcQueueFamilyIndex = fromFamily;
|
|
b.dstQueueFamilyIndex = toFamily;
|
|
b.image = img;
|
|
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers};
|
|
vkCmdPipelineBarrier(c, srcS, dstS, 0, 0, nullptr, 0, nullptr, 1, &b);
|
|
}
|
|
|
|
HslRenderer::Image HslRenderer::createImage2D(uint32_t w, uint32_t h, uint32_t layers, uint32_t mips,
|
|
VkSampleCountFlagBits samples, VkFormat fmt, VkImageUsageFlags usage,
|
|
VkImageAspectFlags aspect, VkImageViewType viewType,
|
|
VkImageCreateFlags flags) {
|
|
Image img;
|
|
// Sizes reach here from files: refuse what Vulkan forbids (a zero side, past the GPU's limit, a mip chain longer
|
|
// than the image's, a non-square cube) rather than hand it to the driver. Failure is an empty Image{}.
|
|
const bool cube = viewType == VK_IMAGE_VIEW_TYPE_CUBE;
|
|
const uint32_t side = std::max(w, h), limit = cube ? maxCubeDim_ : maxImageDim_;
|
|
uint32_t chain = 1;
|
|
while (chain < 32 && (side >> chain)) ++chain;
|
|
if (!w || !h || side > limit || !mips || mips > chain || !layers || (cube && (w != h || layers % 6))) {
|
|
LOG("image %ux%u x%u, %u mips refused", w, h, layers, mips);
|
|
return Image{};
|
|
}
|
|
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
ci.flags = flags;
|
|
ci.imageType = viewType == VK_IMAGE_VIEW_TYPE_3D ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D;
|
|
ci.format = fmt;
|
|
ci.extent = {w, h, 1};
|
|
ci.mipLevels = mips;
|
|
ci.arrayLayers = layers;
|
|
ci.samples = samples;
|
|
ci.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
ci.usage = usage;
|
|
if (vkCreateImage(device_, &ci, nullptr, &img.image) != VK_SUCCESS) { LOG("image %ux%u: create failed", w, h); return Image{}; }
|
|
VkMemoryRequirements req;
|
|
vkGetImageMemoryRequirements(device_, img.image, &req);
|
|
uint32_t type = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
bool lazy = false;
|
|
if (usage & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) {
|
|
uint32_t lz = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT);
|
|
VkPhysicalDeviceMemoryProperties mp;
|
|
vkGetPhysicalDeviceMemoryProperties(physical_, &mp);
|
|
if (mp.memoryTypes[lz].propertyFlags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) { type = lz; lazy = true; }
|
|
}
|
|
if (lazy || (usage & (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT))) {
|
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; // render targets: their own memory
|
|
ai.allocationSize = req.size;
|
|
ai.memoryTypeIndex = type;
|
|
if (vkAllocateMemory(device_, &ai, nullptr, &img.mem) == VK_SUCCESS) ++g_allocCount;
|
|
else img.mem = VK_NULL_HANDLE;
|
|
img.memSize = req.size;
|
|
} else {
|
|
const MemRange m = allocMemory(req, type, false, false);
|
|
img.mem = m.mem;
|
|
img.memOffset = m.offset;
|
|
img.memSize = m.size;
|
|
img.pooled = m.pooled;
|
|
}
|
|
if (!img.mem || vkBindImageMemory(device_, img.image, img.mem, img.memOffset) != VK_SUCCESS) {
|
|
LOG("image %ux%u: no memory", w, h);
|
|
destroy(img);
|
|
return Image{};
|
|
}
|
|
VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
vi.image = img.image;
|
|
vi.viewType = viewType;
|
|
vi.format = fmt;
|
|
vi.subresourceRange = {aspect, 0, mips, 0, layers};
|
|
if (vkCreateImageView(device_, &vi, nullptr, &img.view) != VK_SUCCESS) { img.view = VK_NULL_HANDLE; destroy(img); return Image{}; }
|
|
return img;
|
|
}
|
|
|
|
HslRenderer::Image HslRenderer::createTexture(const u8* rgba, uint32_t w, uint32_t h, bool srgb) {
|
|
return createTextureFmt(rgba, w, h, srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM);
|
|
}
|
|
|
|
// Allocation half of a texture upload — safe on any thread (the streaming workers run it): the image and a
|
|
// staging buffer already holding the pixels. RGBA8 or RGBA16F; the mip chain is blitted when recorded.
|
|
HslRenderer::PreparedTex HslRenderer::prepareTexture(const void* pixels, uint32_t w, uint32_t h, VkFormat fmt, bool ownStaging) {
|
|
PreparedTex t;
|
|
t.w = w;
|
|
t.h = h;
|
|
const size_t bpp = fmt == VK_FORMAT_R16G16B16A16_SFLOAT ? 8 : 4;
|
|
if (!w || !h) return t; // (log2(0) below); t.img stays empty: the caller skips it
|
|
VkFormatProperties fp;
|
|
vkGetPhysicalDeviceFormatProperties(physical_, fmt, &fp);
|
|
const bool blit = (fp.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) &&
|
|
(fp.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) &&
|
|
(fp.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT);
|
|
t.mips = blit ? static_cast<uint32_t>(std::floor(std::log2(std::max(w, h)))) + 1 : 1;
|
|
t.img = createImage2D(w, h, 1, t.mips, VK_SAMPLE_COUNT_1_BIT, fmt,
|
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
|
VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_VIEW_TYPE_2D);
|
|
if (!t.img.image) return t;
|
|
if (ownStaging) {
|
|
t.staging = createBuffer(static_cast<VkDeviceSize>(w) * h * bpp, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
if (!t.staging.buf) { destroy(t.img); return t; }
|
|
std::memcpy(t.staging.mapped, pixels, static_cast<size_t>(w) * h * bpp);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// Recording half: copy mip 0, blit the chain, leave every level shader-readable.
|
|
void HslRenderer::recordTextureUpload(VkCommandBuffer c, const PreparedTex& t) {
|
|
const VkImage image = t.img.image;
|
|
const uint32_t mips = t.mips;
|
|
imageBarrier(c, image, VK_IMAGE_ASPECT_COLOR_BIT, 0, mips, 1, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkBufferImageCopy copy = {};
|
|
copy.bufferOffset = t.stagingOffset;
|
|
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
copy.imageExtent = {t.w, t.h, 1};
|
|
vkCmdCopyBufferToImage(c, t.staging.buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
|
int32_t mw = static_cast<int32_t>(t.w), mh = static_cast<int32_t>(t.h);
|
|
for (uint32_t m = 1; m < mips; ++m) {
|
|
imageBarrier(c, image, VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkImageBlit bl = {};
|
|
bl.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 1};
|
|
bl.srcOffsets[1] = {mw, mh, 1};
|
|
mw = std::max(1, mw / 2);
|
|
mh = std::max(1, mh / 2);
|
|
bl.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 1};
|
|
bl.dstOffsets[1] = {mw, mh, 1};
|
|
vkCmdBlitImage(c, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bl,
|
|
VK_FILTER_LINEAR);
|
|
imageBarrier(c, image, VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
}
|
|
imageBarrier(c, image, VK_IMAGE_ASPECT_COLOR_BIT, mips - 1, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT);
|
|
}
|
|
|
|
// RGBA8 or RGBA16F texture with a blit-generated mip chain.
|
|
HslRenderer::Image HslRenderer::createTextureFmt(const void* pixels, uint32_t w, uint32_t h, VkFormat fmt) {
|
|
PreparedTex t = prepareTexture(pixels, w, h, fmt);
|
|
if (!t.img.image) return Image{};
|
|
oneShot([&](VkCommandBuffer c) { recordTextureUpload(c, t); });
|
|
retire(t.staging);
|
|
return t.img;
|
|
}
|
|
|
|
// 1x1-per-face cube: +Y top, -Y bottom, the four sides `side` (a hemisphere light: bright sky, dim ground).
|
|
HslRenderer::Image HslRenderer::createSolidCube(const float top[3], const float side[3], const float bottom[3]) {
|
|
Image img = createImage2D(1, 1, 6, 1, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R16G16B16A16_SFLOAT,
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
VK_IMAGE_VIEW_TYPE_CUBE, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT);
|
|
uint16_t px[6][4];
|
|
const float* faces[6] = {side, side, top, bottom, side, side}; // +X -X +Y -Y +Z -Z
|
|
for (int f = 0; f < 6; ++f) {
|
|
for (int c = 0; c < 3; ++c) px[f][c] = toHalf(faces[f][c]);
|
|
px[f][3] = toHalf(1.0f);
|
|
}
|
|
Buffer staging = createBuffer(sizeof(px), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
std::memcpy(staging.mapped, px, sizeof(px));
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 6, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkBufferImageCopy copies[6] = {};
|
|
for (uint32_t f = 0; f < 6; ++f) {
|
|
copies[f].bufferOffset = f * 8;
|
|
copies[f].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, f, 1};
|
|
copies[f].imageExtent = {1, 1, 1};
|
|
}
|
|
vkCmdCopyBufferToImage(c, staging.buf, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 6, copies);
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 6, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
});
|
|
retire(staging);
|
|
return img;
|
|
}
|
|
|
|
// Cubemap from six faces (+X -X +Y -Y +Z -Z): RGBA8 sRGB, or RGBA16F linear HDR.
|
|
HslRenderer::Image HslRenderer::createCubeTexture(const void* faces, size_t bytes, uint32_t size, VkFormat fmt, uint32_t mips,
|
|
uint32_t levels) {
|
|
// `faces`: mip-major, six faces per level (level m is size>>m on a side). `levels` past `mips`: the rest of the
|
|
// chain blitted from the last given level.
|
|
mips = std::max(1u, mips);
|
|
uint32_t chain = 1; // a file's mip count past the full chain: keep the levels there are
|
|
while (chain < 32 && (size >> chain)) ++chain;
|
|
mips = std::min(mips, chain);
|
|
levels = std::min(std::max(levels, mips), chain);
|
|
const VkDeviceSize bpp = fmt == VK_FORMAT_R16G16B16A16_SFLOAT ? 8 : 4;
|
|
VkDeviceSize total = 0;
|
|
for (uint32_t m = 0; m < mips; ++m) { const VkDeviceSize s = std::max(1u, size >> m); total += s * s * bpp * 6; }
|
|
if (!faces || !size || total > bytes) { LOG("cube %u^2 (%u mips): %zu bytes, %llu needed", size, mips, bytes, static_cast<unsigned long long>(total)); return Image{}; }
|
|
Image img = createImage2D(size, size, 6, levels, VK_SAMPLE_COUNT_1_BIT, fmt,
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
|
|
(levels > mips ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0),
|
|
VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_VIEW_TYPE_CUBE, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT);
|
|
if (!img.image) return Image{};
|
|
Buffer staging = createBuffer(total, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
if (!staging.buf) { destroy(img); return Image{}; }
|
|
std::memcpy(staging.mapped, faces, static_cast<size_t>(total));
|
|
std::vector<VkBufferImageCopy> copies;
|
|
VkDeviceSize off = 0;
|
|
for (uint32_t m = 0; m < mips; ++m) {
|
|
const uint32_t s = std::max(1u, size >> m);
|
|
for (uint32_t f = 0; f < 6; ++f) {
|
|
VkBufferImageCopy c = {};
|
|
c.bufferOffset = off;
|
|
c.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, m, f, 1};
|
|
c.imageExtent = {s, s, 1};
|
|
copies.push_back(c);
|
|
off += static_cast<VkDeviceSize>(s) * s * bpp;
|
|
}
|
|
}
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, levels, 6, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
vkCmdCopyBufferToImage(c, staging.buf, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
static_cast<uint32_t>(copies.size()), copies.data());
|
|
for (uint32_t m = mips; m < levels; ++m) { // each face of level m from level m-1
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 1, 6, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
const int32_t from = static_cast<int32_t>(std::max(1u, size >> (m - 1))), to = static_cast<int32_t>(std::max(1u, size >> m));
|
|
VkImageBlit bl = {};
|
|
bl.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6};
|
|
bl.srcOffsets[1] = {from, from, 1};
|
|
bl.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6};
|
|
bl.dstOffsets[1] = {to, to, 1};
|
|
vkCmdBlitImage(c, img.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bl,
|
|
VK_FILTER_LINEAR);
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 1, 6, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
}
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, levels, 6, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
});
|
|
retire(staging);
|
|
return img;
|
|
}
|
|
|
|
HslRenderer::Image HslRenderer::createSolid3D(const u8 rgba[4]) {
|
|
Image img = createImage2D(1, 1, 1, 1, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_UNORM,
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
VK_IMAGE_VIEW_TYPE_3D);
|
|
Buffer staging = createBuffer(4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
std::memcpy(staging.mapped, rgba, 4);
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkBufferImageCopy copy = {};
|
|
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
copy.imageExtent = {1, 1, 1};
|
|
vkCmdCopyBufferToImage(c, staging.buf, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
});
|
|
retire(staging);
|
|
return img;
|
|
}
|
|
|
|
// 1x1 depth image cleared to 1.0, sampled through a compare-ALWAYS sampler: every shadow test passes (lit).
|
|
HslRenderer::Image HslRenderer::createShadowDepth() {
|
|
Image img = createImage2D(1, 1, 1, 1, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_D32_SFLOAT,
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT,
|
|
VK_IMAGE_VIEW_TYPE_2D);
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkClearDepthStencilValue one = {1.0f, 0};
|
|
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
|
vkCmdClearDepthStencilImage(c, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &one, 1, &range);
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
});
|
|
return img;
|
|
}
|
|
|
|
// ── Device ───────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
bool HslRenderer::init(uint32_t eyeWidth, uint32_t eyeHeight) {
|
|
width_ = eyeWidth;
|
|
height_ = eyeHeight;
|
|
|
|
std::vector<const char*> iext;
|
|
for (auto& e : instanceExtensions) iext.push_back(e.c_str());
|
|
// Vulkan through a translation layer (MoltenVK on macOS) lists its GPU only to apps that ask for "portability"
|
|
// devices. Drivers without the extension (Windows, Linux, the Frame) don't need it.
|
|
bool portability = false;
|
|
{
|
|
uint32_t n = 0;
|
|
vkEnumerateInstanceExtensionProperties(nullptr, &n, nullptr);
|
|
std::vector<VkExtensionProperties> have(n);
|
|
vkEnumerateInstanceExtensionProperties(nullptr, &n, have.data());
|
|
for (const VkExtensionProperties& e : have) portability |= !std::strcmp(e.extensionName, "VK_KHR_portability_enumeration");
|
|
const bool asked = std::any_of(iext.begin(), iext.end(), [](const char* e) { return !std::strcmp(e, "VK_KHR_portability_enumeration"); });
|
|
if (portability && !asked) iext.push_back("VK_KHR_portability_enumeration"); // (the window may have asked already)
|
|
}
|
|
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
|
app.pApplicationName = "Frame Home";
|
|
app.apiVersion = VK_API_VERSION_1_2;
|
|
VkInstanceCreateInfo ici = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
|
if (portability) ici.flags |= 0x00000001; // VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR
|
|
ici.pApplicationInfo = &app;
|
|
ici.enabledExtensionCount = static_cast<uint32_t>(iext.size());
|
|
ici.ppEnabledExtensionNames = iext.data();
|
|
if (!VK_OK(vkCreateInstance(&ici, nullptr, &instance_))) return false;
|
|
#ifdef FH_USE_VOLK
|
|
volkLoadInstance(instance_);
|
|
#endif
|
|
|
|
physical_ = pickPhysicalDevice ? pickPhysicalDevice(instance_) : VK_NULL_HANDLE;
|
|
if (!physical_) { uint32_t n = 1; vkEnumeratePhysicalDevices(instance_, &n, &physical_); }
|
|
if (!physical_) { LOG("no Vulkan GPU"); return false; }
|
|
VkPhysicalDeviceProperties props;
|
|
vkGetPhysicalDeviceProperties(physical_, &props);
|
|
VkSampleCountFlags ok = props.limits.framebufferColorSampleCounts & props.limits.framebufferDepthSampleCounts;
|
|
samples_ = VK_SAMPLE_COUNT_1_BIT;
|
|
for (VkSampleCountFlagBits c : {VK_SAMPLE_COUNT_8_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_2_BIT})
|
|
if (static_cast<int>(c) <= msaa && (ok & c)) { samples_ = c; break; }
|
|
maxAniso_ = props.limits.maxSamplerAnisotropy;
|
|
maxImageDim_ = props.limits.maxImageDimension2D;
|
|
maxCubeDim_ = props.limits.maxImageDimensionCube;
|
|
|
|
// Meta's shaders use float16/int8/16-bit storage and multiview: enable every core feature the GPU has.
|
|
VkPhysicalDeviceVulkan12Features f12 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
|
|
VkPhysicalDeviceVulkan11Features f11 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES};
|
|
f11.pNext = &f12;
|
|
// HDR ASTC (the lightmaps and panoramas, kept compressed): an extension on Vulkan 1.2.
|
|
VkPhysicalDeviceTextureCompressionASTCHDRFeaturesEXT fHdr = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TEXTURE_COMPRESSION_ASTC_HDR_FEATURES_EXT};
|
|
bool hasHdrExt = false, hasFdm = false, hasFdmOffset = false;
|
|
VkPhysicalDeviceFragmentDensityMapFeaturesEXT fFdm = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT};
|
|
VkPhysicalDeviceFragmentDensityMapOffsetFeaturesQCOM fFdmOff = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_OFFSET_FEATURES_QCOM};
|
|
{
|
|
uint32_t n = 0;
|
|
vkEnumerateDeviceExtensionProperties(physical_, nullptr, &n, nullptr);
|
|
std::vector<VkExtensionProperties> exts(n);
|
|
vkEnumerateDeviceExtensionProperties(physical_, nullptr, &n, exts.data());
|
|
for (const auto& e : exts) {
|
|
hasHdrExt |= std::strcmp(e.extensionName, VK_EXT_TEXTURE_COMPRESSION_ASTC_HDR_EXTENSION_NAME) == 0;
|
|
hasFdm |= std::strcmp(e.extensionName, VK_EXT_FRAGMENT_DENSITY_MAP_EXTENSION_NAME) == 0;
|
|
hasFdmOffset |= std::strcmp(e.extensionName, VK_QCOM_FRAGMENT_DENSITY_MAP_OFFSET_EXTENSION_NAME) == 0;
|
|
}
|
|
}
|
|
// The optional features asked about, chained after the 1.2 ones (only those the device has extensions for).
|
|
auto chainQuery = [&]() {
|
|
void** next = &f12.pNext;
|
|
*next = nullptr;
|
|
if (hasHdrExt) { *next = &fHdr; next = &fHdr.pNext; *next = nullptr; }
|
|
if (foveation && hasFdm) { *next = &fFdm; next = &fFdm.pNext; *next = nullptr; }
|
|
if (foveation && hasFdm && hasFdmOffset) { *next = &fFdmOff; next = &fFdmOff.pNext; *next = nullptr; }
|
|
};
|
|
chainQuery();
|
|
VkPhysicalDeviceFeatures2 f2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
|
|
f2.pNext = &f11;
|
|
vkGetPhysicalDeviceFeatures2(physical_, &f2);
|
|
anisotropy_ = f2.features.samplerAnisotropy == VK_TRUE;
|
|
wireOk_ = f2.features.fillModeNonSolid == VK_TRUE; // (every feature the device has is enabled with f2)
|
|
// Tile-based GPUs (Qualcomm, Arm, Imagination, Apple) shade after all draws are binned: a timestamp around one
|
|
// draw sees only its binning, so the Cost view measures them by drawing a mesh twice instead.
|
|
tiler_ = props.vendorID == 0x5143 || props.vendorID == 0x13B5 || props.vendorID == 0x1010 || props.vendorID == 0x106B;
|
|
astcLdr_ = f2.features.textureCompressionASTC_LDR == VK_TRUE && !(std::getenv("FH_NO_ASTC") && std::string(std::getenv("FH_NO_ASTC")) == "1");
|
|
astcHdr_ = astcLdr_ && hasHdrExt && fHdr.textureCompressionASTC_HDR == VK_TRUE;
|
|
// Rendering straight into the compositor's images (exportDirect), the map itself moves with the gaze (rewritten each
|
|
// frame): the images then need no density-map-offset flag, which the compositor's device couldn't import.
|
|
fdmOffset_ = !exportDirect && hasFdmOffset && fFdmOff.fragmentDensityMapOffset;
|
|
fdmOn_ = foveation && hasFdm && fFdm.fragmentDensityMap && fFdm.fragmentDensityMapNonSubsampledImages && (exportDirect || fdmOffset_);
|
|
if (!fdmOn_) fdmOffset_ = false;
|
|
if (fdmOn_) { // its texel size and how finely the map can be moved
|
|
VkPhysicalDeviceFragmentDensityMapPropertiesEXT pf = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_PROPERTIES_EXT};
|
|
VkPhysicalDeviceFragmentDensityMapOffsetPropertiesQCOM po = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_OFFSET_PROPERTIES_QCOM};
|
|
pf.pNext = &po;
|
|
VkPhysicalDeviceProperties2 p2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
|
p2.pNext = &pf;
|
|
vkGetPhysicalDeviceProperties2(physical_, &p2);
|
|
fdmTexel_ = std::max(32u, pf.minFragmentDensityTexelSize.width);
|
|
// A density map format the driver takes (Turnip: only 16- or 32-bit float pairs, not R8G8_UNORM).
|
|
fdmFormat_ = VK_FORMAT_UNDEFINED;
|
|
for (VkFormat f : {VK_FORMAT_R8G8_UNORM, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R32G32_SFLOAT}) {
|
|
VkFormatProperties fp;
|
|
vkGetPhysicalDeviceFormatProperties(physical_, f, &fp);
|
|
if (fp.optimalTilingFeatures & VK_FORMAT_FEATURE_FRAGMENT_DENSITY_MAP_BIT_EXT) { fdmFormat_ = f; break; }
|
|
}
|
|
if (fdmFormat_ == VK_FORMAT_UNDEFINED) fdmOn_ = false;
|
|
fdmGranularity_ = std::max(1u, po.fragmentDensityOffsetGranularity.width);
|
|
fdmGranularityY_ = std::max(1u, po.fragmentDensityOffsetGranularity.height);
|
|
fFdm.fragmentDensityMapDynamic = VK_FALSE;
|
|
fFdm.fragmentDensityMapNonSubsampledImages = VK_TRUE;
|
|
}
|
|
// What gets enabled: only the optional features in use.
|
|
hasHdrExt = astcHdr_;
|
|
if (!fdmOn_) foveation = false;
|
|
hasFdmOffset = fdmOffset_; // (its features only chained when its extension is enabled)
|
|
chainQuery();
|
|
if (!f11.multiview) LOG("warning: GPU lacks multiview");
|
|
LOG("GPU %s, eye %ux%u, %ux MSAA, float16=%d int8=%d, ASTC textures %s", props.deviceName, width_, height_,
|
|
static_cast<unsigned>(samples_), f12.shaderFloat16, f12.shaderInt8, astcHdr_ ? "native (HDR too)" : astcLdr_ ? "native" : "decoded in the background");
|
|
if (fdmOn_ && fdmOffset_) LOG("foveated rendering on (density texel %u px, offsets in steps of %u px)", fdmTexel_, fdmGranularity_);
|
|
else if (fdmOn_) LOG("foveated rendering on (density texel %u px, the map redrawn where you look each frame)", fdmTexel_);
|
|
|
|
uint32_t nq = 0;
|
|
vkGetPhysicalDeviceQueueFamilyProperties(physical_, &nq, nullptr);
|
|
std::vector<VkQueueFamilyProperties> qf(nq);
|
|
vkGetPhysicalDeviceQueueFamilyProperties(physical_, &nq, qf.data());
|
|
queueFamily_ = UINT32_MAX;
|
|
for (uint32_t i = 0; i < nq; ++i) if (qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { queueFamily_ = i; break; }
|
|
if (queueFamily_ == UINT32_MAX) { LOG("no graphics queue"); return false; }
|
|
|
|
std::vector<const char*> dext;
|
|
for (auto& e : deviceExtensions) dext.push_back(e.c_str());
|
|
{ // a portability device (MoltenVK) must say it knows it is one
|
|
uint32_t n = 0;
|
|
vkEnumerateDeviceExtensionProperties(physical_, nullptr, &n, nullptr);
|
|
std::vector<VkExtensionProperties> have(n);
|
|
vkEnumerateDeviceExtensionProperties(physical_, nullptr, &n, have.data());
|
|
for (const VkExtensionProperties& e : have)
|
|
if (!std::strcmp(e.extensionName, "VK_KHR_portability_subset")) dext.push_back("VK_KHR_portability_subset");
|
|
}
|
|
if (astcHdr_) dext.push_back(VK_EXT_TEXTURE_COMPRESSION_ASTC_HDR_EXTENSION_NAME);
|
|
if (fdmOn_) dext.push_back(VK_EXT_FRAGMENT_DENSITY_MAP_EXTENSION_NAME);
|
|
if (fdmOffset_) dext.push_back(VK_QCOM_FRAGMENT_DENSITY_MAP_OFFSET_EXTENSION_NAME);
|
|
#ifdef _WIN32
|
|
// Windows system mode: import the compositor's Direct3D 11 textures to render the home into them.
|
|
if (importWin32)
|
|
for (const char* e : {"VK_KHR_external_memory", "VK_KHR_external_memory_win32"})
|
|
if (std::none_of(dext.begin(), dext.end(), [&](const char* d) { return !std::strcmp(d, e); })) dext.push_back(e);
|
|
#endif
|
|
// FH_PERF: the GPU's counters, for the bench (see perfCounterAverages).
|
|
std::vector<uint32_t> perfIdx;
|
|
VkPhysicalDevicePerformanceQueryFeaturesKHR perfFeat = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PERFORMANCE_QUERY_FEATURES_KHR};
|
|
if (const char* want = std::getenv("FH_PERF")) {
|
|
auto enumerate = reinterpret_cast<PFN_vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR>(
|
|
vkGetInstanceProcAddr(instance_, "vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR"));
|
|
uint32_t n = 0;
|
|
if (enumerate && enumerate(physical_, queueFamily_, &n, nullptr, nullptr) == VK_SUCCESS && n) {
|
|
std::vector<VkPerformanceCounterKHR> cs(n, {VK_STRUCTURE_TYPE_PERFORMANCE_COUNTER_KHR});
|
|
std::vector<VkPerformanceCounterDescriptionKHR> ds(n, {VK_STRUCTURE_TYPE_PERFORMANCE_COUNTER_DESCRIPTION_KHR});
|
|
enumerate(physical_, queueFamily_, &n, cs.data(), ds.data());
|
|
for (uint32_t i = 0; i < n; ++i) {
|
|
const std::string name = ds[i].name;
|
|
if (std::string(want) == "list") { LOG("perf counter %u: %s (%s)", i, ds[i].name, ds[i].category); continue; }
|
|
for (std::string rest = want; !rest.empty();) {
|
|
const size_t c = rest.find(',');
|
|
const std::string part = rest.substr(0, c);
|
|
if (!part.empty() && name.find(part) != std::string::npos) {
|
|
perfIdx.push_back(i); perfNames_.push_back(name); perfStorage_.push_back(cs[i].storage);
|
|
break;
|
|
}
|
|
rest = c == std::string::npos ? std::string() : rest.substr(c + 1);
|
|
}
|
|
}
|
|
auto passes = reinterpret_cast<PFN_vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR>(
|
|
vkGetInstanceProcAddr(instance_, "vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR"));
|
|
while (!perfIdx.empty() && passes) { // only what one pass can count
|
|
VkQueryPoolPerformanceCreateInfoKHR pc = {VK_STRUCTURE_TYPE_QUERY_POOL_PERFORMANCE_CREATE_INFO_KHR};
|
|
pc.queueFamilyIndex = queueFamily_;
|
|
pc.counterIndexCount = static_cast<uint32_t>(perfIdx.size());
|
|
pc.pCounterIndices = perfIdx.data();
|
|
uint32_t np = 0;
|
|
passes(physical_, &pc, &np);
|
|
if (np <= 1) break;
|
|
LOG("perf: %s left out (more than one pass)", perfNames_.back().c_str());
|
|
perfIdx.pop_back(); perfNames_.pop_back(); perfStorage_.pop_back();
|
|
}
|
|
} else {
|
|
LOG("perf: no counters (on Turnip, TU_DEBUG=perfc)");
|
|
}
|
|
if (!perfIdx.empty()) {
|
|
dext.push_back(VK_KHR_PERFORMANCE_QUERY_EXTENSION_NAME);
|
|
perfFeat.performanceCounterQueryPools = VK_TRUE;
|
|
perfFeat.pNext = f2.pNext;
|
|
f2.pNext = &perfFeat;
|
|
}
|
|
}
|
|
float prio = 1.0f;
|
|
VkDeviceQueueCreateInfo qci = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
|
qci.queueFamilyIndex = queueFamily_;
|
|
qci.queueCount = 1;
|
|
qci.pQueuePriorities = &prio;
|
|
VkDeviceCreateInfo dci = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
|
dci.pNext = &f2;
|
|
dci.queueCreateInfoCount = 1;
|
|
dci.pQueueCreateInfos = &qci;
|
|
dci.enabledExtensionCount = static_cast<uint32_t>(dext.size());
|
|
dci.ppEnabledExtensionNames = dext.data();
|
|
if (!VK_OK(vkCreateDevice(physical_, &dci, nullptr, &device_))) return false;
|
|
vkGetDeviceQueue(device_, queueFamily_, 0, &queue_);
|
|
#ifdef _WIN32
|
|
if (importWin32) { // the adapter this device is on, so the compositor can check it's the one it draws with
|
|
VkPhysicalDeviceIDProperties idp = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
|
|
VkPhysicalDeviceProperties2 pp = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
|
pp.pNext = &idp;
|
|
vkGetPhysicalDeviceProperties2(physical_, &pp);
|
|
if (idp.deviceLUIDValid) std::memcpy(deviceLUID_, idp.deviceLUID, 8);
|
|
}
|
|
#endif
|
|
if (!perfIdx.empty()) {
|
|
VkQueryPoolPerformanceCreateInfoKHR pc = {VK_STRUCTURE_TYPE_QUERY_POOL_PERFORMANCE_CREATE_INFO_KHR};
|
|
pc.queueFamilyIndex = queueFamily_;
|
|
pc.counterIndexCount = static_cast<uint32_t>(perfIdx.size());
|
|
pc.pCounterIndices = perfIdx.data();
|
|
VkQueryPoolCreateInfo qp = {VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
|
|
qp.pNext = &pc;
|
|
qp.queryType = VK_QUERY_TYPE_PERFORMANCE_QUERY_KHR;
|
|
qp.queryCount = 1;
|
|
auto lock = reinterpret_cast<PFN_vkAcquireProfilingLockKHR>(vkGetDeviceProcAddr(device_, "vkAcquireProfilingLockKHR"));
|
|
VkAcquireProfilingLockInfoKHR li = {VK_STRUCTURE_TYPE_ACQUIRE_PROFILING_LOCK_INFO_KHR};
|
|
li.timeout = UINT64_MAX;
|
|
if (lock && lock(device_, &li) == VK_SUCCESS && vkCreateQueryPool(device_, &qp, nullptr, &perfPool_) == VK_SUCCESS) {
|
|
perfSum_.assign(perfNames_.size(), 0.0);
|
|
LOG("perf: counting %zu GPU counters", perfNames_.size());
|
|
} else {
|
|
perfPool_ = VK_NULL_HANDLE;
|
|
LOG("perf: counters unavailable");
|
|
}
|
|
}
|
|
|
|
VkCommandPoolCreateInfo pci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
|
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
|
pci.queueFamilyIndex = queueFamily_;
|
|
vkCreateCommandPool(device_, &pci, nullptr, &pool_);
|
|
VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
|
ai.commandPool = pool_;
|
|
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
ai.commandBufferCount = 2;
|
|
vkAllocateCommandBuffers(device_, &ai, cmds_);
|
|
cmd_ = cmds_[0];
|
|
VkFenceCreateInfo fci = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
|
fci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
|
vkCreateFence(device_, &fci, nullptr, &fence_);
|
|
if (f12.timelineSemaphore) { // enabled with the rest of the 1.2 features
|
|
VkSemaphoreTypeCreateInfo tci = {VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO};
|
|
tci.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
|
|
tci.initialValue = 0;
|
|
VkSemaphoreCreateInfo sci = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
|
sci.pNext = &tci;
|
|
if (vkCreateSemaphore(device_, &sci, nullptr, &timeline_) != VK_SUCCESS) timeline_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
VkDescriptorPoolSize sizes[] = {
|
|
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 16384}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096},
|
|
{VK_DESCRIPTOR_TYPE_SAMPLER, 8192}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 32768},
|
|
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8192}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 256}};
|
|
VkDescriptorPoolCreateInfo dpci = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
|
dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
|
dpci.maxSets = 16384;
|
|
dpci.poolSizeCount = static_cast<uint32_t>(sizeof(sizes) / sizeof(sizes[0]));
|
|
dpci.pPoolSizes = sizes;
|
|
vkCreateDescriptorPool(device_, &dpci, nullptr, &descPool_);
|
|
|
|
{ // Pipeline cache: compiled shader pipelines persist across runs.
|
|
std::vector<char> data;
|
|
if (!pipelineCachePath.empty()) {
|
|
if (FILE* f = texcache::open(std::filesystem::u8path(pipelineCachePath), false)) { // (UTF-8: wide on Windows)
|
|
std::fseek(f, 0, SEEK_END);
|
|
const long n = std::ftell(f);
|
|
std::fseek(f, 0, SEEK_SET);
|
|
if (n > 0) { data.resize(static_cast<size_t>(n)); if (std::fread(data.data(), 1, data.size(), f) != data.size()) data.clear(); }
|
|
std::fclose(f);
|
|
}
|
|
}
|
|
VkPipelineCacheCreateInfo pc = {VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
|
|
pc.initialDataSize = data.size();
|
|
pc.pInitialData = data.empty() ? nullptr : data.data();
|
|
if (vkCreatePipelineCache(device_, &pc, nullptr, &pipelineCache_) != VK_SUCCESS) {
|
|
pc.initialDataSize = 0; // stale / other driver: start empty
|
|
pc.pInitialData = nullptr;
|
|
vkCreatePipelineCache(device_, &pc, nullptr, &pipelineCache_);
|
|
}
|
|
}
|
|
|
|
createTargets();
|
|
createSharedResources();
|
|
createGuidePipelines();
|
|
createSplatPipeline();
|
|
ring_ = createBuffer(kRingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
// Per-draw GPU timestamps: a slow frame names the objects that cost it.
|
|
if (props.limits.timestampComputeAndGraphics && props.limits.timestampPeriod > 0 && !std::getenv("FH_NO_PROF")) {
|
|
timestampPeriod_ = props.limits.timestampPeriod;
|
|
VkQueryPoolCreateInfo qp = {VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
|
|
qp.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
|
qp.queryCount = kMaxQueries;
|
|
vkCreateQueryPool(device_, &qp, nullptr, &queryPool_);
|
|
}
|
|
startWorkers();
|
|
return true;
|
|
}
|
|
|
|
// Multiview stereo target: 2-layer MSAA colour + depth, resolved into the 2-layer image the compositor reads.
|
|
void HslRenderer::createTargets() {
|
|
const bool ms = samples_ != VK_SAMPLE_COUNT_1_BIT;
|
|
const VkFormat color = VK_FORMAT_R8G8B8A8_SRGB;
|
|
// With foveation every attachment of the pass must allow the density map to be moved.
|
|
const VkImageCreateFlags fdmFlag = fdmOffset_ ? VK_IMAGE_CREATE_FRAGMENT_DENSITY_MAP_OFFSET_BIT_QCOM : 0;
|
|
static const bool blocky = std::getenv("FH_BLOCKY_FOV") != nullptr; // A/B: the coarse regions stretched, as before
|
|
smoothFov_ = fdmOn_ && !blocky;
|
|
const VkImageCreateFlags subFlag = smoothFov_ ? VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT : 0;
|
|
if (smoothFov_)
|
|
sub_ = createImage2D(width_, height_, viewCount, 1, VK_SAMPLE_COUNT_1_BIT, color,
|
|
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
VK_IMAGE_VIEW_TYPE_2D_ARRAY, fdmFlag | subFlag);
|
|
resolve_ = createImage2D(width_, height_, viewCount, 1, VK_SAMPLE_COUNT_1_BIT, color,
|
|
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
|
VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | fdmFlag);
|
|
for (uint32_t l = 0; l < viewCount; ++l) {
|
|
VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
vi.image = resolve_.image;
|
|
vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
vi.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, l, 1};
|
|
vkCreateImageView(device_, &vi, nullptr, &layerViews_[l]);
|
|
}
|
|
if (ms) colorMs_ = createImage2D(width_, height_, viewCount, 1, samples_, color,
|
|
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,
|
|
VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_VIEW_TYPE_2D_ARRAY, fdmFlag | subFlag);
|
|
depth_ = createImage2D(width_, height_, viewCount, 1, samples_, VK_FORMAT_D32_SFLOAT,
|
|
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,
|
|
VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_VIEW_TYPE_2D_ARRAY, fdmFlag | subFlag);
|
|
if (fdmOn_) { // offset: twice the eye image each way, so moving it never uncovers an edge; else the eye image's size
|
|
const uint32_t k = fdmOffset_ ? 2 : 1;
|
|
fdmW_ = (k * width_ + fdmTexel_ - 1) / fdmTexel_;
|
|
fdmH_ = (k * height_ + fdmTexel_ - 1) / fdmTexel_;
|
|
fdmKey_ = -1;
|
|
fdmInit_ = false;
|
|
// One layer per view: with multiview the per-view offsets must match the map view's layer count.
|
|
fdm_ = createImage2D(fdmW_, fdmH_, viewCount, 1, VK_SAMPLE_COUNT_1_BIT, fdmFormat_,
|
|
VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
VK_IMAGE_VIEW_TYPE_2D_ARRAY, fdmFlag);
|
|
writeDensityMap();
|
|
}
|
|
|
|
// The pass, built for where its last colour attachment goes: TRANSFER_SRC for resolve_ (copied out, read back), or
|
|
// GENERAL for the compositor's images (exportDirect: the frame lands in them, nothing copied). The two passes differ
|
|
// only in that layout, so they're compatible: the same pipelines draw in both.
|
|
auto makePass = [&](VkImageLayout finalColor, bool external) -> VkRenderPass {
|
|
VkRenderPass made = VK_NULL_HANDLE;
|
|
std::vector<VkAttachmentDescription> att;
|
|
VkAttachmentDescription c = {};
|
|
c.format = color;
|
|
c.samples = samples_;
|
|
c.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
c.storeOp = ms ? VK_ATTACHMENT_STORE_OP_DONT_CARE : VK_ATTACHMENT_STORE_OP_STORE;
|
|
c.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
c.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
c.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
c.finalLayout = ms ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : finalColor;
|
|
att.push_back(c);
|
|
VkAttachmentDescription d = c;
|
|
d.format = VK_FORMAT_D32_SFLOAT;
|
|
d.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
d.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
att.push_back(d);
|
|
if (ms) {
|
|
VkAttachmentDescription r = c;
|
|
r.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
r.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
r.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
r.finalLayout = finalColor;
|
|
att.push_back(r);
|
|
}
|
|
VkAttachmentReference cref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
VkAttachmentReference dref = {1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
|
VkAttachmentReference rref = {2, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
VkSubpassDescription sub = {};
|
|
sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
sub.colorAttachmentCount = 1;
|
|
sub.pColorAttachments = &cref;
|
|
sub.pDepthStencilAttachment = &dref;
|
|
sub.pResolveAttachments = ms ? &rref : nullptr;
|
|
VkSubpassDependency dep = {};
|
|
dep.srcSubpass = 0;
|
|
dep.dstSubpass = VK_SUBPASS_EXTERNAL;
|
|
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
// One dependency for every kind (copied out, read by the smoothing pass, or the compositor's): render passes are
|
|
// compatible only when their dependencies are identical too, and the same pipelines draw in all of them
|
|
// (different ones were VUID-vkCmdDrawIndexed-renderPass-02684 on every export-pass draw).
|
|
(void)external;
|
|
dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
|
dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
|
const uint32_t viewMask = (1u << viewCount) - 1, correlation = viewMask; // both eyes in one pass (VR), or one view (editor)
|
|
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
|
|
mv.subpassCount = 1;
|
|
mv.pViewMasks = &viewMask;
|
|
mv.correlationMaskCount = 1;
|
|
mv.pCorrelationMasks = &correlation;
|
|
VkRenderPassCreateInfo rp = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
|
rp.pNext = &mv;
|
|
rp.attachmentCount = static_cast<uint32_t>(att.size());
|
|
rp.pAttachments = att.data();
|
|
rp.subpassCount = 1;
|
|
rp.pSubpasses = ⊂
|
|
rp.dependencyCount = 1;
|
|
rp.pDependencies = &dep;
|
|
if (fdmOn_) {
|
|
// The same pass, as a RenderPass2 with the density map as its last attachment (the v1 structs can't name it).
|
|
auto a2 = [](const VkAttachmentDescription& a) {
|
|
VkAttachmentDescription2 o = {VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2};
|
|
o.format = a.format; o.samples = a.samples; o.loadOp = a.loadOp; o.storeOp = a.storeOp;
|
|
o.stencilLoadOp = a.stencilLoadOp; o.stencilStoreOp = a.stencilStoreOp; o.initialLayout = a.initialLayout; o.finalLayout = a.finalLayout;
|
|
return o;
|
|
};
|
|
std::vector<VkAttachmentDescription2> att2;
|
|
for (const auto& a : att) att2.push_back(a2(a));
|
|
VkAttachmentDescription2 fd = {VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2};
|
|
fd.format = fdmFormat_;
|
|
fd.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
fd.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
fd.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
fd.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
fd.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
fd.initialLayout = fd.finalLayout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT;
|
|
att2.push_back(fd);
|
|
auto r2 = [](const VkAttachmentReference& r, VkImageAspectFlags aspect) {
|
|
VkAttachmentReference2 o = {VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2};
|
|
o.attachment = r.attachment; o.layout = r.layout; o.aspectMask = aspect;
|
|
return o;
|
|
};
|
|
const VkAttachmentReference2 c2 = r2(cref, VK_IMAGE_ASPECT_COLOR_BIT), d2 = r2(dref, VK_IMAGE_ASPECT_DEPTH_BIT), rs2 = r2(rref, VK_IMAGE_ASPECT_COLOR_BIT);
|
|
VkSubpassDescription2 sub2 = {VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2};
|
|
sub2.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
sub2.viewMask = viewMask;
|
|
sub2.colorAttachmentCount = 1;
|
|
sub2.pColorAttachments = &c2;
|
|
sub2.pDepthStencilAttachment = &d2;
|
|
sub2.pResolveAttachments = ms ? &rs2 : nullptr;
|
|
VkSubpassDependency2 dep2 = {VK_STRUCTURE_TYPE_SUBPASS_DEPENDENCY_2};
|
|
dep2.srcSubpass = dep.srcSubpass; dep2.dstSubpass = dep.dstSubpass;
|
|
dep2.srcStageMask = dep.srcStageMask; dep2.dstStageMask = dep.dstStageMask;
|
|
dep2.srcAccessMask = dep.srcAccessMask; dep2.dstAccessMask = dep.dstAccessMask;
|
|
VkRenderPassFragmentDensityMapCreateInfoEXT fdmInfo = {VK_STRUCTURE_TYPE_RENDER_PASS_FRAGMENT_DENSITY_MAP_CREATE_INFO_EXT};
|
|
fdmInfo.fragmentDensityMapAttachment = {static_cast<uint32_t>(att2.size() - 1), VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT};
|
|
VkRenderPassCreateInfo2 rp2 = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2};
|
|
rp2.pNext = &fdmInfo;
|
|
rp2.attachmentCount = static_cast<uint32_t>(att2.size());
|
|
rp2.pAttachments = att2.data();
|
|
rp2.subpassCount = 1;
|
|
rp2.pSubpasses = &sub2;
|
|
rp2.dependencyCount = 1;
|
|
rp2.pDependencies = &dep2;
|
|
rp2.correlatedViewMaskCount = 1;
|
|
rp2.pCorrelatedViewMasks = &correlation;
|
|
if (vkCreateRenderPass2(device_, &rp2, nullptr, &made) != VK_SUCCESS) { // render without foveation
|
|
LOG("foveated render pass failed: foveation off");
|
|
made = VK_NULL_HANDLE;
|
|
fdmOn_ = false;
|
|
destroy(fdm_);
|
|
}
|
|
}
|
|
if (!made) vkCreateRenderPass(device_, &rp, nullptr, &made);
|
|
return made;
|
|
};
|
|
// (kept across resize(); smooth foveation: the foveated pass ends readable by the full-screen pass, and the frames
|
|
// for the compositor come from that pass)
|
|
if (!renderPass_) renderPass_ = makePass(smoothFov_ ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, false);
|
|
// (the export pass: the foveated pass itself, or with smooth foveation our smoothing pass, unless the compositor
|
|
// smooths as it samples)
|
|
if (exportDirect && !exportPass_)
|
|
exportPass_ = smoothFov_ && !exportsSubsampled() ? colorOnlyPass(VK_IMAGE_LAYOUT_GENERAL) : makePass(VK_IMAGE_LAYOUT_GENERAL, true);
|
|
if (smoothFov_ && !fdmOn_) { // the foveated pass couldn't be made: no subsampled targets either
|
|
smoothFov_ = false;
|
|
destroy(sub_);
|
|
}
|
|
const VkImageView out = smoothFov_ ? sub_.view : resolve_.view;
|
|
std::vector<VkImageView> views = ms ? std::vector<VkImageView>{colorMs_.view, depth_.view, out}
|
|
: std::vector<VkImageView>{out, depth_.view};
|
|
if (fdmOn_) views.push_back(fdm_.view);
|
|
VkFramebufferCreateInfo fb = {VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
fb.renderPass = renderPass_;
|
|
fb.attachmentCount = static_cast<uint32_t>(views.size());
|
|
fb.pAttachments = views.data();
|
|
fb.width = width_;
|
|
fb.height = height_;
|
|
fb.layers = 1;
|
|
vkCreateFramebuffer(device_, &fb, nullptr, &framebuffer_);
|
|
if (smoothFov_) {
|
|
createFovPass();
|
|
VkFramebufferCreateInfo ff = {VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
ff.renderPass = fovPass_;
|
|
ff.attachmentCount = 1;
|
|
ff.pAttachments = &resolve_.view;
|
|
ff.width = width_;
|
|
ff.height = height_;
|
|
ff.layers = 1;
|
|
vkCreateFramebuffer(device_, &ff, nullptr, &fovFb_);
|
|
VkDescriptorImageInfo ii = {VK_NULL_HANDLE, sub_.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
|
VkWriteDescriptorSet w = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
|
w.dstSet = fovSet_;
|
|
w.descriptorCount = 1;
|
|
w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
w.pImageInfo = ⅈ
|
|
if (fovSet_) vkUpdateDescriptorSets(device_, 1, &w, 0, nullptr);
|
|
}
|
|
}
|
|
|
|
// A pass with one single-sample colour attachment over both views (multiview), cleared by nothing (every pixel is
|
|
// written), ending in finalLayout: the smooth-foveation pass's targets.
|
|
VkRenderPass HslRenderer::colorOnlyPass(VkImageLayout finalLayout) const {
|
|
VkAttachmentDescription c = {};
|
|
c.format = VK_FORMAT_R8G8B8A8_SRGB;
|
|
c.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
c.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
c.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
c.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
c.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
c.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
c.finalLayout = finalLayout;
|
|
VkAttachmentReference cref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
VkSubpassDescription sub = {};
|
|
sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
sub.colorAttachmentCount = 1;
|
|
sub.pColorAttachments = &cref;
|
|
VkSubpassDependency deps[2] = {};
|
|
deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; // the foveated pass's writes before this one reads them, and the
|
|
deps[0].dstSubpass = 0; // target's layout transition before this pass writes it
|
|
deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
deps[0].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
deps[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
deps[1].srcSubpass = 0;
|
|
deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
|
deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
// (the same whichever the final layout: passes differing in a dependency aren't compatible, and one pipeline draws
|
|
// in both kinds)
|
|
deps[1].dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
|
deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
deps[1].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
|
const uint32_t viewMask = (1u << viewCount) - 1;
|
|
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
|
|
mv.subpassCount = 1;
|
|
mv.pViewMasks = &viewMask;
|
|
mv.correlationMaskCount = 1;
|
|
mv.pCorrelationMasks = &viewMask;
|
|
VkRenderPassCreateInfo rp = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
|
rp.pNext = &mv;
|
|
rp.attachmentCount = 1;
|
|
rp.pAttachments = &c;
|
|
rp.subpassCount = 1;
|
|
rp.pSubpasses = ⊂
|
|
rp.dependencyCount = 2;
|
|
rp.pDependencies = deps;
|
|
VkRenderPass made = VK_NULL_HANDLE;
|
|
vkCreateRenderPass(device_, &rp, nullptr, &made);
|
|
return made;
|
|
}
|
|
|
|
// The smooth-foveation pass's pipeline: tools/fovpass (one triangle; the frame read through the subsampled sampler,
|
|
// which must be immutable in its set layout).
|
|
void HslRenderer::createFovPass() {
|
|
if (fovPipe_) return;
|
|
if (!fovPass_) fovPass_ = colorOnlyPass(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
VkSamplerCreateInfo s = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
|
s.flags = VK_SAMPLER_CREATE_SUBSAMPLED_BIT_EXT;
|
|
s.magFilter = s.minFilter = VK_FILTER_LINEAR;
|
|
s.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
|
s.addressModeU = s.addressModeV = s.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
vkCreateSampler(device_, &s, nullptr, &subSampler_);
|
|
VkDescriptorSetLayoutBinding b = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, &subSampler_};
|
|
VkDescriptorSetLayoutCreateInfo dl = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
|
dl.bindingCount = 1;
|
|
dl.pBindings = &b;
|
|
vkCreateDescriptorSetLayout(device_, &dl, nullptr, &fovSetLayout_);
|
|
VkPushConstantRange pr = {VK_SHADER_STAGE_FRAGMENT_BIT, 0, 8};
|
|
VkPipelineLayoutCreateInfo pl = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
|
pl.setLayoutCount = 1;
|
|
pl.pSetLayouts = &fovSetLayout_;
|
|
pl.pushConstantRangeCount = 1;
|
|
pl.pPushConstantRanges = ≺
|
|
vkCreatePipelineLayout(device_, &pl, nullptr, &fovLayout_);
|
|
VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
|
VkDescriptorPoolCreateInfo dp = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
|
dp.maxSets = 1;
|
|
dp.poolSizeCount = 1;
|
|
dp.pPoolSizes = &ps;
|
|
vkCreateDescriptorPool(device_, &dp, nullptr, &fovPool_);
|
|
VkDescriptorSetAllocateInfo da = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
da.descriptorPool = fovPool_;
|
|
da.descriptorSetCount = 1;
|
|
da.pSetLayouts = &fovSetLayout_;
|
|
vkAllocateDescriptorSets(device_, &da, &fovSet_);
|
|
auto module = [&](const uint32_t* code, size_t bytes) {
|
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
ci.codeSize = bytes;
|
|
ci.pCode = code;
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
vkCreateShaderModule(device_, &ci, nullptr, &m);
|
|
return m;
|
|
};
|
|
VkShaderModule vs = module(kFovPassVert, sizeof kFovPassVert), fs = module(kFovPassFrag, sizeof kFovPassFrag);
|
|
VkPipelineShaderStageCreateInfo st[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
|
st[0].stage = VK_SHADER_STAGE_VERTEX_BIT; st[0].module = vs; st[0].pName = "main";
|
|
st[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; st[1].module = fs; st[1].pName = "main";
|
|
VkPipelineVertexInputStateCreateInfo vi = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
|
VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
VkPipelineViewportStateCreateInfo vps = {VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
|
vps.viewportCount = 1; vps.scissorCount = 1;
|
|
VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
|
rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.lineWidth = 1.0f;
|
|
VkPipelineMultisampleStateCreateInfo ms = {VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
|
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
|
VkPipelineDepthStencilStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
|
VkPipelineColorBlendAttachmentState cb = {};
|
|
cb.colorWriteMask = 0xF;
|
|
VkPipelineColorBlendStateCreateInfo cbs = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
|
cbs.attachmentCount = 1; cbs.pAttachments = &cb;
|
|
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
|
VkPipelineDynamicStateCreateInfo dsi = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
|
dsi.dynamicStateCount = 2; dsi.pDynamicStates = dyn;
|
|
VkGraphicsPipelineCreateInfo pci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
pci.stageCount = 2; pci.pStages = st; pci.pVertexInputState = &vi; pci.pInputAssemblyState = &ia; pci.pViewportState = &vps;
|
|
pci.pRasterizationState = &rs; pci.pMultisampleState = &ms; pci.pDepthStencilState = &ds; pci.pColorBlendState = &cbs;
|
|
pci.pDynamicState = &dsi; pci.layout = fovLayout_; pci.renderPass = fovPass_;
|
|
if (vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &fovPipe_) != VK_SUCCESS) fovPipe_ = VK_NULL_HANDLE;
|
|
vkDestroyShaderModule(device_, vs, nullptr);
|
|
vkDestroyShaderModule(device_, fs, nullptr);
|
|
LOG("smooth foveation: %s", fovPipe_ ? "the coarse regions filtered (subsampled targets)" : "pipeline failed");
|
|
}
|
|
|
|
void HslRenderer::recordFovPass(VkFramebuffer fb, VkRenderPass pass) {
|
|
VkRenderPassBeginInfo rp = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
|
rp.renderPass = pass;
|
|
rp.framebuffer = fb;
|
|
rp.renderArea = {{0, 0}, {width_, height_}};
|
|
vkCmdBeginRenderPass(cmd_, &rp, VK_SUBPASS_CONTENTS_INLINE);
|
|
const VkViewport vp = {0, 0, static_cast<float>(width_), static_cast<float>(height_), 0, 1};
|
|
const VkRect2D sc = {{0, 0}, {width_, height_}};
|
|
vkCmdSetViewport(cmd_, 0, 1, &vp);
|
|
vkCmdSetScissor(cmd_, 0, 1, &sc);
|
|
vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, fovPipe_);
|
|
vkCmdBindDescriptorSets(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, fovLayout_, 0, 1, &fovSet_, 0, nullptr);
|
|
const float inv[2] = {1.0f / width_, 1.0f / height_};
|
|
vkCmdPushConstants(cmd_, fovLayout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0, 8, inv);
|
|
vkCmdDraw(cmd_, 3, 1, 0, 0);
|
|
vkCmdEndRenderPass(cmd_);
|
|
}
|
|
|
|
// The density map: full density in a circle round the middle, then half, then a quarter (per axis) toward the
|
|
// edges; stronger levels make the full-detail circle smaller. Distances are in eye-image widths, from the map's
|
|
// centre, which the per-eye offset puts where that eye looks.
|
|
// The density for each map texel (across, down), full detail around (cx, cy) (in map texels), then coarser step by
|
|
// step with distance: the pixel area halves at each of four rings, the vertical density first and then the horizontal
|
|
// (1x1, 1x2, 2x2, 2x4, 4x4 pixels), so no step doubles both at once and the falloff reads as a smooth blur, not bands.
|
|
// fovStrength_ places the rings: at 1, 2 and 3 the full-detail radius and the cost are those of the old three levels.
|
|
void HslRenderer::densityTexels(float cx, float cy, std::vector<float>& dens) const {
|
|
static const float kFull[4] = {1.60f, 0.62f, 0.46f, 0.34f}, kHalf[4] = {2.20f, 0.95f, 0.74f, 0.58f}; // per level: full / old half radius
|
|
const float st = std::clamp(fovStrength_, 0.0f, 3.0f);
|
|
const int lo = std::min(2, static_cast<int>(st));
|
|
const float t = st - lo;
|
|
const float r0 = kFull[lo] + (kFull[lo + 1] - kFull[lo]) * t, rh = kHalf[lo] + (kHalf[lo + 1] - kHalf[lo]) * t;
|
|
const float r4 = rh + (rh - r0) * 0.5f; // the last ring: a quarter of the resolution each way past it
|
|
dens.resize(static_cast<size_t>(fdmW_) * fdmH_ * 2);
|
|
const float scale = static_cast<float>(fdmTexel_) / width_; // texels -> image widths
|
|
for (uint32_t y = 0; y < fdmH_; ++y)
|
|
for (uint32_t x = 0; x < fdmW_; ++x) {
|
|
const float dx = (x + 0.5f - cx) * scale, dy = (y + 0.5f - cy) * scale; // round in pixels
|
|
const float r = std::sqrt(dx * dx + dy * dy) * 2.0f; // 1 = half an image width away
|
|
const int k = st <= 0.0f ? 0 : std::clamp(static_cast<int>((r - r0) / (r4 - r0) * 4.0f) + (r >= r0 ? 1 : 0), 0, 4);
|
|
static const float kAcross[5] = {1, 1, 0.5f, 0.5f, 0.25f}, kDown[5] = {1, 0.5f, 0.5f, 0.25f, 0.25f};
|
|
const size_t i = (static_cast<size_t>(y) * fdmW_ + x) * 2;
|
|
dens[i] = kAcross[k];
|
|
dens[i + 1] = kDown[k];
|
|
}
|
|
}
|
|
|
|
void HslRenderer::packDensity(const std::vector<float>& dens, uint8_t* dst) const {
|
|
const size_t texels = static_cast<size_t>(fdmW_) * fdmH_;
|
|
const size_t bpt = fdmFormat_ == VK_FORMAT_R32G32_SFLOAT ? 8 : fdmFormat_ == VK_FORMAT_R16G16_SFLOAT ? 4 : 2;
|
|
for (size_t i = 0; i < texels && i * 2 + 1 < dens.size(); ++i) {
|
|
uint8_t* p = dst + i * bpt;
|
|
const float a = dens[i * 2], d = dens[i * 2 + 1];
|
|
if (bpt == 8) { std::memcpy(p, &a, 4); std::memcpy(p + 4, &d, 4); }
|
|
else if (bpt == 4) { const uint16_t ha = toHalf(a), hd = toHalf(d); std::memcpy(p, &ha, 2); std::memcpy(p + 2, &hd, 2); }
|
|
else { p[0] = static_cast<uint8_t>(std::lround(a * 255)); p[1] = static_cast<uint8_t>(std::lround(d * 255)); }
|
|
}
|
|
}
|
|
|
|
void HslRenderer::writeDensityMap() {
|
|
if (!fdmOn_ || !fdm_.image) return;
|
|
if (!fdmOffset_) { fdmKey_ = -1; return; } // (rewritten where you look by render(), each frame it changes)
|
|
std::vector<float> dens;
|
|
densityTexels(fdmW_ * 0.5f, fdmH_ * 0.5f, dens);
|
|
// In the map's format (two channels: density across, density down), once per layer (view).
|
|
const size_t texels = static_cast<size_t>(fdmW_) * fdmH_;
|
|
const size_t bpt = fdmFormat_ == VK_FORMAT_R32G32_SFLOAT ? 8 : fdmFormat_ == VK_FORMAT_R16G16_SFLOAT ? 4 : 2;
|
|
std::vector<uint8_t> px(texels * bpt * viewCount);
|
|
for (uint32_t l = 0; l < viewCount; ++l) packDensity(dens, &px[l * texels * bpt]);
|
|
Buffer staging = createBuffer(px.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
std::memcpy(staging.mapped, px.data(), px.size());
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, fdm_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
|
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkBufferImageCopy cp = {};
|
|
cp.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
|
|
cp.imageExtent = {fdmW_, fdmH_, 1};
|
|
vkCmdCopyBufferToImage(c, staging.buf, fdm_.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp);
|
|
imageBarrier(c, fdm_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT,
|
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT);
|
|
});
|
|
retire(staging);
|
|
}
|
|
|
|
void HslRenderer::setFoveationLevel(int level) {
|
|
level = std::clamp(level, 0, 3);
|
|
fovLevel_ = level;
|
|
setFoveationStrength(static_cast<float>(level));
|
|
}
|
|
|
|
void HslRenderer::setFoveationStrength(float strength) {
|
|
strength = std::round(std::clamp(strength, 0.0f, 3.0f) * 32.0f) / 32.0f;
|
|
if (strength == fovStrength_) return;
|
|
fovStrength_ = strength;
|
|
fovLevel_ = static_cast<int>(std::lround(strength));
|
|
if (fdmOn_ && device_ && fdmOffset_) { vkDeviceWaitIdle(device_); writeDensityMap(); }
|
|
fdmKey_ = -1;
|
|
}
|
|
|
|
// Samplers, fallback textures and the shared lighting buffers (values match the editor's tuned desktop path).
|
|
void HslRenderer::createSharedResources() {
|
|
VkSamplerCreateInfo s = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
|
s.magFilter = s.minFilter = VK_FILTER_LINEAR;
|
|
s.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
|
s.addressModeU = s.addressModeV = s.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
|
|
s.maxLod = VK_LOD_CLAMP_NONE;
|
|
if (const char* e = std::getenv("FH_MIN_LOD")) s.minLod = static_cast<float>(std::atof(e)); // A/B: texture bandwidth
|
|
vkCreateSampler(device_, &s, nullptr, &wrapSampler_);
|
|
s.anisotropyEnable = anisotropy_ ? VK_TRUE : VK_FALSE;
|
|
s.maxAnisotropy = anisotropy_ ? std::clamp(anisotropy, 1.0f, std::max(1.0f, maxAniso_)) : 1.0f;
|
|
vkCreateSampler(device_, &s, nullptr, &anisoSampler_);
|
|
VkSamplerCreateInfo sh = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
|
sh.magFilter = sh.minFilter = VK_FILTER_LINEAR;
|
|
sh.addressModeU = sh.addressModeV = sh.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sh.compareEnable = VK_TRUE;
|
|
sh.compareOp = VK_COMPARE_OP_ALWAYS; // no shadow pass: every comparison reads "lit"
|
|
sh.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
|
vkCreateSampler(device_, &sh, nullptr, &shadowSampler_);
|
|
|
|
const u8 white[4] = {255, 255, 255, 255}, black[4] = {0, 0, 0, 255}, flatN[4] = {128, 128, 255, 255};
|
|
white_ = createTexture(white, 1, 1, true);
|
|
black_ = createTexture(black, 1, 1, true);
|
|
const u8 none[4] = {0, 0, 0, 0};
|
|
nullTex_ = createTexture(none, 1, 1, true); // the VrShell null texture: black, transparent
|
|
const u8 noMetal[4] = {255, 255, 255, 0};
|
|
noMetal_ = createTexture(noMetal, 1, 1, true);
|
|
flatNormal_ = createTexture(flatN, 1, 1, false);
|
|
const float one[3] = {1, 1, 1};
|
|
whiteCube_ = createSolidCube(one, one, one);
|
|
white3D_ = createSolid3D(white);
|
|
shadowMap_ = createShadowDepth();
|
|
|
|
// Some homes' shaders declare these blocks as storage buffers (validation: VUID-VkWriteDescriptorSet-00331).
|
|
globals_ = createBuffer(globalsSize_, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true);
|
|
lightUniforms_ = createBuffer(2048, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true);
|
|
lightItems_ = createBuffer(4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, true);
|
|
lightGrid_ = createBuffer(65536, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, true);
|
|
}
|
|
|
|
// ── Programs ─────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
int HslRenderer::programFor(const ShaderProgramSrc& src) {
|
|
auto it = programIndex_.find(src.path);
|
|
if (it != programIndex_.end()) return it->second;
|
|
programIndex_[src.path] = -1;
|
|
|
|
Program p;
|
|
p.path = src.path;
|
|
p.clay = p.path.rfind("builtin/clay", 0) == 0;
|
|
if (const char* dir = std::getenv("FH_DUMP_SPV")) { // diagnostics: every program's SPIR-V, <dir>/<shader>.vert|frag.spv
|
|
std::string n = src.path;
|
|
for (char& c : n) if (c == '/' || c == '\\' || c == ':') c = '_';
|
|
for (const auto& [ext, code] : {std::pair<const char*, const std::vector<u32>*>{".vert.spv", &src.vert}, {".frag.spv", &src.frag}})
|
|
if (FILE* f = std::fopen((std::string(dir) + "/" + n + ext).c_str(), "wb")) { std::fwrite(code->data(), 4, code->size(), f); std::fclose(f); }
|
|
}
|
|
spv::Module vr, fr;
|
|
if (!spv::reflect(src.vert, vr) || !spv::reflect(src.frag, fr)) { LOG("reflect failed: %s", src.path.c_str()); return -1; }
|
|
p.multiview = vr.multiview;
|
|
|
|
// Vertex layout from the shader's own inputs, roles by name (the stream semantic names).
|
|
uint32_t off = 0;
|
|
std::vector<spv::Input> ins = vr.inputs;
|
|
std::sort(ins.begin(), ins.end(), [](const spv::Input& a, const spv::Input& b) { return a.location < b.location; });
|
|
for (const auto& in : ins) {
|
|
std::string n = lower(in.name);
|
|
VertexAttr a = {in.location, VK_FORMAT_UNDEFINED, 0, 0, Role::Zero, in.integer};
|
|
if (has(n, "pos")) a.role = Role::Pos;
|
|
else if (has(n, "norm")) a.role = Role::Normal;
|
|
else if (has(n, "tangent")) a.role = Role::Tangent;
|
|
else if (has(n, "lightmap") || has(n, "uv1")) a.role = Role::Uv1;
|
|
else if (has(n, "uv2")) a.role = Role::Uv2;
|
|
else if (has(n, "uv3")) a.role = Role::Uv3;
|
|
else if (has(n, "uv") || has(n, "texcoord")) a.role = Role::Uv0;
|
|
else if (has(n, "boneind") || has(n, "joint") || has(n, "indices")) a.role = Role::BoneIdx;
|
|
else if (has(n, "bonewe") || has(n, "weight")) a.role = Role::BoneWgt;
|
|
else if (has(n, "color") || has(n, "colour")) a.role = Role::Color;
|
|
if (a.role == Role::Color || a.role == Role::BoneIdx || a.role == Role::BoneWgt) {
|
|
a.format = in.integer ? VK_FORMAT_R8G8B8A8_UINT : VK_FORMAT_R8G8B8A8_UNORM;
|
|
a.size = 4;
|
|
} else if (in.integer) {
|
|
static const VkFormat u[] = {VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT, VK_FORMAT_R32G32B32A32_UINT};
|
|
a.format = u[std::min(in.components, 4u) - 1];
|
|
a.size = 4 * in.components;
|
|
} else if (in.width == 16) {
|
|
static const VkFormat h[] = {VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R16G16B16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT};
|
|
a.format = h[std::min(in.components, 4u) - 1];
|
|
a.size = 2 * in.components;
|
|
} else {
|
|
static const VkFormat f[] = {VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT};
|
|
a.format = f[std::min(in.components, 4u) - 1];
|
|
a.size = 4 * in.components;
|
|
}
|
|
a.offset = off;
|
|
off += (a.size + 3) & ~3u;
|
|
p.attrs.push_back(a);
|
|
}
|
|
p.stride = off ? off : 4;
|
|
|
|
// Descriptor sets: union of both stages.
|
|
shaderbake::mergedSets(vr, fr, p.sets);
|
|
p.boned = shaderbake::boned(vr);
|
|
for (int s = 0; s < 3; ++s) {
|
|
std::vector<VkDescriptorSetLayoutBinding> lb;
|
|
for (const auto& r : p.sets[s])
|
|
lb.push_back({r.binding, descriptorType(r.kind), r.count, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, nullptr});
|
|
VkDescriptorSetLayoutCreateInfo lci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
|
lci.bindingCount = static_cast<uint32_t>(lb.size());
|
|
lci.pBindings = lb.data();
|
|
vkCreateDescriptorSetLayout(device_, &lci, nullptr, &p.setLayouts[s]);
|
|
}
|
|
for (const auto& r : p.sets[0])
|
|
if (r.binding == 0 && isBuffer(r.kind) && r.members.size() > globalMembers_.size()) globalMembers_ = r.members;
|
|
for (const auto& r : p.sets[2])
|
|
if (isBuffer(r.kind) && has(lower(r.name), "lightbaker")) p.lightBaker = true;
|
|
|
|
p.pushSize = (std::max(vr.pushSize, fr.pushSize) + 3) & ~3u;
|
|
for (const spv::Module* m : {&vr, &fr})
|
|
for (const auto& pm : m->pushMembers) {
|
|
std::string n = lower(pm.name);
|
|
if (has(n, "worldfrommodel") && p.pcWorld < 0) p.pcWorld = static_cast<int>(pm.offset);
|
|
else if (has(n, "color") && p.pcColor < 0) p.pcColor = static_cast<int>(pm.offset);
|
|
else if (n == "uvtransformu") p.pcUvU = static_cast<int>(pm.offset); // (the built-in Clay shader's)
|
|
else if (n == "uvtransformv") p.pcUvV = static_cast<int>(pm.offset);
|
|
}
|
|
// Unnamed push block: the engine's fixed layout is pushConstants { mat4 worldFromModel @0; vec4 color @64 }.
|
|
if (p.pcWorld < 0 && p.pushSize >= 64) p.pcWorld = 0;
|
|
if (p.pcColor < 0 && p.pushSize >= 80) p.pcColor = 64;
|
|
VkPushConstantRange pcr = {VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, p.pushSize};
|
|
VkPipelineLayoutCreateInfo plci = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
|
plci.setLayoutCount = 3;
|
|
plci.pSetLayouts = p.setLayouts;
|
|
plci.pushConstantRangeCount = p.pushSize ? 1 : 0;
|
|
plci.pPushConstantRanges = &pcr;
|
|
vkCreatePipelineLayout(device_, &plci, nullptr, &p.layout);
|
|
|
|
// (through the SPIR-V optimizer first: spv_opt.h; a module a .fenv carries made is used as it is)
|
|
auto module = [&](const std::vector<u32>& shipped) {
|
|
const std::vector<u32> code = spvopt::run(shipped, p.path);
|
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
ci.codeSize = code.size() * 4;
|
|
ci.pCode = code.data();
|
|
VkShaderModule sm = VK_NULL_HANDLE;
|
|
// (a .fenv's SPIR-V is checked before the driver sees it: spv_check.h)
|
|
if (code.empty() || !shaderOk(code, p.path) || vkCreateShaderModule(device_, &ci, nullptr, &sm) != VK_SUCCESS) sm = VK_NULL_HANDLE;
|
|
return sm;
|
|
};
|
|
// (prepared by the bake for a .fenv: shader_bake.h)
|
|
p.vs = module(src.prepared && shaderbake::bakedUsable(src.vertInvariant) ? src.vertInvariant : shaderbake::invariantPosition(src.vert));
|
|
p.fs = module(src.frag);
|
|
p.src = &src;
|
|
p.foldBlocks = spvspec::blocksRead(src.frag);
|
|
const std::vector<u32> alpha = src.prepared && shaderbake::bakedUsable(src.fragAlpha) ? src.fragAlpha : shaderbake::alphaOnly(src.frag);
|
|
if (!alpha.empty()) p.fsAlpha = module(alpha);
|
|
if (!p.vs || !p.fs) { // SPIR-V the driver rejects: this program's meshes are skipped
|
|
LOG("shader module failed: %s", src.path.c_str());
|
|
if (p.vs) vkDestroyShaderModule(device_, p.vs, nullptr);
|
|
if (p.fs) vkDestroyShaderModule(device_, p.fs, nullptr);
|
|
vkDestroyPipelineLayout(device_, p.layout, nullptr);
|
|
for (auto l : p.setLayouts) vkDestroyDescriptorSetLayout(device_, l, nullptr);
|
|
return -1;
|
|
}
|
|
|
|
VkDescriptorSetLayout l01[2] = {p.setLayouts[0], p.setLayouts[1]};
|
|
VkDescriptorSet s01[2];
|
|
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
ai.descriptorPool = descPool_;
|
|
ai.descriptorSetCount = 2;
|
|
ai.pSetLayouts = l01;
|
|
vkAllocateDescriptorSets(device_, &ai, s01);
|
|
p.set0 = s01[0];
|
|
p.set1 = s01[1];
|
|
|
|
LOG("program %s: %zu inputs (stride %u), sets %zu/%zu/%zu, push %u, multiview %d", src.path.c_str(),
|
|
p.attrs.size(), p.stride, p.sets[0].size(), p.sets[1].size(), p.sets[2].size(), p.pushSize, p.multiview);
|
|
for (const auto& a : p.attrs)
|
|
LOG(" attr loc %u role %d fmt %d off %u size %u int %d", a.location, static_cast<int>(a.role), a.format, a.offset, a.size, a.integer);
|
|
programs_.push_back(std::move(p));
|
|
int idx = static_cast<int>(programs_.size()) - 1;
|
|
programIndex_[src.path] = idx;
|
|
writeSharedSets(programs_[idx]);
|
|
return idx;
|
|
}
|
|
|
|
// set0 / set1 are the same for every mesh of a program: globals, lighting, IBL and shadow resources.
|
|
void HslRenderer::writeSharedSets(Program& p) {
|
|
std::vector<VkWriteDescriptorSet> writes;
|
|
std::vector<std::vector<VkDescriptorBufferInfo>> bufs;
|
|
std::vector<std::vector<VkDescriptorImageInfo>> imgs;
|
|
bufs.reserve(64);
|
|
imgs.reserve(64);
|
|
for (int s = 0; s < 2; ++s) {
|
|
for (const auto& r : p.sets[s]) {
|
|
std::string n = lower(r.name);
|
|
VkWriteDescriptorSet w = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
|
w.dstSet = s == 0 ? p.set0 : p.set1;
|
|
w.dstBinding = r.binding;
|
|
w.descriptorCount = r.count;
|
|
w.descriptorType = descriptorType(r.kind);
|
|
if (isBuffer(r.kind)) {
|
|
Buffer* b = &lightUniforms_;
|
|
if (s == 0) b = &globals_;
|
|
if (s == 1 && b == &lightUniforms_) {
|
|
static bool listed = false;
|
|
if (!listed) {
|
|
listed = true;
|
|
for (const auto& m : r.members) LOG("lightUniforms member %s @%u (%u bytes)", m.name.c_str(), m.offset, m.size);
|
|
}
|
|
for (const auto& m : r.members) {
|
|
const std::string mn = lower(m.name);
|
|
// HSR's derived lightUniforms: these five are 1 (byte 112-156, 168-172), all else zero.
|
|
const bool one = mn == "iblreflectiontint" || mn == "ibldiffusetint" || mn == "externalaverageluminance" ||
|
|
mn == "iblreflectionmaxadaptationfactor" || mn == "iblreflectionluminanceinverse";
|
|
if (!one || m.integer || m.offset + m.size > lightUniforms_.size) continue;
|
|
float* lf = reinterpret_cast<float*>(static_cast<u8*>(lightUniforms_.mapped) + m.offset);
|
|
for (uint32_t k = 0; k * 4 < m.size; ++k) lf[k] = 1.0f;
|
|
}
|
|
}
|
|
else if (has(n, "item")) b = &lightItems_;
|
|
else if (has(n, "grid") || r.kind == spv::ResKind::StorageBuffer) b = &lightGrid_;
|
|
bufs.emplace_back(r.count, VkDescriptorBufferInfo{b->buf, 0, VK_WHOLE_SIZE});
|
|
w.pBufferInfo = bufs.back().data();
|
|
} else {
|
|
VkDescriptorImageInfo ii = {};
|
|
ii.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
if (r.kind == spv::ResKind::Sampler || r.kind == spv::ResKind::CombinedImageSampler)
|
|
ii.sampler = has(n, "shadow") ? shadowSampler_ : wrapSampler_;
|
|
if (r.kind != spv::ResKind::Sampler) {
|
|
if (r.depthImage || has(n, "shadow")) ii.imageView = shadowMap_.view;
|
|
else if (r.dim == spv::ImageDim::Cube) ii.imageView = has(n, "diff") ? whiteCube_.view : iblReflection_.view;
|
|
else if (r.dim == spv::ImageDim::D3) ii.imageView = white3D_.view;
|
|
else ii.imageView = white_.view;
|
|
}
|
|
imgs.emplace_back(r.count, ii);
|
|
w.pImageInfo = imgs.back().data();
|
|
}
|
|
if (r.kind == spv::ResKind::StorageImage) continue;
|
|
writes.push_back(w);
|
|
}
|
|
}
|
|
if (!writes.empty()) vkUpdateDescriptorSets(device_, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
|
}
|
|
|
|
VkPipeline HslRenderer::pipelineFor(Program& p, int blend, bool cull, bool sky, bool a2c, CutPass cut, int variant) {
|
|
const uint32_t key = static_cast<uint32_t>(blend) * 2 + (cull ? 1 : 0) + (sky ? 16 : 0) + (a2c ? 32 : 0) + static_cast<uint32_t>(cut) * 64;
|
|
Program::Variant* var = variant >= 0 && static_cast<size_t>(variant) < p.variants.size() ? &p.variants[variant] : nullptr;
|
|
std::map<uint32_t, VkPipeline>& pipes = var ? var->pipelines : p.pipelines;
|
|
auto it = pipes.find(key);
|
|
if (it != pipes.end()) return it->second;
|
|
|
|
VkPipelineShaderStageCreateInfo stages[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO},
|
|
{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
|
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = p.vs; stages[0].pName = "main";
|
|
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].pName = "main";
|
|
stages[1].module = var ? (cut == kCutDepth ? var->fsAlpha : var->fs) : (cut == kCutDepth ? p.fsAlpha : p.fs);
|
|
if (!stages[1].module) return VK_NULL_HANDLE;
|
|
VkVertexInputBindingDescription vb = {0, p.stride, VK_VERTEX_INPUT_RATE_VERTEX};
|
|
std::vector<VkVertexInputAttributeDescription> va;
|
|
for (const auto& a : p.attrs) va.push_back({a.location, 0, a.format, a.offset});
|
|
VkPipelineVertexInputStateCreateInfo vi = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
|
vi.vertexBindingDescriptionCount = 1;
|
|
vi.pVertexBindingDescriptions = &vb;
|
|
vi.vertexAttributeDescriptionCount = static_cast<uint32_t>(va.size());
|
|
vi.pVertexAttributeDescriptions = va.data();
|
|
VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
VkPipelineViewportStateCreateInfo vps = {VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
|
vps.viewportCount = 1;
|
|
vps.scissorCount = 1;
|
|
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
|
VkPipelineDynamicStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
|
ds.dynamicStateCount = 2;
|
|
ds.pDynamicStates = dyn;
|
|
VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
|
rs.polygonMode = VK_POLYGON_MODE_FILL;
|
|
rs.cullMode = cull ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_NONE;
|
|
// Meta's meshes wind front faces clockwise in Vulkan clip space (its projection flips Y). Shaders that light
|
|
// back faces differently (gl_FrontFacing: the palms' backfaceDarken/translucency) depend on this.
|
|
rs.frontFace = std::getenv("FH_CCW") ? VK_FRONT_FACE_COUNTER_CLOCKWISE : VK_FRONT_FACE_CLOCKWISE;
|
|
rs.lineWidth = 1.0f;
|
|
VkPipelineMultisampleStateCreateInfo msi = {VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
|
msi.rasterizationSamples = samples_;
|
|
// Cut-out foliage: the SHAD pass state enables alpha-to-coverage for the animvege surfaces (HSR Studio's
|
|
// world packets: exactly those pipelines). Matched by path until the SHAD pass-state table is decoded.
|
|
msi.alphaToCoverageEnable = samples_ != VK_SAMPLE_COUNT_1_BIT && blend == 0 && (a2c || has(lower(p.path), "animvege")) ? VK_TRUE : VK_FALSE;
|
|
VkPipelineDepthStencilStateCreateInfo dsi = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
|
// The sky goes behind everything. Meta's skybox shader places it at the clip-space depth the native renderer
|
|
// expects, which is not ours, so its depth comes from the viewport instead: drawn after the opaque meshes with
|
|
// the depth range pinned to the far plane (0, reversed-Z) it fills exactly the samples nothing covers, and the
|
|
// covered ones are rejected before shading (drawn first with no depth test, it was shaded across the whole view).
|
|
dsi.depthTestEnable = sky && skyFirst() ? VK_FALSE : VK_TRUE;
|
|
dsi.depthWriteEnable = blend == 0 && !sky && cut != kCutShade ? VK_TRUE : VK_FALSE;
|
|
dsi.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; // reversed-Z: clear 0, near -> 1
|
|
// A cut-out's shading pass: the samples whose depth is its own (the depth pass wrote it there through the same
|
|
// alpha-to-coverage mask, and nothing nearer covered it) are exactly the ones alpha-to-coverage with GREATER_OR_EQUAL
|
|
// let through, so EQUAL alone picks them: the same picture, and without alpha-to-coverage the GPU tests depth
|
|
// before shading (Turnip ran it after: every frond layer was shaded). Desert Terrace 2-4% less GPU time.
|
|
// FH_CUT_GE=1: the old way (A/B).
|
|
static const bool cutEqual = std::getenv("FH_CUT_GE") == nullptr;
|
|
if (cutEqual && cut == kCutShade && msi.alphaToCoverageEnable) {
|
|
msi.alphaToCoverageEnable = VK_FALSE;
|
|
dsi.depthCompareOp = VK_COMPARE_OP_EQUAL;
|
|
}
|
|
VkPipelineColorBlendAttachmentState cba = {};
|
|
cba.colorWriteMask = cut == kCutDepth ? 0 : 0xF;
|
|
if (blend != 0) {
|
|
cba.blendEnable = VK_TRUE;
|
|
cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
|
cba.dstColorBlendFactor = blend == 2 ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
cba.colorBlendOp = VK_BLEND_OP_ADD;
|
|
cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
|
|
cba.dstAlphaBlendFactor = blend == 2 ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
cba.alphaBlendOp = VK_BLEND_OP_ADD;
|
|
}
|
|
VkPipelineColorBlendStateCreateInfo cb = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
|
cb.attachmentCount = 1;
|
|
cb.pAttachments = &cba;
|
|
VkGraphicsPipelineCreateInfo gci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
gci.stageCount = 2;
|
|
gci.pStages = stages;
|
|
gci.pVertexInputState = &vi;
|
|
gci.pInputAssemblyState = &ia;
|
|
gci.pViewportState = &vps;
|
|
gci.pRasterizationState = &rs;
|
|
gci.pMultisampleState = &msi;
|
|
gci.pDepthStencilState = &dsi;
|
|
gci.pColorBlendState = &cb;
|
|
gci.pDynamicState = &ds;
|
|
gci.layout = p.layout;
|
|
gci.renderPass = renderPass_;
|
|
VkPipeline pipe = VK_NULL_HANDLE;
|
|
VkResult r = vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &gci, nullptr, &pipe);
|
|
if (r != VK_SUCCESS) LOG("pipeline for %s failed: %d", p.path.c_str(), r);
|
|
pipes[key] = pipe;
|
|
return pipe;
|
|
}
|
|
|
|
// ── Meshes ───────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
VkImageView HslRenderer::textureFor(const spv::Resource& r, GpuMesh& gm, const MeshData& md) {
|
|
// Content-deduplicated upload: many meshes share one material texture (each MeshData holds its own copy).
|
|
auto uploadRaw = [&](const void* px, size_t bytes, u32 w, u32 h, VkFormat fmt) -> VkImageView {
|
|
const size_t bpp = fmt == VK_FORMAT_R16G16B16A16_SFLOAT ? 8 : 4;
|
|
if (!px || !w || !h || bytes / bpp / w < h) return white_.view; // (divided: w * h can overflow)
|
|
// Hash ALL of it: sampling every Nth byte made mostly-flat maps (normals, ORM) of the same size collide,
|
|
// so one mesh got another's texture.
|
|
uint64_t key = 1469598103934665603ull ^ (static_cast<uint64_t>(w) << 32 | h) ^ (static_cast<uint64_t>(fmt) * 0x9E3779B97F4A7C15ull);
|
|
const u8* b = static_cast<const u8*>(px);
|
|
size_t i = 0;
|
|
for (; i + 8 <= bytes; i += 8) {
|
|
uint64_t v;
|
|
std::memcpy(&v, b + i, 8);
|
|
key = (key ^ v) * 0x100000001B3ull;
|
|
key ^= key >> 29;
|
|
}
|
|
for (; i < bytes; ++i) key = (key ^ b[i]) * 1099511628211ull;
|
|
auto it = textureCache_.find(key);
|
|
if (it != textureCache_.end()) return it->second.view;
|
|
Image img = createTextureFmt(px, w, h, fmt);
|
|
if (!img.view) return white_.view;
|
|
textureCache_[key] = img;
|
|
return img.view;
|
|
};
|
|
auto upload = [&](const std::vector<u8>& rgba, u32 w, u32 h, bool srgb) -> VkImageView {
|
|
return uploadRaw(rgba.data(), rgba.size(), w, h, srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM);
|
|
};
|
|
// Baked HDR lightmaps are shared atlases (up to 4096^2): one upload per atlas, keyed by identity — hashing
|
|
// 128 MB again for every mesh that samples it froze loading for seconds.
|
|
auto uploadLm = [&]() -> VkImageView { return hdrTexture(md.lmHalf.get(), md.lmSrc, md.lmW, md.lmH); };
|
|
// Clay's 8-bit lightmaps: shared and still compressed (streamed like base maps), else the decoded pixels.
|
|
auto uploadLdrLm = [&]() -> VkImageView {
|
|
if (!md.lmLdrSrc) return upload(md.lmRGBA, md.lmW, md.lmH, true);
|
|
VkImageView v = streamTexture(md.lmLdrSrc, true, white_.view);
|
|
if (slotPending_) gm.wants.push_back(pendingKey_);
|
|
return v;
|
|
};
|
|
const std::string n = lower(r.name);
|
|
if (r.dim == spv::ImageDim::Cube) {
|
|
// The skybox's own colour cubemap; otherwise the shared IBL cubes.
|
|
if (md.cubeSize && !has(n, "diff") && !has(n, "refl")) {
|
|
if (md.cubeSrc) { // kept as ASTC: sampled as-is, or decoded here (PC)
|
|
const VkFormat fmt = astcLdr_ ? astcFormat(md.cubeSrc->bw, md.cubeSrc->bh, true) : VK_FORMAT_UNDEFINED;
|
|
std::vector<u8> faces;
|
|
if (fmt == VK_FORMAT_UNDEFINED &&
|
|
!astc::decodeCubeFaces(md.cubeSrc->astc.data(), static_cast<u32>(md.cubeSrc->astc.size()), md.cubeSize,
|
|
md.cubeSrc->bw, md.cubeSrc->bh, faces))
|
|
return whiteCube_.view;
|
|
Image img = fmt != VK_FORMAT_UNDEFINED ? createAstcTexture(*md.cubeSrc, fmt)
|
|
: createCubeTexture(faces.data(), faces.size(), md.cubeSize);
|
|
if (!img.view) return whiteCube_.view;
|
|
gm.images.push_back(img);
|
|
return img.view;
|
|
}
|
|
Image img = createCubeTexture(md.cubeRGBA.data(), md.cubeRGBA.size(), md.cubeSize);
|
|
if (!img.view) return whiteCube_.view;
|
|
gm.images.push_back(img);
|
|
return img.view;
|
|
}
|
|
return has(n, "diff") ? whiteCube_.view : iblReflection_.view; // HSR: diffuse IBL = neutral white
|
|
}
|
|
if (r.dim == spv::ImageDim::D3) return white3D_.view;
|
|
// Vertex-animation texture: the loader's per-vertex offsets per frame (half floats). A VAT the material binds by
|
|
// sampler name instead (Cascadia's rigid-piece centerpiece/tram: position + rotation per piece per frame) is
|
|
// found by hash below.
|
|
if (has(n, "vat") && md.hasVat && md.vatRaw.size() >= static_cast<size_t>(md.vatW) * md.vatH * 8) {
|
|
if (debugMesh(md)) {
|
|
auto texel = [&](u32 x, u32 y) {
|
|
std::string o;
|
|
for (int c = 0; c < 4; ++c) {
|
|
uint16_t h; std::memcpy(&h, &md.vatRaw[(static_cast<size_t>(y) * md.vatW + x) * 8 + c * 2], 2);
|
|
char b[24]; std::snprintf(b, sizeof b, "%.4g ", fromHalf(h)); o += b;
|
|
}
|
|
return o;
|
|
};
|
|
LOG(" set2 '%s' <- VAT %ux%u half floats; texel (0,0) %s (0,1) %s", r.name.c_str(), md.vatW, md.vatH, texel(0, 0).c_str(),
|
|
md.vatH > 1 ? texel(0, 1).c_str() : "-");
|
|
if (md.vatW > 26 && md.vatH > 1) LOG(" (26,0) %s (26,1) %s", texel(26, 0).c_str(), texel(26, 1).c_str());
|
|
}
|
|
Image img = createTextureFmt(md.vatRaw.data(), md.vatW, md.vatH, VK_FORMAT_R16G16B16A16_SFLOAT);
|
|
if (!img.view) return black_.view; // zero offset = rest pose
|
|
gm.images.push_back(img);
|
|
return img.view;
|
|
}
|
|
if (r.depthImage || has(n, "shadow")) return shadowMap_.view;
|
|
// The baked lightmap comes from the entity's lightingParams (it outranks the material's placeholder,
|
|
// renderer_module/missing_lightmap.png, which is bound by hash otherwise).
|
|
if (has(n, "lightmap") && md.hasLmHdr())
|
|
return uploadLm();
|
|
// Device-faithful: the material texture whose sampler-name hash matches this binding.
|
|
{
|
|
const u32 h = murmur3_x86_32(r.name.data(), r.name.size(), 0);
|
|
for (const auto& nt : md.namedTex) {
|
|
if (nt.first != h || !nt.second) continue;
|
|
const bool linear = has(n, "normal") || has(n, "onxrny") || has(n, "orm") || has(n, "rbao") || has(n, "mask") ||
|
|
has(n, "noise") || has(n, "rough") || has(n, "metal") || has(n, "flow") || has(n, "_ao");
|
|
VkImageView ph = has(n, "emissive") ? black_.view : (has(n, "normal") || has(n, "onxrny")) ? flatNormal_.view
|
|
: has(n, "metallic") ? noMetal_.view : white_.view;
|
|
if (debugMesh(md))
|
|
LOG(" set2 '%s' <- texture %ux%u, %ux%u blocks, %u mips, hdr %d, %zu bytes, %s", r.name.c_str(), nt.second->w, nt.second->h, nt.second->bw,
|
|
nt.second->bh, nt.second->mips, (int)nt.second->hdr, nt.second->astc.size(), linear ? "linear" : "sRGB");
|
|
VkImageView v = streamTexture(nt.second, !linear, ph);
|
|
if (slotPending_) gm.wants.push_back(pendingKey_);
|
|
return v;
|
|
}
|
|
}
|
|
{ // HDR maps the material binds by sampler name (reflection panoramas): RGBA16F, one upload per image.
|
|
const u32 h = murmur3_x86_32(r.name.data(), r.name.size(), 0);
|
|
for (const auto& nh : md.namedHdr)
|
|
if (nh.first == h && (nh.second.half || nh.second.src)) {
|
|
if (debugMesh(md))
|
|
LOG(" set2 '%s' <- HDR %ux%u (%s)", r.name.c_str(), nh.second.w, nh.second.h, nh.second.half ? "half floats" : "ASTC source");
|
|
return hdrTexture(nh.second.half.get(), nh.second.src, nh.second.w, nh.second.h, has(n, "vat"));
|
|
}
|
|
}
|
|
if (debugMesh(md)) {
|
|
std::string have;
|
|
for (const auto& nt : md.namedTex) have += " tex:" + std::to_string(nt.first);
|
|
for (const auto& nh : md.namedHdr) have += " hdr:" + std::to_string(nh.first);
|
|
LOG(" set2 '%s' (hash %u) has no texture; the mesh has%s", r.name.c_str(), murmur3_x86_32(r.name.data(), r.name.size(), 0), have.c_str());
|
|
}
|
|
if (has(n, "vat")) return black_.view; // zero offset = rest pose
|
|
{ // HSR binds an authored texture to every material slot: any slot reaching here is a loader miss
|
|
static int misses = 0;
|
|
if (misses < 60 && !md.namedTex.empty()) {
|
|
++misses;
|
|
LOG("texture miss: '%s' slot '%s' (namedTex %zu, shader %s)", md.name.c_str(), r.name.c_str(), md.namedTex.size(), md.shaderPath.c_str());
|
|
}
|
|
}
|
|
// HSR Studio's rule for a sampler the material does not bind: the VrShell null texture (black, transparent),
|
|
// or neutral white when the material authors no textures at all. Runtime-provided maps come first.
|
|
// Only for materials that bind textures by sampler name (v206+). Older homes (Cascadia's clay_shaders) carry
|
|
// no sampler-name table: their textures come by role below.
|
|
if (!md.namedTex.empty()) {
|
|
if (has(n, "lightmap")) {
|
|
if (md.hasLmHdr()) return uploadLm();
|
|
if (md.hasLightmap) return uploadLdrLm();
|
|
return white_.view; // no baked light shipped for this mesh: neutral, never black
|
|
}
|
|
return nullTex_.view;
|
|
}
|
|
if (has(n, "lightmap") && md.hasLmHdr())
|
|
return uploadLm();
|
|
if (!md.hasTexture) return white_.view;
|
|
const bool isBase = has(n, "basecolor") || has(n, "albedo") || has(n, "diffuse");
|
|
// RBAoDir (baked light: R, B, AO, light dir; sampled with UV2) is its own slot next to ONxRNy.
|
|
const bool isRbao = has(n, "rbao");
|
|
const bool isNorm = !isRbao && (has(n, "onxrny") || has(n, "normal") || has(n, "_nx"));
|
|
const bool isLm = has(n, "lightmap");
|
|
const bool isOrm = has(n, "orm") || has(n, "rough") || has(n, "metal") || has(n, "_ao") || has(n, "occl");
|
|
const bool isEmissive = has(n, "emissive");
|
|
// A map either arrived decoded (RGBA) or deferred (still-compressed TexSource, streamed in).
|
|
auto map = [&](const std::shared_ptr<const TexSource>& src, const std::vector<u8>& rgba, u32 w, u32 h, bool srgb,
|
|
VkImageView placeholder) -> VkImageView {
|
|
if (src) {
|
|
VkImageView v = streamTexture(src, srgb, placeholder);
|
|
if (slotPending_) gm.wants.push_back(pendingKey_); // decoded once this mesh is in view
|
|
return v;
|
|
}
|
|
return upload(rgba, w, h, srgb);
|
|
};
|
|
VkImageView v = VK_NULL_HANDLE;
|
|
if (isRbao) v = md.hasOrm ? map(md.ormSrc, md.ormRGBA, md.ormW, md.ormH, false, white_.view) : white_.view;
|
|
else if (isEmissive) v = md.hasEmissive ? map(md.emissiveSrc, md.emissiveRGBA, md.emissiveW, md.emissiveH, true, black_.view) : black_.view;
|
|
else if (isNorm) v = md.hasNormal ? map(md.normalSrc, md.normalRGBA, md.normalW, md.normalH, false, flatNormal_.view)
|
|
: md.hasOrm ? map(md.ormSrc, md.ormRGBA, md.ormW, md.ormH, false, flatNormal_.view) : flatNormal_.view;
|
|
else if (isLm) {
|
|
if (md.hasLmHdr()) v = uploadLm();
|
|
else if (md.hasLightmap) v = uploadLdrLm();
|
|
else v = white_.view;
|
|
}
|
|
else if (isBase || has(n, "noise")) v = md.hasTexture ? map(md.texSrc, md.texRGBA, md.texW, md.texH, true, white_.view) : white_.view;
|
|
else if (isOrm) v = md.hasOrm ? map(md.ormSrc, md.ormRGBA, md.ormW, md.ormH, false, white_.view) : white_.view;
|
|
return v ? v : white_.view;
|
|
}
|
|
|
|
// ── Texture streaming ────────────────────────────────────────────────────────────────────────────
|
|
|
|
VkFormat HslRenderer::astcFormat(u32 bw, u32 bh, bool srgb) const {
|
|
static const struct { u32 w, h; VkFormat unorm, srgb; } kFormats[] = {
|
|
{4, 4, VK_FORMAT_ASTC_4x4_UNORM_BLOCK, VK_FORMAT_ASTC_4x4_SRGB_BLOCK},
|
|
{5, 4, VK_FORMAT_ASTC_5x4_UNORM_BLOCK, VK_FORMAT_ASTC_5x4_SRGB_BLOCK},
|
|
{5, 5, VK_FORMAT_ASTC_5x5_UNORM_BLOCK, VK_FORMAT_ASTC_5x5_SRGB_BLOCK},
|
|
{6, 5, VK_FORMAT_ASTC_6x5_UNORM_BLOCK, VK_FORMAT_ASTC_6x5_SRGB_BLOCK},
|
|
{6, 6, VK_FORMAT_ASTC_6x6_UNORM_BLOCK, VK_FORMAT_ASTC_6x6_SRGB_BLOCK},
|
|
{8, 5, VK_FORMAT_ASTC_8x5_UNORM_BLOCK, VK_FORMAT_ASTC_8x5_SRGB_BLOCK},
|
|
{8, 6, VK_FORMAT_ASTC_8x6_UNORM_BLOCK, VK_FORMAT_ASTC_8x6_SRGB_BLOCK},
|
|
{8, 8, VK_FORMAT_ASTC_8x8_UNORM_BLOCK, VK_FORMAT_ASTC_8x8_SRGB_BLOCK},
|
|
{10, 5, VK_FORMAT_ASTC_10x5_UNORM_BLOCK, VK_FORMAT_ASTC_10x5_SRGB_BLOCK},
|
|
{10, 6, VK_FORMAT_ASTC_10x6_UNORM_BLOCK, VK_FORMAT_ASTC_10x6_SRGB_BLOCK},
|
|
{10, 8, VK_FORMAT_ASTC_10x8_UNORM_BLOCK, VK_FORMAT_ASTC_10x8_SRGB_BLOCK},
|
|
{10, 10, VK_FORMAT_ASTC_10x10_UNORM_BLOCK, VK_FORMAT_ASTC_10x10_SRGB_BLOCK},
|
|
{12, 10, VK_FORMAT_ASTC_12x10_UNORM_BLOCK, VK_FORMAT_ASTC_12x10_SRGB_BLOCK},
|
|
{12, 12, VK_FORMAT_ASTC_12x12_UNORM_BLOCK, VK_FORMAT_ASTC_12x12_SRGB_BLOCK},
|
|
};
|
|
for (const auto& f : kFormats)
|
|
if (f.w == bw && f.h == bh) return srgb ? f.srgb : f.unorm;
|
|
return VK_FORMAT_UNDEFINED;
|
|
}
|
|
|
|
// The cooked ASTC chain uploaded as-is: no CPU decode at all (GPUs with textureCompressionASTC_LDR).
|
|
HslRenderer::Image HslRenderer::createAstcTexture(const TexSource& src, VkFormat fmt) {
|
|
const bool cube = src.faces == 6;
|
|
const uint32_t faces = cube ? 6 : 1;
|
|
if (!src.bw || !src.bh || src.astc.empty()) return Image{};
|
|
// A chain longer than the image's own (from a damaged file) is cut to the levels the image has.
|
|
uint32_t chain = 1;
|
|
while (chain < 32 && (std::max(src.w, src.h) >> chain)) ++chain;
|
|
const uint32_t mips = std::min(std::max(1u, src.mips), chain);
|
|
Image img = createImage2D(src.w, src.h, faces, mips, VK_SAMPLE_COUNT_1_BIT, fmt,
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
cube ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_2D, cube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0);
|
|
if (!img.image) return Image{};
|
|
Buffer staging = createBuffer(src.astc.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
if (!staging.buf) { destroy(img); return Image{}; }
|
|
std::memcpy(staging.mapped, src.astc.data(), src.astc.size());
|
|
std::vector<VkBufferImageCopy> copies;
|
|
VkDeviceSize off = 0;
|
|
for (uint32_t m = 0; m < mips; ++m) {
|
|
const uint32_t w = std::max(1u, src.w >> m), h = std::max(1u, src.h >> m);
|
|
const VkDeviceSize bytes = static_cast<VkDeviceSize>((w + src.bw - 1) / src.bw) * ((h + src.bh - 1) / src.bh) * 16;
|
|
if (off + bytes * faces > src.astc.size()) break;
|
|
for (uint32_t f = 0; f < faces; ++f) {
|
|
VkBufferImageCopy c = {};
|
|
c.bufferOffset = off;
|
|
c.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, m, f, 1};
|
|
c.imageExtent = {w, h, 1};
|
|
copies.push_back(c);
|
|
off += bytes;
|
|
}
|
|
}
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, mips, faces, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
vkCmdCopyBufferToImage(c, staging.buf, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
static_cast<uint32_t>(copies.size()), copies.data());
|
|
imageBarrier(c, img.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, mips, faces, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
});
|
|
retire(staging);
|
|
return img;
|
|
}
|
|
|
|
VkImageView HslRenderer::hdrTexture(const std::vector<u16>* half, const std::shared_ptr<const TexSource>& src, u32 w, u32 h, bool exact) {
|
|
const uint64_t key = reinterpret_cast<uintptr_t>(half ? static_cast<const void*>(half) : src.get()) * 2 + 1;
|
|
auto it = textureCache_.find(key);
|
|
if (it != textureCache_.end()) return it->second.view;
|
|
if (!half && src && astcHdr_) {
|
|
const VkFormat unorm = astcFormat(src->bw, src->bh, false);
|
|
if (unorm != VK_FORMAT_UNDEFINED) { // the SFLOAT block formats follow the UNORM table's order
|
|
const VkFormat fmt = static_cast<VkFormat>(VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT + (unorm - VK_FORMAT_ASTC_4x4_UNORM_BLOCK) / 2);
|
|
Image img = createAstcTexture(*src, fmt);
|
|
if (!img.view) return white_.view;
|
|
keepAlive_.push_back(src);
|
|
textureCache_[key] = img;
|
|
return img.view;
|
|
}
|
|
}
|
|
std::vector<uint16_t> decoded;
|
|
const uint16_t* px = half ? half->data() : nullptr;
|
|
if (!half) {
|
|
// No HDR ASTC sampling (the PC): decode the chain's first level within maxDecodedSide().
|
|
if (!src || astcFormat(src->bw, src->bh, false) == VK_FORMAT_UNDEFINED) return white_.view; // (divides by bw, bh)
|
|
size_t off = 0;
|
|
auto levelBytes = [&](u32 lw, u32 lh) { return static_cast<size_t>((lw + src->bw - 1) / src->bw) * ((lh + src->bh - 1) / src->bh) * 16; };
|
|
w = src->w; h = src->h;
|
|
for (u32 m = 0; m + 1 < src->mips && std::max(w, h) > maxDecodedSide(); ++m) {
|
|
off += levelBytes(w, h);
|
|
w = std::max(1u, w / 2);
|
|
h = std::max(1u, h / 2);
|
|
}
|
|
if (off + levelBytes(w, h) > src->astc.size() ||
|
|
!astc::decodeASTCHalf(src->astc.data() + off, static_cast<u32>(levelBytes(w, h)), w, h, src->bw, src->bh, decoded))
|
|
return white_.view;
|
|
px = decoded.data();
|
|
}
|
|
if (!w || !h || (half ? half->size() : decoded.size()) / 4 / w < h) return white_.view;
|
|
std::vector<uint16_t> small;
|
|
// (A vertex-animation table shrunk this way averaged neighbouring frames and pieces: Cascadia's 98x2400
|
|
// centerpiece table became 49x1200 and every piece of the mobile over the pool vanished.)
|
|
while (!exact && std::max(w, h) > maxDecodedSide() && w > 1 && h > 1) {
|
|
const u32 nw = w / 2, nh = h / 2;
|
|
std::vector<uint16_t> out(static_cast<size_t>(nw) * nh * 4);
|
|
for (u32 y = 0; y < nh; ++y)
|
|
for (u32 x = 0; x < nw; ++x)
|
|
for (int c = 0; c < 4; ++c) {
|
|
auto at = [&](u32 sx, u32 sy) { return fromHalf(px[(static_cast<size_t>(sy) * w + sx) * 4 + c]); };
|
|
const float v = 0.25f * (at(2 * x, 2 * y) + at(2 * x + 1, 2 * y) + at(2 * x, 2 * y + 1) + at(2 * x + 1, 2 * y + 1));
|
|
out[(static_cast<size_t>(y) * nw + x) * 4 + c] = toHalf(v);
|
|
}
|
|
small = std::move(out);
|
|
px = small.data();
|
|
w = nw;
|
|
h = nh;
|
|
}
|
|
Image img = createTextureFmt(px, w, h, VK_FORMAT_R16G16B16A16_SFLOAT);
|
|
if (!img.view) return white_.view;
|
|
textureCache_[key] = img;
|
|
return img.view;
|
|
}
|
|
|
|
// A deferred texture: uploaded now as ASTC when the GPU can sample it; otherwise `placeholder` now and the
|
|
// decoded texture once a worker finishes (pumpTextures patches the descriptor).
|
|
VkImageView HslRenderer::streamTexture(const std::shared_ptr<const TexSource>& src, bool srgb, VkImageView placeholder) {
|
|
const TexKey key{src.get(), srgb};
|
|
auto it = streamed_.find(key);
|
|
if (it != streamed_.end()) return it->second;
|
|
// Cooked without mips and big: sampled as-is it shimmers and costs bandwidth at every distance. Its ASTC chain
|
|
// from the texture cache (tex_cache.h) when an earlier open built one; else decoded (in the background, within
|
|
// maxDecodedSide) with RGBA mips, while the cache builds its chain for next time.
|
|
std::shared_ptr<const TexSource> use = src;
|
|
bool noMips = src->mips <= 1 && std::max(src->w, src->h) > 256;
|
|
static const bool noTexCache = std::getenv("FH_NO_TEX_CACHE") != nullptr;
|
|
if (noMips && astcLdr_ && srgb && !noTexCache) { // (colour textures: linear ones may hold HDR blocks)
|
|
if (std::shared_ptr<TexSource> chain = texcache::load(*src)) { use = chain; noMips = false; }
|
|
else texcache::Builder::get().add(src);
|
|
} else if (noMips && !noTexCache) {
|
|
texcache::Builder::get().add(src); // (not drawn from here, but ready for saving: a .fenv bakes every chain)
|
|
}
|
|
const VkFormat fmt = astcLdr_ && !noMips ? astcFormat(use->bw, use->bh, srgb) : VK_FORMAT_UNDEFINED;
|
|
if (fmt != VK_FORMAT_UNDEFINED) {
|
|
Image img = createAstcTexture(*use, fmt);
|
|
if (!img.view) return streamed_[key] = placeholder; // refused (damaged sizes): the placeholder for good
|
|
textureCache_[reinterpret_cast<uintptr_t>(src.get()) * 2 + (srgb ? 1 : 0)] = img;
|
|
keepAlive_.push_back(use);
|
|
streamed_[key] = img.view;
|
|
return img.view;
|
|
}
|
|
if (fmt == VK_FORMAT_UNDEFINED && astcFormat(src->bw, src->bh, false) == VK_FORMAT_UNDEFINED)
|
|
return streamed_[key] = placeholder; // no ASTC footprint: nothing a worker could decode
|
|
slotPending_ = true;
|
|
pendingKey_ = key;
|
|
if (!pendingSrc_.count(key)) { // first user: decoded when a mesh using it comes into view
|
|
keepAlive_.push_back(src);
|
|
pendingSrc_[key] = src;
|
|
}
|
|
return placeholder;
|
|
}
|
|
|
|
void HslRenderer::startWorkers() {
|
|
// Half the cores at most, at low priority: decoding must never take time from the frame being drawn.
|
|
const unsigned hw = std::max(2u, std::thread::hardware_concurrency());
|
|
const unsigned n = std::max(1u, std::min(6u, hw / 2));
|
|
for (unsigned i = 0; i < n; ++i)
|
|
workers_.emplace_back([this] {
|
|
#ifdef _WIN32
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
|
#else
|
|
setpriority(PRIO_PROCESS, static_cast<id_t>(syscall(SYS_gettid)), 10);
|
|
#endif
|
|
for (;;) {
|
|
TexJob job;
|
|
{
|
|
std::unique_lock<std::mutex> l(jobMutex_);
|
|
jobCv_.wait(l, [&] { return stopping_ || !jobs_.empty(); });
|
|
if (stopping_) return;
|
|
job = std::move(jobs_.front());
|
|
jobs_.pop_front();
|
|
}
|
|
TexDone d{{job.src.get(), job.srgb}, job.gen, {}, false};
|
|
if (astcFormat(job.src->bw, job.src->bh, false) == VK_FORMAT_UNDEFINED) { // (levelBytes divides by them)
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
done_.push_back(std::move(d));
|
|
continue;
|
|
}
|
|
// The cooked chain already holds every mip: start from the first one within maxDecodedSide().
|
|
u32 w = job.src->w, h = job.src->h;
|
|
size_t off = 0;
|
|
auto levelBytes = [&](u32 lw, u32 lh) {
|
|
return static_cast<size_t>((lw + job.src->bw - 1) / job.src->bw) * ((lh + job.src->bh - 1) / job.src->bh) * 16;
|
|
};
|
|
for (u32 m = 0; m + 1 < job.src->mips && std::max(w, h) > maxDecodedSide(); ++m) {
|
|
const size_t next = off + levelBytes(w, h);
|
|
if (next + levelBytes(std::max(1u, w / 2), std::max(1u, h / 2)) > job.src->astc.size()) break;
|
|
off = next;
|
|
w = std::max(1u, w / 2);
|
|
h = std::max(1u, h / 2);
|
|
}
|
|
const size_t lvl = std::min(levelBytes(w, h), job.src->astc.size() - std::min(off, job.src->astc.size()));
|
|
std::vector<u8> rgba;
|
|
// Linear (UNORM) textures — normals, ORM, masks — may carry HDR-encoded ASTC blocks, which the LDR
|
|
// profile turns into magenta error texels (a corrupt ORM made the converted Cascadia's floor a
|
|
// mirror). Decode them with the HDR profile, unclamped, then clamp to 8 bits (HSR Studio decodes
|
|
// every UNORM ASTC texture through float too). Colour (sRGB) textures start on the LDR path.
|
|
bool decoded = false;
|
|
if (!job.srgb) {
|
|
std::vector<uint16_t> half;
|
|
if (astc::decodeASTCHalf(job.src->astc.data() + off, static_cast<u32>(lvl), w, h, job.src->bw, job.src->bh, half) &&
|
|
half.size() >= static_cast<size_t>(w) * h * 4) {
|
|
rgba.resize(static_cast<size_t>(w) * h * 4);
|
|
for (size_t q = 0; q < rgba.size(); ++q) {
|
|
const float v = fromHalf(half[q]);
|
|
rgba[q] = static_cast<u8>(std::lround(std::clamp(v, 0.0f, 1.0f) * 255.0f));
|
|
}
|
|
decoded = true;
|
|
}
|
|
}
|
|
if (decoded || astc::decodeASTC(job.src->astc.data() + off, static_cast<u32>(lvl), w, h, job.src->bw, job.src->bh, rgba, false, 1)) {
|
|
// A colour texture can be HDR-encoded too (the Focused vista's sky, water and glow): the LDR decode
|
|
// then fills it with magenta error texels. Spot that (a lot of exact-magenta texels) and re-decode
|
|
// through the HDR profile, sRGB-encoding the clamped linear colour so the sRGB view samples it
|
|
// right; alpha is linear either way.
|
|
if (job.srgb && !decoded && rgba.size() >= static_cast<size_t>(w) * h * 4) {
|
|
size_t mag = 0;
|
|
for (size_t q = 0; q + 3 < rgba.size(); q += 4)
|
|
if (rgba[q] > 250 && rgba[q + 1] < 8 && rgba[q + 2] > 250) ++mag;
|
|
std::vector<uint16_t> half;
|
|
if (mag * 4 > static_cast<size_t>(w) * h && // a quarter or more of the texels are the error colour
|
|
astc::decodeASTCHalf(job.src->astc.data() + off, static_cast<u32>(lvl), w, h, job.src->bw, job.src->bh, half) &&
|
|
half.size() >= static_cast<size_t>(w) * h * 4) {
|
|
auto toSrgb = [](float c) { c = std::clamp(c, 0.0f, 1.0f); return c <= 0.0031308f ? c * 12.92f : 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f; };
|
|
for (size_t q = 0; q < rgba.size(); ++q) {
|
|
const float v = fromHalf(half[q]);
|
|
rgba[q] = static_cast<u8>(std::lround(((q & 3) == 3 ? std::clamp(v, 0.0f, 1.0f) : toSrgb(v)) * 255.0f));
|
|
}
|
|
}
|
|
}
|
|
// A texture cooked without mips (a 6000^2 flipbook) arrives full size: brought within the cap here.
|
|
while (std::max(w, h) > maxDecodedSide() && rgba.size() >= static_cast<size_t>(w) * h * 4) halveRGBA(rgba, w, h);
|
|
// The image and its filled staging buffer are made here too: the frame only records the copy.
|
|
d.prep = prepareTexture(nullptr, w, h,
|
|
job.srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
}
|
|
if (d.prep.img.image && rgba.size() >= static_cast<size_t>(w) * h * 4) {
|
|
if (const char* dir = std::getenv("FH_DUMP_TEX")) { // diagnostics: every decoded texture as PPM
|
|
static std::atomic<int> n{0};
|
|
char path[512];
|
|
std::snprintf(path, sizeof path, "%s/%03d_%ux%u_%s_bw%u.ppm", dir, n++, w, h, job.srgb ? "srgb" : "lin", job.src->bw);
|
|
if (FILE* fp = std::fopen(path, "wb")) {
|
|
std::fprintf(fp, "P6\n%u %u\n255\n", w, h);
|
|
for (size_t q = 0; q + 3 < rgba.size(); q += 4) std::fwrite(&rgba[q], 1, 3, fp);
|
|
std::fclose(fp);
|
|
}
|
|
}
|
|
d.prep.pixels = std::move(rgba);
|
|
d.ok = true;
|
|
} else if (d.prep.img.image) {
|
|
destroy(d.prep.img);
|
|
}
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
done_.push_back(std::move(d));
|
|
}
|
|
});
|
|
}
|
|
|
|
void HslRenderer::stopWorkers() {
|
|
{
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
stopping_ = true;
|
|
}
|
|
jobCv_.notify_all();
|
|
for (auto& t : workers_) t.join();
|
|
workers_.clear();
|
|
}
|
|
|
|
// Uploads finished decodes and points every waiting descriptor at them (call between frames).
|
|
void HslRenderer::pumpTextures(double budgetMs) {
|
|
std::vector<TexDone> ready;
|
|
{
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
ready.swap(done_);
|
|
}
|
|
const auto start = std::chrono::steady_clock::now();
|
|
size_t i = 0;
|
|
for (; i < ready.size(); ++i) {
|
|
if (i && std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count() > budgetMs) break;
|
|
TexDone& d = ready[i];
|
|
if (d.gen != generation_ || !d.ok) {
|
|
if (d.ok) { destroy(d.prep.img); destroy(d.prep.staging); }
|
|
if (d.gen == generation_) waiting_.erase(d.key);
|
|
continue;
|
|
}
|
|
const VkDeviceSize bytes = d.prep.pixels.size();
|
|
const VkDeviceSize at = (ringHead_ + 255) & ~VkDeviceSize(255);
|
|
if (bytes > kRingSize) { // cannot happen for <= 8K RGBA8; own staging just in case
|
|
d.prep.staging = createBuffer(bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
if (!d.prep.staging.buf) { destroy(d.prep.img); waiting_.erase(d.key); continue; } // placeholder stays
|
|
std::memcpy(d.prep.staging.mapped, d.prep.pixels.data(), static_cast<size_t>(bytes));
|
|
d.prep.stagingOffset = 0;
|
|
oneShot([&](VkCommandBuffer c) { recordTextureUpload(c, d.prep); });
|
|
retire(d.prep.staging);
|
|
} else {
|
|
if (at + bytes > kRingSize) break; // ring full this frame: the rest next frame
|
|
std::memcpy(static_cast<u8*>(ring_.mapped) + at, d.prep.pixels.data(), static_cast<size_t>(bytes));
|
|
ringHead_ = at + bytes;
|
|
d.prep.staging = ring_; // borrowed, never freed here
|
|
d.prep.stagingOffset = at;
|
|
oneShot([&](VkCommandBuffer c) { recordTextureUpload(c, d.prep); });
|
|
d.prep.staging = Buffer{};
|
|
if (!recordInto_) ringHead_ = 0; // oneShot waited for the GPU: the ring is free again
|
|
}
|
|
d.prep.pixels.clear();
|
|
d.prep.pixels.shrink_to_fit();
|
|
++lastUploads_;
|
|
lastUploadMB_ += static_cast<double>(bytes) / (1024.0 * 1024.0);
|
|
Image img = d.prep.img;
|
|
textureCache_[reinterpret_cast<uintptr_t>(d.key.first) * 2 + (d.key.second ? 1 : 0)] = img;
|
|
streamed_[d.key] = img.view;
|
|
auto w = waiting_.find(d.key);
|
|
if (w == waiting_.end()) continue;
|
|
std::vector<VkWriteDescriptorSet> writes;
|
|
std::vector<std::vector<VkDescriptorImageInfo>> infos;
|
|
infos.reserve(w->second.size());
|
|
for (const TexSlot& s : w->second) {
|
|
if (s.mesh >= meshes_.size()) continue;
|
|
infos.emplace_back(s.count, VkDescriptorImageInfo{s.sampler, img.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL});
|
|
VkWriteDescriptorSet wr = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
|
wr.dstSet = meshes_[s.mesh].set2;
|
|
wr.dstBinding = s.binding;
|
|
wr.descriptorCount = s.count;
|
|
wr.descriptorType = s.type;
|
|
wr.pImageInfo = infos.back().data();
|
|
writes.push_back(wr);
|
|
}
|
|
if (!writes.empty()) vkUpdateDescriptorSets(device_, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
|
waiting_.erase(w);
|
|
}
|
|
if (i < ready.size()) { // over budget: the rest next frame
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
done_.insert(done_.begin(), std::make_move_iterator(ready.begin() + i), std::make_move_iterator(ready.end()));
|
|
}
|
|
}
|
|
|
|
size_t HslRenderer::texturesPending() { return waiting_.size(); }
|
|
|
|
void HslRenderer::createSelectionPipelines() {
|
|
VkDescriptorSetLayoutBinding b[2] = {};
|
|
b[0].binding = 0; b[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; b[0].descriptorCount = 1; b[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
|
b[1].binding = 1; b[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; b[1].descriptorCount = 1; b[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
VkDescriptorSetLayoutCreateInfo lci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
|
lci.bindingCount = 2; lci.pBindings = b;
|
|
vkCreateDescriptorSetLayout(device_, &lci, nullptr, &sel_.layout);
|
|
VkPipelineLayoutCreateInfo pli = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
|
pli.setLayoutCount = 1; pli.pSetLayouts = &sel_.layout;
|
|
vkCreatePipelineLayout(device_, &pli, nullptr, &sel_.pipeLayout);
|
|
|
|
const u8 orange[4] = {0xff, 0x8a, 0x1f, 0xff}; // Blender's selection orange (sRGB)
|
|
sel_.color = createTexture(orange, 1, 1, true);
|
|
sel_.ubo = createBuffer(256, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, true);
|
|
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
ai.descriptorPool = descPool_; ai.descriptorSetCount = 1; ai.pSetLayouts = &sel_.layout;
|
|
vkAllocateDescriptorSets(device_, &ai, &sel_.set);
|
|
VkDescriptorBufferInfo bi = {sel_.ubo.buf, 0, 64};
|
|
VkDescriptorImageInfo ii = {anisoSampler_, sel_.color.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
|
VkWriteDescriptorSet w[2] = {{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}};
|
|
w[0].dstSet = sel_.set; w[0].dstBinding = 0; w[0].descriptorCount = 1; w[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w[0].pBufferInfo = &bi;
|
|
w[1].dstSet = sel_.set; w[1].dstBinding = 1; w[1].descriptorCount = 1; w[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[1].pImageInfo = ⅈ
|
|
vkUpdateDescriptorSets(device_, 2, w, 0, nullptr);
|
|
|
|
auto module = [&](const uint32_t* code, size_t size) {
|
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
ci.codeSize = size; ci.pCode = code;
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
if (vkCreateShaderModule(device_, &ci, nullptr, &m) != VK_SUCCESS) m = VK_NULL_HANDLE;
|
|
return m;
|
|
};
|
|
VkShaderModule vs = module(kSelectionVertSpirv, kSelectionVertSize), fs = module(kSelectionFragSpirv, kSelectionFragSize);
|
|
if (!vs || !fs) { // no highlight rather than a pipeline over a null module
|
|
LOG("selection shaders failed");
|
|
if (vs) vkDestroyShaderModule(device_, vs, nullptr);
|
|
if (fs) vkDestroyShaderModule(device_, fs, nullptr);
|
|
destroySelection();
|
|
return;
|
|
}
|
|
VkPipelineShaderStageCreateInfo st[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
|
st[0].stage = VK_SHADER_STAGE_VERTEX_BIT; st[0].module = vs; st[0].pName = "main";
|
|
st[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; st[1].module = fs; st[1].pName = "main";
|
|
VkVertexInputBindingDescription vb = {0, 20, VK_VERTEX_INPUT_RATE_VERTEX};
|
|
VkVertexInputAttributeDescription va[2] = {{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, {1, 0, VK_FORMAT_R32G32_SFLOAT, 12}};
|
|
VkPipelineVertexInputStateCreateInfo vi = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
|
vi.vertexBindingDescriptionCount = 1; vi.pVertexBindingDescriptions = &vb;
|
|
vi.vertexAttributeDescriptionCount = 2; vi.pVertexAttributeDescriptions = va;
|
|
VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
|
VkPipelineViewportStateCreateInfo vps = {VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
|
vps.viewportCount = 1; vps.scissorCount = 1;
|
|
VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
|
rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
|
|
VkPipelineMultisampleStateCreateInfo ms = {VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
|
ms.rasterizationSamples = samples_;
|
|
VkPipelineDepthStencilStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
|
ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; ds.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL;
|
|
VkPipelineColorBlendAttachmentState cb = {};
|
|
cb.colorWriteMask = 0xF;
|
|
VkPipelineColorBlendStateCreateInfo cbs = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
|
cbs.attachmentCount = 1; cbs.pAttachments = &cb;
|
|
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
|
VkPipelineDynamicStateCreateInfo dsi = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
|
dsi.dynamicStateCount = 2; dsi.pDynamicStates = dyn;
|
|
VkGraphicsPipelineCreateInfo pci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
pci.stageCount = 2; pci.pStages = st; pci.pVertexInputState = &vi; pci.pInputAssemblyState = &ia; pci.pViewportState = &vps;
|
|
pci.pRasterizationState = &rs; pci.pMultisampleState = &ms; pci.pDepthStencilState = &ds; pci.pColorBlendState = &cbs;
|
|
pci.pDynamicState = &dsi; pci.layout = sel_.pipeLayout; pci.renderPass = renderPass_;
|
|
// edges: opaque lines
|
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
|
|
vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &sel_.lines);
|
|
// faces: a faint tint (blend factor = constant alpha)
|
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
cb.blendEnable = VK_TRUE;
|
|
cb.srcColorBlendFactor = VK_BLEND_FACTOR_CONSTANT_ALPHA; cb.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA;
|
|
cb.colorBlendOp = VK_BLEND_OP_ADD;
|
|
cb.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cb.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cb.alphaBlendOp = VK_BLEND_OP_ADD;
|
|
cbs.blendConstants[3] = 0.12f;
|
|
vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &sel_.faces);
|
|
vkDestroyShaderModule(device_, vs, nullptr);
|
|
vkDestroyShaderModule(device_, fs, nullptr);
|
|
}
|
|
|
|
void HslRenderer::setSelection(int mesh) {
|
|
if (mesh == sel_.mesh) return;
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
if (!sel_.lines) createSelectionPipelines();
|
|
if (!sel_.lines || !sel_.faces) return;
|
|
if (sel_.vbo.buf || sel_.edges.buf || sel_.tris.buf) {
|
|
vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX); // the last frame may still draw the old one
|
|
destroy(sel_.vbo); destroy(sel_.edges); destroy(sel_.tris);
|
|
}
|
|
sel_.mesh = -1;
|
|
sel_.edgeCount = sel_.triCount = 0;
|
|
if (mesh < 0 || static_cast<size_t>(mesh) >= meshes_.size() || !scene_) return;
|
|
const MeshData& md = scene_->meshes[meshes_[mesh].sceneIndex];
|
|
const size_t nv = md.positions.size() / 3;
|
|
if (!nv || md.indices.size() < 3) return;
|
|
std::vector<float> v(nv * 5, 0.0f);
|
|
for (size_t i = 0; i < nv; ++i) std::memcpy(&v[i * 5], &md.positions[i * 3], 12);
|
|
// Unique edges (shared by two triangles: drawn once). Sorted keys, not a hash set: a few times faster, and
|
|
// selecting a big mesh in Studio held up a frame for it.
|
|
std::vector<u32> edges, tris;
|
|
std::vector<uint64_t> keys;
|
|
keys.reserve(md.indices.size());
|
|
tris.reserve(md.indices.size());
|
|
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
|
const u32 idx[3] = {md.indices[t], md.indices[t + 1], md.indices[t + 2]};
|
|
if (idx[0] >= nv || idx[1] >= nv || idx[2] >= nv) continue;
|
|
tris.insert(tris.end(), idx, idx + 3);
|
|
for (int e = 0; e < 3; ++e) {
|
|
const u32 a = std::min(idx[e], idx[(e + 1) % 3]), b = std::max(idx[e], idx[(e + 1) % 3]);
|
|
keys.push_back(static_cast<uint64_t>(a) << 32 | b);
|
|
}
|
|
}
|
|
if (tris.empty()) return;
|
|
std::sort(keys.begin(), keys.end());
|
|
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
|
edges.reserve(keys.size() * 2);
|
|
for (uint64_t k : keys) { edges.push_back(static_cast<u32>(k >> 32)); edges.push_back(static_cast<u32>(k)); }
|
|
const bool batch = !loadCmd_ && !recordInto_; // the three uploads in one submit (one wait, not three)
|
|
if (batch) beginUploadBatch();
|
|
sel_.vbo = createDeviceBuffer(v.data(), v.size() * 4, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
|
sel_.edges = createDeviceBuffer(edges.data(), edges.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
|
sel_.tris = createDeviceBuffer(tris.data(), tris.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
|
if (batch) flushUploadBatch();
|
|
if (!sel_.vbo.buf || !sel_.edges.buf || !sel_.tris.buf) { destroy(sel_.vbo); destroy(sel_.edges); destroy(sel_.tris); return; }
|
|
sel_.edgeCount = static_cast<uint32_t>(edges.size());
|
|
sel_.triCount = static_cast<uint32_t>(tris.size());
|
|
sel_.mesh = mesh;
|
|
const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
|
if (ms > 3.0) LOG("selection highlight built in %.1f ms (%u edges, %u triangles)", ms, sel_.edgeCount / 2, sel_.triCount / 3);
|
|
}
|
|
|
|
void HslRenderer::createGuidePipelines() {
|
|
VkPushConstantRange pcr = {VK_SHADER_STAGE_VERTEX_BIT, 0, 128}; // clipFromWorld[2]
|
|
VkPipelineLayoutCreateInfo pli = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
|
pli.pushConstantRangeCount = 1; pli.pPushConstantRanges = &pcr;
|
|
if (vkCreatePipelineLayout(device_, &pli, nullptr, &guide_.layout) != VK_SUCCESS) { guide_.layout = VK_NULL_HANDLE; return; }
|
|
auto module = [&](const uint32_t* code, size_t size) {
|
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
ci.codeSize = size; ci.pCode = code;
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
if (vkCreateShaderModule(device_, &ci, nullptr, &m) != VK_SUCCESS) m = VK_NULL_HANDLE;
|
|
return m;
|
|
};
|
|
VkShaderModule vs = module(kGuideVertSpirv, kGuideVertSize), fs = module(kGuideFragSpirv, kGuideFragSize);
|
|
if (vs && fs) {
|
|
// spec constant 0: the alpha scale (1 in front of the home, faint behind it)
|
|
float alphaScale = 1.0f;
|
|
VkSpecializationMapEntry me = {0, 0, sizeof(float)};
|
|
VkSpecializationInfo si = {1, &me, sizeof(float), &alphaScale};
|
|
VkPipelineShaderStageCreateInfo st[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
|
st[0].stage = VK_SHADER_STAGE_VERTEX_BIT; st[0].module = vs; st[0].pName = "main"; st[0].pSpecializationInfo = &si;
|
|
st[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; st[1].module = fs; st[1].pName = "main";
|
|
VkVertexInputBindingDescription vb = {0, sizeof(GuideVertex), VK_VERTEX_INPUT_RATE_VERTEX};
|
|
VkVertexInputAttributeDescription va[2] = {{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 12}};
|
|
VkPipelineVertexInputStateCreateInfo vi = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
|
vi.vertexBindingDescriptionCount = 1; vi.pVertexBindingDescriptions = &vb;
|
|
vi.vertexAttributeDescriptionCount = 2; vi.pVertexAttributeDescriptions = va;
|
|
VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
VkPipelineViewportStateCreateInfo vps = {VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
|
vps.viewportCount = 1; vps.scissorCount = 1;
|
|
VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
|
rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
|
|
VkPipelineMultisampleStateCreateInfo ms = {VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
|
ms.rasterizationSamples = samples_;
|
|
VkPipelineDepthStencilStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
|
ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; // never writes: the guide doesn't hide itself
|
|
ds.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; // reversed-Z: in front of the home
|
|
VkPipelineColorBlendAttachmentState cb = {};
|
|
cb.colorWriteMask = 0xF;
|
|
cb.blendEnable = VK_TRUE;
|
|
cb.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cb.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cb.colorBlendOp = VK_BLEND_OP_ADD;
|
|
cb.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cb.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cb.alphaBlendOp = VK_BLEND_OP_ADD; // the frame's alpha stays the home's
|
|
VkPipelineColorBlendStateCreateInfo cbs = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
|
cbs.attachmentCount = 1; cbs.pAttachments = &cb;
|
|
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
|
VkPipelineDynamicStateCreateInfo dsi = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
|
dsi.dynamicStateCount = 2; dsi.pDynamicStates = dyn;
|
|
VkGraphicsPipelineCreateInfo pci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
pci.stageCount = 2; pci.pStages = st; pci.pVertexInputState = &vi; pci.pInputAssemblyState = &ia; pci.pViewportState = &vps;
|
|
pci.pRasterizationState = &rs; pci.pMultisampleState = &ms; pci.pDepthStencilState = &ds; pci.pColorBlendState = &cbs;
|
|
pci.pDynamicState = &dsi; pci.layout = guide_.layout; pci.renderPass = renderPass_;
|
|
if (vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &guide_.visible) != VK_SUCCESS) guide_.visible = VK_NULL_HANDLE;
|
|
// Behind the home (a sofa between you and the landing): still there, at a quarter strength.
|
|
ds.depthCompareOp = VK_COMPARE_OP_LESS;
|
|
alphaScale = 0.25f;
|
|
if (vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &guide_.hidden) != VK_SUCCESS) guide_.hidden = VK_NULL_HANDLE;
|
|
}
|
|
if (vs) vkDestroyShaderModule(device_, vs, nullptr);
|
|
if (fs) vkDestroyShaderModule(device_, fs, nullptr);
|
|
if (!guide_.visible) { LOG("guide shaders failed: no teleport arc"); destroyGuide(); }
|
|
}
|
|
|
|
void HslRenderer::setStaticGuide(const std::vector<GuideVertex>& tris) {
|
|
if (!device_) return;
|
|
if (guide_.staticVbo.buf) { vkDeviceWaitIdle(device_); destroy(guide_.staticVbo); }
|
|
guide_.staticVertexCount = 0;
|
|
static const bool noGuide = std::getenv("FH_NO_GUIDE") != nullptr; // diagnostics: the home alone (no walk-area overlay)
|
|
if (tris.empty() || !guide_.visible || noGuide) return;
|
|
guide_.staticVbo = createDeviceBuffer(tris.data(), tris.size() * sizeof(GuideVertex), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
|
if (guide_.staticVbo.buf) guide_.staticVertexCount = static_cast<uint32_t>(tris.size());
|
|
}
|
|
|
|
void HslRenderer::destroyGuide() {
|
|
for (Buffer& b : guide_.vbo) destroy(b);
|
|
destroy(guide_.staticVbo);
|
|
if (guide_.visible) vkDestroyPipeline(device_, guide_.visible, nullptr);
|
|
if (guide_.hidden) vkDestroyPipeline(device_, guide_.hidden, nullptr);
|
|
if (guide_.layout) vkDestroyPipelineLayout(device_, guide_.layout, nullptr);
|
|
guide_ = Guide{};
|
|
}
|
|
|
|
// Inside the frame's render pass, after the home (into this command buffer's own guide buffer).
|
|
void HslRenderer::drawGuide(const EyeView eyes[2]) {
|
|
if (!guide_.visible) return;
|
|
if (guide_.staticVertexCount) {
|
|
float vp[32];
|
|
for (int e = 0; e < 2; ++e) mat4mul4(eyes[e].proj.m, eyes[e].view.m, vp + 16 * e);
|
|
VkDeviceSize zero = 0;
|
|
vkCmdBindVertexBuffers(cmd_, 0, 1, &guide_.staticVbo.buf, &zero);
|
|
vkCmdPushConstants(cmd_, guide_.layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof vp, vp);
|
|
if (guide_.hidden) { vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, guide_.hidden); vkCmdDraw(cmd_, guide_.staticVertexCount, 1, 0, 0); }
|
|
vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, guide_.visible);
|
|
vkCmdDraw(cmd_, guide_.staticVertexCount, 1, 0, 0);
|
|
}
|
|
if (guideVerts_.empty()) return;
|
|
const VkDeviceSize bytes = guideVerts_.size() * sizeof(GuideVertex);
|
|
// This command buffer's own copy: the other one's may still be drawn from (a frame recorded early).
|
|
Buffer& vbo = guide_.vbo[cmdIndex_];
|
|
if (vbo.size < bytes) {
|
|
destroy(vbo);
|
|
vbo = createBuffer(std::max<VkDeviceSize>(bytes * 3 / 2, 64 << 10), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, true);
|
|
if (!vbo.mapped) return;
|
|
}
|
|
std::memcpy(vbo.mapped, guideVerts_.data(), static_cast<size_t>(bytes));
|
|
float vp[32];
|
|
for (int e = 0; e < 2; ++e) mat4mul4(eyes[e].proj.m, eyes[e].view.m, vp + 16 * e);
|
|
VkDeviceSize zero = 0;
|
|
vkCmdBindVertexBuffers(cmd_, 0, 1, &vbo.buf, &zero);
|
|
vkCmdPushConstants(cmd_, guide_.layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof vp, vp);
|
|
const uint32_t n = static_cast<uint32_t>(guideVerts_.size());
|
|
if (guide_.hidden) { vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, guide_.hidden); vkCmdDraw(cmd_, n, 1, 0, 0); }
|
|
vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, guide_.visible);
|
|
vkCmdDraw(cmd_, n, 1, 0, 0);
|
|
}
|
|
|
|
// ── Debug views ──
|
|
|
|
void HslRenderer::setDebugView(int view) {
|
|
view = std::clamp(view, 0, static_cast<int>(kDebugViews) - 1);
|
|
if (view == kViewWireframe && !wireOk_) view = kViewMeshes; // no line drawing on this GPU: the closest thing
|
|
if (view != kViewNormal && !dbg_.layout && !createDebug()) view = kViewNormal;
|
|
if (view != debugView_) LOG("debug view %d", view);
|
|
if (view != kViewCost) cost_.clear();
|
|
costMin_.clear(); costSeen_.clear(); std::fill(std::begin(costBaseRing_), std::end(costBaseRing_), 1e9f);
|
|
doubled_ = -1;
|
|
debugView_ = view;
|
|
}
|
|
|
|
std::vector<std::pair<std::string, double>> HslRenderer::perfCounterAverages() const {
|
|
std::vector<std::pair<std::string, double>> out;
|
|
for (size_t i = 0; i < perfNames_.size() && perfFrames_; ++i) out.push_back({perfNames_[i], perfSum_[i] / perfFrames_});
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::pair<float, int>> HslRenderer::costliest(size_t n) const {
|
|
std::vector<std::pair<float, int>> out;
|
|
for (size_t i = 0; i < cost_.size() && i < meshes_.size(); ++i)
|
|
if (cost_[i] > 0) out.push_back({cost_[i], meshes_[i].sceneIndex});
|
|
std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
|
|
if (out.size() > n) out.resize(n);
|
|
return out;
|
|
}
|
|
|
|
bool HslRenderer::createDebug() {
|
|
VkDescriptorSetLayoutBinding b = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, nullptr};
|
|
VkDescriptorSetLayoutCreateInfo lci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
|
lci.bindingCount = 1; lci.pBindings = &b;
|
|
if (vkCreateDescriptorSetLayout(device_, &lci, nullptr, &dbg_.setLayout) != VK_SUCCESS) { dbg_.setLayout = VK_NULL_HANDLE; return false; }
|
|
VkPipelineLayoutCreateInfo pli = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
|
pli.setLayoutCount = 1; pli.pSetLayouts = &dbg_.setLayout;
|
|
if (vkCreatePipelineLayout(device_, &pli, nullptr, &dbg_.layout) != VK_SUCCESS) { dbg_.layout = VK_NULL_HANDLE; destroyDebug(); return false; }
|
|
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
ai.descriptorPool = descPool_; ai.descriptorSetCount = 1; ai.pSetLayouts = &dbg_.setLayout;
|
|
if (vkAllocateDescriptorSets(device_, &ai, &dbg_.set) != VK_SUCCESS) { dbg_.set = VK_NULL_HANDLE; destroyDebug(); return false; }
|
|
auto module = [&](const uint32_t* code, size_t size) {
|
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
ci.codeSize = size; ci.pCode = code;
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
if (vkCreateShaderModule(device_, &ci, nullptr, &m) != VK_SUCCESS) m = VK_NULL_HANDLE;
|
|
return m;
|
|
};
|
|
dbg_.vs = module(kDebugVertSpirv, kDebugVertSize);
|
|
dbg_.fs = module(kDebugFragSpirv, kDebugFragSize);
|
|
if (!dbg_.vs || !dbg_.fs) { LOG("debug view shaders failed"); destroyDebug(); return false; }
|
|
return true;
|
|
}
|
|
|
|
void HslRenderer::destroyDebug() {
|
|
for (auto& kv : dbg_.pipes) if (kv.second) vkDestroyPipeline(device_, kv.second, nullptr);
|
|
destroy(dbg_.draws);
|
|
if (dbg_.vs) vkDestroyShaderModule(device_, dbg_.vs, nullptr);
|
|
if (dbg_.fs) vkDestroyShaderModule(device_, dbg_.fs, nullptr);
|
|
if (dbg_.set) vkFreeDescriptorSets(device_, descPool_, 1, &dbg_.set);
|
|
if (dbg_.layout) vkDestroyPipelineLayout(device_, dbg_.layout, nullptr);
|
|
if (dbg_.setLayout) vkDestroyDescriptorSetLayout(device_, dbg_.setLayout, nullptr);
|
|
dbg_ = Debug{};
|
|
}
|
|
|
|
// The program's vertex buffer read for its positions only: one pipeline per layout and mode (VK_NULL_HANDLE when the
|
|
// program has no position attribute).
|
|
VkPipeline HslRenderer::debugPipeline(const Program& p, int mode) {
|
|
const VertexAttr* pos = nullptr;
|
|
for (const VertexAttr& a : p.attrs) if (a.role == Role::Pos && !a.integer) { pos = &a; break; }
|
|
if (!pos || !dbg_.layout) return VK_NULL_HANDLE;
|
|
const uint64_t key = (static_cast<uint64_t>(p.stride) << 40) | (static_cast<uint64_t>(pos->offset) << 24) |
|
|
(static_cast<uint64_t>(pos->format) << 4) | static_cast<uint64_t>(mode);
|
|
if (auto it = dbg_.pipes.find(key); it != dbg_.pipes.end()) return it->second;
|
|
const int shade = mode == kDbgSolid || mode == kDbgTint ? 1 : 0;
|
|
VkSpecializationMapEntry me = {0, 0, sizeof(int)};
|
|
VkSpecializationInfo si = {1, &me, sizeof(int), &shade};
|
|
VkPipelineShaderStageCreateInfo st[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
|
st[0].stage = VK_SHADER_STAGE_VERTEX_BIT; st[0].module = dbg_.vs; st[0].pName = "main";
|
|
st[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; st[1].module = dbg_.fs; st[1].pName = "main"; st[1].pSpecializationInfo = &si;
|
|
VkVertexInputBindingDescription vb = {0, p.stride, VK_VERTEX_INPUT_RATE_VERTEX};
|
|
VkVertexInputAttributeDescription va = {0, 0, pos->format, pos->offset};
|
|
VkPipelineVertexInputStateCreateInfo vi = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
|
vi.vertexBindingDescriptionCount = 1; vi.pVertexBindingDescriptions = &vb;
|
|
vi.vertexAttributeDescriptionCount = 1; vi.pVertexAttributeDescriptions = &va;
|
|
VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
VkPipelineViewportStateCreateInfo vps = {VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
|
vps.viewportCount = 1; vps.scissorCount = 1;
|
|
VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
|
rs.polygonMode = mode == kDbgWire ? VK_POLYGON_MODE_LINE : VK_POLYGON_MODE_FILL;
|
|
rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
|
|
VkPipelineMultisampleStateCreateInfo ms = {VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
|
ms.rasterizationSamples = samples_;
|
|
VkPipelineDepthStencilStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
|
ds.depthTestEnable = mode == kDbgAdd ? VK_FALSE : VK_TRUE;
|
|
ds.depthWriteEnable = mode == kDbgSolid ? VK_TRUE : VK_FALSE;
|
|
ds.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; // reversed-Z
|
|
VkPipelineColorBlendAttachmentState cb = {};
|
|
cb.colorWriteMask = 0xF;
|
|
cb.blendEnable = mode == kDbgSolid ? VK_FALSE : VK_TRUE;
|
|
cb.srcColorBlendFactor = mode == kDbgAdd ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_SRC_ALPHA;
|
|
cb.dstColorBlendFactor = mode == kDbgAdd ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
cb.colorBlendOp = VK_BLEND_OP_ADD;
|
|
cb.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cb.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cb.alphaBlendOp = VK_BLEND_OP_ADD;
|
|
if (mode == kDbgSolid) { cb.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cb.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; }
|
|
VkPipelineColorBlendStateCreateInfo cbs = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
|
cbs.attachmentCount = 1; cbs.pAttachments = &cb;
|
|
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
|
VkPipelineDynamicStateCreateInfo dsi = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
|
dsi.dynamicStateCount = 2; dsi.pDynamicStates = dyn;
|
|
VkGraphicsPipelineCreateInfo pci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
pci.stageCount = 2; pci.pStages = st; pci.pVertexInputState = &vi; pci.pInputAssemblyState = &ia; pci.pViewportState = &vps;
|
|
pci.pRasterizationState = &rs; pci.pMultisampleState = &ms; pci.pDepthStencilState = &ds; pci.pColorBlendState = &cbs;
|
|
pci.pDynamicState = &dsi; pci.layout = dbg_.layout; pci.renderPass = renderPass_;
|
|
VkPipeline pipe = VK_NULL_HANDLE;
|
|
if (vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
|
|
dbg_.pipes[key] = pipe;
|
|
return pipe;
|
|
}
|
|
|
|
// One mesh in a debug mode: its matrices and colour go in the next slot of the draws buffer (render() made room).
|
|
// nudge: a hair toward the eye, to sit on the surface the home drew (wireframe, tint).
|
|
void HslRenderer::drawDebug(const GpuMesh& gm, int mode, const float color[4], const EyeView eyes[2], bool nudge) {
|
|
if (dbg_.used >= dbg_.capacity || !dbg_.draws.mapped || gm.program < 0) return;
|
|
VkPipeline pipe = debugPipeline(programs_[gm.program], mode);
|
|
if (!pipe) return;
|
|
float* d = static_cast<float*>(dbg_.draws.mapped) + dbg_.used * 52; // Draw: mat4 clipFromModel[2], mat4 world, vec4 colour
|
|
for (int e = 0; e < 2; ++e) {
|
|
float vp[16];
|
|
mat4mul4(eyes[e].proj.m, eyes[e].view.m, vp);
|
|
mat4mul4(vp, gm.world, d + 16 * e);
|
|
if (nudge) for (int c = 0; c < 4; ++c) d[16 * e + c * 4 + 2] *= 1.0015f; // reversed-Z: nearer
|
|
}
|
|
std::memcpy(d + 32, gm.world, 64);
|
|
std::memcpy(d + 48, color, 16);
|
|
VkDeviceSize zero = 0;
|
|
vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
|
|
vkCmdBindDescriptorSets(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, dbg_.layout, 0, 1, &dbg_.set, 0, nullptr);
|
|
vkCmdBindVertexBuffers(cmd_, 0, 1, &gm.vbo.buf, &zero);
|
|
vkCmdBindIndexBuffer(cmd_, gm.ibo.buf, 0, VK_INDEX_TYPE_UINT32);
|
|
vkCmdDrawIndexed(cmd_, gm.indexCount, 1, 0, 0, dbg_.used);
|
|
++dbg_.used;
|
|
}
|
|
|
|
void HslRenderer::destroySelection() {
|
|
destroy(sel_.vbo); destroy(sel_.edges); destroy(sel_.tris); destroy(sel_.ubo); destroy(sel_.color);
|
|
if (sel_.lines) vkDestroyPipeline(device_, sel_.lines, nullptr);
|
|
if (sel_.faces) vkDestroyPipeline(device_, sel_.faces, nullptr);
|
|
if (sel_.pipeLayout) vkDestroyPipelineLayout(device_, sel_.pipeLayout, nullptr);
|
|
if (sel_.layout) vkDestroyDescriptorSetLayout(device_, sel_.layout, nullptr);
|
|
sel_ = Selection{};
|
|
}
|
|
|
|
void HslRenderer::setDynamicMeshes(const std::vector<int>& sceneIndices) {
|
|
dynamicScene_.clear();
|
|
dynamicScene_.insert(sceneIndices.begin(), sceneIndices.end());
|
|
}
|
|
|
|
// The previous frame is finished (its fence waited), so the mapped vertex buffers are free to rewrite.
|
|
void HslRenderer::refreshDynamicMeshes(bool cpu, bool upload) {
|
|
if (!scene_) return;
|
|
std::vector<DynJob>& jobs = dynJobs_;
|
|
if (cpu) { // (an early-recorded frame ran this part before it was recorded, and uploads after)
|
|
jobs.clear();
|
|
skinCopies_.clear();
|
|
stats_.dynamicMeshes = stats_.dynamicRebuilt = 0;
|
|
static const bool traceOn = std::getenv("FH_DYN_TRACE") != nullptr; // diagnostics: what re-uploads (one frame in 600)
|
|
static uint32_t traceFrame = 0;
|
|
const bool trace = traceOn && traceFrame++ % 600 == 300;
|
|
for (auto& gm : meshes_) {
|
|
if (!gm.dynamic || gm.sceneIndex < 0 || static_cast<size_t>(gm.sceneIndex) >= scene_->meshes.size()) continue;
|
|
const MeshData& md = scene_->meshes[gm.sceneIndex];
|
|
++stats_.dynamicMeshes;
|
|
std::memcpy(gm.color, md.curTint, sizeof gm.color);
|
|
std::memcpy(gm.uvXform, md.uvXform, sizeof gm.uvXform);
|
|
if (md.skinGen != gm.skinGen && gm.skinUbo >= 0 && !md.skinJoints.empty()) { // skinned on the GPU: its joints
|
|
gm.skinGen = md.skinGen;
|
|
skinCopies_.push_back(&gm);
|
|
for (int k = 0; k < 3; ++k) {
|
|
gm.localMin[k] = md.skinBox[k]; gm.localMax[k] = md.skinBox[3 + k];
|
|
gm.localCentroid[k] = 0.5f * (md.skinBox[k] + md.skinBox[3 + k]);
|
|
}
|
|
placeMesh(gm);
|
|
}
|
|
if (std::memcmp(gm.anim, md.animXform, sizeof gm.anim) != 0) { // rigid motion: a matrix, no vertices
|
|
std::memcpy(gm.anim, md.animXform, sizeof gm.anim);
|
|
placeMesh(gm);
|
|
}
|
|
// Most animated meshes hold still for part of the loop (and a tint-only or rigid animation never changes
|
|
// them): rebuild the vertices only when the loader changed the positions or uvs, and only that half.
|
|
static const bool noSkip = std::getenv("FH_NO_DYN_SKIP") != nullptr; // A/B: rebuild every frame (the old way)
|
|
const bool posChanged = md.posGen != gm.posGen || noSkip, uvChanged = md.uvGen != gm.uvGen || noSkip;
|
|
if (!posChanged && !uvChanged) continue;
|
|
gm.posGen = md.posGen; gm.uvGen = md.uvGen;
|
|
++stats_.dynamicRebuilt;
|
|
if (trace)
|
|
LOG("dyn rebuild '%s': %u verts, %s%s", md.name.c_str(), md.nVerts, posChanged ? "positions " : "", uvChanged ? "uvs" : "");
|
|
if (posChanged) measureMesh(gm, md, static_cast<u32>(std::min<size_t>(md.nVerts, md.positions.size() / 3))); // (culling)
|
|
jobs.push_back({&gm, &md, posChanged, uvChanged});
|
|
}
|
|
}
|
|
if (!upload) return;
|
|
for (GpuMesh* g : skinCopies_) {
|
|
const MeshData& md = scene_->meshes[g->sceneIndex];
|
|
Buffer& b = g->ubos[g->skinUbo];
|
|
std::memcpy(b.mapped, md.skinJoints.data(), std::min<size_t>(md.skinJoints.size() * sizeof(float), static_cast<size_t>(b.size)));
|
|
}
|
|
// The rebuilds (skinned meshes: thousands of vertices each) on all cores: each writes only its own mesh.
|
|
par::forEach(jobs.size(), [&](size_t j) {
|
|
GpuMesh& gm = *jobs[j].gm;
|
|
const MeshData& md = *jobs[j].md;
|
|
const bool posChanged = jobs[j].pos, uvChanged = jobs[j].uv;
|
|
static thread_local std::vector<u8> vbo;
|
|
static thread_local std::vector<float> normals;
|
|
const Program& p = programs_[gm.program];
|
|
const u32 nv = static_cast<u32>(std::min<size_t>(md.nVerts, md.positions.size() / 3));
|
|
static const bool fullRebuild = std::getenv("FH_DYN_FULL") != nullptr; // A/B: the whole interleave
|
|
if (fullRebuild || !gm.vbo.mapped || static_cast<VkDeviceSize>(nv) * p.stride > gm.vbo.size) {
|
|
buildVertices(p, md, vbo, normals);
|
|
if (vbo.size() <= gm.vbo.size && gm.vbo.mapped) std::memcpy(gm.vbo.mapped, vbo.data(), vbo.size());
|
|
return;
|
|
}
|
|
// Only what an animation changes (positions, uvs, and the smooth normals that follow the positions) is
|
|
// rewritten, in place: colours, tangents, bones and the rest stay as uploaded.
|
|
bool computed = false;
|
|
for (const auto& a : p.attrs)
|
|
if (a.role == Role::Normal && md.normals.size() < static_cast<size_t>(nv) * 3) computed = true;
|
|
if (!posChanged) computed = false; // (the normals follow the positions)
|
|
else if (!computed)
|
|
for (const auto& a : p.attrs)
|
|
if (a.role == Role::Normal)
|
|
for (u32 v = 0; v < nv && !computed; ++v)
|
|
computed = md.normals[v * 3] == 0 && md.normals[v * 3 + 1] == 0 && md.normals[v * 3 + 2] == 0;
|
|
if (computed && posChanged) {
|
|
normals.assign(static_cast<size_t>(nv) * 3, 0.0f);
|
|
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
|
const u32 a = md.indices[t], b = md.indices[t + 1], c = md.indices[t + 2];
|
|
if (a >= nv || b >= nv || c >= nv) continue;
|
|
const float* pa = &md.positions[a * 3]; const float* pb = &md.positions[b * 3]; const float* pc = &md.positions[c * 3];
|
|
const float e1[3] = {pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]}, e2[3] = {pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]};
|
|
const float n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]};
|
|
for (u32 v : {a, b, c}) for (int k = 0; k < 3; ++k) normals[v * 3 + k] += n[k];
|
|
}
|
|
for (u32 v = 0; v < nv; ++v) {
|
|
float* n = &normals[v * 3];
|
|
const float l = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
|
if (l > 1e-12f) { n[0] /= l; n[1] /= l; n[2] /= l; } else { n[1] = 1; } // (as buildVertices)
|
|
}
|
|
}
|
|
u8* base = static_cast<u8*>(gm.vbo.mapped);
|
|
for (const auto& a : p.attrs) {
|
|
if (a.integer || (a.role != Role::Pos && a.role != Role::Normal && a.role != Role::Uv0 && a.role != Role::Uv1)) continue;
|
|
if ((a.role == Role::Pos || a.role == Role::Normal) ? !posChanged : !uvChanged) continue;
|
|
if (a.role == Role::Uv1 && md.uvs2.size() >= static_cast<size_t>(nv) * 2) continue; // its own UV2: not animated
|
|
const bool half = a.format >= VK_FORMAT_R16_SFLOAT && a.format <= VK_FORMAT_R16G16B16A16_SFLOAT;
|
|
for (u32 v = 0; v < nv; ++v) {
|
|
float tmp[4] = {0, 0, 0, 0};
|
|
switch (a.role) {
|
|
case Role::Pos: std::memcpy(tmp, &(md.skinBindPos.size() >= static_cast<size_t>(nv) * 3 ? md.skinBindPos : md.positions)[v * 3], 12); break;
|
|
case Role::Normal: {
|
|
const float* an = !computed ? &md.normals[v * 3] : nullptr;
|
|
std::memcpy(tmp, an ? an : &normals[v * 3], 12);
|
|
break;
|
|
}
|
|
default: // Uv0, or Uv1 falling back to the uvs (as buildVertices does)
|
|
if (v * 2 + 1 < md.uvs.size()) { tmp[0] = md.uvs[v * 2]; tmp[1] = md.uvs[v * 2 + 1]; }
|
|
break;
|
|
}
|
|
u8* out = base + static_cast<size_t>(v) * p.stride + a.offset;
|
|
if (half) { for (uint32_t k = 0; k < a.size / 2; ++k) { const uint16_t h = toHalf(k < 4 ? tmp[k] : 0); std::memcpy(out + k * 2, &h, 2); } }
|
|
else std::memcpy(out, tmp, std::min<uint32_t>(a.size, 16));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
bool HslRenderer::gpuBusy() const { return fence_ && vkGetFenceStatus(device_, fence_) == VK_NOT_READY; }
|
|
|
|
void HslRenderer::requestAllTextures() {
|
|
for (auto& gm : meshes_) requestTextures(gm);
|
|
texturesRequested_ = waiting_.size();
|
|
preloading_ = texturesRequested_ > 0;
|
|
LOG("preloading %zu textures", texturesRequested_);
|
|
}
|
|
|
|
float HslRenderer::textureProgress() {
|
|
if (!texturesRequested_) return 1.0f;
|
|
return 1.0f - static_cast<float>(std::min(waiting_.size(), texturesRequested_)) / static_cast<float>(texturesRequested_);
|
|
}
|
|
|
|
void HslRenderer::finishStreaming() {
|
|
for (auto& gm : meshes_) requestTextures(gm); // everything, not just what is in view
|
|
while (!waiting_.empty() && !workers_.empty()) {
|
|
pumpTextures(1e9);
|
|
if (!waiting_.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
}
|
|
}
|
|
|
|
// Skeletal animation: sample each rig's clip and write its joints' skin matrices into every mesh it drives.
|
|
void HslRenderer::updateAnimations(float timeSec) {
|
|
if (!scene_ || scene_->anims.empty()) return;
|
|
std::vector<float> skin;
|
|
for (const HslAnimGroup& g : scene_->anims) {
|
|
sampleRendClip(g.skel, g.clip, timeSec, skin);
|
|
const size_t joints = std::min<size_t>(skin.size() / 16, 256);
|
|
for (int si : g.meshes) {
|
|
auto it = sceneToMesh_.find(si);
|
|
if (it == sceneToMesh_.end()) continue;
|
|
GpuMesh& gm = meshes_[it->second];
|
|
if (gm.skinUbo < 0) continue;
|
|
Buffer& b = gm.ubos[gm.skinUbo];
|
|
std::memcpy(b.mapped, skin.data(), std::min<size_t>(joints * 64, static_cast<size_t>(b.size)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// World-space bounds of a mesh (its local box through its world matrix), for culling.
|
|
void HslRenderer::updateBounds(GpuMesh& gm) {
|
|
for (int k = 0; k < 3; ++k) { gm.worldMin[k] = 1e30f; gm.worldMax[k] = -1e30f; }
|
|
for (int c = 0; c < 8; ++c) {
|
|
const float p[3] = {c & 1 ? gm.localMax[0] : gm.localMin[0], c & 2 ? gm.localMax[1] : gm.localMin[1], c & 4 ? gm.localMax[2] : gm.localMin[2]};
|
|
for (int k = 0; k < 3; ++k) {
|
|
const float v = gm.world[k] * p[0] + gm.world[4 + k] * p[1] + gm.world[8 + k] * p[2] + gm.world[12 + k];
|
|
gm.worldMin[k] = std::min(gm.worldMin[k], v);
|
|
gm.worldMax[k] = std::max(gm.worldMax[k], v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Queues the background decode of every streamed texture this mesh is still showing a placeholder for.
|
|
void HslRenderer::requestTextures(GpuMesh& gm) {
|
|
static const bool noStream = std::getenv("FH_NO_STREAM") != nullptr; // diagnostics: placeholders only
|
|
if (gm.wants.empty() || noStream) return;
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
for (const TexKey& key : gm.wants) {
|
|
if (requested_.count(key)) continue;
|
|
auto src = pendingSrc_.find(key);
|
|
if (src == pendingSrc_.end()) continue;
|
|
requested_[key] = true;
|
|
jobs_.push_back({src->second, key.second, generation_});
|
|
}
|
|
gm.wants.clear();
|
|
jobCv_.notify_all();
|
|
}
|
|
|
|
// The vertices a mesh really has: nVerts as loaded, never past its positions (a damaged file can claim more).
|
|
static u32 meshVerts(const MeshData& md) { return static_cast<u32>(std::min<size_t>(md.nVerts, md.positions.size() / 3)); }
|
|
|
|
static double g_loadPipeMs = 0; // pipeline-compile share of the last uploadMesh (load timing log)
|
|
|
|
// The interleaved vertex data `p` reads for `md` (and the smooth normals it was built with): at upload, and every
|
|
// frame again for animated meshes.
|
|
void HslRenderer::buildVertices(const Program& p, const MeshData& md, std::vector<u8>& vbo, std::vector<float>& normals) const {
|
|
const u32 nv = meshVerts(md);
|
|
// (GPU-skinned: the vertex buffer carries the bind pose, the joints place it)
|
|
const std::vector<float>& pos = md.skinBindPos.size() >= static_cast<size_t>(nv) * 3 ? md.skinBindPos : md.positions;
|
|
// Smooth normals from the triangles (MeshData carries positions/uvs, not normals).
|
|
normals.assign(static_cast<size_t>(nv) * 3, 0.0f);
|
|
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
|
u32 a = md.indices[t], b = md.indices[t + 1], c = md.indices[t + 2];
|
|
if (a >= nv || b >= nv || c >= nv) continue;
|
|
const float* pa = &md.positions[a * 3]; const float* pb = &md.positions[b * 3]; const float* pc = &md.positions[c * 3];
|
|
float e1[3] = {pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]}, e2[3] = {pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]};
|
|
float n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]};
|
|
for (u32 v : {a, b, c}) for (int k = 0; k < 3; ++k) normals[v * 3 + k] += n[k];
|
|
}
|
|
for (u32 v = 0; v < nv; ++v) {
|
|
float* n = &normals[v * 3];
|
|
float l = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
|
if (l > 1e-12f) { n[0] /= l; n[1] /= l; n[2] /= l; } else { n[1] = 1; }
|
|
}
|
|
|
|
// Interleave every input the program declares, at the offsets/format the pipeline expects.
|
|
vbo.assign(static_cast<size_t>(nv) * p.stride, 0);
|
|
for (u32 v = 0; v < nv; ++v) {
|
|
u8* dst = vbo.data() + static_cast<size_t>(v) * p.stride;
|
|
for (const auto& a : p.attrs) {
|
|
float tmp[4] = {0, 0, 0, 0};
|
|
u8 b4[4] = {0, 0, 0, 0};
|
|
int nfl = 0;
|
|
switch (a.role) {
|
|
case Role::Pos: std::memcpy(tmp, &pos[v * 3], 12); nfl = 3; break;
|
|
case Role::Normal: { // authored, as HSR feeds inNorm; computed only if absent
|
|
const float* an = md.normals.size() >= static_cast<size_t>(nv) * 3 ? &md.normals[v * 3] : nullptr;
|
|
std::memcpy(tmp, an && (an[0] != 0 || an[1] != 0 || an[2] != 0) ? an : &normals[v * 3], 12);
|
|
nfl = 3; break;
|
|
}
|
|
case Role::Tangent: { // authored xyzw (w = handedness)
|
|
const float* at = md.tangents.size() >= static_cast<size_t>(nv) * 4 ? &md.tangents[v * 4] : nullptr;
|
|
if (at && (at[0] != 0 || at[1] != 0 || at[2] != 0)) std::memcpy(tmp, at, 16);
|
|
else { tmp[0] = 1; tmp[3] = 1; }
|
|
nfl = 4; break;
|
|
}
|
|
case Role::Uv0:
|
|
if (v * 2 + 1 < md.uvs.size()) { tmp[0] = md.uvs[v * 2]; tmp[1] = md.uvs[v * 2 + 1]; }
|
|
nfl = 2; break;
|
|
case Role::Uv1: {
|
|
// UV2 (lightmap / RBAoDir UV) is stored with V flipped relative to UV1. Shaders with
|
|
// lightBakerParams get the flip through that atlas transform instead (see set2 fill).
|
|
const bool own = md.uvs2.size() >= static_cast<size_t>(nv) * 2;
|
|
const auto& uv = own ? md.uvs2 : md.uvs; // raw, as HSR Studio feeds inLightmapUv
|
|
if (v * 2 + 1 < uv.size()) { tmp[0] = uv[v * 2]; tmp[1] = uv[v * 2 + 1]; }
|
|
nfl = 2; break;
|
|
}
|
|
case Role::Uv2: if (v * 2 + 1 < md.uvs3.size()) { tmp[0] = md.uvs3[v * 2]; tmp[1] = md.uvs3[v * 2 + 1]; } nfl = 2; break;
|
|
case Role::Uv3: if (v * 2 + 1 < md.uvs4.size()) { tmp[0] = md.uvs4[v * 2]; tmp[1] = md.uvs4[v * 2 + 1]; } nfl = 2; break;
|
|
case Role::Color:
|
|
if (md.colors.size() >= static_cast<size_t>(nv) * 4) std::memcpy(b4, &md.colors[v * 4], 4);
|
|
else b4[0] = b4[1] = b4[2] = b4[3] = 255; break;
|
|
case Role::BoneIdx: if (md.boneIndices.size() >= static_cast<size_t>(nv) * 4) std::memcpy(b4, &md.boneIndices[v * 4], 4); break;
|
|
case Role::BoneWgt:
|
|
if (md.boneWeights.size() >= static_cast<size_t>(nv) * 4) std::memcpy(b4, &md.boneWeights[v * 4], 4);
|
|
else b4[0] = 255;
|
|
break;
|
|
case Role::Zero: break;
|
|
}
|
|
u8* out = dst + a.offset;
|
|
if (a.size == 4 && (a.role == Role::Color || a.role == Role::BoneIdx || a.role == Role::BoneWgt)) {
|
|
std::memcpy(out, b4, 4);
|
|
} else if (a.format >= VK_FORMAT_R16_SFLOAT && a.format <= VK_FORMAT_R16G16B16A16_SFLOAT) {
|
|
for (uint32_t k = 0; k < a.size / 2; ++k) { uint16_t h = toHalf(k < 4 ? tmp[k] : 0); std::memcpy(out + k * 2, &h, 2); }
|
|
} else if (!a.integer) {
|
|
std::memcpy(out, tmp, std::min<uint32_t>(a.size, 16));
|
|
}
|
|
(void)nfl;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool HslRenderer::uploadMesh(const MeshData& md, int sceneIndex) {
|
|
if (md.overlayKind) return false; // navmesh / collision proxies: never drawn
|
|
if (const char* skip = std::getenv("FH_SKIP")) // diagnostics: leave out meshes of shaders matching any of a,b,c
|
|
for (std::string rest = skip; !rest.empty();) {
|
|
const size_t c = rest.find(',');
|
|
const std::string part = rest.substr(0, c);
|
|
if (!part.empty() && md.shaderPath.find(part) != std::string::npos) return false;
|
|
rest = c == std::string::npos ? std::string() : rest.substr(c + 1);
|
|
}
|
|
if (meshVerts(md) == 0 || md.indices.empty()) { LOG("skip '%s': no geometry", md.name.c_str()); return false; }
|
|
const ShaderProgramSrc* src = findProgram(*scene_, md.shaderPath);
|
|
if (!src && md.isSkybox) // the shell's skybox shader ships in renderer_module
|
|
for (const auto& kv : scene_->programs)
|
|
if (has(lower(kv.first), "skybox")) { src = &kv.second; break; }
|
|
if (!src) { LOG("skip '%s': no shader program for '%s'", md.name.c_str(), md.shaderPath.c_str()); return false; }
|
|
const int pi = programFor(*src);
|
|
if (pi < 0) { LOG("skip '%s': program failed", md.name.c_str()); return false; }
|
|
Program& p = programs_[pi];
|
|
|
|
GpuMesh gm;
|
|
gm.program = pi;
|
|
gm.sceneIndex = sceneIndex;
|
|
const u32 nv = meshVerts(md);
|
|
// Indices past the vertices (a damaged file) would read beyond the vertex buffer on the GPU: those
|
|
// triangles collapse to vertex 0 (degenerate, not drawn). Valid meshes keep their own index list.
|
|
const std::vector<u32>* indices = &md.indices;
|
|
std::vector<u32> clamped;
|
|
if (std::any_of(md.indices.begin(), md.indices.end(), [nv](u32 i) { return i >= nv; })) {
|
|
LOG("mesh '%s': indices past its %u vertices dropped", md.name.c_str(), nv);
|
|
clamped = md.indices;
|
|
clamped.resize(clamped.size() / 3 * 3);
|
|
for (size_t t = 0; t < clamped.size(); t += 3)
|
|
if (clamped[t] >= nv || clamped[t + 1] >= nv || clamped[t + 2] >= nv) clamped[t] = clamped[t + 1] = clamped[t + 2] = 0;
|
|
indices = &clamped;
|
|
}
|
|
// A buffer the GPU refused: this mesh is left out (and nothing it registered stays behind).
|
|
auto drop = [&](const char* why) {
|
|
LOG("skip '%s': %s", md.name.c_str(), why);
|
|
flushUploadBatch(); // its recorded copies run before its buffers go
|
|
destroy(gm.vbo);
|
|
destroy(gm.ibo);
|
|
for (auto& b : gm.ubos) destroy(b);
|
|
for (auto& i : gm.images) destroy(i);
|
|
if (gm.set2) vkFreeDescriptorSets(device_, descPool_, 1, &gm.set2);
|
|
for (auto w = waiting_.begin(); w != waiting_.end();) { // (an empty entry would hold finishStreaming forever)
|
|
w->second.erase(std::remove_if(w->second.begin(), w->second.end(), [&](const TexSlot& t) { return t.mesh == meshes_.size(); }),
|
|
w->second.end());
|
|
w = w->second.empty() ? waiting_.erase(w) : std::next(w);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
std::vector<float> normals;
|
|
std::vector<u8> vbo;
|
|
buildVertices(p, md, vbo, normals);
|
|
if (debugMesh(md)) {
|
|
gm.debug = true; gm.debugName = md.name;
|
|
LOG(" mesh '%s' shader %s: %u verts, stride %u, uv2 %zu, lmUvScaleOffset %.4f,%.4f,%.4f,%.4f, lightmap %ux%u",
|
|
md.name.c_str(), md.shaderPath.c_str(), nv, p.stride, md.uvs2.size() / 2, md.lmUvScaleOffset[0], md.lmUvScaleOffset[1],
|
|
md.lmUvScaleOffset[2], md.lmUvScaleOffset[3], md.lmW, md.lmH);
|
|
LOG(" mesh '%s' blend %d (useBlend %d additive %d) colors %zu", md.name.c_str(), md.useBlend ? (md.additive ? 2 : 1) : 0,
|
|
(int)md.useBlend, (int)md.additive, md.colors.size() / 4);
|
|
if (!md.colors.empty()) {
|
|
size_t gb = 0, n = md.colors.size() / 4;
|
|
double avg[4] = {0, 0, 0, 0};
|
|
for (size_t q = 0; q < n; ++q) {
|
|
gb += md.colors[q * 4 + 1] > 0 || md.colors[q * 4 + 2] > 0;
|
|
for (int c = 0; c < 4; ++c) avg[c] += md.colors[q * 4 + c];
|
|
}
|
|
LOG(" mesh '%s' vertex colours: %.1f%% with green or blue, average %.0f,%.0f,%.0f,%.0f", md.name.c_str(), 100.0 * gb / std::max<size_t>(1, n),
|
|
avg[0] / n, avg[1] / n, avg[2] / n, avg[3] / n);
|
|
}
|
|
{
|
|
float w[16];
|
|
if (md.hasWorldMatrix) std::memcpy(w, md.worldMatrix, 64); else trsToMat4(md.transform, w);
|
|
LOG(" mesh '%s' world %.3f %.3f %.3f | %.3f %.3f %.3f | %.3f %.3f %.3f | t %.3f %.3f %.3f", md.name.c_str(), w[0], w[1], w[2],
|
|
w[4], w[5], w[6], w[8], w[9], w[10], w[12], w[13], w[14]);
|
|
}
|
|
for (const auto& a : p.attrs) LOG(" attr @%u size %u role %d fmt %d", a.offset, a.size, static_cast<int>(a.role), static_cast<int>(a.format));
|
|
for (u32 v = 0; v < std::min<u32>(nv, 4); ++v)
|
|
LOG(" v%u pos %.4f,%.4f,%.4f n %.4f,%.4f,%.4f uv %.4f,%.4f uv2 %.4f,%.4f", v, md.positions[v * 3], md.positions[v * 3 + 1],
|
|
md.positions[v * 3 + 2], normals[v * 3], normals[v * 3 + 1], normals[v * 3 + 2],
|
|
v * 2 + 1 < md.uvs.size() ? md.uvs[v * 2] : -1.f, v * 2 + 1 < md.uvs.size() ? md.uvs[v * 2 + 1] : -1.f,
|
|
v * 2 + 1 < md.uvs2.size() ? md.uvs2[v * 2] : -1.f, v * 2 + 1 < md.uvs2.size() ? md.uvs2[v * 2 + 1] : -1.f);
|
|
for (u32 v = 0; v < std::min<u32>(nv, 2); ++v)
|
|
if (md.normals.size() >= static_cast<size_t>(nv) * 3 && md.tangents.size() >= static_cast<size_t>(nv) * 4)
|
|
LOG(" v%u authored n %.4f,%.4f,%.4f t %.4f,%.4f,%.4f,%.0f", v, md.normals[v * 3], md.normals[v * 3 + 1], md.normals[v * 3 + 2],
|
|
md.tangents[v * 4], md.tangents[v * 4 + 1], md.tangents[v * 4 + 2], md.tangents[v * 4 + 3]);
|
|
}
|
|
if (dynamicScene_.count(sceneIndex)) { // animated: host-visible, rewritten every frame
|
|
gm.vbo = createBuffer(vbo.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, true);
|
|
if (gm.vbo.buf) std::memcpy(gm.vbo.mapped, vbo.data(), vbo.size());
|
|
gm.dynamic = true;
|
|
} else {
|
|
gm.vbo = createDeviceBuffer(vbo.data(), vbo.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
|
}
|
|
gm.ibo = createDeviceBuffer(indices->data(), indices->size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
|
gm.indexCount = static_cast<uint32_t>(indices->size());
|
|
if (!gm.vbo.buf || !gm.ibo.buf) return drop("no memory for its vertices");
|
|
|
|
if (md.hasWorldMatrix) std::memcpy(gm.world, md.worldMatrix, 64);
|
|
else trsToMat4(md.transform, gm.world);
|
|
std::memcpy(gm.baseWorld, gm.world, 64);
|
|
gm.posGen = md.posGen; gm.uvGen = md.uvGen; // (the vertices just built)
|
|
std::memcpy(gm.uvXform, md.uvXform, sizeof gm.uvXform);
|
|
std::memcpy(gm.color, md.curTint, 16);
|
|
gm.blend = md.useBlend ? (md.additive ? 2 : 1) : 0;
|
|
// Cut-outs: an opaque surface whose shader samples an alpha mask (desert balloons), or a pass the SHAD marks
|
|
// alpha-to-coverage (palm foliage), is a solid, depth-writing cut-out, never a see-through blend.
|
|
bool maskCutout = false;
|
|
for (const auto& r : p.sets[2]) if (has(lower(r.name), "alphamask")) maskCutout = true;
|
|
gm.cull = false; // HSR Studio: cullMode none for every pipeline
|
|
gm.skybox = md.isSkybox;
|
|
gm.alphaToCoverage = (md.alphaToCoverage || (maskCutout && gm.blend == 0)) && !std::getenv("FH_NO_A2C");
|
|
if (gm.alphaToCoverage && gm.blend == 1) gm.blend = 0;
|
|
{ // one line per shader: the pipeline state this home's meshes get
|
|
static std::set<std::string> seen;
|
|
if (seen.insert(md.shaderPath).second)
|
|
LOG(" pipeline state %s: blend %d a2c %d (first mesh '%s')", md.shaderPath.c_str(), gm.blend, gm.alphaToCoverage ? 1 : 0, md.name.c_str());
|
|
}
|
|
// Compile this mesh's pipeline now, while loading: compiling on first sight froze the frame whenever new
|
|
// objects came into view (the pipeline cache makes it near-instant on later opens).
|
|
measureMesh(gm, md, nv);
|
|
std::map<uint32_t, std::vector<u8>> set2Bytes; // its constant uniform buffers, for variantFor
|
|
std::vector<uint64_t> lookSig; // what its material buffers and textures are (duplicate check)
|
|
|
|
// set2: per-material resources, routed by name.
|
|
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
ai.descriptorPool = descPool_;
|
|
ai.descriptorSetCount = 1;
|
|
ai.pSetLayouts = &p.setLayouts[2];
|
|
if (vkAllocateDescriptorSets(device_, &ai, &gm.set2) != VK_SUCCESS) { gm.set2 = VK_NULL_HANDLE; return drop("no descriptor set"); }
|
|
std::vector<VkWriteDescriptorSet> writes;
|
|
std::vector<std::vector<VkDescriptorBufferInfo>> bufs;
|
|
std::vector<std::vector<VkDescriptorImageInfo>> imgs;
|
|
bufs.reserve(p.sets[2].size());
|
|
imgs.reserve(p.sets[2].size());
|
|
for (const auto& r : p.sets[2]) {
|
|
std::string n = lower(r.name);
|
|
VkWriteDescriptorSet w = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
|
w.dstSet = gm.set2;
|
|
w.dstBinding = r.binding;
|
|
w.descriptorCount = r.count;
|
|
w.descriptorType = descriptorType(r.kind);
|
|
if (isBuffer(r.kind)) {
|
|
// (routed and filled by shader_bake.h, as the bake fills them for the shaders it specializes)
|
|
const shaderbake::Set2 kind = shaderbake::set2Kind(r, p.boned);
|
|
const bool skinning = kind == shaderbake::Set2::Bones;
|
|
VkDeviceSize size = skinning ? 256 * 64 : std::max<VkDeviceSize>(256, (r.size + 255) & ~255u);
|
|
Buffer b = createBuffer(size, r.kind == spv::ResKind::StorageBuffer ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT
|
|
: VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, true);
|
|
if (!b.buf) return drop("no memory for its material buffers");
|
|
float* f = static_cast<float*>(b.mapped);
|
|
if (skinning) gm.skinUbo = static_cast<int>(gm.ubos.size());
|
|
if (debugMesh(md)) {
|
|
std::string ms;
|
|
for (const auto& m : r.members) ms += m.name + "@" + std::to_string(m.offset) + " ";
|
|
LOG(" set2.b%u buffer '%s' (%u bytes): %s", r.binding, r.name.c_str(), r.size, ms.c_str());
|
|
}
|
|
if (skinning) {
|
|
for (int j = 0; j < 256; ++j) { float* m = f + j * 16; m[0] = m[5] = m[10] = m[15] = 1; }
|
|
if (!md.skinJoints.empty())
|
|
std::memcpy(f, md.skinJoints.data(), std::min<size_t>(md.skinJoints.size(), 256 * 16) * sizeof(float)); // Clay: posed
|
|
} else {
|
|
shaderbake::fillSet2(r, kind, md, p.path, ambient_, f);
|
|
if (kind == shaderbake::Set2::Material && debugMesh(md)) {
|
|
std::string vals;
|
|
for (const auto& m : r.members) { char b[64]; std::snprintf(b, sizeof b, "%s=%.3g ", m.name.c_str(), f[m.offset / 4]); vals += b; }
|
|
LOG(" matParams '%s': %s", md.name.c_str(), vals.c_str());
|
|
}
|
|
}
|
|
if (debugMesh(md) && !skinning) {
|
|
std::string all;
|
|
char num[32];
|
|
for (uint32_t k = 0; k * 4 < r.size && k < 64; ++k) { std::snprintf(num, sizeof num, "%s%g", k ? (k % 4 ? "," : ", ") : "", f[k]); all += num; }
|
|
LOG(" set2.b%u '%s' = %s", r.binding, r.name.c_str(), all.c_str());
|
|
}
|
|
bufs.emplace_back(r.count, VkDescriptorBufferInfo{b.buf, 0, VK_WHOLE_SIZE});
|
|
w.pBufferInfo = bufs.back().data();
|
|
gm.ubos.push_back(b);
|
|
if (!skinning && r.kind == spv::ResKind::UniformBuffer && r.size)
|
|
set2Bytes[r.binding].assign(static_cast<const u8*>(b.mapped), static_cast<const u8*>(b.mapped) + r.size);
|
|
if (!skinning) {
|
|
uint64_t h = 0xcbf29ce484222325ull ^ r.binding;
|
|
const u8* bytes = static_cast<const u8*>(b.mapped);
|
|
for (size_t k = 0, n = std::min<size_t>(r.size, static_cast<size_t>(b.size)); k < n; ++k) h = (h ^ bytes[k]) * 0x100000001B3ull;
|
|
lookSig.push_back(h);
|
|
}
|
|
} else {
|
|
VkDescriptorImageInfo ii = {};
|
|
ii.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
if (r.kind == spv::ResKind::Sampler || r.kind == spv::ResKind::CombinedImageSampler)
|
|
ii.sampler = has(n, "shadow") ? shadowSampler_ : anisoSampler_;
|
|
if (r.kind != spv::ResKind::Sampler) {
|
|
slotPending_ = false;
|
|
ii.imageView = textureFor(r, gm, md);
|
|
// (a placeholder stands for the texture it waits for: two meshes are alike when they wait for the same one)
|
|
lookSig.push_back(reinterpret_cast<uintptr_t>(ii.imageView) ^ (static_cast<uint64_t>(r.binding) << 56));
|
|
if (slotPending_) { // a placeholder for now: patch this binding when it lands
|
|
lookSig.push_back(reinterpret_cast<uintptr_t>(pendingKey_.first) * 2 + (pendingKey_.second ? 1 : 0));
|
|
waiting_[pendingKey_].push_back({meshes_.size(), r.binding, r.count, descriptorType(r.kind), ii.sampler});
|
|
slotPending_ = false;
|
|
}
|
|
if (debugMesh(md))
|
|
LOG(" set2.b%u '%s' dim=%d -> %s", r.binding, r.name.c_str(), static_cast<int>(r.dim),
|
|
ii.imageView == white_.view ? "white" : ii.imageView == black_.view ? "black"
|
|
: ii.imageView == flatNormal_.view ? "flatNormal" : ii.imageView == white3D_.view ? "white3D" : "texture");
|
|
}
|
|
imgs.emplace_back(r.count, ii);
|
|
w.pImageInfo = imgs.back().data();
|
|
}
|
|
if (r.kind == spv::ResKind::StorageImage) continue;
|
|
writes.push_back(w);
|
|
}
|
|
if (!writes.empty()) vkUpdateDescriptorSets(device_, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
|
gm.variant = variantFor(p, set2Bytes);
|
|
// Compile this mesh's pipeline now, while loading: compiling on first sight froze the frame whenever new
|
|
// objects came into view (the pipeline cache makes it near-instant on later opens).
|
|
const auto tp0 = std::chrono::steady_clock::now();
|
|
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutNone, gm.variant);
|
|
if (gm.alphaToCoverage && gm.blend == 0 && (gm.variant >= 0 ? p.variants[gm.variant].fsAlpha : p.fsAlpha)) {
|
|
// ...and a cut-out's two passes (render(): its depth from the alpha-only shader, then shaded): made on first
|
|
// sight, they were a compile in the middle of a frame the first time a fern came into view.
|
|
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutDepth, gm.variant);
|
|
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutShade, gm.variant);
|
|
}
|
|
g_loadPipeMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - tp0).count();
|
|
// An exact copy of a solid mesh already in (the same vertices in the same place, the same shader, material
|
|
// values and textures): some APKs ship a part twice (the hand tutorials' chapters and "archive" copies), and the
|
|
// copy, at the same depth, shaded every one of its pixels a second time to the same colour. It stays loaded (a
|
|
// home edit can hide or move the first one) but isn't drawn while the first shows it (render()).
|
|
static const bool noDedupe = std::getenv("FH_NO_DEDUPE") != nullptr; // A/B: every copy drawn
|
|
if (!noDedupe && !gm.dynamic && !gm.skybox && gm.blend == 0 && gm.skinUbo < 0 && !md.hasVat) {
|
|
uint64_t h = 0xcbf29ce484222325ull;
|
|
auto mix = [&h](const void* p, size_t n) {
|
|
const u8* b = static_cast<const u8*>(p);
|
|
for (size_t k = 0; k < n; ++k) h = (h ^ b[k]) * 0x100000001B3ull;
|
|
};
|
|
mix(vbo.data(), vbo.size());
|
|
mix(indices->data(), indices->size() * 4);
|
|
mix(gm.world, 64); mix(gm.color, 16); mix(gm.uvXform, sizeof gm.uvXform);
|
|
const int state[4] = {gm.program, gm.variant, gm.alphaToCoverage ? 1 : 0, gm.cull ? 1 : 0};
|
|
mix(state, sizeof state);
|
|
mix(lookSig.data(), lookSig.size() * 8);
|
|
const auto it = dedupe_.find(h);
|
|
if (it == dedupe_.end()) {
|
|
dedupe_[h] = meshes_.size();
|
|
} else { // (the hash matched: the geometry checked in full before trusting it)
|
|
const GpuMesh& o = meshes_[it->second];
|
|
const MeshData& om = scene_->meshes[o.sceneIndex];
|
|
if (o.program == gm.program && o.variant == gm.variant && o.indexCount == gm.indexCount && !std::memcmp(o.world, gm.world, 64) &&
|
|
om.positions == md.positions && om.uvs == md.uvs && om.indices == md.indices) {
|
|
gm.dupOf = static_cast<int>(it->second);
|
|
++loadDuplicates_;
|
|
}
|
|
}
|
|
}
|
|
// (see-through ones too: the hand tutorials' thousand-odd plants are alpha-blended; FH_NO_BLEND_BATCH=1 for A/B)
|
|
static const bool noBlendBatch = std::getenv("FH_NO_BLEND_BATCH") != nullptr;
|
|
if (p.clay && !gm.dynamic && !gm.skybox && (gm.blend == 0 || !noBlendBatch) && gm.skinUbo < 0 && !md.hasVat && gm.dupOf < 0) { // (buildBatches)
|
|
uint64_t h = 0xcbf29ce484222325ull;
|
|
auto mix = [&h](const void* q, size_t n) {
|
|
const u8* b = static_cast<const u8*>(q);
|
|
for (size_t k = 0; k < n; ++k) h = (h ^ b[k]) * 0x100000001B3ull;
|
|
};
|
|
const int state[5] = {gm.program, gm.variant, gm.alphaToCoverage ? 1 : 0, gm.cull ? 1 : 0, gm.blend};
|
|
mix(state, sizeof state); mix(gm.color, 16); mix(gm.uvXform, sizeof gm.uvXform);
|
|
mix(lookSig.data(), lookSig.size() * 8);
|
|
gm.lookHash = h | 1;
|
|
}
|
|
sceneToMesh_[sceneIndex] = meshes_.size();
|
|
meshes_.push_back(std::move(gm));
|
|
return true;
|
|
}
|
|
|
|
// A mesh's fragment shader with the uniform values that never change for it baked in (shader_bake.h): its material's
|
|
// buffers (set 2), the scene's lighting (set 1) and the frame globals every frame writes the same (set 0). The
|
|
// compiler then drops what those make dead (a layer mixed in by 0, a branch on a flag). A .fenv carries them made
|
|
// (the bake's, found by the key of these values); a home opened from its APK gets them made here. FH_NO_SPECIALIZE=1:
|
|
// the shaders as shipped.
|
|
int HslRenderer::variantFor(Program& p, const std::map<uint32_t, std::vector<u8>>& set2) {
|
|
static const bool off = std::getenv("FH_NO_SPECIALIZE") != nullptr;
|
|
if (off || p.foldBlocks.empty() || !p.src || p.src->frag.empty()) return -1;
|
|
static const char* only = std::getenv("FH_SPEC_ONLY"); // diagnostics: programs whose path holds this alone
|
|
static const int limit = std::getenv("FH_SPEC_LIMIT") ? std::atoi(std::getenv("FH_SPEC_LIMIT")) : -1; // the first n loads
|
|
if (only && p.path.find(only) == std::string::npos) return -1;
|
|
const int floats = shaderbake::floatsMode();
|
|
const spvspec::Fetch fetch = shaderbake::fetchFor(sceneConsts_, p.sets, set2);
|
|
const uint64_t key = shaderbake::variantKey(p.foldBlocks, fetch, floats) ^ (limit >= 0 ? 0x9E3779B97F4A7C15ull * (limit + 1) : 0);
|
|
if (const auto it = p.variantFor.find(key); it != p.variantFor.end()) return it->second;
|
|
auto module = [&](const std::vector<u32>& specialized) {
|
|
const std::vector<u32> c = spvopt::run(specialized, p.path); // (spv_opt.h)
|
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
ci.codeSize = c.size() * 4;
|
|
ci.pCode = c.data();
|
|
VkShaderModule sm = VK_NULL_HANDLE;
|
|
if (c.empty() || !shaderOk(c, p.path) || vkCreateShaderModule(device_, &ci, nullptr, &sm) != VK_SUCCESS) sm = VK_NULL_HANDLE;
|
|
return sm;
|
|
};
|
|
std::vector<u32> made, madeAlpha;
|
|
const std::vector<u32>* code = nullptr;
|
|
const std::vector<u32>* alpha = nullptr;
|
|
bool baked = false;
|
|
for (const auto& bv : p.src->variants)
|
|
if (bv.key == key && limit < 0 && shaderbake::bakedUsable(bv.frag)) { code = &bv.frag; alpha = &bv.fragAlpha; baked = true; break; }
|
|
uint32_t folded = 0;
|
|
if (!baked) {
|
|
made = spvspec::specialize(p.src->frag, fetch, &folded, limit, floats);
|
|
if (folded) madeAlpha = shaderbake::alphaOnly(made); // (its own: the cut-out depth pass must cover what it shades)
|
|
else made.clear();
|
|
code = &made;
|
|
alpha = &madeAlpha;
|
|
++specMade_;
|
|
} else {
|
|
++specBaked_;
|
|
}
|
|
int v = -1;
|
|
if (!code->empty()) {
|
|
Program::Variant var;
|
|
var.fs = module(*code);
|
|
if (var.fs) {
|
|
if (!alpha->empty()) var.fsAlpha = module(*alpha);
|
|
if (const char* dir = std::getenv("FH_DUMP_SPV")) { // diagnostics: the specialized shader beside the shipped one
|
|
std::string name = p.path;
|
|
for (char& ch : name) if (ch == '/' || ch == '\\') ch = '_';
|
|
char path[512];
|
|
std::snprintf(path, sizeof path, "%s/%s.var%zu.frag.spv", dir, name.c_str(), p.variants.size());
|
|
if (FILE* f = std::fopen(path, "wb")) { std::fwrite(code->data(), 4, code->size(), f); std::fclose(f); }
|
|
}
|
|
v = static_cast<int>(p.variants.size());
|
|
p.variants.push_back(std::move(var));
|
|
static int logged = 0;
|
|
if (logged++ < 40) LOG("specialized %s: variant %d (%s)", p.path.c_str(), v, baked ? "baked" : "made now");
|
|
}
|
|
}
|
|
p.variantFor[key] = v;
|
|
return v;
|
|
}
|
|
|
|
bool HslRenderer::load(const HslScene& scene) {
|
|
beginLoad(scene);
|
|
while (!loadStep(1e9)) {}
|
|
return !meshes_.empty() || !splatObjs_.empty();
|
|
}
|
|
|
|
void HslRenderer::beginLoad(const HslScene& scene) {
|
|
unload();
|
|
loadDone_ = false;
|
|
scene_ = &scene;
|
|
// Real env ambient when the home ships lightprobes; otherwise the editor's synthesized warm white x1.8.
|
|
for (int k = 0; k < 3; ++k) ambient_[k] = scene.ambientRGB[k] * (scene.hasEnvAmbient ? 1.0f : 1.8f);
|
|
|
|
// Lighting (set1), as HSR Studio does it: the engine fills lightUniforms / light items / the light grid at
|
|
// runtime, and a buffer the runtime would fill is ZERO here (no invented sun). Homes are lit by what they ship:
|
|
// baked lightmaps and RBAoDir maps, light probes, and the sky's reflection cube.
|
|
// (zero but for the five HSR fills with 1, for every program's layout of it: the bytes the specialized shaders
|
|
// assume, written once for all of them)
|
|
sceneConsts_ = shaderbake::sceneConstants(scene.programs, globalsSize_, static_cast<uint32_t>(lightUniforms_.size));
|
|
specMade_ = specBaked_ = 0;
|
|
std::memcpy(lightUniforms_.mapped, sceneConsts_.light.data(), std::min<size_t>(sceneConsts_.light.size(), static_cast<size_t>(lightUniforms_.size)));
|
|
std::memset(lightItems_.mapped, 0, static_cast<size_t>(lightItems_.size));
|
|
std::memset(lightGrid_.mapped, 0, static_cast<size_t>(lightGrid_.size));
|
|
|
|
const float top[3] = {ambient_[0], ambient_[1], ambient_[2]};
|
|
const float side[3] = {ambient_[0] * 0.65f, ambient_[1] * 0.63f, ambient_[2] * 0.58f};
|
|
const float bottom[3] = {ambient_[0] * 0.30f, ambient_[1] * 0.27f, ambient_[2] * 0.22f};
|
|
iblDiffuse_ = createSolidCube(top, side, bottom);
|
|
// Reflections: the world's own globalIblReflectionCubeMap (SkyboxPlatformComponent.reflectionMap) when it
|
|
// ships one; otherwise a hemisphere estimate from the ambient light.
|
|
const MeshData* sky = nullptr;
|
|
for (const auto& md : scene.meshes) if (md.reflCubeSize) { sky = &md; break; }
|
|
if (sky) {
|
|
const uint32_t size = sky->reflCubeSize, mips = sky->reflCubeMips;
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
// The shader picks a level by roughness over every level the cube has: Meta's cubes are cooked down to 16^2
|
|
// (256^2, 5 levels; a streamed one like Cascadia's has 1024^2 and 512^2 in its streams, which the loader puts
|
|
// in front). One that still arrives with fewer (an environment baked before the loader did that: mip 0 alone,
|
|
// every floor a mirror) gets the rest of that chain blitted.
|
|
uint32_t levels = 1;
|
|
while ((size >> levels) >= 16) ++levels;
|
|
const TexSource* src = sky->reflCubeSrc.get();
|
|
const VkFormat unorm = src ? astcFormat(src->bw, src->bh, false) : VK_FORMAT_UNDEFINED;
|
|
const char* how = "ASTC HDR";
|
|
if (src && astcHdr_ && mips >= levels && unorm != VK_FORMAT_UNDEFINED) // complete: sampled as cooked
|
|
iblReflection_ = createAstcTexture(*src, static_cast<VkFormat>(VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT + (unorm - VK_FORMAT_ASTC_4x4_UNORM_BLOCK) / 2));
|
|
if (!iblReflection_.view) {
|
|
std::vector<u16> decoded; // still ASTC HDR: every level's faces, decoded across cores
|
|
if (src && unorm != VK_FORMAT_UNDEFINED) {
|
|
std::vector<size_t> in, out; // per face of every level: its ASTC offset and half-float offset
|
|
size_t a = 0, h = 0;
|
|
for (uint32_t m = 0; m < mips; ++m) {
|
|
const uint32_t s = std::max(1u, size >> m);
|
|
const size_t blocks = static_cast<size_t>((s + src->bw - 1) / src->bw) * ((s + src->bh - 1) / src->bh) * 16;
|
|
for (int f = 0; f < 6; ++f) { in.push_back(a); out.push_back(h); a += blocks; h += static_cast<size_t>(s) * s * 4; }
|
|
}
|
|
in.push_back(a);
|
|
if (a <= src->astc.size()) {
|
|
decoded.resize(h);
|
|
std::atomic<bool> ok{true};
|
|
par::forEach(out.size(), [&](size_t i) {
|
|
const uint32_t s = std::max(1u, size >> (i / 6));
|
|
std::vector<u16> face;
|
|
if (astc::decodeASTCHalf(src->astc.data() + in[i], static_cast<u32>(in[i + 1] - in[i]), s, s, src->bw, src->bh, face) &&
|
|
face.size() == static_cast<size_t>(s) * s * 4)
|
|
std::memcpy(decoded.data() + out[i], face.data(), face.size() * 2);
|
|
else
|
|
ok = false;
|
|
});
|
|
if (!ok) decoded.clear();
|
|
}
|
|
}
|
|
const std::vector<u16>& faces = src ? decoded : sky->reflCubeHalf;
|
|
iblReflection_ = createCubeTexture(faces.data(), faces.size() * 2, size, VK_FORMAT_R16G16B16A16_SFLOAT, mips, levels);
|
|
how = "RGBA16F";
|
|
}
|
|
LOG("IBL reflection cube from the skybox reflectionMap (%u^2, %u mips, %u levels, %s, %.0f ms)", size, mips, std::max(mips, levels), how,
|
|
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
|
|
}
|
|
if (!iblReflection_.view) { // none shipped, or the shipped one was refused
|
|
const float rtop[3] = {ambient_[0] * 0.95f, ambient_[1] * 0.92f, ambient_[2] * 0.88f};
|
|
const float rbot[3] = {ambient_[0] * 0.40f, ambient_[1] * 0.37f, ambient_[2] * 0.32f};
|
|
iblReflection_ = createSolidCube(rtop, side, rbot);
|
|
}
|
|
|
|
if (!sel_.lines) createSelectionPipelines(); // now, not on the first click (a 70 ms hitch)
|
|
loadNext_ = 0;
|
|
loadSkipped_ = 0;
|
|
loadDuplicates_ = 0;
|
|
dedupe_.clear();
|
|
loadStart_ = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
// Uploads meshes until `budgetMs` is spent; true once every mesh is in. Lets a window stay responsive
|
|
// (and show the home filling in) while a large footprint + vista goes to the GPU.
|
|
bool HslRenderer::loadStep(double budgetMs) {
|
|
if (!scene_) return true;
|
|
const auto start = std::chrono::steady_clock::now();
|
|
static const bool noBatch = std::getenv("FH_NO_UPLOAD_BATCH") != nullptr; // diagnostics: the old one-at-a-time path
|
|
while (loadNext_ < scene_->meshes.size()) {
|
|
const auto tm = std::chrono::steady_clock::now();
|
|
g_loadPipeMs = 0;
|
|
if (!noBatch) beginUploadBatch();
|
|
if (!uploadMesh(scene_->meshes[loadNext_], static_cast<int>(loadNext_))) ++loadSkipped_;
|
|
if (loadStaged_ > (64ull << 20)) flushUploadBatch(); // bounded staging memory
|
|
const double took = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - tm).count();
|
|
if (took > 30.0)
|
|
LOG("slow mesh upload %.1f ms (pipeline %.1f): '%s' [%s]", took, g_loadPipeMs,
|
|
scene_->meshes[loadNext_].name.c_str(), scene_->meshes[loadNext_].shaderPath.c_str());
|
|
++loadNext_;
|
|
if (std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count() > budgetMs) break;
|
|
}
|
|
flushUploadBatch();
|
|
if (loadNext_ < scene_->meshes.size()) return false;
|
|
if (!splatsUploaded_) uploadSplats(); // (hsl_renderer_splat.cpp)
|
|
if (loadNext_ == scene_->meshes.size() && !loadDone_) {
|
|
loadDone_ = true;
|
|
buildBatches();
|
|
size_t variants = 0;
|
|
for (const auto& p : programs_) variants += p.variants.size();
|
|
size_t batched = 0;
|
|
for (const Batch& b : batches_) batched += b.members.size();
|
|
LOG("uploaded %zu meshes (%zu skipped, %zu exact copies of another not drawn, %zu drawn as %zu batches), %zu shader programs (%zu specialized: %zu baked, %zu made now) in %.1fs",
|
|
meshes_.size(), loadSkipped_, loadDuplicates_, batched, batches_.size(), programs_.size(), variants, specBaked_, specMade_, std::chrono::duration<double>(std::chrono::steady_clock::now() - loadStart_).count());
|
|
if (spvopt::flags()) {
|
|
const spvopt::Stats st = spvopt::stats();
|
|
LOG("spir-v optimizer (flags %d): %u modules made in %.0f ms, %u reused, %u failed", spvopt::flags(), st.made, st.ms, st.cached, st.failed);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int HslRenderer::addMesh(int sceneIndex) {
|
|
if (!device_ || !scene_ || !loadDone_ || recordInto_ || sceneIndex < 0 || sceneIndex >= static_cast<int>(scene_->meshes.size())) return -1;
|
|
beginUploadBatch();
|
|
const bool ok = uploadMesh(scene_->meshes[sceneIndex], sceneIndex);
|
|
flushUploadBatch();
|
|
loadNext_ = scene_->meshes.size(); // (nothing more for loadStep to bring in)
|
|
if (!ok) return -1;
|
|
GpuMesh& gm = meshes_.back();
|
|
gm.batch = -1; // drawn alone (batches are made once, at load)
|
|
gm.dupOf = -1; // (a copy pasted over its original still draws once moved)
|
|
requestTextures(gm);
|
|
return static_cast<int>(meshes_.size()) - 1;
|
|
}
|
|
|
|
void HslRenderer::reloadSplats() {
|
|
if (!device_ || !scene_) return;
|
|
vkDeviceWaitIdle(device_);
|
|
destroySplats();
|
|
uploadSplats();
|
|
}
|
|
|
|
float HslRenderer::loadProgress() const {
|
|
if (!scene_ || scene_->meshes.empty()) return 1.0f;
|
|
return static_cast<float>(loadNext_) / static_cast<float>(scene_->meshes.size());
|
|
}
|
|
|
|
void HslRenderer::unload() {
|
|
if (!device_) return;
|
|
vkDeviceWaitIdle(device_);
|
|
for (Buffer& b : frameStaging_) destroy(b);
|
|
frameStaging_.clear();
|
|
destroySplats();
|
|
for (auto& m : meshes_) {
|
|
destroy(m.vbo);
|
|
destroy(m.ibo);
|
|
for (auto& b : m.ubos) destroy(b);
|
|
for (auto& i : m.images) destroy(i);
|
|
if (m.set2) vkFreeDescriptorSets(device_, descPool_, 1, &m.set2);
|
|
}
|
|
destroy(sel_.vbo); destroy(sel_.edges); destroy(sel_.tris); // the selection pointed into the old home
|
|
sel_.mesh = -1;
|
|
sel_.edgeCount = sel_.triCount = 0;
|
|
meshes_.clear();
|
|
for (Batch& b : batches_) { // (its set2 is its first part's, freed above; a sorted one's ibo is one of its sortIbo)
|
|
destroy(b.gm.vbo);
|
|
if (b.sorted) { for (Buffer& sb : b.sortIbo) destroy(sb); } else destroy(b.gm.ibo);
|
|
}
|
|
batches_.clear();
|
|
dedupe_.clear();
|
|
cost_.clear();
|
|
costMin_.clear(); costSeen_.clear(); std::fill(std::begin(costBaseRing_), std::end(costBaseRing_), 1e9f);
|
|
doubled_ = -1;
|
|
sceneToMesh_.clear();
|
|
{ // drop queued / finished decodes of the old home
|
|
std::lock_guard<std::mutex> l(jobMutex_);
|
|
++generation_;
|
|
jobs_.clear();
|
|
for (TexDone& d : done_) if (d.ok) { destroy(d.prep.img); destroy(d.prep.staging); }
|
|
done_.clear();
|
|
}
|
|
streamed_.clear();
|
|
waiting_.clear();
|
|
pendingSrc_.clear();
|
|
requested_.clear();
|
|
keepAlive_.clear();
|
|
for (auto& kv : textureCache_) destroy(kv.second);
|
|
textureCache_.clear();
|
|
for (auto& p : programs_) {
|
|
for (auto& kv : p.pipelines) if (kv.second) vkDestroyPipeline(device_, kv.second, nullptr);
|
|
VkDescriptorSet s[2] = {p.set0, p.set1};
|
|
vkFreeDescriptorSets(device_, descPool_, 2, s);
|
|
vkDestroyPipelineLayout(device_, p.layout, nullptr);
|
|
for (auto l : p.setLayouts) vkDestroyDescriptorSetLayout(device_, l, nullptr);
|
|
vkDestroyShaderModule(device_, p.vs, nullptr);
|
|
vkDestroyShaderModule(device_, p.fs, nullptr);
|
|
if (p.fsAlpha) vkDestroyShaderModule(device_, p.fsAlpha, nullptr);
|
|
for (auto& v : p.variants) {
|
|
for (auto& kv : v.pipelines) if (kv.second) vkDestroyPipeline(device_, kv.second, nullptr);
|
|
if (v.fs) vkDestroyShaderModule(device_, v.fs, nullptr);
|
|
if (v.fsAlpha) vkDestroyShaderModule(device_, v.fsAlpha, nullptr);
|
|
}
|
|
}
|
|
programs_.clear();
|
|
programIndex_.clear();
|
|
destroy(iblDiffuse_);
|
|
destroy(iblReflection_);
|
|
scene_ = nullptr;
|
|
}
|
|
|
|
void HslRenderer::setMeshWorld(size_t i, const float world[16]) {
|
|
GpuMesh& gm = meshes_[i];
|
|
if (gm.batch >= 0 && std::memcmp(gm.baseWorld, world, 64) != 0) batches_[gm.batch].active = false; // (drawn alone again)
|
|
std::memcpy(gm.baseWorld, world, 64);
|
|
placeMesh(gm);
|
|
}
|
|
|
|
// The mesh's local centroid and box from its positions, then placed.
|
|
void HslRenderer::measureMesh(GpuMesh& gm, const MeshData& md, uint32_t nv) {
|
|
nv = static_cast<uint32_t>(std::min<size_t>(nv, md.positions.size() / 3));
|
|
double c[3] = {0, 0, 0};
|
|
for (int k = 0; k < 3; ++k) { gm.localMin[k] = 1e30f; gm.localMax[k] = -1e30f; }
|
|
for (u32 v = 0; v < nv; ++v)
|
|
for (int k = 0; k < 3; ++k) {
|
|
const float x = md.positions[v * 3 + k];
|
|
c[k] += x;
|
|
gm.localMin[k] = std::min(gm.localMin[k], x);
|
|
gm.localMax[k] = std::max(gm.localMax[k], x);
|
|
}
|
|
for (int k = 0; k < 3; ++k) gm.localCentroid[k] = nv ? static_cast<float>(c[k] / nv) : 0.0f;
|
|
placeMesh(gm);
|
|
}
|
|
|
|
// world = baseWorld * anim, and the centroid and bounds that follow it.
|
|
void HslRenderer::placeMesh(GpuMesh& gm) {
|
|
mat4mul4(gm.baseWorld, gm.anim, gm.world);
|
|
const float* w = gm.world;
|
|
for (int k = 0; k < 3; ++k)
|
|
gm.centroid[k] = w[k] * gm.localCentroid[0] + w[4 + k] * gm.localCentroid[1] + w[8 + k] * gm.localCentroid[2] + w[12 + k];
|
|
updateBounds(gm);
|
|
}
|
|
|
|
// Static batching (Batch, hsl_renderer.h): the solid Clay meshes uploadMesh gave a lookHash, grouped by that look and by
|
|
// the 4 m cell their centre is in, each group of two or more made one mesh: their vertices moved into the world (the
|
|
// shader then places them with an identity matrix), their indices one after another. Big meshes stay alone (one draw
|
|
// is nothing next to their pixels, and they'd be held twice in memory).
|
|
void HslRenderer::buildBatches() {
|
|
batches_.clear();
|
|
static const bool off = std::getenv("FH_NO_BATCH") != nullptr; // A/B: every mesh its own draw
|
|
if (off || !scene_) return;
|
|
constexpr u32 kMaxPartVerts = 16384, kMaxBatchVerts = 262144;
|
|
// See-through parts are sorted back to front as one: a smaller cell keeps that order close to theirs one by one.
|
|
static const float kSolidCell = 4.0f, kBlendCell = std::getenv("FH_BLEND_CELL") ? static_cast<float>(std::atof(std::getenv("FH_BLEND_CELL"))) : 2.0f;
|
|
std::map<std::tuple<uint64_t, int, int, int>, std::vector<size_t>> groups;
|
|
for (size_t i = 0; i < meshes_.size(); ++i) {
|
|
const GpuMesh& gm = meshes_[i];
|
|
if (!gm.lookHash || gm.dupOf >= 0 || !gm.visible || gm.sceneIndex < 0 || static_cast<size_t>(gm.sceneIndex) >= scene_->meshes.size()) continue;
|
|
if (meshVerts(scene_->meshes[gm.sceneIndex]) > kMaxPartVerts) continue;
|
|
const float kCell = gm.blend ? std::max(0.25f, kBlendCell) : kSolidCell;
|
|
groups[{gm.lookHash, static_cast<int>(std::floor(gm.centroid[0] / kCell)), static_cast<int>(std::floor(gm.centroid[1] / kCell)),
|
|
static_cast<int>(std::floor(gm.centroid[2] / kCell))}].push_back(i);
|
|
}
|
|
std::vector<u8> vbo, all;
|
|
std::vector<float> normals;
|
|
std::vector<u32> indices;
|
|
for (auto& g : groups) {
|
|
std::vector<size_t>& parts = g.second;
|
|
if (parts.size() < 2) continue;
|
|
const GpuMesh& first = meshes_[parts.front()];
|
|
const Program& p = programs_[first.program];
|
|
int posOff = -1, nrmOff = -1; // only float positions (and normals) can be moved; anything else stays alone
|
|
bool movable = true;
|
|
for (const auto& a : p.attrs) {
|
|
if (a.role == Role::Pos) { if (a.format == VK_FORMAT_R32G32B32_SFLOAT || a.format == VK_FORMAT_R32G32B32A32_SFLOAT) posOff = static_cast<int>(a.offset); else movable = false; }
|
|
if (a.role == Role::Normal) { if (a.format == VK_FORMAT_R32G32B32_SFLOAT || a.format == VK_FORMAT_R32G32B32A32_SFLOAT) nrmOff = static_cast<int>(a.offset); else movable = false; }
|
|
}
|
|
if (!movable || posOff < 0) continue;
|
|
for (size_t start = 0; start < parts.size();) {
|
|
// as many parts as fit in one batch
|
|
all.clear();
|
|
indices.clear();
|
|
std::vector<size_t> members;
|
|
std::vector<std::pair<u32, u32>> ranges;
|
|
u32 verts = 0;
|
|
for (; start < parts.size(); ++start) {
|
|
const GpuMesh& gm = meshes_[parts[start]];
|
|
const MeshData& md = scene_->meshes[gm.sceneIndex];
|
|
const u32 nv = meshVerts(md);
|
|
if (verts + nv > kMaxBatchVerts && !members.empty()) break;
|
|
if (std::any_of(md.indices.begin(), md.indices.end(), [nv](u32 i) { return i >= nv; })) continue; // (damaged: alone)
|
|
buildVertices(p, md, vbo, normals);
|
|
if (vbo.size() != static_cast<size_t>(nv) * p.stride) continue;
|
|
// world position; normal by the inverse transpose (the cofactors), as the shader would have
|
|
const float* w = gm.world;
|
|
const float cof[9] = {w[5] * w[10] - w[6] * w[9], w[6] * w[8] - w[4] * w[10], w[4] * w[9] - w[5] * w[8],
|
|
w[2] * w[9] - w[1] * w[10], w[0] * w[10] - w[2] * w[8], w[1] * w[8] - w[0] * w[9],
|
|
w[1] * w[6] - w[2] * w[5], w[2] * w[4] - w[0] * w[6], w[0] * w[5] - w[1] * w[4]};
|
|
for (u32 v = 0; v < nv; ++v) {
|
|
u8* base = vbo.data() + static_cast<size_t>(v) * p.stride;
|
|
float q[3];
|
|
std::memcpy(q, base + posOff, 12);
|
|
const float o[3] = {w[0] * q[0] + w[4] * q[1] + w[8] * q[2] + w[12], w[1] * q[0] + w[5] * q[1] + w[9] * q[2] + w[13],
|
|
w[2] * q[0] + w[6] * q[1] + w[10] * q[2] + w[14]};
|
|
std::memcpy(base + posOff, o, 12);
|
|
if (nrmOff >= 0) {
|
|
std::memcpy(q, base + nrmOff, 12);
|
|
float n[3] = {cof[0] * q[0] + cof[3] * q[1] + cof[6] * q[2], cof[1] * q[0] + cof[4] * q[1] + cof[7] * q[2],
|
|
cof[2] * q[0] + cof[5] * q[1] + cof[8] * q[2]};
|
|
const float l = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
|
if (l > 1e-12f) { n[0] /= l; n[1] /= l; n[2] /= l; }
|
|
std::memcpy(base + nrmOff, n, 12);
|
|
}
|
|
}
|
|
all.insert(all.end(), vbo.begin(), vbo.end());
|
|
ranges.push_back({static_cast<u32>(indices.size()), static_cast<u32>(md.indices.size())});
|
|
for (u32 i : md.indices) indices.push_back(verts + i);
|
|
verts += nv;
|
|
members.push_back(parts[start]);
|
|
}
|
|
if (members.size() < 2) continue;
|
|
Batch b;
|
|
GpuMesh& bm = b.gm;
|
|
bm.program = first.program; bm.variant = first.variant; bm.set2 = first.set2;
|
|
std::memcpy(bm.color, first.color, 16);
|
|
std::memcpy(bm.uvXform, first.uvXform, sizeof bm.uvXform);
|
|
bm.blend = first.blend; bm.cull = first.cull; bm.alphaToCoverage = first.alphaToCoverage;
|
|
b.sorted = first.blend != 0;
|
|
const float id[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
|
std::memcpy(bm.baseWorld, id, 64);
|
|
for (int k = 0; k < 3; ++k) { bm.localMin[k] = 1e30f; bm.localMax[k] = -1e30f; }
|
|
for (size_t m : members)
|
|
for (int k = 0; k < 3; ++k) {
|
|
bm.localMin[k] = std::min(bm.localMin[k], meshes_[m].worldMin[k]);
|
|
bm.localMax[k] = std::max(bm.localMax[k], meshes_[m].worldMax[k]);
|
|
}
|
|
for (int k = 0; k < 3; ++k) bm.localCentroid[k] = 0.5f * (bm.localMin[k] + bm.localMax[k]);
|
|
placeMesh(bm);
|
|
beginUploadBatch();
|
|
bm.vbo = createDeviceBuffer(all.data(), all.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
|
if (b.sorted) { // (written each frame the order changes: sortBatch)
|
|
for (Buffer& sb : b.sortIbo) sb = createBuffer(indices.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, true);
|
|
if (b.sortIbo[0].mapped && b.sortIbo[1].mapped) bm.ibo = b.sortIbo[0];
|
|
b.indices = indices;
|
|
b.ranges = std::move(ranges);
|
|
} else {
|
|
bm.ibo = createDeviceBuffer(indices.data(), indices.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
|
}
|
|
flushUploadBatch();
|
|
bm.indexCount = b.sorted ? 0 : static_cast<uint32_t>(indices.size());
|
|
if (!bm.vbo.buf || !bm.ibo.buf) { // (no memory: the parts draw alone)
|
|
destroy(bm.vbo);
|
|
if (b.sorted) { for (Buffer& sb : b.sortIbo) destroy(sb); } else destroy(bm.ibo);
|
|
continue;
|
|
}
|
|
for (size_t m : members) meshes_[m].batch = static_cast<int>(batches_.size());
|
|
b.members = std::move(members);
|
|
batches_.push_back(std::move(b));
|
|
}
|
|
}
|
|
}
|
|
|
|
// A see-through batch this frame (render(), after culling, meshes' inView still their own): the parts in view, far to
|
|
// near by their centres as render() orders see-through meshes, their indices written in that order into this command
|
|
// buffer's index buffer when it holds anything else (it was last read two frames ago: that frame's fence was waited).
|
|
void HslRenderer::sortBatch(Batch& b, const float head[3]) {
|
|
std::vector<std::pair<float, u32>> order;
|
|
order.reserve(b.members.size());
|
|
for (u32 k = 0; k < b.members.size(); ++k) {
|
|
const GpuMesh& gm = meshes_[b.members[k]];
|
|
if (!gm.inView) continue;
|
|
float d = 0;
|
|
for (int c = 0; c < 3; ++c) d += (gm.centroid[c] - head[c]) * (gm.centroid[c] - head[c]);
|
|
order.push_back({d, k});
|
|
}
|
|
std::stable_sort(order.begin(), order.end(), [](const auto& x, const auto& y) { return x.first > y.first; });
|
|
const int s = cmdIndex_ & 1;
|
|
std::vector<u32>& was = b.written[s];
|
|
bool same = was.size() == order.size();
|
|
for (size_t k = 0; same && k < order.size(); ++k) same = was[k] == order[k].second;
|
|
if (!same) {
|
|
u32* dst = static_cast<u32*>(b.sortIbo[s].mapped);
|
|
u32 n = 0;
|
|
was.clear();
|
|
for (const auto& o : order) {
|
|
const auto& r = b.ranges[o.second];
|
|
std::memcpy(dst + n, b.indices.data() + r.first, static_cast<size_t>(r.second) * 4);
|
|
n += r.second;
|
|
was.push_back(o.second);
|
|
}
|
|
b.writtenCount[s] = n;
|
|
}
|
|
b.gm.ibo = b.sortIbo[s];
|
|
b.gm.indexCount = b.writtenCount[s];
|
|
}
|
|
|
|
void HslRenderer::resize(uint32_t eyeWidth, uint32_t eyeHeight) {
|
|
if (!device_ || (eyeWidth == width_ && eyeHeight == height_) || !eyeWidth || !eyeHeight) return;
|
|
vkDeviceWaitIdle(device_);
|
|
for (VkImageView& v : layerViews_) { if (v) vkDestroyImageView(device_, v, nullptr); v = VK_NULL_HANDLE; }
|
|
vkDestroyFramebuffer(device_, framebuffer_, nullptr);
|
|
destroyExportFramebuffers(); // (they hold the old attachments: new ones come with new export images)
|
|
if (fovFb_) { vkDestroyFramebuffer(device_, fovFb_, nullptr); fovFb_ = VK_NULL_HANDLE; }
|
|
for (Image* i : {&colorMs_, &depth_, &resolve_, &fdm_, &sub_}) destroy(*i);
|
|
width_ = eyeWidth;
|
|
height_ = eyeHeight;
|
|
createTargets();
|
|
}
|
|
|
|
// ── Frame ────────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
void HslRenderer::updateGlobals(const EyeView eyes[2], float timeSec) {
|
|
u8* g = static_cast<u8*>(globals_.mapped);
|
|
std::memset(g, 0, globalsSize_);
|
|
auto put = [&](uint32_t off, const void* src, size_t n) { if (off + n <= globalsSize_) std::memcpy(g + off, src, n); };
|
|
Mat4 vp[2], skyVp[2];
|
|
for (int e = 0; e < 2; ++e) {
|
|
mat4mul4(eyes[e].proj.m, eyes[e].view.m, vp[e].m);
|
|
Mat4 rot = eyes[e].view; // the sky: rotation only, it never gets closer
|
|
rot.m[12] = rot.m[13] = rot.m[14] = 0;
|
|
mat4mul4(eyes[e].proj.m, rot.m, skyVp[e].m);
|
|
}
|
|
for (const auto& m : globalMembers_) {
|
|
const std::string n = lower(m.name);
|
|
const bool pair = m.size >= 128;
|
|
if (has(n, "skyboxclipfromworld")) {
|
|
put(m.offset, skyVp[0].m, 64);
|
|
if (pair) put(m.offset + 64, skyVp[1].m, 64);
|
|
} else if (has(n, "clipfromworld")) { // clipFromWorld[2]
|
|
put(m.offset, vp[0].m, 64);
|
|
if (pair) put(m.offset + 64, vp[1].m, 64);
|
|
} else if (has(n, "viewfromworld")) {
|
|
put(m.offset, eyes[0].view.m, 64);
|
|
if (pair) put(m.offset + 64, eyes[1].view.m, 64);
|
|
} else if (has(n, "worldcamerapos")) {
|
|
float p0[4] = {eyes[0].position[0], eyes[0].position[1], eyes[0].position[2], 1};
|
|
float p1[4] = {eyes[1].position[0], eyes[1].position[1], eyes[1].position[2], 1};
|
|
put(m.offset, p0, 16);
|
|
if (m.size >= 32) put(m.offset + 16, p1, 16);
|
|
} else if (n == "reversez") { // HSR: 1 (the corpus only authors Greater depth compares)
|
|
float one = 1;
|
|
// HSR sets 1 with its own sky projection; with this renderer's head-locked sky cube, 1 clips the sky away.
|
|
static const bool revZ = std::getenv("FH_REVERSEZ") != nullptr;
|
|
if (revZ) put(m.offset, &one, 4);
|
|
} else if (n == "fogcolor" || n == "fogstart" || n == "fogdensity") {
|
|
SceneFog fog;
|
|
{
|
|
std::lock_guard<std::mutex> l(g_sceneFogMutex);
|
|
fog = g_sceneFog;
|
|
}
|
|
static const bool noFog = std::getenv("FH_NO_FOG") != nullptr;
|
|
if (noFog) fog = SceneFog{};
|
|
static bool fogLogged = false;
|
|
if (!fogLogged) {
|
|
fogLogged = true;
|
|
for (const auto& gmem : globalMembers_) LOG("globals member %s @%u (%u bytes)", gmem.name.c_str(), gmem.offset, gmem.size);
|
|
LOG("fog: has %d travel %d color %.3f %.3f %.3f %.3f start %.3f density %.5f (member %s %u bytes)", fog.has, fog.travel,
|
|
fog.color[0], fog.color[1], fog.color[2], fog.color[3], fog.start, fog.density, m.name.c_str(), m.size);
|
|
}
|
|
if (n == "fogcolor") put(m.offset, fog.color, std::min<size_t>(16, m.size ? m.size : 16));
|
|
else put(m.offset, n == "fogstart" ? &fog.start : &fog.density, 4);
|
|
} else if (n == "whitepoint") {
|
|
float one = 1; put(m.offset, &one, 4);
|
|
} else if (n == "time" || n == "worldtime") {
|
|
put(m.offset, &timeSec, 4);
|
|
} else if (has(n, "viewportsize")) {
|
|
float s[2] = {static_cast<float>(width_), static_cast<float>(height_)}; put(m.offset, s, 8);
|
|
} else if (has(n, "defaultlightprobes")) {
|
|
// left zero, as in HSR Studio (not part of the frame globals it fills)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each eye's full-detail centre on the density map grid, and the map they and the level make.
|
|
void HslRenderer::fdmCentres(int32_t cx[2], int32_t cy[2]) const {
|
|
for (uint32_t e = 0; e < 2; ++e) {
|
|
const uint32_t ee = std::min(e, viewCount - 1);
|
|
const float px = fovCenter_[ee][0] >= 0 ? fovCenter_[ee][0] : width_ * 0.5f, py = fovCenter_[ee][1] >= 0 ? fovCenter_[ee][1] : height_ * 0.5f;
|
|
cx[e] = static_cast<int32_t>(std::lround(px / fdmTexel_));
|
|
cy[e] = static_cast<int32_t>(std::lround(py / fdmTexel_));
|
|
}
|
|
}
|
|
|
|
int64_t HslRenderer::fdmWantKey() const {
|
|
int32_t cx[2], cy[2];
|
|
fdmCentres(cx, cy);
|
|
return (static_cast<int64_t>(std::lround(fovStrength_ * 32)) << 48) | (static_cast<int64_t>(cx[0] & 0xFFF) << 36) | (static_cast<int64_t>(cy[0] & 0xFFF) << 24) |
|
|
(static_cast<int64_t>(cx[1] & 0xFFF) << 12) | static_cast<int64_t>(cy[1] & 0xFFF);
|
|
}
|
|
|
|
// Whether this frame can be recorded before the last one is done (render()): nothing it records may change what
|
|
// that frame still reads. Not with decoded textures to upload (the upload ring, descriptor sets), debug views or a
|
|
// selection (their buffers), or a density map to rewrite or just rewritten (Turnip reads it as a pass is recorded).
|
|
bool HslRenderer::canRecordEarly() {
|
|
static const bool off = std::getenv("FH_NO_EARLY_RECORD") != nullptr;
|
|
static const bool why = std::getenv("FH_EARLY_WHY") != nullptr; // diagnostics: what keeps frames from it
|
|
static uint32_t asked = 0;
|
|
auto no = [&](const char* reason) {
|
|
if (why && asked++ % 300 == 0) LOG("frame not recorded early: %s", reason);
|
|
return false;
|
|
};
|
|
if (!earlyRecord_ || off) return false;
|
|
if (fenceUnsignalled_ || !scene_ || !loadDone_) return no("not loaded");
|
|
if (preloading_) return no("textures still to load"); // (others wait to be seen: requesting one records nothing)
|
|
if (debugView_ != kViewNormal || sel_.mesh >= 0) return no("a debug view or selection");
|
|
{
|
|
std::lock_guard<std::mutex> l(jobMutex_); // (decoded textures waiting to go up)
|
|
if (!done_.empty()) return no("decoded textures to upload");
|
|
}
|
|
if (fdmOn_ && !fdmOffset_ && fdm_.image && (!fdmInit_ || fdmCopyRecorded_ || fdmWantKey() != fdmKey_)) return no("the density map changes");
|
|
return true;
|
|
}
|
|
|
|
void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|
using clk = std::chrono::steady_clock;
|
|
auto ms = [](clk::time_point a, clk::time_point b) { return std::chrono::duration<double, std::milli>(b - a).count(); };
|
|
const auto t0 = clk::now();
|
|
if (lost_) return; // the GPU reset and took this device with it: nothing draws again
|
|
clk::time_point t1, ta, tb, tg, tc, t2, t3;
|
|
uint32_t pipeBinds = 0, draws = 0;
|
|
uint64_t tris = 0;
|
|
// Early recording (system mode, steady state): this frame is culled and recorded while the GPU still draws the
|
|
// last one, and only then does it wait for it, write what the GPU reads (globals, bones, animated vertices) and
|
|
// go: the GPU waits a few tenths of a millisecond between frames, not the whole recording. Anything that changes
|
|
// what the last frame may still read (texture uploads, a new foveation map, debug views) records the usual way.
|
|
const bool early = canRecordEarly();
|
|
cmdIndex_ ^= 1; // two command buffers: one can be recorded while the other runs
|
|
cmd_ = cmds_[cmdIndex_];
|
|
auto prep = [&]() -> bool {
|
|
// (a frame that failed to submit never signals its fence: waiting for it would never return)
|
|
if (!fenceUnsignalled_ && vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX) == VK_ERROR_DEVICE_LOST) {
|
|
lost_ = true;
|
|
LOG("GPU device lost (the GPU was reset): this renderer can't draw any more");
|
|
return false;
|
|
}
|
|
fenceUnsignalled_ = false;
|
|
vkResetFences(device_, 1, &fence_);
|
|
fdmCopyRecorded_ = false; // (the frame that rewrote the density map is done)
|
|
if (perfPool_ && perfRecorded_) {
|
|
std::vector<VkPerformanceCounterResultKHR> res(perfNames_.size());
|
|
if (vkGetQueryPoolResults(device_, perfPool_, 0, 1, res.size() * sizeof(res[0]), res.data(), res.size() * sizeof(res[0]), 0) == VK_SUCCESS) {
|
|
for (size_t i = 0; i < res.size(); ++i) {
|
|
const VkPerformanceCounterStorageKHR st = perfStorage_[i];
|
|
perfSum_[i] += st == VK_PERFORMANCE_COUNTER_STORAGE_INT32_KHR ? res[i].int32 : st == VK_PERFORMANCE_COUNTER_STORAGE_INT64_KHR ? static_cast<double>(res[i].int64)
|
|
: st == VK_PERFORMANCE_COUNTER_STORAGE_UINT32_KHR ? res[i].uint32 : st == VK_PERFORMANCE_COUNTER_STORAGE_UINT64_KHR ? static_cast<double>(res[i].uint64)
|
|
: st == VK_PERFORMANCE_COUNTER_STORAGE_FLOAT32_KHR ? res[i].float32 : res[i].float64;
|
|
}
|
|
++perfFrames_;
|
|
}
|
|
perfRecorded_ = false;
|
|
}
|
|
t1 = clk::now();
|
|
if (queryPool_ && profFrame_) {
|
|
// kMaxQueries-4: start, -3: uploads done, -2: draws done (render pass ended), -1: end (after the export copy)
|
|
uint64_t ft[4] = {};
|
|
if (vkGetQueryPoolResults(device_, queryPool_, kMaxQueries - 4, 4, sizeof ft, ft, 8, VK_QUERY_RESULT_64_BIT) == VK_SUCCESS) {
|
|
const double up = (ft[1] - ft[0]) * timestampPeriod_ * 1e-6, dr = (ft[3] - ft[1]) * timestampPeriod_ * 1e-6;
|
|
gpuUploadMs_ = static_cast<float>(up);
|
|
gpuCopyMs_ = static_cast<float>((ft[3] - ft[2]) * timestampPeriod_ * 1e-6);
|
|
if (up + dr > 30.0 || ms(t0, t1) > 40.0)
|
|
LOG("GPU frame: uploads %.1f ms (%u textures, %.1f MB), draws %.1f ms; CPU waited %.1f ms; %d allocations so far", up, prevUploads_, prevUploadMB_, dr, ms(t0, t1), g_allocCount.load());
|
|
gpuMs_ = static_cast<float>(up + dr);
|
|
// Cost view on a tiler: the frame just measured either drew mesh doubled_ twice (its cost is what that
|
|
// added) or nothing twice (the baseline). Other work on the GPU (the compositor, another app) only ever adds
|
|
// to a frame's time, so both sides take the least of their recent frames: what the frame costs on its own.
|
|
if (debugView_ == kViewCost && tiler_) {
|
|
if (cost_.size() != meshes_.size()) { cost_.assign(meshes_.size(), -1.0f); costMin_.assign(meshes_.size() * kCostSamples, 1e9f); costSeen_.assign(meshes_.size(), 0); }
|
|
const float ms = static_cast<float>(dr);
|
|
static const bool trace = std::getenv("FH_COST_TRACE") != nullptr;
|
|
static int traced = 0;
|
|
if (trace && traced++ < 60) LOG("cost trace: doubled %d frame %.3f ms (slot %d)", doubled_, ms, timedSlot_);
|
|
if (doubled_ < 0) costBaseRing_[costBaseNext_++ % kCostBase] = ms;
|
|
else if (static_cast<size_t>(doubled_) < cost_.size()) {
|
|
const size_t i = static_cast<size_t>(doubled_);
|
|
costMin_[i * kCostSamples + costSeen_[i]++ % kCostSamples] = ms;
|
|
float base = 1e9f, with = 1e9f;
|
|
for (float b : costBaseRing_) base = std::min(base, b);
|
|
for (uint32_t k = 0; k < kCostSamples; ++k) with = std::min(with, costMin_[i * kCostSamples + k]);
|
|
if (base < 1e8f && costSeen_[i] >= 3) cost_[i] = std::max(0.0f, with - base); // a few samples before it shows
|
|
}
|
|
}
|
|
++gpuResults_;
|
|
gpuSum_ += gpuMs_; gpuMax_ = std::max(gpuMax_, gpuMs_);
|
|
if (++gpuFrames_ == 900) { // every ~10 s at 90 Hz: what a frame costs the GPU here
|
|
LOG("GPU: %.2f ms a frame on average, %.2f ms at worst (%ux%u per eye, %ux MSAA)", gpuSum_ / gpuFrames_, gpuMax_, width_, height_, static_cast<unsigned>(samples_));
|
|
gpuSum_ = gpuMax_ = 0; gpuFrames_ = 0;
|
|
}
|
|
}
|
|
}
|
|
profFrame_ = false;
|
|
prevUploads_ = lastUploads_;
|
|
prevUploadMB_ = lastUploadMB_;
|
|
if (queryPool_ && !profDraws_.empty()) {
|
|
std::vector<uint64_t> ts(profNext_, 0);
|
|
if (vkGetQueryPoolResults(device_, queryPool_, 0, profNext_, ts.size() * 8, ts.data(), 8, VK_QUERY_RESULT_64_BIT) == VK_SUCCESS) {
|
|
double total = 0;
|
|
std::vector<std::pair<double, size_t>> cost;
|
|
if (debugView_ == kViewCost && cost_.size() != meshes_.size()) cost_.assign(meshes_.size(), -1.0f);
|
|
for (const auto& pd : profDraws_) {
|
|
const double d = (ts[pd.second + viewCount] - ts[pd.second]) * timestampPeriod_ * 1e-6;
|
|
total += d;
|
|
cost.push_back({d, pd.first});
|
|
if (debugView_ == kViewCost && pd.first < cost_.size()) { // smoothed: one frame's timings jitter
|
|
float& c = cost_[pd.first];
|
|
c = c < 0 ? static_cast<float>(d) : c * 0.85f + static_cast<float>(d) * 0.15f;
|
|
}
|
|
}
|
|
if ((total > 30.0 || ms(t0, t1) > 40.0) && debugView_ != kViewCost) {
|
|
std::sort(cost.begin(), cost.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
|
|
std::string top;
|
|
for (size_t k = 0; k < cost.size() && k < 6; ++k) {
|
|
const GpuMesh& gm = meshes_[cost[k].second];
|
|
char b[256];
|
|
std::snprintf(b, sizeof b, "\n %.1f ms %s [%s]", cost[k].first,
|
|
scene_ ? scene_->meshes[gm.sceneIndex].name.c_str() : "?", programs_[gm.program].path.c_str());
|
|
top += b;
|
|
}
|
|
LOG("GPU frame %.1f ms over %zu draws; costliest:%s", total, profDraws_.size(), top.c_str());
|
|
}
|
|
}
|
|
}
|
|
profDraws_.clear();
|
|
profNext_ = 0;
|
|
for (Buffer& b : frameStaging_) destroy(b); // the previous frame's uploads are done
|
|
frameStaging_.clear();
|
|
ringHead_ = 0; // ...so is everything they read from the upload ring
|
|
dbg_.used = 0; // ...and the debug views' draws
|
|
if (debugView_ != kViewNormal) {
|
|
const uint32_t want = static_cast<uint32_t>(meshes_.size()) * 2 + 16;
|
|
if (dbg_.capacity < want || !dbg_.draws.mapped) {
|
|
destroy(dbg_.draws);
|
|
dbg_.capacity = 0;
|
|
dbg_.draws = createBuffer(static_cast<VkDeviceSize>(want) * 208, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true);
|
|
if (dbg_.draws.mapped) {
|
|
dbg_.capacity = want;
|
|
VkDescriptorBufferInfo bi = {dbg_.draws.buf, 0, VK_WHOLE_SIZE};
|
|
VkWriteDescriptorSet w = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
|
w.dstSet = dbg_.set; w.dstBinding = 0; w.descriptorCount = 1; w.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w.pBufferInfo = &bi;
|
|
vkUpdateDescriptorSets(device_, 1, &w, 0, nullptr);
|
|
}
|
|
}
|
|
}
|
|
ta = clk::now();
|
|
updateAnimations(timeSec); // bone matrices for skinned meshes (the GPU is idle here)
|
|
tb = clk::now();
|
|
{
|
|
EyeView late[2];
|
|
updateGlobals(latePose && latePose(late) ? late : eyes, timeSec);
|
|
}
|
|
tg = clk::now();
|
|
refreshDynamicMeshes(!early, true); // animated Clay / glTF meshes: this frame's vertices + tint
|
|
tc = clk::now();
|
|
stats_.waitMs = ms(t0, t1);
|
|
stats_.readbackMs = ms(t1, ta);
|
|
stats_.animMs = ms(ta, tb);
|
|
stats_.globalsMs = ms(tb, tg);
|
|
stats_.dynamicMs = ms(tg, tc);
|
|
if (ms(t1, tc) > 20.0)
|
|
LOG("slow prep: profiler readback %.1f, animations %.1f, globals %.1f ms", ms(t1, ta), ms(ta, tb), ms(tb, tc));
|
|
return true;
|
|
};
|
|
auto record = [&]() {
|
|
|
|
vkResetCommandBuffer(cmd_, 0);
|
|
VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
vkBeginCommandBuffer(cmd_, &bi);
|
|
const bool perfThis = perfPool_ && !perfRecorded_; // (one frame's counters in flight at a time)
|
|
if (perfThis) { vkCmdResetQueryPool(cmd_, perfPool_, 0, 1); vkCmdBeginQuery(cmd_, perfPool_, 0, 0); }
|
|
// Streamed textures that finished decoding: copied in this frame's command buffer, before the draws that use
|
|
// them — no queue stall (the old path waited for the whole GPU per texture).
|
|
if (queryPool_) {
|
|
vkCmdResetQueryPool(cmd_, queryPool_, 0, kMaxQueries);
|
|
vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queryPool_, kMaxQueries - 4);
|
|
}
|
|
recordInto_ = cmd_;
|
|
t2 = clk::now();
|
|
lastUploads_ = 0;
|
|
lastUploadMB_ = 0;
|
|
if (!early) pumpTextures(preloading_ ? (uploadsUnseen_ ? 40.0 : 14.0) : 3.0);
|
|
if (preloading_ && waiting_.empty()) {
|
|
preloading_ = false;
|
|
LOG("all %zu textures loaded", texturesRequested_);
|
|
{ // Video memory this process may keep resident vs. what it uses (VK_EXT_memory_budget): over budget,
|
|
// Windows demotes allocations to system RAM and every draw that samples them crawls over PCIe.
|
|
VkPhysicalDeviceMemoryBudgetPropertiesEXT bud = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
|
VkPhysicalDeviceMemoryProperties2 mp2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
|
|
mp2.pNext = &bud;
|
|
vkGetPhysicalDeviceMemoryProperties2(physical_, &mp2);
|
|
for (uint32_t hI = 0; hI < mp2.memoryProperties.memoryHeapCount; ++hI)
|
|
if (mp2.memoryProperties.memoryHeaps[hI].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
|
|
LOG("VRAM heap %u: this app uses %.0f MB of a %.0f MB budget (heap %.0f MB)", hI, bud.heapUsage[hI] / 1048576.0,
|
|
bud.heapBudget[hI] / 1048576.0, mp2.memoryProperties.memoryHeaps[hI].size / 1048576.0);
|
|
}
|
|
}
|
|
recordInto_ = VK_NULL_HANDLE;
|
|
if (queryPool_) vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 3);
|
|
t3 = clk::now();
|
|
VkClearValue clears[3] = {};
|
|
clears[0].color = {{clearColor[0], clearColor[1], clearColor[2], 1}};
|
|
static const char* clearEnv = std::getenv("FH_CLEAR");
|
|
if (const char* c = clearEnv) // debug: tell "drawn black" from "not drawn"
|
|
std::sscanf(c, "%f,%f,%f", &clears[0].color.float32[0], &clears[0].color.float32[1], &clears[0].color.float32[2]);
|
|
if (debugView_ == kViewOverdraw) clears[0].color = {{0, 0, 0, 1}}; // layers add up from black
|
|
else if (debugView_ == kViewMeshes) clears[0].color = {{0.03f, 0.03f, 0.035f, 1}};
|
|
clears[1].depthStencil = {0.0f, 0}; // reversed-Z: far = 0
|
|
clears[2].color = clears[0].color;
|
|
VkRenderPassBeginInfo rp = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
|
// exportDirect: the frame goes straight into the compositor's image for this slot (no copy at the end).
|
|
const bool direct = exportDirect && exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportFbs_.size();
|
|
lastExportSlot_ = direct ? exportSlot : -1;
|
|
if (fdmOn_ && !fdmOffset_ && fdm_.image) {
|
|
// Foveation without offsets: the density map itself is redrawn with each eye's full-detail centre where it
|
|
// looks, whenever that moves by a map texel (or the level changes); otherwise the last one stays.
|
|
int32_t cx[2], cy[2];
|
|
fdmCentres(cx, cy);
|
|
const int64_t key = fdmWantKey();
|
|
const size_t texels = static_cast<size_t>(fdmW_) * fdmH_;
|
|
const size_t bpt = fdmFormat_ == VK_FORMAT_R32G32_SFLOAT ? 8 : fdmFormat_ == VK_FORMAT_R16G16_SFLOAT ? 4 : 2;
|
|
const VkDeviceSize bytes = texels * bpt * viewCount, at = (ringHead_ + 15) & ~VkDeviceSize(15);
|
|
if (!fdmInit_) {
|
|
// The first map since the targets were made goes in before this frame is recorded: the driver reads the map
|
|
// as the pass is recorded, so one copied in by this frame's commands would leave this frame with none (black).
|
|
std::vector<uint8_t> px(bytes);
|
|
std::vector<float> dens;
|
|
for (uint32_t l = 0; l < viewCount; ++l) {
|
|
densityTexels(cx[std::min(l, 1u)] + 0.0f, cy[std::min(l, 1u)] + 0.0f, dens);
|
|
packDensity(dens, &px[l * texels * bpt]);
|
|
}
|
|
Buffer staging = createBuffer(px.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
|
if (staging.mapped) {
|
|
std::memcpy(staging.mapped, px.data(), px.size());
|
|
oneShot([&](VkCommandBuffer c) {
|
|
imageBarrier(c, fdm_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
|
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkBufferImageCopy cp = {};
|
|
cp.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
|
|
cp.imageExtent = {fdmW_, fdmH_, 1};
|
|
vkCmdCopyBufferToImage(c, staging.buf, fdm_.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp);
|
|
imageBarrier(c, fdm_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT);
|
|
});
|
|
destroy(staging);
|
|
fdmInit_ = true;
|
|
fdmKey_ = key;
|
|
}
|
|
}
|
|
if (key != fdmKey_ && at + bytes <= kRingSize && ring_.mapped) {
|
|
uint8_t* dst = static_cast<uint8_t*>(ring_.mapped) + at;
|
|
std::vector<float> dens;
|
|
for (uint32_t l = 0; l < viewCount; ++l) {
|
|
densityTexels(cx[std::min(l, 1u)] + 0.0f, cy[std::min(l, 1u)] + 0.0f, dens);
|
|
packDensity(dens, dst + l * texels * bpt);
|
|
}
|
|
ringHead_ = at + bytes;
|
|
imageBarrier(cmd_, fdm_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount,
|
|
fdmKey_ == -1 && !fdmInit_ ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkBufferImageCopy cp = {};
|
|
cp.bufferOffset = at;
|
|
cp.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
|
|
cp.imageExtent = {fdmW_, fdmH_, 1};
|
|
vkCmdCopyBufferToImage(cmd_, ring_.buf, fdm_.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp);
|
|
imageBarrier(cmd_, fdm_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT);
|
|
fdmKey_ = key;
|
|
fdmInit_ = true;
|
|
fdmCopyRecorded_ = true;
|
|
}
|
|
}
|
|
// Our smoothing pass runs after the foveated one unless the compositor smooths the export as it samples it.
|
|
const bool smoothingPass = smoothFov_ && !(direct && exportsSubsampled());
|
|
rp.renderPass = direct && !smoothingPass ? exportPass_ : renderPass_;
|
|
rp.framebuffer = direct && !smoothingPass ? exportFbs_[exportSlot] : framebuffer_;
|
|
rp.renderArea = {{0, 0}, {width_, height_}};
|
|
rp.clearValueCount = samples_ != VK_SAMPLE_COUNT_1_BIT ? 3 : 2;
|
|
rp.pClearValues = clears;
|
|
vkCmdBeginRenderPass(cmd_, &rp, VK_SUBPASS_CONTENTS_INLINE);
|
|
VkViewport vp = {0, 0, static_cast<float>(width_), static_cast<float>(height_), 0, 1};
|
|
VkRect2D sc = {{0, 0}, {width_, height_}};
|
|
vkCmdSetViewport(cmd_, 0, 1, &vp);
|
|
vkCmdSetScissor(cmd_, 0, 1, &sc);
|
|
|
|
const float head[3] = {(eyes[0].position[0] + eyes[1].position[0]) * 0.5f,
|
|
(eyes[0].position[1] + eyes[1].position[1]) * 0.5f,
|
|
(eyes[0].position[2] + eyes[1].position[2]) * 0.5f};
|
|
// View-frustum culling (either eye): only what can be seen is drawn, and only its textures stream in.
|
|
float planes[2][5][4];
|
|
for (int e = 0; e < 2; ++e) {
|
|
float m[16];
|
|
mat4mul4(eyes[e].proj.m, eyes[e].view.m, m);
|
|
auto row = [&](int r, float* o) { for (int c = 0; c < 4; ++c) o[c] = m[c * 4 + r]; };
|
|
float r0[4], r1[4], r2[4], r3[4];
|
|
row(0, r0); row(1, r1); row(2, r2); row(3, r3);
|
|
for (int c = 0; c < 4; ++c) {
|
|
planes[e][0][c] = r3[c] + r0[c]; // left
|
|
planes[e][1][c] = r3[c] - r0[c]; // right
|
|
planes[e][2][c] = r3[c] + r1[c]; // bottom / top (Vulkan y)
|
|
planes[e][3][c] = r3[c] - r1[c];
|
|
planes[e][4][c] = r3[c] - r2[c]; // near (reversed-Z: z <= w); infinite far
|
|
}
|
|
}
|
|
auto inFrustum = [&](const GpuMesh& gm, int e) {
|
|
for (int i = 0; i < 5; ++i) {
|
|
const float* pl = planes[e][i];
|
|
const float x = pl[0] >= 0 ? gm.worldMax[0] : gm.worldMin[0];
|
|
const float y = pl[1] >= 0 ? gm.worldMax[1] : gm.worldMin[1];
|
|
const float z = pl[2] >= 0 ? gm.worldMax[2] : gm.worldMin[2];
|
|
if (pl[0] * x + pl[1] * y + pl[2] * z + pl[3] < 0) return false;
|
|
}
|
|
return true;
|
|
};
|
|
drawn_ = 0;
|
|
static int cullFrame = 0;
|
|
const int frameNo_ = ++cullFrame;
|
|
for (auto& gm : meshes_) {
|
|
gm.inView = gm.skybox || (gm.visible && (inFrustum(gm, 0) || inFrustum(gm, 1))); // (animated: bounds kept current)
|
|
if (gm.dupOf >= 0 && gm.inView) { // an exact copy of a mesh that already draws these pixels (uploadMesh)
|
|
const GpuMesh& o = meshes_[gm.dupOf];
|
|
if (o.visible && !std::memcmp(o.world, gm.world, 64)) gm.inView = false;
|
|
}
|
|
if (gm.inView && gm.visible) { ++drawn_; requestTextures(gm); }
|
|
if (gm.debug && frameNo_ == 120)
|
|
LOG("[cull] %s eye %.2f %.2f %.2f | visible %d inView %d dynamic %d | world box %.2f %.2f %.2f .. %.2f %.2f %.2f",
|
|
gm.debugName.c_str(), eyes[0].position[0], eyes[0].position[1], eyes[0].position[2], gm.visible, gm.inView, gm.dynamic,
|
|
gm.worldMin[0], gm.worldMin[1], gm.worldMin[2], gm.worldMax[0], gm.worldMax[1], gm.worldMax[2]);
|
|
}
|
|
// A batch draws when any of its parts is in view, in their place (the debug views show the parts themselves).
|
|
for (Batch& b : batches_) {
|
|
b.gm.inView = false;
|
|
if (!b.active || debugView_ != kViewNormal) continue;
|
|
for (size_t m : b.members) b.gm.inView |= meshes_[m].inView;
|
|
if (b.sorted && b.gm.inView) { // see-through: the parts in view, in their order
|
|
sortBatch(b, head);
|
|
b.gm.inView = b.gm.indexCount > 0;
|
|
}
|
|
for (size_t m : b.members) meshes_[m].inView = false;
|
|
}
|
|
auto meshAt = [&](size_t k) -> GpuMesh& { return k < meshes_.size() ? meshes_[k] : batches_[k - meshes_.size()].gm; };
|
|
|
|
// Redundant state is not re-sent: consecutive meshes of one shader share its pipeline and its set 0 / set 1
|
|
// (the same layout keeps them bound), so only set 2 and the push constants change between them.
|
|
VkPipeline boundPipe = VK_NULL_HANDLE;
|
|
VkPipelineLayout boundLayout = VK_NULL_HANDLE;
|
|
VkDescriptorSet boundSet0 = VK_NULL_HANDLE, boundSet1 = VK_NULL_HANDLE;
|
|
VkBuffer boundVbo = VK_NULL_HANDLE, boundIbo = VK_NULL_HANDLE;
|
|
pipeBinds = 0; draws = 0;
|
|
alignas(16) u8 pcBuf[256];
|
|
static const bool noBindCache = std::getenv("FH_NO_BIND_CACHE") != nullptr; // A/B: every bind re-sent (the old way)
|
|
// Cost view on a tiler: two frames in three draw one mesh in view twice (its pixels again, at the same depth: the
|
|
// picture doesn't change), going round all of them; the next render() reads what each added when its frame is
|
|
// timed. (Three, not two: anything else on the GPU at half rate would make every other frame look dearer.)
|
|
doubled_ = -1;
|
|
if (debugView_ == kViewCost && tiler_ && (++costFrame_ % 3) != 0) {
|
|
for (size_t k = 0; k < meshes_.size(); ++k) {
|
|
costNext_ = (costNext_ + 1) % std::max<size_t>(1, meshes_.size());
|
|
const GpuMesh& gm = meshes_[costNext_];
|
|
if (gm.visible && gm.inView && !gm.skybox) { doubled_ = static_cast<int>(costNext_); break; }
|
|
}
|
|
}
|
|
tris = 0;
|
|
const bool replaced = debugView_ == kViewMeshes || debugView_ == kViewOverdraw; // the home isn't drawn as itself
|
|
auto hiddenByDebug = [&](const GpuMesh& gm) {
|
|
return ((debugHide & kHideSeeThrough) && gm.blend != 0 && !gm.skybox) || ((debugHide & kHideCutout) && gm.alphaToCoverage) ||
|
|
((debugHide & kHideSky) && gm.skybox) || ((debugHide & kHideAnimated) && gm.dynamic) ||
|
|
((debugHide & kHideSolid) && gm.blend == 0 && !gm.skybox && !gm.alphaToCoverage);
|
|
};
|
|
auto draw = [&](GpuMesh& gm, CutPass cut = kCutNone) {
|
|
if (!gm.visible || !gm.inView || replaced || (debugHide && hiddenByDebug(gm))) return;
|
|
// A see-through Clay mesh faded all the way out (its tint alpha 0) leaves every pixel as it was: not drawn.
|
|
if (gm.blend != 0 && gm.color[3] <= 0.0f && programs_[gm.program].clay) return;
|
|
if (cut != kCutDepth) tris += gm.indexCount / 3;
|
|
Program& p = programs_[gm.program];
|
|
VkPipeline pipe = pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, cut, gm.variant);
|
|
if (!pipe) return;
|
|
if (noBindCache) boundPipe = VK_NULL_HANDLE, boundLayout = VK_NULL_HANDLE, boundVbo = boundIbo = VK_NULL_HANDLE;
|
|
if (pipe != boundPipe) { vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); boundPipe = pipe; ++pipeBinds; }
|
|
if (p.layout != boundLayout || p.set0 != boundSet0 || p.set1 != boundSet1) {
|
|
VkDescriptorSet sets[3] = {p.set0, p.set1, gm.set2};
|
|
vkCmdBindDescriptorSets(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, p.layout, 0, 3, sets, 0, nullptr);
|
|
boundLayout = p.layout; boundSet0 = p.set0; boundSet1 = p.set1;
|
|
} else {
|
|
vkCmdBindDescriptorSets(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, p.layout, 2, 1, &gm.set2, 0, nullptr);
|
|
}
|
|
++draws;
|
|
if (p.pushSize) {
|
|
std::vector<u8> heapPc;
|
|
u8* pc = pcBuf;
|
|
if (p.pushSize > sizeof pcBuf) { heapPc.assign(p.pushSize, 0); pc = heapPc.data(); }
|
|
else std::memset(pcBuf, 0, p.pushSize);
|
|
float world[16];
|
|
std::memcpy(world, gm.world, 64);
|
|
if (gm.skybox) { // unit sky cube follows the head, scaled far out
|
|
const float S = 5000.0f;
|
|
std::memset(world, 0, 64);
|
|
world[0] = world[5] = world[10] = S;
|
|
world[12] = head[0]; world[13] = head[1]; world[14] = head[2]; world[15] = 1;
|
|
}
|
|
if (p.pcWorld >= 0 && static_cast<uint32_t>(p.pcWorld) + 64 <= p.pushSize) std::memcpy(&pc[p.pcWorld], world, 64);
|
|
if (p.pcColor >= 0 && static_cast<uint32_t>(p.pcColor) + 16 <= p.pushSize) std::memcpy(&pc[p.pcColor], gm.color, 16);
|
|
if (p.pcUvU >= 0 && p.pcUvV >= 0 && static_cast<uint32_t>(std::max(p.pcUvU, p.pcUvV)) + 16 <= p.pushSize) {
|
|
std::memcpy(&pc[p.pcUvU], gm.uvXform, 12);
|
|
std::memcpy(&pc[p.pcUvV], gm.uvXform + 3, 12);
|
|
}
|
|
vkCmdPushConstants(cmd_, p.layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, p.pushSize, pc);
|
|
}
|
|
VkDeviceSize zero = 0;
|
|
if (gm.vbo.buf != boundVbo) { vkCmdBindVertexBuffers(cmd_, 0, 1, &gm.vbo.buf, &zero); boundVbo = gm.vbo.buf; }
|
|
if (gm.ibo.buf != boundIbo) { vkCmdBindIndexBuffer(cmd_, gm.ibo.buf, 0, VK_INDEX_TYPE_UINT32); boundIbo = gm.ibo.buf; }
|
|
// Inside a multiview pass each timestamp takes viewCount consecutive queries.
|
|
// Per-draw timestamps serialize the GPU (each waits for the work before it): opt-in only.
|
|
static const bool profDraws = std::getenv("FH_PROF_DRAWS") != nullptr;
|
|
const bool prof = cut != kCutDepth && (profDraws || (debugView_ == kViewCost && !tiler_)) && queryPool_ && profNext_ + 2 * viewCount <= kMaxQueries - 4 - viewCount;
|
|
if (prof) vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queryPool_, profNext_);
|
|
vkCmdDrawIndexed(cmd_, gm.indexCount, 1, 0, 0, 0);
|
|
if (cut != kCutDepth && doubled_ >= 0 && &gm == &meshes_[doubled_]) vkCmdDrawIndexed(cmd_, gm.indexCount, 1, 0, 0, 0);
|
|
if (prof) {
|
|
vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, profNext_ + viewCount);
|
|
profDraws_.push_back({static_cast<size_t>(&gm - meshes_.data()), profNext_});
|
|
profNext_ += 2 * viewCount;
|
|
}
|
|
};
|
|
if (skyFirst()) for (auto& gm : meshes_) if (gm.skybox) draw(gm);
|
|
// Opaque: solid surfaces in the file's order, then the cut-out ones (leaves, fences: they discard, which can switch a
|
|
// tiler's early depth rejection off for the draws after them). FH_ORDER=0: all in the file's order; 1: solid nearest
|
|
// first (A/B: on the Frame's Adreno, sorting cost more in shader changes than it saved).
|
|
static const int opaqueMode = std::getenv("FH_ORDER") ? std::atoi(std::getenv("FH_ORDER")) : 2;
|
|
if (opaqueMode == 0) {
|
|
for (auto& gm : meshes_) if (gm.blend == 0 && !gm.skybox) draw(gm);
|
|
for (auto& b : batches_) if (b.gm.blend == 0) draw(b.gm);
|
|
} else {
|
|
opaqueOrder_.clear();
|
|
for (size_t i = 0; i < meshes_.size() + batches_.size(); ++i) { // (past the meshes: the batches, at their first part)
|
|
const GpuMesh& gm = meshAt(i);
|
|
if (gm.blend != 0 || gm.skybox || !gm.visible || !gm.inView) continue;
|
|
float d = static_cast<float>(i < meshes_.size() ? i : batches_[i - meshes_.size()].members.front());
|
|
if (opaqueMode == 1) { // distance to the box (0 inside it): a big floor around you is nearest, not its far-off centre
|
|
d = 0;
|
|
for (int k = 0; k < 3; ++k) {
|
|
const float e = std::max({gm.worldMin[k] - head[k], 0.0f, head[k] - gm.worldMax[k]});
|
|
d += e * e;
|
|
}
|
|
}
|
|
opaqueOrder_.push_back({(gm.alphaToCoverage ? 1e12 : 0.0) + d, i}); // (double: the index survives the offset)
|
|
}
|
|
std::sort(opaqueOrder_.begin(), opaqueOrder_.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
|
|
// The cut-outs (last in the order) in two passes: their depth from the alpha-only shader, then Meta's shader
|
|
// with depth writes off, so overlapping fronds and cards are shaded once per sample, by the front-most one
|
|
// (alpha-to-coverage keeps the GPU from rejecting hidden samples before shading: each frond behind another
|
|
// was lit in full). The same samples, the same front-most surface: the same picture.
|
|
static const bool twoPass = !std::getenv("FH_NO_CUT_PREPASS");
|
|
size_t firstCut = opaqueOrder_.size();
|
|
if (twoPass)
|
|
for (size_t k = 0; k < opaqueOrder_.size(); ++k)
|
|
if (meshAt(opaqueOrder_[k].second).alphaToCoverage) { firstCut = k; break; }
|
|
for (size_t k = 0; k < firstCut; ++k) draw(meshAt(opaqueOrder_[k].second));
|
|
for (size_t k = firstCut; k < opaqueOrder_.size(); ++k) {
|
|
GpuMesh& gm = meshAt(opaqueOrder_[k].second);
|
|
const Program& cp = programs_[gm.program];
|
|
if (gm.variant >= 0 ? !cp.variants[gm.variant].fsAlpha : !cp.fsAlpha) gm.cutOnce = true; // no alpha-only shader: drawn the one way
|
|
else draw(gm, kCutDepth);
|
|
}
|
|
for (size_t k = firstCut; k < opaqueOrder_.size(); ++k) {
|
|
GpuMesh& gm = meshAt(opaqueOrder_[k].second);
|
|
draw(gm, gm.cutOnce ? kCutNone : kCutShade);
|
|
}
|
|
}
|
|
if (!skyFirst()) {
|
|
VkViewport skyVp = vp;
|
|
skyVp.minDepth = skyVp.maxDepth = 0.0f;
|
|
bool set = false;
|
|
for (auto& gm : meshes_)
|
|
if (gm.skybox) {
|
|
if (!set) { vkCmdSetViewport(cmd_, 0, 1, &skyVp); set = true; }
|
|
draw(gm);
|
|
}
|
|
if (set) vkCmdSetViewport(cmd_, 0, 1, &vp);
|
|
}
|
|
drawSplats(eyes, head, planes); // Gaussian splats: over what's solid and the sky, under what's see-through
|
|
if (splatsDrawn_) boundPipe = VK_NULL_HANDLE, boundLayout = VK_NULL_HANDLE; // (they bound their own)
|
|
std::vector<std::pair<float, size_t>> order;
|
|
for (size_t i = 0; i < meshes_.size() + batches_.size(); ++i) { // (past the meshes: the see-through batches, at their box's centre)
|
|
const GpuMesh& gm = meshAt(i);
|
|
if (gm.blend == 0 || gm.skybox || (i >= meshes_.size() && !gm.inView)) continue;
|
|
float d = 0;
|
|
for (int k = 0; k < 3; ++k) d += (gm.centroid[k] - head[k]) * (gm.centroid[k] - head[k]);
|
|
order.push_back({d, i});
|
|
}
|
|
std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
|
|
for (auto& o : order) draw(meshAt(o.second));
|
|
|
|
if (debugView_ != kViewNormal && dbg_.draws.mapped) {
|
|
// Each mesh's colour: its own (a hash of its index), its cost, or its kind.
|
|
auto shown = [&](const GpuMesh& gm) { return gm.visible && gm.inView && !gm.skybox && !(debugHide && hiddenByDebug(gm)); };
|
|
auto hashColor = [](uint32_t i, float* c) {
|
|
uint32_t h = i * 2654435761u;
|
|
h ^= h >> 15; h *= 0x2c1b3c6dU; h ^= h >> 12;
|
|
const float hue = (h & 1023) / 1023.0f * 6.0f, sat = 0.45f + ((h >> 10) & 255) / 255.0f * 0.4f;
|
|
const float x = 1 - std::fabs(std::fmod(hue, 2.0f) - 1);
|
|
float r = 0, g = 0, b = 0;
|
|
switch (static_cast<int>(hue) % 6) { case 0: r = 1; g = x; break; case 1: r = x; g = 1; break; case 2: g = 1; b = x; break;
|
|
case 3: g = x; b = 1; break; case 4: r = x; b = 1; break; default: r = 1; b = x; }
|
|
c[0] = 0.9f * (1 - sat + sat * r); c[1] = 0.9f * (1 - sat + sat * g); c[2] = 0.9f * (1 - sat + sat * b);
|
|
};
|
|
if (debugView_ == kViewWireframe) {
|
|
for (auto& gm : meshes_) {
|
|
if (!shown(gm)) continue;
|
|
const float c[4] = {gm.blend ? 0.35f : gm.alphaToCoverage ? 0.45f : 0.92f, gm.blend ? 0.7f : 0.95f, gm.blend ? 1.0f : gm.alphaToCoverage ? 0.45f : 0.92f, 0.55f};
|
|
drawDebug(gm, kDbgWire, c, eyes, true);
|
|
}
|
|
} else if (debugView_ == kViewMeshes) {
|
|
for (size_t pass = 0; pass < 2; ++pass) // solid ones first, see-through over them
|
|
for (size_t i = 0; i < meshes_.size(); ++i) {
|
|
GpuMesh& gm = meshes_[i];
|
|
if (!shown(gm) || (gm.blend != 0) != (pass == 1)) continue;
|
|
float c[4] = {0, 0, 0, pass ? 0.5f : 1.0f};
|
|
hashColor(static_cast<uint32_t>(gm.sceneIndex >= 0 ? gm.sceneIndex : static_cast<int>(i)), c);
|
|
drawDebug(gm, pass ? kDbgTint : kDbgSolid, c, eyes, false);
|
|
tris += gm.indexCount / 3;
|
|
}
|
|
} else if (debugView_ == kViewCost) {
|
|
for (size_t i = 0; i < meshes_.size(); ++i) {
|
|
GpuMesh& gm = meshes_[i];
|
|
if (!shown(gm)) continue;
|
|
const float ms = i < cost_.size() ? cost_[i] : -1.0f;
|
|
float c[4] = {0.5f, 0.5f, 0.5f, 0.75f}; // not measured (yet)
|
|
if (ms >= 0) { // green -> yellow -> red at costScaleMs, by orders of magnitude from 1/50 of it
|
|
const float lo = std::max(costScaleMs, 1e-4f) / 50;
|
|
const float t = std::clamp(std::log(std::max(ms, lo) / lo) / std::log(50.0f), 0.0f, 1.0f);
|
|
c[0] = std::min(1.0f, t * 2); c[1] = std::min(1.0f, 2 - t * 2) * 0.85f + 0.1f; c[2] = 0.12f;
|
|
}
|
|
drawDebug(gm, kDbgTint, c, eyes, true);
|
|
}
|
|
} else if (debugView_ == kViewOverdraw) {
|
|
for (auto& gm : meshes_) {
|
|
if (!shown(gm)) continue;
|
|
const float c[4] = {0.09f, 0.045f, 0.018f, 1}; // a dozen layers is white
|
|
drawDebug(gm, kDbgAdd, c, eyes, false);
|
|
tris += gm.indexCount / 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sel_.mesh >= 0 && static_cast<size_t>(sel_.mesh) < meshes_.size() && meshes_[sel_.mesh].visible) {
|
|
float vp[16], mvp[16];
|
|
mat4mul4(eyes[0].proj.m, eyes[0].view.m, vp);
|
|
mat4mul4(vp, meshes_[sel_.mesh].world, mvp);
|
|
for (int c = 0; c < 4; ++c) mvp[c * 4 + 2] *= 1.0015f; // a hair toward the eye (reversed-Z): on the surface, not in it
|
|
std::memcpy(sel_.ubo.mapped, mvp, 64);
|
|
VkDeviceSize zero = 0;
|
|
vkCmdBindDescriptorSets(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, sel_.pipeLayout, 0, 1, &sel_.set, 0, nullptr);
|
|
vkCmdBindVertexBuffers(cmd_, 0, 1, &sel_.vbo.buf, &zero);
|
|
vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, sel_.faces);
|
|
vkCmdBindIndexBuffer(cmd_, sel_.tris.buf, 0, VK_INDEX_TYPE_UINT32);
|
|
vkCmdDrawIndexed(cmd_, sel_.triCount, 1, 0, 0, 0);
|
|
vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, sel_.lines);
|
|
vkCmdBindIndexBuffer(cmd_, sel_.edges.buf, 0, VK_INDEX_TYPE_UINT32);
|
|
vkCmdDrawIndexed(cmd_, sel_.edgeCount, 1, 0, 0, 0);
|
|
}
|
|
drawGuide(eyes);
|
|
|
|
if (fdmOn_ && fdmOffset_) { // foveation: each eye's full-detail centre moved to where it looks (the map's middle on that point)
|
|
VkOffset2D offs[2] = {};
|
|
for (uint32_t e = 0; e < viewCount && e < 2; ++e) {
|
|
const float cxp = fovCenter_[e][0] >= 0 ? fovCenter_[e][0] : width_ * 0.5f, cyp = fovCenter_[e][1] >= 0 ? fovCenter_[e][1] : height_ * 0.5f;
|
|
const float mx = fdmW_ * fdmTexel_ * 0.5f, my = fdmH_ * fdmTexel_ * 0.5f;
|
|
const int gx = static_cast<int>(fdmGranularity_), gy = static_cast<int>(fdmGranularityY_);
|
|
offs[e].x = static_cast<int32_t>(std::lround((cxp - mx) / gx)) * gx;
|
|
offs[e].y = static_cast<int32_t>(std::lround((cyp - my) / gy)) * gy;
|
|
}
|
|
VkSubpassFragmentDensityMapOffsetEndInfoQCOM off = {VK_STRUCTURE_TYPE_SUBPASS_FRAGMENT_DENSITY_MAP_OFFSET_END_INFO_QCOM};
|
|
off.fragmentDensityOffsetCount = viewCount;
|
|
off.pFragmentDensityOffsets = offs;
|
|
VkSubpassEndInfo end = {VK_STRUCTURE_TYPE_SUBPASS_END_INFO};
|
|
end.pNext = &off;
|
|
vkCmdEndRenderPass2(cmd_, &end);
|
|
} else {
|
|
vkCmdEndRenderPass(cmd_);
|
|
}
|
|
if (smoothingPass && fovPipe_) { // the foveated frame, its coarse regions filtered, where it goes
|
|
if (direct) recordFovPass(exportFbs_[exportSlot], exportPass_);
|
|
else recordFovPass(fovFb_, fovPass_);
|
|
}
|
|
if (queryPool_) vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 2);
|
|
if (!direct && exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportImages_.size()) { // the frame for the compositor (system mode), copied
|
|
VkImage dst = exportImages_[exportSlot];
|
|
// GENERAL between frames: the layout both processes agree on (the compositor samples it as GENERAL). What the
|
|
// slot held is discarded (UNDEFINED): the copy overwrites all of it, and the compositor never hands it back.
|
|
imageBarrier(cmd_, dst, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0,
|
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
imageBarrier(cmd_, resolve_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
VkImageCopy cp = {};
|
|
cp.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
|
|
cp.dstSubresource = cp.srcSubresource;
|
|
cp.extent = {width_, height_, 1};
|
|
vkCmdCopyImage(cmd_, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
|
|
}
|
|
// The finished frame goes to the compositor: its image released from our queue family to the outside one (the
|
|
// compositor's layer acquires it before sampling, layer/frame_home_layer.cpp). GENERAL to GENERAL, so this is no
|
|
// layout transition and an older layer that doesn't acquire reads it exactly as before. The next frame into this
|
|
// slot discards it (UNDEFINED) rather than taking it back.
|
|
if (exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportImages_.size())
|
|
ownershipBarrier(cmd_, exportImages_[exportSlot], viewCount, queueFamily_, VK_QUEUE_FAMILY_EXTERNAL,
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, 0,
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
|
if (queryPool_) { vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 1); profFrame_ = true; }
|
|
timedSlot_ = exportSlot;
|
|
if (perfThis) { vkCmdEndQuery(cmd_, perfPool_, 0); perfRecorded_ = true; }
|
|
vkEndCommandBuffer(cmd_);
|
|
};
|
|
if (early) { // record now; wait for the last frame after
|
|
refreshDynamicMeshes(true, false);
|
|
record();
|
|
if (!prep()) return;
|
|
t1 = t0; // (the wait is after the recording here)
|
|
} else {
|
|
if (!prep()) return;
|
|
record();
|
|
}
|
|
stats_.early = early;
|
|
VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
si.commandBufferCount = 1;
|
|
si.pCommandBuffers = &cmd_;
|
|
VkTimelineSemaphoreSubmitInfo tsi = {VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO};
|
|
const uint64_t signal = serial_ + 1;
|
|
if (timeline_) {
|
|
tsi.signalSemaphoreValueCount = 1;
|
|
tsi.pSignalSemaphoreValues = &signal;
|
|
si.pNext = &tsi;
|
|
si.signalSemaphoreCount = 1;
|
|
si.pSignalSemaphores = &timeline_;
|
|
}
|
|
VkPerformanceQuerySubmitInfoKHR psi = {VK_STRUCTURE_TYPE_PERFORMANCE_QUERY_SUBMIT_INFO_KHR};
|
|
if (perfPool_) { psi.pNext = si.pNext; si.pNext = ψ }
|
|
const auto t4 = clk::now();
|
|
const VkResult submitted = vkQueueSubmit(queue_, 1, &si, fence_);
|
|
if (submitted == VK_SUCCESS) { if (timeline_) serial_ = signal; }
|
|
else {
|
|
fenceUnsignalled_ = true;
|
|
if (submitted == VK_ERROR_DEVICE_LOST) lost_ = true;
|
|
LOG("frame not submitted: %d%s", static_cast<int>(submitted), lost_ ? " (device lost: the GPU was reset)" : "");
|
|
}
|
|
const auto t5 = clk::now();
|
|
stats_.uploadMs = ms(t2, t3);
|
|
stats_.recordMs = ms(t3, t4);
|
|
stats_.submitMs = ms(t4, t5);
|
|
stats_.totalMs = ms(t0, t5);
|
|
stats_.uploads = lastUploads_;
|
|
stats_.draws = draws;
|
|
stats_.pipelineBinds = pipeBinds;
|
|
stats_.triangles = tris;
|
|
// Slow-frame report (stderr / studio.log): where the time went.
|
|
if (ms(t0, t5) > 40.0)
|
|
LOG("slow frame %.1f ms: wait-GPU %.1f, prep %.1f, texture uploads %.1f, record %.1f, submit %.1f (%zu drawn)",
|
|
ms(t0, t5), ms(t0, t1), ms(t1, t2), ms(t2, t3), ms(t3, t4), ms(t4, t5), drawn_);
|
|
}
|
|
|
|
void HslRenderer::waitFrame() {
|
|
if (lost_ || fenceUnsignalled_) return;
|
|
if (vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX) == VK_ERROR_DEVICE_LOST) lost_ = true;
|
|
}
|
|
|
|
bool HslRenderer::waitFrameSerial(uint64_t serial, uint64_t timeoutNs) {
|
|
if (!timeline_) return true;
|
|
VkSemaphoreWaitInfo wi = {VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO};
|
|
wi.semaphoreCount = 1;
|
|
wi.pSemaphores = &timeline_;
|
|
wi.pValues = &serial;
|
|
const VkResult r = vkWaitSemaphores(device_, &wi, timeoutNs);
|
|
if (r == VK_ERROR_DEVICE_LOST) lost_ = true;
|
|
return r == VK_SUCCESS;
|
|
}
|
|
|
|
bool HslRenderer::createExportImages(uint32_t n, std::vector<ExportImage>& out, uint32_t& memoryTypeOut, uint8_t deviceUUID[16]) {
|
|
destroyExportImages(out);
|
|
auto getFd = reinterpret_cast<PFN_vkGetMemoryFdKHR>(vkGetDeviceProcAddr(device_, "vkGetMemoryFdKHR"));
|
|
if (!getFd) { LOG("system mode: no VK_KHR_external_memory_fd"); return false; }
|
|
VkPhysicalDeviceIDProperties idp = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
|
|
VkPhysicalDeviceProperties2 pp = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
|
pp.pNext = &idp;
|
|
vkGetPhysicalDeviceProperties2(physical_, &pp);
|
|
std::memcpy(deviceUUID, idp.deviceUUID, 16);
|
|
for (uint32_t i = 0; i < n; ++i) {
|
|
ExportImage e;
|
|
VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
|
|
ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
|
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
ci.pNext = &ext;
|
|
ci.imageType = VK_IMAGE_TYPE_2D;
|
|
ci.format = VK_FORMAT_R8G8B8A8_SRGB;
|
|
ci.extent = {width_, height_, 1};
|
|
ci.mipLevels = 1;
|
|
ci.arrayLayers = viewCount;
|
|
ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
ci.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
ci.usage = exportUsage();
|
|
ci.flags = exportCreateFlags();
|
|
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
if (vkCreateImage(device_, &ci, nullptr, &e.image) != VK_SUCCESS) { destroyExportImages(out); return false; }
|
|
VkMemoryRequirements req;
|
|
vkGetImageMemoryRequirements(device_, e.image, &req);
|
|
const uint32_t type = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
|
ded.image = e.image;
|
|
VkExportMemoryAllocateInfo exp = {VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO};
|
|
exp.pNext = &ded;
|
|
exp.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
|
ai.pNext = &exp;
|
|
ai.allocationSize = req.size;
|
|
ai.memoryTypeIndex = type;
|
|
if (vkAllocateMemory(device_, &ai, nullptr, &e.mem) != VK_SUCCESS || vkBindImageMemory(device_, e.image, e.mem, 0) != VK_SUCCESS) {
|
|
out.push_back(e);
|
|
destroyExportImages(out);
|
|
return false;
|
|
}
|
|
e.size = req.size;
|
|
VkMemoryGetFdInfoKHR gi = {VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR};
|
|
gi.memory = e.mem;
|
|
gi.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
|
if (getFd(device_, &gi, &e.fd) != VK_SUCCESS) e.fd = -1;
|
|
memoryTypeOut = type;
|
|
out.push_back(e);
|
|
if (e.fd < 0) { destroyExportImages(out); return false; }
|
|
}
|
|
exportImages_.clear();
|
|
for (const ExportImage& e : out) exportImages_.push_back(e.image);
|
|
if (exportDirect) createExportFramebuffers();
|
|
return true;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// Windows system mode: the compositor made kSlots shared Direct3D 11 textures (R8G8B8A8_UNORM_SRGB, `n` layers) at
|
|
// this render size and duplicated their NT handles into us. Each becomes a VkImage we render the home into; render()
|
|
// copies the eye frame into exportImages_[exportSlot] through the ordinary (non-direct) copy path. No cross-API lock:
|
|
// a slot is published only once its frame is retired and the four rotate, as on Linux.
|
|
bool HslRenderer::importWin32Images(uint32_t n, const uint64_t handles[], uint32_t w, uint32_t h) {
|
|
vkDeviceWaitIdle(device_); // an earlier generation may still be in flight
|
|
for (VkImage img : exportImages_) vkDestroyImage(device_, img, nullptr);
|
|
for (VkDeviceMemory m : importMem_) vkFreeMemory(device_, m, nullptr);
|
|
exportImages_.clear();
|
|
importMem_.clear();
|
|
exportSlot = -1;
|
|
exportDirect = false; // copied in (the imported textures aren't our render targets)
|
|
for (uint32_t i = 0; i < n; ++i) {
|
|
VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
|
|
ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
|
|
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
ci.pNext = &ext;
|
|
ci.imageType = VK_IMAGE_TYPE_2D;
|
|
ci.format = VK_FORMAT_R8G8B8A8_SRGB; // == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
|
|
ci.extent = {w, h, 1};
|
|
ci.mipLevels = 1;
|
|
ci.arrayLayers = viewCount; // 2 layers, one per eye
|
|
ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
ci.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
ci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
VkImage image = VK_NULL_HANDLE;
|
|
if (vkCreateImage(device_, &ci, nullptr, &image) != VK_SUCCESS) { LOG("system mode: import image %u failed", i); return false; }
|
|
VkMemoryRequirements req;
|
|
vkGetImageMemoryRequirements(device_, image, &req);
|
|
VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
|
ded.image = image;
|
|
VkImportMemoryWin32HandleInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR};
|
|
imp.pNext = &ded;
|
|
imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
|
|
imp.handle = reinterpret_cast<void*>(static_cast<uintptr_t>(handles[i]));
|
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
|
ai.pNext = &imp;
|
|
ai.allocationSize = req.size;
|
|
ai.memoryTypeIndex = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
VkDeviceMemory mem = VK_NULL_HANDLE;
|
|
if (vkAllocateMemory(device_, &ai, nullptr, &mem) != VK_SUCCESS || vkBindImageMemory(device_, image, mem, 0) != VK_SUCCESS) {
|
|
LOG("system mode: import memory %u failed (adapter mismatch, or the compositor's textures went away)", i);
|
|
if (mem) vkFreeMemory(device_, mem, nullptr);
|
|
vkDestroyImage(device_, image, nullptr);
|
|
return false;
|
|
}
|
|
exportImages_.push_back(image);
|
|
importMem_.push_back(mem);
|
|
}
|
|
LOG("system mode: imported %u compositor textures (%ux%u, %u layers)", n, w, h, viewCount);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
uint32_t HslRenderer::exportUsage() const {
|
|
return VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
|
(exportDirect ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0u);
|
|
}
|
|
|
|
uint32_t HslRenderer::exportCreateFlags() const {
|
|
return exportsSubsampled() ? VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT : 0u;
|
|
}
|
|
|
|
// exportDirect: a framebuffer per export image, its last colour attachment that image (the resolve, or the colour itself
|
|
// without MSAA). None of them (and exportSlot frames are copied instead) if the pass or a view can't be made.
|
|
void HslRenderer::createExportFramebuffers() {
|
|
destroyExportFramebuffers();
|
|
if (!exportPass_) return;
|
|
const bool ms = samples_ != VK_SAMPLE_COUNT_1_BIT;
|
|
for (VkImage img : exportImages_) {
|
|
VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
vi.image = img;
|
|
vi.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
|
vi.format = VK_FORMAT_R8G8B8A8_SRGB;
|
|
vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, viewCount};
|
|
VkImageView v = VK_NULL_HANDLE;
|
|
if (vkCreateImageView(device_, &vi, nullptr, &v) != VK_SUCCESS) { destroyExportFramebuffers(); return; }
|
|
exportViews_.push_back(v);
|
|
std::vector<VkImageView> views = ms ? std::vector<VkImageView>{colorMs_.view, depth_.view, v} : std::vector<VkImageView>{v, depth_.view};
|
|
if (fdmOn_) views.push_back(fdm_.view);
|
|
if (smoothFov_ && !exportsSubsampled()) views = {v}; // our smoothing pass writes it (exportPass_ is colorOnlyPass)
|
|
VkFramebufferCreateInfo fb = {VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
fb.renderPass = exportPass_;
|
|
fb.attachmentCount = static_cast<uint32_t>(views.size());
|
|
fb.pAttachments = views.data();
|
|
fb.width = width_;
|
|
fb.height = height_;
|
|
fb.layers = 1;
|
|
VkFramebuffer f = VK_NULL_HANDLE;
|
|
if (vkCreateFramebuffer(device_, &fb, nullptr, &f) != VK_SUCCESS) { destroyExportFramebuffers(); return; }
|
|
exportFbs_.push_back(f);
|
|
}
|
|
LOG("frames drawn straight into the compositor's %zu images (no copy)", exportFbs_.size());
|
|
}
|
|
|
|
void HslRenderer::destroyExportFramebuffers() {
|
|
for (VkFramebuffer f : exportFbs_) vkDestroyFramebuffer(device_, f, nullptr);
|
|
for (VkImageView v : exportViews_) vkDestroyImageView(device_, v, nullptr);
|
|
exportFbs_.clear();
|
|
exportViews_.clear();
|
|
lastExportSlot_ = -1;
|
|
}
|
|
|
|
void HslRenderer::destroyExportImages(std::vector<ExportImage>& images) {
|
|
if (images.empty()) return;
|
|
vkDeviceWaitIdle(device_);
|
|
for (ExportImage& e : images) {
|
|
#ifndef _WIN32
|
|
if (e.fd >= 0) ::close(e.fd); // export is a Frame Home (Linux) thing
|
|
#endif
|
|
if (e.image) vkDestroyImage(device_, e.image, nullptr);
|
|
if (e.mem) vkFreeMemory(device_, e.mem, nullptr);
|
|
}
|
|
destroyExportFramebuffers();
|
|
images.clear();
|
|
exportImages_.clear();
|
|
exportSlot = -1;
|
|
}
|
|
|
|
bool HslRenderer::readback(uint32_t layer, std::vector<u8>& rgba) {
|
|
vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX);
|
|
const VkDeviceSize size = static_cast<VkDeviceSize>(width_) * height_ * 4;
|
|
Buffer b = createBuffer(size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, true);
|
|
if (!b.buf) return false;
|
|
oneShot([&](VkCommandBuffer c) {
|
|
VkBufferImageCopy copy = {};
|
|
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1};
|
|
copy.imageExtent = {width_, height_, 1};
|
|
if (lastExportSlot_ >= 0 && static_cast<size_t>(lastExportSlot_) < exportImages_.size()) { // exportDirect: the frame is there
|
|
// (released to the compositor: taken back for the copy, then handed over again)
|
|
VkImage img = exportImages_[lastExportSlot_];
|
|
ownershipBarrier(c, img, viewCount, VK_QUEUE_FAMILY_EXTERNAL, queueFamily_, 0, VK_ACCESS_TRANSFER_READ_BIT,
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
vkCmdCopyImageToBuffer(c, img, VK_IMAGE_LAYOUT_GENERAL, b.buf, 1, ©);
|
|
ownershipBarrier(c, img, viewCount, queueFamily_, VK_QUEUE_FAMILY_EXTERNAL, 0, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
|
} else vkCmdCopyImageToBuffer(c, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, b.buf, 1, ©);
|
|
});
|
|
rgba.assign(static_cast<u8*>(b.mapped), static_cast<u8*>(b.mapped) + size);
|
|
destroy(b);
|
|
return true;
|
|
}
|
|
|
|
void HslRenderer::shutdown() {
|
|
if (!device_) return;
|
|
stopWorkers();
|
|
unload();
|
|
destroyGuide();
|
|
destroySplatPipeline();
|
|
destroyDebug();
|
|
for (VkImageView v : layerViews_) if (v) vkDestroyImageView(device_, v, nullptr);
|
|
for (Image& i : pictures_) destroy(i);
|
|
pictures_.clear();
|
|
for (Image* i : {&colorMs_, &depth_, &resolve_, &fdm_, &sub_, &white_, &black_, &nullTex_, &noMetal_, &flatNormal_, &whiteCube_, &white3D_, &shadowMap_}) destroy(*i);
|
|
if (fovFb_) vkDestroyFramebuffer(device_, fovFb_, nullptr);
|
|
if (fovPipe_) vkDestroyPipeline(device_, fovPipe_, nullptr);
|
|
if (fovLayout_) vkDestroyPipelineLayout(device_, fovLayout_, nullptr);
|
|
if (fovPool_) vkDestroyDescriptorPool(device_, fovPool_, nullptr);
|
|
if (fovSetLayout_) vkDestroyDescriptorSetLayout(device_, fovSetLayout_, nullptr);
|
|
if (subSampler_) vkDestroySampler(device_, subSampler_, nullptr);
|
|
if (fovPass_) vkDestroyRenderPass(device_, fovPass_, nullptr);
|
|
for (Buffer* b : {&globals_, &lightUniforms_, &lightItems_, &lightGrid_, &ring_}) destroy(*b);
|
|
for (MemBlock& b : blocks_) {
|
|
if (b.mapped) vkUnmapMemory(device_, b.mem);
|
|
vkFreeMemory(device_, b.mem, nullptr);
|
|
}
|
|
blocks_.clear();
|
|
vkDestroySampler(device_, wrapSampler_, nullptr);
|
|
vkDestroySampler(device_, anisoSampler_, nullptr);
|
|
vkDestroySampler(device_, shadowSampler_, nullptr);
|
|
vkDestroyFramebuffer(device_, framebuffer_, nullptr);
|
|
vkDestroyRenderPass(device_, renderPass_, nullptr);
|
|
if (exportPass_) vkDestroyRenderPass(device_, exportPass_, nullptr);
|
|
#ifdef _WIN32
|
|
for (VkImage img : exportImages_) vkDestroyImage(device_, img, nullptr); // imported compositor textures
|
|
for (VkDeviceMemory m : importMem_) vkFreeMemory(device_, m, nullptr);
|
|
exportImages_.clear();
|
|
importMem_.clear();
|
|
#endif
|
|
if (pipelineCache_) {
|
|
size_t n = 0;
|
|
if (!pipelineCachePath.empty() && vkGetPipelineCacheData(device_, pipelineCache_, &n, nullptr) == VK_SUCCESS && n) {
|
|
std::vector<char> data(n);
|
|
if (vkGetPipelineCacheData(device_, pipelineCache_, &n, data.data()) == VK_SUCCESS)
|
|
if (FILE* f = texcache::open(std::filesystem::u8path(pipelineCachePath), true)) { std::fwrite(data.data(), 1, n, f); std::fclose(f); }
|
|
}
|
|
vkDestroyPipelineCache(device_, pipelineCache_, nullptr);
|
|
}
|
|
if (queryPool_) vkDestroyQueryPool(device_, queryPool_, nullptr);
|
|
if (perfPool_) {
|
|
vkDestroyQueryPool(device_, perfPool_, nullptr);
|
|
if (auto unlock = reinterpret_cast<PFN_vkReleaseProfilingLockKHR>(vkGetDeviceProcAddr(device_, "vkReleaseProfilingLockKHR"))) unlock(device_);
|
|
}
|
|
destroySelection();
|
|
vkDestroyDescriptorPool(device_, descPool_, nullptr);
|
|
vkDestroyFence(device_, fence_, nullptr);
|
|
if (timeline_) vkDestroySemaphore(device_, timeline_, nullptr);
|
|
timeline_ = VK_NULL_HANDLE;
|
|
vkDestroyCommandPool(device_, pool_, nullptr);
|
|
vkDestroyDevice(device_, nullptr);
|
|
vkDestroyInstance(instance_, nullptr);
|
|
device_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
void setTextureCap(uint32_t side) { g_textureCap = side; }
|