diff --git a/include/Inventor/rendering/SoVulkanRenderManager.h b/include/Inventor/rendering/SoVulkanRenderManager.h index 82f22987033..352df58c79b 100644 --- a/include/Inventor/rendering/SoVulkanRenderManager.h +++ b/include/Inventor/rendering/SoVulkanRenderManager.h @@ -151,9 +151,11 @@ class COIN_DLL_API SoVulkanRenderManager { */ void setWireframeOverlay(SbBool enabled); void setPointsOverlay(SbBool enabled); + void setTessellationOverlay(SbBool enabled); void setEdgeColor(const SbColor4f & color); SbBool getWireframeOverlay(void) const; SbBool getPointsOverlay(void) const; + SbBool getTessellationOverlay(void) const; const SbColor4f & getEdgeColor(void) const; void setClearEnabled(SbBool clearwindow, SbBool clearzbuffer); diff --git a/src/rendering/CMakeLists.txt b/src/rendering/CMakeLists.txt index 2aef64a0226..31378ac6d0c 100644 --- a/src/rendering/CMakeLists.txt +++ b/src/rendering/CMakeLists.txt @@ -14,6 +14,7 @@ set(COIN_RENDERING_FILES if(COIN_BUILD_VULKAN_RENDERER) list(APPEND COIN_RENDERING_FILES SoVulkanRenderBackend/SoVulkanRenderBackendCore.cpp + SoVulkanRenderBackend/SoVulkanRenderPassCache.cpp SoVulkanRenderBackend/SoVulkanMemPool.cpp SoVulkanRenderBackend/SoVulkanRenderBackendPipeline.cpp SoVulkanRenderBackend/SoVulkanRenderBackendGeometry.cpp @@ -74,6 +75,7 @@ set(COIN_RENDERING_INTERNAL_FILES SoGLRenderBackend.h SoVulkanRenderBackend.h SoVulkanRenderBackend/SoVulkanRenderBackendP.h + SoVulkanRenderBackend/SoVulkanRenderPassCache.h SoVulkanShared.h SoVulkanRenderManager.h SoRTXRenderBackend.h diff --git a/src/rendering/SoRTXRenderBackend.h b/src/rendering/SoRTXRenderBackend.h index 22472b4459b..5c6bf5576ca 100644 --- a/src/rendering/SoRTXRenderBackend.h +++ b/src/rendering/SoRTXRenderBackend.h @@ -394,6 +394,9 @@ class SoRTXRenderBackend : public SoRenderBackend { VkCommandBuffer cmd); bool refitBlas(RTXCachedGeometry & entry, const SoRenderCommand & command, VkCommandBuffer cmd); + bool blasBuildOrRefit(RTXCachedGeometry & entry, + const SoRenderCommand & command, VkCommandBuffer cmd, + bool refit); void destroyCacheEntry(RTXCachedGeometry & entry); bool buildTlas(const SoDrawList & drawlist, const SoRenderParams & params, VkCommandBuffer cmd); @@ -847,6 +850,14 @@ class SoRTXRenderBackend : public SoRenderBackend { bool ensureNeePoolCapacity(VkDeviceSize bytes); void buildNeePool(const SoDrawList & drawlist); + // Shared grow-only pool (re)allocation used by ensureNormalPoolCapacity() + // and ensureNeePoolCapacity(): double the host-visible pool until the + // requested size fits, preserving the existing contents and used count. + bool ensurePoolCapacity(VkDeviceSize bytes, VkBuffer & poolBuffer, + VkDeviceMemory & poolMemory, void *& poolMapped, + VkDeviceSize & poolCapacity, VkDeviceSize & poolUsed, + bool refreshDescriptors); + // --- Cache bookkeeping --------------------------------------------------- std::vector geometryCache; std::unordered_map commandToCache; @@ -1167,6 +1178,11 @@ class SoRTXRenderBackend : public SoRenderBackend { void updateDenoise(); //! Read the already-traced storage image back to a PPM (FC_VULKAN_PT_DUMP). void dumpStorageImageIfRequested(); + //! Read the float accumulation buffer back to a PPM (FC_VULKAN_PT_DUMP_ACCUM). + void dumpAccumBufferIfRequested(); + //! Read the per-pixel G-buffers (normal/position) back to PPMs + //! (FC_VULKAN_PT_DUMP_GBUF=). + void dumpGbuffersIfRequested(); //! After publishing a denoised result at target: clear the denoise latch and //! transition to converged-idle so the viewport keeps the denoised image and //! stops the continuous-update loop. Also used on the failure paths (with a diff --git a/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendCore.cpp b/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendCore.cpp index fa87743c305..b1977cd4f31 100644 --- a/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendCore.cpp +++ b/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendCore.cpp @@ -4,6 +4,7 @@ // member functions for the "Core" concern of the Vulkan RTX backend. #include "rendering/SoRTXRenderBackend.h" +#include "rendering/SoVulkanShared.h" #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include using namespace SoRTXBackend; @@ -441,6 +443,9 @@ SoRTXRenderBackend::probeComputeQueue(void) SbBool SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) { + SoVulkanShared::initBreadcrumb("SoRTXRenderBackend::initialize enter " + "alreadyInit=%d\n", + this->isInitialized() ? 1 : 0); if (this->isInitialized()) return TRUE; this->setInitParams(params); @@ -450,12 +455,19 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) deviceContext->physicalDevice == VK_NULL_HANDLE || deviceContext->device == VK_NULL_HANDLE || deviceContext->graphicsQueue == VK_NULL_HANDLE) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL invalid device context\n"); this->emitError( "SoRTXRenderBackend requires a SoVulkanDeviceContext in " "SoRenderBackendInitParams::userData"); return FALSE; } if (deviceContext->apiVersion < VK_API_VERSION_1_2) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL apiVersion %u.%u.%u < 1.2\n", + VK_API_VERSION_MAJOR(deviceContext->apiVersion), + VK_API_VERSION_MINOR(deviceContext->apiVersion), + VK_API_VERSION_PATCH(deviceContext->apiVersion)); char buf[192]; std::snprintf(buf, sizeof(buf), "SoRTXRenderBackend requires a Vulkan 1.2+ device (device " @@ -467,6 +479,16 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) return FALSE; } + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize device=0x%llx pdev=0x%llx api=%u.%u.%u " + "computeFam=%u\n", + (unsigned long long)(uintptr_t)deviceContext->device, + (unsigned long long)(uintptr_t)deviceContext->physicalDevice, + VK_API_VERSION_MAJOR(deviceContext->apiVersion), + VK_API_VERSION_MINOR(deviceContext->apiVersion), + VK_API_VERSION_PATCH(deviceContext->apiVersion), + deviceContext->computeQueueFamilyIndex); + this->instance = deviceContext->instance; this->physicalDevice = deviceContext->physicalDevice; this->device = deviceContext->device; @@ -484,6 +506,8 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) // Acquire a compute queue for the optional async-compute path, and report // the capability so a probe/check can verify. this->probeComputeQueue(); + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize probeComputeQueue done\n"); // Cache the physical-device identity so the denoiser selection can gate the // CUDA/OptiX path on NVIDIA hardware (see SoRTXRenderBackend.h). @@ -554,6 +578,9 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) this->hasNvLinearSweptSpheres ? 1 : 0); fprintf(stderr, "%s\n", capsBuf); } + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize nvidia=%d uuid=%d\n", + this->deviceIsNvidia ? 1 : 0, this->haveDeviceUUID ? 1 : 0); // The system loader only exports core entry points; resolve the ray // tracing KHR functions per-device. Failing here means the device is @@ -587,12 +614,16 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) !this->vkGetAccelerationStructureDeviceAddressKHR || !this->vkCmdWriteAccelerationStructuresPropertiesKHR || !this->vkCmdCopyAccelerationStructureKHR) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL resolve AS KHR entry points\n"); this->emitError( "failed to resolve ray tracing KHR entry points; the device or " "loader does not provide VK_KHR_acceleration_structure"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize AS KHR entry points OK\n"); // The ray tracing pipeline (VK_KHR_ray_tracing_pipeline) entry points // power the shader binding table dispatch. @@ -608,12 +639,16 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) if (!this->vkCreateRayTracingPipelinesKHR || !this->vkGetRayTracingShaderGroupHandlesKHR || !this->vkCmdTraceRaysKHR) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL resolve RT pipeline entry points\n"); this->emitError( "failed to resolve VK_KHR_ray_tracing_pipeline entry points; the " "device or loader does not provide the ray tracing pipeline"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize RT pipeline entry points OK\n"); // Dispatch mode: the SBT pipeline is opt-in (FC_VULKAN_RT_SBT=1); the // default ray-query compute path avoids a hang in NVIDIA driver 610.x @@ -658,31 +693,45 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) this->sbtRecordSize += alignment - 1; this->sbtRecordSize -= this->sbtRecordSize % alignment; + SoVulkanShared::initBreadcrumb("SoRTXRenderBackend::initialize " + "creating resources\n"); if (!this->createDescriptorSetLayout()) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL createDescriptorSetLayout\n"); this->emitError("failed to create RT descriptor set layout"); this->shutdown(); return FALSE; } if (!this->createDescriptorPool()) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL createDescriptorPool\n"); this->emitError("failed to create RT descriptor pool"); this->shutdown(); return FALSE; } if (!this->createShaderModules()) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL createShaderModules\n"); this->emitError("failed to create RT shader modules"); this->shutdown(); return FALSE; } if (!this->createPipelines()) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL createPipelines\n"); this->emitError("failed to create ray tracing pipeline"); this->shutdown(); return FALSE; } if (!this->createFrameBuffer()) { + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize FAIL createFrameBuffer\n"); this->emitError("failed to create RT frame uniform buffer"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb( + "SoRTXRenderBackend::initialize resource creation OK\n"); // Optional path tracing tuning (kept out of the public API for now). if (const char * bounces = getenv("FC_VULKAN_PT_BOUNCES")) { @@ -742,6 +791,7 @@ SoRTXRenderBackend::initialize(const SoRenderBackendInitParams & params) } this->setInitialized(TRUE); + SoVulkanShared::initBreadcrumb("SoRTXRenderBackend::initialize DONE\n"); this->emitLog("initialized (Vulkan ray tracing)"); return TRUE; } @@ -1520,10 +1570,19 @@ SoRTXRenderBackend::dumpStorageImageIfRequested() FILE * f = fopen(fullpath, "wb"); if (f) { fprintf(f, "P6\n%u %u\n255\n", w, h); + // Source rows are packed RGBA (4 bytes/px); the PPM is RGB, so + // copy 3 bytes per pixel (drop the alpha) instead of writing the + // raw row, which would interleave alpha into the color channels. + std::vector row(static_cast(w) * 3); const size_t rowbytes = static_cast(w) * 4; for (uint32_t y = 0; y < h; ++y) { const unsigned char * r = src + (static_cast(y) * rowbytes); - fwrite(r, 1, static_cast(w) * 3, f); + for (uint32_t x = 0; x < w; ++x) { + row[3u * x + 0u] = r[4u * x + 0u]; + row[3u * x + 1u] = r[4u * x + 1u]; + row[3u * x + 2u] = r[4u * x + 2u]; + } + fwrite(row.data(), 1, row.size(), f); } fclose(f); fprintf(stderr, "[RTDBG] dumpStorageImage: wrote %s %ux%u\n", @@ -1541,6 +1600,238 @@ SoRTXRenderBackend::dumpStorageImageIfRequested() vkFreeMemory(this->device, stagingMem, this->allocator); } +// Debug: dump the float accumulation buffer (what the present pass and the +// denoiser actually consume) as an 8-bit PPM. Mirrors +// dumpStorageImageIfRequested() for the buffer instead of the single-sample +// storage image. Gated on FC_VULKAN_PT_DUMP_ACCUM with the same +// FC_VULKAN_PT_DUMP_EVERY / FC_VULKAN_PT_DUMP_FRAME cadence. +void +SoRTXRenderBackend::dumpAccumBufferIfRequested() +{ + const char * path = getenv("FC_VULKAN_PT_DUMP_ACCUM"); + if (!path) return; + if (this->accumBuffer == VK_NULL_HANDLE || + this->ptBufferWidth == 0 || this->ptBufferHeight == 0) return; + + const char * everystr = getenv("FC_VULKAN_PT_DUMP_EVERY"); + const char * atstr = getenv("FC_VULKAN_PT_DUMP_FRAME"); + const uint32_t dumpAt = + atstr ? static_cast(std::atoi(atstr)) : this->ptMaxSamples; + bool ok = false; + if (everystr) { + const uint32_t every = static_cast(std::atoi(everystr)); + if (every == 0 || this->ptFrameIndex % every != 0) return; + ok = true; + } else if (this->ptFrameIndex != dumpAt || this->ptDumpDone) { + return; + } + if (ok) this->ptDumpDone = TRUE; + + char fullpath[4096]; + if (everystr) { + std::snprintf(fullpath, sizeof(fullpath), "%s.%03u.ppm", path, + this->ptFrameIndex); + } else { + std::snprintf(fullpath, sizeof(fullpath), "%s", path); + } + + const uint32_t w = this->ptBufferWidth; + const uint32_t h = this->ptBufferHeight; + const VkDeviceSize size = + static_cast(w) * h * 4 * sizeof(float); + + VkBuffer staging = VK_NULL_HANDLE; + VkDeviceMemory stagingMem = VK_NULL_HANDLE; + if (!this->createHostVisibleBuffer( + size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, staging, stagingMem)) { + fprintf(stderr, "[RTDBG] dumpAccum: createHostVisibleBuffer failed\n"); + return; + } + + VkCommandBuffer cmd = this->beginTransientCommandBuffer(); + if (cmd == VK_NULL_HANDLE) { + vkDestroyBuffer(this->device, staging, this->allocator); + vkFreeMemory(this->device, stagingMem, this->allocator); + return; + } + + VkMemoryBarrier bar {}; + bar.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + bar.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + bar.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &bar, 0, + nullptr, 0, nullptr); + VkBufferCopy c {0, 0, size}; + vkCmdCopyBuffer(cmd, this->accumBuffer, staging, 1, &c); + if (vkEndCommandBuffer(cmd) == VK_SUCCESS) { + VkSubmitInfo si {}; + si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + si.commandBufferCount = 1; + si.pCommandBuffers = &cmd; + if (vkQueueSubmit(this->queue, 1, &si, VK_NULL_HANDLE) == VK_SUCCESS && + vkQueueWaitIdle(this->queue) == VK_SUCCESS) { + void * mapped = nullptr; + if (vkMapMemory(this->device, stagingMem, 0, size, 0, &mapped) != + VK_SUCCESS || + mapped == nullptr) { + fprintf(stderr, "[RTDBG] dumpAccum: vkMapMemory failed\n"); + } else { + const float * src = static_cast(mapped); + FILE * f = fopen(fullpath, "wb"); + if (f) { + fprintf(f, "P6\n%u %u\n255\n", w, h); + std::vector row(static_cast(w) * 3); + for (uint32_t y = 0; y < h; ++y) { + const float * r = src + static_cast(y) * w * 4; + for (uint32_t x = 0; x < w; ++x) { + const float a = r[x * 4 + 3]; + const float inv = a > 1e-6f ? 1.0f / a : 0.0f; + for (int k = 0; k < 3; ++k) { + float v = r[x * 4 + k] * inv; + if (v < 0.0f) v = 0.0f; + if (v > 1.0f) v = 1.0f; + row[3u * x + k] = static_cast(v * 255.0f + 0.5f); + } + } + fwrite(row.data(), 1, row.size(), f); + } + fclose(f); + fprintf(stderr, "[RTDBG] dumpAccum: wrote %s %ux%u\n", fullpath, + w, h); + } else { + fprintf(stderr, "[RTDBG] dumpAccum: fopen %s failed\n", fullpath); + } + vkUnmapMemory(this->device, stagingMem); + } + } + } + + vkDestroyBuffer(this->device, staging, this->allocator); + vkFreeMemory(this->device, stagingMem, this->allocator); +} + +// Debug: dump the per-pixel G-buffers the tracer wrote this frame (the +// first-bounce normal and hit position) as 8-bit PPMs into a directory. +// Gated on FC_VULKAN_PT_DUMP_GBUF with the same +// FC_VULKAN_PT_DUMP_EVERY / FC_VULKAN_PT_DUMP_FRAME cadence. +void +SoRTXRenderBackend::dumpGbuffersIfRequested() +{ + const char * dir = getenv("FC_VULKAN_PT_DUMP_GBUF"); + if (!dir) return; + if (this->normalBuffer == VK_NULL_HANDLE || + this->positionBuffer == VK_NULL_HANDLE || + this->ptBufferWidth == 0 || this->ptBufferHeight == 0) return; + + const char * everystr = getenv("FC_VULKAN_PT_DUMP_EVERY"); + const char * atstr = getenv("FC_VULKAN_PT_DUMP_FRAME"); + const uint32_t dumpAt = + atstr ? static_cast(std::atoi(atstr)) : this->ptMaxSamples; + bool ok = false; + if (everystr) { + const uint32_t every = static_cast(std::atoi(everystr)); + if (every == 0 || this->ptFrameIndex % every != 0) return; + ok = true; + } else if (this->ptFrameIndex != dumpAt || this->ptDumpDone) { + return; + } + if (ok) this->ptDumpDone = TRUE; + + const uint32_t w = this->ptBufferWidth; + const uint32_t h = this->ptBufferHeight; + const VkDeviceSize size = + static_cast(w) * h * 4 * sizeof(float); + + char base[4096]; + std::snprintf(base, sizeof(base), "%s/f%03u", dir, this->ptFrameIndex); + char normpath[4160]; + char pospath[4160]; + std::snprintf(normpath, sizeof(normpath), "%s_normals.ppm", base); + std::snprintf(pospath, sizeof(pospath), "%s_positions.ppm", base); + + struct GBuf { + VkBuffer src; + const char * path; + int kind; // 0 = normal (n*0.5+0.5), 1 = position ((p+50)/120) + }; + const GBuf gbs[2] = { + {this->normalBuffer, normpath, 0}, + {this->positionBuffer, pospath, 1}, + }; + + std::vector row(static_cast(w) * 3); + for (const GBuf & gb : gbs) { + if (gb.src == VK_NULL_HANDLE) continue; + VkBuffer staging = VK_NULL_HANDLE; + VkDeviceMemory stagingMem = VK_NULL_HANDLE; + if (!this->createHostVisibleBuffer( + size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, staging, stagingMem)) { + fprintf(stderr, "[RTDBG] dumpGbuf: createHostVisibleBuffer failed\n"); + continue; + } + VkCommandBuffer cmd = this->beginTransientCommandBuffer(); + if (cmd == VK_NULL_HANDLE) { + vkDestroyBuffer(this->device, staging, this->allocator); + vkFreeMemory(this->device, stagingMem, this->allocator); + continue; + } + VkMemoryBarrier bar {}; + bar.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + bar.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + bar.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &bar, 0, + nullptr, 0, nullptr); + VkBufferCopy c {0, 0, size}; + vkCmdCopyBuffer(cmd, gb.src, staging, 1, &c); + if (vkEndCommandBuffer(cmd) == VK_SUCCESS) { + VkSubmitInfo si {}; + si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + si.commandBufferCount = 1; + si.pCommandBuffers = &cmd; + if (vkQueueSubmit(this->queue, 1, &si, VK_NULL_HANDLE) == VK_SUCCESS && + vkQueueWaitIdle(this->queue) == VK_SUCCESS) { + void * mapped = nullptr; + if (vkMapMemory(this->device, stagingMem, 0, size, 0, &mapped) != + VK_SUCCESS || + mapped == nullptr) { + fprintf(stderr, "[RTDBG] dumpGbuf: vkMapMemory failed\n"); + } else { + const float * src = static_cast(mapped); + FILE * f = fopen(gb.path, "wb"); + if (f) { + fprintf(f, "P6\n%u %u\n255\n", w, h); + for (uint32_t y = 0; y < h; ++y) { + const float * r = src + static_cast(y) * w * 4; + for (uint32_t x = 0; x < w; ++x) { + for (int k = 0; k < 3; ++k) { + float v = gb.kind == 0 ? r[x * 4 + k] * 0.5f + 0.5f + : (r[x * 4 + k] + 50.0f) / 120.0f; + if (v < 0.0f) v = 0.0f; + if (v > 1.0f) v = 1.0f; + row[3u * x + k] = + static_cast(v * 255.0f + 0.5f); + } + } + fwrite(row.data(), 1, row.size(), f); + } + fclose(f); + fprintf(stderr, "[RTDBG] dumpGbuf: wrote %s\n", gb.path); + } + vkUnmapMemory(this->device, stagingMem); + } + } + } + vkDestroyBuffer(this->device, staging, this->allocator); + vkFreeMemory(this->device, stagingMem, this->allocator); + } +} + SbBool SoRTXRenderBackend::renderExternal(const SoDrawList & drawlist, const SoRenderParams & params, @@ -1643,6 +1934,8 @@ SbBool // The trace ran in the AS phase above and the queue is idle, so storageImage // holds the current ray-traced result (if the tracer is accumulating). this->dumpStorageImageIfRequested(); + this->dumpAccumBufferIfRequested(); + this->dumpGbuffersIfRequested(); // The present pass is recorded into the caller's buffer (inside its // render pass); the trace ran in the AS phase above. The descriptor set diff --git a/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendGeometry.cpp b/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendGeometry.cpp index 9782d6e48ac..a75cb43e208 100644 --- a/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendGeometry.cpp +++ b/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendGeometry.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include "rendering/vulkan/rt/PathTrace.spv.h" @@ -27,48 +28,11 @@ using namespace SoRTXBackend; bool SoRTXRenderBackend::ensureNormalPoolCapacity(VkDeviceSize bytes) { - if (this->normalPoolBuffer != VK_NULL_HANDLE && - this->normalPoolCapacity >= bytes) { - return true; - } - // Grow-only pool: double until the requested size fits. The new buffer - // is created (and mapped) before the old one is released, so a failed - // allocation leaves the previous pool intact and usable. The old buffer - // is only referenced by acceleration-structure-phase submissions, which - // complete before the next pool resize can run (per-frame queue drain), - // so releasing it here is safe. - VkDeviceSize newCapacity = std::max(64 * 1024, bytes); - while (newCapacity < this->normalPoolCapacity + bytes) { - newCapacity *= 2; - } - VkBuffer newBuffer = VK_NULL_HANDLE; - VkDeviceMemory newMemory = VK_NULL_HANDLE; - void * newMapped = nullptr; - if (!this->createHostVisibleBuffer( - newCapacity, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, - newBuffer, newMemory)) { - return false; - } - if (vkMapMemory(this->device, newMemory, 0, newCapacity, 0, - &newMapped) != VK_SUCCESS) { - vkDestroyBuffer(this->device, newBuffer, this->allocator); - vkFreeMemory(this->device, newMemory, this->allocator); - return false; - } - if (this->normalPoolBuffer != VK_NULL_HANDLE) { - vkDestroyBuffer(this->device, this->normalPoolBuffer, this->allocator); - this->normalPoolBuffer = VK_NULL_HANDLE; - vkFreeMemory(this->device, this->normalPoolMemory, this->allocator); - this->normalPoolMemory = VK_NULL_HANDLE; - this->normalPoolMapped = nullptr; - } - this->normalPoolCapacity = newCapacity; - this->normalPoolBuffer = newBuffer; - this->normalPoolMemory = newMemory; - this->normalPoolMapped = newMapped; - this->normalPoolUsed = 0; - // The pool identity changed: refresh the descriptor sets. - return this->updateDescriptors(); + return this->ensurePoolCapacity(bytes, this->normalPoolBuffer, + this->normalPoolMemory, + this->normalPoolMapped, + this->normalPoolCapacity, + this->normalPoolUsed, true); } VkDeviceSize @@ -82,8 +46,15 @@ SoRTXRenderBackend::appendTriangleNormals(const SoRenderCommand & command, indexed ? entry.indexCount / 3 : entry.vertexCount / 3; if (triangleCount == 0) return 0; + // Six object-space vec4 per triangle: [n0, n1, n2, p0, p1, p2]. The three + // vertex normals let the hit shaders barycentrically interpolate a smooth + // (Phong) normal; the three vertex positions let the SBT closest-hit shader + // recover the barycentric coordinates when the toolchain exposes no + // barycentric built-in. The pool previously stored one flat facet normal + // per triangle, which shaded every smooth surface (sphere, cylinder) as + // hard facets / stripes. const VkDeviceSize bytes = - static_cast(triangleCount) * 4 * sizeof(float); + static_cast(triangleCount) * 6 * 4 * sizeof(float); // Reuse the entry's existing pool slot when the triangle count is // unchanged; otherwise append (the pool grows over the session). @@ -102,36 +73,139 @@ SoRTXRenderBackend::appendTriangleNormals(const SoRenderCommand & command, this->normalPoolUsed += bytes; } entry.normalCount = triangleCount; + if (getenv("FC_VULKAN_RT_DEBUG")) { + fprintf(stderr, + "[RTDBG] appendTriangleNormals assigned off=%u reuse=%d used=%llu " + "entry=%p\n", + entry.normalPoolOffset, reuse ? 1 : 0, + static_cast(this->normalPoolUsed), + static_cast(&entry)); + } + + // Object-space smooth normals, three vec4 per triangle (one per corner) so + // the hit shaders barycentric-interpolate a Phong normal. + // + // The corner normals come straight from the scene's normal stream + // (SoNormal -> SoGeometryDesc), the same per-vertex normals the raster + // viewport shades with. The scene side already accumulated each + // triangle's area-weighted contribution into its own per-face vertex + // slots and welded only the positionally-coincident duplicates whose + // normals agree within Blender's 30-degree Smooth-by-Angle, so hard + // creases stay flat and UV seams stay smooth. Recomputing them from + // this command's position-merged vertex list would instead blend the + // normals of every face sharing a merged rim vertex (e.g. a cap fan + // fused to the cylinder side wall), shading each fan wedge with its own + // tilted rim normal. + // + // Geometry without a normal stream falls back to the flat facet normal + // for the missing corners. + const bool hasNormals = geometry.normals != nullptr; + const uint32_t normalCount = geometry.normalCount; + if (getenv("FC_VULKAN_RT_DEBUG")) { + fprintf(stderr, + "[RTDBG] appendTriangleNormals tris=%u verts=%u hasNormals=%d " + "normalCount=%u entry=%p off=%u\n", + triangleCount, entry.vertexCount, + hasNormals ? 1 : 0, normalCount, static_cast(&entry), + entry.normalPoolOffset); + } + const auto vertexPos = [&geometry, posStrideFloats](uint32_t i) { + return geometry.positions + static_cast(i) * posStrideFloats; + }; + const auto vertexNorm = + [geometry, posStrideFloats, hasNormals, normalCount](uint32_t i, float * out) { + if (hasNormals && i < normalCount) { + const float * n = + geometry.normals + static_cast(i) * posStrideFloats; + out[0] = n[0]; out[1] = n[1]; out[2] = n[2]; + return true; + } + return false; + }; + const auto triIndex = [&geometry, indexed](uint32_t t, uint32_t corner) { + return indexed + ? geometry.indices[static_cast(t) * 3 + corner] + : t * 3 + corner; + }; - // Object-space per-triangle geometric normals (flat shading). float * out = static_cast(this->normalPoolMapped) + static_cast(entry.normalPoolOffset) * 4; - const auto vertex = [&geometry, posStrideFloats](uint32_t i) { - return geometry.positions + static_cast(i) * posStrideFloats; - }; for (uint32_t t = 0; t < triangleCount; ++t) { - const uint32_t i0 = indexed ? geometry.indices[static_cast(t) * 3 + 0] : t * 3 + 0; - const uint32_t i1 = indexed ? geometry.indices[static_cast(t) * 3 + 1] : t * 3 + 1; - const uint32_t i2 = indexed ? geometry.indices[static_cast(t) * 3 + 2] : t * 3 + 2; - const float * p0 = vertex(i0); - const float * p1 = vertex(i1); - const float * p2 = vertex(i2); - const float e1[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; - const float e2[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; - float nx = e1[1] * e2[2] - e1[2] * e2[1]; - float ny = e1[2] * e2[0] - e1[0] * e2[2]; - float nz = e1[0] * e2[1] - e1[1] * e2[0]; - const float len = std::sqrt(nx * nx + ny * ny + nz * nz); - if (len > 1e-12f) { - nx /= len; ny /= len; nz /= len; + const uint32_t i0 = triIndex(t, 0); + const uint32_t i1 = triIndex(t, 1); + const uint32_t i2 = triIndex(t, 2); + const float * p0 = vertexPos(i0); + const float * p1 = vertexPos(i1); + const float * p2 = vertexPos(i2); + + float n0[3] = {0.0f, 0.0f, 1.0f}; + float n1[3] = {0.0f, 0.0f, 1.0f}; + float n2[3] = {0.0f, 0.0f, 1.0f}; + const bool ok0 = vertexNorm(i0, n0); + const bool ok1 = vertexNorm(i1, n1); + const bool ok2 = vertexNorm(i2, n2); + if (getenv("FC_VULKAN_RT_DEBUG") && + (t < 3 || t >= triangleCount - 2 || + (triangleCount > 1000 && t % 360 == 0))) { + fprintf(stderr, + "[RTDBG] tri[%u] n0=(%.4f,%.4f,%.4f) n1=(%.4f,%.4f,%.4f) " + "n2=(%.4f,%.4f,%.4f) p0=(%.3f,%.3f,%.3f) p1=(%.3f,%.3f,%.3f) " + "p2=(%.3f,%.3f,%.3f)\n", + t, n0[0], n0[1], n0[2], n1[0], n1[1], n1[2], n2[0], n2[1], + n2[2], p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], + p2[2]); + } + if (getenv("FC_VULKAN_RT_DEBUG_NORMALLIST")) { + // Facet normal from the position corners for a per-triangle deviation + // check, then print every triangle whose corners disagree with it. + const float e1[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + const float e2[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + const float fx = e1[1] * e2[2] - e1[2] * e2[1]; + const float fy = e1[2] * e2[0] - e1[0] * e2[2]; + const float fz = e1[0] * e2[1] - e1[1] * e2[0]; + const float fl = std::sqrt(fx * fx + fy * fy + fz * fz); + if (fl > 1e-12f) { + const float d0 = (n0[0] * fx + n0[1] * fy + n0[2] * fz) / fl; + const float d1 = (n1[0] * fx + n1[1] * fy + n1[2] * fz) / fl; + const float d2 = (n2[0] * fx + n2[1] * fy + n2[2] * fz) / fl; + if (d0 < 0.9999f || d1 < 0.9999f || d2 < 0.9999f) { + fprintf(stderr, + "[RTDBG-N] tri[%u] facet=(%.4f,%.4f,%.4f) d=(%.4f,%.4f,%.4f) " + "n0=(%.4f,%.4f,%.4f) n1=(%.4f,%.4f,%.4f) n2=(%.4f,%.4f,%.4f) " + "p0=(%.3f,%.3f,%.3f)\n", + t, fx / fl, fy / fl, fz / fl, d0, d1, d2, n0[0], n0[1], + n0[2], n1[0], n1[1], n1[2], n2[0], n2[1], n2[2], p0[0], + p0[1], p0[2]); + } + } } - else { - nx = 0.0f; ny = 0.0f; nz = 1.0f; + if (!ok0 || !ok1 || !ok2) { + // Flat facet normal (normalized) for the corners without a scene + // normal; degenerate to +Z for zero-area triangles. + const float e1[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + const float e2[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + const float fx = e1[1] * e2[2] - e1[2] * e2[1]; + const float fy = e1[2] * e2[0] - e1[0] * e2[2]; + const float fz = e1[0] * e2[1] - e1[1] * e2[0]; + const float fl = std::sqrt(fx * fx + fy * fy + fz * fz); + if (fl > 1e-12f) { + const float inv = 1.0f / fl; + const float flat[3] = {fx * inv, fy * inv, fz * inv}; + if (!ok0) { n0[0] = flat[0]; n0[1] = flat[1]; n0[2] = flat[2]; } + if (!ok1) { n1[0] = flat[0]; n1[1] = flat[1]; n1[2] = flat[2]; } + if (!ok2) { n2[0] = flat[0]; n2[1] = flat[1]; n2[2] = flat[2]; } + } } - out[static_cast(t) * 4 + 0] = nx; - out[static_cast(t) * 4 + 1] = ny; - out[static_cast(t) * 4 + 2] = nz; - out[static_cast(t) * 4 + 3] = 0.0f; + + float * o = out + static_cast(t) * 24; + // Normals (object space) first so the ray-query tracer's layout matches. + o[0] = n0[0]; o[1] = n0[1]; o[2] = n0[2]; o[3] = 0.0f; + o[4] = n1[0]; o[5] = n1[1]; o[6] = n1[2]; o[7] = 0.0f; + o[8] = n2[0]; o[9] = n2[1]; o[10] = n2[2]; o[11] = 0.0f; + // Triangle vertices (object space) for the SBT chit's barycentric solve. + o[12] = p0[0]; o[13] = p0[1]; o[14] = p0[2]; o[15] = 0.0f; + o[16] = p1[0]; o[17] = p1[1]; o[18] = p1[2]; o[19] = 0.0f; + o[20] = p2[0]; o[21] = p2[1]; o[22] = p2[2]; o[23] = 0.0f; } return bytes; } @@ -139,14 +213,34 @@ SoRTXRenderBackend::appendTriangleNormals(const SoRenderCommand & command, bool SoRTXRenderBackend::ensureNeePoolCapacity(VkDeviceSize bytes) { - if (this->neePoolBuffer != VK_NULL_HANDLE && - this->neePoolCapacity >= bytes) { + // The caller (buildNeePool) refreshes the descriptor sets right after the + // per-frame pool build, so no descriptor refresh here. + return this->ensurePoolCapacity(bytes, this->neePoolBuffer, + this->neePoolMemory, this->neePoolMapped, + this->neePoolCapacity, this->neePoolUsed, + false); +} + +// Shared grow-only pool logic. Double the host-visible pool until the +// requested size fits. The new buffer is created (and mapped) before the +// old one is released, so a failed allocation leaves the previous pool +// intact and usable. The old buffer is only referenced by acceleration- +// structure-phase submissions, which complete before the next pool resize +// can run (per-frame queue drain), so releasing it here is safe. +bool +SoRTXRenderBackend::ensurePoolCapacity(VkDeviceSize bytes, + VkBuffer & poolBuffer, + VkDeviceMemory & poolMemory, + void *& poolMapped, + VkDeviceSize & poolCapacity, + VkDeviceSize & poolUsed, + bool refreshDescriptors) +{ + if (poolBuffer != VK_NULL_HANDLE && poolCapacity >= bytes) { return true; } - // Grow-only pool (see ensureNormalPoolCapacity for the lifetime - // argument; the pool is only read by per-frame drained submissions). VkDeviceSize newCapacity = std::max(64 * 1024, bytes); - while (newCapacity < this->neePoolCapacity + bytes) { + while (newCapacity < poolCapacity + bytes) { newCapacity *= 2; } VkBuffer newBuffer = VK_NULL_HANDLE; @@ -163,18 +257,30 @@ SoRTXRenderBackend::ensureNeePoolCapacity(VkDeviceSize bytes) vkFreeMemory(this->device, newMemory, this->allocator); return false; } - if (this->neePoolBuffer != VK_NULL_HANDLE) { - vkDestroyBuffer(this->device, this->neePoolBuffer, this->allocator); - this->neePoolBuffer = VK_NULL_HANDLE; - vkFreeMemory(this->device, this->neePoolMemory, this->allocator); - this->neePoolMemory = VK_NULL_HANDLE; - this->neePoolMapped = nullptr; - } - this->neePoolCapacity = newCapacity; - this->neePoolBuffer = newBuffer; - this->neePoolMemory = newMemory; - this->neePoolMapped = newMapped; - this->neePoolUsed = 0; + if (poolBuffer != VK_NULL_HANDLE) { + // Preserve the existing contents and the used count when growing: every + // entry's pool offset is relative to the pool base, so dropping the old + // data would leave all previously appended entries pointing at + // clobbered memory (they would all read the last-written object's + // normals -- the source of the per-wedge cap artifacts). + std::memcpy(newMapped, poolMapped, poolUsed); + vkDestroyBuffer(this->device, poolBuffer, this->allocator); + poolBuffer = VK_NULL_HANDLE; + vkFreeMemory(this->device, poolMemory, this->allocator); + poolMemory = VK_NULL_HANDLE; + poolMapped = nullptr; + } + else { + poolUsed = 0; + } + poolCapacity = newCapacity; + poolBuffer = newBuffer; + poolMemory = newMemory; + poolMapped = newMapped; + if (refreshDescriptors) { + // The pool identity changed: refresh the descriptor sets. + return this->updateDescriptors(); + } return true; } @@ -1070,32 +1176,53 @@ bool SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, const SoRenderCommand & command, VkCommandBuffer cmd) +{ + return this->blasBuildOrRefit(entry, command, cmd, false); +} + +bool +SoRTXRenderBackend::refitBlas(RTXCachedGeometry & entry, + const SoRenderCommand & command, + VkCommandBuffer cmd) +{ + return this->blasBuildOrRefit(entry, command, cmd, true); +} + +// Shared BLAS build/refit path. Both modes upload the position-only vertex +// data through a staging buffer and record one vkCmdBuildAccelerationStructuresKHR; +// they differ only in the build mode (BUILD vs in-place UPDATE), the creation +// of the device-local buffers/AS (build-only; refits reuse them, and the +// unchanged index buffer is not re-uploaded), and the packing decision +// (builds choose the vertex format, refits must match it). +bool +SoRTXRenderBackend::blasBuildOrRefit(RTXCachedGeometry & entry, + const SoRenderCommand & command, + VkCommandBuffer cmd, + bool refit) { const SoGeometryDesc & geometry = command.geometry; const bool indexed = entry.indexCount > 0 && entry.idxKey != nullptr; const uint32_t posStrideFloats = entry.vertexStride / sizeof(float); + const char * tag = refit ? "refitBlas" : "buildBlas"; if (getenv("FC_VULKAN_RT_DEBUG")) { static uint32_t blasSeq = 0; fprintf(stderr, - "[RTDBG] buildBlas #%u verts=%u idx=%u stride=%u indexed=%d " + "[RTDBG] %s #%u verts=%u idx=%u stride=%u indexed=%d " "pos=%p idxPtr=%p\n", - blasSeq++, entry.vertexCount, entry.indexCount, entry.vertexStride, - indexed ? 1 : 0, static_cast(geometry.positions), + tag, blasSeq++, entry.vertexCount, entry.indexCount, + entry.vertexStride, indexed ? 1 : 0, + static_cast(geometry.positions), static_cast(geometry.indices)); } // The path tracing compute shader shades flat faces from the object-space // triangle-normal pool; append this command's normals (the material - // records pick up the offset afterwards in updateMaterials()). + // records pick up the offset afterwards in updateMaterials()). Refits + // append a fresh record too: moved vertices change the per-corner normals. this->appendTriangleNormals(command, entry); - // Position-only vertex buffer for the BLAS. Optionally packed to 16-bit - // half floats (FC_VULKAN_AS_PACK) when the object positions fit the half - // range: halves AS memory and traversal cost on static geometry. The 32-bit - // path is the default and is used whenever the gate is off or coords would - // overflow half precision. - const bool packEnabled = getenv("FC_VULKAN_AS_PACK") != nullptr; + // Gather the position-only vertices and the object-space bounds. std::vector positions(static_cast(entry.vertexCount) * 3); float pMin[3] = {1e30f, 1e30f, 1e30f}; float pMax[3] = {-1e30f, -1e30f, -1e30f}; @@ -1116,16 +1243,29 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, entry.objectMin[a] = pMin[a]; entry.objectMax[a] = pMax[a]; } - bool fitHalf = true; - for (int a = 0; a < 3; ++a) { - if (std::fabs(pMin[a]) > 60000.0f || std::fabs(pMax[a]) > 60000.0f) { - fitHalf = false; + + // Builds enable half packing when the FC_VULKAN_AS_PACK gate is on and the + // object positions fit the half range (halves AS memory and traversal cost + // on static geometry). Refits must upload the SAME format the BLAS was + // originally built with so the in-place MODE_UPDATE matches the build. + bool useHalf = false; + if (refit) { + useHalf = entry.blasVertexFormat == VK_FORMAT_R16G16B16_SFLOAT; + } + else { + const bool packEnabled = getenv("FC_VULKAN_AS_PACK") != nullptr; + bool fitHalf = true; + for (int a = 0; a < 3; ++a) { + if (std::fabs(pMin[a]) > 60000.0f || + std::fabs(pMax[a]) > 60000.0f) { + fitHalf = false; + } } + useHalf = packEnabled && fitHalf; + entry.blasVertexFormat = + useHalf ? VK_FORMAT_R16G16B16_SFLOAT : VK_FORMAT_R32G32B32_SFLOAT; + entry.blasVertexStride = useHalf ? 6 : 12; } - const bool useHalf = packEnabled && fitHalf; - entry.blasVertexFormat = - useHalf ? VK_FORMAT_R16G16B16_SFLOAT : VK_FORMAT_R32G32B32_SFLOAT; - entry.blasVertexStride = useHalf ? 6 : 12; std::vector packedHalf; const void * vertexSrc = nullptr; if (useHalf) { @@ -1139,19 +1279,37 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, vertexSrc = positions.data(); } if (getenv("FC_VULKAN_RT_DEBUG")) { - fprintf(stderr, "[RTDBG] blasFmt build=1 packed=%d stride=%u fmt=0x%x\n", - useHalf ? 1 : 0, entry.blasVertexStride, + fprintf(stderr, "[RTDBG] blasFmt %s packed=%d stride=%u fmt=0x%x\n", + tag, useHalf ? 1 : 0, entry.blasVertexStride, static_cast(entry.blasVertexFormat)); } const VkDeviceSize vertexBytes = static_cast(entry.vertexCount) * entry.blasVertexStride; - if (!this->createDeviceLocalBuffer( - vertexBytes, - VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | - VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | - VK_BUFFER_USAGE_TRANSFER_DST_BIT, - entry.vertexBuffer, entry.vertexMemory)) { - return false; + const VkDeviceSize indexBytes = + indexed ? static_cast(entry.indexCount) * sizeof(uint32_t) + : 0; + + // Builds create the device-local vertex/index buffers fresh; refits reuse + // the existing buffers (the index buffer and topology are unchanged, the + // refit precondition checked in updateGeometryCache()). + if (!refit) { + if (!this->createDeviceLocalBuffer( + vertexBytes, + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + entry.vertexBuffer, entry.vertexMemory)) { + return false; + } + if (indexed && + !this->createDeviceLocalBuffer( + indexBytes, + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + entry.indexBuffer, entry.indexMemory)) { + return false; + } } VkBuffer staging = VK_NULL_HANDLE; @@ -1165,7 +1323,8 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, if (vkMapMemory(this->device, stagingMemory, 0, vertexBytes, 0, &mapped) != VK_SUCCESS || mapped == nullptr) { - this->emitError("buildBlas: vkMapMemory (vertex staging) failed"); + this->emitError( + (std::string(tag) + ": vkMapMemory (vertex staging) failed").c_str()); vkDestroyBuffer(this->device, staging, this->allocator); vkFreeMemory(this->device, stagingMemory, this->allocator); return false; @@ -1175,20 +1334,7 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, VkBuffer indexStaging = VK_NULL_HANDLE; VkDeviceMemory indexStagingMemory = VK_NULL_HANDLE; - VkDeviceSize indexBytes = 0; - if (indexed) { - indexBytes = - static_cast(entry.indexCount) * sizeof(uint32_t); - if (!this->createDeviceLocalBuffer( - indexBytes, - VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | - VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | - VK_BUFFER_USAGE_TRANSFER_DST_BIT, - entry.indexBuffer, entry.indexMemory)) { - vkDestroyBuffer(this->device, staging, this->allocator); - vkFreeMemory(this->device, stagingMemory, this->allocator); - return false; - } + if (indexed && !refit) { if (!this->createHostVisibleBuffer(indexBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, indexStaging, indexStagingMemory)) { @@ -1200,7 +1346,8 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, if (vkMapMemory(this->device, indexStagingMemory, 0, indexBytes, 0, &imapped) != VK_SUCCESS || imapped == nullptr) { - this->emitError("buildBlas: vkMapMemory (index staging) failed"); + this->emitError( + (std::string(tag) + ": vkMapMemory (index staging) failed").c_str()); vkDestroyBuffer(this->device, staging, this->allocator); vkFreeMemory(this->device, stagingMemory, this->allocator); vkDestroyBuffer(this->device, indexStaging, this->allocator); @@ -1214,7 +1361,7 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, VkBufferCopy vertexCopy {}; vertexCopy.size = vertexBytes; vkCmdCopyBuffer(cmd, staging, entry.vertexBuffer, 1, &vertexCopy); - if (indexed) { + if (indexed && !refit) { VkBufferCopy indexCopy {}; indexCopy.size = indexBytes; vkCmdCopyBuffer(cmd, indexStaging, entry.indexBuffer, 1, &indexCopy); @@ -1236,7 +1383,7 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, this->pendingStagingDestroys.emplace_back(indexStaging, indexStagingMemory); } - // --- Build the BLAS ---------------------------------------------------- + // --- Record the (re)build ---------------------------------------------- VkAccelerationStructureGeometryTrianglesDataKHR triangles {}; triangles.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR; @@ -1264,220 +1411,43 @@ SoRTXRenderBackend::buildBlas(RTXCachedGeometry & entry, buildInfo.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR; buildInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; - // ALLOW_UPDATE: lets position-only edits refit this BLAS in place (see - // refitBlas()) instead of destroying and rebuilding it. ALLOW_COMPACTION - // (FC_VULKAN_AS_COMPACT) lets a later pass shrink the AS residency copy. - // They are mutually exclusive: NVIDIA's compaction docs note that - // ALLOW_UPDATE "must leave room for updated triangles" and that - // PREFER_FAST_TRACE "uses its own compaction method and results can differ - // from ALLOW_COMPACTION". Building a BLAS with ALLOW_UPDATE + PREFER_FAST_TRACE - // and then COMPACT-copying it into the queried compacted-size buffer produced - // a malformed AS whose first use drove the driver to VK_ERROR_DEVICE_LOST. - // So when compaction is requested the BLAS is built with ALLOW_COMPACTION - // ALONE (the NVIDIA "max compaction" recipe); a compacted BLAS loses its - // ALLOW_UPDATE refit capability, and recordAccelerationStructures already - // rebuilds (instead of refits) any compacted entry that needs a position fix. - const bool compactGate = getenv("FC_VULKAN_AS_COMPACT") != nullptr; - if (compactGate) { - buildInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_COMPACTION_BIT_KHR; - entry.wantsCompact = true; - entry.compacted = false; - } - else { - buildInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR | - VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR; - } - buildInfo.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR; buildInfo.geometryCount = 1; buildInfo.pGeometries = &asGeometry; - - VkAccelerationStructureBuildSizesInfoKHR sizeInfo {}; - sizeInfo.sType = - VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR; - vkGetAccelerationStructureBuildSizesKHR( - this->device, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, - &buildInfo, &maxPrimitives, &sizeInfo); - if (!this->createScratchBuffer(sizeInfo.buildScratchSize)) { - return false; - } - - if (!this->createDeviceLocalBuffer( - sizeInfo.accelerationStructureSize, - VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | - VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - entry.blasBuffer, entry.blasMemory)) { - return false; - } - entry.blasSize = sizeInfo.accelerationStructureSize; - VkAccelerationStructureCreateInfoKHR asCI {}; - asCI.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR; - asCI.buffer = entry.blasBuffer; - asCI.size = sizeInfo.accelerationStructureSize; - asCI.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; - if (vkCreateAccelerationStructureKHR(this->device, &asCI, this->allocator, - &entry.blas) != VK_SUCCESS) { - return false; - } - // Capture the BLAS device address now. It is constant for the lifetime of - // the BLAS, so the per-frame instance collection in buildTlas() reuses it - // instead of calling vkGetAccelerationStructureDeviceAddressKHR every frame. - entry.devAddr = 0; - VkAccelerationStructureDeviceAddressInfoKHR devAddrInfo {}; - devAddrInfo.sType = - VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR; - devAddrInfo.accelerationStructure = entry.blas; - entry.devAddr = vkGetAccelerationStructureDeviceAddressKHR(this->device, - &devAddrInfo); - - buildInfo.dstAccelerationStructure = entry.blas; - buildInfo.scratchData.deviceAddress = this->scratchAddress; - VkAccelerationStructureBuildRangeInfoKHR rangeInfo {}; - rangeInfo.primitiveCount = maxPrimitives; - rangeInfo.primitiveOffset = 0; - rangeInfo.firstVertex = 0; - rangeInfo.transformOffset = 0; - const VkAccelerationStructureBuildRangeInfoKHR * rangeInfos[] = {&rangeInfo}; - vkCmdBuildAccelerationStructuresKHR(cmd, 1, &buildInfo, rangeInfos); - - VkMemoryBarrier blasBarrier {}; - blasBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; - blasBarrier.srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; - blasBarrier.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR; - vkCmdPipelineBarrier(cmd, - VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, - VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, - 0, 1, &blasBarrier, 0, nullptr, 0, nullptr); - return true; -} - -bool -SoRTXRenderBackend::refitBlas(RTXCachedGeometry & entry, - const SoRenderCommand & command, - VkCommandBuffer cmd) -{ - const SoGeometryDesc & geometry = command.geometry; - const uint32_t posStrideFloats = entry.vertexStride / sizeof(float); - - if (getenv("FC_VULKAN_RT_DEBUG")) { - fprintf(stderr, - "[RTDBG] refitBlas verts=%u idx=%u stride=%u pos=%p\n", - entry.vertexCount, entry.indexCount, entry.vertexStride, - static_cast(geometry.positions)); - } - - // Upload the new vertex positions into the EXISTING device buffers; the - // index buffer and topology are unchanged (the refit precondition checked - // in updateGeometryCache()). The byte size and packing must match the - // format the BLAS was originally built with (entry.blasVertexFormat). - const VkDeviceSize vertexBytes = - static_cast(entry.vertexCount) * entry.blasVertexStride; - - // Moved vertices change the object-space flat normals: append a fresh - // normal-pool record and let updateMaterials() pick up the new offset - // (the pool is grow-only, matching the rebuild path). - this->appendTriangleNormals(command, entry); - std::vector positions(static_cast(entry.vertexCount) * 3); - float pMin[3] = {1e30f, 1e30f, 1e30f}; - float pMax[3] = {-1e30f, -1e30f, -1e30f}; - for (uint32_t i = 0; i < entry.vertexCount; ++i) { - const float * p = - geometry.positions + static_cast(i) * posStrideFloats; - const float px = p[0], py = p[1], pz = p[2]; - positions[static_cast(i) * 3 + 0] = px; - positions[static_cast(i) * 3 + 1] = py; - positions[static_cast(i) * 3 + 2] = pz; - if (px < pMin[0]) pMin[0] = px; - if (py < pMin[1]) pMin[1] = py; - if (pz < pMin[2]) pMin[2] = pz; - if (px > pMax[0]) pMax[0] = px; - if (py > pMax[1]) pMax[1] = py; - if (pz > pMax[2]) pMax[2] = pz; - } - for (int a = 0; a < 3; ++a) { - entry.objectMin[a] = pMin[a]; - entry.objectMax[a] = pMax[a]; - } - std::vector packedHalf; - const void * vertexSrc = nullptr; - if (entry.blasVertexFormat == VK_FORMAT_R16G16B16_SFLOAT) { - packedHalf.resize(static_cast(entry.vertexCount) * 3); - for (size_t i = 0; i < positions.size(); ++i) { - packedHalf[i] = floatToHalf(positions[i]); - } - vertexSrc = packedHalf.data(); + if (refit) { + buildInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR | + VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR; + buildInfo.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR; + buildInfo.srcAccelerationStructure = entry.blas; + buildInfo.dstAccelerationStructure = entry.blas; } else { - vertexSrc = positions.data(); - } - - VkBuffer staging = VK_NULL_HANDLE; - VkDeviceMemory stagingMemory = VK_NULL_HANDLE; - if (!this->createHostVisibleBuffer(vertexBytes, - VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - staging, stagingMemory)) { - return false; - } - void * mapped = nullptr; - if (vkMapMemory(this->device, stagingMemory, 0, vertexBytes, 0, &mapped) != - VK_SUCCESS || - mapped == nullptr) { - this->emitError("refitBlas: vkMapMemory (vertex staging) failed"); - vkDestroyBuffer(this->device, staging, this->allocator); - vkFreeMemory(this->device, stagingMemory, this->allocator); - return false; + // ALLOW_UPDATE: lets position-only edits refit this BLAS in place (see + // refitBlas()) instead of destroying and rebuilding it. ALLOW_COMPACTION + // (FC_VULKAN_AS_COMPACT) lets a later pass shrink the AS residency copy. + // They are mutually exclusive: NVIDIA's compaction docs note that + // ALLOW_UPDATE "must leave room for updated triangles" and that + // PREFER_FAST_TRACE "uses its own compaction method and results can differ + // from ALLOW_COMPACTION". Building a BLAS with ALLOW_UPDATE + PREFER_FAST_TRACE + // and then COMPACT-copying it into the queried compacted-size buffer produced + // a malformed AS whose first use drove the driver to VK_ERROR_DEVICE_LOST. + // So when compaction is requested the BLAS is built with ALLOW_COMPACTION + // ALONE (the NVIDIA "max compaction" recipe); a compacted BLAS loses its + // ALLOW_UPDATE refit capability, and recordAccelerationStructures already + // rebuilds (instead of refits) any compacted entry that needs a position fix. + const bool compactGate = getenv("FC_VULKAN_AS_COMPACT") != nullptr; + if (compactGate) { + buildInfo.flags = + VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_COMPACTION_BIT_KHR; + entry.wantsCompact = true; + entry.compacted = false; + } + else { + buildInfo.flags = + VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR | + VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR; + } + buildInfo.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR; } - std::memcpy(mapped, vertexSrc, static_cast(vertexBytes)); - vkUnmapMemory(this->device, stagingMemory); - - VkBufferCopy vertexCopy {}; - vertexCopy.size = vertexBytes; - vkCmdCopyBuffer(cmd, staging, entry.vertexBuffer, 1, &vertexCopy); - VkMemoryBarrier copyBarrier {}; - copyBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; - copyBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - copyBarrier.dstAccessMask = - VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, - 0, 1, ©Barrier, 0, nullptr, 0, nullptr); - this->pendingStagingDestroys.emplace_back(staging, stagingMemory); - - // --- In-place UPDATE build --------------------------------------------- - VkAccelerationStructureGeometryTrianglesDataKHR triangles {}; - triangles.sType = - VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR; - triangles.vertexFormat = entry.blasVertexFormat; - triangles.vertexData.deviceAddress = - this->getDeviceAddress(entry.vertexBuffer); - triangles.vertexStride = entry.blasVertexStride; - triangles.maxVertex = entry.vertexCount - 1; - const bool indexed = entry.indexCount > 0; - triangles.indexType = - indexed ? VK_INDEX_TYPE_UINT32 : VK_INDEX_TYPE_NONE_KHR; - triangles.indexData.deviceAddress = - indexed ? this->getDeviceAddress(entry.indexBuffer) : 0; - - VkAccelerationStructureGeometryKHR asGeometry {}; - asGeometry.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR; - asGeometry.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR; - asGeometry.geometry.triangles = triangles; - asGeometry.flags = VK_GEOMETRY_OPAQUE_BIT_KHR; - - const uint32_t maxPrimitives = - indexed ? entry.indexCount / 3 : entry.vertexCount / 3; - if (maxPrimitives == 0) return false; - - VkAccelerationStructureBuildGeometryInfoKHR buildInfo {}; - buildInfo.sType = - VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR; - buildInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; - buildInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR | - VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR; - buildInfo.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR; - buildInfo.srcAccelerationStructure = entry.blas; - buildInfo.dstAccelerationStructure = entry.blas; - buildInfo.geometryCount = 1; - buildInfo.pGeometries = &asGeometry; VkAccelerationStructureBuildSizesInfoKHR sizeInfo {}; sizeInfo.sType = @@ -1490,6 +1460,37 @@ SoRTXRenderBackend::refitBlas(RTXCachedGeometry & entry, if (!this->createScratchBuffer(sizeInfo.buildScratchSize)) { return false; } + + if (!refit) { + if (!this->createDeviceLocalBuffer( + sizeInfo.accelerationStructureSize, + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + entry.blasBuffer, entry.blasMemory)) { + return false; + } + entry.blasSize = sizeInfo.accelerationStructureSize; + VkAccelerationStructureCreateInfoKHR asCI {}; + asCI.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR; + asCI.buffer = entry.blasBuffer; + asCI.size = sizeInfo.accelerationStructureSize; + asCI.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; + if (vkCreateAccelerationStructureKHR(this->device, &asCI, this->allocator, + &entry.blas) != VK_SUCCESS) { + return false; + } + // Capture the BLAS device address now. It is constant for the lifetime of + // the BLAS, so the per-frame instance collection in buildTlas() reuses it + // instead of calling vkGetAccelerationStructureDeviceAddressKHR every frame. + entry.devAddr = 0; + VkAccelerationStructureDeviceAddressInfoKHR devAddrInfo {}; + devAddrInfo.sType = + VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR; + devAddrInfo.accelerationStructure = entry.blas; + entry.devAddr = vkGetAccelerationStructureDeviceAddressKHR(this->device, + &devAddrInfo); + buildInfo.dstAccelerationStructure = entry.blas; + } buildInfo.scratchData.deviceAddress = this->scratchAddress; VkAccelerationStructureBuildRangeInfoKHR rangeInfo {}; @@ -1509,7 +1510,9 @@ SoRTXRenderBackend::refitBlas(RTXCachedGeometry & entry, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, 0, 1, &blasBarrier, 0, nullptr, 0, nullptr); - entry.refitPending = false; + if (refit) { + entry.refitPending = false; + } return true; } @@ -1970,6 +1973,16 @@ SoRTXRenderBackend::updateMaterials(const SoDrawList & drawlist) out.triangleData[1] = static_cast(entry.normalCount); out.triangleData[2] = static_cast(entry.neePoolOffset); out.triangleData[3] = static_cast(entry.neeCount); + if (getenv("FC_VULKAN_RT_DEBUG_MATERIALS")) { + fprintf(stderr, + "[RTDBG-M] cmd[%d] cacheIdx=%d normOff=%u normCnt=%u " + "verts=%u tris=%u\n", + i, cacheFound->second, entry.normalPoolOffset, + entry.normalCount, entry.vertexCount, entry.indexCount / 3); + } + } + else if (getenv("FC_VULKAN_RT_DEBUG_MATERIALS")) { + fprintf(stderr, "[RTDBG-M] cmd[%d] NOT IN CACHE\n", i); } // Optional PBR (metallic-roughness) parameters. Off by default so @@ -2017,6 +2030,10 @@ SoRTXRenderBackend::updateMaterials(const SoDrawList & drawlist) } const int lightCount = std::min( static_cast(lightSource->size()), MAX_SHADER_LIGHTS); + if (getenv("FC_VULKAN_MAT_DEBUG")) { + fprintf(stderr, "[MATDBG] updateMaterials lightCount=%d scene=%zu drawn=%zu\n", + lightCount, this->sceneLights.size(), lighting->lights.size()); + } out.params[2] = static_cast(lightCount); for (int l = 0; l < lightCount; ++l) { const SoLightData & light = (*lightSource)[static_cast(l)]; diff --git a/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendPathTracing.cpp b/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendPathTracing.cpp index f3c1152d4ab..4d6ce34e3ef 100644 --- a/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendPathTracing.cpp +++ b/src/rendering/SoRTXRenderBackend/SoRTXRenderBackendPathTracing.cpp @@ -299,12 +299,15 @@ SoRTXRenderBackend::updatePathTracingState(const SoDrawList & /*drawlist*/, fprintf(stderr, "[RTDBG] ptState frame=%u viewChanged=%d sceneChanged=%d " "bgChanged=%d latch=%d accum=%d frameIndex=%u idle=%u " - "reproject=%d\n", + "reproject=%d camv=%u lastCamv=%u vp=%d,%d lastVp=%u,%u\n", params.frame, viewChanged ? 1 : 0, sceneChanged ? 1 : 0, backgroundChanged ? 1 : 0, this->ptStartLatch ? 1 : 0, this->ptAccumulating ? 1 : 0, this->ptFrameIndex, - this->ptIdleFrames, this->ptReprojectFrame ? 1 : 0); + this->ptIdleFrames, this->ptReprojectFrame ? 1 : 0, + params.cameraVersion, this->lastCameraVersion, + static_cast(vpSize[0]), static_cast(vpSize[1]), + this->lastViewportWidth, this->lastViewportHeight); } if (getenv("FC_VULKAN_PT_DEBUG")) { diff --git a/src/rendering/SoVulkanRenderBackend.h b/src/rendering/SoVulkanRenderBackend.h index ef3369adcde..da3ebaca77c 100644 --- a/src/rendering/SoVulkanRenderBackend.h +++ b/src/rendering/SoVulkanRenderBackend.h @@ -8,6 +8,7 @@ #include "rendering/SoVulkanShared.h" #include "rendering/SoVulkanRenderBackend/SoVulkanMemPool.h" #include "rendering/SoVulkanRenderBackend/SoVulkanRecordContext.h" +#include "rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.h" #include @@ -294,6 +295,7 @@ class SoVulkanRenderBackend : public SoRenderBackend { */ void setWireframeOverlay(SbBool enabled); void setPointsOverlay(SbBool enabled); + void setTessellationOverlay(SbBool enabled); void setEdgeColor(const SbColor4f & color); private: @@ -305,10 +307,6 @@ class SoVulkanRenderBackend : public SoRenderBackend { bool createLightingConstBuffer(); bool createLightingDescriptorSet(); bool createPipelineLayout(); - bool createRenderPass(const SoVulkanRenderTarget & target, - VkAttachmentLoadOp colorLoadOp, - VkAttachmentLoadOp depthLoadOp, - VkRenderPass & renderPass); bool createShaders(VkShaderModule & vertexModule, VkShaderModule & fragmentModule); bool createWideLineShaders(); @@ -317,8 +315,6 @@ class SoVulkanRenderBackend : public SoRenderBackend { bool createBackgroundPipeline(const SoVulkanRenderTarget & target, VkRenderPass renderPass, VkPipeline & pipeline); - bool ensureFramebuffer(const SoVulkanRenderTarget * target, - VkRenderPass renderPass); void recordBackground(const SoRenderParams & params, const SoVulkanRenderTarget & target, VkRenderPass renderPass, @@ -434,6 +430,7 @@ class SoVulkanRenderBackend : public SoRenderBackend { bool buildWorkItems(const SoDrawList & drawlist, const SoRenderParams & params, bool wireframeOverlay, bool pointsOverlay, + bool tessellationOverlay, const float * overlayColor, VkRenderPass renderPass, std::vector & out); @@ -564,6 +561,14 @@ class SoVulkanRenderBackend : public SoRenderBackend { void applyViewportState(const VkViewport & viewport, VulkanRecordContext & ctx); void applyScissorState(const VkRect2D & scissor, VulkanRecordContext & ctx); + // Bind the lighting (set 0) + texture/UBO (set 1) descriptor sets for a + // draw. Re-binds set 1 alone when only the per-draw UBO dynamic offset + // advanced (shared by recordDrawCommand and recordCommandBatch). + void bindDrawDescriptorSets(VulkanRecordContext & ctx, + VkDescriptorSet textureSet, + uint32_t lightingDynamicOffset, + uint32_t uboDynamicOffset, + uint32_t slotIndex); void resetBoundState(VulkanRecordContext & ctx); void recordOverlayDepthClear(const SoRenderCommand & command, const SoVulkanRenderTarget & target, @@ -835,6 +840,11 @@ class SoVulkanRenderBackend : public SoRenderBackend { // Configured through the manager; never part of the shared render params. SbBool wireframeOverlay = FALSE; SbBool pointsOverlay = FALSE; + // Debug overlay: re-draw the triangle commands in polygon-LINES mode so + // the raw tessellation (triangle edges) is visible on top of the shaded + // geometry. Distinct from the wireframe/edge overlay, which draws only + // the true B-Rep feature-edge line commands. + SbBool tessellationOverlay = FALSE; SbColor4f edgeColor = SbColor4f(0.05f, 0.05f, 0.05f, 1.0f); // Texture uploads gathered during updateGeometryCache(). On the own-queue @@ -881,83 +891,16 @@ class SoVulkanRenderBackend : public SoRenderBackend { VkPipelineLayout backgroundPipelineLayout = VK_NULL_HANDLE; // Render passes are cached by their VkRenderPassCreateInfo identity - // (color/depth format, sample count, image layouts). Pipelines are keyed - // on the render-pass handle (see PipelineKey), so reusing the same pass - // across targets that differ only in their images/extent keeps the - // pipeline cache warm -- in particular for swapchain targets whose images - // cycle every frame. - struct RenderPassIdentity { - VkFormat colorFormat = VK_FORMAT_B8G8R8A8_UNORM; - VkFormat depthFormat = VK_FORMAT_UNDEFINED; - VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; - VkImageLayout colorLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - VkImageLayout depthLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - // Load ops distinguish a render pass that clears its attachments at begin - // (full-target clear fast path, FC_VULKAN_RP_CLEAR) from one that loads - // them and clears via vkCmdClearAttachments. Two passes that differ only - // in loadOp must not share a cache entry. - VkAttachmentLoadOp colorLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - - bool operator==(const RenderPassIdentity & other) const - { - return colorFormat == other.colorFormat && - depthFormat == other.depthFormat && - sampleCount == other.sampleCount && - colorLayout == other.colorLayout && - depthLayout == other.depthLayout && - colorLoadOp == other.colorLoadOp && - depthLoadOp == other.depthLoadOp; - } - }; - struct RenderPassIdentityHash - { - size_t operator()(const RenderPassIdentity & key) const - { - size_t hash = std::hash()( - static_cast(key.colorFormat)); - hash = hashCombine(hash, - std::hash()(static_cast(key.depthFormat))); - hash = hashCombine(hash, - std::hash()(static_cast(key.sampleCount))); - hash = hashCombine(hash, - std::hash()(static_cast(key.colorLayout))); - hash = hashCombine(hash, - std::hash()(static_cast(key.depthLayout))); - hash = hashCombine(hash, - std::hash()(static_cast(key.colorLoadOp))); - hash = hashCombine(hash, - std::hash()(static_cast(key.depthLoadOp))); - return hash; - } - }; - RenderPassIdentity renderPassIdentity(const SoVulkanRenderTarget & target) const; - VkRenderPass getOrCreateRenderPass(const SoVulkanRenderTarget & target, - VkAttachmentLoadOp colorLoadOp, - VkAttachmentLoadOp depthLoadOp); - std::unordered_map - renderPassCache; - - // Render pass used by the current frame (looked up from renderPassCache). - VkRenderPass renderPass = VK_NULL_HANDLE; - // Whether the current frame's render pass clears the color/depth attachment - // via its loadOp (full-target-clear fast path). When true, recordClear() - // skips the redundant vkCmdClearAttachments and the begin info supplies the - // corresponding clear value. - bool renderPassColorCleared = false; - bool renderPassDepthCleared = false; - - // Framebuffer cached for the current target identity (image views + - // extent + render pass). Swapchain targets cycle their images every - // frame, so this is recreated on any target change while the render pass - // itself survives in renderPassCache. - VkFramebuffer renderPassFramebuffer = VK_NULL_HANDLE; - VkRenderPass renderPassFramebufferPass = VK_NULL_HANDLE; - VkImage renderPassFramebufferColorImage = VK_NULL_HANDLE; - VkImageView renderPassFramebufferColorView = VK_NULL_HANDLE; - VkImage renderPassFramebufferDepthImage = VK_NULL_HANDLE; - VkImageView renderPassFramebufferDepthView = VK_NULL_HANDLE; - VkExtent2D renderPassFramebufferExtent {0, 0}; + // (color/depth format, sample count, image layouts) plus the color/depth + // load ops. Pipelines are keyed on the render-pass handle (see + // PipelineKey), so reusing the same pass across targets that differ only in + // their images/extent keeps the pipeline cache warm -- in particular for + // swapchain targets whose images cycle every frame. The cache also keeps the + // per-target framebuffer, recreated on any target change while the render + // pass itself survives. All of that state lives in the owned + // SoVulkanRenderPassCache so the backend records a frame with the current + // pass/framebuffer without duplicating the cache bookkeeping. + SoVulkanRenderPassCache renderPasses; // Pipeline cache: keyed by the retained state that affects the created // pipeline. Vulkan pipelines are immutable, so every topology/fill/depth/ diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCommand.cpp b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCommand.cpp index 8cd1de3f933..5fa8bcdc1b6 100644 --- a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCommand.cpp +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCommand.cpp @@ -71,6 +71,41 @@ SoVulkanRenderBackend::applyScissorState(const VkRect2D & scissor, ctx.hasBoundScissor = true; } +void +SoVulkanRenderBackend::bindDrawDescriptorSets(VulkanRecordContext & ctx, + VkDescriptorSet textureSet, + uint32_t lightingDynamicOffset, + uint32_t uboDynamicOffset, + uint32_t slotIndex) +{ + const uint32_t bindingOffsets[2] = {lightingDynamicOffset, uboDynamicOffset}; + if (ctx.lastBoundLightingOffset != lightingDynamicOffset || + ctx.lastBoundTextureSet != textureSet) { + // First draw of a new lighting handle / texture set: bind both sets with + // their dynamic offsets in one call (also covers the frame's first draw). + const VkDescriptorSet both[2] = {this->lightingDescriptorSet, textureSet}; + vkBackendTrace(this->uboFrameIndex, "draw.bindDescSets2", + "slot=%u", slotIndex); + vkCmdBindDescriptorSets(ctx.buffer, + VK_PIPELINE_BIND_POINT_GRAPHICS, + this->pipelineLayout, 0, 2, both, 2, + bindingOffsets); + ctx.lastBoundLightingOffset = lightingDynamicOffset; + ctx.lastBoundTextureSet = textureSet; + } + else { + // Same lighting handle + texture set as the previous draw: only the + // per-draw UBO dynamic offset advances. Re-bind set 1 alone (set 0 stays + // bound from the last 2-set bind) instead of re-emitting both sets. + vkBackendTrace(this->uboFrameIndex, "draw.bindDescSets1", + "slot=%u", slotIndex); + vkCmdBindDescriptorSets(ctx.buffer, + VK_PIPELINE_BIND_POINT_GRAPHICS, + this->pipelineLayout, 1, 1, &textureSet, 1, + &bindingOffsets[1]); + } +} + void SoVulkanRenderBackend::applyViewport(const SoRenderParams & params, const SoVulkanRenderTarget & target, @@ -105,22 +140,12 @@ SoVulkanRenderBackend::applyViewport(const SoRenderParams & params, // Clamp the clear region to the target so an off-screen viewport (origin // outside the target, or a size exceeding the extent) never generates a // clear outside the render area. - const int32_t x0 = std::max(0, static_cast(origin[0])); - const int32_t y0 = std::max( - 0, static_cast(target.extent.height) - - static_cast(origin[1]) - - static_cast(size[1])); - const int32_t x1 = std::min(static_cast(target.extent.width), - static_cast(origin[0]) + - static_cast(size[0])); - const int32_t y1 = std::min( - static_cast(target.extent.height), - static_cast(target.extent.height) - - static_cast(origin[1])); + const VulkanViewportRect rect = + vulkanFlippedViewportRect(origin, size, target.extent); VkRect2D scissor {}; - scissor.offset = {x0, y0}; - scissor.extent = {static_cast(std::max(0, x1 - x0)), - static_cast(std::max(0, y1 - y0))}; + scissor.offset = {rect.x0, rect.y0}; + scissor.extent = {static_cast(std::max(0, rect.x1 - rect.x0)), + static_cast(std::max(0, rect.y1 - rect.y0))}; this->applyScissorState(scissor, ctx); } @@ -213,21 +238,11 @@ SoVulkanRenderBackend::isFullTargetClear(const SoRenderParams & params, // pass can clear via its loadOp instead. Empty viewports clear nothing. const SbVec2s & origin = params.viewport.getViewportOriginPixels(); const SbVec2s & size = params.viewport.getViewportSizePixels(); - const int32_t x0 = std::max(0, static_cast(origin[0])); - const int32_t y0 = std::max( - 0, static_cast(target.extent.height) - - static_cast(origin[1]) - - static_cast(size[1])); - const int32_t x1 = std::min(static_cast(target.extent.width), - static_cast(origin[0]) + - static_cast(size[0])); - const int32_t y1 = std::min( - static_cast(target.extent.height), - static_cast(target.extent.height) - - static_cast(origin[1])); - return x0 == 0 && y0 == 0 && - x1 == static_cast(target.extent.width) && - y1 == static_cast(target.extent.height); + const VulkanViewportRect rect = + vulkanFlippedViewportRect(origin, size, target.extent); + return rect.x0 == 0 && rect.y0 == 0 && + rect.x1 == static_cast(target.extent.width) && + rect.y1 == static_cast(target.extent.height); } void @@ -287,28 +302,18 @@ SoVulkanRenderBackend::recordClear(const SoRenderParams & params, // overwrite other viewports or the backing image outside the viewport. const SbVec2s & origin = params.viewport.getViewportOriginPixels(); const SbVec2s & size = params.viewport.getViewportSizePixels(); - const int32_t x0 = std::max(0, static_cast(origin[0])); - const int32_t y0 = std::max( - 0, static_cast(target.extent.height) - - static_cast(origin[1]) - - static_cast(size[1])); - const int32_t x1 = std::min(static_cast(target.extent.width), - static_cast(origin[0]) + - static_cast(size[0])); - const int32_t y1 = std::min( - static_cast(target.extent.height), - static_cast(target.extent.height) - - static_cast(origin[1])); - if (x1 <= x0 || y1 <= y0) return; - - VkClearRect rect {}; - rect.rect.offset = {x0, y0}; - rect.rect.extent = {static_cast(x1 - x0), - static_cast(y1 - y0)}; - rect.baseArrayLayer = 0; - rect.layerCount = 1; + const VulkanViewportRect rect = + vulkanFlippedViewportRect(origin, size, target.extent); + if (rect.x1 <= rect.x0 || rect.y1 <= rect.y0) return; + + VkClearRect clearRect {}; + clearRect.rect.offset = {rect.x0, rect.y0}; + clearRect.rect.extent = {static_cast(rect.x1 - rect.x0), + static_cast(rect.y1 - rect.y0)}; + clearRect.baseArrayLayer = 0; + clearRect.layerCount = 1; vkCmdClearAttachments(ctx.buffer, attachmentCount, attachments, 1, - &rect); + &clearRect); } void @@ -668,32 +673,8 @@ SoVulkanRenderBackend::recordDrawCommand(const SoDrawList & drawlist, ctx.lastLightingHandle = command.lightingHandle; ctx.lastLightingOffset = lightingDynamicOffset; } - uint32_t bindingOffsets[2] = { lightingDynamicOffset, uboDynamicOffset }; - if (ctx.lastBoundLightingOffset != lightingDynamicOffset || - ctx.lastBoundTextureSet != textureSet) { - // First draw of a new lighting handle / texture set: bind both sets with - // their dynamic offsets in one call (also covers the frame's first draw). - const VkDescriptorSet both[2] = {this->lightingDescriptorSet, textureSet}; - vkBackendTrace(this->uboFrameIndex, "draw.bindDescSets2", - "slot=%u", slotIndex); - vkCmdBindDescriptorSets(ctx.buffer, - VK_PIPELINE_BIND_POINT_GRAPHICS, - this->pipelineLayout, 0, 2, both, 2, - bindingOffsets); - ctx.lastBoundLightingOffset = lightingDynamicOffset; - ctx.lastBoundTextureSet = textureSet; - } - else { - // Same lighting handle + texture set as the previous draw: only the - // per-draw UBO dynamic offset advances. Re-bind set 1 alone (set 0 stays - // bound from the last 2-set bind) instead of re-emitting both sets. - vkBackendTrace(this->uboFrameIndex, "draw.bindDescSets1", - "slot=%u", slotIndex); - vkCmdBindDescriptorSets(ctx.buffer, - VK_PIPELINE_BIND_POINT_GRAPHICS, - this->pipelineLayout, 1, 1, &textureSet, 1, - &bindingOffsets[1]); - } + this->bindDrawDescriptorSets(ctx, textureSet, lightingDynamicOffset, + uboDynamicOffset, slotIndex); const VkDeviceSize vertexOffset = entry.vertexOffset; const bool indexed = @@ -1085,23 +1066,8 @@ SoVulkanRenderBackend::recordCommandBatch(const SoDrawList & drawlist, ctx.lastLightingHandle = command.lightingHandle; ctx.lastLightingOffset = lightingDynamicOffset; } - uint32_t bindingOffsets[2] = { lightingDynamicOffset, uboDynamicOffset }; - if (ctx.lastBoundLightingOffset != lightingDynamicOffset || - ctx.lastBoundTextureSet != textureSet) { - const VkDescriptorSet both[2] = {this->lightingDescriptorSet, textureSet}; - vkCmdBindDescriptorSets(ctx.buffer, - VK_PIPELINE_BIND_POINT_GRAPHICS, - this->pipelineLayout, 0, 2, both, 2, - bindingOffsets); - ctx.lastBoundLightingOffset = lightingDynamicOffset; - ctx.lastBoundTextureSet = textureSet; - } - else { - vkCmdBindDescriptorSets(ctx.buffer, - VK_PIPELINE_BIND_POINT_GRAPHICS, - this->pipelineLayout, 1, 1, &textureSet, 1, - &bindingOffsets[1]); - } + this->bindDrawDescriptorSets(ctx, textureSet, lightingDynamicOffset, + uboDynamicOffset, slotIndex); // Vertex buffer (binding 0). The whole batch shares commands[0]'s geometry, // so one bind serves every instance. diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCore.cpp b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCore.cpp index f7949818269..4183c409f2d 100644 --- a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCore.cpp +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendCore.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -126,6 +127,12 @@ SoVulkanRenderBackend::setPointsOverlay(SbBool enabled) this->pointsOverlay = enabled; } +void +SoVulkanRenderBackend::setTessellationOverlay(SbBool enabled) +{ + this->tessellationOverlay = enabled; +} + void SoVulkanRenderBackend::setEdgeColor(const SbColor4f & color) { @@ -135,6 +142,9 @@ SoVulkanRenderBackend::setEdgeColor(const SbColor4f & color) SbBool SoVulkanRenderBackend::initialize(const SoRenderBackendInitParams & params) { + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize enter " + "alreadyInit=%d\n", + this->isInitialized() ? 1 : 0); if (this->isInitialized()) return TRUE; this->setInitParams(params); @@ -144,12 +154,21 @@ SoVulkanRenderBackend::initialize(const SoRenderBackendInitParams & params) deviceContext->physicalDevice == VK_NULL_HANDLE || deviceContext->device == VK_NULL_HANDLE || deviceContext->graphicsQueue == VK_NULL_HANDLE) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL invalid device context\n"); this->emitError( "SoVulkanRenderBackend requires a SoVulkanDeviceContext in " "SoRenderBackendInitParams::userData"); return FALSE; } + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize device=0x%llx pdev=0x%llx " + "queueFam=%u\n", + (unsigned long long)(uintptr_t)deviceContext->device, + (unsigned long long)(uintptr_t)deviceContext->physicalDevice, + deviceContext->graphicsQueueFamilyIndex); + this->physicalDevice = deviceContext->physicalDevice; this->device = deviceContext->device; this->queue = deviceContext->graphicsQueue; @@ -157,8 +176,22 @@ SoVulkanRenderBackend::initialize(const SoRenderBackendInitParams & params) this->allocator = deviceContext->allocator; this->memProps.setDevice(this->physicalDevice); + // Bind the render-pass/framebuffer cache to this device and hook its + // deferred resource release into the frame ring: an old framebuffer is + // destroyed a few frames after the submission that referenced it completes, + // rather than synchronously (which would race a still-executing frame). + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize renderPasses.setDevice\n"); + this->renderPasses.setDevice(this->device, this->allocator); + this->renderPasses.setDeferredDestroy([this](std::function && fn) { + this->deferDestroy(std::move(fn)); + }); + // Opt-in device-memory sub-allocator (FC_VULKAN_MEM_POOL). Default off so // the behaviour is byte-for-byte the legacy path unless explicitly enabled. + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize memPool=%d\n", + SoVulkanShared::envFlagEnabled("FC_VULKAN_MEM_POOL") ? 1 : 0); if (SoVulkanShared::envFlagEnabled("FC_VULKAN_MEM_POOL")) { this->memPool = std::make_unique( this->device, this->allocator); @@ -204,84 +237,140 @@ SoVulkanRenderBackend::initialize(const SoRenderBackendInitParams & params) }; this->sampledR8 = sampledOptimal(VK_FORMAT_R8_UNORM); this->sampledR8G8 = sampledOptimal(VK_FORMAT_R8G8_UNORM); + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize features fillModeNonSolid=%d " + "sampledR8=%d sampledR8G8=%d\n", + this->fillModeNonSolid ? 1 : 0, this->sampledR8 ? 1 : 0, + this->sampledR8G8 ? 1 : 0); // Mark initialized before creating resources so that a failure in any // create*() below runs the full (null-tolerant) shutdown() cleanup // instead of leaking every handle created so far. this->setInitialized(TRUE); + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "creating resources\n"); if (!this->createCommandPool()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createCommandPool\n"); this->emitError("failed to create Vulkan command pool"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createCommandPool OK\n"); if (!this->createDescriptorSetLayout()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createDescriptorSetLayout\n"); this->emitError("failed to create Vulkan descriptor set layout"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createDescriptorSetLayout OK\n"); if (!this->createDescriptorPool()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createDescriptorPool\n"); this->emitError("failed to create Vulkan descriptor pool"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createDescriptorPool OK\n"); if (!this->createLightingUniformBuffer()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createLightingUniformBuffer\n"); this->emitError("failed to create Vulkan lighting uniform buffer"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createLightingUniformBuffer OK\n"); if (!this->createLightingConstBuffer()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createLightingConstBuffer\n"); this->emitError("failed to create Vulkan lighting constant buffer"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createLightingConstBuffer OK\n"); if (!this->createLightingDescriptorSet()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createLightingDescriptorSet\n"); this->emitError("failed to create Vulkan lighting descriptor set"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createLightingDescriptorSet OK\n"); if (!this->createWhiteTexture()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createWhiteTexture\n"); this->emitError("failed to create Vulkan white fallback texture"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createWhiteTexture OK\n"); if (!this->createPipelineLayout()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createPipelineLayout\n"); this->emitError("failed to create Vulkan pipeline layout"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createPipelineLayout OK\n"); if (!this->createShaders(this->vertexModule, this->fragmentModule)) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createShaders\n"); this->emitError("failed to create Vulkan shader modules"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createShaders OK\n"); if (!this->createWideLineShaders()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createWideLineShaders\n"); this->emitError("failed to create Vulkan wide-line shader modules"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createWideLineShaders OK\n"); if (!this->createBackgroundResources()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createBackgroundResources\n"); this->emitError("failed to create Vulkan background resources"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createBackgroundResources OK\n"); if (!this->createPipelineCache()) { + SoVulkanShared::initBreadcrumb( + "SoVulkanRenderBackend::initialize FAIL createPipelineCache\n"); this->emitError("failed to create Vulkan pipeline cache"); this->shutdown(); return FALSE; } + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize " + "createPipelineCache OK\n"); + SoVulkanShared::initBreadcrumb("SoVulkanRenderBackend::initialize DONE\n"); this->emitLog("initialized"); return TRUE; } @@ -967,6 +1056,39 @@ SoVulkanRenderBackend::cacheFrameMatrices(const SoRenderParams & params) sizeof(float) * 16); this->frameDpr = params.devicePixelRatio > 0.0f ? params.devicePixelRatio : 1.0f; + if (std::getenv("FC_VULKAN_BREADCRUMBS")) { + // TEMP-VKINIT: project the (0..10) document-box center through the resolved + // frame camera and report NDC; |ndc|>1 means the frame camera is NOT + // framing the recorded geometry. + SbVec4f p(5.0f, 5.0f, 5.0f, 1.0f); + const float* v = this->frameViewFloats; + const float* pr = this->frameProjFloats; + float w[4]; + for (int r = 0; r < 4; ++r) { + w[r] = v[r * 4 + 0] * p[0] + v[r * 4 + 1] * p[1] + + v[r * 4 + 2] * p[2] + v[r * 4 + 3] * p[3]; + } + float ndc[4]; + for (int r = 0; r < 4; ++r) { + ndc[r] = pr[r * 4 + 0] * w[0] + pr[r * 4 + 1] * w[1] + + pr[r * 4 + 2] * w[2] + pr[r * 4 + 3] * w[3]; + } + if (std::fabs(ndc[3]) > 1e-6f) { + ndc[0] /= ndc[3]; ndc[1] /= ndc[3]; ndc[2] /= ndc[3]; + } + // perspective proj near/far from pr[2][2]/pr[2][3] + float nearP = 0, farP = 0; + const float a = pr[2 * 4 + 2], b = pr[2 * 4 + 3]; + if (std::fabs(a + 1.0f) > 1e-4f && std::fabs(a - 1.0f) > 1e-4f) { + nearP = b / (a + 1.0f); + farP = b / (a - 1.0f); + } + std::fprintf(stderr, + "[VKINIT] frameCam viewPos(eye)=%.2f,%.2f,%.2f projNear=%.4f far=%.4f " + "boxCenterNDC=(%.3f,%.3f,%.3f)\n", + w[0], w[1], w[2], nearP, farP, ndc[0], ndc[1], ndc[2]); + std::fflush(stderr); + } } void diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendFrame.cpp b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendFrame.cpp index 4142615a486..1879b178968 100644 --- a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendFrame.cpp +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendFrame.cpp @@ -261,18 +261,9 @@ SoVulkanRenderBackend::shutdown() } this->backgroundPipelineCache.clear(); - if (this->renderPassFramebuffer != VK_NULL_HANDLE) { - vkDestroyFramebuffer(this->device, this->renderPassFramebuffer, - this->allocator); - this->renderPassFramebuffer = VK_NULL_HANDLE; - } - for (auto & entry : this->renderPassCache) { - if (entry.second != VK_NULL_HANDLE) { - vkDestroyRenderPass(this->device, entry.second, this->allocator); - } - } - this->renderPassCache.clear(); - this->renderPass = VK_NULL_HANDLE; + // The render-pass/framebuffer cache owns the current pass + framebuffer; + // releasing it after the deferred destroys flush above (queue is idle). + this->renderPasses.destroyAll(); if (this->fragmentModule != VK_NULL_HANDLE) { vkDestroyShaderModule(this->device, this->fragmentModule, this->allocator); this->fragmentModule = VK_NULL_HANDLE; @@ -451,31 +442,8 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, static_cast(overlaysOnly), drawlist.getNumCommands()); if (COIN_VULKAN_ENV_FLAG("FC_VULKAN_BLACK_DEBUG")) { - static int blackFrame = 0; - int nTri = 0, nLine = 0, nOverlay = 0, nTrans = 0, nTriLit = 0; - int nTriUnlit = 0; - for (int i = 0; i < drawlist.getNumCommands(); ++i) { - const SoRenderCommand & c = drawlist.getCommand(i); - if (c.pass == SO_RENDERPASS_OVERLAY) nOverlay++; - else if (c.pass == SO_RENDERPASS_TRANSPARENT) nTrans++; - if (c.geometry.topology == SO_TOPOLOGY_TRIANGLES) { - nTri++; - if (c.material.shadingModel == SO_SHADING_LEGACY_GOURAUD) nTriLit++; - else nTriUnlit++; - } - if (c.geometry.topology == SO_TOPOLOGY_LINES || - c.geometry.topology == SO_TOPOLOGY_LINE_STRIP) { - nLine++; - } - } - fprintf(stderr, - "[BLACK] frame=%d overlaysOnly=%d flags=0x%x clear=(%.2f,%.2f,%.2f,%.2f) " - "cmds=%d tri=%d(lit=%d unlit=%d) line=%d overlay=%d trans=%d\n", - blackFrame++, static_cast(overlaysOnly), - static_cast(params.flags), params.clearColor[0], - params.clearColor[1], params.clearColor[2], params.clearColor[3], - drawlist.getNumCommands(), nTri, nTriLit, nTriUnlit, nLine, - nOverlay, nTrans); + vkBlackDebugStats(drawlist, params, static_cast(overlaysOnly), + "renderInternal"); } const auto * target = @@ -530,14 +498,15 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, (fullTargetClear && hasDepth && clearDepth) ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD; - this->renderPass = this->getOrCreateRenderPass(*target, colorLoadOp, - depthLoadOp); + this->renderPasses.getOrCreateRenderPass(*target, colorLoadOp, + depthLoadOp); // Stash whether the pass cleared each attachment so recordClear() can skip // the redundant vkCmdClearAttachments, and (below) so the begin info carries // the matching clear values. - this->renderPassColorCleared = (colorLoadOp == VK_ATTACHMENT_LOAD_OP_CLEAR); - this->renderPassDepthCleared = (depthLoadOp == VK_ATTACHMENT_LOAD_OP_CLEAR); - if (this->renderPass == VK_NULL_HANDLE) { + this->renderPasses.setClearedByLoad( + colorLoadOp == VK_ATTACHMENT_LOAD_OP_CLEAR, + depthLoadOp == VK_ATTACHMENT_LOAD_OP_CLEAR); + if (this->renderPasses.currentRenderPass() == VK_NULL_HANDLE) { this->emitError("failed to create Vulkan render pass"); return FALSE; } @@ -564,7 +533,8 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, // old framebuffer is released through the deferred ring: an older in-flight // submission may still reference it (the per-frame vkQueueWaitIdle is gone, // so only the current slot's fence has been waited by beginFrame()). - if (!this->ensureFramebuffer(target, this->renderPass)) { + if (!this->renderPasses.ensureFramebuffer( + target, this->renderPasses.currentRenderPass())) { this->emitError("failed to create Vulkan framebuffer"); // The one-shot command buffer was begun above and never submitted; an // implicit reset only happens on submission, so reset it explicitly or @@ -573,7 +543,7 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, vkResetCommandBuffer(this->currentCommandBuffer(), 0); return FALSE; } - const VkFramebuffer framebuffer = this->renderPassFramebuffer; + const VkFramebuffer framebuffer = this->renderPasses.framebuffer(); // Record the pending texture copies into the frame command buffer (one // submit for the whole frame instead of a separate transfer submit) and @@ -588,7 +558,7 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, VkRenderPassBeginInfo rpbi {}; rpbi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rpbi.renderPass = this->renderPass; + rpbi.renderPass = this->renderPasses.currentRenderPass(); rpbi.framebuffer = framebuffer; rpbi.renderArea.offset = {0, 0}; rpbi.renderArea.extent = target->extent; @@ -597,14 +567,14 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, // maps one-to-one to the attachment indices (0 = color, 1 = depth). VkClearValue clearValues[2]; uint32_t clearValueCount = 0; - if (this->renderPassColorCleared) { + if (this->renderPasses.colorClearedByLoad()) { clearValues[0].color.float32[0] = params.clearColor[0]; clearValues[0].color.float32[1] = params.clearColor[1]; clearValues[0].color.float32[2] = params.clearColor[2]; clearValues[0].color.float32[3] = params.clearColor[3]; clearValueCount = 1; } - if (this->renderPassDepthCleared) { + if (this->renderPasses.depthClearedByLoad()) { clearValues[clearValueCount].depthStencil.depth = params.clearDepth; clearValues[clearValueCount].depthStencil.stencil = 0; ++clearValueCount; @@ -618,15 +588,18 @@ SoVulkanRenderBackend::renderInternal(const SoDrawList & drawlist, this->recordContext.buffer = this->currentCommandBuffer(); bool recorded = true; if (overlaysOnly) { - this->recordTracedComposite(drawlist, params, *target, this->renderPass, + this->recordTracedComposite(drawlist, params, *target, + this->renderPasses.currentRenderPass(), this->recordContext); - this->recordOverlayBlock(drawlist, params, *target, this->renderPass, + this->recordOverlayBlock(drawlist, params, *target, + this->renderPasses.currentRenderPass(), this->recordContext); } else { - recorded = this->recordFrame(drawlist, params, *target, this->renderPass, + recorded = this->recordFrame(drawlist, params, *target, + this->renderPasses.currentRenderPass(), this->recordContext, - this->renderPassFramebuffer); + this->renderPasses.framebuffer()); } this->recordContext.buffer = VK_NULL_HANDLE; @@ -661,59 +634,6 @@ SoVulkanRenderBackend::renderOverlaysOnly(const SoDrawList & drawlist, return this->renderInternal(drawlist, params, true); } -bool -SoVulkanRenderBackend::ensureFramebuffer(const SoVulkanRenderTarget * target, - VkRenderPass renderPass) -{ - if (this->renderPassFramebuffer != VK_NULL_HANDLE && - this->renderPassFramebufferPass == renderPass && - this->renderPassFramebufferColorImage == target->colorImage && - this->renderPassFramebufferColorView == target->colorImageView && - this->renderPassFramebufferDepthImage == target->depthImage && - this->renderPassFramebufferDepthView == target->depthImageView && - this->renderPassFramebufferExtent.width == target->extent.width && - this->renderPassFramebufferExtent.height == target->extent.height) { - return true; - } - if (this->renderPassFramebuffer != VK_NULL_HANDLE) { - const VkDevice device = this->device; - const VkAllocationCallbacks * allocator = this->allocator; - const VkFramebuffer oldFramebuffer = this->renderPassFramebuffer; - this->deferDestroy([device, allocator, oldFramebuffer]() { - if (oldFramebuffer != VK_NULL_HANDLE) { - vkDestroyFramebuffer(device, oldFramebuffer, allocator); - } - }); - this->renderPassFramebuffer = VK_NULL_HANDLE; - } - VkFramebufferCreateInfo fci {}; - fci.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fci.renderPass = renderPass; - fci.attachmentCount = - (target->depthImageView != VK_NULL_HANDLE && - target->depthFormat != VK_FORMAT_UNDEFINED) - ? 2u : 1u; - const VkImageView attachments[] = { - target->colorImageView, - target->depthImageView, - }; - fci.pAttachments = attachments; - fci.width = target->extent.width; - fci.height = target->extent.height; - fci.layers = 1; - if (vkCreateFramebuffer(this->device, &fci, this->allocator, - &this->renderPassFramebuffer) != VK_SUCCESS) { - return false; - } - this->renderPassFramebufferPass = renderPass; - this->renderPassFramebufferColorImage = target->colorImage; - this->renderPassFramebufferColorView = target->colorImageView; - this->renderPassFramebufferDepthImage = target->depthImage; - this->renderPassFramebufferDepthView = target->depthImageView; - this->renderPassFramebufferExtent = target->extent; - return true; -} - SbBool SoVulkanRenderBackend::renderExternal(const SoDrawList & drawlist, const SoRenderParams & params, @@ -740,8 +660,7 @@ SoVulkanRenderBackend::renderExternal(const SoDrawList & drawlist, // The external render pass is supplied by the caller (typically created with // LOAD loadOps and layered over a pre-existing image), so no attachment is // cleared by a loadOp here: recordClear() must emit vkCmdClearAttachments. - this->renderPassColorCleared = false; - this->renderPassDepthCleared = false; + this->renderPasses.setClearedByLoad(false, false); if (COIN_VULKAN_ENV_FLAG("FC_VULKAN_BLACK_DEBUG")) fprintf(stderr, "[BLACK] renderExternal ENTER frame=%d cmds=%d\n", @@ -836,8 +755,7 @@ SoVulkanRenderBackend::renderExternalOverlay(const SoDrawList & drawlist, } // External passes are caller-supplied LOAD render passes; see renderExternal(). - this->renderPassColorCleared = false; - this->renderPassDepthCleared = false; + this->renderPasses.setClearedByLoad(false, false); if (COIN_VULKAN_ENV_FLAG("FC_VULKAN_BLACK_DEBUG")) fprintf(stderr, "[BLACK] renderExternalOverlay ENTER frame=%d cmds=%d\n", @@ -900,6 +818,7 @@ bool SoVulkanRenderBackend::buildWorkItems(const SoDrawList & drawlist, const SoRenderParams & params, bool wireframeOverlay, bool pointsOverlay, + bool tessellationOverlay, const float * overlayColor, VkRenderPass renderPass, std::vector & out) @@ -961,13 +880,88 @@ SoVulkanRenderBackend::buildWorkItems(const SoDrawList & drawlist, if (command.pass == SO_RENDERPASS_OVERLAY) continue; if (command.pass == SO_RENDERPASS_TRANSPARENT) continue; if (!command.state.depth.enabled) continue; // on-top annotation (later) - if (!command.geometry.positions || command.geometry.vertexCount == 0) + if (!command.geometry.positions || command.geometry.vertexCount == 0) { + if (std::getenv("FC_VULKAN_BREADCRUMBS") && command.geometry.vertexCount > 0) + std::fprintf(stderr, "[VKINIT] opaque SKIP: lit=%d vc=%d no-positions\n", + command.material.shadingModel == SO_SHADING_LEGACY_GOURAUD ? 1 : 0, + command.geometry.vertexCount), std::fflush(stderr); continue; + } if (vkIsWideLine(command)) continue; // CPU-expanded per command const auto found = this->commandToCache.find(&command); - if (found == this->commandToCache.end()) continue; - if (this->gpuCache[found->second].vertexBuffer == VK_NULL_HANDLE) + if (found == this->commandToCache.end()) { + if (std::getenv("FC_VULKAN_BREADCRUMBS") && + command.material.shadingModel == SO_SHADING_LEGACY_GOURAUD) + std::fprintf(stderr, "[VKINIT] opaque SKIP: lit cmd not in commandToCache\n"), + std::fflush(stderr); continue; + } + if (this->gpuCache[found->second].vertexBuffer == VK_NULL_HANDLE) { + if (std::getenv("FC_VULKAN_BREADCRUMBS") && + command.material.shadingModel == SO_SHADING_LEGACY_GOURAUD) + std::fprintf(stderr, "[VKINIT] opaque SKIP: lit vertexBuffer NULL\n"), + std::fflush(stderr); + continue; + } + if (std::getenv("FC_VULKAN_BREADCRUMBS")) { + // Project the command's local-space cube corners (-1..1) through + // model * frameView * frameProj and report the screen-space NDC extents. + float mv[4][4]; command.modelMatrix.getValue(mv); + const float* v = this->frameViewFloats; + const float* pr = this->frameProjFloats; + float outMin[3] = {1e9f, 1e9f, 1e9f}, outMax[3] = {-1e9f, -1e9f, -1e9f}; + for (int cx = -1; cx <= 1; cx += 2) + for (int cy = -1; cy <= 1; cy += 2) + for (int cz = -1; cz <= 1; cz += 2) { + const float in[4] = {(float)cx, (float)cy, (float)cz, 1.0f}; + // w = view * model * in + float wm[4]; + for (int rr = 0; rr < 4; ++rr) + wm[rr] = v[rr * 4 + 0] * mv[0][0] * in[0] + + v[rr * 4 + 0] * mv[0][1] * in[1] + + v[rr * 4 + 0] * mv[0][2] * in[2] + + v[rr * 4 + 0] * mv[0][3] * in[3] + + v[rr * 4 + 1] * mv[1][0] * in[0] + + v[rr * 4 + 1] * mv[1][1] * in[1] + + v[rr * 4 + 1] * mv[1][2] * in[2] + + v[rr * 4 + 1] * mv[1][3] * in[3] + + v[rr * 4 + 2] * mv[2][0] * in[0] + + v[rr * 4 + 2] * mv[2][1] * in[1] + + v[rr * 4 + 2] * mv[2][2] * in[2] + + v[rr * 4 + 2] * mv[2][3] * in[3] + + v[rr * 4 + 3] * mv[3][0] * in[0] + + v[rr * 4 + 3] * mv[3][1] * in[1] + + v[rr * 4 + 3] * mv[3][2] * in[2] + + v[rr * 4 + 3] * mv[3][3] * in[3]; + float ndc[4]; + for (int rr = 0; rr < 4; ++rr) + ndc[rr] = pr[rr * 4 + 0] * wm[0] + pr[rr * 4 + 1] * wm[1] + + pr[rr * 4 + 2] * wm[2] + pr[rr * 4 + 3] * wm[3]; + if (std::fabs(ndc[3]) > 1e-6f) { + ndc[0] /= ndc[3]; ndc[1] /= ndc[3]; ndc[2] /= ndc[3]; + } + for (int k = 0; k < 3; ++k) { + if (ndc[k] < outMin[k]) outMin[k] = ndc[k]; + if (ndc[k] > outMax[k]) outMax[k] = ndc[k]; + } + } + const float* p = command.geometry.positions; + const uint32_t vertStride = command.geometry.vertexStride ? + command.geometry.vertexStride / sizeof(float) : 0; + const float* v0 = p; const float* v1 = p + vertStride; + const float s0 = std::sqrt(mv[0][0]*mv[0][0]+mv[1][0]*mv[1][0]+mv[2][0]*mv[2][0]); + const float s1 = std::sqrt(mv[0][1]*mv[0][1]+mv[1][1]*mv[1][1]+mv[2][1]*mv[2][1]); + const float s2 = std::sqrt(mv[0][2]*mv[0][2]+mv[1][2]*mv[1][2]+mv[2][2]*mv[2][2]); + std::fprintf(stderr, + "[VKINIT] opaque ACCEPT shading=%d vc=%d modelScale=(%.3f,%.3f,%.3f) " + "NDCX=[%.2f,%.2f] NDCY=[%.2f,%.2f] NDCZ=[%.2f,%.2f] v0=(%.2f,%.2f,%.2f)\n", + command.material.shadingModel, + command.geometry.vertexCount, + s0, s1, s2, + outMin[0], outMax[0], outMin[1], outMax[1], + outMin[2], outMax[2], v0[0], v0[1], v0[2]); + std::fflush(stderr); + } buckets[vkBatchKey(command, contentHashOf(command))].push_back(&command); } for (auto & kv : buckets) { @@ -1001,7 +995,24 @@ SoVulkanRenderBackend::buildWorkItems(const SoDrawList & drawlist, // Wireframe/point overlay: re-draw opaque geometry in the requested fill // mode using a uniform edge color. - if (!transparent && overlayFillMode >= 0) { + // + // When the request is a LINES (edge) overlay, re-draw only the actual + // B-Rep feature-edge commands (SoBrepEdgeSet emits SO_TOPOLOGY_LINES / + // LINE_STRIP). Re-drawing every triangle command in polygon-LINES would + // paint the raw tessellation -- the straight seam meridian on a sphere and + // the radial fan spokes on a cylinder cap -- instead of the true feature + // edges (rims, creases, seams). A CAD edge overlay must show only feature + // edges; smooth curved surfaces carry no feature edges and read as clean. + // + // The debug tessellation overlay is the opposite request: re-draw the + // TRIANGLE commands in polygon-LINES so the raw triangulation edges are + // visible on top of the shaded geometry, and skip the line commands so + // the feature edges are not double-painted. + if (!transparent && (overlayFillMode >= 0 || tessellationOverlay)) { + const bool isEdgeOverlay = (overlayFillMode == SoDrawStyleElement::LINES); + const int redrawFillMode = tessellationOverlay + ? SoDrawStyleElement::LINES + : overlayFillMode; for (int i = 0; i < drawlist.getNumCommands(); ++i) { const int index = i < static_cast(order.size()) ? order[i] : i; @@ -1010,6 +1021,22 @@ SoVulkanRenderBackend::buildWorkItems(const SoDrawList & drawlist, if (command.pass == SO_RENDERPASS_TRANSPARENT) continue; if (!command.geometry.positions || command.geometry.vertexCount == 0) continue; + const SoPrimitiveTopology topo = command.geometry.topology; + // For the edge overlay, restrict to commands that are themselves line + // primitives; skip triangles so tessellation edges never render. + if (isEdgeOverlay) { + if (topo != SO_TOPOLOGY_LINES && + topo != SO_TOPOLOGY_LINE_STRIP) { + continue; + } + } + // For the debug tessellation overlay, restrict to triangle commands. + if (tessellationOverlay) { + if (topo != SO_TOPOLOGY_TRIANGLES && + topo != SO_TOPOLOGY_TRIANGLE_STRIP) { + continue; + } + } const auto found = this->commandToCache.find(&command); if (found == this->commandToCache.end()) continue; if (this->gpuCache[found->second].vertexBuffer == VK_NULL_HANDLE) @@ -1017,7 +1044,7 @@ SoVulkanRenderBackend::buildWorkItems(const SoDrawList & drawlist, VulkanWorkItem item; item.single = &command; item.count = 1; - item.fillModeOverride = overlayFillMode; + item.fillModeOverride = redrawFillMode; item.uniformColorOverride = overlayColor; item.slotBase = nextSlot++; out.push_back(item); @@ -1177,34 +1204,11 @@ SoVulkanRenderBackend::recordFrame(const SoDrawList & drawlist, s_dumpCmdCount = 0; } if (COIN_VULKAN_ENV_FLAG("FC_VULKAN_BLACK_DEBUG")) { - static int blackFrame = 0; - int nTri = 0, nLine = 0, nOverlay = 0, nTrans = 0, nTriLit = 0; - int nTriUnlit = 0; - for (int i = 0; i < drawlist.getNumCommands(); ++i) { - const SoRenderCommand & c = drawlist.getCommand(i); - if (c.pass == SO_RENDERPASS_OVERLAY) nOverlay++; - else if (c.pass == SO_RENDERPASS_TRANSPARENT) nTrans++; - if (c.geometry.topology == SO_TOPOLOGY_TRIANGLES) { - nTri++; - if (c.material.shadingModel == SO_SHADING_LEGACY_GOURAUD) nTriLit++; - else nTriUnlit++; - } - if (c.geometry.topology == SO_TOPOLOGY_LINES || - c.geometry.topology == SO_TOPOLOGY_LINE_STRIP) { - nLine++; - } - } - fprintf(stderr, - "[BLACK] recordFrame frame=%d flags=0x%x clear=(%.2f,%.2f,%.2f,%.2f) " - "cmds=%d tri=%d(lit=%d unlit=%d) line=%d overlay=%d trans=%d\n", - blackFrame++, static_cast(params.flags), - params.clearColor[0], params.clearColor[1], params.clearColor[2], - params.clearColor[3], drawlist.getNumCommands(), nTri, nTriLit, - nTriUnlit, nLine, nOverlay, nTrans); + vkBlackDebugStats(drawlist, params, 0, "recordFrame"); } this->applyViewport(params, target, ctx); - this->recordClear(params, target, this->renderPassColorCleared, - this->renderPassDepthCleared, ctx); + this->recordClear(params, target, this->renderPasses.colorClearedByLoad(), + this->renderPasses.depthClearedByLoad(), ctx); this->recordBackground(params, target, renderPass, ctx); // The background pass overrides the viewport/scissor for its own draw; // restore the viewport from params before recording geometry so draws @@ -1218,6 +1222,8 @@ SoVulkanRenderBackend::recordFrame(const SoDrawList & drawlist, this->wireframeOverlay || COIN_VULKAN_ENV_FLAG("FC_VULKAN_WIREFRAME"); const bool pointsOverlay = this->pointsOverlay || COIN_VULKAN_ENV_FLAG("FC_VULKAN_POINTS"); + const bool tessellationOverlay = + this->tessellationOverlay || COIN_VULKAN_ENV_FLAG("FC_VULKAN_TESS"); float overlayColor[4] = { this->edgeColor[0], this->edgeColor[1], this->edgeColor[2], this->edgeColor[3] @@ -1256,8 +1262,9 @@ SoVulkanRenderBackend::recordFrame(const SoDrawList & drawlist, static int overlayLog = 0; if (overlayLog++ < 3) { fprintf(stderr, - "[OVL] wireframe=%d points=%d fillMode=%d edgeColor=(%.2f,%.2f,%.2f,%.2f)\n", - wireframeOverlay ? 1 : 0, pointsOverlay ? 1 : 0, overlayFillMode, + "[OVL] wireframe=%d points=%d tess=%d fillMode=%d edgeColor=(%.2f,%.2f,%.2f,%.2f)\n", + wireframeOverlay ? 1 : 0, pointsOverlay ? 1 : 0, + tessellationOverlay ? 1 : 0, overlayFillMode, overlayColor[0], overlayColor[1], overlayColor[2], overlayColor[3]); } } @@ -1266,7 +1273,8 @@ SoVulkanRenderBackend::recordFrame(const SoDrawList & drawlist, // overlay redraws) before recording, so slotIndex can never overflow the // ring allocation (VUID-vkCmdBindDescriptorSets-pDynamicOffsets-01972). if (!this->prepareLightingSlots(countDrawCommands(drawlist, - overlayFillMode))) { + overlayFillMode, + tessellationOverlay))) { return FALSE; } @@ -1277,20 +1285,21 @@ SoVulkanRenderBackend::recordFrame(const SoDrawList & drawlist, // per-draw uboCmdIndex++ sequence produced, so recording is identical. std::vector & workItems = this->workItemsScratch; this->buildWorkItems(drawlist, params, wireframeOverlay, pointsOverlay, - overlayColor, renderPass, workItems); + tessellationOverlay, overlayColor, renderPass, + workItems); vkBackendTrace(this->uboFrameIndex, "recordFrame.workItemsBuilt", "items=%zu", workItems.size()); // Secondaries are recorded with RENDER_PASS_CONTINUE inheritance into the // pass the frame is in. On the INTERNAL path that pass is backend-owned - // (renderPass == this->renderPass) and the combination is exercised by the - // testsuite. The EXTERNAL path (FreeCAD's QuarterVulkanWidget) hands us a + // (renderPass == this->renderPasses.currentRenderPass()) and the combination + // is exercised by the testsuite. The EXTERNAL path (FreeCAD's QuarterVulkanWidget) hands us a // caller-owned pass/framebuffer/command-buffer triplet (QVulkanWindow's, // possibly MSAA); recording secondaries against it has proven to corrupt // NVIDIA driver state (crash inside the driver at the first render-pass // command after the replay) so it stays OFF unless explicitly opted in // while that interaction is investigated. - const bool externalPass = renderPass != this->renderPass; + const bool externalPass = renderPass != this->renderPasses.currentRenderPass(); const bool canUseSecondary = !this->secondaryCommandBuffers.empty() && inheritFramebuffer != VK_NULL_HANDLE && diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendGeometry.cpp b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendGeometry.cpp index e7834f1ac1b..f35fabd99c7 100644 --- a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendGeometry.cpp +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendGeometry.cpp @@ -162,16 +162,12 @@ SoVulkanRenderBackend::selectMemoryType(const VkMemoryRequirements & requirement const VkMemoryPropertyFlags desired, uint32_t & memoryTypeIndex) { - const VkPhysicalDeviceMemoryProperties & props = this->memProps.properties(); - for (uint32_t i = 0; i < props.memoryTypeCount; ++i) { - if ((requirements.memoryTypeBits & (1u << i)) && - (props.memoryTypes[i].propertyFlags & desired) == desired) { - memoryTypeIndex = i; - return true; - } - } + // Exact-match policy (no fallback): the selection loop lives in the shared + // SoVulkanShared::MemoryProperties so both backends route memory-type + // selection through one implementation; only the policy differs (the RT + // backend calls pick() to allow a best-effort fallback). memoryTypeIndex = 0; - return false; + return this->memProps.pickExact(requirements, desired, memoryTypeIndex); } bool diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendP.h b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendP.h index e8600baa675..4906a189484 100644 --- a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendP.h +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendP.h @@ -87,14 +87,80 @@ namespace CoinVulkanDetail { inline int s_dumpCmdCount = 0; inline int s_lightLog = 0; +// Coin/OpenGL viewport origins are bottom-left; Vulkan's are top-left. The +// vertex shader flips Y in clip space, so the viewport rectangle must be +// re-anchored to the top edge for the two to cancel out. This computes the +// viewport region in Vulkan coordinates and clamps it to the target so an +// off-screen viewport (origin outside the target, or a size exceeding the +// extent) never produces a clear/clip outside the render area. Single +// source for the math shared by applyViewport(), isFullTargetClear(), +// recordClear() and recordBackground(). +struct VulkanViewportRect { + int32_t x0 = 0, y0 = 0, x1 = 0, y1 = 0; +}; + +inline VulkanViewportRect +vulkanFlippedViewportRect(const SbVec2s & origin, const SbVec2s & size, + const VkExtent2D & extent) +{ + VulkanViewportRect r; + r.x0 = std::max(0, static_cast(origin[0])); + r.y0 = std::max(0, static_cast(extent.height) - + static_cast(origin[1]) - + static_cast(size[1])); + r.x1 = std::min(static_cast(extent.width), + static_cast(origin[0]) + + static_cast(size[0])); + r.y1 = std::min(static_cast(extent.height), + static_cast(extent.height) - + static_cast(origin[1])); + return r; +} + +// [BLACK] per-frame drawlist statistics (FC_VULKAN_BLACK_DEBUG). One line +// per recorded frame with the command breakdown, so a black/blank render +// can be told apart from a frame that drew nothing vs. a frame that drew +// only transparent/overlay commands. +inline void +vkBlackDebugStats(const SoDrawList & drawlist, const SoRenderParams & params, + int overlaysOnly, const char * tag) +{ + static int blackFrame = 0; + int nTri = 0, nLine = 0, nOverlay = 0, nTrans = 0, nTriLit = 0; + int nTriUnlit = 0; + for (int i = 0; i < drawlist.getNumCommands(); ++i) { + const SoRenderCommand & c = drawlist.getCommand(i); + if (c.pass == SO_RENDERPASS_OVERLAY) nOverlay++; + else if (c.pass == SO_RENDERPASS_TRANSPARENT) nTrans++; + if (c.geometry.topology == SO_TOPOLOGY_TRIANGLES) { + nTri++; + if (c.material.shadingModel == SO_SHADING_LEGACY_GOURAUD) nTriLit++; + else nTriUnlit++; + } + if (c.geometry.topology == SO_TOPOLOGY_LINES || + c.geometry.topology == SO_TOPOLOGY_LINE_STRIP) { + nLine++; + } + } + fprintf(stderr, + "[BLACK] %s frame=%d overlaysOnly=%d flags=0x%x " + "clear=(%.2f,%.2f,%.2f,%.2f) cmds=%d tri=%d(lit=%d unlit=%d) " + "line=%d overlay=%d trans=%d\n", + tag, blackFrame++, overlaysOnly, static_cast(params.flags), + params.clearColor[0], params.clearColor[1], params.clearColor[2], + params.clearColor[3], drawlist.getNumCommands(), nTri, nTriLit, + nTriUnlit, nLine, nOverlay, nTrans); +} + // Number of per-draw lighting UBO slots a frame will consume. A command is -// recorded once in its own pass, again when the wireframe/point overlay -// redraw is active (opaque commands only), and overlay commands are recorded -// a second time in the overlay block. recordDrawCommand() bails out before -// claiming a slot for skipped commands, so this worst case is a safe upper -// bound. +// recorded once in its own pass, again when the wireframe/point/tessellation +// overlay redraw is active (opaque commands only), and overlay commands are +// recorded a second time in the overlay block. recordDrawCommand() bails +// out before claiming a slot for skipped commands, so this worst case is a +// safe upper bound. inline uint32_t -countDrawCommands(const SoDrawList & drawlist, const int overlayFillMode) +countDrawCommands(const SoDrawList & drawlist, const int overlayFillMode, + const bool tessellationOverlay) { uint32_t draws = 0; const int num = drawlist.getNumCommands(); @@ -102,7 +168,7 @@ countDrawCommands(const SoDrawList & drawlist, const int overlayFillMode) const SoRenderCommand & command = drawlist.getCommand(i); if (command.pass == SO_RENDERPASS_OVERLAY) continue; ++draws; - if (overlayFillMode >= 0 && + if ((overlayFillMode >= 0 || tessellationOverlay) && command.pass != SO_RENDERPASS_TRANSPARENT) { ++draws; } diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendPipeline.cpp b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendPipeline.cpp index b02a3b314d7..bb2d0f3f593 100644 --- a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendPipeline.cpp +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderBackendPipeline.cpp @@ -171,25 +171,15 @@ SoVulkanRenderBackend::recordBackground(const SoRenderParams & params, // applyViewport()); geometry drawn afterwards restores its own viewport. const SbVec2s & origin = params.viewport.getViewportOriginPixels(); const SbVec2s & size = params.viewport.getViewportSizePixels(); - const int32_t x0 = std::max(0, static_cast(origin[0])); - const int32_t y0 = std::max( - 0, static_cast(target.extent.height) - - static_cast(origin[1]) - - static_cast(size[1])); - const int32_t x1 = std::min(static_cast(target.extent.width), - static_cast(origin[0]) + - static_cast(size[0])); - const int32_t y1 = std::min( - static_cast(target.extent.height), - static_cast(target.extent.height) - - static_cast(origin[1])); - const int32_t w = std::max(0, x1 - x0); - const int32_t h = std::max(0, y1 - y0); + const VulkanViewportRect rect = + vulkanFlippedViewportRect(origin, size, target.extent); + const int32_t w = std::max(0, rect.x1 - rect.x0); + const int32_t h = std::max(0, rect.y1 - rect.y0); if (w == 0 || h == 0) return; VkViewport viewport {}; - viewport.x = static_cast(x0); - viewport.y = static_cast(y0); + viewport.x = static_cast(rect.x0); + viewport.y = static_cast(rect.y0); viewport.width = static_cast(w); viewport.height = static_cast(h); viewport.minDepth = 0.0f; @@ -197,7 +187,7 @@ SoVulkanRenderBackend::recordBackground(const SoRenderParams & params, this->applyViewportState(viewport, ctx); VkRect2D scissor {}; - scissor.offset = {x0, y0}; + scissor.offset = {rect.x0, rect.y0}; scissor.extent = {static_cast(w), static_cast(h)}; this->applyScissorState(scissor, ctx); @@ -214,8 +204,8 @@ SoVulkanRenderBackend::recordBackground(const SoRenderParams & params, push.bottomColor[3] = params.backgroundBottomColor[3]; push.viewport[0] = static_cast(w); push.viewport[1] = static_cast(h); - push.viewport[2] = static_cast(x0); - push.viewport[3] = static_cast(y0); + push.viewport[2] = static_cast(rect.x0); + push.viewport[3] = static_cast(rect.y0); vkCmdPushConstants(ctx.buffer, this->backgroundPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, @@ -224,101 +214,6 @@ SoVulkanRenderBackend::recordBackground(const SoRenderParams & params, vkCmdDraw(ctx.buffer, 3, 1, 0, 0); } -bool -SoVulkanRenderBackend::createRenderPass(const SoVulkanRenderTarget & target, - VkAttachmentLoadOp colorLoadOp, - VkAttachmentLoadOp depthLoadOp, - VkRenderPass & pass) -{ - VkAttachmentDescription attachments[2]; - uint32_t attachmentCount = 1; - - attachments[0].flags = 0; - attachments[0].format = target.colorFormat; - attachments[0].samples = target.sampleCount; - attachments[0].loadOp = colorLoadOp; - attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[0].initialLayout = target.colorLayout; - attachments[0].finalLayout = target.colorLayout; - - VkAttachmentReference colorRef {}; - colorRef.attachment = 0; - colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - - VkAttachmentReference depthRef {}; - const bool hasDepth = target.depthImageView != VK_NULL_HANDLE && - target.depthFormat != VK_FORMAT_UNDEFINED; - if (hasDepth) { - attachments[1].flags = 0; - attachments[1].format = target.depthFormat; - attachments[1].samples = target.sampleCount; - attachments[1].loadOp = depthLoadOp; - attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[1].initialLayout = target.depthLayout; - attachments[1].finalLayout = target.depthLayout; - depthRef.attachment = 1; - depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - attachmentCount = 2; - } - - VkSubpassDescription subpass {}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &colorRef; - subpass.pDepthStencilAttachment = hasDepth ? &depthRef : nullptr; - - VkRenderPassCreateInfo ci {}; - ci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; - ci.attachmentCount = attachmentCount; - ci.pAttachments = attachments; - ci.subpassCount = 1; - ci.pSubpasses = &subpass; - ci.dependencyCount = 0; - ci.pDependencies = nullptr; - - return vkCreateRenderPass(this->device, &ci, this->allocator, &pass) == - VK_SUCCESS; -} - -SoVulkanRenderBackend::RenderPassIdentity -SoVulkanRenderBackend::renderPassIdentity(const SoVulkanRenderTarget & target) const -{ - RenderPassIdentity identity; - identity.colorFormat = target.colorFormat; - identity.sampleCount = target.sampleCount; - identity.colorLayout = target.colorLayout; - // createRenderPass() only adds a depth attachment when a depth view is - // present, so a configured-but-viewless depth format must not be part of - // the identity. - identity.depthFormat = - (target.depthImageView != VK_NULL_HANDLE) ? target.depthFormat - : VK_FORMAT_UNDEFINED; - identity.depthLayout = target.depthLayout; - return identity; -} - -VkRenderPass -SoVulkanRenderBackend::getOrCreateRenderPass(const SoVulkanRenderTarget & target, - VkAttachmentLoadOp colorLoadOp, - VkAttachmentLoadOp depthLoadOp) -{ - RenderPassIdentity identity = this->renderPassIdentity(target); - identity.colorLoadOp = colorLoadOp; - identity.depthLoadOp = depthLoadOp; - const auto found = this->renderPassCache.find(identity); - if (found != this->renderPassCache.end()) return found->second; - - VkRenderPass pass = VK_NULL_HANDLE; - if (!this->createRenderPass(target, colorLoadOp, depthLoadOp, pass)) { - return VK_NULL_HANDLE; - } - this->renderPassCache.emplace(identity, pass); - return pass; -} bool SoVulkanRenderBackend::getOrCreatePipeline(const SoRenderCommand & command, @@ -641,7 +536,9 @@ SoVulkanRenderBackend::getOrCreatePipeline(const SoRenderCommand & command, depthStencil.depthTestEnable = (command.state.depth.enabled || overlay) ? VK_TRUE : VK_FALSE; depthStencil.depthWriteEnable = - (!transparent && !overlay && command.state.depth.writeEnabled) + (overlayPass ? command.state.depth.writeEnabled + : (!transparent && !overlay && + command.state.depth.writeEnabled)) ? VK_TRUE : VK_FALSE; depthStencil.depthCompareOp = overlay ? VK_COMPARE_OP_LESS_OR_EQUAL diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.cpp b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.cpp new file mode 100644 index 00000000000..59c2fe62f9b --- /dev/null +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.cpp @@ -0,0 +1,210 @@ +// src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.cpp +// +// Render-pass and framebuffer caching for the Vulkan raster backend. See +// SoVulkanRenderPassCache.h for the design contract. + +#include "rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.h" + +SoVulkanRenderPassCache::SoVulkanRenderPassCache() = default; + +SoVulkanRenderPassCache::~SoVulkanRenderPassCache() +{ + this->destroyAll(); +} + +void +SoVulkanRenderPassCache::setDevice(VkDevice device, + const VkAllocationCallbacks * allocator) +{ + this->device_ = device; + this->allocator_ = allocator; +} + +void +SoVulkanRenderPassCache::setDeferredDestroy( + std::function &&)> fn) +{ + this->deferDestroyFn_ = std::move(fn); +} + +SoVulkanRenderPassCache::RenderPassIdentity +SoVulkanRenderPassCache::identityFor(const SoVulkanRenderTarget & target) const +{ + RenderPassIdentity identity; + identity.colorFormat = target.colorFormat; + identity.sampleCount = target.sampleCount; + identity.colorLayout = target.colorLayout; + // createRenderPass() only adds a depth attachment when a depth view is + // present, so a configured-but-viewless depth format must not be part of + // the identity. + identity.depthFormat = + (target.depthImageView != VK_NULL_HANDLE) ? target.depthFormat + : VK_FORMAT_UNDEFINED; + identity.depthLayout = target.depthLayout; + return identity; +} + +bool +SoVulkanRenderPassCache::createRenderPass(const SoVulkanRenderTarget & target, + VkAttachmentLoadOp colorLoadOp, + VkAttachmentLoadOp depthLoadOp, + VkRenderPass & pass) +{ + VkAttachmentDescription attachments[2]; + uint32_t attachmentCount = 1; + + attachments[0].flags = 0; + attachments[0].format = target.colorFormat; + attachments[0].samples = target.sampleCount; + attachments[0].loadOp = colorLoadOp; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = target.colorLayout; + attachments[0].finalLayout = target.colorLayout; + + VkAttachmentReference colorRef {}; + colorRef.attachment = 0; + colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentReference depthRef {}; + const bool hasDepth = target.depthImageView != VK_NULL_HANDLE && + target.depthFormat != VK_FORMAT_UNDEFINED; + if (hasDepth) { + attachments[1].flags = 0; + attachments[1].format = target.depthFormat; + attachments[1].samples = target.sampleCount; + attachments[1].loadOp = depthLoadOp; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[1].initialLayout = target.depthLayout; + attachments[1].finalLayout = target.depthLayout; + depthRef.attachment = 1; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + attachmentCount = 2; + } + + VkSubpassDescription subpass {}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = hasDepth ? &depthRef : nullptr; + + VkRenderPassCreateInfo ci {}; + ci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + ci.attachmentCount = attachmentCount; + ci.pAttachments = attachments; + ci.subpassCount = 1; + ci.pSubpasses = &subpass; + ci.dependencyCount = 0; + ci.pDependencies = nullptr; + + return vkCreateRenderPass(this->device_, &ci, this->allocator_, &pass) == + VK_SUCCESS; +} + +VkRenderPass +SoVulkanRenderPassCache::getOrCreateRenderPass( + const SoVulkanRenderTarget & target, + VkAttachmentLoadOp colorLoadOp, + VkAttachmentLoadOp depthLoadOp) +{ + RenderPassIdentity identity = this->identityFor(target); + identity.colorLoadOp = colorLoadOp; + identity.depthLoadOp = depthLoadOp; + const auto found = this->passCache_.find(identity); + if (found != this->passCache_.end()) { + this->renderPass_ = found->second; + return found->second; + } + + VkRenderPass pass = VK_NULL_HANDLE; + if (!this->createRenderPass(target, colorLoadOp, depthLoadOp, pass)) { + this->renderPass_ = VK_NULL_HANDLE; + return VK_NULL_HANDLE; + } + this->passCache_.emplace(identity, pass); + this->renderPass_ = pass; + return pass; +} + +bool +SoVulkanRenderPassCache::ensureFramebuffer(const SoVulkanRenderTarget * target, + VkRenderPass renderPass) +{ + if (this->framebuffer_ != VK_NULL_HANDLE && + this->framebufferPass_ == renderPass && + this->framebufferColorImage_ == target->colorImage && + this->framebufferColorView_ == target->colorImageView && + this->framebufferDepthImage_ == target->depthImage && + this->framebufferDepthView_ == target->depthImageView && + this->framebufferExtent_.width == target->extent.width && + this->framebufferExtent_.height == target->extent.height) { + return true; + } + if (this->framebuffer_ != VK_NULL_HANDLE) { + const VkDevice device = this->device_; + const VkAllocationCallbacks * allocator = this->allocator_; + const VkFramebuffer oldFramebuffer = this->framebuffer_; + if (this->deferDestroyFn_) { + this->deferDestroyFn_([device, allocator, oldFramebuffer]() { + if (oldFramebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(device, oldFramebuffer, allocator); + } + }); + } + else { + vkDestroyFramebuffer(device, oldFramebuffer, allocator); + } + this->framebuffer_ = VK_NULL_HANDLE; + } + VkFramebufferCreateInfo fci {}; + fci.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fci.renderPass = renderPass; + fci.attachmentCount = + (target->depthImageView != VK_NULL_HANDLE && + target->depthFormat != VK_FORMAT_UNDEFINED) + ? 2u : 1u; + const VkImageView attachments[] = { + target->colorImageView, + target->depthImageView, + }; + fci.pAttachments = attachments; + fci.width = target->extent.width; + fci.height = target->extent.height; + fci.layers = 1; + if (vkCreateFramebuffer(this->device_, &fci, this->allocator_, + &this->framebuffer_) != VK_SUCCESS) { + return false; + } + this->framebufferPass_ = renderPass; + this->framebufferColorImage_ = target->colorImage; + this->framebufferColorView_ = target->colorImageView; + this->framebufferDepthImage_ = target->depthImage; + this->framebufferDepthView_ = target->depthImageView; + this->framebufferExtent_ = target->extent; + return true; +} + +void +SoVulkanRenderPassCache::destroyAll() +{ + if (this->framebuffer_ != VK_NULL_HANDLE) { + vkDestroyFramebuffer(this->device_, this->framebuffer_, this->allocator_); + this->framebuffer_ = VK_NULL_HANDLE; + } + for (auto & entry : this->passCache_) { + if (entry.second != VK_NULL_HANDLE) { + vkDestroyRenderPass(this->device_, entry.second, this->allocator_); + } + } + this->passCache_.clear(); + this->renderPass_ = VK_NULL_HANDLE; + this->framebufferPass_ = VK_NULL_HANDLE; + this->framebufferColorImage_ = VK_NULL_HANDLE; + this->framebufferColorView_ = VK_NULL_HANDLE; + this->framebufferDepthImage_ = VK_NULL_HANDLE; + this->framebufferDepthView_ = VK_NULL_HANDLE; + this->framebufferExtent_ = {0, 0}; +} diff --git a/src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.h b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.h new file mode 100644 index 00000000000..b30fccbc9be --- /dev/null +++ b/src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.h @@ -0,0 +1,187 @@ +// src/rendering/SoVulkanRenderBackend/SoVulkanRenderPassCache.h +// +// Render-pass and framebuffer cache for the Vulkan render backend. +// +// Vulkan render passes and framebuffers are expensive, immutable objects. A +// scene is drawn every frame into a swapchain whose images cycle, so this +// cache creates a VkRenderPass per unique attachment identity (color/depth +// format, sample count, image layouts, and the color/depth load ops) and a +// VkFramebuffer per unique target identity (image views + extent + render +// pass). Pipelines are keyed on the render-pass handle (see PipelineKey), so +// reusing the same pass across targets that differ only in their images keeps +// the pipeline cache warm as well. +// +// This is a private helper of the raster backend (not public API): the owning +// SoVulkanRenderBackend creates it, binds a device/allocator at +// initialize(), and queries the current pass/framebuffer while recording each +// frame. Deferred destruction (releasing an old framebuffer a few frames +// after the submissions that referenced it have completed) is supplied by the +// backend through setDeferredDestroy() so this class stays free of any +// backend/ring dependencies. + +#ifndef COIN_SOVULKANRENDERPASSCACHE_H +#define COIN_SOVULKANRENDERPASSCACHE_H + +#include +#include +#include + +#include + +#include + +class SoVulkanRenderPassCache { +public: + SoVulkanRenderPassCache(); + ~SoVulkanRenderPassCache(); + SoVulkanRenderPassCache(const SoVulkanRenderPassCache &) = delete; + SoVulkanRenderPassCache & operator=(const SoVulkanRenderPassCache &) = delete; + + //! Bind the device/allocator used to create and destroy render passes and + //! framebuffers. Called once from the owning backend's initialize(). + void setDevice(VkDevice device, const VkAllocationCallbacks * allocator); + + /*! + \brief Register the deferred-destruction mechanism. + + \a fn queues a GPU resource's destruction a few frames behind the producer + (the backend's frame ring), so a resource is never destroyed while a + submission that references it may still be in flight. When unset, replaced + framebuffers are freed synchronously instead of deferred. + */ + void setDeferredDestroy(std::function &&)> fn); + + /*! + \brief Recompute + cache the render pass for the target's attachment + identity and the requested load ops. + + Returns the cached VkRenderPass (also available from + currentRenderPass()), or VK_NULL_HANDLE when creation fails. + */ + VkRenderPass getOrCreateRenderPass(const SoVulkanRenderTarget & target, + VkAttachmentLoadOp colorLoadOp, + VkAttachmentLoadOp depthLoadOp); + + /*! + \brief Ensure the cached framebuffer matches the current target/render + pass. + + Recreates the framebuffer (deferring the old one) whenever the target's + image views, extent or render pass change, as swapchain targets cycle + their images every frame. The current framebuffer is available from + framebuffer(). + */ + bool ensureFramebuffer(const SoVulkanRenderTarget * target, + VkRenderPass renderPass); + + //! Destroy the cached render passes and framebuffer (shutdown). Idempotent. + void destroyAll(); + + // --- per-frame state queried by the owning backend ---------------------- + + //! Render pass cached for the current frame. + VkRenderPass currentRenderPass() const { return renderPass_; } + + //! Framebuffer cached for the current target identity. + VkFramebuffer framebuffer() const { return framebuffer_; } + + //! Whether the current render pass clears the color attachment via its loadOp. + bool colorClearedByLoad() const { return colorCleared_; } + + //! Whether the current render pass clears the depth attachment via its loadOp. + bool depthClearedByLoad() const { return depthCleared_; } + + //! Set the current pass's clear-by-load flags (per frame / external pass). + void setClearedByLoad(bool color, bool depth) + { + colorCleared_ = color; + depthCleared_ = depth; + } + +private: + // Immutable render-pass identity. Created once per unique combination and + // reused across targets that share the definition (so the pipeline cache + // keyed on the render-pass handle stays warm). + struct RenderPassIdentity { + VkFormat colorFormat = VK_FORMAT_B8G8R8A8_UNORM; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; + VkImageLayout colorLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + VkImageLayout depthLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // Load ops distinguish a render pass that clears its attachments at begin + // (full-target clear fast path) from one that loads them and clears via + // vkCmdClearAttachments. Two passes that differ only in loadOp must not + // share a cache entry. + VkAttachmentLoadOp colorLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + + bool operator==(const RenderPassIdentity & other) const + { + return colorFormat == other.colorFormat && + depthFormat == other.depthFormat && + sampleCount == other.sampleCount && + colorLayout == other.colorLayout && + depthLayout == other.depthLayout && + colorLoadOp == other.colorLoadOp && + depthLoadOp == other.depthLoadOp; + } + }; + + struct RenderPassIdentityHash { + size_t operator()(const RenderPassIdentity & key) const + { + size_t hash = std::hash()( + static_cast(key.colorFormat)); + hash = SoVulkanRenderPassCache::hashCombine( + hash, std::hash()(static_cast(key.depthFormat))); + hash = SoVulkanRenderPassCache::hashCombine( + hash, std::hash()(static_cast(key.sampleCount))); + hash = SoVulkanRenderPassCache::hashCombine( + hash, std::hash()(static_cast(key.colorLayout))); + hash = SoVulkanRenderPassCache::hashCombine( + hash, std::hash()(static_cast(key.depthLayout))); + hash = SoVulkanRenderPassCache::hashCombine( + hash, std::hash()(static_cast(key.colorLoadOp))); + hash = SoVulkanRenderPassCache::hashCombine( + hash, std::hash()(static_cast(key.depthLoadOp))); + return hash; + } + }; + //! Shared combine step for the hand-rolled hash functors above. + static size_t hashCombine(size_t hash, size_t value) + { + return hash ^ (value + 0x9e3779b9 + (hash << 6) + (hash >> 2)); + } + + //! Derive the immutable render-pass identity from a target's attachments. + RenderPassIdentity identityFor(const SoVulkanRenderTarget & target) const; + //! Native Vulkan render-pass creation (no caching). + bool createRenderPass(const SoVulkanRenderTarget & target, + VkAttachmentLoadOp colorLoadOp, + VkAttachmentLoadOp depthLoadOp, + VkRenderPass & pass); + + VkDevice device_ = VK_NULL_HANDLE; + const VkAllocationCallbacks * allocator_ = nullptr; + //! Deferred-release hook (backend frame ring); empty = synchronous free. + std::function &&)> deferDestroyFn_; + + std::unordered_map + passCache_; + //! Render pass used by the current frame (looked up from passCache_). + VkRenderPass renderPass_ = VK_NULL_HANDLE; + bool colorCleared_ = false; + bool depthCleared_ = false; + + //! Framebuffer cached for the current target identity (image views + + //! extent + render pass). + VkFramebuffer framebuffer_ = VK_NULL_HANDLE; + VkRenderPass framebufferPass_ = VK_NULL_HANDLE; + VkImage framebufferColorImage_ = VK_NULL_HANDLE; + VkImageView framebufferColorView_ = VK_NULL_HANDLE; + VkImage framebufferDepthImage_ = VK_NULL_HANDLE; + VkImageView framebufferDepthView_ = VK_NULL_HANDLE; + VkExtent2D framebufferExtent_ = {0, 0}; +}; + +#endif // COIN_SOVULKANRENDERPASSCACHE_H diff --git a/src/rendering/SoVulkanRenderManager.cpp b/src/rendering/SoVulkanRenderManager.cpp index 0eba1fbca90..a382f11c656 100644 --- a/src/rendering/SoVulkanRenderManager.cpp +++ b/src/rendering/SoVulkanRenderManager.cpp @@ -35,9 +35,11 @@ static void vulkanSceneGraphChangedCallback(void * data, SoSensor * sensor); #include #include #include +#include #include #include #include +#include #include #include @@ -309,6 +311,7 @@ class SoVulkanRenderManagerP { SbColor4f backgroundBottomColor = SbColor4f(0.0f, 0.0f, 0.0f, 1.0f); SbBool wireframeOverlay = FALSE; SbBool pointsOverlay = FALSE; + SbBool tessellationOverlay = FALSE; SbColor4f edgeColor = SbColor4f(0.05f, 0.05f, 0.05f, 1.0f); SbBool clearWindow = TRUE; SbBool clearDepth = TRUE; @@ -495,6 +498,11 @@ void SoVulkanRenderManager::setSceneGraph(SoNode * root) { SoNode *& stored = this->pimpl->scene; + // TEMP-VKINIT + if (stored != root) { + SoVulkanShared::initBreadcrumb("setSceneGraph scene %p (was %p)\n", + static_cast(root), static_cast(stored)); + } if (stored == root) { return; } @@ -540,6 +548,9 @@ SoVulkanRenderManager::setOverlaySceneGraph(SoNode * root) if (stored) { stored->ref(); } + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("setOverlaySceneGraph %s %p\n", root ? "overlay" : "NULL", + static_cast(root)); } SoNode * @@ -562,6 +573,9 @@ SoVulkanRenderManager::setDecorationSceneGraph(SoNode * root) if (stored) { stored->ref(); } + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("setDecorationSceneGraph %s %p\n", root ? "decor" : "NULL", + static_cast(root)); } SoNode * @@ -596,6 +610,9 @@ SoVulkanRenderManager::setCamera(SoCamera * camera) if (stored) { stored->ref(); } + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("setCamera %s %p\n", camera ? "cam" : "NULL", + static_cast(camera)); } SoCamera * @@ -694,6 +711,13 @@ SoVulkanRenderManager::setPointsOverlay(SbBool enabled) this->pimpl->backend.setPointsOverlay(enabled); } +void +SoVulkanRenderManager::setTessellationOverlay(SbBool enabled) +{ + this->pimpl->tessellationOverlay = enabled; + this->pimpl->backend.setTessellationOverlay(enabled); +} + void SoVulkanRenderManager::setEdgeColor(const SbColor4f & color) { @@ -713,6 +737,12 @@ SoVulkanRenderManager::getPointsOverlay(void) const return this->pimpl->pointsOverlay; } +SbBool +SoVulkanRenderManager::getTessellationOverlay(void) const +{ + return this->pimpl->tessellationOverlay; +} + const SbColor4f & SoVulkanRenderManager::getEdgeColor(void) const { @@ -766,15 +796,24 @@ SoVulkanRenderManager::initialize(SoVulkanDeviceContext * context) // device-lost fixed in SoRTXRenderBackend* (VUID-vkCmdDispatch-None-08114). // Do not chase it: the remedy (per-swapchain-image semaphores) is a Qt/ // QVulkanWindow change, not a FreeCAD one. + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("initialize enter ctx.device=%p backendInit=%d rtx=%d\n", + static_cast(context->device), + this->pimpl->backendInitialized ? 1 : 0, + this->pimpl->rayTracing ? 1 : 0); + // TEMP-VKINIT if (this->pimpl->backendInitialized && this->pimpl->initContext && this->pimpl->initContext->device == context->device) { this->pimpl->initContext = context; + SoVulkanShared::initBreadcrumb("initialize reuse (device unchanged)\n"); return TRUE; } + SoVulkanShared::initBreadcrumb("initialize calling backend.initialize\n"); SoRenderBackendInitParams params; params.userData = context; if (!this->pimpl->backend.initialize(params)) { + SoVulkanShared::initBreadcrumb("initialize FAILED backend.initialize\n"); SoDebugError::postWarning("SoVulkanRenderManager::initialize", "backend initialization failed"); return FALSE; @@ -783,6 +822,7 @@ SoVulkanRenderManager::initialize(SoVulkanDeviceContext * context) // Retain the borrowed context so ensureRayTracing() can bring the RT // backend up later if it was skipped at startup (path tracing off). this->pimpl->initContext = context; + SoVulkanShared::initBreadcrumb("initialize backend OK rtx=%d\n", this->pimpl->rtxBackendInitialized ? 1 : 0); // Ray tracing is best-effort and only attempted when it was requested // (setRayTracing(TRUE)). A device created without the KHR extensions @@ -791,21 +831,36 @@ SoVulkanRenderManager::initialize(SoVulkanDeviceContext * context) // re-initializes. When it IS requested but unavailable, fall back to // the raster backend with a warning. if (this->pimpl->rayTracing) { + SoVulkanShared::initBreadcrumb("initialize requesting RTX backend\n"); if (this->pimpl->rtxBackend.initialize(params)) { this->pimpl->rtxBackendInitialized = TRUE; + SoVulkanShared::initBreadcrumb("initialize RTX backend OK\n"); } else { + SoVulkanShared::initBreadcrumb("initialize RTX backend FAILED (fallback)\n"); SoDebugError::postWarning( "SoVulkanRenderManager::initialize", "ray-tracing backend unavailable; raster Vulkan backend will be used"); } } + SoVulkanShared::initBreadcrumb("initialize DONE backendInit=%d rtxInit=%d\n", + this->pimpl->backendInitialized ? 1 : 0, + this->pimpl->rtxBackendInitialized ? 1 : 0); return TRUE; } void SoVulkanRenderManager::shutdown(void) { + // TEMP-BT: locate who tears the backend down during initial open (delete me) + if (std::getenv("FC_VK_SHUT_DBG")) { + std::fprintf(stderr, "[SHUT-DBG] SoVulkanRenderManager::shutdown backend=%d rtx=%d\n", + this->pimpl->backendInitialized ? 1 : 0, + this->pimpl->rtxBackendInitialized ? 1 : 0); + void* bt[24]; int n = backtrace(bt, 24); + backtrace_symbols_fd(bt, n, 2); + std::fflush(stderr); + } if (this->pimpl->backendInitialized) { this->pimpl->backend.shutdown(); this->pimpl->backendInitialized = FALSE; @@ -1275,6 +1330,9 @@ SoVulkanRenderManagerP::resolveActiveCamera() // No camera in the scene graph: fall back to the retained pointer (used by // overlay-only or programmatic render setups that manage a camera outside // the scene). + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("resolveActiveCamera FALLBACK to retained camera %p\n", + static_cast(this->camera)); return this->camera; } @@ -1288,7 +1346,17 @@ void SoVulkanRenderManagerP::refreshActiveCamera() { SoCamera * resolved = this->resolveActiveCamera(); + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("refreshActiveCamera resolved=%p retained=%p camv=%d\n", + static_cast(resolved), static_cast(this->camera), + this->cameraVersion); if (resolved && resolved != this->camera) { + if (getenv("FC_VULKAN_RT_DEBUG")) { + fprintf(stderr, + "[RTDBG] cameraNode swap resolved=%p stored=%p camv=%d\n", + static_cast(resolved), static_cast(this->camera), + this->cameraVersion); + } SoCamera *& stored = this->camera; if (stored) { stored->unref(); @@ -1310,9 +1378,26 @@ SoVulkanRenderManagerP::refreshActiveCamera() (uint32_t)((int)(pos[2] * 256.0f)) ^ (uint32_t)((int)(fp[0] * 256.0f)); if (fp1 != this->cameraPoseFingerprint) { + if (getenv("FC_VULKAN_RT_DEBUG")) { + fprintf(stderr, + "[RTDBG] cameraPose fp=%08x -> %08x pos=(%.6f,%.6f,%.6f) " + "fwd=(%.6f,%.6f,%.6f)\n", + this->cameraPoseFingerprint, fp1, pos[0], pos[1], pos[2], + fp[0], fp[1], fp[2]); + } this->cameraPoseFingerprint = fp1; this->cameraVersion++; } + else if (getenv("FC_VULKAN_RT_DEBUG")) { + static uint32_t samp = 0; + if (samp < 150) { + ++samp; + fprintf(stderr, + "[RTDBG] cameraPose SAME fp=%08x pos=(%.6f,%.6f,%.6f) " + "fwd=(%.6f,%.6f,%.6f)\n", + fp1, pos[0], pos[1], pos[2], fp[0], fp[1], fp[2]); + } + } } } @@ -1543,12 +1628,21 @@ SoVulkanRenderManagerP::prepareRenderParams(SbBool clearwindow, SoRenderParams & params) { if (!this->backendInitialized || !this->renderTarget) { + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("prepareRenderParams BAIL: backend=%d rt=%d\n", + this->backendInitialized ? 1 : 0, + this->renderTarget ? 1 : 0); SoDebugError::postWarning("SoVulkanRenderManager::prepareRenderParams", "backend %s, render target %s", this->backendInitialized ? "initialized" : "NOT initialized", this->renderTarget ? "set" : "NOT set"); return FALSE; } + // TEMP-VKINIT + SoVulkanShared::initBreadcrumb("prepareRenderParams frame scene=%p camera=%p overlay=%p\n", + static_cast(this->scene), + static_cast(this->camera), + static_cast(this->overlayScene)); const long prepBcStart = vkRenderBreadcrumbEnabled() ? vkRenderBreadcrumbNowUs() : 0; const bool wantCpuTiming = frameTimingEnabled(); @@ -1656,19 +1750,20 @@ SoVulkanRenderManagerP::prepareRenderParams(SbBool clearwindow, // below. They are re-recorded separately afterwards (cheap) and merged onto // this main list. SbBool irReplayed = FALSE; - // The graph fingerprint walk is O(N) over the scene nodes, so on a retained - // (replayed) frame with no scene change at all it is pure waste. The walk - // folds scene node-ids but deliberately skips camera, light, environment and - // tag/infra nodes, so the fingerprint is invariant under camera motion; the - // only thing that changes it is a change to a render-affecting node. An - // SoNodeSensor attached to the scene root fires whenever any descendant is - // notified (a field write or a child-list edit, including the camera pose -- - // FreeCAD keeps the camera inside the scene graph). When the sensor has NOT - // fired since the last walk the cached fingerprint is still exact and the - // walk/re-traversal can be skipped via the branch below. When it HAS fired - // we must recompute the fingerprint (see the else) to distinguish camera-only - // churn (identical fingerprint -> replay) from a real content change - // (different fingerprint -> re-traverse). + // Cheap fast-path: the graph fingerprint walk is O(N) over the whole scene, + // so on a retained (replayed) frame with no scene change at all it is pure + // waste. The walk folds scene node-ids but deliberately SKIPS camera, light, + // environment and tag/infra nodes, so the fingerprint is invariant under + // camera motion; the only thing that changes it is a change to a + // render-affecting node. An SoNodeSensor attached to the scene root fires + // whenever any descendant is notified (a field write or a child-list edit, + // including the camera pose -- FreeCAD keeps the camera inside the scene + // graph). When the sensor has NOT fired since the last walk the cached + // fingerprint is still exact and both the walk and the re-traversal can be + // skipped via the branch below. When it HAS fired we must recompute the + // fingerprint (see the else) to distinguish camera-only churn (identity + // fingerprint -> replay) from a real content change (different fingerprint + // -> re-traverse). uint64_t graphFp; const SbVec2s fpVpSize = this->viewportRegion.getViewportSizePixels(); if (this->graphFingerprintValid && this->lastFpValid && @@ -1705,6 +1800,21 @@ SoVulkanRenderManagerP::prepareRenderParams(SbBool clearwindow, this->lastFpValid = TRUE; if (this->scene || this->camera || this->overlayScene || this->decorationScene) { + if (breadcrumbsEnabled()) { + static long frames = 0; + if (++frames == 1 || frames % 30 == 0) { + std::fprintf(stderr, + "[VKINIT] replayDecision frame=%ld dirty=%d fpValid=%d " + "fpMatch=%d irReplay%d cmds=%u fp=0x%llx cachedFp=0x%llx\n", + frames, sceneGraphDirty ? 1 : 0, + graphFingerprintValid ? 1 : 0, + (graphFingerprintValid && graphFp == this->graphFingerprint) ? 1 : 0, + irReplayEnabled() ? 1 : 0, + this->mainCommandCount, (unsigned long long)graphFp, + (unsigned long long)this->graphFingerprint); + std::fflush(stderr); + } + } if (irReplayEnabled() && this->graphFingerprintValid && graphFp == this->graphFingerprint) { // Camera-only frame: the main graph, the viewport, and the diff --git a/src/rendering/SoVulkanShared.h b/src/rendering/SoVulkanShared.h index 1025059edd3..ba74cda0f8f 100644 --- a/src/rendering/SoVulkanShared.h +++ b/src/rendering/SoVulkanShared.h @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include @@ -31,6 +33,27 @@ envFlagEnabled(const char * name) std::strcmp(value, "off") != 0; } +// Permanent, shared initialization breadcrumb for the Vulkan renderer +// bring-up path. Every step from SoRenderBackend::initialize() down through +// the concrete SoVulkanRenderBackend / SoRTXRenderBackend / SoVulkanRenderManager +// and the Qt QuarterVulkanWidget orchestration emits one line, so a single +// interleaved [VKINIT] log records the exact order and success/failure of the +// whole initialization. Gated by the same FC_VULKAN_BREADCRUMBS flag the +// SoVulkanRenderManager [VKINIT]/frame breadcrumbs use, so one env var enables +// the entire trace. The point of permanence is that a backend bring-up bug +// (a step skipped, an init/shutdown ordering issue, a half-failed initialize) +// becomes directly visible in the log without re-adding instrumentation. +inline void initBreadcrumb(const char * fmt, ...) +{ + if (!envFlagEnabled("FC_VULKAN_BREADCRUMBS")) return; + std::fprintf(stderr, "[VKINIT] "); + va_list args; + va_start(args, fmt); + std::vfprintf(stderr, fmt, args); + va_end(args); + std::fflush(stderr); +} + // Literal-name fast path: the per-call-site static resolves the flag once, so // per-frame hot paths pay no getenv() at all. Shared by both backends so the // env-flag policy lives in one place. @@ -65,12 +88,14 @@ class MemoryProperties { return m_props; } - // Pick the first memory type matching `desired`, falling back to any type - // the device offers for this resource. Returns false only when no type is - // usable (or no device is bound). - bool pick(const VkMemoryRequirements & requirements, - VkMemoryPropertyFlags desired, - uint32_t & memoryTypeIndex) const + // Pick the first memory type that exactly satisfies `desired`, with no + // fallback to a merely-compatible type. On failure leaves memoryTypeIndex + // untouched and returns false. This is the raster backend's policy: a + // buffer/image that cannot be placed in the requested property class is a + // hard error rather than a silent host-visible degradation. + bool pickExact(const VkMemoryRequirements & requirements, + VkMemoryPropertyFlags desired, + uint32_t & memoryTypeIndex) const { this->ensure(); if (!m_valid) return false; @@ -81,6 +106,21 @@ class MemoryProperties { return true; } } + return false; + } + + // Pick the first memory type matching `desired`, falling back to any type + // the device offers for this resource. Returns false only when no type is + // usable (or no device is bound). This is the RT backend's policy: the + // best-effort fallback keeps a renderer allocation on memory it can use + // rather than failing outright. + bool pick(const VkMemoryRequirements & requirements, + VkMemoryPropertyFlags desired, + uint32_t & memoryTypeIndex) const + { + if (this->pickExact(requirements, desired, memoryTypeIndex)) return true; + this->ensure(); + if (!m_valid) return false; for (uint32_t i = 0; i < m_props.memoryTypeCount; ++i) { if (requirements.memoryTypeBits & (1u << i)) { memoryTypeIndex = i; diff --git a/src/shapenodes/SoShape.cpp b/src/shapenodes/SoShape.cpp index 86847969265..9d71539f2d1 100644 --- a/src/shapenodes/SoShape.cpp +++ b/src/shapenodes/SoShape.cpp @@ -445,18 +445,18 @@ soshape_emit_ir_commands(SoIRRenderAction * action, SoShape * shape, command.projMatrix = SoProjectionMatrixElement::get(state); if (clipDebug) { static int mmLog = 0; - if (mmLog++ < 6) { + if (mmLog++ < 40) { SbBool isId = FALSE; const SbMatrix & el = SoModelMatrixElement::get(state, isId); SbMatrix mm = command.modelMatrix; - fprintf(stderr, "[SHAPE] cmd rec pass=%d verts=%u model00=%.3f m11=%.3f " - "m22=%.3f trans=(%.3f,%.3f,%.3f) isIdentity=%d " - "el00=%.3f eltrans=(%.3f,%.3f,%.3f)\n", + fprintf(stderr, "[SHAPE] cmd rec type=%s pass=%d verts=%u topo=%d " + "modelScale=(%.3f,%.3f,%.3f) trans=(%.3f,%.3f,%.3f)\n", + shape->getTypeId().getName().getString(), static_cast(command.pass), static_cast(command.geometry.vertexCount), + static_cast(geom.topology), mm[0][0], mm[1][1], mm[2][2], - mm[3][0], mm[3][1], mm[3][2], - isId ? 1 : 0, el[0][0], el[3][0], el[3][1], el[3][2]); + mm[3][0], mm[3][1], mm[3][2]); } } SoRenderIR::fillMaterialFromState( @@ -859,7 +859,7 @@ SoShape::IRRender(SoIRRenderAction * action) if (!action) return; if (getenv("FC_IR_BREADCRUMB")) { static int n = 0; - if (n++ < 10) fprintf(stderr, "[BC-IR] IRRender shape=%p type=%s\n", + if (n++ < 400) fprintf(stderr, "[BC-IR] IRRender shape=%p type=%s\n", (void *)this, this->getTypeId().getName().getString()); } @@ -877,6 +877,17 @@ SoShape::IRRender(SoIRRenderAction * action) SbVec3f center; this->getBBox(action, box, center); if (box.isEmpty()) return; + if (std::getenv("FC_IR_BREADCRUMB")) { + SbBool isId = FALSE; + const SbMatrix & mm = SoModelMatrixElement::get(state, isId); + std::fprintf(stderr, "[IR-BBOX] shape=%s bboxMin=(%.2f,%.2f,%.2f) " + "bboxMax=(%.2f,%.2f,%.2f) modelScale=(%.3f,%.3f,%.3f)\n", + this->getTypeId().getName().getString(), + box.getMin()[0], box.getMin()[1], box.getMin()[2], + box.getMax()[0], box.getMax()[1], box.getMax()[2], + mm[0][0], mm[1][1], mm[2][2]); + std::fflush(stderr); + } const SbVec3f min = box.getMin(); const SbVec3f max = box.getMax();